# 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 ```sh 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) ```sh 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). ```sh # 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: ```sh 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 ` | Keep a destination out of the tunnel | | `prx bypass rm ` | 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`: ```sh 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: ```sh man --local-file man/prx.1 ``` --- ## Configuration `~/.config/prx/config.json` (override with `--config` or `$PRX_CONFIG`): ```json { "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: ```sh sudo systemctl enable --now prx-reapply.timer ``` --- ## Troubleshooting Start with: ```sh 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: ```sh sudo nft list table inet prx ip rule show ip route show table 100 ``` --- ## Testing ```sh 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`.