TTR Networks

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

Sharing connections with conntrackd

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

conntrackd watches the kernel's connection table and sends every change to the other router over the sync link. Debian's default configuration only collects statistics, so it needs a sync configuration on both routers.

  • Mode FTFW (fault-tolerant firewall): a reliable protocol on top of UDP, with acknowledgements and resends, so a lost update doesn't leave a hole in the other router's table.
  • DisableExternalCache On: the other router's connections go straight into this router's kernel table. A backup router then always has every connection ready, and a failover needs no extra steps. (The alternative keeps them in conntrackd's own cache, and a keepalived notify script commits them into the kernel when the router becomes master.)
  • UDP: this router's address on the sync link, the other router's address, and the port. The firewall allows that port, on that interface only.
  • The filter syncs TCP, UDP and ICMP, and ignores the sync link's own traffic and loopback.

With conntrackd restarted on both routers, the stream's connection appears in both kernel tables: on r1, where it really goes through, and on r2, put there by conntrackd. conntrackd -s on r2 counts the injected connections and the sync traffic.

The same failure, with sync

r1 is killed again, with a new stream running. The ping gap is about the same, 3.3 seconds, because it's VRRP's timing. But this time the stream paused for 3.5 seconds, then carried on from line 28, to the end of the test. r2 already knew the connection and its NAT translation, so the web server's packets matched, and TCP retransmitted the few that were lost. The person on the staff PC saw a short stall, not a broken connection.

/etc/conntrackd/conntrackd.conf on r1
# /etc/conntrackd/conntrackd.conf on r1: send our connection table to the other router, and
# put theirs straight into our kernel, so either router can carry on any connection.
Sync {
	Mode FTFW {
		DisableExternalCache On
	}
	UDP {
		IPv4_address 10.99.0.1
		IPv4_Destination_Address 10.99.0.2
		Port 3780
		Interface eth2
		Checksum on
	}
}

General {
	HashSize 8192
	HashLimit 65535
	Syslog on
	LockFile /var/lock/conntrackd.lock
	UNIX {
		Path /var/run/conntrackd.sock
	}
	Systemd on
	Filter From Userspace {
		Protocol Accept {
			TCP
			UDP
			ICMP
		}
		Address Ignore {
			IPv4_address 127.0.0.1
			IPv4_address 10.99.0.0/30
		}
	}
}
  • Lines 4–6: FTFW, with the other router's connections put straight into the kernel.
  • Lines 7–13: r1's sync address, r2's, and the port. On r2 the two addresses are swapped.
  • Lines 25–35: Sync TCP, UDP and ICMP, but not loopback or the sync link itself.
Sync on, and the same failure (real output)
root@r1:~# cp conntrackd.conf /etc/conntrackd/ && systemctl restart conntrackd

root@r2:~# cp conntrackd.conf /etc/conntrackd/ && systemctl restart conntrackd

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=44080 dport=9000 src=198.51.100.10 dst=203.0.113.4 sport=9000 dport=44080 [ASSURED] mark=0 use=1

root@r2:~# conntrack -L -p tcp --dport 9000 2>/dev/null
tcp      6 431994 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

root@r2:~# conntrackd -s | sed -n "/external inject/,/^$/p;/UDP traffic/,/^$/p"
external inject:
connections created:		           2	failed:	           0
connections updated:		           5	failed:	           0
connections destroyed:		           0	failed:	           0

UDP traffic (active device=eth2):
                 660 Bytes sent                  636 Bytes recv
                   9 Pckts sent                   10 Pckts recv
                   0 Error send                    0 Error recv


root@staff:~# awk '/bytes from/{print $1}' /tmp/ping2.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/stream2.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/stream2.log
ping: no replies for 3.3 s
22:48:29.898 126 22:48:29
22:48:30.101 127 22:48:30
stream: nothing for 3.5 s, then line 28
  • Lines 1–3: Both routers switch from statistics to sync.
  • Lines 5–9: The stream's connection, on r1 and, put there by conntrackd, on r2.
  • Lines 11–20: conntrackd on r2: connections injected into its kernel, and the sync traffic.
  • Lines 23–27: After killing r1: ping back in 3.3 s, and the stream paused 3.5 s, then carried on from line 28 to the end of the test.

Check your understanding

Why doesn't connection sync shorten the 3.3-second gap?

  • The gap is VRRP noticing r1 is gone (three missed advertisements); sync only decides whether open connections survive it.
  • conntrackd is slow to copy connections.
  • TCP has to reconnect.
  • It does; without sync the gap was longer.