Bookmark

PBR with netifd on OpenWrt: split VPN, WAN, and exception routes

I use netifd to separate routing decisions instead of piling several default routes into one table. The official guide covers both IPv4 and IPv6, so this adaptation keeps every meaningful scenario rather than reducing it to one generic VPN configuration.1

Scope and limits

PBR selects a path from administrator-defined policies. With pbr_netifd, each interface receives its own routing table and rules with explicit priorities select the table for matching traffic. This is suitable for interfaces, subnets, IP addresses, and ports; domain matching belongs in PBR app. mwan3 is for multi-WAN load balancing or failover, not a direct replacement for the individual rules below.1

Before changing anything, I need working lan, vpn, wan, or wan6 interfaces, SSH access, a known route to the VPN endpoint, and backups of /etc/config/network and /etc/config/firewall. Each block ends with a commit and restart, so I apply one scenario at a time and test it before moving on.

Guidelines

  • Assign every interface to a separate routing table and let netifd create the base routes.
  • Keep default routes enabled on upstream interfaces, but do not duplicate identical static routes across tables.
  • Put tunnel endpoints and local networks in specific rules before broad default-route rules.
  • A priority around 30000 commonly precedes the main table; 4000080000 follows it. Confirm the actual order with ip rule after reload.
  • For IPv6, account for NAT/NPT and source filtering according to the topology; an IPv4-only policy does not prevent IPv6 leaks.1

Instructions

The UCI syntax below is preserved from the source. Names such as lan, vpn, dmz, wan, wan6, and wgclient, along with table IDs, are assumptions of the examples. Map them to the real router configuration consistently rather than changing only half of a block.

Route LAN to VPN and DMZ to WAN

The first example assigns lan to table 1, vpn to table 2, and dmz to table 3, then makes LAN traffic look up table 2 at priority 30000. The DMZ default route remains in the WAN table supplied by the existing setup.1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for IPV in 4 6
do
uci set network.lan.ip${IPV}table="1"
uci set network.vpn.ip${IPV}table="2"
uci set network.dmz.ip${IPV}table="3"
uci -q delete network.lan_vpn${IPV%4}
uci set network.lan_vpn${IPV%4}="rule${IPV%4}"
uci set network.lan_vpn${IPV%4}.in="lan"
uci set network.lan_vpn${IPV%4}.lookup="2"
uci set network.lan_vpn${IPV%4}.priority="30000"
done
uci commit network
service network restart

Route VPN server LAN via VPN client

This assumes a WireGuard client peer already exists. The two allowed_ips entries make the peer accept IPv4 and IPv6 default routes, after which the LAN is directed into the VPN table.1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
uci add_list network.wgclient.allowed_ips="0.0.0.0/0"
uci add_list network.wgclient.allowed_ips="::/0"
for IPV in 4 6
do
uci set network.lan.ip${IPV}table="1"
uci set network.vpn.ip${IPV}table="2"
uci -q delete network.lan_vpn${IPV%4}
uci set network.lan_vpn${IPV%4}="rule${IPV%4}"
uci set network.lan_vpn${IPV%4}.in="lan"
uci set network.lan_vpn${IPV%4}.lookup="2"
uci set network.lan_vpn${IPV%4}.priority="30000"
done
uci commit network
service network restart

Route LAN to VPN with failover to WAN

This example assigns wan/wan6 to table 2 with priority 40000, allowing WAN to act as the fallback after the VPN-specific rule no longer applies. I still verify the tunnel state and ip rule output rather than assuming failover from the presence of the UCI entries.1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for IPV in 4 6
do
uci set network.lan.ip${IPV}table="1"
uci set network.wan${IPV%4}.ip${IPV}table="2"
uci -q delete network.lan_wan${IPV%4}
uci set network.lan_wan${IPV%4}="rule${IPV%4}"
uci set network.lan_wan${IPV%4}.in="lan"
uci set network.lan_wan${IPV%4}.lookup="2"
uci set network.lan_wan${IPV%4}.priority="40000"
done
uci commit network
service network restart

Route LAN to VPN by IP set

The source creates an IP set containing WAN networks, marks LAN traffic whose destination does not match those networks with 0x1, and then looks up the VPN table. This is the branch for keeping selected destinations outside the VPN.1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
for IPV in 4 6
do
uci -q delete firewall.wan_set${IPV%4}
uci set firewall.wan_set${IPV%4}="ipset"
uci set firewall.wan_set${IPV%4}.name="wan${IPV%4}"
uci set firewall.wan_set${IPV%4}.family="ipv${IPV}"
uci set firewall.wan_set${IPV%4}.match="net"
uci -q delete firewall.lan_mark${IPV%4}
uci set firewall.lan_mark${IPV%4}="rule"
uci set firewall.lan_mark${IPV%4}.name="Mark-LAN-VPN"
uci set firewall.lan_mark${IPV%4}.src="lan"
uci set firewall.lan_mark${IPV%4}.dest="*"
uci set firewall.lan_mark${IPV%4}.ipset="!wan${IPV%4} dest"
uci set firewall.lan_mark${IPV%4}.proto="all"
uci set firewall.lan_mark${IPV%4}.family="ipv${IPV}"
uci set firewall.lan_mark${IPV%4}.set_mark="0x1"
uci set firewall.lan_mark${IPV%4}.target="MARK"
uci set network.lan.ip${IPV}table="1"
uci set network.vpn.ip${IPV}table="2"
uci -q delete network.lan_vpn${IPV%4}
uci set network.lan_vpn${IPV%4}="rule${IPV%4}"
uci set network.lan_vpn${IPV%4}.in="lan"
uci set network.lan_vpn${IPV%4}.mark="1"
uci set network.lan_vpn${IPV%4}.lookup="2"
uci set network.lan_vpn${IPV%4}.priority="30000"
done
uci commit firewall
uci commit network
service firewall restart
service network restart

