README
¶
pr0xteus
Your Docker services need to leave through a VPN, but you do not want to hand them a provider account, turn the host into a VPN client, or accidentally run an open proxy. pr0xteus is the small private control plane between those things: give it WireGuard files you are allowed to use, and trusted containers get short-lived SOCKS5 exits from the pools you approve.
Contents
- What it does
- Quick start
- Tailscale
- Run it yourself
- Complete example
- How it is wired
- Configuration
- API
- Agent integrations
- Development
- Security shape
- Project layout
- More docs
- License and notices
What it does
pr0xteus starts a short-lived Docker cell when a trusted caller needs a SOCKS5 exit. Each cell owns one WireGuard configuration, waits for a handshake, runs microSocks, and is reaped once it is idle or unhealthy. Pools and country routing are operator-owned local files; callers cannot name configs, images, Docker arguments, or host paths.
It is not an open proxy. The control API needs a bearer token, binds to host loopback in the supplied Compose stack, and reaches Docker only through a restricted socket proxy. Caller input picks a configured country or pool — not an image, a host path, or a provider config.
Quick start
You need Linux, Docker, and a WireGuard .conf file from a VPN provider or
private network you are allowed to use. Docker already includes Compose, so
there is no separate Compose install dance.
Install it
curl -fsSL https://raw.githubusercontent.com/psyb0t/pr0xteus/main/install.sh | sudo bash
That makes ~/.pr0xteus/, puts the local docker-compose.yml and starter
config there, generates a bearer token in owner-only .env, and installs the
pr0xteus command. No source checkout required. It pins to the latest
tagged release — never :latest on your box — and the controller derives its
matching cell image from that tag, so both move together only when you upgrade.
Right after installing, edit ~/.pr0xteus/.env if you want to change the
loopback ports, tune logging, or turn on the optional tailnet API (below) —
everything is a plain key you edit, not a CLI flag to remember.
Want to track main instead of a release? Add --rolling to force the moving
:latest image for a single run — on the installer
(… | sudo bash -s -- --rolling) or on any pr0xteus start / pr0xteus upgrade.
Give it one WireGuard exit
Copy your provider or private-network file into the config directory:
cp /wherever/you/keep/your-vpn.conf ~/.pr0xteus/secrets/wireguard/us.conf
Then edit these two small files so us means your file without .conf and
US is the country you want callers to request:
# ~/.pr0xteus/secrets/pools.yaml
pools:
us:
region: north-america
purpose: private-service-egress
configs: [us]
exit_countries:
us: US
# ~/.pr0xteus/config/egress-routing.yaml
country_to_pool:
US: us
default_pool: us
Start it:
pr0xteus start
The controller stays private on http://127.0.0.1:8000; metrics and health
stay on http://127.0.0.1:9091. Useful commands are deliberately boring:
pr0xteus status
pr0xteus logs --follow
pr0xteus stop
pr0xteus upgrade # re-pin to the newest release, pull it, drop the old image
pr0xteus uninstall # stop the stack, remove the command, ask before deleting data
upgrade re-pins ~/.pr0xteus/.env to the latest release and removes the
previous image so dangling layers don't pile up; uninstall only deletes your
~/.pr0xteus data and volumes if you say yes at the prompt.
The complete example shows an allocation, a private SOCKS5 consumer, and an actual egress proof.
Tailscale
Want the same private API from another machine without opening a host port?
Give the installed stack its own Tailscale identity. Add these lines to
~/.pr0xteus/.env:
PR0XTEUS_TAILSCALE_ENABLED=true
TS_AUTHKEY=your-tailscale-auth-key
TS_HOSTNAME=pr0xteus
Then run:
pr0xteus start
That starts an optional sidecar in its own network namespace, waits for it to
join your tailnet, and configures Tailscale Serve to proxy
http://pr0xteus/v1/... to the private controller. It exposes no host port,
does not touch a host Tailscale client, and still requires the bearer token.
The sidecar's tailnet state lives in ~/.pr0xteus/tailscale/state, so it keeps
the same identity across restarts. For Headscale, add its login argument to
TS_EXTRA_ARGS.
Run it yourself
The wrapper is the normal operator path. If you want every Docker command in front of you, initialize the same local stack directly:
mkdir -p ~/.pr0xteus
docker run --rm --user "$(id -u):$(id -g)" \
-v "$HOME/.pr0xteus:/config" \
psyb0t/pr0xteus:latest config init \
--config-dir /config \
--host-config-dir "$HOME/.pr0xteus" \
--controller-image psyb0t/pr0xteus:latest
Add your WireGuard file and edit the generated pool and routing files exactly as in Quick start. Validate and start it with Docker itself:
docker run --rm --user "$(id -u):$(id -g)" \
-v "$HOME/.pr0xteus:/config:ro" \
psyb0t/pr0xteus:latest config check --config-dir /config
docker compose --project-directory "$HOME/.pr0xteus" \
--env-file "$HOME/.pr0xteus/.env" \
-f "$HOME/.pr0xteus/docker-compose.yml" \
up --detach --pull always
That local docker-compose.yml is generated by the image and is yours to
inspect or run directly. The wrapper is only a small guardrail around these
same commands. Deployment details include the direct
Tailscale command too.
Complete example
The operator walkthrough starts with the installer, maps a real WireGuard file into a logical country pool, starts the private stack, then proves a transient consumer actually exits through its returned SOCKS5 URL. It also covers replacing a bad allocation and why a health check is not a live-tunnel check.
How it is wired
trusted client ── private HTTP API ── pr0xteus ── socket proxy ── Docker
│
└── private SOCKS5 cell ── WireGuard peer
- Controller — validates requests, selects an approved pool config, starts and monitors cells, and exposes Prometheus metrics.
- Socket proxy — has the raw Docker socket; the controller gets only the required container API surface.
- Cell — gets
NET_ADMIN,SETUID,SETGID,/dev/net/tun, and one read-only config file; it starts as root for network setup and then runs the SOCKS5 daemon as UID 1500.
The fuller story is in docs/architecture.md, with implementation detail in internal/README.md and cell/README.md.
Configuration
All sensitive or provider-specific material lives under ~/.pr0xteus/, stays
out of Git, and stays out of Docker build contexts:
secrets/wireguard/*.conf— real WireGuard files.secrets/pools.yaml— named pools and their approved config basenames.config/egress-routing.yaml— country-to-pool policy..env— the private bearer token, absolute host configuration path, and image selection.
The installer writes .env; it is not something you need to create by hand.
A published controller already names its matching cell: latest uses
cell-latest, while vX.Y.Z uses cell-vX.Y.Z. An operator can override
PR0XTEUS_CELL_IMAGE, but that override must be pinned by digest. Local
development uses the locally built psyb0t/pr0xteus:cell-dev with the explicit
unpinned escape hatch in the generated .env.
To stay on a particular release, change
PR0XTEUS_CONTROLLER_IMAGE=psyb0t/pr0xteus:vX.Y.Z in
~/.pr0xteus/.env, run pr0xteus setup, then run pr0xteus start.
Pool filenames need not be provider-specific. For a file that does not follow
the legacy <country>-<location>.conf convention, add its country explicitly:
pools:
primary:
configs: [my-provider-node]
exit_countries:
my-provider-node: US
API
The API is versioned and JSON-only:
POST /v1/proxies # request exactly one configured country or pool
GET /v1/pools # authenticated pool/tunnel operator view
GET /healthz # separate metrics listener, keep it private
GET /metrics # Prometheus, separate metrics listener
POST /v1/proxies returns a private socks5:// URL only after the cell has
completed its WireGuard handshake wait and opened its SOCKS5 listener. The
exact request, response, and failure contract live in docs/api.md.
Agent integrations
This repo ships a documentation skill for agents that need to drive a trusted pr0xteus controller. It knows the private control API, the real setup sequence, and the sharp edge around private Docker networking. It does not pretend pr0xteus is an MCP server, because it is not one.
Claude Code
claude plugin marketplace add psyb0t/agents
claude plugin install pr0xteus@psyb0t
Claude Code asks for the private controller URL and bearer token when the plugin is enabled; the token is stored as sensitive user configuration.
Codex
codex plugin marketplace add psyb0t/agents
codex plugin add pr0xteus@psyb0t
Inside this repository, use $pr0xteus. After marketplace installation, use
$pr0xteus:pr0xteus.
OpenClaw
The same documentation skill is published to ClawHub on tagged releases:
openclaw skills install @psyb0t/pr0xteus
There is intentionally no OpenClaw MCP bridge: pr0xteus exposes a private HTTP API, not an MCP endpoint. The detailed setup reference is here.
Development
Everything supported goes through Make. Go tooling, formatting, linting, and
tests run inside Dockerfile.dev, not through a host Go installation.
Source checkout and Make are for development only; an operator uses the image and the installer quick start above.
make help # every supported operation
make format # gofumpt + shfmt
make lint # Go, shell, and format checks
make test # unit tests plus a real Testcontainers WireGuard/SOCKS5 stack
make test-real # opt-in real Surfshark allocation and public-IP egress proof
make test-coverage # runs all test packages and requires 90% production coverage
make audit # govulncheck
make audit-compose # Compose safety checks
make build # controller image
make build-cell # WireGuard + microSocks image
make test-integration uses Testcontainers to build and start the production
controller, a real WireGuard peer, the production cell image, and a sibling
SOCKS5 client on an isolated Docker network. It allocates a proxy through the
real API and proves that SOCKS5 traffic traverses the WireGuard tunnel to a
private test HTTP server. It needs no provider account, real WireGuard bundle,
host port, or persistent container; Testcontainers tears down only the exact
resources it created.
make test-coverage runs go test -tags=integration -race ./..., so it
executes unit tests and every Testcontainers package, including tests/*_test.go.
Its 90% gate measures pr0xteus production code only
(internal/pkg/services/pr0xteus and pkg/client): Go does execute test
source, but does not treat _test.go files as coverable production code. The
fixture exports native coverage from its test-only controller image into
ignored .cover/ storage and merges it with the test profile, so the reported
number includes the real controller process instead of only test binaries.
Normal integration runs keep using Dockerfile, the production controller
image.
make test-real is deliberately separate from make test and CI. It loads
the ignored local Surfshark bundle at secrets/wg/surfshark-wireguard/ with
the matching secrets/wg/pools.yaml and config/egress-routing.yaml, starts
its own Testcontainers controller and consumer, requests a real egress proxy,
then verifies that the consumer's public IPv4 address changes when traffic
uses the returned SOCKS5 URL. Set PR0XTEUS_REAL_TEST_COUNTRY=US before
running make test-real to select the routing input. It creates a fresh test
token and a unique controller scope; it neither reads the production token nor
touches a running stack.
The public Go client surface is under pkg/client; it is useful
when another Go service should request and retry egress proxies without
re-implementing the HTTP contract.
Security shape
- The raw Docker socket is only mounted into
docker-socket-proxy, never the controller or cells. - The controller is non-root, read-only, capability-empty, resource-capped, log-capped, and exposes only loopback ports.
- Optional tailnet access is a separate, capability-minimized Tailscale
sidecar. It is the only service with
/dev/net/tun,NET_ADMIN, andNET_RAW; it has its own tailnet identity and exposes only the authenticated controller API through Tailscale Serve. - A cell has the specific WireGuard exception:
NET_ADMINand/dev/net/tun, plusSETUID/SETGIDsolely for its one-way final drop to the non-root microSocks account. It begins with default-drop firewall policy, allows the WireGuard peer, and does not start microSocks until a handshake arrives. - Real configuration and tokens are neither tracked nor included in either image build context.
- The controller accepts a strict, size-bounded JSON body and stores only a SHA-256 digest of the bearer token after startup.
- Every cell carries a controller scope label, so shutdown and orphan recovery only ever act on cells from that controller scope.
These are meaningful boundaries, not magic. Anyone able to modify local pool policy, read the bearer token, or control Docker on the host is already inside the operator trust boundary.
Project layout
cmd/ — process entry point
internal/ — API, pool manager, Docker spawner, reaper, metrics
pkg/client/ — public Go client for the private control API
tests/ — Testcontainers-backed controller, WireGuard, cell, and SOCKS5 tests
cell/ — WireGuard + microSocks worker image and entrypoint
docs/ — architecture, deployment, and API references
scripts/ — Makefile-backed dependency and image helpers
More docs
- Architecture
- Complete setup and egress proof
- Deployment guide
- Control API
- Internal control-plane detail
- Cell boot and firewall detail
- Changelog
- Third-party notices
License and notices
The project source is MIT licensed. See
THIRD_PARTY_NOTICES.md and vendor/ for dependency
licenses and attribution, including the development-only GPL-3.0 linter.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
pkg/services/pr0xteus
Package pr0xteus implements logical pools of cell-backed WireGuard tunnels with on-demand spawn, idle reap, and an authenticated HTTP control API.
|
Package pr0xteus implements logical pools of cell-backed WireGuard tunnels with on-demand spawn, idle reap, and an authenticated HTTP control API. |
|
pkg
|
|
|
client
Package client is the HTTP client library consumers use to send outbound requests through pr0xteus.
|
Package client is the HTTP client library consumers use to send outbound requests through pr0xteus. |