diff --git a/docs/caveats.md b/docs/caveats.md index 42bd0f9b04..ea10c1095b 100644 --- a/docs/caveats.md +++ b/docs/caveats.md @@ -86,7 +86,7 @@ nodes: * You must build the BIRD container image with the **netlab clab build bird** command. Without additional arguments, that command installs the latest BIRD v3 package from CZ.NIC BIRD repository on top of Ubuntu 24.04. See [](build-bird) for build targets and version options. * BIRD is implemented as a pure control-plane daemon running as a container with a single external interface. You can set the node **role** to **router** to turn a BIRD instance into a more traditional networking device with a loopback interface. -* BIRD supports a single router ID used for BGP and OSPF. +* BIRD uses the node BGP router ID as the default router ID for BGP and OSPF protocol instances. * The VM or container running BIRD in host mode starts with static routes pointing to one of the adjacent routers (see [](linux-forwarding)). After establishing routing adjacencies, BIRD copies BGP and OSPF into the kernel IP routing table. * The **bird** container starts BIRD in foreground mode with logging messages (including debugging messages) sent to *stderr*. Use the **docker logs** command to inspect the BIRD messages. diff --git a/docs/module/bgp.md b/docs/module/bgp.md index 4af2617855..79a254ff74 100644 --- a/docs/module/bgp.md +++ b/docs/module/bgp.md @@ -61,7 +61,7 @@ The following features are only supported on a subset of platforms: | ------------------------ | :-: | :-: | :-: | :-: | | Arista EOS | ✅ | ✅ | ✅ | ✅ | | Aruba AOS-CX | ✅ | ❌ | ✅ | ✅ | -| BIRD | ✅ | ✅ | ❌ | ❌ | +| BIRD | ✅ | ✅ | ✅ | ✅ | | Cisco IOS/IOS XE[^18v] | ✅ | ✅ | ✅ | ✅ | | Cisco IOS XR[^XR] | ✅ | ✅ | ✅ | ✅ | | Cumulus Linux 4.x | ✅ | ❌ | ✅ | ✅ | diff --git a/docs/module/routing-static.txt b/docs/module/routing-static.txt index ca320547cd..9b5dd1910f 100644 --- a/docs/module/routing-static.txt +++ b/docs/module/routing-static.txt @@ -11,6 +11,7 @@ _netlab_ supports static routes on these platforms: |---------------------|:--:|:--:|:--:|:--:| | Arista EOS | ✅ | ✅ | ✅ | ✅ | | Aruba AOS-CX | ✅ | ✅ | ✅ | [❗](caveats-aruba) | +| BIRD | ✅ | ✅ | ✅ | ✅ | | Cisco IOS/XE[^18v] | ✅ | ✅ | ✅ | ✅ | | Cisco IOS XR[^XR] | ✅ | ✅ | ✅ | ✅ | | Cisco Nexus OS | ✅ | ✅ | ✅ | ❌ | diff --git a/docs/module/routing.md b/docs/module/routing.md index 89c9f97f31..18fb065e86 100644 --- a/docs/module/routing.md +++ b/docs/module/routing.md @@ -25,6 +25,7 @@ The following table describes high-level per-platform support of generic routing | ------------------ |:--:|:--:|:--:|:--:|:--:| | Arista EOS | ✅ | ✅ | ✅ | ✅ | ✅ | | Aruba AOS-CX | ✅ | ✅ | ✅ | ✅ | ✅ | +| BIRD | ❌ | ❌ | ❌ | ❌ | ✅ | | Cisco IOS/IOS XE[^18v] | ✅ | ✅ | ✅ | ✅ | ✅ | | Cisco IOS XR[^XR] | ✅ | ✅ | ✅ | ✅ | ✅ | | Cisco Nexus OS | ❌ | ❌ | ❌ | ❌ | ✅ | diff --git a/docs/module/vrf.md b/docs/module/vrf.md index d6774c8368..e8224cdd91 100644 --- a/docs/module/vrf.md +++ b/docs/module/vrf.md @@ -19,6 +19,7 @@ VRFs are supported on these platforms: | --------------------- | :-: | :-: | :-: | | Arista EOS | ✅ | ✅ | ✅ | | Aruba AOS-CX | ✅ | ✅ | ✅ | +| BIRD | ✅ | ✅ | ✅ | | Cisco IOS | ✅ | ✅ | ✅ | | Cisco IOS XE[^18v] | ✅ | ✅ | ✅ | | Cisco IOS XR[^XR] | ✅ | ✅ | ✅ | @@ -51,6 +52,7 @@ These platforms support routing protocols in VRFs: | --------------------- | :-: | :-: | :-: | :-: | :-: | :-: | | Arista EOS | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | | Aruba AOS-CX | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| BIRD | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | | Cisco IOS/IOSvL2 | ✅ [❗](caveats-iosv) | ✅ | ✅ | ✅ | ✅ | ❌ | | Cisco IOS XE[^18v] | ✅ [❗](caveats-csr) | ✅ | ✅ | ✅ | ✅ | ❌ | | Cisco IOS XR[^XR] | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | diff --git a/docs/platforms.md b/docs/platforms.md index d1e35414ce..f515e904d2 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -464,7 +464,7 @@ The data plane [configuration modules](module-reference.md) are supported on the | --------------------- |:--:|:--:|:--:|:--:|:--:|:--:| | Arista EOS | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | Aruba AOS-CX | ✅ | ✅ | ✅[❗](caveats-aruba) | [❗](caveats-aruba) | ❌ | ❌ | -| BIRD | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| BIRD | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | | Cisco 8000v (IOS XR) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | | Cisco Catalyst 8000v | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Cisco CSR 1000v | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | diff --git a/netsim/ansible/templates/vrf/bird.j2 b/netsim/ansible/templates/vrf/bird.j2 index 5c5132a3c0..6fa681e0ba 100644 --- a/netsim/ansible/templates/vrf/bird.j2 +++ b/netsim/ansible/templates/vrf/bird.j2 @@ -1,7 +1,10 @@ -#!/bin/sh +#!/bin/bash # set -e # -# Placeholder: VRF data-plane configuration for BIRD +{% if vrfs is defined %} +{% include "frr.data-plane.j2" +%} +{% endif %} # touch /var/run/vrf.done +exit 0 diff --git a/netsim/ansible/templates/vrf/frr.data-plane.j2 b/netsim/ansible/templates/vrf/frr.data-plane.j2 new file mode 100644 index 0000000000..f40d5faf22 --- /dev/null +++ b/netsim/ansible/templates/vrf/frr.data-plane.j2 @@ -0,0 +1,14 @@ +# Create VRF tables +{% set _vrfs = vrfs|default({}) %} +{% for vname,vdata in _vrfs.items() %} +if [ ! -e /sys/devices/virtual/net/{{vname}} ]; then +ip link add {{vname}} type vrf table {{ vdata.vrfidx }} +fi +ip link set {{vname}} up +{% endfor %} + +# Move interfaces and loopbacks to vrfs +{% for i in interfaces if i.vrf is defined %} +sysctl -qw net/ipv6/conf/{{ i.ifname }}/keep_addr_on_down=1 +ip link set {{ i.ifname }} master {{ i.vrf }} +{% endfor %} diff --git a/netsim/ansible/templates/vrf/frr.j2 b/netsim/ansible/templates/vrf/frr.j2 index 5cc90b9ac2..51bc5567df 100644 --- a/netsim/ansible/templates/vrf/frr.j2 +++ b/netsim/ansible/templates/vrf/frr.j2 @@ -1,24 +1,8 @@ #!/bin/bash # -set -e # Exit immediately when any command fails +set -e # - -# Create VRF tables -{% set _vrfs = vrfs|default({}) %} -{% for vname,vdata in _vrfs.items() %} -if [ ! -e /sys/devices/virtual/net/{{vname}} ]; then -ip link add {{vname}} type vrf table {{ vdata.vrfidx }} -fi -ip link set {{vname}} up -{% endfor %} - -# Move interfaces and loopbacks to vrfs -{% for i in interfaces if i.vrf is defined %} -sysctl -qw net.ipv6.conf.{{ i.ifname }}.keep_addr_on_down=1 -ip link set {{ i.ifname }} master {{ i.vrf }} -{% endfor %} - -{% if _vrfs %} +{% if vrfs is defined %} +{% include "frr.data-plane.j2" +%} {% include "frr.frr-config.j2" +%} {% endif %} -exit $? diff --git a/netsim/daemons/bird.yml b/netsim/daemons/bird.yml index abd88e3ca2..224e89956e 100644 --- a/netsim/daemons/bird.yml +++ b/netsim/daemons/bird.yml @@ -52,7 +52,7 @@ features: large: [ large ] extended: [ extended ] 2octet: [ standard ] - import: [ ospf, connected, static ] + import: [ ospf, connected, static, vrf ] bfd: true default_originate: true gtsm: true @@ -72,7 +72,10 @@ features: unnumbered: true areas: true routing: - static.discard: true + static: + vrf: true + inter_vrf: true + discard: true dhcp: false gateway: protocol: [ anycast ] @@ -85,4 +88,7 @@ features: roles: [ host, router ] vxlan: vtep6: true -# vrf: {} # Placeholder, has to be replaced with the real VRF code + vrf: + ospfv2: true + ospfv3: true + bgp: true diff --git a/netsim/daemons/bird/bgp.j2 b/netsim/daemons/bird/bgp.j2 index a2b605b17e..b31ea75c2d 100644 --- a/netsim/daemons/bird/bgp.j2 +++ b/netsim/daemons/bird/bgp.j2 @@ -1,154 +1,12 @@ +{% import 'bgp.macros.j2' as bgp_config with context %} + {% if 'router_id' in bgp %} router id {{ bgp.router_id }}; {% endif %} - -{% for ngb in bgp.neighbors|default([]) if ngb.default_originate|default(False) %} -{% if loop.first %} -{% for _af in ['ipv4','ipv6'] if _af in af %} -protocol static static_bgp_default_{{ _af }} { - {{ _af }}; - route {{ '0.0.0.0/0' if _af=='ipv4' else '::/0' }} reject; -} -{% endfor %} -{% endif %} -{% endfor %} -{% for pfx_af in ['ipv4','ipv6'] %} -{% for pfx in bgp.advertise|default([]) if pfx_af in pfx %} -{% if loop.first %} - -define bgp_advertise_{{ pfx_af }} = [ -{% endif %} - {{ pfx[pfx_af] }}{% if not loop.last %},{% endif +%} -{% if loop.last %} -]; -{% endif %} -{% endfor %} -{% endfor %} -function bgp_prefixes( bool originate_default ) { - if net.len = 0 && !originate_default - then reject "Don't originate default route"; - - if source ~ [ RTS_BGP ] - then accept "BGP route:", net; - - if proto ~ "static_bgp_default*" - then accept "BGP prefix origination:", net; - -{% if bgp.import is defined %} -{% for proto in bgp.import %} - if source ~ [ {{ netlab_import_map[proto] }} ] - then accept "{{ proto }} route:", net; -{% endfor %} -{% endif %} -{% for pfx_af in ['ipv4','ipv6'] %} -{% for pfx in bgp.advertise|default([]) if pfx_af in pfx %} -{% if loop.first %} - - if net ~ bgp_advertise_{{ pfx_af }} then accept "advertise prefix:", net; -{% endif %} -{% endfor %} -{% endfor %} - - reject "not accepted:", net, " source=", source, " preference=", preference, " proto=", proto; -} - -function remove_private_as() { - bgp_path.delete([64512..65534, 4200000000..4294967294]); -} - -{# - Build a BGP export filter per neighbor type, to filter communities -#} -{% for ntype in [ 'ebgp', 'ibgp', 'localas_ibgp', 'confed_ebgp' ] %} -function bgp_export_{{ ntype }}( bool originate_default; bool rem_private_as ) { -{% set list = bgp.community[ntype]|default([]) %} -{% if 'standard' not in list %} - bgp_community.empty; -{% endif %} -{% if 'large' not in list %} - bgp_large_community.empty; -{% endif %} -{% if 'extended' not in list %} - bgp_ext_community.empty; -{% endif %} - - if rem_private_as then remove_private_as(); - bgp_prefixes(originate_default); -} -{% endfor %} - -{% for n in bgp.neighbors %} -{% for af in [ 'ipv4','ipv6' ] if af in n and n[af] is string %} -protocol bgp bgp_{{ n.name }}_{{ af }} { -{% set loopback = loopback|default({}) %} -{% set local_ip = loopback[af]|default('') %} - local {{ local_ip.split('/')[0] if n.type == 'ibgp' else '' }} as {{ n.local_as|default(bgp.as) }}; -{% if bgp.confederation is defined %} - confederation {{ bgp.confederation.as }}; -{% if n.type == 'confed_ebgp' %} - confederation member yes; -{% endif %} -{% endif %} - neighbor {{ n[af] }} as {{ n['as'] }}; - connect retry time 5; - error wait time 5,10; - startup hold time 10; -{% if n.local_if is defined %} - interface "{{ n.local_if }}"; -{% endif %} -{% if n.bfd is defined %} - bfd yes; -{% endif %} -{% if n.passive is defined %} - passive yes; -{% endif %} -{% if n.timers is defined %} - hold time {{ n.timers.hold|default(180) }}; - keepalive time {{ n.timers.keepalive|default(60) }}; -{% endif %} -{% if n.password is defined %} - password "{{ n.password }}"; -{% endif %} -{% if n.rs|default(False) %} - rs client; -{% endif %} -{% if n.rs_client|default(False) %} - enforce first as off; -{% endif %} -{% if n.role is defined %} - local role {{ n.role.name|replace('-', '_') }}; -{% if n.role.strict|default(False) %} - require roles; -{% endif %} -{% endif %} -{% if bgp.rr|default('') and ((not n.rr|default('') and n.type == 'ibgp') or n.type == 'localas_ibgp') %} - rr client; -{% if bgp.rr|default(False) and bgp.rr_cluster_id|default(False) %} - rr cluster id {{ bgp.rr_cluster_id }}; -{% endif %} -{% endif %} -{% if n.gtsm is defined %} - ttl security on; -{% if n.type=='ibgp' %} - multihop {{ n.gtsm }}; -{% endif %} -{% endif %} -{% for _af in [ 'ipv4','ipv6' ] if _af==af or (_af=='ipv4' and n.ipv4_rfc8950|default(False)) %} -{% if _af in n.activate and n.activate[_af] %} - {{ _af }} { - import all; - export filter { bgp_export_{{ n.type }}( {{ 'true' if n.get('default_originate') else 'false' }}, - {{ 'true' if n.get('remove_private_as') else 'false' }}); }; -{% if n.next_hop_self|default(false) %} - next hop self {{ 'on' if n.next_hop_self == 'all' else 'ebgp' }}; -{% endif %} -{% if _af=='ipv4' and n.ipv4_rfc8950|default(False) %} - extended next hop on; - # require extended next hop on; -{% endif %} - }; -{% endif %} -{% endfor %} -} -{% endfor %} +{{ bgp_config.bgp_default_originate_routes(bgp) }} +{{ bgp_config.bgp_advertise_list('bgp_advertise',bgp.advertise) }} +{{ bgp_config.bgp_prefixes_function('bgp_prefixes',bgp,'bgp_advertise',bgp.import|default([]),True) }} +{{ bgp_config.bgp_export_filters('bgp_export_','bgp_prefixes') }} +{% for n in bgp.neighbors|default([]) %} +{{ bgp_config.bgp_session(n,bgp,'',{},netlab_interfaces) }} {% endfor %} diff --git a/netsim/daemons/bird/bgp.macros.j2 b/netsim/daemons/bird/bgp.macros.j2 new file mode 100644 index 0000000000..598446f079 --- /dev/null +++ b/netsim/daemons/bird/bgp.macros.j2 @@ -0,0 +1,205 @@ +{# + # rt_value: Return a BIRD extended community route-target tuple from a netlab RT string. + #} +{% macro rt_value(rt) -%} +(rt, {{ rt.split(':')[0] }}, {{ rt.split(':')[1] }}) +{%- endmacro %} + +{# + # tag_vrf_route: Tag an imported VRF route with all export route targets configured on the VRF. + #} +{% macro tag_vrf_route(vrf_data) %} + netlab_vrf_rt.empty; +{% for rt in vrf_data.export|default([]) %} + netlab_vrf_rt.add({{ rt_value(rt) }}); +{% endfor %} + accept; +{% endmacro %} + +{# + # bgp_default_originate_routes: Render reject static routes used to originate default routes toward selected BGP neighbors. + #} +{% macro bgp_default_originate_routes(bgp_data) %} +{% for ngb in bgp_data.neighbors|default([]) if ngb.default_originate|default(False) %} +{% if loop.first %} +{% for _af in ['ipv4','ipv6'] if _af in af %} +protocol static static_bgp_default_{{ _af }} { + {{ _af }}; + route {{ '0.0.0.0/0' if _af=='ipv4' else '::/0' }} reject; +} +{% endfor %} +{% endif %} +{% endfor %} +{% endmacro %} + +{# + # bgp_advertise_list: Render BIRD prefix-set definitions for the configured advertise list. + #} +{% macro bgp_advertise_list(list_name,pfx_list) %} +{% for pfx_af in ['ipv4','ipv6'] %} +{% for pfx in pfx_list|default([]) if pfx_af in pfx %} +{% if loop.first %} + +define {{ list_name }}_{{ pfx_af }} = [ +{% endif %} + {{ pfx[pfx_af] }}{% if not loop.last %},{% endif +%} +{% if loop.last %} +]; +{% endif %} +{% endfor %} +{% endfor %} +{% endmacro %} + +{# + # bgp_prefixes_function: Render the BGP export prefix-selection function used by neighbor export filters. + #} +{% macro bgp_prefixes_function(func_name,bgp_data,list_name,import_list,include_default_static) %} +function {{ func_name }}( bool originate_default ) { + if net.len = 0 && !originate_default + then reject "Don't originate default route"; + + if source ~ [ RTS_BGP ] + then accept "BGP route:", net; + +{% if include_default_static %} + if proto ~ "static_bgp_default*" + then accept "BGP prefix origination:", net; + +{% endif %} +{% for proto in import_list|default([]) %} + if source ~ [ {{ netlab_import_map[proto] }} ] + then accept "{{ proto }} route:", net; +{% endfor %} +{% for pfx_af in ['ipv4','ipv6'] %} +{% for pfx in bgp_data.advertise|default([]) if pfx_af in pfx %} +{% if loop.first %} + + if net ~ {{ list_name }}_{{ pfx_af }} then accept "advertise prefix:", net; +{% endif %} +{% endfor %} +{% endfor %} + + reject "not accepted:", net, " source=", source, " preference=", preference, " proto=", proto; +} +{% endmacro %} + +{# + # bgp_export_filter: Render one BGP export filter function for a neighbor type and community policy. + #} +{% macro bgp_export_filter(func_name,ntype,prefix_func) %} +function {{ func_name }}( bool originate_default; bool rem_private_as ) { +{% set list = bgp.community[ntype]|default([]) %} +{% if 'standard' not in list %} + bgp_community.empty; +{% endif %} +{% if 'large' not in list %} + bgp_large_community.empty; +{% endif %} +{% if 'extended' not in list %} + bgp_ext_community.empty; +{% endif %} + + if rem_private_as then bgp_path.delete([64512..65534, 4200000000..4294967294]); + {{ prefix_func }}(originate_default); +} +{% endmacro %} + +{# + # bgp_export_filters: Render all BGP export filter functions needed by the global or VRF BGP instance. + #} +{% macro bgp_export_filters(func_prefix,prefix_func) %} +{% for ntype in [ 'ebgp', 'ibgp', 'localas_ibgp', 'confed_ebgp' ] %} +{{ bgp_export_filter(func_prefix + ntype,ntype,prefix_func) }} +{% endfor %} +{% endmacro %} + +{# + # bgp_session: Render per-address-family BGP protocol instances for a single transformed neighbor. + #} +{% macro bgp_session(n,bgp_data,vrf_name,vrf_data,bgp_interfaces) %} +{% for af in [ 'ipv4','ipv6' ] if af in n and n[af] is string %} +protocol bgp bgp_{{ ('vrf_' + vrf_name + '_') if vrf_name else '' }}{{ n.name }}_{{ af }} { +{% if vrf_name %} + vrf "{{ vrf_name }}"; +{% if 'router_id' in vrf_data.bgp %} + router id {{ vrf_data.bgp.router_id }}; +{% endif %} +{% endif %} +{% set local_if = bgp_interfaces|selectattr('ifindex','eq',n.ifindex)|first if n.type != 'ibgp' else loopback %} + local {{ local_if[af]|ansible.utils.ipaddr('address') }} as {{ n.local_as|default(bgp_data.as|default(bgp.as)) }}; +{% if bgp_data.confederation is defined %} + confederation {{ bgp_data.confederation.as }}; +{% if n.type == 'confed_ebgp' %} + confederation member yes; +{% endif %} +{% endif %} + neighbor {{ n[af] }} as {{ n['as'] }}; + connect retry time 5; + error wait time 5,10; + startup hold time 10; +{% if n.local_if is defined %} + interface "{{ n.local_if }}"; +{% endif %} +{% if n.bfd is defined %} + bfd yes; +{% endif %} +{% if n.passive is defined %} + passive yes; +{% endif %} +{% if n.timers is defined %} + hold time {{ n.timers.hold|default(180) }}; + keepalive time {{ n.timers.keepalive|default(60) }}; +{% endif %} +{% if n.password is defined %} + password "{{ n.password }}"; +{% endif %} +{% if n.rs|default(False) %} + rs client; +{% endif %} +{% if n.rs_client|default(False) %} + enforce first as off; +{% endif %} +{% if n.role is defined %} + local role {{ n.role.name|replace('-', '_') }}; +{% if n.role.strict|default(False) %} + require roles; +{% endif %} +{% endif %} +{% if bgp_data.rr|default('') and ((not n.rr|default('') and n.type == 'ibgp') or n.type == 'localas_ibgp') %} + rr client; +{% if bgp_data.rr|default(False) and bgp_data.rr_cluster_id|default(False) %} + rr cluster id {{ bgp_data.rr_cluster_id }}; +{% endif %} +{% endif %} +{% if n.gtsm is defined %} + ttl security on; +{% if n.type=='ibgp' %} + multihop {{ n.gtsm }}; +{% endif %} +{% endif %} +{% for _af in [ 'ipv4','ipv6' ] if _af==af or (_af=='ipv4' and n.ipv4_rfc8950|default(False)) %} +{% if _af in n.activate and n.activate[_af] %} + {{ _af }} { +{% if vrf_name %} + table vrf_{{ vrf_name }}_{{ _af }}; + import filter { +{{ tag_vrf_route(vrf_data) }} + }; +{% else %} + import all; +{% endif %} + export filter { bgp_export_{{ ('vrf_' + vrf_name + '_') if vrf_name else '' }}{{ n.type }}( {{ 'true' if n.get('default_originate') else 'false' }}, + {{ 'true' if n.get('remove_private_as') else 'false' }}); }; +{% if n.next_hop_self|default(false) %} + next hop self {{ 'on' if n.next_hop_self == 'all' else 'ebgp' }}; +{% endif %} +{% if _af=='ipv4' and n.ipv4_rfc8950|default(False) %} + extended next hop on; + # require extended next hop on; +{% endif %} + }; +{% endif %} +{% endfor %} +} +{% endfor %} +{% endmacro -%} diff --git a/netsim/daemons/bird/ospf.j2 b/netsim/daemons/bird/ospf.j2 index f93399bd64..a458351c00 100644 --- a/netsim/daemons/bird/ospf.j2 +++ b/netsim/daemons/bird/ospf.j2 @@ -1,86 +1,8 @@ -{% if 'router_id' in ospf %} +{% import 'ospf.macros.j2' as ospf_config with context %} + +{% if ospf is defined %} +{% if 'router_id' in ospf %} router id {{ ospf.router_id }}; -{% endif %} -{% set KW_NETWORK_TYPE = { 'broadcast': 'bcast', 'point-to-point': 'ptp','point-to-multipoint': 'ptmp', 'non-broadcast': 'nbma' } %} -{% for af in ['ipv4','ipv6'] if af in ospf.af %} -{% set ver = 'v2' if af == 'ipv4' else 'v3' %} -protocol ospf {{ ver }} ospf_{{ ver }} { -{% if ospf.import is defined %} - {{ af }} { - export filter { - if source ~ [ {% for p in ospf.import %}{{ netlab_import_map[p] }}{% if not loop.last %},{% endif %}{% endfor %} ] then { - ospf_metric2 = 20; - accept; - } - reject; - }; - }; {% endif %} -{% for adata in ospf.areas %} - area {{ adata.area }} { -{% if adata.default.cost is defined %} - default cost {{ adata.default.cost }}; -{% endif %} -{% if adata.kind == 'stub' %} - stub; - summary {{ 'yes' if adata.inter_area else 'no' }}; -{% elif adata.kind == 'nssa' %} - nssa; - summary {{ 'yes' if adata.inter_area else 'no' }}; - translator yes; - translator stability 3; -{% if ospf._abr and adata.default|default(false) %} - default nssa on; -{% endif %} - external { -{% for r_data in adata.external_range|default([]) if af in r_data %} - {{ r_data[af] }}; -{% endfor %} -{% for r_data in adata.external_filter|default([]) if af in r_data %} - {{ r_data[af] }} hidden; -{% endfor %} - }; -{% endif %} - networks { -{% for r_data in adata.range|default([]) if af in r_data %} - {{ r_data[af] }}; -{% endfor %} -{% for r_data in adata.filter|default([]) if af in r_data %} - {{ r_data[af] }} hidden; -{% endfor %} - }; -{% for l in netlab_interfaces if af in l and l.ospf.area|default('') == adata.area %} -{% if l.ifname == 'lo' and af == 'ipv6' %} - stubnet {{ loopback.ipv6 | ipaddr('address') }}/128; -{% endif %} - interface "{{ l.ifname }}" { -{% if l.ospf.passive|default(False) or l.type == 'loopback' %} - stub; -{% endif %} -{% if 'network_type' in l.ospf %} - type {{ KW_NETWORK_TYPE[l.ospf.network_type] }}; -{% endif %} -{% if 'cost' in l.ospf %} - cost {{ l.ospf.cost }}; -{% endif %} -{% if 'timers' in l.ospf %} -{% if 'hello' in l.ospf.timers %} - hello {{ l.ospf.timers.hello }}; -{% endif %} -{% if 'dead' in l.ospf.timers %} - dead {{ l.ospf.timers.dead }}; -{% endif %} -{% endif %} -{% if 'priority' in l.ospf %} - priority {{ l.ospf.priority }}; -{% endif %} -{% if 'password' in l.ospf %} - authentication {{ 'simple' if af=='ipv4' else 'cryptographic' }}; - password "{{ l.ospf.password }}"; -{% endif %} - }; -{% endfor %} - }; -{% endfor %} -} -{% endfor %} +{{ ospf_config.ospf_config(ospf,'global','',netlab_interfaces,{}) }} +{% endif %} diff --git a/netsim/daemons/bird/ospf.macros.j2 b/netsim/daemons/bird/ospf.macros.j2 new file mode 100644 index 0000000000..312c8fc8f1 --- /dev/null +++ b/netsim/daemons/bird/ospf.macros.j2 @@ -0,0 +1,116 @@ +{% macro ospf_config(ospf,proto_suffix,vrf_name,ospf_interfaces,vrf_data) %} +{% macro rt_value(rt) -%} +(rt, {{ rt.split(':')[0] }}, {{ rt.split(':')[1] }}) +{%- endmacro %} +{% macro tag_vrf_route(vrf_data) %} + netlab_vrf_rt.empty; +{% for rt in vrf_data.export|default([]) %} + netlab_vrf_rt.add({{ rt_value(rt) }}); +{% endfor %} + accept; +{% endmacro %} +{% set KW_NETWORK_TYPE = { 'broadcast': 'bcast', 'point-to-point': 'ptp','point-to-multipoint': 'ptmp', 'non-broadcast': 'nbma' } %} +{% for af in ['ipv4','ipv6'] if af in ospf.af %} +{% set import_list = ospf.import|default(['bgp','connected','static'] if vrf_name else []) %} +{% set ver = 'v2' if af == 'ipv4' else 'v3' %} +protocol ospf {{ ver }} ospf_{{ proto_suffix }}_{{ ver }} { +{% if vrf_name %} + vrf "{{ vrf_name }}"; +{% if 'router_id' in ospf %} + router id {{ ospf.router_id }}; +{% endif %} +{% endif %} +{% if import_list or vrf_name %} + {{ af }} { +{% if vrf_name %} + table vrf_{{ vrf_name }}_{{ af }}; + import filter { +{{ tag_vrf_route(vrf_data) }} + }; +{% endif %} +{% if import_list %} + export filter { +{% if vrf_name and vrf_data.import|default([]) %} + if netlab_vrf_rt ~ [ {% for rt in vrf_data.import %}{{ rt_value(rt) }}{% if not loop.last %}, {% endif %}{% endfor %} ] then { + ospf_metric2 = 20; + accept; + } +{% endif %} + if source ~ [ {% for p in import_list %}{{ netlab_import_map[p] }}{% if not loop.last %},{% endif %}{% endfor %} ] then { + ospf_metric2 = 20; + accept; + } + reject; + }; +{% endif %} + }; +{% endif %} +{% for adata in ospf.areas %} + area {{ adata.area }} { +{% if adata.default.cost is defined %} + default cost {{ adata.default.cost }}; +{% endif %} +{% if adata.kind == 'stub' %} + stub; + summary {{ 'yes' if adata.inter_area else 'no' }}; +{% elif adata.kind == 'nssa' %} + nssa; + summary {{ 'yes' if adata.inter_area else 'no' }}; + translator yes; + translator stability 3; +{% if ospf._abr and adata.default|default(false) %} + default nssa on; +{% endif %} + external { +{% for r_data in adata.external_range|default([]) if af in r_data %} + {{ r_data[af] }}; +{% endfor %} +{% for r_data in adata.external_filter|default([]) if af in r_data %} + {{ r_data[af] }} hidden; +{% endfor %} + }; +{% endif %} + networks { +{% for r_data in adata.range|default([]) if af in r_data %} + {{ r_data[af] }}; +{% endfor %} +{% for r_data in adata.filter|default([]) if af in r_data %} + {{ r_data[af] }} hidden; +{% endfor %} + }; +{% for l in ospf_interfaces if af in l and l.ospf.area|default('') == adata.area %} +{% if l.ifname == 'lo' and af == 'ipv6' %} + stubnet {{ (vrf_data.loopback_address.ipv6 if vrf_name else loopback.ipv6) | ipaddr('address') }}/128; +{% endif %} + interface "{{ l.ifname }}" { +{% if l.ospf.passive|default(False) or l.type == 'loopback' %} + stub; +{% endif %} +{% if 'network_type' in l.ospf %} + type {{ KW_NETWORK_TYPE[l.ospf.network_type] }}; +{% endif %} +{% if 'cost' in l.ospf %} + cost {{ l.ospf.cost }}; +{% endif %} +{% if 'timers' in l.ospf %} +{% if 'hello' in l.ospf.timers %} + hello {{ l.ospf.timers.hello }}; +{% endif %} +{% if 'dead' in l.ospf.timers %} + dead {{ l.ospf.timers.dead }}; +{% endif %} +{% endif %} +{% if 'priority' in l.ospf %} + priority {{ l.ospf.priority }}; +{% endif %} +{% if 'password' in l.ospf %} + authentication {{ 'simple' if af=='ipv4' else 'cryptographic' }}; + password "{{ l.ospf.password }}"; +{% endif %} + }; +{% endfor %} + }; +{% endfor %} +} +{% endfor %} +{% endmacro -%} diff --git a/netsim/daemons/bird/protocols.j2 b/netsim/daemons/bird/protocols.j2 index 8a66be76c7..e066aeb0cc 100644 --- a/netsim/daemons/bird/protocols.j2 +++ b/netsim/daemons/bird/protocols.j2 @@ -3,6 +3,9 @@ protocol device { } protocol direct { +{% for l in netlab_interfaces if l.vrf is defined %} + interface -"{{ l.ifname }}"; +{% endfor %} ipv4; ipv6; } diff --git a/netsim/daemons/bird/routing.j2 b/netsim/daemons/bird/routing.j2 index 7ecd6a6689..692131084a 100644 --- a/netsim/daemons/bird/routing.j2 +++ b/netsim/daemons/bird/routing.j2 @@ -1,7 +1,7 @@ {# Static routes #} {% macro config_sr(sr_data,af) %} {% set sr_intf = ' dev "'+sr_data.nexthop.intf+'"' if 'intf' in sr_data.nexthop else '' %} -{% set sr_intf = '' %} +{# set sr_intf = '' #} {% set sr_nh = 'unreachable' if 'discard' in sr_data.nexthop else 'via ' + sr_data.nexthop[af] %} route {{ sr_data[af] }} {{ sr_nh }}{{ sr_intf }}; {% endmacro -%} diff --git a/netsim/daemons/bird/vrf-daemon.j2 b/netsim/daemons/bird/vrf-daemon.j2 index 29c437705a..fa865e4b3e 100644 --- a/netsim/daemons/bird/vrf-daemon.j2 +++ b/netsim/daemons/bird/vrf-daemon.j2 @@ -1,3 +1,143 @@ +{# VRF routing tables and kernel synchronization #} +{% import 'ospf.macros.j2' as bird_ospf with context %} +{% import 'bgp.macros.j2' as bird_bgp with context %} +{% import 'routing.j2' as bird_routing with context %} + +{# + # vrf_table: Return the BIRD routing table name for a VRF address family. + #} +{% macro vrf_table(vname,af) -%} +vrf_{{ vname }}_{{ af }} +{%- endmacro %} + +{# + # rt_value: Return a BIRD extended community route-target tuple from a netlab RT string. + #} +{% macro rt_value(rt) -%} +(rt, {{ rt.split(':')[0] }}, {{ rt.split(':')[1] }}) +{%- endmacro %} + +{# + # tag_vrf_route: Tag an imported VRF route with all export route targets configured on the VRF. + #} +{% macro tag_vrf_route(vdata) %} + netlab_vrf_rt.empty; +{% for rt in vdata.export|default([]) %} + netlab_vrf_rt.add({{ rt_value(rt) }}); +{% endfor %} + accept; +{% endmacro -%} + +attribute eclist netlab_vrf_rt; +{% for vname,vdata in vrfs|default({})|dictsort %} + # -# Placeholder: BIRD daemon config for VRFs +# VRF {{ vname }} # +{% for _af in ['ipv4','ipv6'] if _af in vdata.af %} +{{ _af }} table {{ vrf_table(vname,_af) }}; +{% endfor %} + +{% for _af in ['ipv4','ipv6'] if _af in vdata.af %} +protocol direct direct_vrf_{{ vname }}_{{ _af }} { + vrf "{{ vname }}"; + {{ _af }} { + table {{ vrf_table(vname,_af) }}; + import filter { +{{ tag_vrf_route(vdata) }} + }; + }; +} + +protocol kernel kernel_vrf_{{ vname }}_{{ _af }} { + vrf "{{ vname }}"; + kernel table {{ vdata.vrfidx }}; + learn; + {{ _af }} { + table {{ vrf_table(vname,_af) }}; + export all; + import filter { +{{ tag_vrf_route(vdata) }} + }; + }; +} +{% endfor %} +{% for sr_af in ['ipv4','ipv6'] %} +{% for sr_data in routing.static|default([]) if sr_data.vrf|default('') == vname and 'vrf' not in sr_data.nexthop and sr_af in sr_data %} +{% if loop.first %} + +protocol static static_vrf_{{ vname }}_{{ sr_af }} { + {{ sr_af }} { + table {{ vrf_table(vname,sr_af) }}; + import filter { +{{ tag_vrf_route(vdata) }} + }; + }; + check link; +{% endif %} + {{ bird_routing.config_sr(sr_data,sr_af) }} +{% if loop.last %} +} +{% endif %} +{% endfor %} +{% endfor %} +{% for dst_name,dst_data in vrfs|default({})|dictsort if dst_name != vname %} +{% for sr_af in ['ipv4','ipv6'] %} +{% for sr_data in routing.static|default([]) if sr_data.nexthop.vrf|default(None) == vname and sr_data.vrf|default(None) == dst_name and sr_af in sr_data %} +{% if loop.first %} + +protocol static static_vrf_leak_{{ vname }}_{{ dst_name }}_{{ sr_af }} { + {{ sr_af }} { + table {{ vrf_table(vname,sr_af) }}; + import filter { +{{ tag_vrf_route(dst_data) }} + }; + }; + check link; +{% endif %} + {{ bird_routing.config_sr(sr_data,sr_af) }} +{% if loop.last %} +} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} + +{# VRF OSPF configuration #} +{% if vdata.ospf is defined %} +{{ bird_ospf.ospf_config(vdata.ospf,'vrf_' + vname,vname,vdata.ospf.interfaces|default([]),vdata) }} +{% endif %} + +{# VRF BGP configuration #} +{% if vdata.bgp is defined %} +{{ bird_bgp.bgp_advertise_list('bgp_advertise_vrf_' + vname,vdata.bgp.advertise) }} +{{ bird_bgp.bgp_prefixes_function('bgp_prefixes_vrf_' + vname,vdata.bgp,'bgp_advertise_vrf_' + vname,vdata.bgp.import|default(['connected','ospf']),False) }} +{{ bird_bgp.bgp_export_filters('bgp_export_vrf_' + vname + '_','bgp_prefixes_vrf_' + vname) }} +{% for n in vdata.bgp.neighbors|default([]) %} +{{ bird_bgp.bgp_session(n,bgp,vname,vdata,netlab_interfaces) }} +{% endfor %} +{% endif %} +{% endfor %} + +{# Route leaking between VRF tables based on transformed import/export route targets #} +{% for vname,vdata in vrfs|default({})|dictsort %} +filter netlab_vrf_{{ vname }}_import { +{% if vdata.import|default([]) %} + if netlab_vrf_rt ~ [ {% for rt in vdata.import %}{{ rt_value(rt) }}{% if not loop.last %}, {% endif %}{% endfor %} ] then accept; +{% endif %} + reject; +} +{% endfor %} +{% for src_name,src_data in vrfs|default({})|dictsort %} +{% for dst_name,dst_data in vrfs|default({})|dictsort if src_data.vrfidx < dst_data.vrfidx %} +{% for _af in ['ipv4','ipv6'] if _af in src_data.af and _af in dst_data.af %} + +protocol pipe pipe_vrf_{{ src_name }}_{{ dst_name }}_{{ _af }} { + table {{ vrf_table(src_name,_af) }}; + peer table {{ vrf_table(dst_name,_af) }}; + import filter netlab_vrf_{{ src_name }}_import; + export filter netlab_vrf_{{ dst_name }}_import; +} +{% endfor %} +{% endfor %} +{% endfor %}