One default route isn't enough
Start with what most people do first: plug in the second ISP and leave a single default route, through ISP 1.
Outbound traffic is fine. The office reaches the internet through ISP 1, masqueraded as 198.51.100.2.
Inbound is half broken. The internet can reach the router's ISP 1 address, but not its ISP 2 address, and the port forward only works through ISP 1. The requests do arrive on ISP 2's link. The problem is the reply: the router has only one default route, so the reply leaves through ISP 1, still carrying ISP 2's address as its source. ISP 1 sees an address that isn't its own and drops it, as its filter's counter shows. ip route get … from 203.0.113.2 confirms it: even traffic from ISP 2's address is routed via ISP 1.
This is the central problem of having two ISPs: routing is chosen by destination, but the right exit depends on the source. Linux can do that, with more than one routing table.
# /etc/sysctl.d/99-router.conf: route between the office and both ISPs.
net.ipv4.ip_forward = 1#!/usr/sbin/nft -f
# /etc/nftables.conf on gw: firewall and NAT for an office with two ISPs.
flush ruleset
define WAN1 = "eth0"
define WAN2 = "eth1"
define LAN = "eth2"
define WEBSERVER = 10.20.0.10
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
icmp type { echo-request, destination-unreachable, time-exceeded } accept
iifname $LAN tcp dport 22 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iifname $LAN oifname { $WAN1, $WAN2 } accept comment "the office out through either ISP"
iifname { $WAN1, $WAN2 } oifname $LAN ct status dnat accept comment "port forwards"
}
}
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat;
iifname { $WAN1, $WAN2 } tcp dport 8080 dnat to $WEBSERVER:80 comment "the office web server, on both ISPs"
}
chain postrouting {
type nat hook postrouting priority srcnat;
oifname { $WAN1, $WAN2 } masquerade comment "leave with the address of whichever ISP is used"
}
}
include "/etc/nftables.d/*.nft"- Line 24: The office may leave through either ISP.
- Line 25: Port forwards from either ISP, as in the nftables lesson.
- Line 32: The office web server, published on both ISPs' addresses.
- Line 37: Masquerade picks the address of whichever interface a packet leaves by, so it's per ISP automatically.
- Line 41: Extra files go in /etc/nftables.d; the dual-ISP marks will be one.
root@gw:~# sysctl --system | grep ip_forward; nft -c -f /etc/nftables.conf && systemctl enable --now nftables && nft list tables
net.ipv4.ip_forward = 1
table inet filter
table ip nat
root@gw:~# ip route add default via 198.51.100.1 dev eth0 && ip route
default via 198.51.100.1 dev eth0
10.20.0.0/24 dev eth2 proto kernel scope link src 10.20.0.1
198.51.100.0/28 dev eth0 proto kernel scope link src 198.51.100.2
203.0.113.0/28 dev eth1 proto kernel scope link src 203.0.113.2
root@pc:~# curl -s -o /dev/null -w "%{http_code}\n" -m 3 http://192.0.2.80/
200
root@web:~# tail -1 /tmp/web.log
198.51.100.2 - - [21/Oct/2026 08:02:27] "GET / HTTP/1.1" 200 -- Lines 1–4: Forwarding and the firewall on.
- Lines 6–7: One default route, through ISP 1.
- Lines 12–16: The office reaches the internet as ISP 1's address.
root@web:~# ping -c 2 -W 1 198.51.100.2 | tail -2; ping -c 2 -W 1 203.0.113.2 | tail -2
2 packets transmitted, 2 received, 0% packet loss, time 1031ms
rtt min/avg/max/mdev = 0.056/0.063/0.070/0.007 ms
2 packets transmitted, 0 received, 100% packet loss, time 1023ms
root@isp1:~# nft list chain ip bcp38 customer | grep drop
iifname "eth0" ip saddr != 198.51.100.0/28 counter packets 2 bytes 168 drop
root@gw:~# ip route get 192.0.2.80 from 203.0.113.2
192.0.2.80 from 203.0.113.2 via 198.51.100.1 dev eth0 uid 0
cache
root@web:~# curl -s -o /dev/null -w "%{http_code}\n" -m 3 http://198.51.100.2:8080/; curl -s -o /dev/null -w "%{http_code}\n" -m 3 http://203.0.113.2:8080/
200
000- Lines 1–4: The router's ISP 1 address answers; its ISP 2 address doesn't.
- Lines 7–8: ISP 1 dropped the two replies: they carried ISP 2's address.
- Lines 10–11: Why: even traffic from 203.0.113.2 is routed via ISP 1.
- Lines 14–16: The same for the port forward: through ISP 1 it works, through ISP 2 it doesn't.
Check your understanding
A request arrives on ISP 2's link, but the router's reply leaves through ISP 1. Why does it fail?
- ISP 1 drops packets whose source isn't one of its own addresses.
- The router's firewall drops the reply.
- Masquerade changes the reply's address to ISP 1's.
- ISP 2 needs a return route.