docs: add README and manual pages

This commit is contained in:
ahp
2026-10-15 14:29:19 +03:30
parent 7f11dd2c01
commit 5cb1c282c1
3 changed files with 716 additions and 0 deletions
+356
View File
@@ -0,0 +1,356 @@
# 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.
---
## Author
- **Amir Husayn Panahifar** &lt;ahp@panahifar.ir&gt; — author and maintainer.
Contributions are welcome.
---
## License
MIT. See `LICENSE`.
+81
View File
@@ -0,0 +1,81 @@
.\" Manual page for prx-helper
.\" Copyright (C) 2026 Amir Husayn Panahifar <ahp@panahifar.ir>
.TH PRX-HELPER 1 "2026-10-15" "prx 0.1.0" "User Commands"
.SH NAME
prx-helper \- privileged core of prx
.SH SYNOPSIS
.B prx-helper
.SH DESCRIPTION
.B prx-helper
is the privileged half of
.BR prx (1).
It reads a single JSON
.I plan
from standard input, validates it, executes it, and writes a JSON result to
standard output.
.PP
It is not meant to be run by hand; the unprivileged
.B prx
CLI launches it through
.BR pkexec (1)
or
.BR sudo (8),
and communicates over standard input and standard output.
It refuses to run without root privileges.
.PP
Every field of the incoming plan is re\-validated before any system change is
made, so the privileged attack surface stays small.
The
.B apply
operation is transactional: if a post\-apply health check fails, the changes
made so far are rolled back.
If a rollback step itself fails, that is reported in the result rather than
being ignored.
.SH ACTIONS
The plan's
.B action
field selects one of:
.TP
.B apply
Detect the WAN interface and gateway, install the nftables table, the policy
rule and route, any bypass rules and routes, ensure the engine is running, run
health checks, and record the state snapshot.
A health\-check failure triggers a rollback.
.TP
.B remove
Remove the prx\-managed rules, tables and routes, optionally stop the engine,
and delete the state snapshot.
.TP
.B inspect
Report the state of the rules, tables, engine and TUN interfaces without
changing anything.
.TP
.B engine
Start, stop, reload or query the engine service.
.TP
.B init-engine
Write a generated engine configuration to disk after validating it as JSON.
.SH SECURITY
Input is treated as untrusted.
Interface names, IP addresses, CIDR blocks, route table identifiers, rule
priorities, marks and service names are validated against strict patterns and
ranges before being used to build nftables scripts or
.B ip
commands.
.SH "EXIT STATUS"
.TP
.B 0
The action succeeded.
.TP
.B 1
The action failed, or the process was not run as root.
.TP
.B 2
The plan on standard input was malformed, or the result could not be encoded.
.SH "SEE ALSO"
.BR prx (1),
.BR polkit (8),
.BR pkexec (1),
.BR sudo (8)
.SH AUTHOR
Amir Husayn Panahifar <ahp@panahifar.ir>.
+279
View File
@@ -0,0 +1,279 @@
.\" Manual page for prx
.\" Copyright (C) 2026 Amir Husayn Panahifar <ahp@panahifar.ir>
.TH PRX 1 "2026-10-15" "prx 0.1.0" "User Commands"
.SH NAME
prx \- safe proxy routing manager
.SH SYNOPSIS
.B prx
.RI [ options ]
.I command
.RI [ arguments ]
.SH DESCRIPTION
.B prx
turns proxy routing on and off without breaking the connections that reach
your host.
In particular, an inbound session that is already open when the proxy is
enabled (for example an SSH login) keeps working, regardless of the port it
uses.
.PP
The tool orchestrates an external proxy engine (by default
.BR sing-box (1)
in TUN mode, managed through systemd) and installs the network configuration
needed to keep reply traffic on the physical uplink:
.IP "1." 4
Incoming connections on the WAN interface are tagged with a conntrack mark
(\fB0x100\fR by default).
.IP "2." 4
Outgoing packets belonging to a connection whose
.I original
destination is one of the host's WAN addresses are tagged as well.
Because the original destination is matched, connections that were already
established before
.B prx on
are protected too.
.IP "3." 4
An
.BR ip-rule (8)
entry routes marked packets through a dedicated routing table whose default
route points at the physical gateway rather than the tunnel.
.PP
Because the decision is based on the connection and not on a port number, the
protection is port\-independent.
.PP
All privileged work is performed by a small helper,
.BR prx-helper (1),
which the unprivileged
.B prx
CLI invokes through
.BR pkexec (1)
(polkit), falling back to
.BR sudo (8)
when polkit is not installed.
.SH OPTIONS
.SS Global options
.TP
.BR \-\-config " " \fIFILE\fR
Read configuration from
.IR FILE .
Defaults to
.IR $XDG_CONFIG_HOME/prx/config.json
(or
.IR ~/.config/prx/config.json ).
The environment variable
.B PRX_CONFIG
overrides this default.
.TP
.BR \-v ", " \-\-verbose
Enable verbose logging on standard error.
.TP
.BR \-h ", " \-\-help
Show help for
.B prx
or for a subcommand.
.SH COMMANDS
.TP
.BR on " [" \-\-skip\-engine "] [" \-\-dry\-run ]
Enable proxy routing.
Installs the nftables table
.BR "inet prx" ,
the policy rule and route, and (for extra bypass CIDRs) the bypass rules and
routes, then ensures the engine service is running and runs health checks.
If a blocking health check fails, every change made so far is rolled back and
the command exits non\-zero.
Re\-running
.B on
is idempotent.
.TP
.B off
Disable proxy routing.
Removes only the objects
.B prx
created: the
.B "inet prx"
table, any rules that look up
.BR prx 's
routing tables, and
.BR prx 's
routes.
It never flushes the whole ruleset.
Running
.B off
twice is a no\-op.
.TP
.B toggle
Run
.B on
or
.B off
depending on the current state.
.TP
.BR status " [" \-\-json ]
Print the current status.
Reads the state snapshot and inspects the policy rules without requiring root.
With
.B \-\-json
the result is emitted as JSON.
.TP
.B doctor
Run diagnostics.
Checks required binaries, the configuration, the engine configuration, mark
collisions, the applied rules and the engine status.
Exits non\-zero when a critical (local) problem is found, or when
.B prx
is enabled but the applied rules are inconsistent.
.TP
.BR bypass " add " \fICIDR\fR | \fIIP\fR | \fIDOMAIN\fR
Add a destination to the bypass list.
A CIDR or bare IP is additionally enforced at the routing layer: a route is
installed in the bypass table and a destination\-based
.B ip-rule
is added, so traffic to it never enters the tunnel.
A domain is recorded as an engine hint only.
.TP
.BR bypass " rm " \fICIDR\fR | \fIIP\fR | \fIDOMAIN\fR
Remove an entry from the bypass list.
.TP
.B bypass ls
List the bypass entries.
.TP
.BR logs " [" \-f | \-\-follow ]
Show the proxy engine's journal output (via
.BR journalctl (1)).
.TP
.BR init " [" \-\-force "] [" \-\-engine\-config "]"
Write a default configuration file to the configured path.
With
.B \-\-engine\-config
also generate a baseline engine (sing\-box) configuration.
.TP
.B version
Print version information.
.SH "ON OPTIONS"
.TP
.B \-\-skip\-engine
Apply only the routing layer without starting or stopping the engine.
Useful for testing and when the engine is managed separately.
.TP
.B \-\-dry\-run
Validate the plan with
.BR "nft \-c"
without changing anything.
.SH "INIT OPTIONS"
.TP
.B \-\-force
Overwrite an existing configuration file.
.TP
.B \-\-engine\-config
Also write a baseline engine configuration containing a TUN inbound and a
placeholder outbound.
.TP
.BR \-\-engine\-config\-path " " \fIPATH\fR
Path for the generated engine configuration.
Defaults to
.B engine.config_path
from the prx configuration.
.SH FILES
.TP
.I ~/.config/prx/config.json
User configuration.
.TP
.I /run/prx/state.json
Runtime snapshot written when
.B prx
is enabled; used by
.B off
and for rollback.
.TP
.I /run/prx/prx.lock
Advisory lock held during
.B on
and
.B off
so concurrent runs do not interleave.
.TP
.I /etc/sing-box/config.json
Default engine configuration.
.TP
.I /usr/share/polkit-1/actions/org.prx.policy
Polkit action definition mapping
.B prx-helper
to
.BR org.prx.manage .
.TP
.I /usr/lib/prx/prx-helper
Installed location of the privileged helper.
.B prx
also searches
.IR /usr/libexec/prx ,
.IR /usr/local/lib/prx
and
.IR /usr/local/libexec/prx .
.SH ENVIRONMENT
.TP
.B PRX_CONFIG
Default configuration file path, used when
.B \-\-config
is not given.
.TP
.B PRX_HELPER
Explicit path to the privileged helper, overriding the search locations.
.SH "EXIT STATUS"
.TP
.B 0
Success.
.TP
.B 1
An error occurred; a message is printed to standard error.
.TP
.B 126
Authorization was cancelled or is not permitted (from
.BR pkexec ).
.TP
.B 127
The privileged helper could not be started.
.SH EXAMPLES
Enable the proxy:
.PP
.RS 4
.B prx on
.RE
.PP
Enable only the routing layer, without touching the engine:
.PP
.RS 4
.B prx on \-\-skip\-engine
.RE
.PP
Validate without making changes:
.PP
.RS 4
.B prx on \-\-dry\-run
.RE
.PP
Keep a subnet out of the tunnel and refresh the rules:
.PP
.RS 4
.B prx bypass add 203.0.113.0/24
.RE
.PP
Check status as JSON:
.PP
.RS 4
.B prx status \-\-json
.RE
.PP
Disable the proxy:
.PP
.RS 4
.B prx off
.RE
.SH "SEE ALSO"
.BR prx-helper (1),
.BR nft (8),
.BR ip-rule (8),
.BR ip-route (8),
.BR pkexec (1),
.BR sudo (8),
.BR systemctl (1)
.SH AUTHOR
Amir Husayn Panahifar <ahp@panahifar.ir>.