Routing · Linux · Qeli 0.8.1

Egress through a separate exit node

Route internet traffic from some clients through another Linux client's physical WAN without making the Qeli server the exit point.

Three roles

Consumer → server → exit client → internet

The server forwards packets between clients in the profile. The exit client applies source NAT and sends the packets through its physical WAN. Replies return along the same path.

RoleKey settingPurpose
Qeli serverrouting.client_to_client=trueAllows forwarding between clients
Exit clientgateway=false + exit_node=trueSends tunnel traffic out through its WAN
Consumer clientgateway=trueSends public IPv4 traffic into Qeli
Do not confuse this with gateway_nat. gateway_nat brings a LAN behind a client into the tunnel, while exit_node sends traffic from the tunnel out through the client's physical WAN.
Minimal setup

Server and two-client configuration

The exit node must be a separate Linux client. It stays in split-tunnel mode, otherwise its own default route may loop through Qeli.

/etc/qeli/server.conf · [profile:main]
routing.client_to_client = true
/etc/qeli/users.conf
# user used by the exit client
[user:exit]
profiles = main
client_subnet = 0.0.0.0/0

# regular consumer user; route=0/0 is not needed here
[user:consumer]
profiles = main
exit client · [qeli]
gateway = false
exit_node = true
consumer client · [qeli]
gateway = true
kill_switch = true
Do not use server-pushed 0.0.0.0/0 as the switch. The shared client core filters overly broad pushed prefixes. Full tunnel is selected locally with gateway=true; client_subnet=0.0.0.0/0 on the exit user serves a different purpose: registering the path on the server.
Non-working combination: with exit_node=true and gateway=true, Qeli logs a warning but continues startup. The WAN path is then lost, so forwarding fails; fix the configuration before starting.
iptables

What Qeli configures on the exit client

On connection Qeli enables forwarding, relaxes reverse-path filtering on TUN/WAN and creates tagged rules. The WAN is selected from the lowest-metric default route, with the route to 1.1.1.1 as a fallback.

LayerAction
sysctlnet.ipv4.ip_forward=1 and rp_filter=0 on TUN and WAN
mangleMarks TUN → WAN packets
natSource-address translation (MASQUERADE) only for marked packets leaving through the WAN
filterAllows FORWARD in both directions and installs TCPMSS clamping

Rules are idempotent, persist across reconnects and are removed on a clean client stop. Every rule has the qeli-exit-node comment.

After startup

Verify the path and NAT

journalctl -u qeli-client -b | grep 'Exit-node engaged'confirms that exit mode is active
curl -s https://api.ipify.org ; echoon the consumer, should show the exit node's public IP
sudo iptables -t nat -L POSTROUTING -v -n | grep MASQUERADEcounters should grow with consumer traffic
sudo iptables -t mangle -S | grep qeli-exit-nodeshows packet-marking rules
sudo iptables -S | grep qeli-exit-nodeshows FORWARD and TCPMSS rules
After a crash

Remove only Qeli-tagged rules

After a crash the process cannot run cleanup. First list rules with the qeli-exit-node comment, then replace -A with -D for each exact rule and run it in the same table. Do not flush entire chains.

inventory before removal
sudo iptables -t mangle -S | grep qeli-exit-node
sudo iptables -t nat -S | grep qeli-exit-node
sudo iptables -S | grep qeli-exit-node
Restore sysctl values deliberately. Set net.ipv4.ip_forward=0 and the previous rp_filter value only if they were actually different before Qeli; the host may serve other routing roles.
Mode boundaries

Platform, cost and responsibility

Exit platformA Linux client with iptables and network privileges only; consumers may use any current client
PathThe extra network hop increases latency and consumes bandwidth on both the server and exit node
Public IPExternal services see the exit node's address; its owner is responsible for outgoing traffic
The server itselfTo egress through the server's public IP, use normal full tunnel and server NAT; exit_node is unnecessary
Primary sources

v0.8.1 implementation and source documentation