Skip to content
1 change: 1 addition & 0 deletions docs/module/bgp.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ _netlab_ BGP configuration module supports these features:
Even more BGP features are implemented in the following plugins:

* [bgp.session](plugin-bgp-session): implements numerous BGP session features, including session protection and AS-path manipulation.
* [bgp.local_role](plugin-bgp-local-role): implements [RFC 9234](https://www.rfc-editor.org/rfc/rfc9234.html) BGP Roles on EBGP sessions (route-leak prevention).
* [bgp.policy](plugin-bgp-policy): implements simple BGP routing policies, including weights, local preference, and MED.
* [ebgp.multihop](plugin-ebgp-multihop): implements multihop EBGP sessions.
* [bgp.domain](plugin-bgp-domain): allows you to build topologies that reuse the same BGP ASN in different network parts.
Expand Down
1 change: 1 addition & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

plugins/bgp.domain.md
plugins/bgp.session.md
plugins/bgp.local_role.md
plugins/bgp.policy.md
plugins/bonding.md
plugins/ebgp.multihop.md
Expand Down
78 changes: 78 additions & 0 deletions docs/plugins/bgp.local_role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
(plugin-bgp-local-role)=
# BGP Local Role Plugin

The **bgp.local_role** plugin configures [RFC 9234](https://www.rfc-editor.org/rfc/rfc9234.html) BGP Roles on EBGP sessions. BGP Roles negotiate the peering relationship in the BGP OPEN message and enable automatic Only-to-Customer (OTC) handling for route-leak prevention.

```eval_rst
.. contents:: Table of Contents
:depth: 2
:local:
:backlinks: none
```

## Supported BGP Attributes

* **bgp.local_role** -- the local BGP role for an EBGP session. Valid values: **provider**, **customer**, **peer**, **rs-server**, **rs-client**.
* **bgp.local_role_strict** -- when set to _true_, the BGP session is established only if the remote router also advertises a compatible BGP Role capability (RFC 9234 strict mode).

BGP local role attributes can be specified at the global, node, link, or interface level:

| BGP local role attribute | Global | Node | Link | Interface |
|--------------------------|:------:|:----:|:----:|:---------:|
| local_role | ✅ | ✅ | ✅ | ✅ |
| local_role_strict | ✅ | ✅ | ✅ | ✅ |

The plugin applies these attributes to **EBGP** neighbors only. Using **bgp.local_role** on IBGP sessions results in a configuration error.

## Role Pairing

When both routers implement RFC 9234, the local role on one router must match the expected remote role on the other:

| Local role | Remote role |
|------------|-------------|
| provider | customer |
| customer | provider |
| peer | peer |
| rs-server | rs-client |
| rs-client | rs-server |

## Platform Support

(bgp-local-role-platforms)=
| Operating system | BGP Roles | Strict mode |
| ---------------- | :-------: | :---------: |
| FRR | ✅ | ✅ |
| BIRD | ✅ | ✅ |

FRR implements BGP Roles starting with release 8.4. BIRD implements them starting with release 2.0.11.

On BIRD, local roles are rendered into the BGP module configuration file (`daemons/bird/bgp.j2`); the plugin does not deploy a separate configuration script.

This plugin is independent of the **[bgp.session](bgp.session.md)** plugin. You can use both plugins in the same lab; list **bgp.session** before **bgp.local_role** if you use route server session features:

```
plugin: [ bgp.session, bgp.local_role ]
```

## Example

```yaml
plugin: [ bgp.local_role ]
module: [ bgp ]

nodes: [ isp, customer, peer ]

links:
- isp:
bgp.local_role: provider
customer:
bgp.local_role: customer
- isp:
bgp.local_role: peer
peer:
bgp.local_role: peer
```

## Test Topology

Integration test cases are in the `tests/integration/bgp.local_role` directory. A sample topology file is in `tests/topology/input/bgp-local-role.yml`.
1 change: 1 addition & 0 deletions netsim/daemons/bird.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ features:
remove_private_as: true
rs: true
rs_client: true
local_role: true
timers: true
ospf:
import: [ bgp, connected, static ]
Expand Down
6 changes: 6 additions & 0 deletions netsim/daemons/bird/bgp.j2
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ protocol bgp bgp_{{ n.name }}_{{ af }} {
{% if n.rs_client|default(False) %}
enforce first as off;
{% endif %}
{% if n.local_role is defined %}
local role {{ n.local_role|replace('-', '_') }};
{% if n.local_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) %}
Expand Down
1 change: 1 addition & 0 deletions netsim/devices/frr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ features:
remove_private_as: true
rs: true
rs_client: true
local_role: true
timers: true
_default_locpref: true
aggregate: true
Expand Down
1 change: 1 addition & 0 deletions netsim/devices/none.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ features:
local_as: True
vrf_local_as: True
local_as_ibgp: True
local_role: true
ipv6_lla: True
rfc8950: True
bandwidth: True
Expand Down
34 changes: 34 additions & 0 deletions netsim/extra/bgp.local_role/defaults.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# bgp.local_role default settings -- RFC 9234 BGP Roles
#
---
bgp:
attributes:
local_role:
attr: [ local_role, local_role_strict ]
global:
local_role:
_description: |
Local BGP Role for the EBGP session (RFC 9234). Negotiated in the BGP OPEN
message to prevent route leaks; enables automatic Only-to-Customer handling.
type: str
valid_values: [ provider, customer, peer, rs-server, rs-client ]
local_role_strict:
_description: |
Require the remote router to advertise a compatible BGP Role capability
(RFC 9234 strict mode); otherwise the session is not established.
type: bool
node:
local_role:
copy: global
local_role_strict:
copy: global
link:
local_role:
copy: global
local_role_strict:
copy: global
interface:
local_role:
copy: global
local_role_strict:
copy: global
48 changes: 48 additions & 0 deletions netsim/extra/bgp.local_role/frr.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
#
{% macro ebgp_local_role(n,af) -%}
{% set peer = n[af] if n[af] is string else n.local_if|default('?') %}
neighbor {{ peer }} local-role {{ n.local_role }}{% if n.local_role_strict|default(False) %} strict-mode{% endif +%}
{%- endmacro %}
{#

#}
set -e
cat >/tmp/config <<CONFIG
!
router bgp {{ bgp.as }}
{% for n in bgp.neighbors %}
{% for af in ['ipv4','ipv6'] if n[af] is defined %}
{% if n.local_role is defined %}
{{ ebgp_local_role(n,af) }}
{% endif %}
{% endfor %}
{% endfor %}

{% if vrfs is defined %}
{% for vname,vdata in vrfs.items() if vdata.bgp is defined and vdata.bgp.neighbors is defined %}
router bgp {{ bgp.as }} vrf {{ vname }}
{% for n in vdata.bgp.neighbors %}
{% for af in ['ipv4','ipv6'] if n[af] is defined %}
{% if n.local_role is defined %}
{{ ebgp_local_role(n,af) }}
{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
{% endif %}
CONFIG
#
# Change BGP configuration
#
vtysh -f /tmp/config
vtysh -c 'write'
#
# Clear BGP sessions
#
vtysh -c 'clear bgp *'
{% if vrfs is defined %}
{% for vname,vdata in vrfs.items() if vdata.bgp is defined and vdata.bgp.neighbors is defined %}
vtysh -c 'clear bgp vrf {{ vname }} *'
{% endfor %}
{% endif %}
103 changes: 103 additions & 0 deletions netsim/extra/bgp.local_role/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import typing

from box import Box

from netsim import api, modules
from netsim.utils import log
from netsim.utils import routing as _bgp

_config_name = "bgp.local_role"
_requires = ["bgp"]
_execute_after = ["bgp.session"]

_ATTR_LIST = ["local_role", "local_role_strict"]


def _use_plugin_template(node: Box) -> bool:
"""Return False for the BIRD daemon (roles are rendered in daemons/bird/bgp.j2)."""
return not (node.get("_daemon") and node.device == "bird")


def _check_strict_without_role(
ndata: Box, topology: Box, intf: typing.Optional[Box] = None) -> bool:
"""Reject bgp.local_role_strict without bgp.local_role. Return True if the check passed."""
strict = modules.get_effective_module_attribute(
path="bgp.local_role_strict", intf=intf, node=ndata, topology=topology)
role = modules.get_effective_module_attribute(
path="bgp.local_role", intf=intf, node=ndata, topology=topology)
if strict and not role:
where = f"node {ndata.name}"
if intf is not None:
where += f" interface {intf.name}"
log.error(
f"Cannot use bgp.local_role_strict without bgp.local_role ({where})",
category=log.IncorrectValue,
module=_config_name,
)
return False
return True


def _check_ibgp_local_role(ndata: Box, topology: Box, intf: typing.Optional[Box] = None) -> None:
"""Report an error if RFC 9234 role attributes are set on an IBGP session."""
for attr in _ATTR_LIST:
if not modules.get_effective_module_attribute(
path=f"bgp.{attr}", intf=intf, node=ndata, topology=topology):
continue
where = f"node {ndata.name}"
if intf is not None:
where += f" interface {intf.name}"
log.error(
f"Cannot use bgp.{attr} on IBGP session ({where})",
category=log.IncorrectValue,
module=_config_name,
)
return


def apply_neighbor_attributes(node: Box, ngb: Box, intf: Box, topology: Box) -> bool:
"""Copy bgp.local_role* interface attributes to an EBGP neighbor.

Returns:
True if at least one attribute was applied to the neighbor.
"""
if not _check_strict_without_role(node, topology, intf):
return False

values: dict[str, typing.Any] = {}
for attr in _ATTR_LIST:
attr_value = modules.get_effective_module_attribute(
path=f"bgp.{attr}", intf=intf, node=node)
if attr_value:
values[attr] = attr_value

if not values:
return False

if not _bgp.check_device_attribute_support(
"local_role", node, ngb, topology, _config_name):
return False

for attr, attr_value in values.items():
ngb[attr] = attr_value

if _use_plugin_template(node):
api.node_config(node, _config_name)
_bgp.clear_bgp_session(node, ngb)

return True


def post_transform(topology: Box) -> None:
"""Apply RFC 9234 BGP role attributes to EBGP neighbors and reject IBGP usage."""
for ndata in topology.nodes.values():
if "bgp" not in ndata.get("module", []):
continue

_bgp.cleanup_neighbor_attributes(ndata, topology, _ATTR_LIST)

for intf, ngb in _bgp.intf_neighbors(ndata, select=["ibgp", "ebgp", "localas_ibgp"]):
if ngb.type == "ebgp":
apply_neighbor_attributes(ndata, ngb, intf, topology)
else:
_check_ibgp_local_role(ndata, topology, intf)
Loading