Fold init-only VNC and SSH setters into Config-struct constructors

This commit is contained in:
Viktor Liu
2026-05-22 13:32:25 +02:00
parent f00b34f3a7
commit 8ca5c2f4b4
7 changed files with 116 additions and 113 deletions
+7 -5
View File
@@ -28,8 +28,11 @@ func noiseTestServer(t *testing.T) (net.Addr, *Server, []byte) {
kp, err := noise.DH25519.GenerateKeypair(nil)
require.NoError(t, err)
srv := New(&testCapturer{}, &StubInputInjector{}, kp.Private)
srv.SetDisableAuth(false)
srv := New(Config{
Capturer: &testCapturer{},
Injector: &StubInputInjector{},
IdentityKey: kp.Private,
})
addr := netip.MustParseAddrPort("127.0.0.1:0")
network := netip.MustParsePrefix("127.0.0.0/8")
@@ -338,8 +341,7 @@ func TestNoise_RevokedKey_RejectedAfterAuthUpdate(t *testing.T) {
// without a static private key still rejects authenticated connections
// fail-closed; it must not silently accept the client.
func TestNoise_NoIdentityKey_FailsClosed(t *testing.T) {
srv := New(&testCapturer{}, &StubInputInjector{}, nil)
srv.SetDisableAuth(false)
srv := New(Config{Capturer: &testCapturer{}, Injector: &StubInputInjector{}})
addr := netip.MustParseAddrPort("127.0.0.1:0")
network := netip.MustParsePrefix("127.0.0.0/8")
require.NoError(t, srv.Start(t.Context(), addr, network))
@@ -384,7 +386,7 @@ func TestNoise_DerivedIdentityPublicMatchesPrivate(t *testing.T) {
for i := range priv {
priv[i] = byte(i + 1)
}
srv := New(&testCapturer{}, &StubInputInjector{}, priv)
srv := New(Config{Capturer: &testCapturer{}, Injector: &StubInputInjector{}, IdentityKey: priv})
expected, err := curve25519.X25519(priv, curve25519.Basepoint)
require.NoError(t, err)
+43 -57
View File
@@ -254,30 +254,57 @@ type virtualSessionManager interface {
StopAll()
}
// New creates a VNC server. identityKey is the 32-byte X25519 private
// key used by the daemon in the Noise_IK handshake; nil disables auth.
// The protocol-level VNC password scheme is not supported.
func New(capturer ScreenCapturer, injector InputInjector, identityKey []byte) *Server {
// Config bundles the values the VNC server needs at construction time.
// Fields are read once by New; mutating them afterwards has no effect.
// Optional fields are nil/zero when unused. The hex-encoded AgentTokenHex
// is decoded internally and an invalid value is logged and treated as
// empty, matching the legacy SetAgentToken behavior.
type Config struct {
Capturer ScreenCapturer
Injector InputInjector
IdentityKey []byte
ServiceMode bool
SessionRecorder func(SessionTick)
DisableAuth bool
AgentTokenHex string
NetstackNet *netstack.Net
}
// New creates a VNC server from the provided Config. IdentityKey is the
// 32-byte X25519 private key used in the Noise_IK handshake; nil disables
// auth. The protocol-level VNC password scheme is not supported.
func New(cfg Config) *Server {
s := &Server{
capturer: capturer,
injector: injector,
identityKey: identityKey,
authorizer: sshauth.NewAuthorizer(),
log: log.WithField("component", "vnc-server"),
sessions: make(map[uint64]ActiveSessionInfo),
sessionConns: make(map[uint64]net.Conn),
acceptedConns: make(map[net.Conn]struct{}),
connAuth: make(map[net.Conn]connAuthInfo),
connSem: make(chan struct{}, maxConcurrentVNCConns),
capturer: cfg.Capturer,
injector: cfg.Injector,
identityKey: cfg.IdentityKey,
serviceMode: cfg.ServiceMode,
sessionRecorder: cfg.SessionRecorder,
disableAuth: cfg.DisableAuth,
netstackNet: cfg.NetstackNet,
authorizer: sshauth.NewAuthorizer(),
log: log.WithField("component", "vnc-server"),
sessions: make(map[uint64]ActiveSessionInfo),
sessionConns: make(map[uint64]net.Conn),
acceptedConns: make(map[net.Conn]struct{}),
connAuth: make(map[net.Conn]connAuthInfo),
connSem: make(chan struct{}, maxConcurrentVNCConns),
}
if len(identityKey) == 32 {
pub, err := curve25519.X25519(identityKey, curve25519.Basepoint)
if len(cfg.IdentityKey) == 32 {
pub, err := curve25519.X25519(cfg.IdentityKey, curve25519.Basepoint)
if err == nil {
s.identityPublic = pub
} else {
s.log.Warnf("derive identity public key: %v", err)
}
}
if cfg.AgentTokenHex != "" {
if b, err := hex.DecodeString(cfg.AgentTokenHex); err == nil {
s.agentToken = b
} else {
s.log.Warnf("invalid agent token: %v", err)
}
}
return s
}
@@ -407,47 +434,6 @@ func (s *Server) revokeUnauthorizedSessions() {
}
}
// SetServiceMode enables proxy-to-agent mode for Windows service operation.
func (s *Server) SetServiceMode(enabled bool) {
s.serviceMode = enabled
}
// SetSessionRecorder installs a callback that receives a SessionTick
// each sessionTickInterval during a VNC session and one final tick on
// session close. Pass nil to disable. Empty ticks (no wire activity)
// are skipped.
func (s *Server) SetSessionRecorder(recorder func(SessionTick)) {
s.sessionRecorder = recorder
}
// SetDisableAuth disables authentication entirely.
func (s *Server) SetDisableAuth(disable bool) {
s.disableAuth = disable
}
// SetAgentToken sets a hex-encoded token that must be presented by incoming
// connections before any VNC data. Used in agent mode to verify that only the
// trusted service process connects.
func (s *Server) SetAgentToken(hexToken string) {
if hexToken == "" {
return
}
b, err := hex.DecodeString(hexToken)
if err != nil {
s.log.Warnf("invalid agent token: %v", err)
return
}
s.agentToken = b
}
// SetNetstackNet sets the netstack network for userspace-only listening.
// When set, the VNC server listens via netstack instead of a real OS socket.
func (s *Server) SetNetstackNet(n *netstack.Net) {
s.mu.Lock()
defer s.mu.Unlock()
s.netstackNet = n
}
// UpdateVNCAuth updates the fine-grained authorization configuration and
// closes any live session whose identity no longer authenticates under
// the new policy. Revocation is event-driven: there is no periodic
+29 -14
View File
@@ -28,8 +28,11 @@ func (t *testCapturer) Capture() (*image.RGBA, error) {
func startTestServer(t *testing.T, disableAuth bool) (net.Addr, *Server) {
t.Helper()
srv := New(&testCapturer{}, &StubInputInjector{}, nil)
srv.SetDisableAuth(disableAuth)
srv := New(Config{
Capturer: &testCapturer{},
Injector: &StubInputInjector{},
DisableAuth: disableAuth,
})
addr := netip.MustParseAddrPort("127.0.0.1:0")
network := netip.MustParsePrefix("127.0.0.0/8")
@@ -112,8 +115,11 @@ func TestAuthDisabled_AllowsConnection(t *testing.T) {
// server must close immediately and the client must see EOF before any RFB
// version greeting is written.
func TestAuth_NoUnauthBytesPastHeader(t *testing.T) {
srv := New(&testCapturer{}, &StubInputInjector{}, nil)
srv.SetDisableAuth(true)
srv := New(Config{
Capturer: &testCapturer{},
Injector: &StubInputInjector{},
DisableAuth: true,
})
addr := netip.MustParseAddrPort("127.0.0.1:0")
// Tight overlay that excludes 127.0.0.0/8 and a non-loopback local IP, so
// the loopback short-circuit in isAllowedSource doesn't apply.
@@ -193,7 +199,7 @@ func TestIsAllowedSource(t *testing.T) {
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
srv := New(&testCapturer{}, &StubInputInjector{}, nil)
srv := New(Config{Capturer: &testCapturer{}, Injector: &StubInputInjector{}})
srv.localAddr = tc.localAddr
srv.network = tc.network
assert.Equal(t, tc.want, srv.isAllowedSource(tc.remote))
@@ -202,7 +208,7 @@ func TestIsAllowedSource(t *testing.T) {
}
func TestStart_InvalidNetworkRejected(t *testing.T) {
srv := New(&testCapturer{}, &StubInputInjector{}, nil)
srv := New(Config{Capturer: &testCapturer{}, Injector: &StubInputInjector{}})
addr := netip.MustParseAddrPort("127.0.0.1:0")
err := srv.Start(t.Context(), addr, netip.Prefix{})
require.Error(t, err, "Start must refuse an invalid overlay prefix")
@@ -210,9 +216,12 @@ func TestStart_InvalidNetworkRejected(t *testing.T) {
}
func TestAgentToken_MismatchClosesConnection(t *testing.T) {
srv := New(&testCapturer{}, &StubInputInjector{}, nil)
srv.SetDisableAuth(true)
srv.SetAgentToken("deadbeefcafebabe")
srv := New(Config{
Capturer: &testCapturer{},
Injector: &StubInputInjector{},
DisableAuth: true,
AgentTokenHex: "deadbeefcafebabe",
})
addr := netip.MustParseAddrPort("127.0.0.1:0")
network := netip.MustParsePrefix("127.0.0.0/8")
@@ -238,10 +247,13 @@ func TestAgentToken_MismatchClosesConnection(t *testing.T) {
}
func TestAgentToken_MatchAllowsHandshake(t *testing.T) {
srv := New(&testCapturer{}, &StubInputInjector{}, nil)
srv.SetDisableAuth(true)
const tokenHex = "deadbeefcafebabe"
srv.SetAgentToken(tokenHex)
srv := New(Config{
Capturer: &testCapturer{},
Injector: &StubInputInjector{},
DisableAuth: true,
AgentTokenHex: tokenHex,
})
token, err := hex.DecodeString(tokenHex)
require.NoError(t, err)
@@ -275,8 +287,11 @@ func TestAgentToken_MatchAllowsHandshake(t *testing.T) {
func TestSessionMode_RejectedWhenNoVMGR(t *testing.T) {
// Default platformSessionManager() on non-Linux returns nil, so ModeSession
// must be rejected with the UNSUPPORTED reason rather than crashing.
srv := New(&testCapturer{}, &StubInputInjector{}, nil)
srv.SetDisableAuth(true)
srv := New(Config{
Capturer: &testCapturer{},
Injector: &StubInputInjector{},
DisableAuth: true,
})
addr := netip.MustParseAddrPort("127.0.0.1:0")
network := netip.MustParsePrefix("127.0.0.0/8")