WIP: Ship the tools the VPN toggle grants capability for #28
@@ -203,7 +203,8 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
|
|||||||
### Container (`container/`)
|
### Container (`container/`)
|
||||||
|
|
||||||
- **`Dockerfile`** — Ubuntu 24.04 base with Claude Code, Node.js 22, Python 3.12, Rust, Docker CLI, git, gh, AWS CLI v2, ripgrep, pnpm, uv, ruff pre-installed, plus the shared
|
- **`Dockerfile`** — Ubuntu 24.04 base with Claude Code, Node.js 22, Python 3.12, Rust, Docker CLI, git, gh, AWS CLI v2, ripgrep, pnpm, uv, ruff pre-installed, plus the shared
|
||||||
libraries a browser links against (see below)
|
libraries a browser links against (see below) and the VPN tooling the `vpn_support_enabled`
|
||||||
|
toggle grants capability for (`iproute2`, `wireguard-tools`, `nftables`)
|
||||||
- **Browser runtime libraries are baked in; browser *binaries* are not.** A layer runs
|
- **Browser runtime libraries are baked in; browser *binaries* are not.** A layer runs
|
||||||
`npx --yes playwright@latest install-deps chromium` as root, so Playwright names its own
|
`npx --yes playwright@latest install-deps chromium` as root, so Playwright names its own
|
||||||
dependencies and the list cannot rot against Ubuntu 24.04's `t64` renames or a new Chromium
|
dependencies and the list cannot rot against Ubuntu 24.04's `t64` renames or a new Chromium
|
||||||
@@ -316,9 +317,22 @@ container is created once by a very long function where a dropped capability is
|
|||||||
base-image migration — leaving a project holding the capability with nothing able to exercise it,
|
base-image migration — leaving a project holding the capability with nothing able to exercise it,
|
||||||
and no error that points at why. `iptables` is deliberately absent; see the Dockerfile comment.
|
and no error that points at why. `iptables` is deliberately absent; see the Dockerfile comment.
|
||||||
- **Anything built on this fails open.** The network namespace is rebuilt on every start and no
|
- **Anything built on this fails open.** The network namespace is rebuilt on every start and no
|
||||||
service manager runs inside, so a tunnel never survives stop/start or recreation while `/run`
|
service manager runs inside, so a tunnel never survives stop/start or recreation — while leftover
|
||||||
state persists through the snapshot and makes it look as though it did. Traffic silently reverts
|
`/run` state makes it look as though it did. Note the two different mechanisms: `/run` is in the
|
||||||
to the real address. Any future autostart or killswitch work starts here.
|
writable layer, so on a stop/start it is simply the same container's files, and on a recreation
|
||||||
|
`docker commit` has carried it into the snapshot. Traffic silently reverts to the real address.
|
||||||
|
Any future autostart or killswitch work starts here.
|
||||||
|
- **`/run` riding the snapshot means a VPN client's key material can end up in an image.** Verified:
|
||||||
|
a fresh container off the whp snapshot already contained the `wg.priv` a previous tunnel left in
|
||||||
|
`/run`. Anything writing key material there inherits the problem — the same `docker commit`
|
||||||
|
hazard as `triple-c.git-token-hash` and the custom-env fingerprint, in a directory that looks
|
||||||
|
ephemeral and is not. A VPN client that does this should delete its key on teardown.
|
||||||
|
- **`wg-quick` full tunnels need `xt_CONNMARK` from the host kernel**, which WSL2 does not have and
|
||||||
|
a container cannot load; `Recommends: nftables | iptables` is also stripped by
|
||||||
|
`--no-install-recommends`, so `nftables` is baked explicitly. See the Dockerfile comment — the
|
||||||
|
short version is that shipping the backend fixes native Linux and Docker Desktop for Mac, nothing
|
||||||
|
fixes Docker Desktop for Windows, and adding the routes directly with `ip route` sidesteps it on
|
||||||
|
all three.
|
||||||
|
|
||||||
### Container Lifecycle
|
### Container Lifecycle
|
||||||
|
|
||||||
|
|||||||
+32
-18
@@ -475,13 +475,18 @@ When enabled, the host Docker socket is mounted into the container so Claude Cod
|
|||||||
|
|
||||||
When enabled, the container is given the three things a VPN client needs to build a tunnel:
|
When enabled, the container is given the three things a VPN client needs to build a tunnel:
|
||||||
the `NET_ADMIN` capability, the `/dev/net/tun` device, and the `net.ipv4.conf.all.src_valid_mark`
|
the `NET_ADMIN` capability, the `/dev/net/tun` device, and the `net.ipv4.conf.all.src_valid_mark`
|
||||||
sysctl that WireGuard requires. The `ip` and `wg` commands are always present to use them. This is
|
sysctl that WireGuard requires. This is **off by default**.
|
||||||
**off by default**.
|
|
||||||
|
The `ip`, `wg` and `nft` commands ship in the container image so there is something able to use
|
||||||
|
them. If your project's container was created from an older base image it will not have them, and
|
||||||
|
`wg` will simply not be found — **migrating the project onto the current base image** is what picks
|
||||||
|
them up. Installing them by hand with `sudo apt install wireguard-tools` works in the meantime, but
|
||||||
|
lives in the writable layer, so it is undone by a **Reset** and by a migration.
|
||||||
|
|
||||||
**This setting makes a tunnel possible; it does not make one.** Nothing is connected, no traffic is
|
**This setting makes a tunnel possible; it does not make one.** Nothing is connected, no traffic is
|
||||||
redirected, and no client is installed or started on your behalf. Enabling it and expecting the
|
redirected, and no tunnel is configured or started on your behalf. Enabling it and expecting the
|
||||||
container's traffic to start leaving through a VPN is the most common misreading of what it does —
|
container's traffic to start leaving through a VPN is the most common misreading of what it does —
|
||||||
installing a client and routing traffic into it remains yours to do.
|
configuring a tunnel and routing traffic into it remains yours to do.
|
||||||
|
|
||||||
Without it, a client such as PIA, WireGuard or OpenVPN installs and its daemon starts normally, but
|
Without it, a client such as PIA, WireGuard or OpenVPN installs and its daemon starts normally, but
|
||||||
the connection attempt **hangs until it times out** — a default container has no tun device to open
|
the connection attempt **hangs until it times out** — a default container has no tun device to open
|
||||||
@@ -504,20 +509,29 @@ Things worth knowing:
|
|||||||
- A VPN client's kill switch applies to everything in the container, Claude Code included. If the
|
- A VPN client's kill switch applies to everything in the container, Claude Code included. If the
|
||||||
tunnel drops, expect API calls to fail until it reconnects or the kill switch is turned off.
|
tunnel drops, expect API calls to fail until it reconnects or the kill switch is turned off.
|
||||||
- **No tunnel survives a restart.** The network namespace is built fresh every time the container
|
- **No tunnel survives a restart.** The network namespace is built fresh every time the container
|
||||||
starts, and there is no service manager inside to reconnect anything. Files under `/run` may
|
starts, and there is no service manager inside to reconnect anything. Leftover state under `/run`
|
||||||
persist via the snapshot and make it *look* like the tunnel is still configured, but after any
|
makes it *look* like the tunnel is still configured — that directory is in the container's
|
||||||
stop/start, Reset or recreation the interface and its routes are gone and traffic goes out your
|
writable layer, so it is simply still there after a stop/start, and `docker commit` carries it
|
||||||
real address again — with no error and nothing visibly different. Re-establish it after every
|
into the snapshot that a recreation is built from. Either way the interface and its routes are
|
||||||
start, and check rather than assume.
|
gone and traffic goes out your real address again, with no error and nothing visibly different.
|
||||||
- **A full tunnel breaks DNS unless the client is told to leave private ranges alone.** Containers
|
Re-establish it after every start, and check rather than assume.
|
||||||
resolve through an address on the Docker network (`192.168.65.7` under Docker Desktop) that sits
|
- **A full tunnel breaks DNS unless the client is told to leave private ranges alone.** Your
|
||||||
outside the container's own subnet, so a default route of `0.0.0.0/0` — or a `0.0.0.0/1` plus
|
resolver is whatever `/etc/resolv.conf` says, and if that address is outside the container's own
|
||||||
`128.0.0.0/1` pair — captures it and sends every lookup into a tunnel that cannot carry it. The
|
subnet then a default route of `0.0.0.0/0` — or a `0.0.0.0/1` plus `128.0.0.0/1` pair — captures
|
||||||
symptom is total: Claude Code reports it cannot connect, because it cannot resolve
|
it and sends every lookup into a tunnel that cannot carry it. Under Docker Desktop it is
|
||||||
`api.anthropic.com`. Route `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` and `169.254.0.0/16`
|
`192.168.65.7`, which is exactly that case; on a user-defined Docker network it is `127.0.0.11`,
|
||||||
via the original gateway, and use the VPN provider's own resolver for everything else. Note that
|
which is loopback and unaffected. Check yours rather than assuming. The symptom when it bites is
|
||||||
a health check which fetches an IP literal such as `1.1.1.1` passes cleanly while this is broken —
|
total: Claude Code reports it cannot connect, because it cannot resolve `api.anthropic.com`.
|
||||||
resolve a name instead.
|
Route `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` and `169.254.0.0/16` via the original
|
||||||
|
gateway — and give the tunnel a resolver it can actually reach, normally the VPN provider's own,
|
||||||
|
or you have a tunnel that leaks every DNS query outside itself. Also pin the VPN endpoint's own
|
||||||
|
address via the original gateway, or the tunnel's encrypted packets try to route through the
|
||||||
|
tunnel. Note that a health check which fetches an IP literal such as `1.1.1.1` passes cleanly
|
||||||
|
while DNS is broken — resolve a name instead.
|
||||||
|
- **`wg-quick` cannot bring up a full tunnel on Docker Desktop for Windows.** Its `Table=auto` mode
|
||||||
|
routes by firewall mark and needs `xt_CONNMARK` from the host kernel, which WSL2's does not have
|
||||||
|
and a container cannot load. Split tunnels (a specific `AllowedIPs`) work fine, as does adding
|
||||||
|
the routes yourself with `ip route`. Native Linux and Docker Desktop for Mac are unaffected.
|
||||||
|
|
||||||
> This setting can only be changed when the container is stopped. Capabilities and devices are
|
> This setting can only be changed when the container is stopped. Capabilities and devices are
|
||||||
> fixed when a container is created, so toggling it recreates the container on the next start.
|
> fixed when a container is created, so toggling it recreates the container on the next start.
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ pub const FEATURE_PROBES: &[(&str, &str)] = &[
|
|||||||
("/usr/local/bin/triple-c-task-runner", "Scheduled task runner"),
|
("/usr/local/bin/triple-c-task-runner", "Scheduled task runner"),
|
||||||
("/usr/local/bin/triple-c-sso-refresh", "AWS SSO auto-refresh"),
|
("/usr/local/bin/triple-c-sso-refresh", "AWS SSO auto-refresh"),
|
||||||
("/opt/mission-control", "Mission Control (Flight Control)"),
|
("/opt/mission-control", "Mission Control (Flight Control)"),
|
||||||
|
("/usr/bin/wg", "VPN support (WireGuard tools)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Headroom demanded on Docker's storage backend on top of the measured
|
/// Headroom demanded on Docker's storage backend on top of the measured
|
||||||
|
|||||||
+43
-9
@@ -36,6 +36,7 @@ RUN for i in 1 2 3 4 5; do \
|
|||||||
socat \
|
socat \
|
||||||
iproute2 \
|
iproute2 \
|
||||||
wireguard-tools \
|
wireguard-tools \
|
||||||
|
nftables \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# `libnss3-tools` above provides `certutil`. Chrome/Chromium read neither
|
# `libnss3-tools` above provides `certutil`. Chrome/Chromium read neither
|
||||||
@@ -44,22 +45,55 @@ RUN for i in 1 2 3 4 5; do \
|
|||||||
# corporate CA, no matter what the system trust store says. entrypoint.sh
|
# corporate CA, no matter what the system trust store says. entrypoint.sh
|
||||||
# degrades to a warning if it is ever missing.
|
# degrades to a warning if it is ever missing.
|
||||||
|
|
||||||
# `iproute2` and `wireguard-tools` above are what the VPN support toggle
|
# `iproute2`, `wireguard-tools` and `nftables` above are what the VPN support
|
||||||
# (`vpn_support_enabled`) grants capability *for*. That toggle hands a project
|
# toggle (`vpn_support_enabled`) grants capability *for*. That toggle hands a
|
||||||
# CAP_NET_ADMIN and /dev/net/tun; without `ip` there is then no way to add a
|
# project CAP_NET_ADMIN and /dev/net/tun; without `ip` there is then no way to
|
||||||
# route, and without `wg` no way to build the tunnel those two exist to serve —
|
# add a route, and without `wg` no way to build the tunnel those two exist to
|
||||||
# a capability with nothing able to use it.
|
# serve — a capability with nothing able to use it.
|
||||||
#
|
#
|
||||||
# They are baked rather than left to a runtime `apt-get install` for the same
|
# They are baked rather than left to a runtime `apt-get install` for the same
|
||||||
# reason as the Playwright libraries below: the writable layer is re-paid after
|
# reason as the Playwright libraries below: the writable layer is re-paid after
|
||||||
# every Reset and lost on base-image migration. A hand-installed `wg` therefore
|
# every Reset and lost on base-image migration. A hand-installed `wg` therefore
|
||||||
# works right up until an upgrade, then disappears and takes the tunnel with it
|
# works right up until an upgrade, then disappears and takes the tunnel with it
|
||||||
# — silently, since a VPN that fails to come up looks exactly like one that was
|
# — silently, since a VPN that fails to come up looks exactly like one that was
|
||||||
# never started. Together they are ~4.3 MB including dependencies.
|
# never started.
|
||||||
#
|
#
|
||||||
# `iptables` is deliberately NOT here. The only thing that wants it is a desktop
|
# Measured against the *current base image*, not a bare ubuntu:24.04 — the base
|
||||||
# VPN client's killswitch, and those clients need a GUI that a container has no
|
# already ships libelf1t64, so measuring on bare ubuntu over-counts by ~209 kB:
|
||||||
# way to give them; leaving it out keeps the reach of CAP_NET_ADMIN smaller.
|
# +9 packages, 5,614 kB on amd64 (4,153 kB of that is iproute2+wireguard-tools,
|
||||||
|
# 1,461 kB is nftables). The same set on arm64 is 7,422 kB, measured against
|
||||||
|
# ubuntu:24.04 since the arm64 base is not cached here.
|
||||||
|
#
|
||||||
|
# ## Why `nftables` specifically
|
||||||
|
#
|
||||||
|
# `wireguard-tools` declares `Recommends: nftables | iptables`, which the
|
||||||
|
# `--no-install-recommends` above strips. That is not cosmetic: `wg-quick`'s
|
||||||
|
# `add_default()` runs whenever a config has `AllowedIPs = 0.0.0.0/0` — i.e.
|
||||||
|
# every stock full-tunnel config every provider hands out — and it shells out to
|
||||||
|
# a firewall backend with no `type -p` guard. Measured without one:
|
||||||
|
#
|
||||||
|
# [#] iptables-restore -n
|
||||||
|
# /usr/bin/wg-quick: line 32: iptables-restore: command not found
|
||||||
|
# wg-quick EXIT=127 (interface rolled back, split tunnels unaffected)
|
||||||
|
#
|
||||||
|
# `nftables` rather than `iptables` because `wg-quick` prefers it (`if type -p
|
||||||
|
# nft`, so with both installed iptables is dead weight), it is the first
|
||||||
|
# alternative in the package's own Recommends, and it is roughly half the size.
|
||||||
|
#
|
||||||
|
# This does NOT make `wg-quick`'s full-tunnel mode work everywhere. `Table=auto`
|
||||||
|
# routes by fwmark and needs connection-mark tracking from the *host* kernel:
|
||||||
|
#
|
||||||
|
# Warning: Extension CONNMARK revision 0 not supported, missing kernel module?
|
||||||
|
#
|
||||||
|
# WSL2's kernel has no `xt_CONNMARK` and containers have no /lib/modules to load
|
||||||
|
# one from, so on Docker Desktop for Windows `wg-quick up` on a full tunnel fails
|
||||||
|
# regardless of what is installed here. Native Linux and Docker Desktop for Mac
|
||||||
|
# have it. Shipping the backend is what makes the difference on those two;
|
||||||
|
# nothing shipped here can make the difference on WSL2, where the way out is to
|
||||||
|
# add the routes with `ip route` instead of going through `wg-quick` at all.
|
||||||
|
#
|
||||||
|
# `iptables` is deliberately still NOT here: with `nftables` present `wg-quick`
|
||||||
|
# never reaches for it, so it would add size and firewall surface for nothing.
|
||||||
|
|
||||||
# Remove default ubuntu user to free UID 1000 for host-user remapping
|
# Remove default ubuntu user to free UID 1000 for host-user remapping
|
||||||
RUN if id ubuntu >/dev/null 2>&1; then userdel -r ubuntu 2>/dev/null || userdel ubuntu; fi \
|
RUN if id ubuntu >/dev/null 2>&1; then userdel -r ubuntu 2>/dev/null || userdel ubuntu; fi \
|
||||||
|
|||||||
Reference in New Issue
Block a user