WIP: Ship the tools the VPN toggle grants capability for #28

Draft
jknapp wants to merge 4 commits from feat/vpn-tooling into main
4 Commits
Author SHA1 Message Date
shadow-testandClaude Opus 5 5dd1ab5217 Stop resting the iptables case on a kernel config I cannot verify
Build App (Preview) / compute-version (pull_request) Successful in 6s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build Container / build-container (pull_request) Successful in 29s
Build App (Preview) / build-macos (pull_request) Successful in 2m39s
Build App (Preview) / build-windows (pull_request) Successful in 5m46s
Build App (Preview) / build-linux (pull_request) Successful in 5m59s
Build App (Preview) / prune-previews (pull_request) Successful in 3s
Round 3 argued the macOS rationale is stale: that Docker Desktop no longer
builds from linuxkit/linuxkit and has enabled nft_fib_ipv4 since 4.35. I could
not confirm or refute that from a Linux host — searching turned up no version
matrix either way.

But the decision does not depend on it, and the comment should not have implied
it did. `xt_CONNMARK`, which the iptables path needs, was present in every
kernel config examined. `nft_fib_ipv4`, which the nft path needs, was absent
from the config read here and may be present in current Docker Desktop. That
asymmetry is the actual argument: nftables' viability varies by Docker Desktop
version in a way nobody here can pin down, iptables' requirement did not vary
anywhere it was checked. If nft_fib_ipv4 is present this costs 1.6 MB and
nothing else; if it is absent it is the difference between a working full
tunnel and none.

Rewritten to say that, and to say plainly what is verified versus assumed —
this is the third round in which the previous round's central premise did not
survive, and a confidently-worded paragraph is what the next round inherits.

Also from review:

- CLAUDE.md still said "`iptables` is deliberately absent", the opposite of what
  this PR now does, contradicting the Dockerfile and both other docs.
- The Dockerfile referenced "the pia-vpn skill", which does not exist on this
  branch — the third forward reference of that kind, now gone.
- "full tunnels work on native Linux, Mac and WSL2 6.6" was unconditional and
  contradicted ten lines later by the `DNS =` concession, which stops them on
  every platform. Reordered so the DNS hurdle is named as the first one.
- The WSL2 gap was written as a permanent platform limitation. It is a stale
  install: `wsl --update` moves the host to a current kernel that has the
  symbol. That remedy was missing from the user-facing doc.
- The migration probe label carried an internal comma, which `joinFeatures`
  renders into a comma-joined list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:11:17 -07:00
shadow-testandClaude Opus 5 92d64cf252 Ship iptables, not nftables — nftables forfeits macOS
Build App (Preview) / compute-version (pull_request) Successful in 6s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m36s
Build App (Preview) / build-linux (pull_request) Successful in 6m29s
Build App (Preview) / build-windows (pull_request) Successful in 6m59s
Build App (Preview) / prune-previews (pull_request) Successful in 13s
Build Container / build-container (pull_request) Successful in 11m10s
Re-review overturned the previous commit's package choice, and verifying it
proved the reviewer right.

`wg-quick` picks nft *unconditionally* when it is present (`type -p nft`, line
241), so installing nftables makes the iptables path unreachable. Its nft
ruleset then needs a third expression family the iptables path does not.
Isolating the rules on this host, the two connmark rules install fine and this
is what fails:

    nft add rule ... fib saddr type != local drop
    Error: Could not process rule: No such file or directory

That decides it, because of how the hosts differ. LinuxKit's kernel config —
Docker Desktop for Mac, identical on x86_64 and aarch64:

    CONFIG_NETFILTER_XT_CONNMARK=y      <- the iptables path works
    # CONFIG_NFT_FIB_IPV4 is not set    <- the nft path does not

So nftables would have broken the platform it was added to fix. With iptables,
full tunnels work on native Linux, Docker Desktop for Mac, and WSL2 from 6.6.
Costs 7,203 kB rather than 5,614 kB on amd64.

That also means the mechanism the previous commit documented was wrong: with
nftables installed `xt_CONNMARK` is never consulted, and the real blocker on
that path is `nft_fib_ipv4`. Rewritten around what actually fails.

