From 65bb3ae4f5e6770f8d5f87e233a467a35a2a86c1 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 22 Sep 2026 20:17:23 +0200 Subject: [PATCH] Initialize agent capture and input before publishing its socket, and request Screen Recording in direct macOS mode --- client/cmd/vnc_agent.go | 16 +++++++++++----- client/internal/engine_vnc_darwin.go | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/client/cmd/vnc_agent.go b/client/cmd/vnc_agent.go index 905870f58..e7dcc3b4b 100644 --- a/client/cmd/vnc_agent.go +++ b/client/cmd/vnc_agent.go @@ -65,6 +65,17 @@ var vncAgentCmd = &cobra.Command{ return fmt.Errorf("drop privileges to uid %d: %w", vncAgentTargetUID, err) } + // Before the socket exists, not after: the daemon treats the socket + // appearing as "this agent is ready to serve". Building the capturer + // and injector can take a while and can fail — on macOS it raises the + // Screen Recording prompt and waits on the user — so binding first + // publishes an agent that is not serving yet, and the first connection + // stalls or fails against it. + capturer, injector, err := newAgentResources() + if err != nil { + return err + } + if err := os.Remove(vncAgentSocket); err != nil && !os.IsNotExist(err) { log.Debugf("remove stale socket %s: %v", vncAgentSocket, err) } @@ -78,11 +89,6 @@ var vncAgentCmd = &cobra.Command{ ctx := cmd.Context() - capturer, injector, err := newAgentResources() - if err != nil { - _ = ln.Close() - return err - } srv := vncserver.New(vncserver.Config{ Capturer: capturer, Injector: injector, diff --git a/client/internal/engine_vnc_darwin.go b/client/internal/engine_vnc_darwin.go index 58635a6f3..39db0e1bb 100644 --- a/client/internal/engine_vnc_darwin.go +++ b/client/internal/engine_vnc_darwin.go @@ -12,10 +12,20 @@ import ( func newPlatformVNC() (vncserver.ScreenCapturer, vncserver.InputInjector, bool) { capturer := vncserver.NewMacPoller() - // No permission request here. Screen Recording is a user-scope TCC service, - // so a request from this process is dropped when it runs as a LaunchDaemon: - // no prompt appears and NetBird never even shows up in the Screen Recording - // list. The per-user agent asks instead, see newAgentResources. + + // Ask only when this process is the one that will capture. Screen Recording + // is a user-scope TCC service, so the request is dropped from a + // LaunchDaemon: no prompt appears and NetBird never even reaches the Screen + // Recording list. In that case the per-user agent asks instead, see + // newAgentResources. + // + // Without service mode there is no agent, so this process captures and + // nothing else will ever raise the prompt — the client would serve a + // windowless desktop with no indication why. + if !vncNeedsServiceMode() { + vncserver.RequestScreenRecording() + } + injector, err := vncserver.NewMacInputInjector() if err != nil { log.Debugf("VNC: macOS input injector: %v", err)