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.
How fwmark + connmark works
Section titled “How fwmark + connmark works”The Linux “Incoming Only” config uses four kernel mechanisms working together:
1. Routing tables
Section titled “1. Routing tables”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.
ip route add default dev getrealip table 198This table says “if you consult me, send everything out getrealip.” But nothing consults it yet — you need a policy rule.
2. Policy routing (ip rule)
Section titled “2. Policy routing (ip rule)”Policy routing lets you say “packets with mark X should use table Y instead of the main routing table.”
ip rule add fwmark 0xc6 table 198Now 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:
iptables -t mangle -A PREROUTING -i getrealip -j CONNMARK --set-mark 0xc6This 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.
4. Restoring marks on reply packets
Section titled “4. Restoring marks on reply packets”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:
iptables -t mangle -A OUTPUT -m connmark --mark 0xc6 -j MARK --set-mark 0xc6Now the reply packet has fwmark 0xc6 → policy rule sends it to table 198 → routed out getrealip → back through the tunnel.
The full flow
Section titled “The full flow”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 clientMeanwhile, your outbound traffic (you browsing the web, DNS, updates) has no connmark, no fwmark, and routes normally via your ISP.
Why AllowedIPs = 0.0.0.0/0?
Section titled “Why AllowedIPs = 0.0.0.0/0?”WireGuard’s AllowedIPs serves two purposes:
- Routing: which destinations to send into the tunnel (disabled by
Table = off) - 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: pf reply-to
Section titled “macOS: pf reply-to”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 allpf maintains connection state internally and handles the return routing automatically. No marks, no custom tables.
BSD routers: pfSense / OPNsense
Section titled “BSD routers: pfSense / OPNsense”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