diff --git a/docs/conf.py b/docs/conf.py index 6e5890c76..0da05da4c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,6 +47,7 @@ "html_admonition", "replacements", "smartquotes", + "strikethrough", "tasklist", "attrs_block" ] diff --git a/docs/plugins/bgp.session.md b/docs/plugins/bgp.session.md index 6e770768e..d2a0de7b9 100644 --- a/docs/plugins/bgp.session.md +++ b/docs/plugins/bgp.session.md @@ -1,6 +1,10 @@ # BGP Sessions Plugin -The **bgp.session** plugin (initially contributed by Stefano Sasso) implements several BGP session nerd knobs, including: +The **bgp.session** plugin (initially contributed by Stefano Sasso) implements numerous BGP session ~~nerd knobs~~ attributes, from BGP session protection to AS path manipulation. + +## Supported BGP Attributes + +The plugin adds the following BGP session attributes: * **bgp.allowas_in** is an attribute that controls whether a BGP router accepts one or more copies of its own AS number in incoming update. It takes an integer value between 1 and 10. A *true* value sets it to 1. * **bgp.as_override** is a boolean attribute that controls whether a BGP router replaces peer AS with its own AS. @@ -28,6 +32,8 @@ BGP session attributes can be specified on global, node, link, or interface (nod | tcp_ao | ✅ | ✅ | ✅ | ✅ | | timers | ✅ | ✅ | ✅ | ✅ | +## Platform Support + The plugin implements generic BGP session features for the following platforms: | Operating system | password | default
originate | GTSM | BGP
timers | TCP-AO | BFD | @@ -62,9 +68,47 @@ The plugin implements AS-path-mangling nerd knobs for the following platforms: | Nokia SR OS | ✅ | ✅ | | VyOS | ✅ | ✅ | +## Applying BGP Session Attributes to IBGP Sessions + +The plugin applies BGP session attributes to EBGP sessions -- either all EBGP sessions a node has if the attribute has been specified on a global- or node level, or all EBGP sessions on a link where the BGP session attribute has been specified. + +If you want to apply BGP session attributes to IBGP sessions, you have to set the **bgp.session.apply** global/node parameter -- a list or a dictionary of BGP neighbor types to which the BGP session attributes are applied. The default behavior is equivalent to `bgp.session.apply: [ ebgp ]`; if you want to apply the session attributes to IBGP and EBGP sessions, specify `bgp.session.apply: [ ibgp, ebgp ]` as a global or node attribute. + +IBGP sessions are not associated with links/interfaces and therefore cannot inherit the BGP session parameters specified on links or interfaces -- only the parameters specified on global- or node level are applied to IBGP sessions. For example, to use MD5 password `secret` on all IBGP and EBGP sessions in your lab, use the following global settings: + +``` +bgp.session.apply: [ ibgp, ebgp ] +bgp.password: secret +``` + +If you don't want to apply all node-level BGP session parameters to IBGP sessions, you could use the dictionary format of **bgp.session.apply** parameter: the BGP neighbor types are the dictionary keys, the values are lists of parameters that you want to apply to IBGP and EBGP sessions (missing value means _apply all parameters_). + +For example, the node settings from the next example cause the BGP password to be applied to IBGP and EBGP sessions, while all other BGP session parameters (including the default route origination) are applied only to EBGP sessions: + +``` +bgp.password: secret + +nodes: + core: + bgp.default_originate: True + bgp.session.apply: + ibgp: [ password ] + ebgp: +``` + +The following example is even more specific: default route origination and a few AS-related session parameters (but not MD5 password) are applied to EBGP sessions, while the MD5 passwords are applied only to IBGP sessions: + +``` +bgp.password: secret +bgp.session.apply: + ibgp: [ password ] + ebgp: [ default_originate, allowas_in, as_override ] +``` ## Test Topology +The following test topology illustrates a subset of plugin capabilities. You might also want to explore the topologies used in [ipSpace.net BGP labs](https://github.com/ipspace/bgplab) and the [BGP session plugin test topology](https://github.com/ipspace/netlab/blob/dev/tests/topology/input/ebgp.utils.yml). + ``` --- defaults: diff --git a/netsim/extra/bgp.session/defaults.yml b/netsim/extra/bgp.session/defaults.yml index fe7d516ae..3993705a4 100644 --- a/netsim/extra/bgp.session/defaults.yml +++ b/netsim/extra/bgp.session/defaults.yml @@ -106,6 +106,20 @@ bgp: min_value: 3 max_value: 3600 password: str + session: + apply: + _list_to_dict: True + ibgp: + type: list + create_empty: False + _alt_types: [ NoneType ] + true_value: [ '*' ] + ebgp: + type: list + create_empty: False + _alt_types: [ NoneType ] + true_value: [ '*' ] + node: bfd: bool default_originate: bool @@ -116,6 +130,8 @@ bgp: timers: copy: global password: str + session: + copy: global interface: allowas_in: type: int diff --git a/netsim/extra/bgp.session/plugin.py b/netsim/extra/bgp.session/plugin.py index 6327edd4b..a20ec42ad 100644 --- a/netsim/extra/bgp.session/plugin.py +++ b/netsim/extra/bgp.session/plugin.py @@ -1,3 +1,4 @@ +import typing from box import Box from netsim.utils import log,bgp as _bgp from netsim import api @@ -18,7 +19,7 @@ def pre_link_transform(topology: Box) -> None: check_device_attribute_support -- using device BGP features, check whether the device supports the attribute applied to a BGP neighbor ''' -def check_device_attribute_support(attr: str, ndata: Box, neigh: Box, topology: Box) -> None: +def check_device_attribute_support(attr: str, ndata: Box, neigh: Box, topology: Box) -> bool: global _config_name features = devices.get_device_features(ndata,topology.defaults) @@ -28,15 +29,19 @@ def check_device_attribute_support(attr: str, ndata: Box, neigh: Box, topology: f'Attribute {attr} used on BGP neighbor {neigh.name} is not supported by node {ndata.name} (device {ndata.device})', log.IncorrectValue, _config_name) + return False if not isinstance(enabled,list): - return + return True if not topology.provider in enabled: log.error( f'Node {ndata.name} (device {ndata.device}) does not support BGP attribute {attr} when running with {topology.provider} provider', log.IncorrectValue, _config_name) + return False + + return True ''' Remove session attributes with local significance from BGP neighbors @@ -47,23 +52,59 @@ def cleanup_neighbor_attributes(ndata: Box, topology: Box) -> None: for attr in topology.defaults.bgp.attributes.ebgp_utils.local: ngb.pop(attr,None) +''' +Get a list of attributes to apply to IBGP or EBGP sessions +''' +def get_attribute_list(apply_list: typing.Any, topology: Box) -> list: + if apply_list is None or apply_list is True or (isinstance(apply_list,list) and '*' in apply_list): + return topology.defaults.bgp.attributes.ebgp_utils.attr + + return list(apply_list) + +''' +Apply attributes supported by bgp.session plugin to a single neighbor +Returns False is some of the attributes are not supported +''' +def apply_neighbor_attributes(node: Box, ngb: Box, intf: typing.Optional[Box], apply_list: list, topology: Box) -> bool: + global _config_name + + OK = True + for attr in apply_list: + attr_value = None if intf is None else \ + intf.get('bgp',{}).get(attr,None) # Get attribute value from interface if specified + attr_value = attr_value or node.bgp.get(attr,None) # ... and try node attribute value if there's no interface value + if not attr_value: # Attribute not defined in node or interface, move on + continue + + # Check that the node(device) supports the desired attribute + OK = OK and check_device_attribute_support(attr,node,ngb,topology) + ngb[attr] = attr_value # Set neighbor attribute from interface/node value + api.node_config(node,_config_name) # And remember that we have to do extra configuration + + return OK + ''' Copy local session attributes to BGP neighbors because we need them there in the configuration templates ''' def copy_local_attributes(ndata: Box, topology: Box) -> None: - global _config_name + apply = ndata.get('bgp.session.apply',{ 'ebgp': None }) # By default we apply all session attributes only to EBGP neighbors + + OK = True + if 'ibgp' in apply: # Do we apply session attributes to IBGP neighbors? + apply_list = get_attribute_list(apply.ibgp,topology) # Get attributes to apply to IBGP neighbors + for ngb in _bgp.neighbors(ndata,select=['ibgp']): # Iterate over all neighbors using node attributes + OK = OK and apply_neighbor_attributes(ndata,ngb,None,apply_list,topology) + + if not OK: # Skip the second step if we encountered unsupported device + return # ... doesn't make sense to throw the same error(s) twice - # Iterate over all ebgp.utils link/interface attributes - for (intf,ngb) in _bgp.intf_neighbors(ndata): - for attr in topology.defaults.bgp.attributes.ebgp_utils.attr: - attr_value = intf.get('bgp',{}).get(attr,None) or ndata.bgp.get(attr,None) - if not attr_value: # Attribute not defined in node or interface, move on - continue + if 'ebgp' in apply: # Do we apply session attributes to EBGP neighbors? + apply_list = get_attribute_list(apply.ebgp,topology) # Get attributes to apply to IBGP neighbors - check_device_attribute_support(attr,ndata,ngb,topology) - ngb[attr] = attr_value # Set neighbor attribute from interface/node value - api.node_config(ndata,_config_name) # And remember that we have to do extra configuration + # Iterate over neighbors that have associated interfaces (all EBGP neighbors are supposed to be directly connected) + for (intf,ngb) in _bgp.intf_neighbors(ndata,select=['ebgp']): + apply_neighbor_attributes(ndata,ngb,intf,apply_list,topology) ''' For platforms that collect tcp_ao secrets in global management profiles diff --git a/netsim/utils/bgp.py b/netsim/utils/bgp.py index 05b5701d4..ef985fc23 100644 --- a/netsim/utils/bgp.py +++ b/netsim/utils/bgp.py @@ -7,28 +7,30 @@ # Return all global and optionaly VRF neighbors # -def neighbors(node: Box, vrf: bool = True) -> typing.Generator: +def neighbors(node: Box, vrf: bool = True, select: list = ['ibgp','ebgp']) -> typing.Generator: for ngb in node.get('bgp.neighbors',[]): - yield ngb + if ngb.type in select: + yield ngb if not vrf: return for vdata in node.get('vrfs',{}).values(): for ngb in vdata.get('bgp.neighbors',[]): - yield ngb + if ngb.type in select: + yield ngb # Return all BGP neighbors associated with interfaces (usually EBGP neighbors) # -def intf_neighbors(node: Box, vrf: bool = True) -> typing.Generator: +def intf_neighbors(node: Box, vrf: bool = True, select: list = ['ibgp','ebgp']) -> typing.Generator: for intf in node.interfaces: if 'vrf' in intf: if not vrf: continue for ngb in node.vrfs[intf.vrf].get('bgp.neighbors',[]): - if ngb.get('ifindex',None) == intf.ifindex: + if ngb.get('ifindex',None) == intf.ifindex and ngb.type in select: yield (intf,ngb) else: for ngb in node.get('bgp.neighbors',[]): - if ngb.get('ifindex',None) == intf.ifindex: + if ngb.get('ifindex',None) == intf.ifindex and ngb.type in select: yield (intf,ngb) diff --git a/tests/topology/expected/ebgp.utils.yml b/tests/topology/expected/ebgp.utils.yml index 54f06492d..65f107e69 100644 --- a/tests/topology/expected/ebgp.utils.yml +++ b/tests/topology/expected/ebgp.utils.yml @@ -34,6 +34,7 @@ groups: as65001: members: - r1 + - rr as65002: members: - r2 @@ -78,25 +79,39 @@ links: ipv4: 10.1.0.4/30 role: external type: p2p +- interfaces: + - ifindex: 3 + ifname: Ethernet3 + ipv4: 10.1.0.9/30 + node: r1 + - ifindex: 1 + ifname: Ethernet1 + ipv4: 10.1.0.10/30 + node: rr + linkindex: 3 + node_count: 2 + prefix: + ipv4: 10.1.0.8/30 + type: p2p - bgp: password: InVrf interfaces: - bgp: default_originate: true - ifindex: 3 - ifname: Ethernet3 - ipv4: 10.1.0.9/30 + ifindex: 4 + ifname: Ethernet4 + ipv4: 10.1.0.13/30 node: r1 - bgp: allowas_in: 1 ifindex: 2 ifname: Ethernet2 - ipv4: 10.1.0.10/30 + ipv4: 10.1.0.14/30 node: r2 - linkindex: 3 + linkindex: 4 node_count: 2 prefix: - ipv4: 10.1.0.8/30 + ipv4: 10.1.0.12/30 role: external type: p2p vrf: red @@ -121,6 +136,14 @@ nodes: default_originate: true ipv4: true neighbors: + - activate: + ipv4: true + as: 65001 + ipv4: 10.0.0.4 + name: rr + password: Secret + rr: true + type: ibgp - activate: ipv4: true as: 65002 @@ -142,6 +165,11 @@ nodes: next_hop_self: true password: Secret router_id: 10.0.0.1 + session: + apply: + ebgp: null + ibgp: + - password box: arista/veos config: - bgp.session @@ -175,20 +203,30 @@ nodes: node: r3 role: external type: p2p - - bgp: - default_originate: true - password: InVrf - ifindex: 3 + - ifindex: 3 ifname: Ethernet3 ipv4: 10.1.0.9/30 linkindex: 3 + name: r1 -> rr + neighbors: + - ifname: Ethernet1 + ipv4: 10.1.0.10/30 + node: rr + type: p2p + - bgp: + default_originate: true + password: InVrf + ifindex: 4 + ifname: Ethernet4 + ipv4: 10.1.0.13/30 + linkindex: 4 name: r1 -> r2 neighbors: - bgp: allowas_in: 1 password: InVrf ifname: Ethernet2 - ipv4: 10.1.0.10/30 + ipv4: 10.1.0.14/30 node: r2 vrf: red role: external @@ -214,8 +252,8 @@ nodes: neighbors: - as: 65002 default_originate: true - ifindex: 3 - ipv4: 10.1.0.10 + ifindex: 4 + ipv4: 10.1.0.14 name: r2 password: InVrf type: ebgp @@ -285,15 +323,15 @@ nodes: password: InVrf ifindex: 2 ifname: Ethernet2 - ipv4: 10.1.0.10/30 - linkindex: 3 + ipv4: 10.1.0.14/30 + linkindex: 4 name: r2 -> r1 neighbors: - bgp: default_originate: true password: InVrf - ifname: Ethernet3 - ipv4: 10.1.0.9/30 + ifname: Ethernet4 + ipv4: 10.1.0.13/30 node: r1 vrf: red role: external @@ -320,7 +358,7 @@ nodes: - allowas_in: 1 as: 65001 ifindex: 2 - ipv4: 10.1.0.9 + ipv4: 10.1.0.13 name: r1 password: InVrf type: ebgp @@ -401,6 +439,63 @@ nodes: name: r3 vrf: as: 65000 + rr: + af: + ipv4: true + bgp: + advertise_loopback: true + as: 65001 + community: + ebgp: + - standard + ibgp: + - standard + - extended + ipv4: true + neighbors: + - activate: + ipv4: true + as: 65001 + ipv4: 10.0.0.1 + name: r1 + password: Secret + type: ibgp + next_hop_self: true + password: Secret + router_id: 10.0.0.4 + rr: true + rr_cluster_id: 10.0.0.4 + session: + apply: + ibgp: + - '*' + box: arista/veos + config: + - bgp.session + device: eos + id: 4 + interfaces: + - ifindex: 1 + ifname: Ethernet1 + ipv4: 10.1.0.10/30 + linkindex: 3 + name: rr -> r1 + neighbors: + - ifname: Ethernet3 + ipv4: 10.1.0.9/30 + node: r1 + type: p2p + loopback: + ipv4: 10.0.0.4/32 + mgmt: + ifname: Management1 + ipv4: 192.168.121.104 + mac: 08:4f:a9:00:00:04 + module: + - bgp + name: rr + vrf: + as: 65000 plugin: - bgp.session - ebgp.multihop diff --git a/tests/topology/input/ebgp.utils.yml b/tests/topology/input/ebgp.utils.yml index db1dcbac4..be0046a25 100644 --- a/tests/topology/input/ebgp.utils.yml +++ b/tests/topology/input/ebgp.utils.yml @@ -26,10 +26,17 @@ nodes: r1: bgp.as: 65001 bgp.default_originate: True + bgp.session.apply: + ibgp: [ password ] + ebgp: r2: bgp.as: 65002 r3: bgp.as: 65003 + rr: + bgp.as: 65001 + bgp.rr: true + bgp.session.apply: [ ibgp ] links: - r1: @@ -38,3 +45,4 @@ links: r3: bgp.allowas_in: True bgp.password: SomethingElse +- r1-rr