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: interface-scoped route
Section titled “macOS: interface-scoped route”macOS (like the BSDs) already returns reply traffic on the interface a connection arrived on — the kernel tracks this without any packet-filter rules. So unlike Linux, there’s nothing to mark and no custom routing policy to install. The kernel only needs a valid egress route for the tunnel’s source address, otherwise it has no path to send replies.
The macOS “Incoming Only” config provides exactly that with a single scoped route:
route -q -n add -ifscope utun4 -inet default -interface utun4The -ifscope flag installs a default route scoped to that interface only. macOS scoped routing keeps it out of the global routing decision, so it never overrides the system’s primary default route (your ISP). It applies only to packets whose source is bound to the tunnel interface — the replies to inbound connections. Combined with Table = off (which prevents wg-quick from installing its own routes), the result is: inbound connections answered over the tunnel, outbound traffic unaffected.
The route is removed on PreDown, before the interface is torn down, since the delete requires the interface to still exist.
BSD routers: pfSense / OPNsense
Section titled “BSD routers: pfSense / OPNsense”pfSense and OPNsense use pf’s reply-to directive, which tells pf: “for any connection that arrives on this interface, force replies back out the same interface.” 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