Files

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, iproute2 and systemd. 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:

  1. Incoming connections on the WAN interface are given a conntrack mark (0x100 by default).
  2. 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 before prx on are protected as well.
  3. An ip rule routes 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_gateway6 are optional overrides; otherwise the default route is detected from ip route show default (IPv4 first, then IPv6 for an IPv6-only uplink).
  • health.probe is a host:port dialed after enabling to confirm connectivity. Set it to "" to disable the check.
  • marks.inbound_mark is 256 (0x100). Keep it distinct from the mark your engine uses; prx doctor warns on a collision.
  • always_cidrs are hints for the engine (they are baked into the generated engine config). extra_cidrs are additionally enforced by the routing layer: each one gets a route in the bypass table and a destination-based ip 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 prx binary, then ../lib/prx and ../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)

  1. Acquire a prx.lock file lock so concurrent runs never interleave.
  2. Detect the WAN interface, gateway and local addresses.
  3. Install the nftables table inet prx in a single atomic transaction (destroy + full redefinition, so re-running never duplicates rules).
  4. 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.
  5. Ensure the engine service is running.
  6. Run health checks (rules present, engine active, connectivity probe).
  7. 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 tun inbound with auto_route;
  • a direct outbound 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 (runs prx on, which is idempotent). It only runs when /run/prx/state.json exists, 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 with sudo or install the polkit policy so pkexec can prompt.
  • required binary "nft" not found ... install nftables — install the named package (nftables, iproute2 or systemd).
  • nftables may be too old ... needs >= 0.9.4 — upgrade your nftables package; older releases do not understand the destroy keyword 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 — run sudo prx ... directly, or install polkit for a graphical prompt.
  • journalctl not found / prx logs unavailable — install systemd.
  • state file unreadable — /run/prx/state.json is corrupt; remove it and re-run prx 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.probe to "" to skip the check).
  • pkexec returns "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.


Developer and Contributor

  • Amir Husayn Panahifar <ahp@panahifar.ir> — developer, contributor, author, and maintainer.

Contributions are welcome.


License

MIT. See LICENSE.