Route LAN to VPN with WAN port forwarding

This example marks HTTPS traffic from the LAN host with MAC 00:11:22:33:44:55 and source port 443, then sends the marked traffic to the WAN table. The MAC and port are source examples; I replace them with the actual server and verify the separate firewall forward/redirect rules.1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
uci -q delete firewall.lan_web
uci set firewall.lan_web="rule"
uci set firewall.lan_web.name="Mark-HTTPS"
uci set firewall.lan_web.src="lan"
uci set firewall.lan_web.src_mac="00:11:22:33:44:55"
uci set firewall.lan_web.src_port="443"
uci set firewall.lan_web.dest="*"
uci set firewall.lan_web.proto="tcp"
uci set firewall.lan_web.set_mark="0x1"
uci set firewall.lan_web.target="MARK"
for IPV in 4 6
do
uci set network.lan.ip${IPV}table="1"
uci set network.wan${IPV%4}.ip${IPV}table="2"
uci -q delete network.lan_web${IPV%4}
uci set network.lan_web${IPV%4}="rule${IPV%4}"
uci set network.lan_web${IPV%4}.in="lan"
uci set network.lan_web${IPV%4}.mark="1"
uci set network.lan_web${IPV%4}.lookup="2"
uci set network.lan_web${IPV%4}.priority="30000"
done
uci commit firewall
uci commit network
service firewall restart
service network restart

Route LAN to OpenVPN

The source assumes an OpenVPN interface named vpn. It places 0.0.0.0/0 and ::/0 in table 2, then makes LAN traffic look up that table.1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
for IPV in 4 6
do
uci set network.lan.ip${IPV}table="1"
uci set network.vpn.ip${IPV}table="2"
uci -q delete network.vpn_rt${IPV%4}
uci set network.vpn_rt${IPV%4}="route${IPV%4}"
uci set network.vpn_rt${IPV%4}.interface="vpn"
uci -q delete network.lan_vpn${IPV%4}
uci set network.lan_vpn${IPV%4}="rule${IPV%4}"
uci set network.lan_vpn${IPV%4}.in="lan"
uci set network.lan_vpn${IPV%4}.lookup="2"
uci set network.lan_vpn${IPV%4}.priority="30000"
done
uci set network.vpn_rt.target="0.0.0.0/0"
uci set network.vpn_rt6.target="::/0"
uci commit network
service network restart

Route LAN to Tailscale

Tailscale can create its own rules. This example adds a rule at priority 30000 and uses goto="10000" to override the built-in rule chain, so I inspect ip rule after Tailscale starts.1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
for IPV in 4 6
do
uci set network.lan.ip${IPV}table="1"
uci -q delete network.lan_vpn${IPV%4}
uci set network.lan_vpn${IPV%4}="rule"
uci set network.lan_vpn${IPV%4}.in="lan"
uci set network.lan_vpn${IPV%4}.lookup="52"
uci set network.lan_vpn${IPV%4}.priority="30000"
uci -q delete network.pbr${IPV%4}
uci set network.pbr${IPV%4}="rule"
uci set network.pbr${IPV%4}.goto="10000"
uci set network.pbr${IPV%4}.priority="1"
done
uci commit network
service network restart

Prohibitive routes

Instead of allowing traffic to fall through to an unwanted default route, this block creates prohibit routes in the VPN table. The source assumes loopback is always up; I verify the result with ip route before applying it to a production router.1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
for IPV in 4 6
do
uci set network.vpn.ip${IPV}table="2"
uci -q delete network.vpn_ks${IPV%4}
uci set network.vpn_ks${IPV%4}="route${IPV%4}"
uci set network.vpn_ks${IPV%4}.interface="loopback"
uci set network.vpn_ks${IPV%4}.type="prohibit"
uci set network.vpn_ks${IPV%4}.metric="9000"
uci set network.vpn_ks${IPV%4}.table="2"
done
uci set network.vpn_ks.target="0.0.0.0/0"
uci set network.vpn_ks6.target="::/0"
uci commit network
service network restart

Prohibitive rules

The final branch creates a prohibit rule for LAN traffic at priority 32000. It is a useful boundary against leaks when the VPN has no usable route, but an incorrect priority can also block valid exceptions.1

Testing and rollback

After each scenario, run ip rule show, ip -6 rule show, ip route show table all, and ip -6 route show table all, then test the VPN endpoint. Use one LAN client, one destination that must use the VPN, and one that must use WAN. If connectivity is lost, restore the prepared /etc/config/network or /etc/config/firewall copy through console/SSH and restart the services; do not stack several scenarios before identifying the failing change.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for IPV in 4 6
do
uci -q delete network.lan_ks${IPV%4}
uci set network.lan_ks${IPV%4}="rule${IPV%4}"
uci set network.lan_ks${IPV%4}.in="lan"
uci set network.lan_ks${IPV%4}.action="prohibit"
uci set network.lan_ks${IPV%4}.priority="32000"
done
uci commit network
service network restart

Sources


  1. https://openwrt.org/docs/guide-user/network/routing/pbr_netifd — OpenWrt Wiki – PBR with netifd. Independently adapted from the complete 2025-12-30 snapshot; this is not a verbatim translation. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎


0 Bình luận

Góp Ý / Bình Luận / Đánh giá