Stop a failed up from tearing down a working tunnel
The previous commit moved `down` to the top of `up` to fix a resolv.conf idempotency bug, and in doing so put it *before* every network fetch that can fail. Re-review caught it and I reproduced it: with a tunnel up, an `up` that fails on bad credentials left `pia0` gone and traffic silently back on the real address, while the error talked only about credentials. A privacy regression introduced by a correctness fix. `down` now runs after the token, server list and key registration have all succeeded — nothing above that line touches the network stack — and still clears the stale backup it was added for. Everything after it is covered by a rollback. Note this is an EXIT trap with a flag, not `trap ... ERR`: my first attempt used ERR and did not fire at all, because ERR is not inherited by shell functions without `set -E`, so a failure inside add_route missed it, and `die` exits explicitly, which is not an error. Verified by forcing a route collision — the tunnel is torn down and DNS is intact, where before the fix it was left half-configured with DNS dead. Also from the review: - **The private key lived on disk for the whole life of the tunnel.** `commit_container_snapshot` blanks env vars, never files, and nothing tears the tunnel down before a recreate or migrate — so the `down`-time cleanup never covered the path that put a key in a snapshot in the first place. It is now deleted the moment `wg set` has read it; the kernel keeps its own copy, verified by checking the interface still works afterwards. - **`up` claimed success without a handshake.** An unreachable peer still routes — into a black hole — so `up --full` could exit 0 having pointed all traffic and resolv.conf at a peer that never answered, with `status` printing "mode: full tunnel". Now polls for a handshake and rolls back if none arrives. - **`status` needed root and did not check.** `wg show` fails unprivileged and was swallowed, so an unprivileged run printed "no tunnel up" and then "mode: full tunnel" in the same breath. An agent reading the first line would re-run `up` — which, before the fix above, destroyed the tunnel it failed to see. - **The killswitch bullet was false.** It said `iptables` is not in the image; it is, so a killswitch is buildable. It stays unbuilt because it would cut Claude Code's own API traffic — an honest reason, unlike the previous one. - `install_feature_skill` rejects path-traversal names, not just blank ones — verified `../skills` would have deleted the whole skills directory including Mission Control's — and reports `mkdir`/`cp` failures instead of printing a success line regardless. - The usage text ended by printing `set -euo pipefail`, off by one line. - HOW-TO-USE claimed "the container says so on start". entrypoint prints to PID 1's stdout, which no terminal or UI surfaces — `docker logs` appears nowhere in the repo. Now points at the migration pre-flight, which does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -183,22 +183,34 @@ true in test mode too, and means much less than it sounds like.
|
||||
`down` restores `resolv.conf` from its backup (only if that backup still looks
|
||||
like a resolver file — restoring a truncated one would leave the container with
|
||||
no DNS at all), removes exactly the routes that were added, in reverse order,
|
||||
deletes the interface, and shreds the WireGuard private key. It is safe to run
|
||||
when nothing is up, and `up` runs it first so a repeat `up` cannot stack state.
|
||||
Confirm afterwards that the public address is back to the container's own.
|
||||
and deletes the interface. It is safe to run when nothing is up. Confirm
|
||||
afterwards that the public address is back to the container's own.
|
||||
|
||||
The key deletion is not housekeeping: `/run` is in the container's writable
|
||||
layer, so `docker commit` bakes whatever is there into the project's snapshot
|
||||
image. A key left behind rides that image into every future container.
|
||||
`up` calls it too, but only *after* every network fetch has succeeded, so a
|
||||
failed `up` leaves an existing tunnel alone rather than tearing it down to
|
||||
report a bad password. From that point on a rollback is armed: if any step of
|
||||
the setup fails, the tunnel is torn down rather than left half-configured.
|
||||
|
||||
The private key is deleted earlier still — the moment `wg set` has read it,
|
||||
while the tunnel is being built. That is not housekeeping: `/run` is in the
|
||||
container's writable layer, and recreating or migrating the project runs
|
||||
`docker commit` over it *without* tearing the tunnel down first. A key that
|
||||
lived for the tunnel's lifetime would be baked into the snapshot image and
|
||||
copied forward from then on. The kernel keeps its own copy, so nothing is lost.
|
||||
|
||||
## What this deliberately does not do
|
||||
|
||||
- **No killswitch.** Blocking non-tunnel egress needs `iptables`, which is not
|
||||
in the image, and would cut Claude Code's API traffic whenever the tunnel is
|
||||
down. If the user needs guaranteed egress rather than convenient egress, say
|
||||
so plainly rather than improvising one — it is a real design decision.
|
||||
- **No killswitch.** `iptables` *is* in the image, so one is buildable — this
|
||||
is a deliberate omission, not a missing dependency. Blocking non-tunnel egress
|
||||
cuts Claude Code's own API traffic the moment the tunnel drops, which ends the
|
||||
session that would otherwise fix it. If the user needs guaranteed egress
|
||||
rather than convenient egress, say so plainly and let them decide, rather than
|
||||
improvising one.
|
||||
- **No autostart.** There is no service manager in the container and Triple-C
|
||||
has no start hook, so nothing can re-establish the tunnel automatically.
|
||||
has no start hook, so nothing re-establishes the tunnel on its own. `cron` is
|
||||
in the image and `triple-c-scheduler` runs on it, so a scheduled reconnect is
|
||||
possible if the user wants one — it is just not set up, and a tunnel that
|
||||
reconnects unattended deserves an explicit decision.
|
||||
- **Not PIA's desktop client.** `pia-daemon` and `piactl` are installable but
|
||||
cannot work headless: the daemon never accepts a client connection without
|
||||
the GUI, and `piactl --help` states that connecting requires it. If you find
|
||||
|
||||
Reference in New Issue
Block a user