Skip to content
C.W.K.
Stream
Lesson 06 of 06 · published

The Local Network Gate

~11 min · war-story, permissions, launch-context, networking

Level 0Empty Shelf
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The service is healthy, its health check is green, and every call it makes to the other machine fails. Same binary, same address, works instantly from your shell."

A Third Time, the Same Shape

You've now seen this twice. A synced path that resolves instantly in your shell and hangs under the service manager. A permission the operating system grants your interactive session and denies the same interpreter when the service manager launches it. Here is the third, and it is worth a lesson of its own because it is the one that looks least like an infrastructure problem.

Recall's control plane dials the other machine for some of its derived work. One day those calls started failing — every one of them, immediately, with "no route to host." The obvious conclusion is that the other machine is down. It isn't. Its health check answers. You SSH over and run the exact same call, from the exact same Python, against the exact same address, and it returns in a fraction of a second. Re-run the service and it fails again, identically, every time.

That determinism is the tell, and it's the opposite of what your instincts say. A network that is genuinely flaky fails intermittently. A verdict that is fixed — the same answer every single run, from one launch context and not the other — is not the network. It's a policy decision being made about your process before the packet ever leaves.

What Modern macOS Actually Gates

Recent macOS treats talking to other machines on your own local network as a privacy-relevant capability, the same way it treats the camera. Apple's technote spells out the rules, and two of them do all the damage here:

  • Who is auto-allowed. Daemons started by the service manager are allowed. Programs running as root are allowed. Command-line tools you run from Terminal or over SSH are allowed, and so is anything they spawn. That last clause is exactly why your by-hand test passes.
  • The exception you were counting on doesn't cover you. The auto-allow for launch daemons explicitly does not extend to launch agents — and a per-user background service, the ordinary way to run something that belongs to a logged-in account, is an agent. The category that fails is the category almost everyone actually ships.

And the grant doesn't attach to your code. The system tracks down the responsible program and records the user's choice against that — the identical model as the file-access grant in the previous lesson, in a different subsystem. A bundled app with a stable identity gets one durable row that a person can recognize and approve. A bare interpreter does not: every environment you build mints another anonymous entry, and rebuilding one mints a fresh one that has to be approved all over again. You cannot manage a list you cannot read.

The Fix Is to Stop Being on the Local Network

Here is where this war story diverges from the last one. With file access, the answer was to own a stable identity — a signed launcher the grant could live on. That worked because there was exactly one identity to own. Here there isn't: the list grows without bound and every row looks the same, so no amount of clicking ever converges.

So the fix is to leave the gated network entirely. The technote's own definition is the escape hatch: a local network is one associated with a broadcast-capable interface — Wi-Fi and Ethernet — and it explicitly excludes VPN interfaces. Traffic over an encrypted overlay network between your own machines is therefore not a local network operation at all, and never meets the gate. Recall's inter-machine calls now address peers by their overlay names rather than by a name that ends in .local or an address on the wire in the next room. The same call, over a different route, stops being the kind of call that needs permission.

The same definition explains the symptoms that seemed unrelated. Resolving a name ending in .local requires local network access on its own, so mDNS names fail before a connection is even attempted. A direct point-to-point link between two machines in the same room is a broadcast-capable interface like any other, so it is gated exactly as hard as the house network — which feels absurd until you remember the gate is about who is being talked to, not how far away they are.

The Diagnostic, and Why You Need One

This failure is exceptionally good at sending you to the wrong machine. The remote service stays healthy, so the dashboard is green. Only the code paths that dial out break, so the symptom looks like missing data rather than a network fault. The client library reports it as a connection error, which reads as "the server is down." Every instinct points away from the truth.

So learn the one-step test, and reach for it whenever a background service and your shell disagree: run the same binary under the service manager and from a shell, and compare. Two different answers from one binary against one address is not a network condition. It is a launch-context verdict, and this whole track has been three variations on that single sentence.

Code

Who is allowed to talk to the machine next door·text
AUTO-ALLOWED (why your by-hand test always passes):
  - launch DAEMONS
  - anything running as root
  - command-line tools run from Terminal or over SSH,
    INCLUDING every child process they spawn

NOT COVERED (what you actually ship):
  - launch AGENTS  <- the per-user background service
    the daemon exception explicitly does not extend to agents

GATED (a "local network" = broadcast-capable interface):
  - the house LAN                     -> needs permission
  - a direct machine-to-machine link  -> needs permission
  - resolving any *.local name        -> needs permission

NOT GATED (the way out):
  - VPN / overlay interfaces are excluded BY DEFINITION
  - so peer-to-peer over the private overlay never meets
    the gate -- same call, different route, no policy check

SYMPTOM: "no route to host", instantly, EVERY run, from the
service -- and a sub-second success from your shell.
The one-step diagnostic·text
# Don't debug the remote machine. Its health check is green
# and it is telling the truth.

# Run the SAME binary, against the SAME address, two ways:
#
#   1. from your interactive shell        -> works
#   2. under the service manager           -> fails, identically,
#                                             every single time
#
# Two different answers from one binary is not a network
# condition. It is a launch-context verdict, decided about
# your process before a packet leaves the host.

# The determinism IS the evidence:
#   flaky network  -> intermittent failure
#   fixed verdict  -> policy, every time, same answer

External links

Exercise

Find a background service in your own setup that calls another machine — a scheduled job, a daemon, a worker. Run its exact command twice: once from your interactive shell, once the way the service manager actually launches it. If the answers differ, you've found a launch-context verdict, not a network problem. Then ask the design question: is the permission attached to an identity you can own and keep stable, or to something that gets rebuilt? If it's the second, find the route that doesn't need the permission.
Hint
Two probes. (1) Reproduce the failure the way it actually happens — under the service manager, not in the shell where you find it convenient to test. The gap between those two environments is where this entire class of bug lives. (2) When you do find a permissions gate, count the identities: one stable identity you control is manageable, an unbounded list of lookalike entries is not, and that count should decide whether you grant or reroute.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.