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()returnsEHOSTUNREACH→ "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 upgradechanges 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.