Skip to content

Policy-Based Routing Internals

This page explains the kernel mechanisms behind the “Incoming Only” WireGuard configs. You don’t need this to use policy-based routing (the downloadable configs handle it), but it’s useful for debugging or customization.


The Linux “Incoming Only” config uses four kernel mechanisms working together:

Linux can have up to 252 custom routing tables (numbered 1–252). Each table is a complete, independent routing table. The config creates one dedicated table (numbered by your IP’s third octet, e.g. 198 for 134.49.198.16) with a single rule: default route via the WireGuard interface.

Terminal window
ip route add default dev getrealip table 198

This table says “if you consult me, send everything out getrealip.” But nothing consults it yet — you need a policy rule.

Policy routing lets you say “packets with mark X should use table Y instead of the main routing table.”

Terminal window
ip rule add fwmark 0xc6 table 198

Now any packet stamped with mark 0xc6 will be routed via table 198 (→ out getrealip). Packets without the mark use the normal default route (→ your ISP).

3. Packet marks (fwmark via iptables mangle)

Section titled “3. Packet marks (fwmark via iptables mangle)”

When a packet arrives on the WireGuard interface, we stamp it:

Terminal window
iptables -t mangle -A PREROUTING -i getrealip -j CONNMARK --set-mark 0xc6

This marks the connection (not just the packet) in the kernel’s conntrack table. Every future packet belonging to this connection — including replies generated locally — inherits the mark.

When your machine generates a reply, it arrives in the OUTPUT chain with no interface mark (it was locally generated). But conntrack remembers this connection was marked. The OUTPUT rule restores the mark:

Terminal window
iptables -t mangle -A OUTPUT -m connmark --mark 0xc6 -j MARK --set-mark 0xc6

Now the reply packet has fwmark 0xc6 → policy rule sends it to table 198 → routed out getrealip → back through the tunnel.

Remote client → Internet → Concentrator → WireGuard tunnel → getrealip interface
→ PREROUTING: connmark set 0xc6 (connection tracked)
→ Delivered to local process (e.g. your web server)
→ Process generates reply
→ OUTPUT: connmark restored → fwmark 0xc6 set
→ Routing decision: fwmark 0xc6 → table 198 → default dev getrealip
→ Reply goes back through tunnel → Concentrator → Internet → Remote client

Meanwhile, your outbound traffic (you browsing the web, DNS, updates) has no connmark, no fwmark, and routes normally via your ISP.

WireGuard’s AllowedIPs serves two purposes:

  1. Routing: which destinations to send into the tunnel (disabled by Table = off)
  2. Filtering: which source IPs to accept from the tunnel

You need 0.0.0.0/0 on the filtering side because inbound packets from the internet arrive with arbitrary source IPs (remote clients connecting to your static IP). If you restricted AllowedIPs to just your own IP, WireGuard would drop all inbound traffic.

Table = off prevents 0.0.0.0/0 from creating a default route — so it only affects filtering, not routing.


macOS’s packet filter (pf) has a built-in mechanism that does what Linux needs 4 rules to accomplish. The reply-to directive tells pf: “for any connection that arrives on this interface, force replies back out the same interface.”

pass in on utun4 reply-to utun4 all

pf maintains connection state internally and handles the return routing automatically. No marks, no custom tables.


Same reply-to mechanism as raw pf, but configured via the GUI:

pfSense: Firewall → Rules → WireGuard interface → Edit rule → Advanced → Reply-to → select WireGuard gateway

OPNsense: Does this automatically when the WireGuard gateway is properly configured. If not, same location: Firewall → Rules → WireGuard → Advanced → Reply To.

Under the hood, both generate pf rules like:

pass in quick on tun_wg0 reply-to (tun_wg0 10.0.0.1) all