mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-21 04:49:10 +02:00
Surface VNC initiator in status, clarify proxy logs, dampen capture noise
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,8 +12,12 @@ type vncServer interface{}
|
|||||||
|
|
||||||
func (e *Engine) updateVNC() error { return nil }
|
func (e *Engine) updateVNC() error { return nil }
|
||||||
|
|
||||||
func (e *Engine) updateVNCServerAuth(_ *mgmProto.VNCAuth) {
|
func (e *Engine) updateVNCServerAuth(auth *mgmProto.VNCAuth) {
|
||||||
// no-op on platforms without a VNC server
|
if auth == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("ignoring VNC auth push on platform without a VNC server: %d session pubkeys, %d authorized users",
|
||||||
|
len(auth.GetSessionPubKeys()), len(auth.GetAuthorizedUsers()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) stopVNCServer() error { return nil }
|
func (e *Engine) stopVNCServer() error { return nil }
|
||||||
|
|||||||
@@ -2128,7 +2128,10 @@ type VNCSessionInfo struct {
|
|||||||
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
|
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
|
||||||
// userID is the Noise-verified session identity (hashed user ID from
|
// userID is the Noise-verified session identity (hashed user ID from
|
||||||
// the ACL session-key entry), empty when auth is disabled.
|
// the ACL session-key entry), empty when auth is disabled.
|
||||||
UserID string `protobuf:"bytes,4,opt,name=userID,proto3" json:"userID,omitempty"`
|
UserID string `protobuf:"bytes,4,opt,name=userID,proto3" json:"userID,omitempty"`
|
||||||
|
// initiator is the human-readable display name of the dashboard user
|
||||||
|
// who minted the SessionPubKey, when known.
|
||||||
|
Initiator string `protobuf:"bytes,5,opt,name=initiator,proto3" json:"initiator,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -2191,6 +2194,13 @@ func (x *VNCSessionInfo) GetUserID() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *VNCSessionInfo) GetInitiator() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Initiator
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// VNCServerState contains the latest state of the VNC server
|
// VNCServerState contains the latest state of the VNC server
|
||||||
type VNCServerState struct {
|
type VNCServerState struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@@ -6717,12 +6727,13 @@ const file_daemon_proto_rawDesc = "" +
|
|||||||
"\fportForwards\x18\x05 \x03(\tR\fportForwards\"^\n" +
|
"\fportForwards\x18\x05 \x03(\tR\fportForwards\"^\n" +
|
||||||
"\x0eSSHServerState\x12\x18\n" +
|
"\x0eSSHServerState\x12\x18\n" +
|
||||||
"\aenabled\x18\x01 \x01(\bR\aenabled\x122\n" +
|
"\aenabled\x18\x01 \x01(\bR\aenabled\x122\n" +
|
||||||
"\bsessions\x18\x02 \x03(\v2\x16.daemon.SSHSessionInfoR\bsessions\"~\n" +
|
"\bsessions\x18\x02 \x03(\v2\x16.daemon.SSHSessionInfoR\bsessions\"\x9c\x01\n" +
|
||||||
"\x0eVNCSessionInfo\x12$\n" +
|
"\x0eVNCSessionInfo\x12$\n" +
|
||||||
"\rremoteAddress\x18\x01 \x01(\tR\rremoteAddress\x12\x12\n" +
|
"\rremoteAddress\x18\x01 \x01(\tR\rremoteAddress\x12\x12\n" +
|
||||||
"\x04mode\x18\x02 \x01(\tR\x04mode\x12\x1a\n" +
|
"\x04mode\x18\x02 \x01(\tR\x04mode\x12\x1a\n" +
|
||||||
"\busername\x18\x03 \x01(\tR\busername\x12\x16\n" +
|
"\busername\x18\x03 \x01(\tR\busername\x12\x16\n" +
|
||||||
"\x06userID\x18\x04 \x01(\tR\x06userID\"^\n" +
|
"\x06userID\x18\x04 \x01(\tR\x06userID\x12\x1c\n" +
|
||||||
|
"\tinitiator\x18\x05 \x01(\tR\tinitiator\"^\n" +
|
||||||
"\x0eVNCServerState\x12\x18\n" +
|
"\x0eVNCServerState\x12\x18\n" +
|
||||||
"\aenabled\x18\x01 \x01(\bR\aenabled\x122\n" +
|
"\aenabled\x18\x01 \x01(\bR\aenabled\x122\n" +
|
||||||
"\bsessions\x18\x02 \x03(\v2\x16.daemon.VNCSessionInfoR\bsessions\"\xef\x04\n" +
|
"\bsessions\x18\x02 \x03(\v2\x16.daemon.VNCSessionInfoR\bsessions\"\xef\x04\n" +
|
||||||
|
|||||||
@@ -418,6 +418,9 @@ message VNCSessionInfo {
|
|||||||
// userID is the Noise-verified session identity (hashed user ID from
|
// userID is the Noise-verified session identity (hashed user ID from
|
||||||
// the ACL session-key entry), empty when auth is disabled.
|
// the ACL session-key entry), empty when auth is disabled.
|
||||||
string userID = 4;
|
string userID = 4;
|
||||||
|
// initiator is the human-readable display name of the dashboard user
|
||||||
|
// who minted the SessionPubKey, when known.
|
||||||
|
string initiator = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VNCServerState contains the latest state of the VNC server
|
// VNCServerState contains the latest state of the VNC server
|
||||||
|
|||||||
@@ -1205,6 +1205,7 @@ func (s *Server) getVNCServerState() *proto.VNCServerState {
|
|||||||
Mode: sess.Mode,
|
Mode: sess.Mode,
|
||||||
Username: sess.Username,
|
Username: sess.Username,
|
||||||
UserID: sess.UserID,
|
UserID: sess.UserID,
|
||||||
|
Initiator: sess.Initiator,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return &proto.VNCServerState{
|
return &proto.VNCServerState{
|
||||||
|
|||||||
+24
-9
@@ -136,6 +136,7 @@ type VNCSessionOutput struct {
|
|||||||
Mode string `json:"mode" yaml:"mode"`
|
Mode string `json:"mode" yaml:"mode"`
|
||||||
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
||||||
UserID string `json:"userID,omitempty" yaml:"userID,omitempty"`
|
UserID string `json:"userID,omitempty" yaml:"userID,omitempty"`
|
||||||
|
Initiator string `json:"initiator,omitempty" yaml:"initiator,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VNCServerStateOutput struct {
|
type VNCServerStateOutput struct {
|
||||||
@@ -297,6 +298,7 @@ func mapVNCServer(state *proto.VNCServerState) VNCServerStateOutput {
|
|||||||
Mode: sess.GetMode(),
|
Mode: sess.GetMode(),
|
||||||
Username: sess.GetUsername(),
|
Username: sess.GetUsername(),
|
||||||
UserID: sess.GetUserID(),
|
UserID: sess.GetUserID(),
|
||||||
|
Initiator: sess.GetInitiator(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return VNCServerStateOutput{
|
return VNCServerStateOutput{
|
||||||
@@ -582,15 +584,7 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS
|
|||||||
|
|
||||||
if showSSHSessions && vncSessionCount > 0 {
|
if showSSHSessions && vncSessionCount > 0 {
|
||||||
for _, sess := range o.VNCServerState.Sessions {
|
for _, sess := range o.VNCServerState.Sessions {
|
||||||
var line string
|
vncServerStatus += "\n " + formatVNCSessionLine(sess)
|
||||||
if sess.UserID != "" {
|
|
||||||
line = fmt.Sprintf("[%s@%s -> %s] mode=%s",
|
|
||||||
sess.UserID, sess.RemoteAddress, sess.Username, sess.Mode)
|
|
||||||
} else {
|
|
||||||
line = fmt.Sprintf("[%s] mode=%s user=%s",
|
|
||||||
sess.RemoteAddress, sess.Mode, sess.Username)
|
|
||||||
}
|
|
||||||
vncServerStatus += "\n " + line
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1004,6 +998,26 @@ func anonymizePeerDetail(a *anonymize.Anonymizer, peer *PeerStateDetailOutput) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatVNCSessionLine renders a single VNC session row for the detailed
|
||||||
|
// status output. The leading slot identifies the initiator (display name
|
||||||
|
// when known, hashed UserID otherwise); the post-arrow slot is the OS
|
||||||
|
// user the session targets and is omitted in attach mode where the
|
||||||
|
// destination is the current console user (unknown to the daemon).
|
||||||
|
func formatVNCSessionLine(sess VNCSessionOutput) string {
|
||||||
|
who := sess.Initiator
|
||||||
|
if who == "" {
|
||||||
|
who = sess.UserID
|
||||||
|
}
|
||||||
|
prefix := sess.RemoteAddress
|
||||||
|
if who != "" {
|
||||||
|
prefix = fmt.Sprintf("%s@%s", who, sess.RemoteAddress)
|
||||||
|
}
|
||||||
|
if sess.Username != "" {
|
||||||
|
return fmt.Sprintf("[%s -> %s] mode=%s", prefix, sess.Username, sess.Mode)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s] mode=%s", prefix, sess.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
func anonymizeOverview(a *anonymize.Anonymizer, overview *OutputOverview) {
|
func anonymizeOverview(a *anonymize.Anonymizer, overview *OutputOverview) {
|
||||||
for i, peer := range overview.Peers.Details {
|
for i, peer := range overview.Peers.Details {
|
||||||
peer := peer
|
peer := peer
|
||||||
@@ -1077,5 +1091,6 @@ func anonymizeServerSessions(a *anonymize.Anonymizer, overview *OutputOverview)
|
|||||||
overview.VNCServerState.Sessions[i].RemoteAddress = anonymizeRemoteAddress(a, sess.RemoteAddress)
|
overview.VNCServerState.Sessions[i].RemoteAddress = anonymizeRemoteAddress(a, sess.RemoteAddress)
|
||||||
overview.VNCServerState.Sessions[i].Username = a.AnonymizeString(sess.Username)
|
overview.VNCServerState.Sessions[i].Username = a.AnonymizeString(sess.Username)
|
||||||
overview.VNCServerState.Sessions[i].UserID = a.AnonymizeString(sess.UserID)
|
overview.VNCServerState.Sessions[i].UserID = a.AnonymizeString(sess.UserID)
|
||||||
|
overview.VNCServerState.Sessions[i].Initiator = a.AnonymizeString(sess.Initiator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (s *Server) handleServiceConnection(conn net.Conn, sa sessionAgent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authedLog, _, ok := s.authorizeSession(conn, header, connLog)
|
authedLog, sessionUserID, ok := s.authorizeSession(conn, header, connLog)
|
||||||
if !ok {
|
if !ok {
|
||||||
authedLog.Info("VNC connection rejected: auth failed")
|
authedLog.Info("VNC connection rejected: auth failed")
|
||||||
return
|
return
|
||||||
@@ -101,6 +101,19 @@ func (s *Server) handleServiceConnection(conn net.Conn, sa sessionAgent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var initiator string
|
||||||
|
if s.authorizer != nil {
|
||||||
|
initiator = s.authorizer.LookupSessionDisplayName(header.clientStatic)
|
||||||
|
}
|
||||||
|
sessionID := s.addSession(ActiveSessionInfo{
|
||||||
|
RemoteAddress: conn.RemoteAddr().String(),
|
||||||
|
Mode: modeString(header.mode),
|
||||||
|
Username: header.username,
|
||||||
|
UserID: sessionUserID,
|
||||||
|
Initiator: initiator,
|
||||||
|
}, conn)
|
||||||
|
defer s.removeSession(sessionID)
|
||||||
|
|
||||||
replayConn := &prefixConn{
|
replayConn := &prefixConn{
|
||||||
Reader: io.MultiReader(&headerBuf, conn),
|
Reader: io.MultiReader(&headerBuf, conn),
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
@@ -198,8 +211,8 @@ func proxyToAgent(ctx context.Context, client net.Conn, socketPath, authToken st
|
|||||||
log.Debugf("proxy %s: %d bytes, err=%v", label, n, err)
|
log.Debugf("proxy %s: %d bytes, err=%v", label, n, err)
|
||||||
done <- struct{}{}
|
done <- struct{}{}
|
||||||
}
|
}
|
||||||
go cp("client→agent", agentConn, client)
|
go cp("client->agent", agentConn, client)
|
||||||
go cp("agent→client", client, agentConn)
|
go cp("agent->client", client, agentConn)
|
||||||
<-done
|
<-done
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,6 +445,14 @@ type captureWorker struct {
|
|||||||
lastDesktop string
|
lastDesktop string
|
||||||
nextInitRetry time.Time
|
nextInitRetry time.Time
|
||||||
cursor cursorSampler
|
cursor cursorSampler
|
||||||
|
// lastBackend records the last capturer kind that came out of
|
||||||
|
// createCapturer ("dxgi" or "gdi"); used to demote repeat "using X"
|
||||||
|
// and DXGI-unavailable logs to debug when nothing changed.
|
||||||
|
lastBackend string
|
||||||
|
// lastDXGIErr is the textual DXGI failure printed in the most recent
|
||||||
|
// fallback warning; suppresses repeat warns when DXGI keeps failing
|
||||||
|
// the same way across desktop changes (login -> lock -> login).
|
||||||
|
lastDXGIErr string
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleNextRequest waits for either shutdown or a capture request and runs
|
// handleNextRequest waits for either shutdown or a capture request and runs
|
||||||
@@ -503,9 +511,14 @@ func (w *captureWorker) prepCapturer() (frameCapturer, error) {
|
|||||||
w.cap = fc
|
w.cap = fc
|
||||||
sw, sh := screenSize()
|
sw, sh := screenSize()
|
||||||
w.c.mu.Lock()
|
w.c.mu.Lock()
|
||||||
|
sizeChanged := w.c.w != sw || w.c.h != sh
|
||||||
w.c.w, w.c.h = sw, sh
|
w.c.w, w.c.h = sw, sh
|
||||||
w.c.mu.Unlock()
|
w.c.mu.Unlock()
|
||||||
log.Infof("screen capturer ready: %dx%d", sw, sh)
|
if sizeChanged {
|
||||||
|
log.Infof("screen capturer ready: %dx%d", sw, sh)
|
||||||
|
} else {
|
||||||
|
log.Debugf("screen capturer ready: %dx%d", sw, sh)
|
||||||
|
}
|
||||||
return w.cap, nil
|
return w.cap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,15 +549,32 @@ func (w *captureWorker) refreshDesktop() error {
|
|||||||
func (w *captureWorker) createCapturer() (frameCapturer, error) {
|
func (w *captureWorker) createCapturer() (frameCapturer, error) {
|
||||||
dc, err := newDXGICapturer()
|
dc, err := newDXGICapturer()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Info("using DXGI Desktop Duplication for capture")
|
if w.lastBackend != "dxgi" {
|
||||||
|
log.Info("using DXGI Desktop Duplication for capture")
|
||||||
|
} else {
|
||||||
|
log.Debug("using DXGI Desktop Duplication for capture")
|
||||||
|
}
|
||||||
|
w.lastBackend = "dxgi"
|
||||||
|
w.lastDXGIErr = ""
|
||||||
return dc, nil
|
return dc, nil
|
||||||
}
|
}
|
||||||
log.Warnf("DXGI Desktop Duplication unavailable, falling back to slower GDI BitBlt: %v", err)
|
errStr := err.Error()
|
||||||
|
if errStr != w.lastDXGIErr {
|
||||||
|
log.Warnf("DXGI Desktop Duplication unavailable, falling back to slower GDI BitBlt: %v", err)
|
||||||
|
w.lastDXGIErr = errStr
|
||||||
|
} else {
|
||||||
|
log.Debugf("DXGI Desktop Duplication still unavailable, falling back to slower GDI BitBlt: %v", err)
|
||||||
|
}
|
||||||
gc, err := newGDICapturer()
|
gc, err := newGDICapturer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Info("using GDI BitBlt for capture")
|
if w.lastBackend != "gdi" {
|
||||||
|
log.Info("using GDI BitBlt for capture")
|
||||||
|
} else {
|
||||||
|
log.Debug("using GDI BitBlt for capture")
|
||||||
|
}
|
||||||
|
w.lastBackend = "gdi"
|
||||||
return gc, nil
|
return gc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,10 @@ type ActiveSessionInfo struct {
|
|||||||
// UserID is the authenticated session identity (hashed user ID from
|
// UserID is the authenticated session identity (hashed user ID from
|
||||||
// the Noise_IK static-key registration), empty when auth is disabled.
|
// the Noise_IK static-key registration), empty when auth is disabled.
|
||||||
UserID string
|
UserID string
|
||||||
|
// Initiator is the dashboard-supplied display name of the user who
|
||||||
|
// minted the SessionPubKey, when known. Empty when auth is disabled
|
||||||
|
// or the authorizer has no display-name mapping.
|
||||||
|
Initiator string
|
||||||
}
|
}
|
||||||
|
|
||||||
// vncSession provides capturer and injector for a virtual display session.
|
// vncSession provides capturer and injector for a virtual display session.
|
||||||
@@ -852,11 +856,16 @@ func (s *Server) handleConnection(conn net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var initiator string
|
||||||
|
if s.authorizer != nil {
|
||||||
|
initiator = s.authorizer.LookupSessionDisplayName(header.clientStatic)
|
||||||
|
}
|
||||||
sessionID := s.addSession(ActiveSessionInfo{
|
sessionID := s.addSession(ActiveSessionInfo{
|
||||||
RemoteAddress: conn.RemoteAddr().String(),
|
RemoteAddress: conn.RemoteAddr().String(),
|
||||||
Mode: modeString(header.mode),
|
Mode: modeString(header.mode),
|
||||||
Username: header.username,
|
Username: header.username,
|
||||||
UserID: sessionUserID,
|
UserID: sessionUserID,
|
||||||
|
Initiator: initiator,
|
||||||
}, conn)
|
}, conn)
|
||||||
defer s.removeSession(sessionID)
|
defer s.removeSession(sessionID)
|
||||||
|
|
||||||
|
|||||||
@@ -480,10 +480,10 @@ func (p *VNCProxy) runNoiseHandshake(conn net.Conn, dest vncDestination) error {
|
|||||||
defer conn.SetReadDeadline(time.Time{}) //nolint:errcheck
|
defer conn.SetReadDeadline(time.Time{}) //nolint:errcheck
|
||||||
msg2 := make([]byte, noiseResponderMsgLen)
|
msg2 := make([]byte, noiseResponderMsgLen)
|
||||||
if _, err := io.ReadFull(conn, msg2); err != nil {
|
if _, err := io.ReadFull(conn, msg2); err != nil {
|
||||||
return fmt.Errorf("read noise msg2: %w", err)
|
return fmt.Errorf("read noise msg2 from server: %w", err)
|
||||||
}
|
}
|
||||||
if _, _, _, err := state.ReadMessage(nil, msg2); err != nil {
|
if _, _, _, err := state.ReadMessage(nil, msg2); err != nil {
|
||||||
return fmt.Errorf("noise read msg2: %w", err)
|
return fmt.Errorf("decrypt noise msg2 (peer pubkey mismatch or session revoked): %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user