README
¶
pr0xteus
Your application needs to leave through a VPN, but you do not want to hand it 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 host or container clients get short-lived SOCKS5 and HTTP exits from the pools you approve. Every live exit is observable. See how many requests and bytes went through each cell and to which destinations, then destroy any of them on demand.
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 an exit. Each cell owns one WireGuard configuration, waits for a handshake, and runs cellproxy, a first-party SOCKS5 proxy that also serves a control endpoint with per-cell traffic metrics and a real liveness check. A cell is reaped once it is idle (and carries no live connections) 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 by default in the supplied Compose stack, and reaches Docker only through a restricted socket proxy. You can remove every host binding for a private Docker-network gateway, or explicitly choose another bind address. 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
The installer works two ways. Per-user — no root, just for you:
curl -fsSL https://raw.githubusercontent.com/psyb0t/pr0xteus/main/install.sh | bash
That puts the pr0xteus command in ~/.local/bin/ and your config in
~/.config/pr0xteus/ (owner-only). If ~/.local/bin isn't on your PATH the installer
prints the exact one-liner to add it for bash or zsh.
System-wide — run it with sudo for one shared stack any docker-group user
can drive:
curl -fsSL https://raw.githubusercontent.com/psyb0t/pr0xteus/main/install.sh | sudo bash
That puts the command in /usr/local/bin/ and the config in /etc/pr0xteus/
(root-owned, readable by the docker group). The mode is chosen from who runs
it — root → system-wide, otherwise per-user — and --system / --user force it.
Either way it drops managed local Compose templates and starter config,
generates a bearer token in an owner-only .env, refreshes a readable
.env.example, and installs the pr0xteus command. Later setup refreshes
only those managed Compose templates. upgrade refreshes them and the command,
then starts the stack through the refreshed command. .env, pools, routing,
WireGuard files, and Tailscale state are never replaced. 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.
The generated stack runs the controller as the installing operator's UID/GID,
not as root. That lets it read the owner-only config mount without weakening
your WireGuard-file permissions. setup and upgrade keep that generated
PR0XTEUS_RUNTIME_USER value current; do not change it unless the config files
are deliberately owned by another local account.
Right after installing, edit the .env in your config directory 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
(… | 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 ~/.config/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:
# ~/.config/pr0xteus/secrets/pools.yaml
pools:
us:
region: north-america
purpose: private-service-egress
configs: [us]
exit_countries:
us: US
# ~/.config/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 restart # restart the stack
pr0xteus upgrade # re-pin, refresh the command, start the stack, drop the old image
pr0xteus uninstall # stop the stack, remove the command, ask before deleting data
setup and upgrade refresh .env.example and the managed Compose templates
while preserving operator config. upgrade also refreshes the command, then
starts the stack through that new command so current Tailscale routes and
public proxy addresses take effect. It re-pins
~/.config/pr0xteus/.env to the selected image and removes the previous image
so dangling layers do not pile up; uninstall only deletes your
~/.config/pr0xteus data and volumes if you say yes at the prompt.
The complete example below shows an allocation and an actual host-side egress proof.
Tailscale
Want the same private API from another machine without opening any pr0xteus
host port? Give the installed stack its own Tailscale identity. Set these in
~/.config/pr0xteus/.env, then run pr0xteus start:
PR0XTEUS_TAILSCALE_ENABLED=true
PR0XTEUS_DISABLE_HOST_PORTS=true
TS_AUTHKEY=tskey-auth-xxxx # reusable or ephemeral auth key
TS_HOSTNAME=pr0xteus # tailnet machine name
TS_EXTRA_ARGS=--accept-dns=false # extra `tailscale up` flags (see below)
That starts an optional sidecar in its own network namespace, waits for it to
join your tailnet, then serves the authenticated HTTP API on port 80, the
private metrics listener on port 9091, the lease-authenticated SOCKS5 gateway
on port 1080, and the lease-authenticated HTTP proxy on port 8080. With
PR0XTEUS_DISABLE_HOST_PORTS=true, the controller, metrics, SOCKS5 gateway,
and HTTP proxy
have no host binding at all: the sidecar reaches the controller only through
the internal Docker control network. It does not touch a host Tailscale
client, and the tailnet API still requires the bearer token. Keep tailnet
access to port 9091 restricted because /healthz and /metrics are not
authenticated. The wrapper
derives both proxy public addresses from the node's MagicDNS name, so either
lease URL works directly from another tailnet machine. The sidecar's tailnet
state lives in ~/.config/pr0xteus/tailscale/state, so it keeps the same
identity across restarts.
For the normal host-local path, leave PR0XTEUS_DISABLE_HOST_PORTS=false (the
default). The PR0XTEUS_*_HOST_PORT values are complete HOST:PORT mappings
and default to 127.0.0.1. Set one to 0.0.0.0:PORT only when an authenticated
private boundary protects that port. If you change a proxy binding, also set
its matching *_PUBLIC_ADDRESS to a real address clients can reach.
0.0.0.0 is a bind-all address, not a client destination.
Those values are the only Tailscale setup you need. The compose file fixes
the rest for kernel-mode Tailscale — TS_USERSPACE=false with NET_ADMIN,
NET_RAW, and /dev/net/tun — so the sidecar runs a real tailscale0
interface and outbound traffic to 100.64.0.0/10 uses the sidecar's own tailnet
identity, not the host's. TS_STATE_DIR is likewise fixed to the bind-mounted
state dir above.
TS_EXTRA_ARGS is passed verbatim to tailscale up, which is where every
other option goes — the sidecar image has no dedicated env var for them:
- Headscale (self-hosted control server) — point it at your server and use a
Headscale-issued pre-auth key:
TS_AUTHKEY=<headscale-preauthkey> TS_EXTRA_ARGS=--login-server=https://headscale.example.com --accept-dns=false - Tags / ACLs —
--advertise-tags=tag:proxy. - Ephemeral node — issue an ephemeral auth key; it deregisters on stop.
Run tailscale up --help for the full flag set.
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 ~/.config/pr0xteus
docker run --rm --user "$(id -u):$(id -g)" \
-v "$HOME/.config/pr0xteus:/config" \
psyb0t/pr0xteus:latest config init \
--config-dir /config \
--host-config-dir "$HOME/.config/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/.config/pr0xteus:/config:ro" \
psyb0t/pr0xteus:latest config check --config-dir /config
docker compose --project-directory "$HOME/.config/pr0xteus" \
--env-file "$HOME/.config/pr0xteus/.env" \
-f "$HOME/.config/pr0xteus/docker-compose.yml" \
-f "$HOME/.config/pr0xteus/docker-compose.host-ports.yml" \
up --detach --pull always
If .env has PR0XTEUS_DISABLE_HOST_PORTS=true, use the generated no-host
override instead of the host-ports override:
docker compose --project-directory "$HOME/.config/pr0xteus" \
--env-file "$HOME/.config/pr0xteus/.env" \
-f "$HOME/.config/pr0xteus/docker-compose.yml" \
-f "$HOME/.config/pr0xteus/docker-compose.no-host-ports.yml" \
up --detach --pull always
That local docker-compose.yml is generated by the image and is yours to
inspect or run directly. config init also refreshes .env.example while
preserving the real .env. The wrapper is only a small guardrail around these
same commands. Deployment details include the direct
Tailscale command too.
Complete example
Quick start got the stack running. Here is the payoff:
allocate one exit and prove traffic actually leaves through it. Read the token
from .env so it does not land in shell history. The returned proxy URL is a
short-lived credential: keep it out of logs and do not share it.
token="$(sed -n 's/^PR0XTEUS_API_TOKEN=//p' ~/.config/pr0xteus/.env)"
auth=(--header @<(printf 'Authorization: Bearer %s' "$token"))
# POST allocates one US exit. The response comes back only after the cell
# finishes its WireGuard handshake wait and issues both proxy URLs for one lease.
allocation="$(
curl --fail-with-body --request POST "${auth[@]}" \
--header 'Content-Type: application/json' \
--data '{"country":"US"}' \
http://127.0.0.1:8000/v1/proxies
)"
socks5_proxy="$(jq -er '.proxies.socks5' <<<"$allocation")"
http_proxy="$(jq -er '.proxies.http' <<<"$allocation")"
Both URLs work straight from the host or any client that can reach their published controller port. They share the same short-lived credentials, expiry, and selected cell. The controller authenticates the lease, then forwards either protocol to that cell over its internal control network; DNS and outbound traffic happen in the cell through WireGuard:
curl --fail --silent --show-error \
--proxy "$socks5_proxy" https://api.ipify.org
curl --fail --silent --show-error \
--proxy "$http_proxy" https://api.ipify.org
unset token allocation socks5_proxy http_proxy; unset -a auth
That prints the public IP the world sees for that cell — your configured exit country, not your host. Use only providers and targets you are authorized to use; the IP echo is a smoke check, not something pr0xteus depends on.
The full walkthrough runs this from a clean
install and adds the parts a README should not carry: swapping out a bad
allocation, and why a green /healthz is not proof a tunnel can be allocated
right now.
How it is wired
trusted client ── private HTTP API ── pr0xteus ── socket proxy ── Docker
│
client ── lease-authenticated SOCKS5 or HTTP proxy ─┘
│
└── 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 your config directory
(~/.config/pr0xteus/ per-user, /etc/pr0xteus/ system-wide), 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 controller image selection.
The installer writes .env; it is not something you need to create by hand.
The controller chooses its cell itself: latest pulls cell-latest, and
vX.Y.Z pulls cell-vX.Y.Z before allocating a tunnel. That pairing is baked
into the controller binary and cannot be changed through environment settings.
Local development builds both images with the dev version, so it uses
psyb0t/pr0xteus:cell-dev automatically.
To stay on a particular release, change
PR0XTEUS_CONTROLLER_IMAGE=psyb0t/pr0xteus:vX.Y.Z in
~/.config/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 # allocate exactly one configured country or pool
GET /v1/proxies # paginated active-proxy inventory and latest leases
GET /v1/pools # paginated authenticated pool/tunnel operator view
GET /v1/cells # paginated live cells with traffic metrics
GET /v1/cells/{containerID} # one cell, including its traffic snapshot
DELETE /v1/cells/{containerID} # destroy a cell on demand
GET /healthz # separate metrics listener, keep it private
GET /metrics # Prometheus, separate metrics listener
POST /v1/proxies returns short-lived, credentialed socks5:// and http://
URLs only after the cell has completed its WireGuard handshake wait. They target
the controller's loopback-published SOCKS5 gateway and HTTP proxy by default, so
normal host clients do not need Docker network membership. With
PR0XTEUS_DISABLE_HOST_PORTS=true, use the tailnet addresses or another
internal controller gateway rather than host-side proxy clients.
GET /v1/proxies lists live tunnels with lastUsedAt, their latest issued
lease URL and expiry, and exit metadata; it does not create a cell or a new
lease.
GET /v1/cells is the observability view. It uses limit and offset like
the other collections, discovers cells straight from Docker (by a
pr0xteus.parent.id label — no in-memory registry to drift), and carries each
cell's live traffic snapshot from its cellproxy control port:
$ curl -sH "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/v1/cells
{
"cells": [
{
"containerId": "9f3c1a2b4d5e", "pool": "primary", "state": "running",
"traffic": {
"requests": 42, "bytesUp": 18234, "bytesDown": 918273, "active": 3,
"destinations": [
{ "destination": "example.com:443", "requests": 40, "bytesDown": 900000 }
]
}
}
],
"limit": 100,
"offset": 0,
"total": 1
}
GET /v1/cells/{id} inspects one cell and DELETE /v1/cells/{id} destroys it on
demand. 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 controller-fronted proxy lease flow. 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-api # build pr0xteus from its Dockerfile in Testcontainers, hit every route and proxy protocol
make test-real # opt-in real provider allocation and public-IP egress proof
make test-coverage # gate every package at 90% (servicepack coverage engine)
make sec # govulncheck + semgrep merged to sec.sarif; gates on findings
make audit-compose # Compose safety checks
make build # static controller binary (./build)
make docker-build # hardened production controller image
make build-cell # WireGuard + cellproxy image
make run # local images through the real installer and installed wrapper
make test and make test-coverage run the Testcontainers unit and integration
suites. They stand up their own containers and need no local stack, which is
what lets CI run them. make test-installed is the exception: it drives the live
installer stack, so it resets and starts that stack before its suite and stops it
after, including on failure. make test-api and make test-real are standalone
Testcontainers suites you call on their own.
make test-api (and the broader make test-integration) use Testcontainers to
build and start the production controller image, a self-contained WireGuard
peer container (built from
tests/testinfra/wireguard/), the cell image, and
a sibling proxy client on an isolated Docker network. The
API test drives every control-plane route over real HTTP and proves that SOCKS5
and HTTP-proxy traffic traverse the WireGuard tunnel to a private test HTTP
server on the peer.
It needs no provider account, real WireGuard bundle, host port, or persistent
container, and Testcontainers tears down only the exact resources it created.
make test-coverage runs the servicepack coverage engine over
go test -tags=integration ./... with -coverpkg=<module>/..., so every
package is gated at 90% — the controller service included, via native coverage
data merged from the real controller container (test runs swap in a -cover
image; normal test-api/test-integration runs use the production Dockerfile).
It excludes only non-hand-written code under test: cmd/ mains, the tests/
harness, generated code, and mocks.
make test-real is deliberately separate from make test and CI. It loads
the ignored local provider bundle at secrets/wg/provider-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 both returned proxy URLs. 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, and log-capped. Its ports default to loopback and can be removed altogether for an internal gateway; all-interface publication is explicit.
- 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 plus the lease-authenticated controller SOCKS5 gateway and HTTP proxy 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 cellproxy account. It begins with default-drop firewall policy, allows the WireGuard peer, and does not start cellproxy until a handshake arrives. The cellproxy control server is opened only on the internal cell network, never on the WireGuard egress side. - 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 + cellproxy 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 |
|---|---|
|
cellproxy
command
Command cellproxy is the metrics-emitting SOCKS5 proxy that runs inside a pr0xteus cell in place of microsocks.
|
Command cellproxy is the metrics-emitting SOCKS5 proxy that runs inside a pr0xteus cell in place of microsocks. |
|
internal
|
|
|
pkg/cellproxy
Package cellproxy is the metrics-emitting SOCKS5 proxy that runs inside a pr0xteus cell in place of microsocks.
|
Package cellproxy is the metrics-emitting SOCKS5 proxy that runs inside a pr0xteus cell in place of microsocks. |
|
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. |