Skip to content
Open
Show file tree
Hide file tree
Changes from 150 commits
Commits
Show all changes
164 commits
Select commit Hold shift + click to select a range
9f0aa1c
Add embedded VNC server with JWT auth and per-peer toggle
lixmal May 16, 2026
62cf9e8
Track active VNC sessions in status and address CodeRabbit findings
lixmal May 16, 2026
7123e6d
Fix Windows lint errcheck/unused and Linux nilerr in console VNC fall…
lixmal May 16, 2026
9b5541d
Extract session-address anonymization helper to lower status complexity
lixmal May 16, 2026
738c585
Guard VNC session negotiated encoding state with RWMutex
lixmal May 17, 2026
94068d3
Drop -ac from Xvfb/Xorg invocations to keep xhost localuser grant aut…
lixmal May 17, 2026
a8541a1
Apply posture and validated-peers filtering on ResourceTypePeer polic…
lixmal May 17, 2026
8bf13b0
Merge SSH wildcard authorized users across matching rules
lixmal May 17, 2026
fa90283
Extract wildcard user merge helper to satisfy case-clause length
lixmal May 17, 2026
d6d3fa9
Drop unused getPeerFromResource helper
lixmal May 17, 2026
44ed0c1
Drop xclip-no-selection trace log that fires every 2s on Xvfb
lixmal May 17, 2026
cd005ef
Add CopyRect detection and emission for tile-aligned moves
lixmal May 17, 2026
047cc95
Throttle capture-failure log to once per 5s while capturer is down
lixmal May 17, 2026
e759487
Prompt for macOS Accessibility and Screen Recording at VNC enable time
lixmal May 17, 2026
db5b6cf
Add DesktopSize, DesktopName, LastRect pseudo-encodings with resize d…
lixmal May 17, 2026
6d937af
Drop dead Hextile and standalone Zlib encoding paths
lixmal May 17, 2026
b4f6962
Drop unused VNC DES auth path
lixmal May 17, 2026
2bed8b6
Lock pixel format to 32bpp little-endian truecolour and reject other …
lixmal May 17, 2026
4f884d9
Add QEMU Extended Key Event for layout-independent input
lixmal May 17, 2026
da37a28
Exclude VNC server from js, ios, and android builds
lixmal May 17, 2026
b135d46
Drop unused zlibState.scratch field
lixmal May 17, 2026
a11341f
Add ExtendedClipboard pseudo-encoding for UTF-8 bidirectional clipboard
lixmal May 17, 2026
76add0b
Fix ExtendedClipboard auto-request by advertising all actions in Caps
lixmal May 17, 2026
61ec8d6
Honor QualityLevel and CompressLevel pseudo-encodings
lixmal May 17, 2026
2d0a54f
Fix golangci-lint and Sonar: drop newZlibState, extract applyEncoding…
lixmal May 17, 2026
0b8fc5d
Split session.go: encoder pipeline and clipboard handling into separa…
lixmal May 17, 2026
ee393d0
Clamp Tight length to 22 bits and fall back to Raw on overflow
lixmal May 17, 2026
f5e1057
Latin-1 round-trip for legacy CutText and soft-fail ext clipboard errors
lixmal May 18, 2026
bfb6750
Reset encoding capability flags on each SetEncodings
lixmal May 18, 2026
785f94d
Guard buildExtClipProvideText against oversized input
lixmal May 18, 2026
7e7e056
Reset Tight zlib stream when deflater is recreated mid-session
lixmal May 18, 2026
97d0a67
Release sticky modifiers and mouse buttons on client disconnect
lixmal May 18, 2026
b9f5264
Restore createRDPProxy wasm entry point for dashboard RDP
lixmal May 18, 2026
c2fdf62
Detect dead VNC peers on both ends and report session stats
lixmal May 18, 2026
5543404
Cap honored VNC client JPEG quality at 50
lixmal May 18, 2026
bc40752
Register VNC netstack service only when netstack is active
lixmal May 18, 2026
6bb66e0
[management] Avoid peer IP reallocation when account settings update …
lixmal May 16, 2026
d3f3e08
Avoid context cancellation in `cancelPeerRoutines` (#6175)
mlsmaycon May 16, 2026
d927ef4
Clean up legacy 32-bit and HKCU registry entries on Windows install (…
lixmal May 16, 2026
32a5a06
[management] fix: device redirect uri wasn't registered (#6191)
jnfrati May 18, 2026
97bc1ee
[management] Fence peer status updates with a session token (#6193)
mlsmaycon May 18, 2026
8e2505b
[management] Add metrics for peer status updates and ephemeral cleanu…
mlsmaycon May 18, 2026
9d189bb
Restore Hextile SolidFill and Zlib encoding paths
lixmal May 19, 2026
24a5f22
Accept any RGB shift permutation as Tight-compatible per RFB 7.7.6
lixmal May 19, 2026
b41fbad
Surface DXGI fallback to GDI at warn level on Windows
lixmal May 19, 2026
393c102
Throttle VNC encoder JPEG quality and skip frames under write backpre…
lixmal May 19, 2026
5eec996
Honour client JPEG quality fully now that backpressure caps it dynami…
lixmal May 19, 2026
b3f0f53
Collapse dirty rects to their bounding box when the bbox is densely d…
lixmal May 19, 2026
2285db2
Treat ExtendedClipboard messages with the Caps bit as Caps regardless…
lixmal May 19, 2026
fe15688
Emit Cursor pseudo-encoding on Linux, Windows, and macOS
lixmal May 19, 2026
b1b04f9
Composite remote cursor into the framebuffer when the dashboard toggl…
lixmal May 19, 2026
df9a6fb
Drop pbpaste trace log that fires whenever the macOS pasteboard is empty
lixmal May 19, 2026
62b3611
Extract daemon-to-agent loopback proxy and token helpers into a platf…
lixmal May 19, 2026
7d61975
Proxy macOS VNC connections from the LaunchDaemon to a per-user agent…
lixmal May 19, 2026
5e200fa
Drop unreliable Sequoia preflight from macOS Screen Recording check
lixmal May 20, 2026
02b9fe7
Use pixel-mode scroll on macOS for smoother wheel events
lixmal May 20, 2026
c28e41e
Track macOS click count and pixel-scale wheel scroll
lixmal May 20, 2026
354fd00
Enable IdP JWKS refresh in VNC JWT validator
lixmal May 20, 2026
896530f
Add ExtendedMouseButtons for back/forward mouse buttons
lixmal May 20, 2026
517bea0
Collapse X11 DISPLAY/XAUTHORITY auto-detect logs into one line
lixmal May 20, 2026
7e5846a
Resolve merge conflicts with main
lixmal May 20, 2026
17359cd
Fix VNC lint, 386 atomic alignment, and Sonar code smells
lixmal May 20, 2026
640a267
Address CodeRabbit feedback on VNC server
lixmal May 20, 2026
f37e228
Replace magic env-var and subcommand strings with named constants
lixmal May 20, 2026
b41d11b
Allow Cursor pseudo-encoding in session mode and cache last XFixes sp…
lixmal May 20, 2026
ef4ea2e
Set Fn flag on macOS navigation keycodes so the next key isn't treate…
lixmal May 20, 2026
98d533c
Address CodeRabbit feedback on VNC server agent matching and session …
lixmal May 21, 2026
2f4ddf0
Emit explicit Fn flagsChanged transitions around macOS navigation keys
lixmal May 21, 2026
3d3055d
Replace VNC JWT auth with a Noise_IK handshake bound to ACL-pushed pu…
lixmal May 21, 2026
ee348ba
Abort VNC agent dial retry loop on server shutdown
lixmal May 21, 2026
5e67feb
Address Sonar findings and move noise to direct dependency
lixmal May 21, 2026
412193c
Address CodeRabbit VNC review feedback
lixmal May 21, 2026
1cc5967
Address follow-up CodeRabbit VNC findings
lixmal May 22, 2026
0f03c61
Lower CreateTemporaryAccess complexity and emit VncAuth for session p…
lixmal May 22, 2026
030c571
Signal Zlib encode failure and fall back to Raw
lixmal May 22, 2026
97b7b01
Fold init-only VNC and SSH setters into Config-struct constructors
lixmal May 22, 2026
c29ef63
Switch VNC daemon-to-agent IPC to Unix sockets and audit-log every co…
lixmal May 22, 2026
8e72967
Add per-connection user-approval prompts for VNC
lixmal May 23, 2026
8d329da
Evict orphaned packet captures and annotate VNC streams
lixmal May 23, 2026
1f912be
Address codespell and Sonar findings on embedded-vnc
lixmal May 23, 2026
7cb6388
Decline VNC approval early when no console user is logged in
lixmal May 23, 2026
fa57eed
Address CodeRabbit review and fix CI on embedded-vnc
lixmal May 23, 2026
f557e66
Return error from gateApproval and log at the caller
lixmal May 23, 2026
5e2830b
Harden VNC server, IPC, and management plumbing
lixmal May 24, 2026
4e3e3ce
Surface VNC initiator in status, clarify proxy logs, dampen capture n…
lixmal May 24, 2026
bf2fb2f
Address CodeRabbit review on embedded VNC PR
lixmal May 24, 2026
2f67841
Reuse /var/run/netbird as VNC agent socket parent via configs.RuntimeDir
lixmal May 25, 2026
65f302b
Authenticate virtual X11 sessions with per-session MIT-MAGIC-COOKIE-1
lixmal May 25, 2026
3bcacff
Rename xauth_linux.go to xauth_x11.go so FreeBSD picks it up
lixmal May 25, 2026
6cd5d60
Split prepareAgentSocketDir to reduce cognitive complexity
lixmal May 25, 2026
6c9465d
Handle Lstat error in purgeStaleAgentSubdir
lixmal May 25, 2026
144dfbc
Capture listener locally in accept loops to avoid nil deref on Stop
lixmal May 25, 2026
2fdc3ae
Merge branch 'main' into embedded-vnc
lixmal Jun 10, 2026
f2c7920
Fix review findings for embedded VNC server
lixmal Jun 10, 2026
c1eecaa
Merge branch 'main' into embedded-vnc
lixmal Jun 14, 2026
d568084
Merge remote-tracking branch 'origin/main' into embedded-vnc
lixmal Jun 28, 2026
fd7bf98
Split CreateTemporaryAccess into smaller functions
lixmal Jun 28, 2026
dbc7b84
Merge origin/main into embedded-vnc
lixmal Jul 12, 2026
02e7c0e
Port VNC settings and connection-approval prompt to the Wails UI
lixmal Jul 12, 2026
cec9ea8
Support IPv6 for the embedded VNC server and browser proxy
lixmal Jul 12, 2026
125250c
Regenerate proto gateway and OpenAPI code with pinned tool versions
lixmal Jul 12, 2026
eb6e8dc
Reject empty approval request_id and verify gid after setgid
lixmal Jul 13, 2026
152ba28
Add VNC allow and approval settings to MDM policy
lixmal Jul 13, 2026
2b051ad
Merge main into embedded-vnc
lixmal Jul 30, 2026
abe8bc1
Require privilege for VNC server and approval config changes
lixmal Jul 30, 2026
05a75d8
Carry netbird-vnc policies over the component network map
lixmal Jul 30, 2026
0e2bd09
Build VNC auth for peers on the component network map path
lixmal Jul 30, 2026
548cbfb
Drop the VNC port constant left unused by the shared types move
lixmal Jul 30, 2026
1836616
Account for the VNC peer flag in the peer meta field-count guard
lixmal Jul 30, 2026
8e919b4
Share one VNC session agent manager across all accept loops
lixmal Jul 30, 2026
abd5022
Flag active VNC sessions on the main screen
lixmal Jul 30, 2026
90d2687
Push a status snapshot when VNC sessions start or end
lixmal Jul 30, 2026
9c33470
Restrict the SAS event to SYSTEM and restore its registry snapshot pe…
lixmal Jul 31, 2026
59c7512
Move the service agent accessor to the platforms that have one
lixmal Jul 31, 2026
a117c19
Treat an unreadable SoftwareSASGeneration as unknown rather than absent
lixmal Jul 31, 2026
86d775f
Merge main into embedded-vnc
lixmal Jul 31, 2026
3d4137a
Wait for the Windows VNC agent to respawn instead of refusing the con…
lixmal Aug 3, 2026
4984e8e
Draw inverted monochrome cursor pixels so the text caret is visible
lixmal Aug 3, 2026
78bc656
Fail fast when no console session exists and keep the SAS value type
lixmal Aug 6, 2026
fcfe643
Merge main into embedded-vnc
lixmal Aug 6, 2026
ef1d406
Ask for Screen Recording from the agent and Accessibility on first input
lixmal Aug 6, 2026
12b18f5
Recycle the macOS VNC agent per connection so permission prompts work
lixmal Aug 6, 2026
a730672
Stamp modifiers on injected macOS key events so shifted input works
lixmal Aug 6, 2026
4b72019
Merge branch 'main' into embedded-vnc
lixmal Aug 25, 2026
f7e186f
Merge branch 'main' into embedded-vnc
lixmal Aug 27, 2026
90483da
Address review findings on the VNC server, session auth and capture d…
lixmal Aug 27, 2026
64d808e
Split the VNC, routing and metrics flag copying out of the config req…
lixmal Aug 27, 2026
c804bee
Put the approval prompt's Deny and Allow buttons on one row
lixmal Aug 27, 2026
8997670
Regenerate the daemon proto with the protoc version the tree was gene…
lixmal Aug 27, 2026
4d7ca23
Split the status summary into per-section helpers and scope retrackCo…
lixmal Aug 27, 2026
071bd94
Close view-only input, approval-responder and inbound-block gaps in t…
lixmal Aug 27, 2026
5986929
Carry the VNC session key through the network map DB path
lixmal Aug 27, 2026
913a1c6
Fix framebuffer layout handling, capturer lifecycle and macOS pointer…
lixmal Aug 27, 2026
f5d821c
Describe session auth as SSH and VNC, cover the conflicting key path,…
lixmal Aug 27, 2026
432d249
Accept bare marker protocols, reject msb_right framebuffers, split th…
lixmal Aug 29, 2026
4a0fe09
Merge branch 'main' into embedded-vnc
lixmal Aug 29, 2026
95e86de
Resolve VNC authorized users on the components path and fix uinput, X…
lixmal Aug 29, 2026
fb9c0ef
Make session key authorization atomic, unblock the encoder on teardow…
lixmal Aug 29, 2026
8b2db16
Route resource endpoints through the shared policy peer filter
lixmal Aug 29, 2026
312b73f
Persist virtual session processes for crash recovery and identify the…
lixmal Aug 29, 2026
1ecd1da
Collect auth requirements for bidirectional source peers and fix foll…
lixmal Aug 29, 2026
827098c
Compare cursor serials by identity so returning to an earlier cursor …
lixmal Aug 29, 2026
d196b23
Stop narrowing the shared runtime dir, close the injector on stop, al…
lixmal Aug 29, 2026
d823600
Honour the negotiated pixel format for the cursor, enqueue key edges …
lixmal Aug 29, 2026
9672e04
Scope crash recovery to Linux, drain connection handlers before teardown
lixmal Aug 29, 2026
8d6b7b6
Leave the VNC approver nil when there is no broker to ask
lixmal Aug 29, 2026
ba104a1
Encode one framebuffer update at a single negotiated pixel format
lixmal Aug 29, 2026
9ec27f9
Authenticate the daemon and its VNC agent to each other without sendi…
lixmal Aug 29, 2026
e104cef
Require a real first DXGI frame and read the FreeBSD framebuffer at i…
lixmal Aug 29, 2026
d0d8813
Fail an approval request fast when no subscriber received the prompt
lixmal Aug 29, 2026
5f739ef
Reject an unsupported VNC session id and say why a cursor rect was sk…
lixmal Aug 29, 2026
15d6e3b
Keep DXGI on an idle desktop, honour the FreeBSD pitch when swizzling…
lixmal Aug 29, 2026
fecd7cf
Attach to the X server on the active VT and keep a retryable DXGI fra…
lixmal Aug 29, 2026
12040b1
Refuse an ambiguous X display and settle the approval timeout race un…
lixmal Aug 29, 2026
3ce0957
Describe the approval no-match result and the cursor-skip key as they…
lixmal Aug 30, 2026
932c87e
Stop retrying an Accept that will not recover
lixmal Aug 31, 2026
3af7764
Give the user a minute to answer an approval prompt
lixmal Aug 30, 2026
bad63c1
Say whether an approval was refused, unanswered, or never shown
lixmal Aug 31, 2026
336fc9a
Drop the VNC port helpers and approval kind nothing calls
lixmal Aug 31, 2026
be33e9e
Merge branch 'main' into embedded-vnc
lixmal Sep 2, 2026
cb8fbfa
Retry VNC accept on transient connection and resource errors
lixmal Sep 3, 2026
eb3dabb
Merge branch 'main' into embedded-vnc
lixmal Sep 3, 2026
6bb1727
Add the VNC and approval strings to the Ukrainian bundle
lixmal Sep 4, 2026
8d77231
Rebind the VNC listeners with the other overlay services
lixmal Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 61 additions & 30 deletions client/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,16 @@ func setSSHSetConfigFields(req *proto.SetConfigRequest, cmd *cobra.Command) {
}
}

// setVNCSetConfigFields is setSSHSetConfigFields for the VNC server flags.
func setVNCSetConfigFields(req *proto.SetConfigRequest, cmd *cobra.Command) {
if cmd.Flag(serverVNCAllowedFlag).Changed {
req.ServerVNCAllowed = &serverVNCAllowed
}
if cmd.Flag(disableVNCApprovalFlag).Changed {
req.DisableVNCApproval = &disableVNCApproval
}
}

func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, profileName, username string) *proto.SetConfigRequest {
var req proto.SetConfigRequest
req.ProfileName = profileName
Expand All @@ -447,6 +457,7 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro
req.RosenpassPermissive = &rosenpassPermissive
}
setSSHSetConfigFields(&req, cmd)
setVNCSetConfigFields(&req, cmd)

if cmd.Flag(interfaceNameFlag).Changed {
if err := parseInterfaceName(interfaceName); err != nil {
Expand Down Expand Up @@ -475,46 +486,50 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro
req.DisableAutoConnect = &autoConnectDisabled
}

setNetworkSetConfigFields(&req, cmd)
setLocalMetricsSetConfigFields(&req, cmd)

return &req
}

// setNetworkSetConfigFields copies the routing, DNS and firewall toggles the
// user actually passed into req.
func setNetworkSetConfigFields(req *proto.SetConfigRequest, cmd *cobra.Command) {
if cmd.Flag(dnsRouteIntervalFlag).Changed {
req.DnsRouteInterval = durationpb.New(dnsRouteInterval)
}

if cmd.Flag(disableClientRoutesFlag).Changed {
req.DisableClientRoutes = &disableClientRoutes
}

if cmd.Flag(disableServerRoutesFlag).Changed {
req.DisableServerRoutes = &disableServerRoutes
}

if cmd.Flag(disableDNSFlag).Changed {
req.DisableDns = &disableDNS
}

if cmd.Flag(disableFirewallFlag).Changed {
req.DisableFirewall = &disableFirewall
}

if cmd.Flag(blockLANAccessFlag).Changed {
req.BlockLanAccess = &blockLANAccess
}

if cmd.Flag(blockInboundFlag).Changed {
req.BlockInbound = &blockInbound
}

if cmd.Flag(disableIPv6Flag).Changed {
req.DisableIpv6 = &disableIPv6
}
}

// setLocalMetricsSetConfigFields copies the local metrics endpoint flags the
// user actually passed into req.
func setLocalMetricsSetConfigFields(req *proto.SetConfigRequest, cmd *cobra.Command) {
if cmd.Flag(enableLocalMetricsFlag).Changed {
req.EnableLocalMetrics = &localMetricsEnabled
}
if cmd.Flag(localMetricsAddressFlag).Changed {
req.LocalMetricsAddress = &localMetricsAddr
}

return &req
}

func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFilePath string) (*profilemanager.ConfigInput, error) {
Expand All @@ -538,30 +553,14 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil
if cmd.Flag(serverSSHAllowedFlag).Changed {
ic.ServerSSHAllowed = &serverSSHAllowed
}

if cmd.Flag(enableSSHRootFlag).Changed {
ic.EnableSSHRoot = &enableSSHRoot
}

if cmd.Flag(enableSSHSFTPFlag).Changed {
ic.EnableSSHSFTP = &enableSSHSFTP
if cmd.Flag(serverVNCAllowedFlag).Changed {
ic.ServerVNCAllowed = &serverVNCAllowed
}

if cmd.Flag(enableSSHLocalPortForwardFlag).Changed {
ic.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward
if cmd.Flag(disableVNCApprovalFlag).Changed {
ic.DisableVNCApproval = &disableVNCApproval
}

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 {
Expand Down Expand Up @@ -642,6 +641,27 @@ 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
}
}

// setSSHLoginFields copies the SSH server flags the user actually passed
// into req, leaving the rest unset so the daemon keeps the persisted
// values.
Expand Down Expand Up @@ -670,6 +690,16 @@ func setSSHLoginFields(req *proto.LoginRequest, cmd *cobra.Command) {
}
}

// setVNCLoginFields is setSSHLoginFields for the VNC server flags.
func setVNCLoginFields(req *proto.LoginRequest, cmd *cobra.Command) {
if cmd.Flag(serverVNCAllowedFlag).Changed {
req.ServerVNCAllowed = &serverVNCAllowed
}
if cmd.Flag(disableVNCApprovalFlag).Changed {
req.DisableVNCApproval = &disableVNCApproval
}
}

func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte, cmd *cobra.Command) (*proto.LoginRequest, error) {
loginRequest := proto.LoginRequest{
SetupKey: providedSetupKey,
Expand Down Expand Up @@ -697,6 +727,7 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
}

setSSHLoginFields(&loginRequest, cmd)
setVNCLoginFields(&loginRequest, cmd)

if cmd.Flag(disableAutoConnectFlag).Changed {
loginRequest.DisableAutoConnect = &autoConnectDisabled
Expand Down
104 changes: 104 additions & 0 deletions client/cmd/vnc_agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//go:build windows || (darwin && !ios)

package cmd

import (
"fmt"
"net"
"net/netip"
"os"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

vncserver "github.com/netbirdio/netbird/client/vnc/server"
)

var (
vncAgentSocket string
vncAgentTargetUID uint32
)

func init() {
vncAgentCmd.Flags().StringVar(&vncAgentSocket, "socket", "", "Unix-domain socket path the agent listens on (required)")
vncAgentCmd.Flags().Uint32Var(&vncAgentTargetUID, "target-uid", 0, "uid the agent drops privileges to before listening (darwin only; required there, and must not be 0)")
rootCmd.AddCommand(vncAgentCmd)
}

// vncAgentCmd runs a VNC server inside the user's interactive session,
// listening on a Unix-domain socket. The NetBird service spawns it: on
// Windows via CreateProcessAsUser into the console session, on macOS via
// launchctl asuser into the Aqua session.
var vncAgentCmd = &cobra.Command{
Use: "vnc-agent",
Short: "Run VNC capture agent (internal, spawned by service)",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
log.SetReportCaller(true)
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(os.Stderr)

if vncAgentSocket == "" {
return fmt.Errorf("--socket is required")
}

token := os.Getenv("NB_VNC_AGENT_TOKEN")
if token == "" {
return fmt.Errorf("NB_VNC_AGENT_TOKEN not set; agent requires a token from the service")
}
// Purge the token from env so it doesn't leak via /proc/<pid>/environ.
if err := os.Unsetenv("NB_VNC_AGENT_TOKEN"); err != nil {
log.Debugf("unset NB_VNC_AGENT_TOKEN: %v", err)
}

// Drop root privileges to the target console user BEFORE creating
// the listening socket: keeps a post-auth bug in the encoder /
// input / capture paths confined to the user's own privileges
// rather than escalating to host root, and makes the daemon's
// LOCAL_PEERCRED check see the right uid. No-op on Windows, where
// both processes run as SYSTEM.
//
// Called unconditionally: a missing or zero --target-uid is exactly
// the case the Darwin implementation refuses, and skipping the call
// for it would leave the agent running as root instead.
if err := dropAgentPrivileges(vncAgentTargetUID); err != nil {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return fmt.Errorf("drop privileges to uid %d: %w", vncAgentTargetUID, err)
}

if err := os.Remove(vncAgentSocket); err != nil && !os.IsNotExist(err) {
log.Debugf("remove stale socket %s: %v", vncAgentSocket, err)
}
ln, err := net.Listen("unix", vncAgentSocket)
if err != nil {
return fmt.Errorf("listen on %s: %w", vncAgentSocket, err)
}
if err := os.Chmod(vncAgentSocket, 0o600); err != nil {
log.Debugf("chmod %s: %v", vncAgentSocket, err)
}

ctx := cmd.Context()

capturer, injector, err := newAgentResources()
if err != nil {
_ = ln.Close()
return err
}
srv := vncserver.New(vncserver.Config{
Capturer: capturer,
Injector: injector,
DisableAuth: true,
AgentTokenHex: token,
Listener: ln,
})

if err := srv.Start(ctx, netip.AddrPort{}, netip.Prefix{}); err != nil {
return fmt.Errorf("start vnc server: %w", err)
}
log.Infof("vnc-agent listening on %s, ready", vncAgentSocket)

<-ctx.Done()
log.Info("vnc-agent context cancelled, shutting down")
return srv.Stop()
},
SilenceUsage: true,
}
25 changes: 25 additions & 0 deletions client/cmd/vnc_agent_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build darwin && !ios

package cmd

import (
"fmt"

vncserver "github.com/netbirdio/netbird/client/vnc/server"
)

func newAgentResources() (vncserver.ScreenCapturer, vncserver.InputInjector, error) {
// Ask for Screen Recording here and nowhere else. This process runs as the
// console user, which TCC requires for a user-scope service, and it is fresh
// per connection, which is what makes the dialog appear at all: TCC shows it
// once per process. The request blocks until the user answers, so it also
// keeps the Accessibility ask that follows the first input out of its way.
vncserver.RequestScreenRecording()

capturer := vncserver.NewMacPoller()
injector, err := vncserver.NewMacInputInjector()
if err != nil {
return nil, nil, fmt.Errorf("macOS input injector: %w", err)
}
return capturer, injector, nil
}
77 changes: 77 additions & 0 deletions client/cmd/vnc_agent_dropprivs_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//go:build darwin && !ios

package cmd

import (
"fmt"
"os"
"os/user"
"strconv"
"syscall"
)

// dropAgentPrivileges drops the vnc-agent process from root (its
// launchctl-asuser-inherited starting uid) to the target console user
// before any other initialisation runs. Without this the agent runs as
// root for the lifetime of the session; any post-auth memory-safety
// issue in the capture/input/encode paths would then be a root-level
// RCE on the host instead of a user-level one. Also makes the daemon's
// LOCAL_PEERCRED check correctly identify the agent as the console user,
// not as root.
//
// Returns an error when the agent is running as a non-root uid that
// differs from targetUID: non-root can only setuid to itself, so a
// mismatch here means the spawn went to the wrong session.
func dropAgentPrivileges(targetUID uint32) error {
if targetUID == 0 {
return fmt.Errorf("refusing to keep agent running as root (target uid 0)")
}
cur := uint32(os.Getuid())
if cur == targetUID {
return nil
}
if cur != 0 {
return fmt.Errorf("agent uid %d does not match expected %d and we lack root to fix it", cur, targetUID)
}
// Resolve the target user's real primary group rather than reusing
// targetUID as the gid: a user's primary group on macOS is typically
// staff(20), not gid==uid. Fail closed if the lookup fails.
targetGID, err := primaryGroupID(targetUID)
if err != nil {
return err
}
// Drop supplementary groups first: setgid alone doesn't touch the
// auxiliary group list, leaving root's groups attached would let the
// dropped process write to root-only group-writable files.
if err := syscall.Setgroups([]int{}); err != nil {
return fmt.Errorf("setgroups([]): %w", err)
}
if err := syscall.Setgid(targetGID); err != nil {
return fmt.Errorf("setgid(%d): %w", targetGID, err)
}
if os.Getgid() != targetGID || os.Getegid() != targetGID {
return fmt.Errorf("setgid verification: gid=%d egid=%d, expected %d", os.Getgid(), os.Getegid(), targetGID)
}
if err := syscall.Setuid(int(targetUID)); err != nil {
return fmt.Errorf("setuid(%d): %w", targetUID, err)
}
if uint32(os.Getuid()) != targetUID || uint32(os.Geteuid()) != targetUID {
return fmt.Errorf("setuid verification: uid=%d euid=%d, expected %d", os.Getuid(), os.Geteuid(), targetUID)
}
return nil
}

// primaryGroupID resolves the real primary group id of the user with the
// given uid. Fails closed: a lookup or parse error returns an error so the
// caller never falls back to using uid as the gid.
func primaryGroupID(targetUID uint32) (int, error) {
u, err := user.LookupId(strconv.Itoa(int(targetUID)))
if err != nil {
return 0, fmt.Errorf("look up uid %d: %w", targetUID, err)
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
return 0, fmt.Errorf("parse gid %q for uid %d: %w", u.Gid, targetUID, err)
}
return gid, nil
}
Loading
Loading