hardware/mac-studio/local-network-privacy.md

Mac Studio — Local Network Privacy (daemons denied LAN access by default)

Machine-wide gotcha. Bit us hard once (nginx, ~6h of debugging); will bite again.

macOS 15+ (this box is on 26.x) gates outbound connections to RFC1918 addresses per-process, in the kernel. When a process isn't granted:

  • connect() returns EHOSTUNREACH → "No route to host"
  • zero packets hit the wire (confirmed via tcpdump — no SYN, nothing)
  • nothing is logged, at any level. No os_log entry, no TCC denial record. Total silence.

Grants attach to the responsible process, not the binary you typed:

Process Responsible app LAN access
curl from a terminal Terminal/iTerm (prompted once, allowed) ✅ inherited
Chrome Chrome (prompted, allowed) ✅
Apple platform binaries (/usr/bin/curl etc.) as a daemon — ✅ exempt
Any third-party LaunchDaemon itself ❌ denied

A LaunchDaemon has no GUI session, so macOS can never show it the prompt → denied by default, permanently, silently. Running the same binary once from Terminal does not fix the daemon — that grant goes to Terminal, not to the daemon.

Fix: System Settings → Privacy & Security → Local Network → enable the process. Takes effect immediately, no restart needed. Daemons do get auto-registered in that list without ever prompting — they just sit there defaulted to off.

Diagnostic that isolates it — run the same binary from an interactive shell vs. via launchd. If shell works and daemon doesn't, it's this, and the binary/config/firewall/DNS are all innocent:

# generic shape: minimal instance of the daemon, run in the foreground from a shell
<binary> <minimal-config-pointing-at-the-LAN-target>   # -> works
# vs. the launchd-managed one                          # -> EHOSTUNREACH

Gotchas:

  • Homebrew binaries are only ad-hoc signed, with a per-build-unique identifier. A brew upgrade changes the signature and may drop the grant — if a daemon's LAN access breaks right after an upgrade, check this pane first.
  • Symptoms mimic a firewall/VPN/content-filter problem, which sends you chasing pf rules, AdGuard, Little Snitch, etc. We burned a long session blaming AdGuard's network extension. It was never AdGuard.
  • Look at the pane before theorizing about the pane. The entry was there the whole time, just switched off.

Workarounds if the toggle is ever unavailable: run the service as a LaunchAgent in the GUI session (promptable context; use a high port + pf redirect if it needs 80/443), put it in a container (OrbStack/Docker egress is attributed to the VM's vmnet interface), or move the LAN hop to a Linux box.