Commit 77beaae871 for frr
commit 77beaae8713cd6d7f61258161e8048b73a2baf53
Author: Sougata Barik <sougatab@nvidia.com>
Date: Wed Oct 14 15:39:42 2026 +0530
tests: cover keyed install of every BGP BFD session type
Problem:
No topotest covered a single-hop BGP BFD session being deleted and
re-created (new discriminator, remote BFD Down) when BGP connects, for
any of the BGP BFD session types.
RCA:
See "bgpd: register single-hop BFD sessions already keyed". The session
types differ in when the egress interface, local address and peer address
become known, so each needs its own expectation:
- keyed before TCP: update-source (also on another interface than the
link), strict per peer and inherited from a peer-group (incl. numbered
link-local strict, guessed link-local source), VRF + BFD profile
- installed once at TCP connect: no update-source (IPv4/IPv6),
unnumbered, numbered link-local, unnumbered VLAN sub-interfaces sharing
one link-local via a peer-group, directly connected iBGP (multihop
until shared_network is learnt)
- unnumbered strict: peer address only learnt at BGP start, installed
once
- IPv4/IPv6 multihop: unchanged
Tests:
- New topotest bgp_bfd_singlehop_defer_install (r1 eBGP/iBGP to r2/r3,
one link per type above, pinned link-local addresses so the remote
static BFD sessions are deterministic):
- All single-hop sessions end up keyed with their egress interface and
source, are never re-created, the remote never sees BFD Down, BGP is
established exactly once; sub-interface sessions stay distinct.
- A peer with update-source on r1 lo stays keyed on the link across
BGP establishment.
- Strict with egress unknown falls back to an immediate install (no
deadlock).
- Clear BGP on numbered, unnumbered and sub-interface peers keeps the
BFD session.
- BFD removal while pending, 'neighbor bfd strict' while pending, and
'bfd strict' set at runtime on a peer-group with a pending member
install the session or clean it up.
- Without the fix: numbered sessions (incl. VRF and IPv6 without
update-source) are re-created and the remote sees BFD Down, and the
numbered link-local strict session is not Up before TCP. Unnumbered,
link-local, sub-interface and iBGP sessions already pass there (lib
link-local guard / multihop without source not sent) and act as
regression guards. All pass with the fix.
Ticket: #5324193
Signed-off-by: Sougata Barik <sougatab@nvidia.com>
diff --git a/tests/topotests/bgp_bfd_singlehop_defer_install/r1/frr.conf b/tests/topotests/bgp_bfd_singlehop_defer_install/r1/frr.conf
new file mode 100644
index 0000000000..bc46e40c3e
--- /dev/null
+++ b/tests/topotests/bgp_bfd_singlehop_defer_install/r1/frr.conf
@@ -0,0 +1,213 @@
+!
+bfd
+ profile slow
+ receive-interval 600
+ transmit-interval 600
+ exit
+exit
+!
+interface lo
+ ip address 198.51.100.1/32
+ ip address 198.51.100.11/32
+ ipv6 address 2001:db8:ff::1/128
+!
+interface r1-eth0
+ ip address 192.0.2.1/30
+!
+interface r1-eth1
+ ipv6 address 2001:db8:2::1/64
+!
+interface r1-eth2
+ ip address 192.0.2.5/30
+!
+interface r1-eth3
+ ipv6 address 2001:db8:4::1/64
+!
+interface r1-eth4
+ ip address 192.0.2.9/30
+!
+interface r1-eth5
+ ip address 192.0.2.13/30
+!
+interface r1-eth6
+ ipv6 address 2001:db8:6::1/64
+!
+interface r1-eth12
+ ip address 192.0.2.21/30
+!
+interface r1-eth13 vrf RED
+ ip address 192.0.2.17/30
+!
+interface r1-eth14
+ ip address 192.0.2.25/30
+!
+ip route 198.51.100.2/32 192.0.2.10
+ip route 198.51.100.3/32 192.0.2.2
+ip route 198.51.100.4/32 192.0.2.14
+ip route 198.51.100.5/32 192.0.2.10
+ipv6 route 2001:db8:ff::3/128 2001:db8:2::2
+!
+router bgp 65001
+ bgp router-id 198.51.100.1
+ no bgp ebgp-requires-policy
+ neighbor PG6 peer-group
+ neighbor PG6 remote-as external
+ neighbor PG6 timers 3 10
+ neighbor PG6 timers connect 1
+ neighbor PG6 bfd
+ neighbor PG6S peer-group
+ neighbor PG6S remote-as external
+ neighbor PG6S timers 3 10
+ neighbor PG6S timers connect 1
+ neighbor PG6S bfd
+ neighbor PG6S bfd strict
+ neighbor PG4S peer-group
+ neighbor PG4S remote-as external
+ neighbor PG4S timers 3 10
+ neighbor PG4S timers connect 1
+ neighbor PG4S bfd
+ neighbor PG4S bfd strict
+ neighbor PGU peer-group
+ neighbor PGU remote-as external
+ neighbor PGU timers 3 10
+ neighbor PGU timers connect 1
+ neighbor PGU bfd
+ !
+ ! IPv4 single-hop, no update-source
+ neighbor 192.0.2.2 remote-as external
+ neighbor 192.0.2.2 shutdown
+ neighbor 192.0.2.2 timers 3 10
+ neighbor 192.0.2.2 timers connect 1
+ neighbor 192.0.2.2 bfd
+ !
+ ! IPv6 global single-hop, peer-group, update-source <interface>
+ neighbor 2001:db8:2::2 peer-group PG6
+ neighbor 2001:db8:2::2 shutdown
+ neighbor 2001:db8:2::2 update-source r1-eth1
+ !
+ ! IPv4 single-hop, strict
+ neighbor 192.0.2.6 remote-as external
+ neighbor 192.0.2.6 shutdown
+ neighbor 192.0.2.6 timers 3 10
+ neighbor 192.0.2.6 timers connect 1
+ neighbor 192.0.2.6 bfd
+ neighbor 192.0.2.6 bfd strict
+ !
+ ! IPv6 global single-hop, strict via peer-group
+ neighbor 2001:db8:4::2 peer-group PG6S
+ neighbor 2001:db8:4::2 shutdown
+ !
+ ! single-hop, egress interface unknown until TCP is up
+ neighbor 198.51.100.2 remote-as external
+ neighbor 198.51.100.2 shutdown
+ neighbor 198.51.100.2 timers 3 10
+ neighbor 198.51.100.2 timers connect 1
+ neighbor 198.51.100.2 disable-connected-check
+ neighbor 198.51.100.2 bfd
+ !
+ ! multihop
+ neighbor 198.51.100.3 remote-as external
+ neighbor 198.51.100.3 shutdown
+ neighbor 198.51.100.3 timers 3 10
+ neighbor 198.51.100.3 timers connect 1
+ neighbor 198.51.100.3 ebgp-multihop 2
+ neighbor 198.51.100.3 update-source lo
+ neighbor 198.51.100.3 bfd
+ !
+ ! single-hop strict inherited from peer-group, egress interface unknown
+ ! until TCP is up
+ neighbor 198.51.100.4 peer-group PG4S
+ neighbor 198.51.100.4 shutdown
+ neighbor 198.51.100.4 disable-connected-check
+ !
+ ! IPv6 global single-hop, no update-source
+ neighbor 2001:db8:6::2 peer-group PG6
+ neighbor 2001:db8:6::2 shutdown
+ !
+ ! unnumbered
+ neighbor r1-eth7 interface remote-as external
+ neighbor r1-eth7 shutdown
+ neighbor r1-eth7 timers 3 10
+ neighbor r1-eth7 timers connect 1
+ neighbor r1-eth7 bfd
+ !
+ ! unnumbered, strict
+ neighbor r1-eth8 interface remote-as external
+ neighbor r1-eth8 shutdown
+ neighbor r1-eth8 timers 3 10
+ neighbor r1-eth8 timers connect 1
+ neighbor r1-eth8 bfd
+ neighbor r1-eth8 bfd strict
+ !
+ ! numbered link-local
+ neighbor fe80::9:2 remote-as external
+ neighbor fe80::9:2 interface r1-eth9
+ neighbor fe80::9:2 shutdown
+ neighbor fe80::9:2 timers 3 10
+ neighbor fe80::9:2 timers connect 1
+ neighbor fe80::9:2 bfd
+ !
+ ! numbered link-local, strict
+ neighbor fe80::a:2 remote-as external
+ neighbor fe80::a:2 interface r1-eth10
+ neighbor fe80::a:2 shutdown
+ neighbor fe80::a:2 timers 3 10
+ neighbor fe80::a:2 timers connect 1
+ neighbor fe80::a:2 bfd
+ neighbor fe80::a:2 bfd strict
+ !
+ ! unnumbered VLAN sub-interfaces sharing one link-local, peer-group
+ neighbor r1-eth11.100 interface peer-group PGU
+ neighbor r1-eth11.100 shutdown
+ neighbor r1-eth11.101 interface peer-group PGU
+ neighbor r1-eth11.101 shutdown
+ !
+ ! iBGP, directly connected (single-hop only once TCP is up)
+ neighbor 192.0.2.22 remote-as internal
+ neighbor 192.0.2.22 shutdown
+ neighbor 192.0.2.22 timers 3 10
+ neighbor 192.0.2.22 timers connect 1
+ neighbor 192.0.2.22 bfd
+ !
+ ! IPv6 multihop
+ neighbor 2001:db8:ff::3 remote-as external
+ neighbor 2001:db8:ff::3 shutdown
+ neighbor 2001:db8:ff::3 timers 3 10
+ neighbor 2001:db8:ff::3 timers connect 1
+ neighbor 2001:db8:ff::3 ebgp-multihop 2
+ neighbor 2001:db8:ff::3 update-source 2001:db8:ff::1
+ neighbor 2001:db8:ff::3 bfd
+ !
+ ! IPv4 single-hop, update-source address on another interface (lo)
+ neighbor 192.0.2.26 remote-as external
+ neighbor 192.0.2.26 shutdown
+ neighbor 192.0.2.26 timers 3 10
+ neighbor 192.0.2.26 timers connect 1
+ neighbor 192.0.2.26 update-source 198.51.100.11
+ neighbor 192.0.2.26 bfd
+ !
+ address-family ipv4 unicast
+ redistribute connected
+ exit-address-family
+ !
+ address-family ipv6 unicast
+ redistribute connected
+ neighbor PG6 activate
+ neighbor PG6S activate
+ neighbor fe80::9:2 activate
+ neighbor fe80::a:2 activate
+ neighbor 2001:db8:ff::3 activate
+ exit-address-family
+!
+! VRF single-hop, update-source <interface>, BFD profile
+router bgp 65001 vrf RED
+ bgp router-id 198.51.100.1
+ no bgp ebgp-requires-policy
+ neighbor 192.0.2.18 remote-as external
+ neighbor 192.0.2.18 shutdown
+ neighbor 192.0.2.18 timers 3 10
+ neighbor 192.0.2.18 timers connect 1
+ neighbor 192.0.2.18 update-source r1-eth13
+ neighbor 192.0.2.18 bfd
+ neighbor 192.0.2.18 bfd profile slow
+!
diff --git a/tests/topotests/bgp_bfd_singlehop_defer_install/r2/frr.conf b/tests/topotests/bgp_bfd_singlehop_defer_install/r2/frr.conf
new file mode 100644
index 0000000000..6140a3302b
--- /dev/null
+++ b/tests/topotests/bgp_bfd_singlehop_defer_install/r2/frr.conf
@@ -0,0 +1,127 @@
+!
+interface lo
+ ip address 198.51.100.2/32
+ ip address 198.51.100.3/32
+ ip address 198.51.100.4/32
+ ipv6 address 2001:db8:ff::3/128
+!
+interface r2-eth0
+ ip address 192.0.2.2/30
+!
+interface r2-eth1
+ ipv6 address 2001:db8:2::2/64
+!
+interface r2-eth2
+ ip address 192.0.2.6/30
+!
+interface r2-eth3
+ ipv6 address 2001:db8:4::2/64
+!
+interface r2-eth4
+ ip address 192.0.2.10/30
+!
+interface r2-eth5
+ ip address 192.0.2.14/30
+!
+interface r2-eth6
+ ipv6 address 2001:db8:6::2/64
+!
+interface r2-eth12
+ ip address 192.0.2.18/30
+!
+interface r2-eth13
+ ip address 192.0.2.26/30
+!
+ip route 198.51.100.1/32 192.0.2.1
+ip route 198.51.100.11/32 192.0.2.25
+ipv6 route 2001:db8:ff::1/128 2001:db8:2::1
+!
+bfd
+ peer 192.0.2.1 local-address 192.0.2.2 interface r2-eth0
+ exit
+ peer 2001:db8:2::1 local-address 2001:db8:2::2 interface r2-eth1
+ exit
+ peer 192.0.2.5 local-address 192.0.2.6 interface r2-eth2
+ exit
+ peer 2001:db8:4::1 local-address 2001:db8:4::2 interface r2-eth3
+ exit
+ peer 192.0.2.9 local-address 198.51.100.2 interface r2-eth4
+ exit
+ peer 192.0.2.13 local-address 198.51.100.4 interface r2-eth5
+ exit
+ peer 198.51.100.1 multihop local-address 198.51.100.3
+ exit
+ peer 2001:db8:6::1 local-address 2001:db8:6::2 interface r2-eth6
+ exit
+ peer fe80::7:1 local-address fe80::7:2 interface r2-eth7
+ exit
+ peer fe80::8:1 local-address fe80::8:2 interface r2-eth8
+ exit
+ peer fe80::9:1 local-address fe80::9:2 interface r2-eth9
+ exit
+ peer fe80::a:1 local-address fe80::a:2 interface r2-eth10
+ exit
+ peer fe80::b:1 local-address fe80::b:2 interface r2-eth11.100
+ exit
+ peer fe80::b:1 local-address fe80::b:2 interface r2-eth11.101
+ exit
+ peer 192.0.2.17 local-address 192.0.2.18 interface r2-eth12
+ exit
+ peer 2001:db8:ff::1 multihop local-address 2001:db8:ff::3
+ exit
+ peer 198.51.100.11 local-address 192.0.2.26 interface r2-eth13
+ exit
+exit
+!
+router bgp 65002
+ bgp router-id 198.51.100.2
+ no bgp ebgp-requires-policy
+ neighbor 192.0.2.1 remote-as external
+ neighbor 192.0.2.1 passive
+ neighbor 2001:db8:2::1 remote-as external
+ neighbor 2001:db8:2::1 passive
+ neighbor 192.0.2.5 remote-as external
+ neighbor 192.0.2.5 passive
+ neighbor 2001:db8:4::1 remote-as external
+ neighbor 2001:db8:4::1 passive
+ neighbor 192.0.2.9 remote-as external
+ neighbor 192.0.2.9 passive
+ neighbor 192.0.2.13 remote-as external
+ neighbor 192.0.2.13 passive
+ neighbor 198.51.100.1 remote-as external
+ neighbor 198.51.100.1 passive
+ neighbor 198.51.100.1 ebgp-multihop 2
+ neighbor 2001:db8:6::1 remote-as external
+ neighbor 2001:db8:6::1 passive
+ neighbor r2-eth7 interface remote-as external
+ neighbor r2-eth7 passive
+ neighbor r2-eth8 interface remote-as external
+ neighbor r2-eth8 passive
+ neighbor fe80::9:1 remote-as external
+ neighbor fe80::9:1 interface r2-eth9
+ neighbor fe80::9:1 passive
+ neighbor fe80::a:1 remote-as external
+ neighbor fe80::a:1 interface r2-eth10
+ neighbor fe80::a:1 passive
+ neighbor r2-eth11.100 interface remote-as external
+ neighbor r2-eth11.100 passive
+ neighbor r2-eth11.101 interface remote-as external
+ neighbor r2-eth11.101 passive
+ neighbor 192.0.2.17 remote-as external
+ neighbor 192.0.2.17 passive
+ neighbor 2001:db8:ff::1 remote-as external
+ neighbor 2001:db8:ff::1 passive
+ neighbor 2001:db8:ff::1 ebgp-multihop 2
+ neighbor 198.51.100.11 remote-as external
+ neighbor 198.51.100.11 passive
+ neighbor 198.51.100.11 disable-connected-check
+ !
+ address-family ipv6 unicast
+ neighbor 2001:db8:2::1 activate
+ neighbor 2001:db8:4::1 activate
+ neighbor 2001:db8:6::1 activate
+ neighbor fe80::9:1 activate
+ neighbor fe80::a:1 activate
+ neighbor 2001:db8:ff::1 activate
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_bfd_singlehop_defer_install/r3/frr.conf b/tests/topotests/bgp_bfd_singlehop_defer_install/r3/frr.conf
new file mode 100644
index 0000000000..2630a45eec
--- /dev/null
+++ b/tests/topotests/bgp_bfd_singlehop_defer_install/r3/frr.conf
@@ -0,0 +1,14 @@
+!
+interface r3-eth0
+ ip address 192.0.2.22/30
+!
+bfd
+ peer 192.0.2.21 local-address 192.0.2.22 interface r3-eth0
+ exit
+exit
+!
+router bgp 65001
+ bgp router-id 198.51.100.6
+ neighbor 192.0.2.21 remote-as internal
+ neighbor 192.0.2.21 passive
+!
diff --git a/tests/topotests/bgp_bfd_singlehop_defer_install/test_bgp_bfd_singlehop_defer_install.py b/tests/topotests/bgp_bfd_singlehop_defer_install/test_bgp_bfd_singlehop_defer_install.py
new file mode 100644
index 0000000000..54812877e4
--- /dev/null
+++ b/tests/topotests/bgp_bfd_singlehop_defer_install/test_bgp_bfd_singlehop_defer_install.py
@@ -0,0 +1,515 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: ISC
+
+#
+# test_bgp_bfd_singlehop_defer_install.py
+#
+# Copyright (c) 2026 by NVIDIA Corporation
+# Sougata Barik <sougatab@nvidia.com>
+#
+
+"""
+Single-hop BGP BFD sessions must be registered with bfdd already carrying
+their egress interface and local address.
+
+Before the fix bgpd registered single-hop sessions at config time without an
+interface/local address and filled them in once the TCP connection came up.
+bfdd keys sessions on those fields, so the (already Up) session was deleted
+and re-created with a new discriminator: the remote saw BFD go Down and, in
+the field, reset BGP.
+
+Topology (r2/r3 are passive, their BFD sessions are static so their counters
+only reflect what r1 does; link-local addresses are pinned to fe80::<n>:1 on
+r1 and fe80::<n>:2 on r2):
+
+ r1-eth0 192.0.2.1/30 ---- r2-eth0 192.0.2.2/30 IPv4, no update-
+ source: waits for
+ the TCP source
+ r1-eth1 2001:db8:2::1/64 ---- r2-eth1 2001:db8:2::2/64 IPv6, peer-group,
+ update-source if
+ r1-eth2 192.0.2.5/30 ---- r2-eth2 192.0.2.6/30 IPv4, strict
+ r1-eth3 2001:db8:4::1/64 ---- r2-eth3 2001:db8:4::2/64 IPv6, strict
+ inherited from pg
+ r1-eth4 192.0.2.9/30 ---- r2-eth4 192.0.2.10/30 r2 lo .2, egress
+ unknown pre-TCP
+ r1-eth5 192.0.2.13/30 ---- r2-eth5 192.0.2.14/30 r2 lo .4, same,
+ strict inherited
+ from pg (fallback)
+ r1-eth6 2001:db8:6::1/64 ---- r2-eth6 2001:db8:6::2/64 IPv6, no update-
+ source
+ r1-eth7 fe80::7:1 ---- r2-eth7 fe80::7:2 unnumbered
+ r1-eth8 fe80::8:1 ---- r2-eth8 fe80::8:2 unnumbered, strict
+ r1-eth9 fe80::9:1 ---- r2-eth9 fe80::9:2 link-local
+ neighbor
+ r1-eth10 fe80::a:1 ---- r2-eth10 fe80::a:2 link-local
+ neighbor, strict
+ r1-eth11.{100,101} fe80::b:1 -- r2-eth11.{100,101} fe80::b:2
+ unnumbered VLAN
+ sub-interfaces,
+ same link-local,
+ peer-group
+ r1-eth12 192.0.2.21/30 ---- r3-eth0 192.0.2.22/30 iBGP, connected
+ r1-eth13 192.0.2.17/30 ---- r2-eth12 192.0.2.18/30 VRF RED on r1,
+ update-source if,
+ BFD profile
+ r1-eth14 192.0.2.25/30 ---- r2-eth13 192.0.2.26/30 update-source
+ r1 lo .11: TCP
+ local address is
+ on lo, BFD egress
+ stays r1-eth14
+ r1 lo 198.51.100.1 ---(via eth0)--- r2 lo 198.51.100.3 multihop
+ r1 lo 2001:db8:ff::1 ---(via eth1)--- r2 lo 2001:db8:ff::3 IPv6 multihop
+"""
+
+import os
+import sys
+import json
+import time
+import pytest
+import functools
+
+CWD = os.path.dirname(os.path.realpath(__file__))
+sys.path.append(os.path.join(CWD, "../"))
+
+# pylint: disable=C0413
+from lib import topotest
+from lib.topogen import Topogen, TopoRouter, get_topogen
+from lib.common_config import step
+
+pytestmark = [pytest.mark.bfdd, pytest.mark.bgpd, pytest.mark.staticd]
+
+VLANS = (100, 101)
+LINK_LOCAL = {
+ "r1": {
+ "r1-eth7": "fe80::7:1",
+ "r1-eth8": "fe80::8:1",
+ "r1-eth9": "fe80::9:1",
+ "r1-eth10": "fe80::a:1",
+ "r1-eth11.100": "fe80::b:1",
+ "r1-eth11.101": "fe80::b:1",
+ },
+ "r2": {
+ "r2-eth7": "fe80::7:2",
+ "r2-eth8": "fe80::8:2",
+ "r2-eth9": "fe80::9:2",
+ "r2-eth10": "fe80::a:2",
+ "r2-eth11.100": "fe80::b:2",
+ "r2-eth11.101": "fe80::b:2",
+ },
+}
+
+# BFD sessions are identified by "<peer>" or, for link-local peers that can
+# share an address, "<peer>%<interface>". Remote side is (router, session).
+
+# Keyed before TCP is up.
+# r1 neighbor -> (r1 BFD session, expected interface, expected local, remote)
+RESOLVABLE = {
+ "2001:db8:2::2": ("2001:db8:2::2", "r1-eth1", "2001:db8:2::1", ("r2", "2001:db8:2::1")),
+ "192.0.2.6": ("192.0.2.6", "r1-eth2", "192.0.2.5", ("r2", "192.0.2.5")),
+ "2001:db8:4::2": ("2001:db8:4::2", "r1-eth3", "2001:db8:4::1", ("r2", "2001:db8:4::1")),
+ "fe80::a:2": ("fe80::a:2%r1-eth10", "r1-eth10", "fe80::a:1", ("r2", "fe80::a:1%r2-eth10")),
+ "192.0.2.18": ("192.0.2.18", "r1-eth13", "192.0.2.17", ("r2", "192.0.2.17")),
+ "192.0.2.26": ("192.0.2.26", "r1-eth14", "198.51.100.11", ("r2", "198.51.100.11")),
+}
+# Installed only once TCP is up.
+DEFERRED = {
+ "192.0.2.2": ("192.0.2.2", "r1-eth0", "192.0.2.1", ("r2", "192.0.2.1")),
+ "198.51.100.2": ("198.51.100.2", "r1-eth4", "192.0.2.9", ("r2", "192.0.2.9")),
+ "2001:db8:6::2": ("2001:db8:6::2", "r1-eth6", "2001:db8:6::1", ("r2", "2001:db8:6::1")),
+ "r1-eth7": ("fe80::7:2%r1-eth7", "r1-eth7", "fe80::7:1", ("r2", "fe80::7:1%r2-eth7")),
+ "fe80::9:2": ("fe80::9:2%r1-eth9", "r1-eth9", "fe80::9:1", ("r2", "fe80::9:1%r2-eth9")),
+ "r1-eth11.100": (
+ "fe80::b:2%r1-eth11.100",
+ "r1-eth11.100",
+ "fe80::b:1",
+ ("r2", "fe80::b:1%r2-eth11.100"),
+ ),
+ "r1-eth11.101": (
+ "fe80::b:2%r1-eth11.101",
+ "r1-eth11.101",
+ "fe80::b:1",
+ ("r2", "fe80::b:1%r2-eth11.101"),
+ ),
+ "192.0.2.22": ("192.0.2.22", "r1-eth12", "192.0.2.21", ("r3", "192.0.2.21")),
+}
+# Strict, but the peer address is only learnt when BGP starts.
+STRICT_UNNUMBERED = {
+ "r1-eth8": ("fe80::8:2%r1-eth8", "r1-eth8", "fe80::8:1", ("r2", "fe80::8:1%r2-eth8")),
+}
+SINGLEHOP = dict(RESOLVABLE, **DEFERRED, **STRICT_UNNUMBERED)
+MULTIHOP = ("198.51.100.3", "2001:db8:ff::3")
+STRICT_FALLBACK = "198.51.100.4"
+PENDING_REMOVE = "198.51.100.5"
+PENDING_GROUP = ("PGP", "198.51.100.6")
+VRF_NEIGHBORS = {"192.0.2.18": "RED"}
+ALL_NEIGHBORS = list(SINGLEHOP) + list(MULTIHOP) + [STRICT_FALLBACK]
+
+# r1 BFD ids / remote session-down counters once the held-down sessions are Up
+BASELINE = {}
+DOWN_BASELINE = {}
+
+
+def build_topo(tgen):
+ for routern in range(1, 4):
+ tgen.add_router("r{}".format(routern))
+
+ for switchn in range(1, 13):
+ switch = tgen.add_switch("s{}".format(switchn))
+ switch.add_link(tgen.gears["r1"])
+ switch.add_link(tgen.gears["r2"])
+
+ switch = tgen.add_switch("s13")
+ switch.add_link(tgen.gears["r1"])
+ switch.add_link(tgen.gears["r3"])
+
+ for switchn in (14, 15):
+ switch = tgen.add_switch("s{}".format(switchn))
+ switch.add_link(tgen.gears["r1"])
+ switch.add_link(tgen.gears["r2"])
+
+
+def setup_module(mod):
+ tgen = Topogen(build_topo, mod.__name__)
+ tgen.start_topology()
+
+ for rname in ("r1", "r2"):
+ router = tgen.gears[rname]
+ for vlan in VLANS:
+ router.net.add_vlan(
+ "{}-eth11.{}".format(rname, vlan), "{}-eth11".format(rname), vlan
+ )
+ for ifname, addr in LINK_LOCAL[rname].items():
+ router.cmd_raises(
+ "echo 1 > /proc/sys/net/ipv6/conf/{0}/addr_gen_mode && "
+ "ip -6 addr flush dev {0} scope link && "
+ "ip -6 addr add {1}/64 dev {0} nodad".format(ifname, addr)
+ )
+
+ tgen.gears["r1"].cmd_raises(
+ "ip link add RED type vrf table 10 && ip link set RED up && "
+ "ip link set r1-eth13 master RED"
+ )
+
+ for rname, router in tgen.routers().items():
+ router.load_frr_config(
+ os.path.join(CWD, "{}/frr.conf".format(rname)),
+ [
+ (TopoRouter.RD_ZEBRA, None),
+ (TopoRouter.RD_MGMTD, None),
+ (TopoRouter.RD_STATIC, None),
+ (TopoRouter.RD_BFD, None),
+ (TopoRouter.RD_BGP, None),
+ ],
+ )
+
+ tgen.start_router()
+
+
+def teardown_module(mod):
+ tgen = get_topogen()
+ tgen.stop_topology()
+
+
+def bfd_key(p):
+ if p["peer"].startswith("fe80") and p.get("interface"):
+ return "{}%{}".format(p["peer"], p["interface"])
+ return p["peer"]
+
+
+def bfd_peers(router):
+ output = json.loads(router.vtysh_cmd("show bfd peers json"))
+ return {bfd_key(p): p for p in output}
+
+
+def bfd_counters(router):
+ output = json.loads(router.vtysh_cmd("show bfd peers counters json"))
+ return {bfd_key(p): p for p in output}
+
+
+def remote_downs():
+ tgen = get_topogen()
+ downs = {}
+ for rname in ("r2", "r3"):
+ for peer, values in bfd_counters(tgen.gears[rname]).items():
+ downs[(rname, peer)] = values["session-down"]
+ return downs
+
+
+def bgp_neighbors(router):
+ output = json.loads(router.vtysh_cmd("show bgp vrf all neighbors json"))
+ neighbors = {}
+ for vrf in output.values():
+ neighbors.update({n: v for n, v in vrf.items() if isinstance(v, dict)})
+ return neighbors
+
+
+def r1_neighbors_cmd(fmt, neighbors):
+ r1 = get_topogen().gears["r1"]
+ cmds = ["configure terminal"]
+ for n in neighbors:
+ vrf = VRF_NEIGHBORS.get(n)
+ cmds.append("router bgp 65001" + (" vrf {}".format(vrf) if vrf else ""))
+ cmds.append(fmt.format(n))
+ cmds.append("exit")
+ r1.vtysh_multicmd("\n".join(cmds), pretty_output=False)
+
+
+def record_baseline():
+ "Wait for the held-down single-hop sessions to be Up, remember their ids."
+ r1 = get_topogen().gears["r1"]
+
+ def _up():
+ expected = {peer: {"status": "up"} for peer, _, _, _ in RESOLVABLE.values()}
+ return topotest.json_cmp(bfd_peers(r1), expected)
+
+ topotest.run_and_expect(_up, None, count=60, wait=1)
+
+ peers = bfd_peers(r1)
+ for peer, _, _, _ in RESOLVABLE.values():
+ if peers.get(peer, {}).get("status") == "up":
+ BASELINE[peer] = peers[peer]["id"]
+ DOWN_BASELINE.update(remote_downs())
+
+
+def test_bfd_installed_with_interface_before_bgp_connects():
+ "While BGP is held down, single-hop sessions are Up and already keyed."
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+
+ def _sessions_up():
+ peers = bfd_peers(r1)
+ expected = {
+ peer: {"status": "up", "interface": ifname, "local": local}
+ for peer, ifname, local, _ in RESOLVABLE.values()
+ }
+ expected[STRICT_FALLBACK] = {"status": "up"}
+ expected[RESOLVABLE["192.0.2.18"][0]]["vrf"] = "RED"
+ return topotest.json_cmp(peers, expected)
+
+ step("BFD sessions of BGP-down single-hop peers come Up with interface/local")
+ _, result = topotest.run_and_expect(_sessions_up, None, count=60, wait=1)
+ assert result is None, "single-hop BFD session not installed with egress interface: {}".format(result)
+
+ record_baseline()
+ peers = bfd_peers(r1)
+
+ step("Non-strict sessions that cannot be keyed yet are deferred, not installed")
+ for peer, _, _, _ in DEFERRED.values():
+ assert peer not in peers, "BFD session for {} installed before keyed: {}".format(
+ peer, peers.get(peer)
+ )
+
+ step("Strict mode with unresolvable egress falls back to an interface-less install")
+ assert peers[STRICT_FALLBACK].get("interface") is None
+
+ step("BFD profile applied to the VRF session")
+ vrf_peer = peers[RESOLVABLE["192.0.2.18"][0]]
+ assert vrf_peer["receive-interval"] == 600, vrf_peer
+
+
+def test_bgp_up_does_not_recreate_bfd_session():
+ "Bringing BGP up must not delete/re-create the Up BFD session."
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+
+ if not BASELINE:
+ record_baseline()
+
+ step("Bring all r1 neighbors up")
+ r1_neighbors_cmd("no neighbor {} shutdown", ALL_NEIGHBORS)
+
+ def _bgp_up():
+ expected = {
+ n: {"bgpState": "Established", "peerBfdInfo": {"status": "Up"}}
+ for n in ALL_NEIGHBORS
+ }
+ return topotest.json_cmp(bgp_neighbors(r1), expected)
+
+ _, result = topotest.run_and_expect(_bgp_up, None, count=90, wait=1)
+ assert result is None, "BGP/BFD did not converge (strict deadlock?): {}".format(result)
+
+ # Let any re-created session's old instance time out on the remote.
+ time.sleep(5)
+
+ peers = bfd_peers(r1)
+ downs = remote_downs()
+ neighbors = bgp_neighbors(r1)
+
+ problems = []
+
+ step("Local discriminator unchanged across BGP establishment")
+ for peer, _, _, _ in RESOLVABLE.values():
+ if peer not in BASELINE:
+ problems.append("r1 {}: BFD session not Up before BGP connected".format(peer))
+ elif peers[peer]["id"] != BASELINE[peer]:
+ problems.append(
+ "r1 {}: BFD session re-created (id {} -> {})".format(
+ peer, BASELINE[peer], peers[peer]["id"]
+ )
+ )
+
+ step("Every single-hop session keyed with its egress interface and source")
+ for peer, ifname, local, _ in SINGLEHOP.values():
+ session = peers.get(peer, {})
+ if (session.get("interface"), session.get("local"), session.get("multihop")) != (
+ ifname,
+ local,
+ False,
+ ):
+ problems.append("r1 {}: keyed {}".format(peer, session))
+
+ step("Sub-interfaces sharing a link-local get distinct sessions")
+ sub_ids = {peers.get(DEFERRED[n][0], {}).get("id") for n in ("r1-eth11.100", "r1-eth11.101")}
+ if len(sub_ids) != 2:
+ problems.append("r1 sub-interface sessions collapsed: {}".format(sub_ids))
+
+ step("Remote never saw BFD go Down")
+ for _, _, _, remote in SINGLEHOP.values():
+ count = downs[remote] - DOWN_BASELINE.get(remote, 0)
+ if count:
+ problems.append("{} {}: saw BFD Down {} time(s)".format(remote[0], remote[1], count))
+
+ step("BGP came up exactly once on every non-fallback peer")
+ for n in list(SINGLEHOP) + list(MULTIHOP):
+ if neighbors[n]["connectionsEstablished"] != 1:
+ problems.append(
+ "r1 {}: BGP established {} times, last reset '{}'".format(
+ n, neighbors[n]["connectionsEstablished"], neighbors[n].get("lastResetDueTo")
+ )
+ )
+
+ step("Multihop sessions are unaffected")
+ for peer in MULTIHOP:
+ if not peers[peer]["multihop"] or peers[peer]["status"] != "up":
+ problems.append("r1 {}: multihop session {}".format(peer, peers[peer]))
+
+ assert not problems, "\n".join(problems)
+
+
+def test_bgp_peer_reset_keeps_bfd_session():
+ "Clearing BGP re-uses the existing, already keyed BFD session."
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+ cleared = ("192.0.2.2", "2001:db8:2::2", "r1-eth7", "r1-eth11.100")
+
+ before = {p: v["id"] for p, v in bfd_peers(r1).items()}
+ down_before = remote_downs()
+
+ step("Clear numbered and unnumbered single-hop sessions")
+ for n in cleared:
+ r1.vtysh_cmd("clear bgp {}".format(n))
+
+ def _bgp_up():
+ expected = {
+ n: {"bgpState": "Established", "connectionsEstablished": 2} for n in cleared
+ }
+ return topotest.json_cmp(bgp_neighbors(r1), expected)
+
+ _, result = topotest.run_and_expect(_bgp_up, None, count=60, wait=1)
+ assert result is None, "BGP did not re-establish after clear: {}".format(result)
+ time.sleep(3)
+
+ after = bfd_peers(r1)
+ down_after = remote_downs()
+ for n in cleared:
+ peer, _, _, remote = SINGLEHOP[n]
+ assert after[peer]["id"] == before[peer], "{} re-created".format(peer)
+ assert after[peer]["status"] == "up"
+ assert down_after[remote] == down_before[remote], "{} saw BFD Down".format(remote)
+
+
+def test_bfd_config_change_while_pending():
+ "Removing BFD while deferred is clean; enabling strict installs it."
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ r1 = tgen.gears["r1"]
+
+ step("Add a held-down peer whose egress interface is unknown, with BFD")
+ r1_neighbors_cmd(
+ "{}",
+ [
+ "neighbor {} remote-as external".format(PENDING_REMOVE),
+ "neighbor {} shutdown".format(PENDING_REMOVE),
+ "neighbor {} disable-connected-check".format(PENDING_REMOVE),
+ "neighbor {} bfd".format(PENDING_REMOVE),
+ ],
+ )
+ time.sleep(2)
+ assert PENDING_REMOVE not in bfd_peers(r1), "pending session was installed"
+
+ step("Remove BFD while the install is pending")
+ r1_neighbors_cmd("no neighbor {} bfd", [PENDING_REMOVE])
+ time.sleep(2)
+ assert PENDING_REMOVE not in bfd_peers(r1)
+ neighbor = json.loads(r1.vtysh_cmd("show bgp neighbors {} json".format(PENDING_REMOVE)))
+ assert PENDING_REMOVE in neighbor, neighbor
+
+ step("Re-enable BFD (pending again), then strict: must install now")
+ r1_neighbors_cmd("neighbor {} bfd", [PENDING_REMOVE])
+ time.sleep(2)
+ assert PENDING_REMOVE not in bfd_peers(r1)
+ r1_neighbors_cmd("neighbor {} bfd strict", [PENDING_REMOVE])
+
+ def _installed():
+ return PENDING_REMOVE in bfd_peers(r1)
+
+ _, result = topotest.run_and_expect(_installed, True, count=10, wait=1)
+ assert result, "strict mode left the BFD session uninstalled (BGP would deadlock)"
+
+ step("Pending peer-group member: enabling strict on the group installs it")
+ group, member = PENDING_GROUP
+ r1_neighbors_cmd(
+ "{}",
+ [
+ "neighbor {} peer-group".format(group),
+ "neighbor {} remote-as external".format(group),
+ "neighbor {} disable-connected-check".format(group),
+ "neighbor {} bfd".format(group),
+ "neighbor {} peer-group {}".format(member, group),
+ "neighbor {} shutdown".format(member),
+ ],
+ )
+ time.sleep(2)
+ assert member not in bfd_peers(r1), "pending member session was installed"
+ r1_neighbors_cmd("neighbor {} bfd strict", [group])
+
+ def _member_installed():
+ return member in bfd_peers(r1)
+
+ _, result = topotest.run_and_expect(_member_installed, True, count=10, wait=1)
+ assert result, "strict inherited from the peer-group left the member uninstalled"
+
+ step("Delete the neighbors, sessions go away")
+ r1_neighbors_cmd("no neighbor {}", [PENDING_REMOVE, member, group])
+
+ def _gone():
+ peers = bfd_peers(r1)
+ return PENDING_REMOVE not in peers and member not in peers
+
+ _, result = topotest.run_and_expect(_gone, True, count=10, wait=1)
+ assert result, "BFD session leaked after neighbor removal"
+
+
+def test_memory_leak():
+ "Run the memory leak test and report results."
+ tgen = get_topogen()
+ if not tgen.is_memleak_enabled():
+ pytest.skip("Memory leak test/report is disabled")
+
+ tgen.report_memory_leaks()
+
+
+if __name__ == "__main__":
+ args = ["-s"] + sys.argv[1:]
+ sys.exit(pytest.main(args))