Chapter 02 · 15 minutes
Catch the phone-homes
Apps call home in quiet ways. Find every one, at run time and by reading the code, and give the island an offline switch.
Goal
Measure what a hidden call costs on the island, turn the calls off with one switch, and find the calls the guard cannot see.
Silent calls cost seconds
Parrot's telemetry and update check swallow their errors, so they never break anything. On the mainland they fail fast. On an island they often do not: many firewalls drop packets instead of refusing them, so every call waits for its timeout.
--drop 3 makes the guard wait three seconds before failing, like a firewall that drops:
uv run python -m ferry guard --drop 3 parrot --selftest
BLOCKED telemetry.parrot.example:443 from parrot/telemetry.py:18
BLOCKED updates.parrot.example:443 from parrot/updates.py:15
parrot 1.0.0 from .../air-gap-demo/lab/parrot
ready in 6.0 s on http://127.0.0.1:8600 offline=False
Six seconds to start, from two calls nobody remembers writing. With longer timeouts, retries, or a health check that gives up after five seconds, this is how "it works everywhere except at the customer" begins.
One switch for the island
Parrot reads PARROT_OFFLINE in lab/parrot/config.py, and telemetry and the update check skip their work when it is 1:
PARROT_OFFLINE=1 uv run python -m ferry guard parrot --selftest
In PowerShell, set it first with $env:PARROT_OFFLINE = "1", and clear it afterwards with Remove-Item Env:PARROT_OFFLINE.
parrot 1.0.0 from .../air-gap-demo/lab/parrot
ready in 0.0 s on http://127.0.0.1:8600 offline=True
selftest: parrot says 'I only talk to localhost now.'
gap guard: nothing tried to leave
Clean. Or is it? The guard only sees code that runs. Ask for a mood Parrot does not have, so the error path runs:
PARROT_OFFLINE=1 uv run python -m ferry guard parrot --selftest --mood seasick
parrot 1.0.0 from .../air-gap-demo/lab/parrot
ready in 0.0 s on http://127.0.0.1:8600 offline=True
BLOCKED errors.parrot.example:443 from parrot/crash.py:15
selftest: 500 parrot has no such mood
gap guard: 1 call tried to leave
The crash reporter has no offline switch. It only calls home when something goes wrong, which is exactly when nobody is watching.
Read the code as well
ferry scan lists every URL in a folder that points off the machine:
uv run python -m ferry scan
code errors.parrot.example parrot/crash.py:9
browser fonts.parrot-cdn.example parrot/static/index.html:7
browser cdn.parrot-cdn.example parrot/static/index.html:9
code telemetry.parrot.example parrot/telemetry.py:9
code updates.parrot.example parrot/updates.py:8
5 outbound URLs. 2 load in the browser, where the guard can't see.
The two browser rows are the answer to chapter 01's exercise. A font and a script load from a CDN in the visitor's browser. On the island, the page loads without them, or hangs waiting for them.
The two tools work together. The guard proves what runs. The scan finds what could run. Neither is enough alone.
Check it works
--drop 3makes Parrot take about 6 seconds to start;PARROT_OFFLINE=1brings it back to 0.- You found all five outbound URLs and know which two the guard cannot see.
Exercise
Make Parrot fully island-safe:
- In
lab/parrot/crash.py, skip the report whenconfig.OFFLINEis set. Run theseasickcommand again: the guard should say nothing tried to leave. - In
lab/parrot/static/index.html, load the confetti from/static/confetti.js, which already ships with Parrot, and remove the font link. Runferry scanagain: only the three switched-off calls in Python should remain.
More to try
- Flip the switch around: offline by default, online only when
PARROT_ONLINE=1. Which default would your island customer prefer? - Run
uv run python -m ferry scan path/to/your/project. How many findings are real?