A second failure neither round had found: `wireguard-tools` only *Suggests*
`openresolv | resolvconf`, so neither is installed, and every provider's stock
config has a `DNS =` line. That fails in `set_dns()` — before any routing — so
it takes split tunnels down too, contradicting what this PR previously claimed:

    [#] resolvconf -a sp -m 0 -x
    /usr/bin/wg-quick: line 32: resolvconf: command not found   EXIT=127

Not fixed, deliberately: `openresolv` has no installation candidate on noble,
and `resolvconf` resolves only by pulling in systemd-resolved — a resolver
daemon and systemd units, into a container with no systemd. Documented instead.

Smaller corrections from the same review:

- the size caveat blamed ~209 kB of libelf1t64; for this package set the real
  over-count is libelf1t64 + netbase. Restated, and arm64 now given against the
  real base rather than left as a bare-ubuntu figure.
- the manual-install fallback omitted `iproute2`, so it left the user without
  `ip` — the command the tunnel needs most.
- "Without it" had been orphaned from its antecedent by inserted paragraphs and
  read as referring to configuring a tunnel.
- the migration probe said "VPN support", presenting VPN as a feature gained to
  users who never enabled it. Now names the tools and the toggle.
- "What's Inside the Container" gains a row; the key-material-in-snapshot
  hazard was in CLAUDE.md only, and is the one genuinely user-facing warning
  here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:24:07 -07:00
shadow-testandClaude Opus 5 ab2c75d0b2 Ship a firewall backend, and correct three claims review disproved
Build App (Preview) / compute-version (pull_request) Successful in 5s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 2m46s
Build App (Preview) / build-linux (pull_request) Successful in 7m22s
Build App (Preview) / build-windows (pull_request) Successful in 7m43s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
Build Container / build-container (pull_request) Successful in 11m20s
Review of #28 found the iptables exclusion was justified by a false premise,
and I confirmed it: `wireguard-tools` declares `Recommends: nftables | iptables`,
`--no-install-recommends` strips it, and `wg-quick`'s add_default() shells out
to a firewall backend with no `type -p` guard. Measured on the image as this PR
shipped it:

    [#] iptables-restore -n
    /usr/bin/wg-quick: line 32: iptables-restore: command not found
    wg-quick EXIT=127

That fires for `AllowedIPs = 0.0.0.0/0` — every stock full-tunnel config from
every provider — not for a desktop client's killswitch as the comment claimed.
Split tunnels are unaffected.

Ship `nftables` rather than `iptables`: wg-quick prefers it (`type -p nft`, so
with both installed iptables is dead weight), it is first in the package's own
Recommends, and it is half the size.

The review's proposed fix stopped there; it does not hold. Adding nftables does
not make wg-quick work on this host, and neither does iptables:

    Warning: Extension CONNMARK revision 0 not supported, missing kernel module?

`Table=auto` routes by fwmark and needs xt_CONNMARK from the *host* kernel.
WSL2 has none and containers have no /lib/modules to load one from. So this
fixes native Linux and Docker Desktop for Mac — which other WHP users are on —
and cannot fix Docker Desktop for Windows, where the answer is to add routes
with `ip route` directly. Documented rather than left to be rediscovered.

Also from review:

- "`ip` and `wg` are always present" was false. A project keeps the base image
  it was first built from, so this reaches new projects only. Reworded to match
  the wording already used for the Playwright libraries, and `/usr/bin/wg` added
  to FEATURE_PROBES so an existing project is *told* it is missing VPN tooling
  and prompted to migrate, rather than finding out via `wg: command not found`.
- "no client is installed" contradicted shipping `wg` four lines earlier. The
  true claim is that no tunnel is configured or started.
- The size figure measured against bare ubuntu:24.04, which over-counts by the
  ~209 kB of libelf1t64 the real base already has, and covered one arch. Now
  measured against the current base on amd64 and stated for arm64 too, per the
  standard CLAUDE.md sets for the Playwright layer.
- `/run` persistence conflated two mechanisms: same-container files on a
  stop/start, `docker commit` on a recreation. Both stated, plus the corollary
  that key material written to /run ends up inside a snapshot image — observed,
  a `wg.priv` was already sitting in one.
- The DNS bullet presented a Docker Desktop address as the general case. Now
  leads with the mechanism, notes 127.0.0.11 on a user-defined network is
  unaffected, and adds the two things the advice omitted: a resolver the tunnel
  can reach (or it leaks every query), and pinning the endpoint via the old
  gateway (or the tunnel routes through itself).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:55:44 -07:00
shadow-testandClaude Opus 5 00937745f7 Ship the tools the VPN toggle grants capability for
Build Container / build-container (pull_request) Successful in 11m28s
`vpn_support_enabled` hands a project CAP_NET_ADMIN and /dev/net/tun, and the
image then contains no `ip` and no `wg` — a capability with nothing able to
exercise it. Bake `iproute2` and `wireguard-tools` (~4.3 MB with deps).

They belong in the image rather than a runtime install for the reason the
Dockerfile already gives for the Playwright libraries: the writable layer is
lost on base-image migration. A hand-installed `wg` works until an upgrade and
then vanishes, which presents as a tunnel that will not come up rather than as
a missing package. One project only had `ip` at all because MariaDB pulled in
iproute2 as a transitive dependency.

`iptables` stays out. Only a desktop client's killswitch wants it, and those
clients need a GUI the container cannot provide.

Also correct three things the docs left users to discover:

- the toggle grants capability and routes nothing, which is being reported as
  the default network "not routing through the VPN automatically"
- no tunnel survives a restart, and `/run` state riding the snapshot makes it
  look as though one did while traffic goes out the real address
- a full tunnel captures the Docker resolver, which sits outside the
  container's subnet, and takes DNS down with it — Claude Code then reports a
  connection failure because it cannot resolve api.anthropic.com, and a health
  check aimed at an IP literal passes throughout

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:38:37 -07:00