Files
prx/README.md
T

357 lines
12 KiB
Markdown
Raw Normal View History

2026-10-15 14:29:19 +03:30
# 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 <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`:
```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.
---
## Developer and Contributor
2026-10-15 14:29:19 +03:30
- **Amir Husayn Panahifar** &lt;ahp@panahifar.ir&gt; — developer, contributor, author, and maintainer.
2026-10-15 14:29:19 +03:30
Contributions are welcome.
---
## License
MIT. See `LICENSE`.