Support IPv6 for the embedded VNC server and browser proxy

This commit is contained in:
Viktor Liu
2026-07-12 18:46:47 +02:00
parent 6c3dac6fa0
commit 4b946ec219
10 changed files with 183 additions and 63 deletions
+64 -19
View File
@@ -169,15 +169,22 @@ type Server struct {
localAddr netip.Addr
// network is the NetBird overlay network.
network netip.Prefix
log *log.Entry
// localAddr6 and network6 are the v6 overlay address and network, set
// when a v6 listener is added; zero when the overlay has no v6.
localAddr6 netip.Addr
network6 netip.Prefix
log *log.Entry
mu sync.Mutex
listener net.Listener
ctx context.Context
cancel context.CancelFunc
vmgr virtualSessionManager
authorizer *sshauth.Authorizer
netstackNet *netstack.Net
mu sync.Mutex
listener net.Listener
// extraListeners holds additional listeners (e.g. the v6 overlay), closed
// alongside listener on Stop.
extraListeners []net.Listener
ctx context.Context
cancel context.CancelFunc
vmgr virtualSessionManager
authorizer *sshauth.Authorizer
netstackNet *netstack.Net
// agentToken holds the raw token bytes for agent-mode auth.
agentToken []byte
// invalidAgentToken latches when AgentTokenHex was provided but failed
@@ -609,21 +616,54 @@ func (s *Server) Start(ctx context.Context, addr netip.AddrPort, network netip.P
}
if s.serviceMode {
go s.serviceAcceptLoop()
go s.serviceAcceptLoop(s.listener)
} else {
go s.acceptLoop()
go s.acceptLoop(s.listener)
}
s.log.Infof("started on %s (service_mode=%v)", listenDesc, s.serviceMode)
return nil
}
// AddListener opens an additional overlay listener (e.g. the v6 overlay
// address) and serves it with the same accept path as the primary listener.
// The server must already be running. Mirrors the primary listener's mode so
// service-mode connections still route through the per-session agent proxy.
func (s *Server) AddListener(_ context.Context, addr netip.AddrPort, network netip.Prefix) error {
s.mu.Lock()
if s.listener == nil {
s.mu.Unlock()
return fmt.Errorf("server not running")
}
ln, desc, err := s.openOverlayListener(addr, network)
if err != nil {
s.mu.Unlock()
return err
}
s.extraListeners = append(s.extraListeners, ln)
serviceMode := s.serviceMode
s.mu.Unlock()
s.log.Infof("also listening on %s (service_mode=%v)", desc, serviceMode)
if serviceMode {
go s.serviceAcceptLoop(ln)
} else {
go s.acceptLoop(ln)
}
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 addr.Addr().Is6() {
s.localAddr6 = addr.Addr()
s.network6 = network
} else {
s.localAddr = addr.Addr()
s.network = network
}
if s.netstackNet != nil {
ln, err := s.netstackNet.ListenTCPAddrPort(addr)
if err != nil {
@@ -658,6 +698,12 @@ func (s *Server) Stop() error {
listenerErr = s.listener.Close()
s.listener = nil
}
for _, ln := range s.extraListeners {
if err := ln.Close(); err != nil && listenerErr == nil {
listenerErr = err
}
}
s.extraListeners = nil
s.closeActiveSessions()
if s.vmgr != nil {
@@ -681,10 +727,7 @@ func (s *Server) Stop() error {
}
// acceptLoop handles VNC connections directly (user session mode).
func (s *Server) acceptLoop() {
s.mu.Lock()
ln := s.listener
s.mu.Unlock()
func (s *Server) acceptLoop(ln net.Listener) {
if ln == nil {
return
}
@@ -790,16 +833,18 @@ func (s *Server) isAllowedSource(addr net.Addr) bool {
return true
}
if remoteIP == s.localAddr {
if remoteIP == s.localAddr || (s.localAddr6.IsValid() && remoteIP == s.localAddr6) {
s.log.Warnf("connection rejected from own IP %s", remoteIP)
return false
}
if !s.network.IsValid() {
if !s.network.IsValid() && !s.network6.IsValid() {
s.log.Warnf("connection rejected: overlay network not configured")
return false
}
if !s.network.Contains(remoteIP) {
inV4 := s.network.IsValid() && s.network.Contains(remoteIP)
inV6 := s.network6.IsValid() && s.network6.Contains(remoteIP)
if !inV4 && !inV6 {
s.log.Warnf("connection rejected from non-NetBird IP %s", remoteIP)
return false
}
+5 -7
View File
@@ -24,18 +24,16 @@ func (s *Server) platformSessionManager() virtualSessionManager {
// to the per-user agent darwinAgentManager spawns via launchctl asuser
// (the only spawn mode that lands a child in the user's Aqua session with
// WindowServer + TCC access).
func (s *Server) serviceAcceptLoop() {
func (s *Server) serviceAcceptLoop(ln net.Listener) {
if ln == nil {
return
}
mgr := newDarwinAgentManager(s.ctx)
defer mgr.stop()
log.Info("service mode, proxying connections to per-user agent over Unix socket")
s.mu.Lock()
ln := s.listener
s.mu.Unlock()
if ln == nil {
return
}
for {
conn, err := ln.Accept()
if err != nil {
+43 -5
View File
@@ -145,11 +145,13 @@ func TestAuth_NoUnauthBytesPastHeader(t *testing.T) {
func TestIsAllowedSource(t *testing.T) {
tests := []struct {
name string
localAddr netip.Addr
network netip.Prefix
remote net.Addr
want bool
name string
localAddr netip.Addr
network netip.Prefix
localAddr6 netip.Addr
network6 netip.Prefix
remote net.Addr
want bool
}{
{
// Unix-domain remotes (per-session agent path) are local IPC,
@@ -202,12 +204,48 @@ func TestIsAllowedSource(t *testing.T) {
remote: &net.TCPAddr{IP: net.ParseIP("10.99.99.2"), Port: 5900},
want: false,
},
{
name: "v6 overlay IP allowed",
localAddr: netip.MustParseAddr("10.99.99.1"),
network: netip.MustParsePrefix("10.99.0.0/16"),
localAddr6: netip.MustParseAddr("fd00:1234::1"),
network6: netip.MustParsePrefix("fd00:1234::/64"),
remote: &net.TCPAddr{IP: net.ParseIP("fd00:1234::2"), Port: 5900},
want: true,
},
{
name: "v6 own IP rejected",
localAddr: netip.MustParseAddr("10.99.99.1"),
network: netip.MustParsePrefix("10.99.0.0/16"),
localAddr6: netip.MustParseAddr("fd00:1234::1"),
network6: netip.MustParsePrefix("fd00:1234::/64"),
remote: &net.TCPAddr{IP: net.ParseIP("fd00:1234::1"), Port: 5900},
want: false,
},
{
name: "v6 outside overlay rejected",
localAddr: netip.MustParseAddr("10.99.99.1"),
network: netip.MustParsePrefix("10.99.0.0/16"),
localAddr6: netip.MustParseAddr("fd00:1234::1"),
network6: netip.MustParsePrefix("fd00:1234::/64"),
remote: &net.TCPAddr{IP: net.ParseIP("2001:db8::5"), Port: 5900},
want: false,
},
{
name: "v6 rejected when only v4 overlay configured",
localAddr: netip.MustParseAddr("10.99.99.1"),
network: netip.MustParsePrefix("10.99.0.0/16"),
remote: &net.TCPAddr{IP: net.ParseIP("fd00:1234::2"), Port: 5900},
want: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
srv := New(Config{Capturer: &testCapturer{}, Injector: &StubInputInjector{}})
srv.localAddr = tc.localAddr
srv.network = tc.network
srv.localAddr6 = tc.localAddr6
srv.network6 = tc.network6
assert.Equal(t, tc.want, srv.isAllowedSource(tc.remote))
})
}
+4 -8
View File
@@ -241,20 +241,16 @@ func (s *Server) platformInit() {
// serviceAcceptLoop runs in Session 0. It validates the source IP and
// hands accepted connections to handleServiceConnection, which runs the
// Noise_IK handshake before proxying to the user-session agent.
func (s *Server) serviceAcceptLoop() {
func (s *Server) serviceAcceptLoop(ln net.Listener) {
if ln == nil {
return
}
sm := newSessionManager()
go sm.run()
log.Info("service mode, proxying connections to agent over Unix socket")
s.mu.Lock()
ln := s.listener
s.mu.Unlock()
if ln == nil {
sm.Stop()
return
}
for {
conn, err := ln.Accept()
if err != nil {
+4 -2
View File
@@ -2,14 +2,16 @@
package server
import "net"
func (s *Server) platformInit() {
// no-op on X11
}
// serviceAcceptLoop is not supported on Linux.
func (s *Server) serviceAcceptLoop() {
func (s *Server) serviceAcceptLoop(ln net.Listener) {
s.log.Warn("service mode not supported on Linux, falling back to direct mode")
s.acceptLoop()
s.acceptLoop(ln)
}
func (s *Server) platformSessionManager() virtualSessionManager {