diff --git a/client/cmd/up.go b/client/cmd/up.go index 8ffc7c2f7..e461e9488 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -480,29 +480,7 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil ic.DisableVNCApproval = &disableVNCApproval } - if cmd.Flag(enableSSHRootFlag).Changed { - ic.EnableSSHRoot = &enableSSHRoot - } - - if cmd.Flag(enableSSHSFTPFlag).Changed { - ic.EnableSSHSFTP = &enableSSHSFTP - } - - if cmd.Flag(enableSSHLocalPortForwardFlag).Changed { - ic.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward - } - - if cmd.Flag(enableSSHRemotePortForwardFlag).Changed { - ic.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward - } - - if cmd.Flag(disableSSHAuthFlag).Changed { - ic.DisableSSHAuth = &disableSSHAuth - } - - if cmd.Flag(sshJWTCacheTTLFlag).Changed { - ic.SSHJWTCacheTTL = &sshJWTCacheTTL - } + applySSHFlagsToConfig(cmd, &ic) if cmd.Flag(interfaceNameFlag).Changed { if err := parseInterfaceName(interfaceName); err != nil { @@ -578,6 +556,49 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil return &ic, nil } +func applySSHFlagsToConfig(cmd *cobra.Command, ic *profilemanager.ConfigInput) { + if cmd.Flag(enableSSHRootFlag).Changed { + ic.EnableSSHRoot = &enableSSHRoot + } + if cmd.Flag(enableSSHSFTPFlag).Changed { + ic.EnableSSHSFTP = &enableSSHSFTP + } + if cmd.Flag(enableSSHLocalPortForwardFlag).Changed { + ic.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward + } + if cmd.Flag(enableSSHRemotePortForwardFlag).Changed { + ic.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward + } + if cmd.Flag(disableSSHAuthFlag).Changed { + ic.DisableSSHAuth = &disableSSHAuth + } + if cmd.Flag(sshJWTCacheTTLFlag).Changed { + ic.SSHJWTCacheTTL = &sshJWTCacheTTL + } +} + +func applySSHFlagsToLogin(cmd *cobra.Command, req *proto.LoginRequest) { + if cmd.Flag(enableSSHRootFlag).Changed { + req.EnableSSHRoot = &enableSSHRoot + } + if cmd.Flag(enableSSHSFTPFlag).Changed { + req.EnableSSHSFTP = &enableSSHSFTP + } + if cmd.Flag(enableSSHLocalPortForwardFlag).Changed { + req.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward + } + if cmd.Flag(enableSSHRemotePortForwardFlag).Changed { + req.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward + } + if cmd.Flag(disableSSHAuthFlag).Changed { + req.DisableSSHAuth = &disableSSHAuth + } + if cmd.Flag(sshJWTCacheTTLFlag).Changed { + ttl := int32(sshJWTCacheTTL) + req.SshJWTCacheTTL = &ttl + } +} + func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte, cmd *cobra.Command) (*proto.LoginRequest, error) { loginRequest := proto.LoginRequest{ SetupKey: providedSetupKey, @@ -614,30 +635,7 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte loginRequest.DisableVNCApproval = &disableVNCApproval } - if cmd.Flag(enableSSHRootFlag).Changed { - loginRequest.EnableSSHRoot = &enableSSHRoot - } - - if cmd.Flag(enableSSHSFTPFlag).Changed { - loginRequest.EnableSSHSFTP = &enableSSHSFTP - } - - if cmd.Flag(enableSSHLocalPortForwardFlag).Changed { - loginRequest.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward - } - - if cmd.Flag(enableSSHRemotePortForwardFlag).Changed { - loginRequest.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward - } - - if cmd.Flag(disableSSHAuthFlag).Changed { - loginRequest.DisableSSHAuth = &disableSSHAuth - } - - if cmd.Flag(sshJWTCacheTTLFlag).Changed { - sshJWTCacheTTL32 := int32(sshJWTCacheTTL) - loginRequest.SshJWTCacheTTL = &sshJWTCacheTTL32 - } + applySSHFlagsToLogin(cmd, &loginRequest) if cmd.Flag(disableAutoConnectFlag).Changed { loginRequest.DisableAutoConnect = &autoConnectDisabled diff --git a/client/ui/approval.go b/client/ui/approval.go index 9e5beaf5c..70c8963d5 100644 --- a/client/ui/approval.go +++ b/client/ui/approval.go @@ -171,7 +171,7 @@ func (r approvalRequest) displayPeer() string { } // deadline returns the wall-clock auto-deny moment. Falls back to a short -// local window when the daemon's expires_at is missing/unparseable, so a +// local window when the daemon's expires_at is missing/unparsable, so a // stale value never leaves the dialog open indefinitely. func (r approvalRequest) deadline() time.Time { if t, err := time.Parse(time.RFC3339, r.expiresAt); err == nil { diff --git a/client/vnc/server/handshake.go b/client/vnc/server/handshake.go new file mode 100644 index 000000000..50231cff8 --- /dev/null +++ b/client/vnc/server/handshake.go @@ -0,0 +1,250 @@ +//go:build !js && !ios && !android + +package server + +import ( + "bufio" + "bytes" + "crypto/subtle" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "io" + "net" + "time" + + "github.com/flynn/noise" + log "github.com/sirupsen/logrus" +) + +var vncIdentityMagic = []byte("NBV3") + +// Noise_IK_25519_ChaChaPoly_SHA256 message sizes (with empty payloads). +// +// msg1 = e(32) + s_AEAD(32+16) + payload_AEAD(0+16) = 96 bytes +// msg2 = e(32) + payload_AEAD(0+16) = 48 bytes +const ( + noiseInitiatorMsgLen = 96 + noiseResponderMsgLen = 48 +) + +// vncNoiseSuite pins the cipher suite for the VNC handshake. Changing +// it requires bumping vncIdentityMagic so old clients fail closed. +var vncNoiseSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256) + +func (s *Server) authenticateSession(header *connectionHeader) (string, error) { + if !header.identityVerified { + return "", fmt.Errorf("identity proof missing") + } + if len(header.clientStatic) != 32 { + return "", fmt.Errorf("client static key missing") + } + + userIDHash, err := s.authorizer.LookupSessionKey(header.clientStatic) + if err != nil { + return "", fmt.Errorf("lookup session pubkey: %w", err) + } + + osUser := "*" + if header.mode == ModeSession { + osUser = header.username + } + if _, err := s.authorizer.AuthorizeOSUserBySessionKey(userIDHash, osUser); err != nil { + return "", fmt.Errorf("authorize OS user %q: %w", osUser, err) + } + return userIDHash.String(), nil +} + +// readConnectionHeader reads the NetBird VNC session header. Format: +// +// [mode: 1] [username_len: 2 BE] [username: N] +// [opt magic "NBV3": 4] [noise_msg1: 96] +// (server writes [noise_msg2: 48] here when the magic is present) +// [session_id: 4 BE] [width: 2 BE] [height: 2 BE] +// +// Standard VNC clients don't speak first, so they time out on the first +// read and fall through to attach mode (which auth still rejects when +// no Noise handshake completed). +func (s *Server) readConnectionHeader(conn net.Conn) (*connectionHeader, error) { + if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { + return nil, fmt.Errorf("set deadline: %w", err) + } + defer conn.SetReadDeadline(time.Time{}) //nolint:errcheck + + var hdr [3]byte + if _, err := io.ReadFull(conn, hdr[:]); err != nil { + return &connectionHeader{mode: ModeAttach}, nil + } + + if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + return nil, fmt.Errorf("set deadline: %w", err) + } + + mode := hdr[0] + usernameLen := binary.BigEndian.Uint16(hdr[1:3]) + + var username string + if usernameLen > 0 { + if usernameLen > 256 { + return nil, fmt.Errorf("username too long: %d", usernameLen) + } + buf := make([]byte, usernameLen) + if _, err := io.ReadFull(conn, buf); err != nil { + return nil, fmt.Errorf("read username: %w", err) + } + username = string(buf) + } + + br := bufio.NewReader(conn) + clientStatic, identityVerified, err := s.maybeRunNoiseHandshake(conn, br) + if err != nil { + return nil, err + } + + var sessionID uint32 + var sidBuf [4]byte + if _, err := io.ReadFull(br, sidBuf[:]); err == nil { + sessionID = binary.BigEndian.Uint32(sidBuf[:]) + } + + var width, height uint16 + var geomBuf [4]byte + if _, err := io.ReadFull(br, geomBuf[:]); err == nil { + width = binary.BigEndian.Uint16(geomBuf[0:2]) + height = binary.BigEndian.Uint16(geomBuf[2:4]) + } + + return &connectionHeader{ + mode: mode, + username: username, + clientStatic: clientStatic, + sessionID: sessionID, + width: width, + height: height, + identityVerified: identityVerified, + }, nil +} + +// maybeRunNoiseHandshake performs the responder side of a Noise_IK +// handshake when the client sends the v3 magic. Returns the client static +// public key learned from the handshake. Any handshake failure is fatal +// (fail closed). +func (s *Server) maybeRunNoiseHandshake(conn net.Conn, br *bufio.Reader) ([]byte, bool, error) { + peek, _ := br.Peek(len(vncIdentityMagic)) + if !bytes.Equal(peek, vncIdentityMagic) { + return nil, false, nil + } + if _, err := br.Discard(len(vncIdentityMagic)); err != nil { + return nil, false, fmt.Errorf("discard identity magic: %w", err) + } + + msg1 := make([]byte, noiseInitiatorMsgLen) + if _, err := io.ReadFull(br, msg1); err != nil { + return nil, false, fmt.Errorf("read noise msg1: %w", err) + } + + // Agents on loopback authenticate via the agent token, not this + // handshake. Consume the replayed bytes and skip the response. + if s.disableAuth { + return nil, true, nil + } + + if len(s.identityKey) != 32 || len(s.identityPublic) != 32 { + return nil, false, errors.New("identity key not configured") + } + state, err := noise.NewHandshakeState(noise.Config{ + CipherSuite: vncNoiseSuite, + Pattern: noise.HandshakeIK, + Initiator: false, + StaticKeypair: noise.DHKey{Private: s.identityKey, Public: s.identityPublic}, + }) + if err != nil { + return nil, false, fmt.Errorf("noise responder init: %w", err) + } + if _, _, _, err := state.ReadMessage(nil, msg1); err != nil { + return nil, false, fmt.Errorf("noise read msg1: %w", err) + } + msg2, _, _, err := state.WriteMessage(nil, nil) + if err != nil { + return nil, false, fmt.Errorf("noise write msg2: %w", err) + } + if len(msg2) != noiseResponderMsgLen { + return nil, false, fmt.Errorf("noise responder produced %d bytes, expected %d", len(msg2), noiseResponderMsgLen) + } + if _, err := conn.Write(msg2); err != nil { + return nil, false, fmt.Errorf("write noise msg2: %w", err) + } + + clientStatic := state.PeerStatic() + if len(clientStatic) != 32 { + return nil, false, errors.New("noise peer static missing") + } + return clientStatic, true, nil +} + +// verifyAgentToken validates the agent token prefix when configured and +// reads the trailing view-only flag byte the daemon writes alongside it. +// Returns (ok, viewOnly). ok=false closes the connection. +func (s *Server) verifyAgentToken(conn net.Conn, connLog *log.Entry) (bool, bool) { + if len(s.agentToken) == 0 { + return true, false + } + buf := make([]byte, len(s.agentToken)+1) + if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + connLog.Debugf("set agent token deadline: %v", err) + conn.Close() + return false, false + } + if _, err := io.ReadFull(conn, buf); err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + // Connect-then-close probes (port liveness checks) hit this + // path on every dial; logging them would just flood the + // daemon log without surfacing a real failure. + connLog.Tracef("agent auth: read preamble: %v", err) + } else { + connLog.Warnf("agent auth: read preamble: %v", err) + } + conn.Close() + return false, false + } + if err := conn.SetReadDeadline(time.Time{}); err != nil { + connLog.Debugf("clear agent token deadline: %v", err) + } + if subtle.ConstantTimeCompare(buf[:len(s.agentToken)], s.agentToken) != 1 { + connLog.Warn("agent auth: invalid token, rejecting") + conn.Close() + return false, false + } + return true, buf[len(s.agentToken)] != 0 +} + +// authorizeSession runs the Noise_IK handshake when auth is enabled. +// Returns the enriched log entry, user identity hash (empty when auth +// disabled), and ok=false if the connection was rejected. +func (s *Server) authorizeSession(conn net.Conn, header *connectionHeader, connLog *log.Entry) (*log.Entry, string, bool) { + if s.disableAuth { + return connLog, "", true + } + userID, err := s.authenticateSession(header) + if err != nil { + rejectConnection(conn, codeMessage(RejectCodeAuthForbidden, err.Error())) + connLog.Warnf("auth rejected: %v", err) + return connLog, "", false + } + return connLog.WithFields(log.Fields{ + "session_user": userID, + "session_key": sessionKeyFingerprint(header.clientStatic), + }), userID, true +} + +// sessionKeyFingerprint returns a short hex fingerprint of a client +// static key for log correlation. Distinct VNC sessions of the same +// user end up with distinct fingerprints because each session mints a +// fresh keypair, so this lets an operator tell parallel sessions apart. +func sessionKeyFingerprint(clientStatic []byte) string { + if len(clientStatic) < 4 { + return "" + } + return hex.EncodeToString(clientStatic[:4]) +} diff --git a/client/vnc/server/server.go b/client/vnc/server/server.go index d350be70f..f7c74ffdd 100644 --- a/client/vnc/server/server.go +++ b/client/vnc/server/server.go @@ -3,10 +3,7 @@ package server import ( - "bufio" - "bytes" "context" - "crypto/subtle" "encoding/binary" "encoding/hex" "errors" @@ -18,7 +15,6 @@ import ( "sync" "time" - "github.com/flynn/noise" log "github.com/sirupsen/logrus" "golang.org/x/crypto/curve25519" "golang.zx2c4.com/wireguard/tun/netstack" @@ -572,27 +568,12 @@ func (s *Server) Start(ctx context.Context, addr netip.AddrPort, network netip.P s.listener = s.preListener listenDesc = s.preListener.Addr().String() default: - if !network.IsValid() { - return fmt.Errorf("invalid overlay network prefix") - } - s.localAddr = addr.Addr() - s.network = network - if s.netstackNet != nil { - ln, err := s.netstackNet.ListenTCPAddrPort(addr) - if err != nil { - return fmt.Errorf("listen on netstack %s: %w", addr, err) - } - s.listener = ln - listenDesc = fmt.Sprintf("netstack %s", addr) - } else { - tcpAddr := net.TCPAddrFromAddrPort(addr) - ln, err := net.ListenTCP("tcp", tcpAddr) - if err != nil { - return fmt.Errorf("listen on %s: %w", addr, err) - } - s.listener = ln - listenDesc = addr.String() + ln, desc, err := s.openOverlayListener(addr, network) + if err != nil { + return err } + s.listener = ln + listenDesc = desc } if s.serviceMode { @@ -609,6 +590,27 @@ func (s *Server) Start(ctx context.Context, addr netip.AddrPort, network netip.P return nil } +func (s *Server) openOverlayListener(addr netip.AddrPort, network netip.Prefix) (net.Listener, string, error) { + if !network.IsValid() { + return nil, "", fmt.Errorf("invalid overlay network prefix") + } + s.localAddr = addr.Addr() + s.network = network + if s.netstackNet != nil { + ln, err := s.netstackNet.ListenTCPAddrPort(addr) + if err != nil { + return nil, "", fmt.Errorf("listen on netstack %s: %w", addr, err) + } + return ln, fmt.Sprintf("netstack %s", addr), nil + } + tcpAddr := net.TCPAddrFromAddrPort(addr) + ln, err := net.ListenTCP("tcp", tcpAddr) + if err != nil { + return nil, "", fmt.Errorf("listen on %s: %w", addr, err) + } + return ln, addr.String(), nil +} + // Stop shuts down the server and closes all connections. func (s *Server) Stop() error { s.mu.Lock() @@ -869,240 +871,6 @@ func rejectConnection(conn net.Conn, reason string) { _, _ = conn.Write(buf) } -// authenticateSession resolves the Noise-verified client static public -// key to a hashed user identity via the authorizer, and checks OS-user -// mapping for session mode. Returns the hashed user identity on success. -func (s *Server) authenticateSession(header *connectionHeader) (string, error) { - if !header.identityVerified { - return "", fmt.Errorf("identity proof missing") - } - if len(header.clientStatic) != 32 { - return "", fmt.Errorf("client static key missing") - } - - userIDHash, err := s.authorizer.LookupSessionKey(header.clientStatic) - if err != nil { - return "", fmt.Errorf("lookup session pubkey: %w", err) - } - - osUser := "*" - if header.mode == ModeSession { - osUser = header.username - } - if _, err := s.authorizer.AuthorizeOSUserBySessionKey(userIDHash, osUser); err != nil { - return "", fmt.Errorf("authorize OS user %q: %w", osUser, err) - } - return userIDHash.String(), nil -} - -var vncIdentityMagic = []byte("NBV3") - -// Noise_IK_25519_ChaChaPoly_SHA256 message sizes (with empty payloads). -// -// msg1 = e(32) + s_AEAD(32+16) + payload_AEAD(0+16) = 96 bytes -// msg2 = e(32) + payload_AEAD(0+16) = 48 bytes -const ( - noiseInitiatorMsgLen = 96 - noiseResponderMsgLen = 48 -) - -// vncNoiseSuite pins the cipher suite for the VNC handshake. Changing -// it requires bumping vncIdentityMagic so old clients fail closed. -var vncNoiseSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256) - -// readConnectionHeader reads the NetBird VNC session header. Format: -// -// [mode: 1] [username_len: 2 BE] [username: N] -// [opt magic "NBV3": 4] [noise_msg1: 96] -// (server writes [noise_msg2: 48] here when the magic is present) -// [session_id: 4 BE] [width: 2 BE] [height: 2 BE] -// -// Standard VNC clients don't speak first, so they time out on the first -// read and fall through to attach mode (which auth still rejects when -// no Noise handshake completed). -func (s *Server) readConnectionHeader(conn net.Conn) (*connectionHeader, error) { - if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { - return nil, fmt.Errorf("set deadline: %w", err) - } - defer conn.SetReadDeadline(time.Time{}) //nolint:errcheck - - var hdr [3]byte - if _, err := io.ReadFull(conn, hdr[:]); err != nil { - return &connectionHeader{mode: ModeAttach}, nil - } - - if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { - return nil, fmt.Errorf("set deadline: %w", err) - } - - mode := hdr[0] - usernameLen := binary.BigEndian.Uint16(hdr[1:3]) - - var username string - if usernameLen > 0 { - if usernameLen > 256 { - return nil, fmt.Errorf("username too long: %d", usernameLen) - } - buf := make([]byte, usernameLen) - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("read username: %w", err) - } - username = string(buf) - } - - br := bufio.NewReader(conn) - clientStatic, identityVerified, err := s.maybeRunNoiseHandshake(conn, br) - if err != nil { - return nil, err - } - - var sessionID uint32 - var sidBuf [4]byte - if _, err := io.ReadFull(br, sidBuf[:]); err == nil { - sessionID = binary.BigEndian.Uint32(sidBuf[:]) - } - - var width, height uint16 - var geomBuf [4]byte - if _, err := io.ReadFull(br, geomBuf[:]); err == nil { - width = binary.BigEndian.Uint16(geomBuf[0:2]) - height = binary.BigEndian.Uint16(geomBuf[2:4]) - } - - return &connectionHeader{ - mode: mode, - username: username, - clientStatic: clientStatic, - sessionID: sessionID, - width: width, - height: height, - identityVerified: identityVerified, - }, nil -} - -// maybeRunNoiseHandshake performs the responder side of a Noise_IK -// handshake when the client sends the v3 magic. Returns the client static -// public key learned from the handshake. Any handshake failure is fatal -// (fail closed). -func (s *Server) maybeRunNoiseHandshake(conn net.Conn, br *bufio.Reader) ([]byte, bool, error) { - peek, _ := br.Peek(len(vncIdentityMagic)) - if !bytes.Equal(peek, vncIdentityMagic) { - return nil, false, nil - } - if _, err := br.Discard(len(vncIdentityMagic)); err != nil { - return nil, false, fmt.Errorf("discard identity magic: %w", err) - } - - msg1 := make([]byte, noiseInitiatorMsgLen) - if _, err := io.ReadFull(br, msg1); err != nil { - return nil, false, fmt.Errorf("read noise msg1: %w", err) - } - - // Agents on loopback authenticate via the agent token, not this - // handshake. Consume the replayed bytes and skip the response. - if s.disableAuth { - return nil, true, nil - } - - if len(s.identityKey) != 32 || len(s.identityPublic) != 32 { - return nil, false, errors.New("identity key not configured") - } - state, err := noise.NewHandshakeState(noise.Config{ - CipherSuite: vncNoiseSuite, - Pattern: noise.HandshakeIK, - Initiator: false, - StaticKeypair: noise.DHKey{Private: s.identityKey, Public: s.identityPublic}, - }) - if err != nil { - return nil, false, fmt.Errorf("noise responder init: %w", err) - } - if _, _, _, err := state.ReadMessage(nil, msg1); err != nil { - return nil, false, fmt.Errorf("noise read msg1: %w", err) - } - msg2, _, _, err := state.WriteMessage(nil, nil) - if err != nil { - return nil, false, fmt.Errorf("noise write msg2: %w", err) - } - if len(msg2) != noiseResponderMsgLen { - return nil, false, fmt.Errorf("noise responder produced %d bytes, expected %d", len(msg2), noiseResponderMsgLen) - } - if _, err := conn.Write(msg2); err != nil { - return nil, false, fmt.Errorf("write noise msg2: %w", err) - } - - clientStatic := state.PeerStatic() - if len(clientStatic) != 32 { - return nil, false, errors.New("noise peer static missing") - } - return clientStatic, true, nil -} - -// verifyAgentToken validates the agent token prefix when configured and -// reads the trailing view-only flag byte the daemon writes alongside it. -// Returns (ok, viewOnly). ok=false closes the connection. -func (s *Server) verifyAgentToken(conn net.Conn, connLog *log.Entry) (bool, bool) { - if len(s.agentToken) == 0 { - return true, false - } - buf := make([]byte, len(s.agentToken)+1) - if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { - connLog.Debugf("set agent token deadline: %v", err) - conn.Close() - return false, false - } - if _, err := io.ReadFull(conn, buf); err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { - // Connect-then-close probes (port liveness checks) hit this - // path on every dial; logging them would just flood the - // daemon log without surfacing a real failure. - connLog.Tracef("agent auth: read preamble: %v", err) - } else { - connLog.Warnf("agent auth: read preamble: %v", err) - } - conn.Close() - return false, false - } - if err := conn.SetReadDeadline(time.Time{}); err != nil { - connLog.Debugf("clear agent token deadline: %v", err) - } - if subtle.ConstantTimeCompare(buf[:len(s.agentToken)], s.agentToken) != 1 { - connLog.Warn("agent auth: invalid token, rejecting") - conn.Close() - return false, false - } - return true, buf[len(s.agentToken)] != 0 -} - -// authorizeSession runs the Noise_IK handshake when auth is enabled. -// Returns the enriched log entry, user identity hash (empty when auth -// disabled), and ok=false if the connection was rejected. -func (s *Server) authorizeSession(conn net.Conn, header *connectionHeader, connLog *log.Entry) (*log.Entry, string, bool) { - if s.disableAuth { - return connLog, "", true - } - userID, err := s.authenticateSession(header) - if err != nil { - rejectConnection(conn, codeMessage(RejectCodeAuthForbidden, err.Error())) - connLog.Warnf("auth rejected: %v", err) - return connLog, "", false - } - return connLog.WithFields(log.Fields{ - "session_user": userID, - "session_key": sessionKeyFingerprint(header.clientStatic), - }), userID, true -} - -// sessionKeyFingerprint returns a short hex fingerprint of a client -// static key for log correlation. Distinct VNC sessions of the same -// user end up with distinct fingerprints because each session mints a -// fresh keypair, so this lets an operator tell parallel sessions apart. -func sessionKeyFingerprint(clientStatic []byte) string { - if len(clientStatic) < 4 { - return "" - } - return hex.EncodeToString(clientStatic[:4]) -} - // acquireSessionResources returns the capturer/injector to use for this // connection and a cleanup func to call when the session ends. ok is false // when the connection was rejected (and the caller must just return). diff --git a/client/vnc/server/session_encode.go b/client/vnc/server/session_encode.go index 38fc058ba..bff37e6e7 100644 --- a/client/vnc/server/session_encode.go +++ b/client/vnc/server/session_encode.go @@ -449,19 +449,11 @@ func (s *session) sendFullUpdate(img *image.RGBA) error { case useTight && tight != nil && pfIsTightCompatible(pf): rectBuf = encodeTightRect(img, pf, 0, 0, w, h, tight) case useZlib && zlib != nil: - // encodeZlibRect bakes in its own FBU header; reuse it for the - // single-rect path when there is no cursor to prepend. Fall back - // to Raw if the compressor errors out. - if zb, ok := encodeZlibRect(img, pf, 0, 0, w, h, zlib); ok { - if cursorRect == nil { - return s.writeFramed(zb) - } - rectBuf = zb[4:] - } else if cursorRect == nil { - return s.writeFramed(encodeRawRect(img, pf, 0, 0, w, h)) - } else { - rectBuf = encodeRawRect(img, pf, 0, 0, w, h)[4:] + body, done, err := s.encodeZlibSingle(img, pf, w, h, zlib, cursorRect) + if done { + return err } + rectBuf = body default: if cursorRect == nil { return s.writeFramed(encodeRawRect(img, pf, 0, 0, w, h)) @@ -478,11 +470,37 @@ func (s *session) sendFullUpdate(img *image.RGBA) error { return s.writeFramed(buf) } +// encodeZlibSingle encodes one full-frame rect with Zlib. When cursorRect is +// nil it writes the encodeZlibRect-baked FBU header directly and returns +// done=true with the writeFramed error. Otherwise it returns the rect body +// (header-stripped) so the caller can prepend a cursor rect. On compressor +// failure it falls back to Raw. +func (s *session) encodeZlibSingle(img *image.RGBA, pf clientPixelFormat, w, h int, zlib *zlibState, cursorRect []byte) (body []byte, done bool, err error) { + if zb, ok := encodeZlibRect(img, pf, 0, 0, w, h, zlib); ok { + if cursorRect == nil { + if werr := s.writeFramed(zb); werr != nil { + return nil, true, werr + } + return nil, true, nil + } + return zb[4:], false, nil + } + if cursorRect == nil { + if werr := s.writeFramed(encodeRawRect(img, pf, 0, 0, w, h)); werr != nil { + return nil, true, werr + } + return nil, true, nil + } + return encodeRawRect(img, pf, 0, 0, w, h)[4:], false, nil +} + func (s *session) writeFramed(buf []byte) error { s.writeMu.Lock() - _, err := s.conn.Write(buf) - s.writeMu.Unlock() - return err + defer s.writeMu.Unlock() + if _, err := s.conn.Write(buf); err != nil { + return err + } + return nil } // sendDirtyAndMoves writes one FramebufferUpdate combining CopyRect moves