Provide useful IPv4 policy-based routing or at least anti-spoofing by default

People might read documentation similar to https://openwrt.org/docs/guide-user/network/wan/simple_wan_failover and set up fail-over scenarios based purely on route metrics, without any policy-based routing.

I also made this mistake by configuring my USB LTE modem for the second WAN with metric = 90. The primary WAN is a PPPoE-based fiber connection with metric = 0.

This is a mistake because of the following consideration.

Suppose that LTE connects first, and the fiber connection comes up second. Devices in LAN could establish some connections to servers on the Internet in the meanwhile (i.e., while the LTE connection was the only working one). But then, the default route changes.

Servers on the Internet still send us packets over wwan0:

12:48:21.734832 IP 142.251.10.94.443 > 100.66.218.127.60968: Flags [P.], seq 0:73, ack 1, win 1041, options [nop,nop,TS val 523506531 ecr 3258400847], length 73

Yet, the replies go out via pppoe-wan, using the IP address of wwan0:

12:48:21.734915 IP 100.66.218.127.60968 > 142.251.10.94.443: Flags [R], seq 3538110809, win 0, length 0

This constitutes IP spoofing and, for some ISPs, even a single spoofed packet is a ground for contract termination.

Please make sure that OpenWrt, when NAT is enabled on a WAN connection, never sends any packets out with the "wrong" source address, unless a specific range is excluded from NAT. Even when a user follows such misguided guides.

EDIT: let me make this more specific.

The default firewall should examine each outgoing packet at the last possible point before sending it out, when all addresses in the packet header are final, regardless of whether it has undergone a NAT or not.

As a general rule, if masquerading is enabled on the interface, and the source address of the packet does not match any of the addresses assigned to the outgoing logical interface, then drop the packet. Even better, in addition to the safety check mentioned in the previous sentence (not instead!), make sure that the packet is not routed to the wrong interface in the first place.

Of course, facilities should be provided when creating a NAT rule to override this check. For example, it would be needed if a certain source or destination prefix, or their combination, is excluded from NAT.

Note that there is already a firewall rule with the purpose to prevent NAT leakage:

table inet fw4 {
        chain accept_to_wan {
                meta nfproto ipv4 oifname { "wan", "wwan0", "pppoe-wan" } ct state invalid counter packets 97 bytes 3880 drop comment "!fw4: Prevent NAT leakage"
                oifname { "wan", "wwan0", "pppoe-wan" } counter packets 1049 bytes 214309 accept comment "!fw4: accept wan IPv4/IPv6 traffic"
        }
}

But it only deals with leakage from connections established before the router has rebooted, not leakage from changed routing due to a lower-metric interface going up—and that's the subject of this request.

1 Like

Is it sysctl net.ipv4.icmp_errors_use_inbound_ifaddr=1 ?

No, I have not customized any sysctls.

root@OpenWrt:~# sysctl net.ipv4.icmp_errors_use_inbound_ifaddr
net.ipv4.icmp_errors_use_inbound_ifaddr = 0

Try?

Is the rst coming from socket or from ntables?

I believe (but have no proof) that the RST is coming from the laptop. The IP in question belongs to Google. I will try it once I have a safer setup (in GNS3?) that would not involve sending any spoofed packets to my ISP.

What you see is data leak...
Try enabling global "drop invalid" flag in firewall, it sort of is by default enabled on wan without global flag.

I trust you you configured all correctly, but lets check it :wink:

Please connect to your OpenWrt device using ssh and copy the output of the following commands and post it here using the "Preformatted text </> " button:
grafik
Remember to redact passwords, MAC addresses and any public IP addresses you may have:

ubus call system board
cat /etc/config/network
cat /etc/config/dhcp
cat /etc/config/firewall
root@OpenWrt:~# ubus call system board
{
	"kernel": "6.6.67",
	"hostname": "OpenWrt",
	"system": "ARMv8 Processor rev 4",
	"model": "Linksys E8450 (UBI)",
	"board_name": "linksys,e8450-ubi",
	"rootfs_type": "squashfs",
	"release": {
		"distribution": "OpenWrt",
		"version": "24.10.0-rc4",
		"revision": "r28211-d55754ce0d",
		"target": "mediatek/mt7622",
		"description": "OpenWrt 24.10.0-rc4 r28211-d55754ce0d",
		"builddate": "1734915335"
	}
}
root@OpenWrt:~# cat /etc/config/network

config interface 'loopback'
	option device 'lo'
	option proto 'static'
	option ipaddr '127.0.0.1'
	option netmask '255.0.0.0'

config globals 'globals'
	option ula_prefix 'fd7b:7d49:75e3::/48'
	option packet_steering '1'

config device
	option name 'br-lan'
	option type 'bridge'
	list ports 'lan1'
	list ports 'lan2'
	list ports 'lan3'
	list ports 'lan4'

