Attach VPN Proxy to a container
Route a Linux container's IPv4 TCP and UDP traffic through an assigned VPN exit.
VPN Proxy can attach transparent egress to a Linux container without changing its image. A
host-side forwarder creates a TUN device in the container's network namespace, replaces that
namespace's default route, and carries packets over an authenticated WebSocket to the central
proxy. The container does not receive VPN credentials and does not need HTTP_PROXY, an SDK, or a
VPN client.
Current network constraint
The container image and application can be arbitrary, but the current forwarder is not yet
network-topology agnostic. It expects an internal Docker network on 172.30.0.0/24 with gateway
172.30.0.1, and installs a bypass route for the host control-plane gateway at 172.17.0.1.
Use the topology below until those addresses become configurable.
Before you start
You need:
- a Linux Docker host with systemd,
/dev/net/tun,nsenter,ip, andsysctl; - host root access, including permission to inspect the target container;
- the VPN Proxy binary installed on the host;
- a root-protected client configuration supplied by the proxy operator; and
- a canonical, lowercase, non-zero UUID for the workload.
The workload UUID is routing identity, not a secret. Keep it stable when a logical workload should retain its assigned exit. Never reuse one UUID for two live containers: the server admits only one active tunnel per UUID.
The client configuration stays on the host and must never be mounted into the workload container:
Store it as /etc/misanthropic-proxy/client.json, owned by root and mode 0600. The compatibility
path and protocol headers still use the Misanthropic name. The shared key can assert any workload
UUID, so treat it as a host credential rather than a per-container credential.
Prepare a fail-closed container network
Reuse misanthropic-agent-proxy when it already exists with the expected settings. Otherwise,
create an internal network:
An internal network prevents the workload from bypassing the proxy before the forwarder is ready
or after it disconnects. Do not attach the container to a second network with direct Internet
access. Supply an external IPv4 resolver through a bind-mounted resolv.conf; Docker's embedded
resolver would otherwise send DNS through the host rather than through the workload's tunnel.
Attach a container
Start the workload on only the internal network. This example uses a disposable shell container; replace the image and command with the real workload.
Run one host-side forwarder for that network namespace:
The forwarder creates misproxy0, disables IPv6 in the namespace, and routes public IPv4 TCP and
UDP through the central server. Private, loopback, link-local, multicast, documentation, and other
non-public destinations are rejected by the central packet stack rather than sent through a VPN.
Wait for readiness
Do not mark the workload ready or give it jobs until the readiness file exists and identifies the current namespace:
The forwarder writes readiness only after the authenticated WebSocket and packet pumps are live. It removes the file whenever the connection drops. A missing file means egress is unavailable; it does not mean traffic has fallen back to Direct.
For workloads that must not start processing early, make the orchestrator's health/readiness gate depend on this check. The internal network still prevents direct egress while the tunnel is down.
Assign and verify the route
On first connection, the server persists a preferred VPN assignment using its available-route policy. It can temporarily use Direct when that VPN is unavailable. An operator can instead persist an explicit Direct or VPN assignment from the proxy server:
Verify the namespace and public exit:
Expect the default IPv4 route to use misproxy0, IPv6 to report disabled, and the container's
observed public IP to match the proxy server's admin ip result.
Stop cleanly
Stop the workload first so it cannot receive work after its tunnel starts shutting down, then stop the forwarder:
The forwarder also watches the namespace inode and exits when the original container process ends
or Docker reuses its PID. Always resolve the PID again after a container restart; never retain an
old /proc/PID/ns/net path.
Integrate BBM workers through Nomad
BBM's current worker lifecycle has two owners: Sonar decides desired capacity, while Nomad places, restarts, drains, and removes worker allocations. VPN Proxy should follow that boundary:
- Install the forwarder and protected client configuration once when a Nomad client host enrolls. Do not add VPN software or credentials to worker images.
- Opt fixed worker pools into VPN egress in
NomadJobSpecFactory; do not let task input or user-supplied worker tags change host networking. - Add a Nomad-managed companion for each opted-in worker allocation. It resolves the worker's
Docker PID from allocation metadata, starts
forwardon the host, and usesNOMAD_ALLOC_IDas the workload UUID. - Put opted-in workers on the dedicated internal network and gate worker health on exact forwarder readiness. A worker must not pulse as available or consume RabbitMQ work before its tunnel is ready.
- During scale-down or a 135-minute BBM drain window, keep the companion alive until the worker finishes its active task. Stop the worker first and the forwarder second.
NOMAD_ALLOC_ID gives every worker incarnation a canonical UUID and avoids duplicate live tunnel
identity during rolling replacement. If BBM later needs a worker to retain the same VPN assignment
after Nomad replaces its allocation, introduce a separate durable routing UUID with explicit
handoff; do not derive one from mutable tags or reuse it while an old allocation is live.
BBM integration is not implemented yet
Current BBM Nomad jobs do not add the forwarder companion, required internal network, or readiness gate. The generic host adapter must also replace Misanthropic-specific container discovery. Treat the steps above as the rollout contract, not as a claim that existing workers already use VPN Proxy.
Before enabling a BBM pool, verify one disposable allocation end to end: no pre-ready egress, correct assigned exit, TCP and UDP success, DNS through the tunnel, no IPv6 path, fail-closed behavior during a forced WebSocket disconnect, and clean Nomad drain/replacement without two live connections for one UUID.
Current limitations
- Linux network namespaces only; the host-side forwarder must run as root.
- IPv4 TCP and UDP only. IPv6 is disabled; ICMP and non-public destinations are not proxied.
- The internal subnet, original host-gateway bypass, readiness directory, service paths, and protocol header names are currently fixed for compatibility.
- A container restart creates a new network namespace even if its name is unchanged. Restart its forwarder with the new PID.
- Client authentication is shared-key based, not workload scoped.
deploy/install.sh environmentinstalls the existing Misanthropic lifecycle adapter. Other orchestrators should invokeforwarddirectly and own discovery, readiness, and shutdown.
If setup fails, inspect the unit without printing the protected configuration:
Common causes are a reused workload UUID, a stale container PID, incorrect network subnet/gateway,
missing /dev/net/tun, wrong permissions on /run/misanthropic-proxy, or an unreachable WSS
endpoint from the host.