A failover, without connection sync
Now the test that matters. With the long TCP stream running from the staff PC to the web server, and a ping alongside it, r1 is killed outright, as a power failure would.
The addresses move quickly. r2 entered MASTER, took over 10.10.10.1 and 203.0.113.4, and the ping recovered after a gap of 3.5 seconds, close to VRRP's expected 3.6. New connections work again from that moment.
The open connection doesn't survive. The stream stopped at line 22 and never resumed. Before the failure, r1's connection table had the stream, translated to 203.0.113.4; r2's table had nothing. After the failover, the web server's next packet arrived at r2, addressed to 203.0.113.4 and port 46026. r2 owns that address now, but has no connection that matches, so to its firewall the packet is a new connection to the router itself, and the input chain's policy dropped it: the counter at the end of the chain caught five.
For a web page, that's a short glitch: the browser opens a new connection. For a video call, an SSH session, a VPN tunnel or a big download, it's a dropped call, a lost session or a failed transfer. That's what connection sync is for.
When r1 comes back
When r1 restarts, its priority is still higher, so by default it takes the addresses back (preemption), and r2 returns to BACKUP. That's a second, planned failover, with its own few seconds of disruption. If you'd rather keep the backup in charge until someone switches back on purpose, use nopreempt (both routers must then start as BACKUP), or preempt_delay to wait until the recovered router has settled.
root@r1:~# conntrack -L -p tcp --dport 9000 2>/dev/null
tcp 6 431999 ESTABLISHED src=10.10.10.20 dst=198.51.100.10 sport=46026 dport=9000 src=198.51.100.10 dst=203.0.113.4 sport=9000 dport=46026 [ASSURED] mark=0 use=1
root@r2:~# conntrack -L -p tcp --dport 9000 2>/dev/null | wc -l
0
root@r2:~# journalctl -u keepalived --no-pager -o short-precise | grep -E "MASTER" | tail -2; ip -br -4 addr show dev staff
Nov 04 22:47:28.877311 r2 Keepalived_vrrp[223]: VRRP_Group(OFFICE) Syncing instances to MASTER state
Nov 04 22:47:28.877336 r2 Keepalived_vrrp[223]: (STAFF) Entering MASTER STATE
staff@eth1 UP 10.10.10.3/24 10.10.10.1/24
root@staff:~# awk '/bytes from/{print $1}' /tmp/ping1.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/stream1.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/stream1.log
ping: no replies for 3.5 s
22:47:25.428 21 22:47:25
22:47:25.629 22 22:47:25
root@r2:~# conntrack -L -p tcp --dport 9000 2>/dev/null | wc -l; nft list chain inet filter input | grep "everything else"
0
counter packets 5 bytes 372 comment "everything else to the router"- Lines 1–2: On r1: the stream, translated to the shared address 203.0.113.4.
- Lines 4–5: r2 doesn't know it.
- Lines 7–10: r2 becomes master and takes 10.10.10.1.
- Lines 12–15: Ping came back after 3.5 s; the stream stopped at line 22 for good.
- Lines 17–19: Still unknown to r2; the web server's packets were dropped by the input chain.
root@r2:~# journalctl -u keepalived --no-pager -o short-precise | grep -E "BACKUP|MASTER" | tail -3
Nov 04 22:47:51.404360 r2 Keepalived_vrrp[223]: (WAN) Entering BACKUP STATE
Nov 04 22:47:51.404441 r2 Keepalived_vrrp[223]: VRRP_Group(OFFICE) Syncing instances to BACKUP state
Nov 04 22:47:51.404446 r2 Keepalived_vrrp[223]: (STAFF) Entering BACKUP STATE
root@r1:~# ip -br -4 addr show dev staff
staff@eth1 UP 10.10.10.2/24 10.10.10.1/24 - Lines 1–4: r1's higher priority wins: r2 steps back to BACKUP.
- Lines 6–7: r1 holds the gateway again.
Check your understanding
After the failover, why did r2 drop the web server's packets for the open stream?
- They matched no connection in r2's table, so the firewall saw a new, unrequested connection to the router itself.
- r2 didn't have the shared public address yet.
- The web server sent them to r1's MAC address.
- TCP connections can't change routers.