|
| 1 | +#!/usr/bin/env python |
| 2 | +# SPDX-License-Identifier: ISC |
| 3 | + |
| 4 | +# |
| 5 | +# Copyright (c) 2025 by Cline SR |
| 6 | +# |
| 7 | + |
| 8 | +""" |
| 9 | +Test RFC 4271 Section 5.1.3 conformance: |
| 10 | +"A route originated by a BGP speaker SHALL NOT be |
| 11 | +advertised to a peer using an address of that peer |
| 12 | +as NEXT_HOP." |
| 13 | +
|
| 14 | +This test verifies that when an outbound route-map uses |
| 15 | +"set ip next-hop <peer-address>" on a locally originated route, the |
| 16 | +NEXT_HOP is reset to the local address rather than left as the peer's |
| 17 | +own address (which would cause a blackhole). |
| 18 | +
|
| 19 | +r1 advertises two prefixes to r2: |
| 20 | + - 192.0.2.0/24 with NEXT_HOP set to r2's address (10.0.12.2) |
| 21 | + => must be reset to 10.0.12.1 (r1's address) |
| 22 | + - 198.51.100.0/24 with NEXT_HOP set to a non-peer address (10.0.0.1) |
| 23 | + => must be left unchanged (no regression) |
| 24 | +""" |
| 25 | + |
| 26 | +import os |
| 27 | +import sys |
| 28 | +import json |
| 29 | +import functools |
| 30 | +import pytest |
| 31 | + |
| 32 | +CWD = os.path.dirname(os.path.realpath(__file__)) |
| 33 | +sys.path.append(os.path.join(CWD, "../")) |
| 34 | + |
| 35 | +# pylint: disable=C0413 |
| 36 | +from lib import topotest |
| 37 | +from lib.topogen import Topogen, get_topogen |
| 38 | + |
| 39 | +pytestmark = [pytest.mark.bgpd] |
| 40 | + |
| 41 | + |
| 42 | +def build_topo(tgen): |
| 43 | + """Build a simple two-router topology: r1 <---> r2""" |
| 44 | + r1 = tgen.add_router("r1") |
| 45 | + r2 = tgen.add_router("r2") |
| 46 | + |
| 47 | + switch = tgen.add_switch("s1") |
| 48 | + switch.add_link(r1) |
| 49 | + switch.add_link(r2) |
| 50 | + |
| 51 | + |
| 52 | +def setup_module(mod): |
| 53 | + tgen = Topogen(build_topo, mod.__name__) |
| 54 | + tgen.start_topology() |
| 55 | + |
| 56 | + for router in tgen.routers().values(): |
| 57 | + router.load_frr_config() |
| 58 | + |
| 59 | + tgen.start_router() |
| 60 | + |
| 61 | + |
| 62 | +def teardown_module(mod): |
| 63 | + tgen = get_topogen() |
| 64 | + tgen.stop_topology() |
| 65 | + |
| 66 | + |
| 67 | +def test_bgp_next_hop_not_peer_address(): |
| 68 | + """ |
| 69 | + r1 advertises 192.0.2.0/24 to r2 with an outbound route-map that |
| 70 | + sets the NEXT_HOP to r2's address (10.0.12.2). Per RFC 4271 §5.1.3, |
| 71 | + the NEXT_HOP must be reset to r1's local address (10.0.12.1), |
| 72 | + not left as 10.0.12.2. |
| 73 | + """ |
| 74 | + tgen = get_topogen() |
| 75 | + |
| 76 | + if tgen.routers_have_failure(): |
| 77 | + pytest.skip(tgen.errors) |
| 78 | + |
| 79 | + r2 = tgen.gears["r2"] |
| 80 | + |
| 81 | + def _bgp_converge(): |
| 82 | + output = json.loads( |
| 83 | + r2.vtysh_cmd("show bgp ipv4 unicast 192.0.2.0/24 json") |
| 84 | + ) |
| 85 | + expected = { |
| 86 | + "routes": { |
| 87 | + "192.0.2.0/24": [ |
| 88 | + {"valid": True, "nexthops": [{"ip": "10.0.12.1"}]} |
| 89 | + ] |
| 90 | + } |
| 91 | + } |
| 92 | + return topotest.json_cmp(output, expected) |
| 93 | + |
| 94 | + test_func = functools.partial(_bgp_converge) |
| 95 | + _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) |
| 96 | + assert result is None, ( |
| 97 | + "192.0.2.0/24 NEXT_HOP is not 10.0.12.1; RFC 4271 5.1.3 violation" |
| 98 | + ) |
| 99 | + |
| 100 | + |
| 101 | +def test_bgp_next_hop_normal_route_map(): |
| 102 | + """ |
| 103 | + Verify that a normal route-map setting NEXT_HOP to a non-peer address |
| 104 | + still works correctly (no regression). 198.51.100.0/24 should have |
| 105 | + NEXT_HOP 10.0.0.1. |
| 106 | + """ |
| 107 | + tgen = get_topogen() |
| 108 | + |
| 109 | + if tgen.routers_have_failure(): |
| 110 | + pytest.skip(tgen.errors) |
| 111 | + |
| 112 | + r2 = tgen.gears["r2"] |
| 113 | + |
| 114 | + def _bgp_converge(): |
| 115 | + output = json.loads( |
| 116 | + r2.vtysh_cmd("show bgp ipv4 unicast 198.51.100.0/24 json") |
| 117 | + ) |
| 118 | + expected = { |
| 119 | + "routes": { |
| 120 | + "198.51.100.0/24": [ |
| 121 | + {"valid": True, "nexthops": [{"ip": "10.0.0.1"}]} |
| 122 | + ] |
| 123 | + } |
| 124 | + } |
| 125 | + return topotest.json_cmp(output, expected) |
| 126 | + |
| 127 | + test_func = functools.partial(_bgp_converge) |
| 128 | + _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) |
| 129 | + assert result is None, ( |
| 130 | + "198.51.100.0/24 NEXT_HOP is not 10.0.0.1 (regression)" |
| 131 | + ) |
| 132 | + |
| 133 | + |
| 134 | +def test_memory_leak(): |
| 135 | + "Run the memory leak test and report results." |
| 136 | + tgen = get_topogen() |
| 137 | + if not tgen.is_memleak_enabled(): |
| 138 | + pytest.skip("Memory leak test/report is disabled") |
| 139 | + |
| 140 | + tgen.report_memory_leaks() |
| 141 | + |
| 142 | + |
| 143 | +if __name__ == "__main__": |
| 144 | + args = ["-s"] + sys.argv[1:] |
| 145 | + sys.exit(pytest.main(args)) |
0 commit comments