Pakkit.net
← Back to blog

Engineering Practice

Teach Your Scripts Where They're Running

If a script needs to behave differently depending on the network it's on, it should detect its environment from ranked, reliable signals — not from the most convenient value, which is usually the one that lies.

  • Engineering Practice
  • Networking
  • Automation
  • Scripting

I keep a laptop that lives in two worlds. Sometimes it’s on a full-tunnel VPN with a TLS-inspecting proxy in front of everything; sometimes it’s on my local network with a direct path out. A few of my scripts need to behave differently depending on which world they’re in — which proxy to trust, which hosts are reachable, whether to even try a certain request. The naive version of “know where you are” is a flag I set by hand and forget to flip. The honest version is a detector that figures it out from the system itself. Building that detector taught me more about reliable signals than about networking.

Detection beats assumption beats a manual flag

The worst option is a hand-set flag (ENV=vpn) because it’s a second source of truth that drifts the moment you forget it — and you will forget it. The next-worst is assuming based on something incidental (“if it’s a weekday I’m probably on the VPN”). The right move is to read the actual state of the machine and decide from that. A script that detects its environment can’t get out of sync with reality, because reality is its input.

But “read the actual state” has a catch, which is the whole point of this post: not every observable value is equally truthful.

The convenient signal is usually the one that lies

My first detector checked the obvious thing: what IP address does my main network interface have? It was wrong constantly, and the reason is a great lesson in itself.

A full-tunnel VPN doesn’t change your interface’s address. The interface keeps its local IP — but the VPN installs a route that pulls all the traffic up through the tunnel anyway. So the interface IP says “I’m on the local network” while every packet is actually going through the tunnel. The convenient value (the address sitting right there on the interface) is exactly the one that misleads you.

Check the state you actually care about, not the value that’s easiest to read. They’re often not the same value.

The thing I actually cared about was “where does my traffic go,” and the truthful signal for that is the effective default route — which interface the system would send a packet out of right now. Read that, and the tunnel stops hiding. The general habit: when a proxy variable disagrees with the thing it’s supposed to represent, stop trusting the proxy and measure the real thing.

Rank your signals and let the strong ones win

No single check is perfect, so I stopped looking for one and instead ranked several by how hard they are to fake, then combined them:

  • Strongest: the effective route topology — is traffic actually going up a tunnel interface? That’s structural and hard to spoof accidentally.
  • Strong and independent: is a public site’s TLS certificate being re-signed by an inspecting proxy’s internal CA instead of the site’s real one? If interception is happening, that’s near-definitive about which network egress I’m on — and it’s a completely different mechanism than the route check, so the two confirm each other.
  • Weaker, used only as a tiebreaker: the local address range, which only means anything once I’ve already ruled out the tunnel.

Combining independent signals is what makes a detector trustworthy. Two checks that rely on the same underlying fact aren’t two checks. Two that come at the answer from genuinely different angles — routing and TLS interception — give you confidence even when one is noisy.

Absence is a signal too

The TLS check has a quietly useful property: its absence is informative. If a public site comes back signed by its real certificate authority, then I’m not behind the inspecting proxy — which is a reliable “I’m not on that network” even though nothing positive fired. I almost discarded that because it felt like “no result.” It isn’t. A check that confidently tells you a state is not present is worth as much as one that confirms a state is.

Match patterns, not snapshots

An early version hard-coded specifics I’d observed once: a particular tunnel interface name, a particular assigned address. Both were wrong on the next run, because they rotate — the VPN hands out a different address each session and the interface name isn’t stable across reconnects. So the rule became: match the pattern (a tunnel-style interface holding an address in the VPN’s range), never the exact instance I happened to see today. Detectors built on a single observed snapshot are brittle by construction; detectors built on the shape of the thing survive.

Your tools can betray you silently

The bug that cost me the most had nothing to do with networking. I’d bounded a network probe with a timeout wrapper to keep it from hanging — a totally reasonable instinct. But timeout isn’t part of the stock macOS toolset; it’s a GNU utility that may simply not be there. When it’s missing, the shell returns “command not found,” my probe produced empty output, and the detector quietly read that empty result as “no interception detected” — confidently giving the wrong answer with zero errors on screen.

That’s the most dangerous class of bug: not a crash, but a missing tool turning into a wrong answer that looks like a right one. The lesson generalizes hard: a script that runs in more than one environment can’t assume its tools exist everywhere any more than it can assume its network is the same everywhere. Check that a dependency is present before you trust its output, and make the detector explain its reasoning (a verbose mode that prints which signals fired) so a wrong call is debuggable instead of mysterious.

That last part — make the decision legible — is the same instinct behind why I think automation needs a dry-run mode: a tool that can show its work is a tool you can actually trust to act on its own. If you’ve built environment detection that survived contact with reality, I’d enjoy comparing notes.