TTR Networks

Intermediate · Two Debian routers: VRRP and connection sync · section 5 of 9

NAT to the shared address

✓ Lab-tested 4 November 2026 on Debian 13.7, keepalived 2.3.3, conntrackd 1.4.8, nftables 1.1.3, systemd 257

The base lesson's router uses masquerade: NAT to whatever address the WAN interface has. With two routers, that's a trap.

Masquerade picks the router's own address. On r1, that's 203.0.113.2. conntrackd faithfully copies the connection to r2, translation and all, and the copy says: replies to 203.0.113.2. But 203.0.113.2 is r1's address, and r1 is dead. The web server sends its replies to an address that nobody answers for any more.

The lab shows it. With masquerade on both routers and sync on, r2's table has the new stream with replies going to 203.0.113.2. After r1 is killed, the stream stopped at line 27 and never resumed, and even the ping died: 26 replies, then nothing for the rest of the test, since its replies were also going to r1's address.

So both routers translate to the shared address instead, with snat to 203.0.113.4: an address that moves with the master. The rule only applies to traffic from the office (ip saddr 10.10.0.0/16), so each router's own traffic, such as its health-check pings and conntrackd, keeps its own address.

/etc/nftables.conf on r1 and r2
#!/usr/sbin/nft -f
# /etc/nftables.conf on r1 and r2 (the same file on both).
flush ruleset

define WAN = "eth0"
define SYNC = "eth2"

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 { "staff", $WAN } ip protocol 112 accept comment "VRRP from the other router"
    iifname $SYNC udp dport 3780 accept comment "conntrackd from the other router"
    iifname "staff" tcp dport 22 accept comment "SSH from staff only"
    counter comment "everything else to the router"
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
    ct state established,related accept
    ct state invalid drop
    iifname "staff" oifname $WAN accept comment "staff to the internet"
    counter comment "every other pair is dropped"
  }
}

table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat;
    oifname $WAN ip saddr 10.10.0.0/16 snat to 203.0.113.4 comment "the office uses the shared address"
  }
}
  • Line 15: VRRP from the other router, on both shared networks.
  • Line 16: conntrackd, only on the sync link.
  • Line 33: NAT to the shared address, which moves with the master; only for the office's traffic.
With masquerade instead (real output)
root@r1:~# sed -i "s/snat to 203.0.113.4/masquerade/" /etc/nftables.conf && systemctl reload nftables && nft list chain ip nat postrouting | grep -E "masquerade"
		oifname "eth0" ip saddr 10.10.0.0/16 masquerade comment "the office uses the shared address"

root@r2:~# sed -i "s/snat to 203.0.113.4/masquerade/" /etc/nftables.conf && systemctl reload nftables && nft list chain ip nat postrouting | grep -c masquerade
1

root@r2:~# conntrack -L -p tcp --dport 9000 2>/dev/null
tcp      6 431976 ESTABLISHED src=10.10.10.20 dst=198.51.100.10 sport=44080 dport=9000 src=198.51.100.10 dst=203.0.113.4 sport=9000 dport=44080 [ASSURED] mark=0 use=1
tcp      6 431994 ESTABLISHED src=10.10.10.20 dst=198.51.100.10 sport=48740 dport=9000 src=198.51.100.10 dst=203.0.113.2 sport=9000 dport=48740 [ASSURED] mark=0 use=1

root@staff:~# awk '/bytes from/{print $1}' /tmp/ping3.log | tr -d '[]' | awk 'NR>1 && $1-p>0.5 {printf "ping: no replies for %.1f s\n", $1-p} {p=$1}'; tail -2 /tmp/stream3.log; awk '{split($1,t,":"); s=t[1]*3600+t[2]*60+t[3]; if (NR>1 && s-p>0.5) printf "stream: nothing for %.1f s, then line %s\n", s-p, $2; p=s}' /tmp/stream3.log
22:48:58.755 26 22:48:58
22:48:58.954 27 22:48:58

root@r1:~# nft list chain ip nat postrouting | grep -c "snat to 203.0.113.4"
1
  • Lines 1–5: Both routers switched to masquerade.
  • Lines 7–9: r2 has the new stream (and an older one), but its replies go to 203.0.113.2: r1's own address.
  • Lines 11–13: After killing r1, the stream stopped at line 27.
  • Lines 15–16: Back to snat afterwards.
The ping during that test (real output)
root@staff:~# tail -2 /tmp/ping3.log
125 packets transmitted, 26 received, 79.2% packet loss, time 25788ms
rtt min/avg/max/mdev = 0.051/0.135/0.304/0.063 ms
  • Line 2: 26 replies before the failure, none after it.

Check your understanding

Why does masquerade break failover even with conntrackd?

  • It translates to the router's own WAN address, so the copied connections expect replies at an address that dies with that router.
  • conntrackd can't copy masqueraded connections.
  • Masquerade is slower than snat.
  • VRRP doesn't support NAT.