diff --git a/netsim/ansible/templates/bgp/srlinux.j2 b/netsim/ansible/templates/bgp/srlinux.j2 index ee03a8e749..adc5df5e87 100644 --- a/netsim/ansible/templates/bgp/srlinux.j2 +++ b/netsim/ansible/templates/bgp/srlinux.j2 @@ -1,29 +1,4 @@ -updates: - -{# - Define default import/export policies for iBGP and eBGP: accept all -#} -- path: /routing-policy/policy[name=accept_all] - value: - default-action: - policy-result: accept - -- path: /routing-policy/community-set[name=ibgp-mark] - value: - member: [ "65536:0:65536" ] - -- path: /routing-policy/policy[name=ibgp-mark] - value: - default-action: - policy-result: reject - statement: - - name: mark-ibgp-routes - action: - bgp: - communities: - add: ibgp-mark - policy-result: accept - {% from "srlinux.macro.j2" import bgp_config,bgp_shortcut with context %} +updates: {{ bgp_config('default',bgp.as,bgp.router_id,bgp,{ 'af' : af }) }} {{ bgp_shortcut(bgp,'default') }} diff --git a/netsim/ansible/templates/bgp/srlinux.macro.j2 b/netsim/ansible/templates/bgp/srlinux.macro.j2 index 1863a24eae..7ebb35c34e 100644 --- a/netsim/ansible/templates/bgp/srlinux.macro.j2 +++ b/netsim/ansible/templates/bgp/srlinux.macro.j2 @@ -1,3 +1,29 @@ +{% macro bgp_common_policies() %} +{# + Define default import/export policies for iBGP and eBGP: accept all +#} +- path: /routing-policy/policy[name=accept_all] + value: + default-action: + policy-result: accept + +- path: /routing-policy/community-set[name=ibgp-mark] + value: + member: [ "65536:0:65536" ] + +- path: /routing-policy/policy[name=ibgp-mark] + value: + default-action: + policy-result: reject + statement: + - name: mark-ibgp-routes + action: + bgp: + communities: + add: ibgp-mark + policy-result: accept +{% endmacro %} + {% macro bgp_export_prefix(vrf,prefix) %} - path: /routing-policy/prefix-set[name={{vrf}}_bgp_advertise] value: @@ -180,6 +206,7 @@ for loops are there to ensure the AF-IBGP export policy is defined only once) #} +{{ bgp_common_policies() }} {{ bgp_export_policy(vrf,'bgp',import=vrf_bgp.import|default([])) }} {% if bgp.next_hop_self|default(False) and bgp.rr|default(False) %} {% for n in vrf_bgp.neighbors|default([]) if n.type == 'ibgp' %} diff --git a/netsim/ansible/templates/routing/srlinux/redistribute.j2 b/netsim/ansible/templates/routing/srlinux/redistribute.j2 index 1f212eefcf..c0e98b3841 100644 --- a/netsim/ansible/templates/routing/srlinux/redistribute.j2 +++ b/netsim/ansible/templates/routing/srlinux/redistribute.j2 @@ -42,14 +42,20 @@ {% endif %} {% for s_proto in p_data.import|default([]) %} {% for srl_proto in netlab_match_protomap[s_proto] - if (srl_proto != 'bgp-evpn' or evpn_active) and - (srl_proto != 'bgp-ipvpn' or (vrf != 'default' and mpls.vpn is defined)) %} + if (srl_proto != 'bgp-evpn' or evpn_active) and srl_proto != 'bgp-ipvpn' %} - name: export_{{ srl_proto }} match: protocol: {{ srl_proto }} action: policy-result: accept {% endfor %} +{% if s_proto == 'bgp' and vrf != 'default' %} + - name: export_leaked + match: + network-instance-leaked-route: true + action: + policy-result: accept +{% endif %} {% endfor %} {% endif %} {% endmacro %} diff --git a/netsim/ansible/templates/vrf/srlinux.j2 b/netsim/ansible/templates/vrf/srlinux.j2 index d075fd042c..2dea7c63f1 100644 --- a/netsim/ansible/templates/vrf/srlinux.j2 +++ b/netsim/ansible/templates/vrf/srlinux.j2 @@ -27,30 +27,32 @@ updates: expression: "{{ vdata.as }}" {% endif %} -{# Creata a community set with a single member for each imported community #} -{% if vdata.import|default([]) %} -{% for c in vdata.import %} -- path: /routing-policy/community-set[name=C{{ c|replace(':','_') }}] - value: - member: - - "target:{{ c }}" # Single member, else matching is AND -{% endfor -%} +{% if 'bgp' in vdata and 'af' in vdata %} +{{ bgp_config(vname,bgp.as,vdata.bgp.router_id|default(bgp.router_id),vdata.bgp,vdata) }} {% endif %} -{# Create a single community set for all exported communities, to be added upon import #} -{% if vdata.export|default([]) %} -- path: /routing-policy/community-set[name={{vname}}_export] +{% if vdata._leaked_routes|default(False) %} +{% for intf in interfaces|default([]) if intf.vrf|default('') == vname %} +{% set if_name_index = intf.ifname.split('.') %} +{% set if_name = if_name_index[0] %} +{% set if_index = if_name_index[1] if if_name_index|length > 1 else '0' %} +{% if intf.ipv4 is string or intf.ipv6 is string %} +- path: /interface[name={{ if_name }}]/subinterface[index={{ if_index }}] value: - member: -{% for c in vdata.export %} - - "target:{{ c }}" +{% for ip,arpnd in [('ipv4','arp'),('ipv6','neighbor-discovery')] %} +{% if intf[ip] is string %} + {{ ip }}: + {{ arpnd }}: + host-route: + populate: + - route-type: dynamic + datapath-programming: true +{% endif %} +{% endfor %} +{% endif %} {% endfor %} {% endif %} -{% if 'bgp' in vdata and 'af' in vdata %} -{{ bgp_config(vname,bgp.as,vdata.bgp.router_id|default(bgp.router_id),vdata.bgp,vdata) }} -{% endif %} - - path: /network-instance[name={{vname}}]/protocols/bgp-vpn value: bgp-instance: @@ -83,9 +85,6 @@ updates: value: default-action: policy-result: "accept" - bgp: - communities: - add: "{{vname}}_export" - path: /routing-policy/policy[name={{ vname }}_vpn_import] value: @@ -93,12 +92,13 @@ updates: policy-result: "reject" statement: {% for c in vdata.import %} - - name: {{ 10 + loop.index }} +{% for src_name,src_data in vrfs.items() if c in src_data.export|default([]) %} + - name: {{ 10 + src_data.vrfidx }} match: - bgp: - community-set: "C{{ c|replace(':','_') }}" + origin-network-instance: "{{ src_name }}" action: policy-result: "accept" +{% endfor %} {% endfor %} {% endif %} diff --git a/netsim/devices/srlinux.py b/netsim/devices/srlinux.py index 483c19427d..bd5eaa5737 100644 --- a/netsim/devices/srlinux.py +++ b/netsim/devices/srlinux.py @@ -201,19 +201,6 @@ def device_quirks(self, node: Box, topology: Box) -> None: is_licensed = True mods = node.get('module',[]) - if 'vrf' in mods and 'evpn' not in mods: - vlist = [] - for vname,vrf in node.get('vrfs', {}).items(): - if len(vrf['import']) > 1 or len(vrf['export']) > 1: - vlist.append(vname) - - if vlist: - report_quirk( - text='Inter-VRF route leaking is supported only in combination with BGP EVPN', - more_data=[ f'Node {node.name} VRF(s) {",".join(vlist)}' ], - node=node, - quirk='vrf_route_leaking', - category=log.IncorrectType) if 'bgp' in mods: cleanup_neighbor_transport(node,topology) diff --git a/tests/integration/vrf/srl-asymmetric-leaking-fails.yml b/tests/integration/vrf/srl-asymmetric-leaking-fails.yml new file mode 100644 index 0000000000..5c8c9f2046 --- /dev/null +++ b/tests/integration/vrf/srl-asymmetric-leaking-fails.yml @@ -0,0 +1,101 @@ +--- +message: | + The device under test has three VRFs with one OSPF-speaking CE router in + each VRF. Every VRF imports routes from exactly one other VRF: + + * red imports green + * green imports blue + * blue imports red + + The lab tests asymmetric inter-VRF route leaking by checking that every CE + router receives only the routes imported by its local VRF. + +defaults.sources.extra: [ ../wait_times.yml, ../warnings.yml ] + +defaults.interfaces.mtu: 1500 + +groups: + _auto_create: True + ce: + members: [ r1, r2, r3 ] + module: [ ospf ] + device: frr + provider: clab + pe: + members: [ dut ] + module: [ vrf, ospf ] + +vrfs: + red: + import: [ green ] + links: [ dut-r1 ] + green: + import: [ blue ] + links: [ dut-r2 ] + blue: + import: [ red ] + links: [ dut-r3 ] + +nodes: # Set different router ID on every OSPF process + dut: # ... to keep Cisco IOS happy + id: 1 + vrfs: + red: + ospf.router_id: 10.100.0.100 + green: + ospf.router_id: 10.100.0.101 + blue: + ospf.router_id: 10.100.0.102 + +validate: + red_adj: + description: Check OSPF adjacencies in red VRF + wait_msg: Waiting for OSPF adjacencies to form + wait: ospfv2_adj_p2p + nodes: [ r1 ] + plugin: ospf_neighbor(nodes.dut.vrfs.red.ospf.router_id) + stop_on_error: true + green_adj: + description: Check OSPF adjacencies in green VRF + wait_msg: Waiting for OSPF adjacencies to form + wait: ospfv2_adj_p2p + nodes: [ r2 ] + plugin: ospf_neighbor(nodes.dut.vrfs.green.ospf.router_id) + stop_on_error: true + blue_adj: + description: Check OSPF adjacencies in blue VRF + wait_msg: Waiting for OSPF adjacencies to form + wait: ospfv2_adj_p2p + nodes: [ r3 ] + plugin: ospf_neighbor(nodes.dut.vrfs.blue.ospf.router_id) + stop_on_error: true + red_import: + description: Check green route imported into red VRF + wait: ospf_import + wait_msg: Waiting for inter-VRF OSPF route leaking + nodes: [ r1 ] + plugin: ospf_prefix(nodes.r2.loopback.ipv4) + red_no_blue: + description: Check blue route is not imported into red VRF + nodes: [ r1 ] + plugin: ospf_prefix(nodes.r3.loopback.ipv4,state='missing') + green_import: + description: Check blue route imported into green VRF + wait: ospf_import + wait_msg: Waiting for inter-VRF OSPF route leaking + nodes: [ r2 ] + plugin: ospf_prefix(nodes.r3.loopback.ipv4) + green_no_red: + description: Check red route is not imported into green VRF + nodes: [ r2 ] + plugin: ospf_prefix(nodes.r1.loopback.ipv4,state='missing') + blue_import: + description: Check red route imported into blue VRF + wait: ospf_import + wait_msg: Waiting for inter-VRF OSPF route leaking + nodes: [ r3 ] + plugin: ospf_prefix(nodes.r1.loopback.ipv4) + blue_no_green: + description: Check green route is not imported into blue VRF + nodes: [ r3 ] + plugin: ospf_prefix(nodes.r2.loopback.ipv4,state='missing')