summaryrefslogtreecommitdiff
path: root/smoketest/scripts/cli
diff options
context:
space:
mode:
authorAlex Kudentsov <43482574+alexk37@users.noreply.github.com>2026-06-20 20:38:51 +0700
committerAlex Kudentsov <43482574+alexk37@users.noreply.github.com>2026-06-20 20:51:25 +0700
commit23649ea9dec442795997aac3da148e3312018d75 (patch)
treebf3299599652b8c4f5e9e1ec97fb165a3bd17654 /smoketest/scripts/cli
parent6c0fd99099a6251a12bc2287d5ba2ab115cd8015 (diff)
downloadvyos-1x-23649ea9dec442795997aac3da148e3312018d75.tar.gz
vyos-1x-23649ea9dec442795997aac3da148e3312018d75.zip
T8990: bgp: fix VPNv4/VPNv6 leaked routes flapping on every commit
VRF route leaking via VPNv4/VPNv6 caused all imported (leaked) routes to be withdrawn and reinstalled on every commit - even commits unrelated to BGP, VRF or routing - briefly blackholing traffic that depends on them. Root cause: the BGP address-family template rendered the route-target using the "route-target vpn ..." alias. FRR accepts that alias on input but its config writer only ever emits the canonical "rt vpn ..." form. frr-reload.py diffs the generated config against FRR's running config line by line and has no normalisation for this, so the route-target lines never matched and were deleted and re-added on every reload. Re-applying the route-target runs FRR's vpn_leak_prechange()/postchange(), a non-atomic withdraw-all then reinstall-all of every leaked route. Emit the canonical "rt vpn" keyword so the rendered config round-trips through frr-reload unchanged. FRR also folds an identical import+export route-target into "both" (an order-sensitive, byte-identical match), so do the same to stay idempotent; reordered or differing lists stay split on both sides. Add test_bgp_33_vpn_route_target_idempotency covering both / identical export+import / asymmetric route-target, reading the generated FRR config (getFRRconfig/vtysh returns FRR's already-canonical form and cannot catch the alias).
Diffstat (limited to 'smoketest/scripts/cli')
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py49
1 files changed, 49 insertions, 0 deletions
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index e9f615921..680829bce 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -25,6 +25,7 @@ from vyos.configsession import ConfigSessionError
from vyos.template import is_ipv6
from vyos.utils.process import process_named_running
from vyos.utils.process import cmd
+from vyos.utils.file import read_file
from vyos.frrender import bgp_daemon
ASN = '64512'
@@ -1792,6 +1793,54 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'router bgp {ASN}', frrconfig)
self.assertIn(f' neighbor {neighbor} bfd strict hold-time {bfd_hold_time}', frrconfig)
+ def test_bgp_33_vpn_route_target_idempotency(self):
+ # T8990: VPNv4/VPNv6 inter-VRF route-leak config must be rendered using
+ # FRR's canonical "rt vpn" keyword (not the "route-target vpn" alias),
+ # and an equal import+export route-target must be collapsed into "both"
+ # - exactly as FRR stores it. Otherwise the generated config never
+ # matches FRR's running config, frr-reload re-applies the route-target
+ # on every commit, and the leaked routes are withdrawn/reinstalled
+ # (flap) on every - even unrelated - commit.
+ #
+ # This intentionally reads the generated FRR config file instead of
+ # getFRRconfig()/vtysh: vtysh returns FRR's already-canonicalized
+ # config ("rt vpn"), so it cannot detect us emitting the
+ # "route-target vpn" alias - which is the actual T8990 defect.
+ frr_conf = '/run/frr/config/vyos.frr.conf'
+ afi_path = base_path + ['address-family', 'ipv4-unicast']
+
+ self.cli_set(afi_path + ['export', 'vpn'])
+ self.cli_set(afi_path + ['import', 'vpn'])
+ self.cli_set(afi_path + ['rd', 'vpn', 'export', f'{ASN}:1'])
+
+ # symmetric route-target via "both"
+ self.cli_set(afi_path + ['route-target', 'vpn', 'both', f'{ASN}:100'])
+ self.cli_commit()
+ frrconfig = read_file(frr_conf)
+ self.assertIn(f' rt vpn both {ASN}:100', frrconfig)
+ self.assertNotIn('route-target vpn', frrconfig)
+ self.cli_delete(afi_path + ['route-target', 'vpn', 'both'])
+
+ # symmetric route-target via equal import + export must collapse to
+ # "both" to match FRR's canonical rendering
+ self.cli_set(afi_path + ['route-target', 'vpn', 'export', f'{ASN}:100'])
+ self.cli_set(afi_path + ['route-target', 'vpn', 'import', f'{ASN}:100'])
+ self.cli_commit()
+ frrconfig = read_file(frr_conf)
+ self.assertIn(f' rt vpn both {ASN}:100', frrconfig)
+ self.assertNotIn('route-target vpn', frrconfig)
+ self.cli_delete(afi_path + ['route-target', 'vpn', 'export'])
+ self.cli_delete(afi_path + ['route-target', 'vpn', 'import'])
+
+ # asymmetric route-target -> separate import/export lines
+ self.cli_set(afi_path + ['route-target', 'vpn', 'export', f'{ASN}:200'])
+ self.cli_set(afi_path + ['route-target', 'vpn', 'import', f'{ASN}:300'])
+ self.cli_commit()
+ frrconfig = read_file(frr_conf)
+ self.assertIn(f' rt vpn export {ASN}:200', frrconfig)
+ self.assertIn(f' rt vpn import {ASN}:300', frrconfig)
+ self.assertNotIn('route-target vpn', frrconfig)
+
def test_bgp_99_bmp(self):
target_name = 'instance-bmp'
target_address = '127.0.0.1'