config interface 'lan'
	option device 'br-lan'
	option proto 'static'
	option ipaddr '192.168.10.1'
	option netmask '255.255.255.0'
	option ip6assign '64'
	list ip6class 'local'
	list ip6class 'wan_6'
	# Updated dynamically with a script
	# See https://forum.openwrt.org/t/ipv6-wan-fail-over-without-ipv6-nat/146403/66

config interface 'modem'
	option proto 'static'
	option ipaddr '192.168.254.2'
	option netmask '255.255.255.0'
	option defaultroute '0'
	option delegate '0'
	option device 'wan'
	option mtu '1508'

config interface 'lte'
	option proto 'ncm'
	option pdptype 'IPV4V6'
	option ipv6 'auto'
	option peerdns '0'
	option metric '90'
	option apn 'internet'
	option username 'lte'
	option password 'lte'
	option delay '20'
	option device '/dev/cdc-wdm0'
	option mode 'preferlte'
	list dns '76.76.2.0'
	list dns '76.76.10.0'
	list dns '2606:1a40::'
	list dns '2606:1a40:1::'
	option dns_metric '90'

config interface 'wan'
	option device '@modem'
	option proto 'pppoe'
	option username '<censored>'
	option password '<censored>'
	option ipv6 'auto'
	option keepalive '0 1'
	option peerdns '0'
	list dns '8.8.8.8'
	list dns '8.8.4.4'
	option mtu '1508'
	option metric '80'
	option host_uniq 'cd1a6054'
	# host_uniq helps against the "too many logins" error on reboot

config switch
	option name 'switch0'
	option reset '1'
	option enable_vlan '1'

config switch_vlan
	option device 'switch0'
	option vlan '1'
	option ports '2 3 4 5 0t'

config switch_vlan
	option device 'switch0'
	option vlan '2'
	option ports '1 6t'

config device
	option name 'eth0'
	option mtu '1508'

config interface 's2s'
	option proto 'wireguard'
	option private_key '<censored>'
	option mtu '1400'
	option peerdns '0'
	option metric '110'
	option delegate '0'
	option nohostroute '1'
	option listen_port '50056'

config wireguard_s2s
	option description '<censored>'
	option public_key '<censored>'
	list allowed_ips '0.0.0.0/0'
	list allowed_ips '::/0'
	option endpoint_host '<censored>'
	option endpoint_port '51822'
	option persistent_keepalive '30'

config route
	option interface 's2s'
	option target '192.168.4.0/24'
	option source '192.168.10.1'

# <snip> a few other IPv4 routes through s2s

config route6
	option interface 's2s'
	option target 'fd25:b1c2:efb1::/48'
root@OpenWrt:~# cat /etc/config/dhcp

config dnsmasq
	option domainneeded '1'
	option localise_queries '1'
	option rebind_protection '0'
	option local '/lan/'
	option domain 'lan'
	option expandhosts '1'
	option cachesize '1000'
	option authoritative '1'
	option readethers '1'
	option leasefile '/tmp/dhcp.leases'
	option localservice '1'
	option ednspacket_max '1232'
	option noresolv '1'
	option localuse '1'
	# Stubby
	list server '127.0.0.1#5453'
	list server '0::1#5453'
	# Prevent Cisco in GNS3 from phoning home
	list server '/xml.cisco.com/'

config dhcp 'lan'
	option interface 'lan'
	option start '100'
	option limit '150'
	option leasetime '12h'
	option dhcpv4 'server'
	option ra 'server'
	option force '1'
	option ra_default '2'
	# Trick route so that these nets bypass any VPN on my laptop
	list dhcp_option 'option:classless-static-route,192.168.254.0/24,192.168.10.1,192.168.4.0/24,192.168.10.1'
	list ra_flags 'none'
	option preferred_lifetime '10m'

config dhcp 'wan'
	option interface 'wan'
	option ignore '1'

config odhcpd 'odhcpd'
	option maindhcp '0'
	option leasefile '/tmp/hosts/odhcpd'
	option leasetrigger '/usr/sbin/odhcpd-update'
	option loglevel '4'

config host
	option name 'aep-haswell'
	option ip '192.168.10.235'
	option mac 'D4:3D:7E:D9:0A:39'

config host
	option name 'dell-laptop'
	option ip '192.168.10.234'
	option mac '10:51:07:8E:75:60'

config host
	option name 'Netgear'
	option dns '1'
	list mac '34:98:B5:19:B6:16'
	list mac '32:98:B5:19:B6:19'
	option ip '192.168.10.2'
root@OpenWrt:~# cat /etc/config/firewall

config defaults
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'REJECT'
	option synflood_protect '1'
	option flow_offloading '1'

config zone
	option name 'lan'
	list network 'lan'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'

config zone
	option name 'wan'
	list network 'wan'
	list network 'lte'
	list network 'modem'
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'REJECT'
	option masq '1'
	option mtu_fix '1'

