mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 00:51:28 +02:00
Ask for Screen Recording from the agent and Accessibility on first input
This commit is contained in:
@@ -9,6 +9,13 @@ import (
|
||||
)
|
||||
|
||||
func newAgentResources() (vncserver.ScreenCapturer, vncserver.InputInjector, error) {
|
||||
// Ask for Screen Recording here and nowhere else: this process runs as the
|
||||
// console user, which is what TCC requires for a user-scope service, and it
|
||||
// is the point where somebody is demonstrably trying to view the screen.
|
||||
// Granting it also requires the capturing process to restart, which comes
|
||||
// for free since the agent is respawned per session.
|
||||
vncserver.PrimeScreenCapturePermission()
|
||||
|
||||
capturer := vncserver.NewMacPoller()
|
||||
injector, err := vncserver.NewMacInputInjector()
|
||||
if err != nil {
|
||||
|
||||
@@ -12,11 +12,10 @@ import (
|
||||
|
||||
func newPlatformVNC() (vncserver.ScreenCapturer, vncserver.InputInjector, bool) {
|
||||
capturer := vncserver.NewMacPoller()
|
||||
// Prompt for Screen Recording at server-enable time rather than first
|
||||
// client-connect. The native prompt is far easier for users to act on
|
||||
// in the moment they toggled VNC on than later when "the screen looks
|
||||
// like wallpaper" would otherwise be the only clue.
|
||||
vncserver.PrimeScreenCapturePermission()
|
||||
// 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.
|
||||
injector, err := vncserver.NewMacInputInjector()
|
||||
if err != nil {
|
||||
log.Debugf("VNC: macOS input injector: %v", err)
|
||||
|
||||
@@ -148,6 +148,18 @@ func notifyScreenRecordingMissing() {
|
||||
}
|
||||
|
||||
// NewCGCapturer creates a screen capturer for the main display.
|
||||
// screenCaptureWorking records that a real capture succeeded, which is the only
|
||||
// trustworthy signal that Screen Recording is granted: CGPreflight lies on
|
||||
// Sequoia. The input side waits for this before asking for Accessibility, so the
|
||||
// two permission panes never compete (macOS shows one at a time, and losing the
|
||||
// Screen Recording pane is the worse outcome: without it there is no picture).
|
||||
var screenCaptureWorking atomic.Bool
|
||||
|
||||
// ScreenCaptureWorking reports whether a capture has succeeded in this process.
|
||||
func ScreenCaptureWorking() bool {
|
||||
return screenCaptureWorking.Load()
|
||||
}
|
||||
|
||||
func NewCGCapturer() (*CGCapturer, error) {
|
||||
initDarwinCapture()
|
||||
if !darwinCaptureReady {
|
||||
@@ -162,6 +174,7 @@ func NewCGCapturer() (*CGCapturer, error) {
|
||||
notifyScreenRecordingMissing()
|
||||
return nil, fmt.Errorf("probe capture: %w", err)
|
||||
}
|
||||
screenCaptureWorking.Store(true)
|
||||
nativeW := img.Rect.Dx()
|
||||
nativeH := img.Rect.Dy()
|
||||
c.hasHash = false
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -301,6 +302,9 @@ type MacInputInjector struct {
|
||||
// field on each posted event, not from event timing.
|
||||
clickCount [5]int64
|
||||
clickAt [5]time.Time
|
||||
// axAsked is set once the Accessibility request has been made, so the
|
||||
// per-event check is one atomic load.
|
||||
axAsked atomic.Bool
|
||||
}
|
||||
|
||||
// NewMacInputInjector creates a macOS input injector.
|
||||
@@ -309,7 +313,7 @@ func NewMacInputInjector() (*MacInputInjector, error) {
|
||||
if !darwinInputReady {
|
||||
return nil, fmt.Errorf("CoreGraphics not available for input injection")
|
||||
}
|
||||
checkMacPermissions()
|
||||
logAccessibilityStatus()
|
||||
|
||||
m := &MacInputInjector{}
|
||||
if path, err := exec.LookPath("pbcopy"); err == nil {
|
||||
@@ -328,20 +332,48 @@ func NewMacInputInjector() (*MacInputInjector, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// checkMacPermissions probes Accessibility access. Prefers the prompting
|
||||
// variant of AXIsProcessTrusted: when the process is not yet trusted,
|
||||
// macOS shows its native "would like to control your computer" dialog
|
||||
// with an "Open System Settings" button. The silent variant is the
|
||||
// fallback when the prompting symbol or its CF dictionary plumbing
|
||||
// couldn't be loaded.
|
||||
func checkMacPermissions() {
|
||||
if !axProcessIsTrusted() {
|
||||
log.Warn("Accessibility permission not granted. Input injection will not work. " +
|
||||
"Approve the prompt or grant in System Settings > Privacy & Security > Accessibility.")
|
||||
openPrivacyPane("Privacy_Accessibility")
|
||||
// logAccessibilityStatus reports Accessibility state without prompting. Asking
|
||||
// here would put the Accessibility pane on screen the moment a session starts,
|
||||
// on top of the Screen Recording request, and macOS shows only one pane at a
|
||||
// time: the Accessibility one wins and the more important request is buried.
|
||||
// The ask happens on the first input instead, see ensureAccessibility.
|
||||
func logAccessibilityStatus() {
|
||||
if axIsProcessTrusted != nil && !axIsProcessTrusted() {
|
||||
log.Info("Accessibility permission not granted yet; asking on the first input event")
|
||||
}
|
||||
}
|
||||
|
||||
// ensureAccessibility asks for Accessibility at most once per process, on the
|
||||
// first input that is actually delivered. Injection happens per event, so the
|
||||
// common path has to be a single atomic load.
|
||||
func (m *MacInputInjector) ensureAccessibility() {
|
||||
if m.axAsked.Load() {
|
||||
return
|
||||
}
|
||||
m.askAccessibility()
|
||||
}
|
||||
|
||||
// askAccessibility is the cold path of ensureAccessibility.
|
||||
//
|
||||
// It waits for a capture to have succeeded before prompting: Screen Recording is
|
||||
// the permission a session cannot do without, and requesting Accessibility while
|
||||
// that pane is open replaces it. A session that never captures never gets here,
|
||||
// which is the right outcome, since input on a black screen is not useful.
|
||||
func (m *MacInputInjector) askAccessibility() {
|
||||
if !ScreenCaptureWorking() {
|
||||
return
|
||||
}
|
||||
if !m.axAsked.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
if axProcessIsTrusted() {
|
||||
return
|
||||
}
|
||||
log.Warn("Accessibility permission not granted. Input injection will not work. " +
|
||||
"Approve the prompt or grant in System Settings > Privacy & Security > Accessibility.")
|
||||
openPrivacyPane("Privacy_Accessibility")
|
||||
}
|
||||
|
||||
// axProcessIsTrusted asks macOS whether netbird has Accessibility access,
|
||||
// and triggers the native prompt the first time when not trusted. Returns
|
||||
// the current trust status either way.
|
||||
@@ -384,6 +416,7 @@ func openPrivacyPane(pane string) {
|
||||
|
||||
// InjectKey simulates a key press or release.
|
||||
func (m *MacInputInjector) InjectKey(keysym uint32, down bool) {
|
||||
m.ensureAccessibility()
|
||||
wakeDisplay()
|
||||
src := ensureEventSource()
|
||||
if src == 0 {
|
||||
@@ -401,6 +434,7 @@ func (m *MacInputInjector) InjectKey(keysym uint32, down bool) {
|
||||
// entirely different scheme from PC AT scancodes, so the table is the
|
||||
// authoritative bridge. On miss we fall back to the keysym path.
|
||||
func (m *MacInputInjector) InjectKeyScancode(scancode, keysym uint32, down bool) {
|
||||
m.ensureAccessibility()
|
||||
wakeDisplay()
|
||||
src := ensureEventSource()
|
||||
if src == 0 {
|
||||
@@ -491,6 +525,7 @@ func isFnShiftedKeycode(keycode uint16) bool {
|
||||
|
||||
// InjectPointer simulates mouse movement and button events.
|
||||
func (m *MacInputInjector) InjectPointer(buttonMask uint16, px, py, serverW, serverH int) {
|
||||
m.ensureAccessibility()
|
||||
wakeDisplay()
|
||||
if serverW == 0 || serverH == 0 {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user