diff --git a/tests/topotests/pim_basic_acl/r1/frr.conf b/tests/topotests/pim_basic_acl/r1/frr.conf new file mode 100644 index 000000000000..cc639b304b37 --- /dev/null +++ b/tests/topotests/pim_basic_acl/r1/frr.conf @@ -0,0 +1,39 @@ +hostname r1 +! +!debug pim events +!debug igmp events +!debug igmp packets +! +ip prefix-list pim-oil-plist seq 10 deny 229.1.1.0/24 +ip prefix-list pim-oil-plist seq 20 permit any +! +access-list pim-acl seq 10 deny ip host 10.0.20.2 232.1.1.0 0.0.0.255 +access-list pim-acl seq 20 permit ip any any +! +interface r1-eth0 + ip address 10.0.20.1/24 + ip igmp + ip pim +! +interface r1-eth1 + ip address 10.0.30.1/24 + ip pim +! +interface r1-eth2 + ip address 10.0.40.1/24 + ip igmp + ip pim +! +interface lo + ip address 10.254.0.1/32 + ip pim +! +router pim + rp 10.254.0.3 + join-prune-interval 5 +! +router bgp 65001 + no bgp ebgp-requires-policy + neighbor 10.0.30.3 remote-as external + neighbor 10.0.30.3 timers 3 10 + redistribute connected diff --git a/tests/topotests/pim_basic_acl/r2/frr.conf b/tests/topotests/pim_basic_acl/r2/frr.conf new file mode 100644 index 000000000000..10ace947b2bc --- /dev/null +++ b/tests/topotests/pim_basic_acl/r2/frr.conf @@ -0,0 +1,19 @@ +hostname r2 +! +!debug pim events +!debug igmp events +!debug igmp packets +! +ip prefix-list pim-oil-plist seq 10 deny 229.1.1.0/24 +ip prefix-list pim-oil-plist seq 20 permit any +! +access-list pim-acl seq 10 deny ip host 10.0.20.2 232.1.1.0 0.0.0.255 +access-list pim-acl seq 20 permit ip any any +! +interface r2-eth0 + ip address 10.0.20.2/24 + ip pim +! +interface lo + ip address 10.254.0.2/32 +! diff --git a/tests/topotests/pim_basic_acl/r3/frr.conf b/tests/topotests/pim_basic_acl/r3/frr.conf new file mode 100644 index 000000000000..972077426643 --- /dev/null +++ b/tests/topotests/pim_basic_acl/r3/frr.conf @@ -0,0 +1,13 @@ +hostname r3 +! +!debug pim events +!debug igmp events +!debug igmp packets +! +interface r3-eth0 + ip address 10.0.40.4/24 + ip pim +! +interface lo + ip address 10.254.0.4/32 +! diff --git a/tests/topotests/pim_basic_acl/rp/frr.conf b/tests/topotests/pim_basic_acl/rp/frr.conf new file mode 100644 index 000000000000..f6eed2391705 --- /dev/null +++ b/tests/topotests/pim_basic_acl/rp/frr.conf @@ -0,0 +1,22 @@ +hostname rp +! +interface rp-eth0 + ip address 10.0.30.3/24 + ip pim +! +interface lo + ip address 10.254.0.3/32 + ip pim +! +router pim + rp 10.254.0.3 + join-prune-interval 5 + register-accept-list ACCEPT +! +ip prefix-list ACCEPT seq 5 permit 10.0.20.0/24 le 32 +! +router bgp 65003 + no bgp ebgp-requires-policy + neighbor 10.0.30.1 remote-as external + neighbor 10.0.30.1 timers 3 10 + redistribute connected \ No newline at end of file diff --git a/tests/topotests/pim_basic_acl/test_pim_basic_acl.py b/tests/topotests/pim_basic_acl/test_pim_basic_acl.py new file mode 100644 index 000000000000..b725781a4d03 --- /dev/null +++ b/tests/topotests/pim_basic_acl/test_pim_basic_acl.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC + +# +# test_pim_basic_acl.py +# +# Copyright (c) 2024 Architecture Technology Corporation +# Corey Siltala +# + +""" +test_pim_basic_acl.py: Test multicast boundary commands (access-lists and prefix-lists) +""" + +import os +import sys +import pytest +import json +from functools import partial + +pytestmark = [pytest.mark.pimd] + +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.topolog import logger + +ASM_GROUP="229.1.1.1" +SSM_GROUP="232.1.1.1" + +def build_topo(tgen): + "Build function" + + for routern in range(1, 4): + tgen.add_router("r{}".format(routern)) + + tgen.add_router("rp") + + # rp ------ r1 -------- r2 + # \ + # --------- r3 + # r1 -> .1 + # r2 -> .2 + # rp -> .3 + # r3 -> .4 + # loopback network is 10.254.0.X/32 + # + # r1 <- sw1 -> r2 + # r1-eth0 <-> r2-eth0 + # 10.0.20.0/24 + sw = tgen.add_switch("sw1") + sw.add_link(tgen.gears["r1"]) + sw.add_link(tgen.gears["r2"]) + + # r1 <- sw2 -> rp + # r1-eth1 <-> rp-eth0 + # 10.0.30.0/24 + sw = tgen.add_switch("sw2") + sw.add_link(tgen.gears["r1"]) + sw.add_link(tgen.gears["rp"]) + + # r1 <- sw3 -> r3 + # r1-eth2 <-> r3-eth0 + # 10.0.40.0/24 + sw = tgen.add_switch("sw3") + sw.add_link(tgen.gears["r1"]) + sw.add_link(tgen.gears["r3"]) + + +def setup_module(mod): + "Sets up the pytest environment" + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + + # For all registered routers, load the zebra configuration file + for rname, router in tgen.routers().items(): + logger.info("Loading router %s" % rname) + router.load_frr_config(os.path.join(CWD, "{}/frr.conf".format(rname))) + + # After loading the configurations, this function loads configured daemons. + tgen.start_router() + # tgen.mininet_cli() + + +def teardown_module(): + "Teardown the pytest environment" + tgen = get_topogen() + + # This function tears down the whole topology. + tgen.stop_topology() + + +def test_pim_rp_setup(): + "Ensure basic routing has come up and the rp has an outgoing interface" + # Ensure rp and r1 establish pim neighbor ship and bgp has come up + # Finally ensure that the rp has an outgoing interface on r1 + tgen = get_topogen() + + r1 = tgen.gears["r1"] + expected = { + "10.254.0.3":[ + { + "outboundInterface":"r1-eth1", + "group":"224.0.0.0/4", + "source":"Static" + } + ] + } + + test_func = partial( + topotest.router_json_cmp, r1, "show ip pim rp-info json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) + assertmsg = '"{}" JSON output mismatches'.format(r1.name) + assert result is None, assertmsg + # tgen.mininet_cli() + + +def test_pim_asm_igmp_join_acl(): + "Test ASM IGMP joins with prefix-list ACLs" + logger.info("Send IGMP joins from r2 to r1 with ACL enabled and disabled") + + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + r2 = tgen.gears["r2"] + r1 = tgen.gears["r1"] + + # No IGMP sources other than from self for AutoRP Discovery group initially + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "224.0.1.40":"*", + "229.1.1.1":None + }, + "r1-eth2":{ + "name":"r1-eth2", + "224.0.1.40":"*", + "229.1.1.1":None + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected no IGMP sources other than for AutoRP Discovery" + + # Send IGMP join from r2, check if r1 has IGMP source + r2.vtysh_cmd(( + """ + configure terminal + interface {} + ip igmp join {} + """ + ).format("r2-eth0", ASM_GROUP)) + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "229.1.1.1":{ + "group":"229.1.1.1", + "sources":[ + { + "source":"*", + "timer":"--:--", + "forwarded":False, + "uptime":"*" + } + ] + } + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP source to be present but is absent" + + # Test inbound boundary on r1 + # Enable multicast boundary on r1, toggle IGMP join on r2 + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + no ip igmp join {} + """ + ).format(ASM_GROUP)) + r1.vtysh_cmd( + """ + configure terminal + interface r1-eth0 + ip multicast boundary oil pim-oil-plist + """ + ) + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + ip igmp join {} + """ + ).format(ASM_GROUP)) + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "229.1.1.1":None + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP source to be absent but is present" + + # Test outbound boundary on r2 + # Enable multicast boundary on r2, toggle IGMP join (test outbound) + # Note: json_cmp treats "*" as wildcard but in this case that's actually what the source is + expected = { + "vrf":"default", + "r2-eth0":{ + "name":"r2-eth0", + "groups":[ + { + "source":"*", + "group":"229.1.1.1", + "primaryAddr":"10.0.20.2", + "sockFd":"*", + "upTime":"*" + } + ] + } + } + test_func = partial( + topotest.router_json_cmp, r2, "show ip igmp join json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP join to be present but is absent" + + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + no ip igmp join {} + ip multicast boundary oil pim-oil-plist + ip igmp join {} + """ + ).format(ASM_GROUP, ASM_GROUP)) + expected = { + "vrf":"default", + "r2-eth0":None + } + test_func = partial( + topotest.router_json_cmp, r2, "show ip igmp join json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP join to be absent but is present" + + # Cleanup + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + no ip igmp join {} + no ip multicast boundary oil pim-oil-plist + """ + ).format(ASM_GROUP)) + + +def test_pim_ssm_igmp_join_acl(): + "Test SSM IGMP joins with extended ACLs" + logger.info("Send IGMP joins from r2 to r1 with ACL enabled and disabled") + + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + r3 = tgen.gears["r3"] + r2 = tgen.gears["r2"] + r1 = tgen.gears["r1"] + + # No IGMP sources other than from self for AutoRP Discovery group initially + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "224.0.1.40":"*", + "229.1.1.1":None, + "232.1.1.1":None + }, + "r1-eth2":{ + "name":"r1-eth2", + "224.0.1.40":"*", + "229.1.1.1":None, + "232.1.1.1":None + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", {} + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected no IGMP sources other than from AutoRP Discovery" + + # Send IGMP join from r2, check if r1 has IGMP source + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + ip igmp join {} 10.0.20.2 + """ + ).format(SSM_GROUP)) + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "232.1.1.1":{ + "group":"232.1.1.1", + "sources":[ + { + "source":"10.0.20.2", + "timer":"*", + "forwarded":False, + "uptime":"*" + } + ] + } + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP source to be present but is absent" + + # Test inbound boundary on r1 + # Enable multicast boundary on r1, toggle IGMP join on r2 + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + no ip igmp join {} 10.0.20.2 + """ + ).format(SSM_GROUP)) + r1.vtysh_cmd( + """ + configure terminal + interface r1-eth0 + ip multicast boundary pim-acl + """ + ) + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + ip igmp join {} 10.0.20.2 + """ + ).format(SSM_GROUP)) + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "232.1.1.1":None + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP source to be absent but is present" + + # Add lower, more-specific permit rule to access-list + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + no ip igmp join {} 10.0.20.2 + """ + ).format(SSM_GROUP)) + tgen.routers()["r2"].cmd(("vtysh -c 'conf t' -c 'int {}' -c 'no ip igmp join {} 10.0.20.2'").format("r2-eth0", SSM_GROUP)) + r1.vtysh_cmd(( + """ + configure terminal + access-list pim-acl seq 5 permit ip host 10.0.20.2 {} 0.0.0.128 + """ + ).format(SSM_GROUP)) + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + ip igmp join {} 10.0.20.2 + """ + ).format(SSM_GROUP)) + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "232.1.1.1":{ + "group":"232.1.1.1", + "sources":[ + { + "source":"10.0.20.2", + "timer":"*", + "forwarded":False, + "uptime":"*" + } + ] + } + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP source to be present but is absent" + + # Test outbound boundary on r2 + # Enable multicast boundary on r2, toggle IGMP join (test outbound) + # Note: json_cmp treats "*" as wildcard but in this case that's actually what the source is + expected = { + "vrf":"default", + "r2-eth0":{ + "name":"r2-eth0", + "groups":[ + { + "source":"10.0.20.2", + "group":"232.1.1.1", + "primaryAddr":"10.0.20.2", + "sockFd":"*", + "upTime":"*" + } + ] + } + } + test_func = partial( + topotest.router_json_cmp, r2, "show ip igmp join json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP join to be present but is absent" + + # Enable boundary ACL, check join is absent + r2.vtysh_cmd(( + """ + configure terminal + interface r2-eth0 + no ip igmp join {} 10.0.20.2 + ip multicast boundary pim-acl + ip igmp join {} 10.0.20.2 + """ + ).format(SSM_GROUP, SSM_GROUP)) + expected = { + "vrf":"default", + "r2-eth0":None + } + test_func = partial( + topotest.router_json_cmp, r2, "show ip igmp join json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP join to be absent but is present" + # Check sources on r1 again, should be absent even though we permitted it because r2 is blocking it outbound + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "232.1.1.1":None + }, + "r1-eth2":{ + "name":"r1-eth2", + "232.1.1.1":None + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP source to be absent but is present" + + # Send IGMP join from r3 with different source, should show up on r1 + # Add lower, more-specific permit rule to access-list + r3.vtysh_cmd(( + """ + configure terminal + interface r3-eth0 + ip igmp join {} 10.0.40.4 + """ + ).format(SSM_GROUP)) + expected = { + "r1-eth0":{ + "name":"r1-eth0", + "232.1.1.1":None + }, + "r1-eth2":{ + "name":"r1-eth2", + "232.1.1.1":{ + "group":"232.1.1.1", + "sources":[ + { + "source":"10.0.40.4", + "timer":"*", + "forwarded":False, + "uptime":"*" + } + ] + } + } + } + test_func = partial( + topotest.router_json_cmp, r1, "show ip igmp sources json", expected + ) + _, result = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result is None, "Expected IGMP source to be present but is absent" + + # PIM join + # PIM-DM forwarding + + +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))