config zone
	option name 'ruvpn'
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	option mtu_fix '1'
	list network 's2s'

config forwarding
	option src 'lan'
	option dest 'ruvpn'

config forwarding
	option src 'lan'
	option dest 'wan'

config rule
	option name 'Allow-DHCP-Renew'
	option src 'wan'
	option proto 'udp'
	option dest_port '68'
	option target 'ACCEPT'
	option family 'ipv4'

config rule
	option name 'Allow-Ping'
	option src 'wan'
	option proto 'icmp'
	option icmp_type 'echo-request'
	option family 'ipv4'
	option target 'ACCEPT'

config rule
	option name 'Allow-IGMP'
	option src 'wan'
	option proto 'igmp'
	option family 'ipv4'
	option target 'ACCEPT'

config rule
	option name 'Allow-DHCPv6'
	option src 'wan'
	option proto 'udp'
	option dest_port '546'
	option family 'ipv6'
	option target 'ACCEPT'

config rule
	option name 'Allow-MLD'
	option src 'wan'
	option proto 'icmp'
	option src_ip 'fe80::/10'
	list icmp_type '130/0'
	list icmp_type '131/0'
	list icmp_type '132/0'
	list icmp_type '143/0'
	option family 'ipv6'
	option target 'ACCEPT'

config rule
	option name 'Allow-ICMPv6-Input'
	option src 'wan'
	option proto 'icmp'
	list icmp_type 'echo-request'
	list icmp_type 'echo-reply'
	list icmp_type 'destination-unreachable'
	list icmp_type 'packet-too-big'
	list icmp_type 'time-exceeded'
	list icmp_type 'bad-header'
	list icmp_type 'unknown-header-type'
	list icmp_type 'router-solicitation'
	list icmp_type 'neighbour-solicitation'
	list icmp_type 'router-advertisement'
	list icmp_type 'neighbour-advertisement'
	option limit '1000/sec'
	option family 'ipv6'
	option target 'ACCEPT'

config rule
	option name 'Allow-ICMPv6-Forward'
	option src 'wan'
	option dest '*'
	option proto 'icmp'
	list icmp_type 'echo-request'
	list icmp_type 'echo-reply'
	list icmp_type 'destination-unreachable'
	list icmp_type 'packet-too-big'
	list icmp_type 'time-exceeded'
	list icmp_type 'bad-header'
	list icmp_type 'unknown-header-type'
	option limit '1000/sec'
	option family 'ipv6'
	option target 'ACCEPT'

config rule
	option name 'Allow-IPSec-ESP'
	option src 'wan'
	option dest 'lan'
	option proto 'esp'
	option target 'ACCEPT'

config rule
	option name 'Allow-ISAKMP'
	option src 'wan'
	option dest 'lan'
	option dest_port '500'
	option proto 'udp'
	option target 'ACCEPT'

The global "drop invalid" checkbox was indeed off.

EDIT: tested by enabling the global "drop invalid" checkbox and restarting the pppoe-wan interface.

No packets leaked on wwan0 with the source IP that officially belongs to pppoe-wan, but the opposite leak still exists:

# Tailscale on the laptop
09:23:09.938604 pppoe-wan Out IP 100.66.218.127 > x.y.z.5: ICMP 100.66.218.127 udp port 41641 unreachable, length 160
09:23:10.162403 pppoe-wan Out IP 100.66.218.127 > x.y.z.5: ICMP 100.66.218.127 udp port 41641 unreachable, length 160
# Stubby on the router
09:23:12.097386 pppoe-wan Out IP 100.66.218.127.37102 > 94.140.14.140.853: Flags [R], seq 3885673560, win 0, length 0
09:23:12.482406 pppoe-wan Out IP 100.66.218.127.57948 > 94.140.14.141.853: Flags [R], seq 3226844999, win 0, length 0

EDIT 2: the sysctl did not help either.

I think the crux of the issue is that the packets are not invalid from the viewpoint of connection tracking. They do belong to an established connection (trying to terminate it, in this case), it's just the routing that has changed and sent them through a different interface. Of course, they should not have been sent out at all, as the only situation where they would have the intended effect is when the ISP allows customers to spoof IPs.

Cannot make your config better.

Some other things to try, not sure any/all would work:

  • A without offload (i know suffering it brings)
  • B enable strict rp_filter - goes away with reboot, official way is via netifd per-interface which will remain unchanged during experiment.
sysctl net.ipv4.conf.default.rp_filter=1 net.ipv4.conf.all.rp_filter=1
#maybe log too
sysctl net.ipv4.conf.default.log_martians=1 net.ipv4.conf.all.log_martians=1
  • C IMO improved ordering of "drop invalid" and "offload" application points
    https://github.com/openwrt/firewall4/pull/22/files
    just replace (raw) ruleset.uc on the device, original can be found relative to /rom/, changed file goes with any sysupgrade either way.

PS i dial wwan when wan is down for X minutes, so it is rarely dual-route.