Address review findings on the VNC server, session auth and capture decoder

This commit is contained in:
Viktor Liu
2026-08-27 18:33:52 +02:00
parent 09e069a175
commit 47d8f912e5
18 changed files with 179 additions and 72 deletions
+3 -3
View File
@@ -448,7 +448,7 @@ func (o *OutputOverview) YAML() (string, error) {
} }
// GeneralSummary returns a general summary of the status overview. // GeneralSummary returns a general summary of the status overview.
func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameServers bool, showSSHSessions bool) string { func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameServers bool, showSessions bool) string {
var managementConnString string var managementConnString string
if o.ManagementState.Connected { if o.ManagementState.Connected {
managementConnString = "Connected" managementConnString = "Connected"
@@ -573,7 +573,7 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS
sshServerStatus = "Enabled" sshServerStatus = "Enabled"
} }
if showSSHSessions && sessionCount > 0 { if showSessions && sessionCount > 0 {
for _, session := range o.SSHServerState.Sessions { for _, session := range o.SSHServerState.Sessions {
var sessionDisplay string var sessionDisplay string
if session.JWTUsername != "" { if session.JWTUsername != "" {
@@ -611,7 +611,7 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS
vncServerStatus = "Enabled" vncServerStatus = "Enabled"
} }
if showSSHSessions && vncSessionCount > 0 { if showSessions && vncSessionCount > 0 {
for _, sess := range o.VNCServerState.Sessions { for _, sess := range o.VNCServerState.Sessions {
vncServerStatus += "\n " + formatVNCSessionLine(sess) vncServerStatus += "\n " + formatVNCSessionLine(sess)
} }
+5 -6
View File
@@ -800,17 +800,16 @@ func relogAgentOutput(pipe windows.Handle) {
// each call site, while still capturing diagnostic info when the OS reports // each call site, while still capturing diagnostic info when the OS reports
// a failure. // a failure.
func logCleanupCall(name string, proc *windows.LazyProc) { func logCleanupCall(name string, proc *windows.LazyProc) {
r, _, err := proc.Call() logCleanupCallArgs(name, proc)
if r == 0 && err != nil && err != windows.NTE_OP_OK {
log.Tracef("%s: %v", name, err)
}
} }
// logCleanupCallArgs is logCleanupCall with one argument; common pattern for // logCleanupCallArgs is logCleanupCall with arguments; common pattern for
// release-by-handle syscalls. // release-by-handle syscalls.
func logCleanupCallArgs(name string, proc *windows.LazyProc, args ...uintptr) { func logCleanupCallArgs(name string, proc *windows.LazyProc, args ...uintptr) {
r, _, err := proc.Call(args...) r, _, err := proc.Call(args...)
if r == 0 && err != nil && err != windows.NTE_OP_OK { // LazyProc.Call always returns a non-nil error carrying the thread's last
// error code, so a zero code is what "the call did not fail" looks like.
if r == 0 && !errors.Is(err, windows.ERROR_SUCCESS) {
log.Tracef("%s: %v", name, err) log.Tracef("%s: %v", name, err)
} }
} }
+24 -10
View File
@@ -31,25 +31,27 @@ func (c *X11Capturer) initSHM() error {
return fmt.Errorf("shmat: %w", err) return fmt.Errorf("shmat: %w", err)
} }
if _, err := unix.SysvShmCtl(id, unix.IPC_RMID, nil); err != nil {
log.Debugf("shmctl IPC_RMID: %v", err)
}
seg, err := shm.NewSegId(c.conn) seg, err := shm.NewSegId(c.conn)
if err != nil { if err != nil {
if detachErr := unix.SysvShmDetach(addr); detachErr != nil { releaseShmSegment(id, addr)
log.Debugf("shmdt on new-seg failure: %v", detachErr)
}
return fmt.Errorf("new SHM seg: %w", err) return fmt.Errorf("new SHM seg: %w", err)
} }
// The X server attaches before the segment is marked for deletion: since
// Linux 3.10 a shmat() against an IPC_RMID'd segment fails with EIDRM, so
// marking it first would push us onto the slow non-SHM path.
if err := shm.AttachChecked(c.conn, seg, uint32(id), false).Check(); err != nil { if err := shm.AttachChecked(c.conn, seg, uint32(id), false).Check(); err != nil {
if detachErr := unix.SysvShmDetach(addr); detachErr != nil { releaseShmSegment(id, addr)
log.Debugf("shmdt on attach-checked failure: %v", detachErr)
}
return fmt.Errorf("SHM attach to X: %w", err) return fmt.Errorf("SHM attach to X: %w", err)
} }
// Both ends hold the segment at this point, so marking it for deletion
// frees it as soon as the last of them detaches, even if this process
// dies without cleaning up.
if _, err := unix.SysvShmCtl(id, unix.IPC_RMID, nil); err != nil {
log.Debugf("shmctl IPC_RMID: %v", err)
}
c.shmID = id c.shmID = id
c.shmAddr = addr c.shmAddr = addr
c.shmSeg = uint32(seg) c.shmSeg = uint32(seg)
@@ -57,6 +59,18 @@ func (c *X11Capturer) initSHM() error {
return nil return nil
} }
// releaseShmSegment gives a segment back on a setup path that failed after
// attaching it: detach this process, then mark it for deletion so it does not
// linger in the kernel's IPC table for the life of the host.
func releaseShmSegment(id int, addr []byte) {
if err := unix.SysvShmDetach(addr); err != nil {
log.Debugf("shmdt on setup failure: %v", err)
}
if _, err := unix.SysvShmCtl(id, unix.IPC_RMID, nil); err != nil {
log.Debugf("shmctl IPC_RMID on setup failure: %v", err)
}
}
func (c *X11Capturer) captureSHM() (*image.RGBA, error) { func (c *X11Capturer) captureSHM() (*image.RGBA, error) {
if err := c.fillSHM(); err != nil { if err := c.fillSHM(); err != nil {
return nil, err return nil, err
+19 -27
View File
@@ -120,28 +120,15 @@ func (s *cursorSampler) sample() (*cursorSnapshot, error) {
// cursor and stay armed for the next handle change rather than // cursor and stay armed for the next handle change rather than
// treating this as a hard failure that would latch us off for // treating this as a hard failure that would latch us off for
// the session. // the session.
if s.lastHandle == hiddenHandle { if s.lastHandle == hiddenHandle && s.snapshot != nil {
s.snapshot.posX = int(ci.PtPos.X) return s.publish(*s.snapshot, ci), nil
s.snapshot.posY = int(ci.PtPos.Y)
s.snapshot.hasPos = true
return s.snapshot, nil
} }
s.lastHandle = hiddenHandle s.lastHandle = hiddenHandle
s.serial++ s.serial++
s.snapshot = &cursorSnapshot{ return s.publish(cursorSnapshot{img: transparentCursorImage(), serial: s.serial}, ci), nil
img: transparentCursorImage(),
posX: int(ci.PtPos.X),
posY: int(ci.PtPos.Y),
hasPos: true,
serial: s.serial,
}
return s.snapshot, nil
} }
if ci.Cursor == s.lastHandle && s.snapshot != nil { if ci.Cursor == s.lastHandle && s.snapshot != nil {
s.snapshot.posX = int(ci.PtPos.X) return s.publish(*s.snapshot, ci), nil
s.snapshot.posY = int(ci.PtPos.Y)
s.snapshot.hasPos = true
return s.snapshot, nil
} }
img, hotX, hotY, err := decodeCursor(ci.Cursor) img, hotX, hotY, err := decodeCursor(ci.Cursor)
if err != nil { if err != nil {
@@ -149,16 +136,21 @@ func (s *cursorSampler) sample() (*cursorSnapshot, error) {
} }
s.lastHandle = ci.Cursor s.lastHandle = ci.Cursor
s.serial++ s.serial++
s.snapshot = &cursorSnapshot{ return s.publish(cursorSnapshot{img: img, hotX: hotX, hotY: hotY, serial: s.serial}, ci), nil
img: img, }
hotX: hotX,
hotY: hotY, // publish stamps the cursor's current position onto snap and stores it as the
posX: int(ci.PtPos.X), // sampler's latest snapshot. A fresh value every time, never an update in
posY: int(ci.PtPos.Y), // place: the session encoder reads the snapshot the sampler last handed out,
hasPos: true, // and must not see a position that is halfway between two samples. The sprite
serial: s.serial, // fields are carried over by value, so a snapshot the encoder still holds keeps
} // pointing at the same immutable image.
return s.snapshot, nil func (s *cursorSampler) publish(snap cursorSnapshot, ci winCursorInfo) *cursorSnapshot {
snap.posX = int(ci.PtPos.X)
snap.posY = int(ci.PtPos.Y)
snap.hasPos = true
s.snapshot = &snap
return s.snapshot
} }
// decodeCursor extracts the sprite at hCur as RGBA along with the hotspot. // decodeCursor extracts the sprite at hCur as RGBA along with the hotspot.
+36 -1
View File
@@ -3,6 +3,8 @@
package server package server
import ( import (
"bytes"
"compress/zlib"
"encoding/binary" "encoding/binary"
"strings" "strings"
"testing" "testing"
@@ -93,10 +95,43 @@ func TestExtClipProvideRoundTripLarge(t *testing.T) {
assert.Equal(t, original, text) assert.Equal(t, original, text)
} }
func TestParseExtClipProvideTextRejectsOversized(t *testing.T) { func TestParseExtClipProvideTextRejectsMalformedStream(t *testing.T) {
var fakePayload [4]byte var fakePayload [4]byte
// 4 bytes of zlib-compressed garbage won't decode; we want to ensure we // 4 bytes of zlib-compressed garbage won't decode; we want to ensure we
// don't panic, not that we accept it. // don't panic, not that we accept it.
_, err := parseExtClipProvideText(extClipActionProvide|extClipFormatText, fakePayload[:]) _, err := parseExtClipProvideText(extClipActionProvide|extClipFormatText, fakePayload[:])
assert.Error(t, err) assert.Error(t, err)
} }
// The size caps are what keep a peer from making us allocate a record of its
// choosing, on either side of the wire.
func TestParseExtClipProvideTextRejectsOversizedRecord(t *testing.T) {
// A well-formed stream whose declared record size is past the cap: the
// bytes behind it are never read, so the guard is the only thing that
// stops the allocation.
var body bytes.Buffer
var lenBuf [4]byte
binary.BigEndian.PutUint32(lenBuf[:], uint32(extClipMaxText)+1)
body.Write(lenBuf[:])
var compressed bytes.Buffer
zw := zlib.NewWriter(&compressed)
_, err := zw.Write(body.Bytes())
require.NoError(t, err)
require.NoError(t, zw.Close())
_, err = parseExtClipProvideText(extClipActionProvide|extClipFormatText, compressed.Bytes())
require.Error(t, err)
assert.Contains(t, err.Error(), "record too large")
}
func TestBuildExtClipProvideTextRejectsOversizedText(t *testing.T) {
// extClipMaxText itself is already one over: the builder appends a NUL
// terminator, and the length it writes counts it.
_, err := buildExtClipProvideText(strings.Repeat("a", extClipMaxText))
require.Error(t, err)
assert.Contains(t, err.Error(), "exceeds extClipMaxText")
_, err = buildExtClipProvideText(strings.Repeat("a", extClipMaxText-1))
require.NoError(t, err, "one byte under the cap must still build")
}
+4 -2
View File
@@ -32,9 +32,11 @@ type X11InputInjector struct {
// NewX11InputInjector connects to the X11 display and initializes XTest. // NewX11InputInjector connects to the X11 display and initializes XTest.
// Empty cookieHex/authFile fall back to XAUTHORITY env lookup. // Empty cookieHex/authFile fall back to XAUTHORITY env lookup.
func NewX11InputInjector(display, cookieHex, authFile string) (*X11InputInjector, error) { func NewX11InputInjector(display, cookieHex, authFile string) (*X11InputInjector, error) {
detectX11Display() // Only probe for a display when the caller named none: detection writes
// DISPLAY and XAUTHORITY into this process's environment, which has no
// business changing when the caller already knows which display to use.
if display == "" { if display == "" {
detectX11Display()
display = os.Getenv(envDisplay) display = os.Getenv(envDisplay)
} }
if display == "" { if display == "" {
+21 -1
View File
@@ -459,6 +459,16 @@ func (s *Server) trackConn(c net.Conn) {
s.sessionsMu.Unlock() s.sessionsMu.Unlock()
} }
// retrackConn replaces a tracked raw connection with the wrapper its handler
// will actually hold, so shutdown and the handler's own untrackConn agree on
// which object is registered.
func (s *Server) retrackConn(raw, wrapped net.Conn) {
s.sessionsMu.Lock()
delete(s.acceptedConns, raw)
s.acceptedConns[wrapped] = struct{}{}
s.sessionsMu.Unlock()
}
// untrackConn forgets a connection once its handler is returning. // untrackConn forgets a connection once its handler is returning.
func (s *Server) untrackConn(c net.Conn) { func (s *Server) untrackConn(c net.Conn) {
s.sessionsMu.Lock() s.sessionsMu.Lock()
@@ -1046,9 +1056,19 @@ func (s *Server) acquireVirtualSession(conn net.Conn, header *connectionHeader,
return nil, nil, nil, false return nil, nil, nil, false
} }
vs.ClientConnect() vs.ClientConnect()
// GetOrCreate checks the session is alive, but nothing stops it being torn
// down between that check and here, and a nil capturer would only surface
// as a panic in the encoder.
capturer, injector := vs.Capturer(), vs.Injector()
if capturer == nil {
vs.ClientDisconnect()
rejectConnection(conn, codeMessage(RejectCodeSessionError, "virtual session stopped"))
(*connLog).Warnf("virtual session for %s stopped before the client attached", header.username)
return nil, nil, nil, false
}
*connLog = (*connLog).WithField("vnc_user", header.username) *connLog = (*connLog).WithField("vnc_user", header.username)
(*connLog).Infof("session mode: user=%s display=%s", header.username, vs.Display()) (*connLog).Infof("session mode: user=%s display=%s", header.username, vs.Display())
return vs.Capturer(), vs.Injector(), vs.ClientDisconnect, true return capturer, injector, vs.ClientDisconnect, true
} }
// acquireAttachSession bumps the shared capturer's per-session refcount // acquireAttachSession bumps the shared capturer's per-session refcount
+8 -3
View File
@@ -49,19 +49,24 @@ func (s *Server) serviceAcceptLoop(ln net.Listener) {
continue continue
} }
// Track before any early-reject path so a concurrent Stop's
// closeActiveSessions snapshot can never miss a just-accepted
// socket and let it survive shutdown.
s.trackConn(conn)
if !s.tryAcquireConnSlot() { if !s.tryAcquireConnSlot() {
s.untrackConn(conn)
s.log.Warnf("rejecting VNC connection from %s: %d concurrent connections in flight", conn.RemoteAddr(), maxConcurrentVNCConns) s.log.Warnf("rejecting VNC connection from %s: %d concurrent connections in flight", conn.RemoteAddr(), maxConcurrentVNCConns)
_ = conn.Close() _ = conn.Close()
continue continue
} }
enableTCPKeepAlive(conn, s.log) enableTCPKeepAlive(conn, s.log)
conn = newMetricsConn(conn, s.sessionRecorder) metered := newMetricsConn(conn, s.sessionRecorder)
s.trackConn(conn) s.retrackConn(conn, metered)
go func(c net.Conn) { go func(c net.Conn) {
defer s.releaseConnSlot() defer s.releaseConnSlot()
defer s.untrackConn(c) defer s.untrackConn(c)
s.handleServiceConnection(c, mgr) s.handleServiceConnection(c, mgr)
}(conn) }(metered)
} }
} }
+8 -3
View File
@@ -336,19 +336,24 @@ func (s *Server) serviceAcceptLoop(ln net.Listener) {
continue continue
} }
// Track before any early-reject path so a concurrent Stop's
// closeActiveSessions snapshot can never miss a just-accepted
// socket and let it survive shutdown.
s.trackConn(conn)
if !s.tryAcquireConnSlot() { if !s.tryAcquireConnSlot() {
s.untrackConn(conn)
s.log.Warnf("rejecting VNC connection from %s: %d concurrent connections in flight", conn.RemoteAddr(), maxConcurrentVNCConns) s.log.Warnf("rejecting VNC connection from %s: %d concurrent connections in flight", conn.RemoteAddr(), maxConcurrentVNCConns)
_ = conn.Close() _ = conn.Close()
continue continue
} }
enableTCPKeepAlive(conn, s.log) enableTCPKeepAlive(conn, s.log)
conn = newMetricsConn(conn, s.sessionRecorder) metered := newMetricsConn(conn, s.sessionRecorder)
s.trackConn(conn) s.retrackConn(conn, metered)
go func(c net.Conn) { go func(c net.Conn) {
defer s.releaseConnSlot() defer s.releaseConnSlot()
defer s.untrackConn(c) defer s.untrackConn(c)
s.handleServiceConnection(c, sm) s.handleServiceConnection(c, sm)
}(conn) }(metered)
} }
} }
+4 -1
View File
@@ -4,6 +4,7 @@ package server
import ( import (
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"image" "image"
"io" "io"
@@ -201,7 +202,9 @@ func (s *session) serve() {
<-encoderDone <-encoderDone
}() }()
if err := s.messageLoop(); err != nil && err != io.EOF { // messageLoop only ever returns an error, so the interesting question is
// which one: a clean client disconnect is io.EOF and not worth a warning.
if err := s.messageLoop(); !errors.Is(err, io.EOF) {
s.log.Warnf("client %s disconnected: %v", s.addr(), err) s.log.Warnf("client %s disconnected: %v", s.addr(), err)
} else { } else {
s.log.Infof("client disconnected: %s", s.addr()) s.log.Infof("client disconnected: %s", s.addr())
+19 -2
View File
@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"time" "time"
"unicode/utf8"
) )
// clipboardPoll periodically checks the server-side clipboard and sends // clipboardPoll periodically checks the server-side clipboard and sends
@@ -192,8 +193,10 @@ func (s *session) handleExtClipProvide(flags uint32, payload []byte) {
// host clipboard contents, capped to extClipMaxText. // host clipboard contents, capped to extClipMaxText.
func (s *session) sendExtClipProvideText() error { func (s *session) sendExtClipProvideText() error {
text := s.injector.GetClipboard() text := s.injector.GetClipboard()
if len(text) > extClipMaxText { // One byte short of the cap: buildExtClipProvideText appends a NUL
text = text[:extClipMaxText] // terminator, which counts against extClipMaxText.
if len(text) > extClipMaxText-1 {
text = trimPartialRune(text[:extClipMaxText-1])
} }
payload, err := buildExtClipProvideText(text) payload, err := buildExtClipProvideText(text)
if err != nil { if err != nil {
@@ -202,6 +205,20 @@ func (s *session) sendExtClipProvideText() error {
return s.writeExtClipMessage(payload) return s.writeExtClipMessage(payload)
} }
// trimPartialRune drops the trailing bytes a byte-length cut left halfway
// through a UTF-8 rune, so the client is never handed invalid UTF-8. A real
// U+FFFD in the text decodes as three bytes and is kept.
func trimPartialRune(s string) string {
for s != "" {
r, size := utf8.DecodeLastRuneInString(s)
if r != utf8.RuneError || size > 1 {
return s
}
s = s[:len(s)-1]
}
return s
}
// writeExtClipMessage frames an ExtendedClipboard payload as a ServerCutText // writeExtClipMessage frames an ExtendedClipboard payload as a ServerCutText
// message with a negative length, then writes it under writeMu. // message with a negative length, then writes it under writeMu.
func (s *session) writeExtClipMessage(payload []byte) error { func (s *session) writeExtClipMessage(payload []byte) error {
+11 -1
View File
@@ -231,13 +231,23 @@ func (vs *VirtualSession) isAlive() bool {
return true return true
} }
// Capturer returns the screen capturer for this virtual session. // Capturer returns the screen capturer for this virtual session, or nil once
// Stop has torn it down. Read under vs.mu, which is what Stop writes it under.
func (vs *VirtualSession) Capturer() ScreenCapturer { func (vs *VirtualSession) Capturer() ScreenCapturer {
vs.mu.Lock()
defer vs.mu.Unlock()
if vs.poller == nil {
return nil
}
return vs.poller return vs.poller
} }
// Injector returns the input injector for this virtual session. // Injector returns the input injector for this virtual session.
func (vs *VirtualSession) Injector() InputInjector { func (vs *VirtualSession) Injector() InputInjector {
vs.mu.Lock()
defer vs.mu.Unlock()
return vs.injector return vs.injector
} }
+6 -3
View File
@@ -81,14 +81,17 @@ func writeXAuthFile(path, hostname, display string, cookie []byte, uid, gid uint
} }
// ensureTraversable walks up from dir to configs.RuntimeDir (inclusive) and // ensureTraversable walks up from dir to configs.RuntimeDir (inclusive) and
// sets mode 0711 on each component. Stops once it leaves the runtime dir so // sets mode 0711 on each component. A dir outside the runtime dir is refused
// it never touches /var/run or /run. // before anything is chmodded, so it never touches /var/run or /run.
func ensureTraversable(dir string) error { func ensureTraversable(dir string) error {
root := filepath.Clean(configs.RuntimeDir) root := filepath.Clean(configs.RuntimeDir)
if root == "" { if root == "" {
return nil return nil
} }
cur := filepath.Clean(dir) cur := filepath.Clean(dir)
if cur != root && !strings.HasPrefix(cur, root+string(os.PathSeparator)) {
return fmt.Errorf("xauth dir %s is outside the runtime dir %s", cur, root)
}
for { for {
if err := os.Chmod(cur, 0711); err != nil { if err := os.Chmod(cur, 0711); err != nil {
return fmt.Errorf("chmod %s: %w", cur, err) return fmt.Errorf("chmod %s: %w", cur, err)
@@ -97,7 +100,7 @@ func ensureTraversable(dir string) error {
return nil return nil
} }
parent := filepath.Dir(cur) parent := filepath.Dir(cur)
if parent == cur || !strings.HasPrefix(cur, root+string(os.PathSeparator)) { if parent == cur {
return nil return nil
} }
cur = parent cur = parent
@@ -8,8 +8,8 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/golang/mock/gomock"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"go.uber.org/mock/gomock"
nbcontext "github.com/netbirdio/netbird/management/server/context" nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions"
@@ -10,10 +10,10 @@ import (
"sync" "sync"
"time" "time"
auth "github.com/netbirdio/netbird/shared/sessionauth"
nbdns "github.com/netbirdio/netbird/dns" nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/domain"
auth "github.com/netbirdio/netbird/shared/sessionauth"
) )
type NetworkMapComponents struct { type NetworkMapComponents struct {
@@ -7,7 +7,6 @@ import (
"net/url" "net/url"
"strings" "strings"
auth "github.com/netbirdio/netbird/shared/sessionauth"
nbconfig "github.com/netbirdio/netbird/management/internals/server/config" nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/types"
@@ -15,6 +14,7 @@ import (
"github.com/netbirdio/netbird/shared/management/networkmap" "github.com/netbirdio/netbird/shared/management/networkmap"
"github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/netiputil" "github.com/netbirdio/netbird/shared/netiputil"
auth "github.com/netbirdio/netbird/shared/sessionauth"
) )
func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route { func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
+3 -3
View File
@@ -104,7 +104,7 @@ func (a *Authorizer) Update(config *Config) {
a.machineUsers = make(map[string][]uint32) a.machineUsers = make(map[string][]uint32)
a.sessionPubKeys = make(map[[sessionPubKeyLen]byte]sshuserhash.UserIDHash) a.sessionPubKeys = make(map[[sessionPubKeyLen]byte]sshuserhash.UserIDHash)
a.sessionDisplayNames = make(map[[sessionPubKeyLen]byte]string) a.sessionDisplayNames = make(map[[sessionPubKeyLen]byte]string)
log.Info("SSH authorization cleared") log.Info("session authorization cleared")
return return
} }
@@ -139,7 +139,7 @@ func (a *Authorizer) Update(config *Config) {
continue continue
} }
if existing, ok := sessionPubKeys[key]; ok && existing != e.UserIDHash { if existing, ok := sessionPubKeys[key]; ok && existing != e.UserIDHash {
log.Warnf("SSH auth: session pubkey bound to conflicting user hashes; dropping binding") log.Warn("session auth: session pubkey bound to conflicting user hashes; dropping binding")
delete(sessionPubKeys, key) delete(sessionPubKeys, key)
delete(sessionDisplayNames, key) delete(sessionDisplayNames, key)
conflicted[key] = struct{}{} conflicted[key] = struct{}{}
@@ -153,7 +153,7 @@ func (a *Authorizer) Update(config *Config) {
a.sessionPubKeys = sessionPubKeys a.sessionPubKeys = sessionPubKeys
a.sessionDisplayNames = sessionDisplayNames a.sessionDisplayNames = sessionDisplayNames
log.Debugf("SSH auth: updated with %d authorized users, %d machine user mappings, %d session pubkeys", log.Debugf("session auth: updated with %d authorized users, %d machine user mappings, %d session pubkeys",
len(config.AuthorizedUsers), len(machineUsers), len(sessionPubKeys)) len(config.AuthorizedUsers), len(machineUsers), len(sessionPubKeys))
} }
+5 -3
View File
@@ -538,11 +538,13 @@ func matchRFBSecurityFailure(p []byte) (string, bool) {
if len(p) < 5 || p[0] != 0 { if len(p) < 5 || p[0] != 0 {
return "", false return "", false
} }
reasonLen := int(p[1])<<24 | int(p[2])<<16 | int(p[3])<<8 | int(p[4]) // Kept as uint32: converting to int first would wrap to a negative value
if reasonLen <= 0 || reasonLen > 4096 || 5+reasonLen != len(p) { // on a 32-bit build and lose the annotation to the length check below.
reasonLen := binary.BigEndian.Uint32(p[1:5])
if reasonLen == 0 || reasonLen > 4096 || 5+int(reasonLen) != len(p) {
return "", false return "", false
} }
return string(p[5 : 5+reasonLen]), true return string(p[5 : 5+int(reasonLen)]), true
} }
// vncRejectCodes mirrors the RejectCode* constants in // vncRejectCodes mirrors the RejectCode* constants in