12 KiB
prx
Safe proxy routing manager for Linux.
prx turns proxy routing on and off without breaking the connections that
reach you. In particular, an SSH session that is already open when you enable
the proxy keeps working — regardless of which port it uses.
prx on # route outbound traffic through the proxy, protect inbound replies
prx off # restore a clean state
prx status # what is currently applied
- Proxy engine: sing-box (TUN mode) by default, managed through systemd.
- Routing: nftables + ip rule policy routing.
- Privileges: an unprivileged CLI plus a small privileged helper invoked with polkit/pkexec (falling back to sudo).
- Target: Arch Linux, and any other Linux with
nftables,iproute2andsystemd. Nothing is hard-coded to a particular interface, gateway, address or distribution layout.
Why inbound connections survive
When a TUN-based proxy is enabled, the default route is usually replaced so that traffic enters the tunnel. That is exactly what breaks inbound sessions: the replies you send to a client that connected to you get pulled into the tunnel and never reach the client.
prx prevents this at the routing layer:
- Incoming connections on the WAN interface are given a conntrack mark
(
0x100by default). - Outgoing packets belonging to a connection whose original destination is
one of our WAN addresses are marked too. Matching on the original
destination — instead of only
ct state new— means connections that were already established beforeprx onare protected as well. - An
ip ruleroutes marked packets through a dedicated table whose default route points at the physical gateway, not the tunnel.
Because the decision is based on the connection, not a port number, it works for SSH on any port and for any other inbound service.
Install
From source
git clone https://github.com/example/prx
cd prx
make build # produces bin/prx and bin/prx-helper
sudo make install # installs binaries, man pages, polkit policy and systemd units
Arch Linux (PKGBUILD)
cd packaging
makepkg -si
Dependencies
Runtime: nftables (>= 0.9.4, for the destroy keyword), iproute2 (with -j
JSON support), systemd, and a way to elevate privileges — polkit/pkexec
or sudo. Optional: sing-box (the default engine).
# Arch
sudo pacman -S nftables iproute2 polkit sing-box
# other distributions: install the equivalent packages
prx makes no assumptions about this machine: the WAN interface, gateway and
local addresses are detected at runtime from ip route/ip addr, IPv6 is used
only when the host actually has an IPv6 uplink, and the helper is found by
searching the standard install locations (see Privileges). Missing tools
produce an actionable message that names the package to install.
Create a starting configuration:
prx init # writes ~/.config/prx/config.json
prx init --engine-config # also writes a baseline sing-box config
Commands
| Command | Description |
|---|---|
prx on [--skip-engine] [--dry-run] |
Enable routing; installs nftables + policy routing rules |
prx off |
Remove prx-managed rules and restore a clean state |
prx toggle |
Enable or disable based on current state |
prx status [--json] |
Human readable status, or machine readable JSON |
prx doctor |
Diagnose configuration and applied rules |
prx bypass add <cidr|ip|domain> |
Keep a destination out of the tunnel |
prx bypass rm <cidr|ip|domain> |
Remove a bypass entry |
prx bypass ls |
List bypass entries |
prx logs [--follow] |
Tail the engine journal |
prx init [--engine-config] |
Write a default configuration |
prx version |
Print version information |
Exit codes are 0 on success and non-zero with an actionable message on
failure.
Useful flags
prx on --dry-run— validate the plan without changing anything.prx on --skip-engine— apply only the routing layer (useful for testing or when you manage the engine yourself).
Man pages
Two manual pages are installed under share/man/man1:
man prx # the CLI
man prx-helper # the privileged helper
They are listed in man/prx.1 and man/prx-helper.1 and can be previewed
without installing:
man --local-file man/prx.1
Configuration
~/.config/prx/config.json (override with --config or $PRX_CONFIG):
{
"engine": {
"type": "sing-box",
"config_path": "/etc/sing-box/config.json",
"service_name": "sing-box",
"manage": true,
"stop_on_off": false
},
"network": {
"wan_interface": "",
"wan_gateway": "",
"wan_gateway6": "",
"dns_keepalive": true
},
"marks": { "inbound_mark": 256 },
"routing": {
"table_v4": 100,
"table_v6": 101,
"priority_v4": 100,
"priority_v6": 100,
"bypass_table_v4": 102,
"bypass_table_v6": 103,
"bypass_priority_v4": 50,
"bypass_priority_v6": 50
},
"bypass": {
"always_cidrs": ["127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "::1/128", "fc00::/7", "fe80::/10"],
"extra_cidrs": [],
"domains": []
},
"process_bypass": { "names": ["ssh", "scp", "sftp"] },
"health": { "probe": "1.1.1.1:443" },
"state": { "path": "/run/prx/state.json" }
}
network.wan_interface/network.wan_gateway/network.wan_gateway6are optional overrides; otherwise the default route is detected fromip route show default(IPv4 first, then IPv6 for an IPv6-only uplink).health.probeis ahost:portdialed after enabling to confirm connectivity. Set it to""to disable the check.marks.inbound_markis256(0x100). Keep it distinct from the mark your engine uses;prx doctorwarns on a collision.always_cidrsare hints for the engine (they are baked into the generated engine config).extra_cidrsare additionally enforced by the routing layer: each one gets a route in the bypass table and a destination-basedip rule, so that traffic to it never enters the tunnel.
Unknown keys are rejected (the config is decoded strictly), and every value is validated before it is written.
State is stored in /run/prx/state.json so off and rollback know what was
applied. A companion prx.lock file serialises concurrent on/off so two
runs cannot interleave.
Privileges
Privileged actions go through a small helper:
prx (unprivileged) --pkexec--> prx-helper (root)
--sudo----->
The CLI resolves and validates everything, then sends a JSON plan on stdin to
prx-helper. The helper re-validates every field before touching the system, so
the privileged attack surface stays small. on, off, toggle, bypass,
init --engine-config and doctor use the helper; status, logs and
version do not.
Elevation prefers pkexec (a polkit prompt) and falls back to sudo -n when
polkit is not installed, so prx also works on minimal systems. The helper is
located in this order:
$PRX_HELPER(explicit override),- next to the
prxbinary, then../lib/prxand../libexec/prx, /usr/libexec/prx,/usr/lib/prx,/usr/local/libexec/prx,/usr/local/lib/prx,- finally
$PATH.
If sudo needs a password, run the command as sudo prx ... directly (the CLI
then runs the helper in-process), or install the polkit policy for a prompt.
The polkit action is defined in polkit/org.prx.policy and requires
authentication (auth_admin) for changes.
How on works (transactionally)
- Acquire a
prx.lockfile lock so concurrent runs never interleave. - Detect the WAN interface, gateway and local addresses.
- Install the nftables table
inet prxin a single atomic transaction (destroy+ full redefinition, so re-running never duplicates rules). - Install the policy rule and route (and bypass rules), idempotently. A rollback handler is registered before each table is touched, so a failure part-way through still cleans up.
- Ensure the engine service is running.
- Run health checks (rules present, engine active, connectivity probe).
- If any blocking check fails, roll back everything (in reverse order) and
exit non-zero. If a rollback step itself fails, prx says so explicitly (a
rollback warning:line and an error stating the rollback was incomplete) instead of pretending the system is clean.
off removes only objects prx created: the inet prx table, rules that look up
prx's tables, and prx's routes. It never flushes your whole ruleset.
Re-running on is idempotent; running off twice is a no-op. Configuration and
state are written atomically (temp file + fsync + rename) so they are never
observed half-written, and Ctrl-C cancels cleanly via signal handling. A
corrupt state file never crashes a command: it is reported (for example by
prx status) and the rest of the information is still shown.
The generated sing-box config
prx init --engine-config writes a baseline TUN configuration:
- a
tuninbound withauto_route; - a
directoutbound that you should replace with your real proxy outbound (Shadowsocks, VLESS, …); - a route rule that sends the bypass CIDRs, domains and process names direct.
prx does not rewrite an existing engine config on every on; it only
validates that a TUN inbound is present and warns otherwise.
systemd
Two optional units are shipped:
prx-reapply.service— re-applies the routing (runsprx on, which is idempotent). It only runs when/run/prx/state.jsonexists, i.e. when prx was enabled.prx-reapply.timer— triggers the service periodically so a changed gateway (DHCP) is picked up.
Enable the timer if you want automatic re-application:
sudo systemctl enable --now prx-reapply.timer
Troubleshooting
Start with:
prx doctor
Common situations:
nft: ... Operation not permitted— run the command withsudoor install the polkit policy sopkexeccan prompt.required binary "nft" not found ... install nftables— install the named package (nftables,iproute2orsystemd).nftables may be too old ... needs >= 0.9.4— upgrade your nftables package; older releases do not understand thedestroykeyword prx uses.prx-helper not found— the helper is not installed or lives outside the searched paths; install prx or set$PRX_HELPER.sudo needs a password— runsudo prx ...directly, or install polkit for a graphical prompt.journalctl not found/prx logsunavailable — installsystemd.state file unreadable—/run/prx/state.jsonis corrupt; remove it and re-runprx on.- engine-config warning: no tun inbound — enable TUN mode in your engine config; prx relies on it.
- mark-collision — your engine uses the same mark as prx; change
marks.inbound_mark(or the engine's mark). - connectivity check fails — the marking/routing is installed but the host
could not open an outbound TCP connection; check the engine and upstream (or
set
health.probeto""to skip the check). pkexecreturns "authorization cancelled" — you dismissed the polkit prompt.
Inspect the applied objects manually:
sudo nft list table inet prx
ip rule show
ip route show table 100
Testing
make test # unit tests (no root required)
sudo -E PRX_BIN="$PWD/bin/prx" ./scripts/integration-test.sh
The integration script enables the routing layer with --skip-engine, asserts
the rules exist, checks idempotency, and then disables it and asserts a clean
state.
Author
- Amir Husayn Panahifar <ahp@panahifar.ir> — author and maintainer.
Contributions are welcome.
License
MIT. See LICENSE.