summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/topotests/bgp_bmp/__init__.py0
-rw-r--r--tests/topotests/bgp_bmp/r1/bgpd.conf22
-rw-r--r--tests/topotests/bgp_bmp/r1/zebra.conf7
-rw-r--r--tests/topotests/bgp_bmp/r2/bgpd.conf19
-rw-r--r--tests/topotests/bgp_bmp/r2/zebra.conf8
-rw-r--r--tests/topotests/bgp_bmp/test_bgp_bmp.py246
-rw-r--r--tests/topotests/lib/bmp_collector/bgp/__init__.py0
-rw-r--r--tests/topotests/lib/bmp_collector/bgp/open/__init__.py34
-rw-r--r--tests/topotests/lib/bmp_collector/bgp/update/__init__.py54
-rw-r--r--tests/topotests/lib/bmp_collector/bgp/update/af.py53
-rw-r--r--tests/topotests/lib/bmp_collector/bgp/update/nlri.py140
-rw-r--r--tests/topotests/lib/bmp_collector/bgp/update/path_attributes.py304
-rw-r--r--tests/topotests/lib/bmp_collector/bgp/update/rd.py59
-rw-r--r--tests/topotests/lib/bmp_collector/bmp.py420
-rwxr-xr-xtests/topotests/lib/bmp_collector/bmpserver45
-rw-r--r--tests/topotests/lib/topogen.py43
16 files changed, 1454 insertions, 0 deletions
diff --git a/tests/topotests/bgp_bmp/__init__.py b/tests/topotests/bgp_bmp/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/topotests/bgp_bmp/__init__.py
diff --git a/tests/topotests/bgp_bmp/r1/bgpd.conf b/tests/topotests/bgp_bmp/r1/bgpd.conf
new file mode 100644
index 0000000000..69acf6e750
--- /dev/null
+++ b/tests/topotests/bgp_bmp/r1/bgpd.conf
@@ -0,0 +1,22 @@
+router bgp 65501
+ bgp router-id 192.168.0.1
+ bgp log-neighbor-changes
+ no bgp ebgp-requires-policy
+ neighbor 192.168.0.2 remote-as 65502
+ neighbor 192:168::2 remote-as 65502
+!
+ bmp targets bmp1
+ bmp connect 192.0.178.10 port 1789 min-retry 100 max-retry 10000
+ exit
+!
+ address-family ipv4 unicast
+ neighbor 192.168.0.2 activate
+ neighbor 192.168.0.2 soft-reconfiguration inbound
+ no neighbor 192:168::2 activate
+ exit-address-family
+!
+ address-family ipv6 unicast
+ neighbor 192:168::2 activate
+ neighbor 192:168::2 soft-reconfiguration inbound
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_bmp/r1/zebra.conf b/tests/topotests/bgp_bmp/r1/zebra.conf
new file mode 100644
index 0000000000..6a25a6f4c2
--- /dev/null
+++ b/tests/topotests/bgp_bmp/r1/zebra.conf
@@ -0,0 +1,7 @@
+interface r1-eth0
+ ip address 192.0.178.1/24
+!
+interface r1-eth1
+ ip address 192.168.0.1/24
+ ipv6 address 192:168::1/64
+!
diff --git a/tests/topotests/bgp_bmp/r2/bgpd.conf b/tests/topotests/bgp_bmp/r2/bgpd.conf
new file mode 100644
index 0000000000..7c8255a175
--- /dev/null
+++ b/tests/topotests/bgp_bmp/r2/bgpd.conf
@@ -0,0 +1,19 @@
+router bgp 65502
+ bgp router-id 192.168.0.2
+ bgp log-neighbor-changes
+ no bgp ebgp-requires-policy
+ no bgp network import-check
+ neighbor 192.168.0.1 remote-as 65501
+ neighbor 192:168::1 remote-as 65501
+!
+ address-family ipv4 unicast
+ neighbor 192.168.0.1 activate
+ no neighbor 192:168::1 activate
+ redistribute connected
+ exit-address-family
+!
+ address-family ipv6 unicast
+ neighbor 192:168::1 activate
+ redistribute connected
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_bmp/r2/zebra.conf b/tests/topotests/bgp_bmp/r2/zebra.conf
new file mode 100644
index 0000000000..9d82bfe2df
--- /dev/null
+++ b/tests/topotests/bgp_bmp/r2/zebra.conf
@@ -0,0 +1,8 @@
+interface r2-eth0
+ ip address 192.168.0.2/24
+ ipv6 address 192:168::2/64
+!
+interface r2-eth1
+ ip address 172.31.0.2/24
+ ipv6 address 172:31::2/64
+!
diff --git a/tests/topotests/bgp_bmp/test_bgp_bmp.py b/tests/topotests/bgp_bmp/test_bgp_bmp.py
new file mode 100644
index 0000000000..65f191b33a
--- /dev/null
+++ b/tests/topotests/bgp_bmp/test_bgp_bmp.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+
+"""
+test_bgp_bmp.py: Test BGP BMP functionalities
+
+ +------+ +------+ +------+
+ | | | | | |
+ | BMP1 |------------| R1 |---------------| R2 |
+ | | | | | |
+ +------+ +------+ +------+
+
+Setup two routers R1 and R2 with one link configured with IPv4 and
+IPv6 addresses.
+Configure BGP in R1 and R2 to exchange prefixes from
+the latter to the first router.
+Setup a link between R1 and the BMP server, activate the BMP feature in R1
+and ensure the monitored BGP sessions logs are well present on the BMP server.
+"""
+
+from functools import partial
+from ipaddress import ip_network
+import json
+import os
+import platform
+import pytest
+import sys
+
+# Save the Current Working Directory to find configuration files.
+CWD = os.path.dirname(os.path.realpath(__file__))
+sys.path.append(os.path.join("../"))
+sys.path.append(os.path.join("../lib/"))
+
+# pylint: disable=C0413
+# Import topogen and topotest helpers
+from lib import topotest
+from lib.bgp import verify_bgp_convergence_from_running_config
+from lib.topogen import Topogen, TopoRouter, get_topogen
+from lib.topolog import logger
+
+pytestmark = [pytest.mark.bgpd]
+
+# remember the last sequence number of the logging messages
+SEQ = 0
+
+PRE_POLICY = "pre-policy"
+POST_POLICY = "post-policy"
+
+
+def build_topo(tgen):
+ tgen.add_router("r1")
+ tgen.add_router("r2")
+ tgen.add_bmp_server("bmp1", ip="192.0.178.10", defaultRoute="via 192.0.178.1")
+
+ switch = tgen.add_switch("s1")
+ switch.add_link(tgen.gears["r1"])
+ switch.add_link(tgen.gears["bmp1"])
+
+ tgen.add_link(tgen.gears["r1"], tgen.gears["r2"], "r1-eth1", "r2-eth0")
+
+
+def setup_module(mod):
+ tgen = Topogen(build_topo, mod.__name__)
+ tgen.start_topology()
+
+ for rname, router in tgen.routers().items():
+ router.load_config(
+ TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
+ )
+ router.load_config(
+ TopoRouter.RD_BGP,
+ os.path.join(CWD, "{}/bgpd.conf".format(rname)),
+ "-M bmp",
+ )
+
+ tgen.start_router()
+
+ logger.info("starting BMP servers")
+ for _, server in tgen.get_bmp_servers().items():
+ server.start()
+
+
+def teardown_module(_mod):
+ tgen = get_topogen()
+ tgen.stop_topology()
+
+
+def test_bgp_convergence():
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ result = verify_bgp_convergence_from_running_config(tgen, dut="r1")
+ assert result is True, "BGP is not converging"
+
+
+def get_bmp_messages():
+ """
+ Read the BMP logging messages.
+ """
+ messages = []
+ tgen = get_topogen()
+ text_output = tgen.gears["bmp1"].run("cat /var/log/bmp.log")
+
+ for m in text_output.splitlines():
+ # some output in the bash can break the message decoding
+ try:
+ messages.append(json.loads(m))
+ except Exception as e:
+ logger.warning(str(e) + " message: {}".format(str(m)))
+ continue
+
+ if not messages:
+ logger.error("Bad BMP log format, check your BMP server")
+
+ return messages
+
+
+def check_for_prefixes(expected_prefixes, bmp_log_type, post_policy):
+ """
+ Check for the presence of the given prefixes in the BMP server logs with
+ the given message type and the set policy.
+ """
+ global SEQ
+ # we care only about the new messages
+ messages = [
+ m for m in sorted(get_bmp_messages(), key=lambda d: d["seq"]) if m["seq"] > SEQ
+ ]
+
+ # get the list of pairs (prefix, policy, seq) for the given message type
+ prefixes = [
+ m["ip_prefix"]
+ for m in messages
+ if "ip_prefix" in m.keys()
+ and "bmp_log_type" in m.keys()
+ and m["bmp_log_type"] == bmp_log_type
+ and m["post_policy"] == post_policy
+ ]
+
+ # check for prefixes
+ for ep in expected_prefixes:
+ if ep not in prefixes:
+ msg = "The prefix {} is not present in the {} log messages."
+ logger.debug(msg.format(ep, bmp_log_type))
+ return False
+
+ SEQ = messages[-1]["seq"]
+ return True
+
+
+def set_bmp_policy(tgen, node, asn, target, safi, policy, vrf=None):
+ """
+ Configure the bmp policy.
+ """
+ vrf = " vrf {}" if vrf else ""
+ cmd = [
+ "con t\n",
+ "router bgp {}{}\n".format(asn, vrf),
+ "bmp targets {}\n".format(target),
+ "bmp monitor ipv4 {} {}\n".format(safi, policy),
+ "bmp monitor ipv6 {} {}\n".format(safi, policy),
+ "end\n",
+ ]
+ tgen.gears[node].vtysh_cmd("".join(cmd))
+
+
+def configure_prefixes(tgen, node, asn, safi, prefixes, vrf=None, update=True):
+ """
+ Configure the bgp prefixes.
+ """
+ withdraw = "no " if not update else ""
+ vrf = " vrf {}" if vrf else ""
+ for p in prefixes:
+ ip = ip_network(p)
+ cmd = [
+ "conf t\n",
+ "router bgp {}{}\n".format(asn, vrf),
+ "address-family ipv{} {}\n".format(ip.version, safi),
+ "{}network {}\n".format(withdraw, ip),
+ "exit-address-family\n",
+ ]
+ logger.debug("setting prefix: ipv{} {} {}".format(ip.version, safi, ip))
+ tgen.gears[node].vtysh_cmd("".join(cmd))
+
+
+def unicast_prefixes(policy):
+ """
+ Setup the BMP monitor policy, Add and withdraw ipv4/v6 prefixes.
+ Check if the previous actions are logged in the BMP server with the right
+ message type and the right policy.
+ """
+ tgen = get_topogen()
+ set_bmp_policy(tgen, "r1", 65501, "bmp1", "unicast", policy)
+
+ prefixes = ["172.31.0.15/32", "2111::1111/128"]
+ # add prefixes
+ configure_prefixes(tgen, "r2", 65502, "unicast", prefixes)
+
+ logger.info("checking for updated prefixes")
+ # check
+ test_func = partial(check_for_prefixes, prefixes, "update", policy == POST_POLICY)
+ success, _ = topotest.run_and_expect(test_func, True, wait=0.5)
+ assert success, "Checking the updated prefixes has been failed !."
+
+ # withdraw prefixes
+ configure_prefixes(tgen, "r2", 65502, "unicast", prefixes, update=False)
+ logger.info("checking for withdrawed prefxies")
+ # check
+ test_func = partial(check_for_prefixes, prefixes, "withdraw", policy == POST_POLICY)
+ success, _ = topotest.run_and_expect(test_func, True, wait=0.5)
+ assert success, "Checking the withdrawed prefixes has been failed !."
+
+
+def test_bmp_server_logging():
+ """
+ Assert the logging of the bmp server.
+ """
+
+ def check_for_log_file():
+ tgen = get_topogen()
+ output = tgen.gears["bmp1"].run("ls /var/log/")
+ if "bmp.log" not in output:
+ return False
+ return True
+
+ success, _ = topotest.run_and_expect(check_for_log_file, True, wait=0.5)
+ assert success, "The BMP server is not logging"
+
+
+def test_bmp_bgp_unicast():
+ """
+ Add/withdraw bgp unicast prefixes and check the bmp logs.
+ """
+ logger.info("*** Unicast prefixes pre-policy logging ***")
+ unicast_prefixes(PRE_POLICY)
+ logger.info("*** Unicast prefixes post-policy logging ***")
+ unicast_prefixes(POST_POLICY)
+
+
+if __name__ == "__main__":
+ args = ["-s"] + sys.argv[1:]
+ sys.exit(pytest.main(args))
diff --git a/tests/topotests/lib/bmp_collector/bgp/__init__.py b/tests/topotests/lib/bmp_collector/bgp/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bgp/__init__.py
diff --git a/tests/topotests/lib/bmp_collector/bgp/open/__init__.py b/tests/topotests/lib/bmp_collector/bgp/open/__init__.py
new file mode 100644
index 0000000000..6c814ee9aa
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bgp/open/__init__.py
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+import ipaddress
+import struct
+
+
+class BGPOpen:
+ UNPACK_STR = '!16sHBBHH4sB'
+
+ @classmethod
+ def dissect(cls, data):
+ (marker,
+ length,
+ open_type,
+ version,
+ my_as,
+ hold_time,
+ bgp_id,
+ optional_params_len) = struct.unpack_from(cls.UNPACK_STR, data)
+
+ data = data[struct.calcsize(cls.UNPACK_STR) + optional_params_len:]
+
+ # XXX: parse optional parameters
+
+ return data, {
+ 'version': version,
+ 'my_as': my_as,
+ 'hold_time': hold_time,
+ 'bgp_id': ipaddress.ip_address(bgp_id),
+ 'optional_params_len': optional_params_len,
+ }
diff --git a/tests/topotests/lib/bmp_collector/bgp/update/__init__.py b/tests/topotests/lib/bmp_collector/bgp/update/__init__.py
new file mode 100644
index 0000000000..d079b35113
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bgp/update/__init__.py
@@ -0,0 +1,54 @@
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+import ipaddress
+import struct
+
+from .nlri import NlriIPv4Unicast
+from .path_attributes import PathAttribute
+
+
+#------------------------------------------------------------------------------
+class BGPUpdate:
+ UNPACK_STR = '!16sHBH'
+ STATIC_SIZE = 23
+
+ @classmethod
+ def dissect(cls, data):
+ msg = {'bmp_log_type': 'update'}
+ common_size = struct.calcsize(cls.UNPACK_STR)
+ (marker,
+ length,
+ update_type,
+ withdrawn_routes_len) = struct.unpack_from(cls.UNPACK_STR, data)
+
+ # get withdrawn routes
+ withdrawn_routes = ''
+ if withdrawn_routes_len:
+ withdrawn_routes = NlriIPv4Unicast.parse(
+ data[common_size:common_size + withdrawn_routes_len]
+ )
+ msg['bmp_log_type'] = 'withdraw'
+ msg.update(withdrawn_routes)
+
+ # get path attributes
+ (total_path_attrs_len,) = struct.unpack_from(
+ '!H', data[common_size+withdrawn_routes_len:])
+
+ if total_path_attrs_len:
+ offset = cls.STATIC_SIZE + withdrawn_routes_len
+ path_attrs_data = data[offset:offset + total_path_attrs_len]
+ while path_attrs_data:
+ path_attrs_data, pattr = PathAttribute.dissect(path_attrs_data)
+ if pattr:
+ msg = {**msg, **pattr}
+
+ # get nlri
+ nlri_len = length - cls.STATIC_SIZE - withdrawn_routes_len - total_path_attrs_len
+ if nlri_len > 0:
+ nlri = NlriIPv4Unicast.parse(data[length - nlri_len:length])
+ msg.update(nlri)
+
+ return data[length:], msg
diff --git a/tests/topotests/lib/bmp_collector/bgp/update/af.py b/tests/topotests/lib/bmp_collector/bgp/update/af.py
new file mode 100644
index 0000000000..01af1ae2be
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bgp/update/af.py
@@ -0,0 +1,53 @@
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+
+# IANA Address Family Identifier
+AFI_IP = 1
+AFI_IP6 = 2
+AFI_L2VPN = 25
+
+# IANA Subsequent Address Family Idenitifier
+SAFI_UNICAST = 1
+SAFI_MULTICAST = 2
+SAFI_MPLS_LABEL = 4
+SAFI_EVPN = 70
+SAFI_MPLS_VPN = 128
+SAFI_IP_FLOWSPEC = 133
+SAFI_VPN_FLOWSPEC = 134
+
+
+#------------------------------------------------------------------------------
+class AddressFamily:
+ def __init__(self, afi, safi):
+ self.afi = afi
+ self.safi = safi
+
+ def __eq__(self, other):
+ if not isinstance(other, type(self)):
+ return False
+ return (self.afi, self.safi) == (other.afi, other.safi)
+
+ def __str__(self):
+ return f'afi: {self.afi}, safi: {self.safi}'
+
+ def __hash__(self):
+ return hash((self.afi, self.safi))
+
+
+#------------------------------------------------------------------------------
+class AF:
+ IPv4_UNICAST = AddressFamily(AFI_IP, SAFI_UNICAST)
+ IPv6_UNICAST = AddressFamily(AFI_IP6, SAFI_UNICAST)
+ IPv4_VPN = AddressFamily(AFI_IP, SAFI_MPLS_VPN)
+ IPv6_VPN = AddressFamily(AFI_IP6, SAFI_MPLS_VPN)
+ IPv4_MPLS = AddressFamily(AFI_IP, SAFI_MPLS_LABEL)
+ IPv6_MPLS = AddressFamily(AFI_IP6, SAFI_MPLS_LABEL)
+ IPv4_FLOWSPEC = AddressFamily(AFI_IP, SAFI_IP_FLOWSPEC)
+ IPv6_FLOWSPEC = AddressFamily(AFI_IP6, SAFI_IP_FLOWSPEC)
+ VPNv4_FLOWSPEC = AddressFamily(AFI_IP, SAFI_VPN_FLOWSPEC)
+ VPNv6_FLOWSPEC = AddressFamily(AFI_IP6, SAFI_VPN_FLOWSPEC)
+ L2EVPN = AddressFamily(AFI_L2VPN, SAFI_EVPN)
+ L2VPN_FLOWSPEC = AddressFamily(AFI_L2VPN, SAFI_VPN_FLOWSPEC)
diff --git a/tests/topotests/lib/bmp_collector/bgp/update/nlri.py b/tests/topotests/lib/bmp_collector/bgp/update/nlri.py
new file mode 100644
index 0000000000..c1720f126c
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bgp/update/nlri.py
@@ -0,0 +1,140 @@
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+import ipaddress
+import struct
+
+from .af import AddressFamily, AF
+from .rd import RouteDistinguisher
+
+
+def decode_label(label):
+ # from frr
+ # frr encode just one label
+ return (label[0] << 12) | (label[1] << 4) | (label[2] & 0xf0) >> 4
+
+def padding(databin, len_):
+ """
+ Assumption:
+ One nlri per update/withdraw message, so we can add
+ a padding to the prefix without worrying about its length
+ """
+ if len(databin) >= len_:
+ return databin
+ return databin + b'\0' * (len_ - len(databin))
+
+def dissect_nlri(nlri_data, afi, safi):
+ """
+ Exract nlri information based on the address family
+ """
+ addr_family = AddressFamily(afi, safi)
+ if addr_family == AF.IPv6_VPN:
+ return NlriIPv6Vpn.parse(nlri_data)
+ elif addr_family == AF.IPv4_VPN:
+ return NlriIPv4Vpn.parse(nlri_data)
+ elif addr_family == AF.IPv6_UNICAST:
+ return NlriIPv6Unicast.parse(nlri_data)
+
+ return {'ip_prefix': 'Unknown'}
+
+
+#------------------------------------------------------------------------------
+class NlriIPv4Unicast:
+
+ @staticmethod
+ def parse(data):
+ """parses prefixes from withdrawn_routes or nrli data"""
+ (prefix_len,) = struct.unpack_from('!B', data)
+ prefix = padding(data[1:], 4)
+
+ return {'ip_prefix': f'{ipaddress.IPv4Address(prefix)}/{prefix_len}'}
+
+
+#------------------------------------------------------------------------------
+class NlriIPv6Unicast:
+ @staticmethod
+ def parse(data):
+ """parses prefixes from withdrawn_routes or nrli data"""
+ (prefix_len,) = struct.unpack_from('!B', data)
+ prefix = padding(data[1:], 16)
+
+ return {'ip_prefix': f'{ipaddress.IPv6Address(prefix)}/{prefix_len}'}
+
+
+#------------------------------------------------------------------------------
+class NlriIPv4Vpn:
+ UNPACK_STR = '!B3s8s'
+
+ @classmethod
+ def parse(cls, data):
+ (bit_len, label, rd) = struct.unpack_from(cls.UNPACK_STR, data)
+ offset = struct.calcsize(cls.UNPACK_STR)
+
+ ipv4 = padding(data[offset:], 4)
+ # prefix_len = total_bits_len - label_bits_len - rd_bits_len
+ prefix_len = bit_len - 3*8 - 8*8
+ return {
+ 'label': decode_label(label),
+ 'rd': str(RouteDistinguisher(rd)),
+ 'ip_prefix': f'{ipaddress.IPv4Address(ipv4)}/{prefix_len}',
+ }
+
+
+#------------------------------------------------------------------------------
+class NlriIPv6Vpn:
+ UNPACK_STR = '!B3s8s'
+
+ @classmethod
+ def parse(cls, data):
+ # rfc 3107, 8227
+ (bit_len, label, rd) = struct.unpack_from(cls.UNPACK_STR, data)
+ offset = struct.calcsize(cls.UNPACK_STR)
+
+ ipv6 = padding(data[offset:], 16)
+ prefix_len = bit_len - 3*8 - 8*8
+ return {
+ 'label': decode_label(label),
+ 'rd': str(RouteDistinguisher(rd)),
+ 'ip_prefix': f'{ipaddress.IPv6Address(ipv6)}/{prefix_len}',
+ }
+
+
+#------------------------------------------------------------------------------
+class NlriIPv4Mpls:
+ pass
+
+
+#------------------------------------------------------------------------------
+class NlriIPv6Mpls:
+ pass
+
+
+#------------------------------------------------------------------------------
+class NlriIPv4FlowSpec:
+ pass
+
+
+#------------------------------------------------------------------------------
+class NlriIPv6FlowSpec:
+ pass
+
+
+#------------------------------------------------------------------------------
+class NlriVpn4FlowSpec:
+ pass
+
+
+#------------------------------------------------------------------------------
+class NlriVpn6FlowSpec:
+ pass
+
+
+#------------------------------------------------------------------------------
+class NlriL2EVPN:
+ pass
+
+#------------------------------------------------------------------------------
+class NlriL2VPNFlowSpec:
+ pass
diff --git a/tests/topotests/lib/bmp_collector/bgp/update/path_attributes.py b/tests/topotests/lib/bmp_collector/bgp/update/path_attributes.py
new file mode 100644
index 0000000000..6e82e9c170
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bgp/update/path_attributes.py
@@ -0,0 +1,304 @@
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+import struct
+import ipaddress
+
+from . import nlri as NLRI
+from .af import AddressFamily, AF
+from .rd import RouteDistinguisher
+
+
+PATH_ATTR_FLAG_OPTIONAL = 1 << 7
+PATH_ATTR_FLAG_TRANSITIVE = 1 << 6
+PATH_ATTR_FLAG_PARTIAL = 1 << 5
+PATH_ATTR_FLAG_EXTENDED_LENGTH = 1 << 4
+
+PATH_ATTR_TYPE_ORIGIN = 1
+PATH_ATTR_TYPE_AS_PATH = 2
+PATH_ATTR_TYPE_NEXT_HOP = 3
+PATH_ATTR_TYPE_MULTI_EXIT_DISC = 4
+PATH_ATTR_TYPE_LOCAL_PREF = 5
+PATH_ATTR_TYPE_ATOMIC_AGGREGATE = 6
+PATH_ATTR_TYPE_AGGREGATOR = 7
+PATH_ATTR_TYPE_COMMUNITIES = 8
+PATH_ATTR_TYPE_ORIGINATOR_ID = 9
+PATH_ATTR_TYPE_CLUSTER_LIST = 10
+PATH_ATTR_TYPE_MP_REACH_NLRI = 14
+PATH_ATTR_TYPE_MP_UNREACH_NLRI = 15
+PATH_ATTR_TYPE_EXTENDED_COMMUNITIES = 16
+PATH_ATTR_TYPE_AS4_PATH = 17
+PATH_ATTR_TYPE_AS4_AGGREGATOR = 18
+PATH_ATTR_TYEP_PMSI_TUNNEL_ATTRIBUTE = 22
+
+ORIGIN_IGP = 0x00
+ORIGIN_EGP = 0x01
+ORIGIN_INCOMPLETE = 0x02
+
+
+#------------------------------------------------------------------------------
+class PathAttribute:
+ PATH_ATTRS = {}
+ UNKNOWN_ATTR = None
+ UNPACK_STR = '!BB'
+
+ @classmethod
+ def register_path_attr(cls, path_attr):
+ def _register_path_attr(subcls):
+ cls.PATH_ATTRS[path_attr] = subcls
+ return subcls
+ return _register_path_attr
+
+ @classmethod
+ def lookup_path_attr(cls, type_code):
+ return cls.PATH_ATTRS.get(type_code, cls.UNKNOWN_ATTR)
+
+ @classmethod
+ def dissect(cls, data):
+ flags, type_code = struct.unpack_from(cls.UNPACK_STR, data)
+ offset = struct.calcsize(cls.UNPACK_STR)
+
+ # get attribute length
+ attr_len_str = '!H' if (flags & PATH_ATTR_FLAG_EXTENDED_LENGTH) else '!B'
+
+ (attr_len,) = struct.unpack_from(attr_len_str, data[offset:])
+
+ offset += struct.calcsize(attr_len_str)
+
+ path_attr_cls = cls.lookup_path_attr(type_code)
+ if path_attr_cls == cls.UNKNOWN_ATTR:
+ return data[offset + attr_len:], None
+
+ return data[offset+attr_len:], path_attr_cls.dissect(data[offset:offset+attr_len])
+
+
+#------------------------------------------------------------------------------
+@PathAttribute.register_path_attr(PATH_ATTR_TYPE_ORIGIN)
+class PathAttrOrigin:
+ ORIGIN_STR = {
+ ORIGIN_IGP: 'IGP',
+ ORIGIN_EGP: 'EGP',
+ ORIGIN_INCOMPLETE: 'INCOMPLETE',
+ }
+
+ @classmethod
+ def dissect(cls, data):
+ (origin,) = struct.unpack_from('!B', data)
+
+ return {'origin': cls.ORIGIN_STR.get(origin, 'UNKNOWN')}
+
+
+#------------------------------------------------------------------------------
+@PathAttribute.register_path_attr(PATH_ATTR_TYPE_AS_PATH)
+class PathAttrAsPath:
+ AS_PATH_TYPE_SET = 0x01
+ AS_PATH_TYPE_SEQUENCE= 0x02
+
+ @staticmethod
+ def get_asn_len(asns):
+ """XXX: Add this nightmare to determine the ASN length"""
+ pass
+
+ @classmethod
+ def dissect(cls, data):
+ (_type, _len) = struct.unpack_from('!BB', data)
+ data = data[2:]
+
+ _type_str = 'Ordred' if _type == cls.AS_PATH_TYPE_SEQUENCE else 'Raw'
+ segment = []
+ while data:
+ (asn,) = struct.unpack_from('!I', data)
+ segment.append(asn)
+ data = data[4:]
+
+ return {'as_path': ' '.join(str(a) for a in segment)}
+
+
+#------------------------------------------------------------------------------
+@PathAttribute.register_path_attr(PATH_ATTR_TYPE_NEXT_HOP)
+class PathAttrNextHop:
+ @classmethod
+ def dissect(cls, data):
+ (nexthop,) = struct.unpack_from('!4s', data)
+ return {'bgp_nexthop': str(ipaddress.IPv4Address(nexthop))}
+
+
+#------------------------------------------------------------------------------
+class PathAttrMultiExitDisc:
+ pass
+
+
+#------------------------------------------------------------------------------
+@PathAttribute.register_path_attr(PATH_ATTR_TYPE_MP_REACH_NLRI)
+class PathAttrMpReachNLRI:
+ """
+ +---------------------------------------------------------+
+ | Address Family Identifier (2 octets) |
+ +---------------------------------------------------------+
+ | Subsequent Address Family Identifier (1 octet) |
+ +---------------------------------------------------------+
+ | Length of Next Hop Network Address (1 octet) |
+ +---------------------------------------------------------+
+ | Network Address of Next Hop (variable) |
+ +---------------------------------------------------------+
+ | Number of SNPAs (1 octet) |
+ +---------------------------------------------------------+
+ | Length of first SNPA(1 octet) |
+ +---------------------------------------------------------+
+ | First SNPA (variable) |
+ +---------------------------------------------------------+
+ | Length of second SNPA (1 octet) |
+ +---------------------------------------------------------+
+ | Second SNPA (variable) |
+ +---------------------------------------------------------+
+ | ... |
+ +---------------------------------------------------------+
+ | Length of Last SNPA (1 octet) |
+ +---------------------------------------------------------+
+ | Last SNPA (variable) |
+ +---------------------------------------------------------+
+ | Network Layer Reachability Information (variable) |
+ +---------------------------------------------------------+
+ """
+ UNPACK_STR = '!HBB'
+ NLRI_RESERVED_LEN = 1
+
+ @staticmethod
+ def dissect_nexthop(nexthop_data, nexthop_len):
+ msg = {}
+ if nexthop_len == 4:
+ # IPv4
+ (ipv4,) = struct.unpack_from('!4s', nexthop_data)
+ msg['nxhp_ip'] = str(ipaddress.IPv4Address(ipv4))
+ elif nexthop_len == 12:
+ # RD + IPv4
+ (rd, ipv4) = struct.unpack_from('!8s4s', nexthop_data)
+ msg['nxhp_ip'] = str(ipaddress.IPv4Address(ipv4))
+ msg['nxhp_rd'] = str(RouteDistinguisher(rd))
+ elif nexthop_len == 16:
+ # IPv6
+ (ipv6,) = struct.unpack_from('!16s', nexthop_data)
+ msg['nxhp_ip'] = str(ipaddress.IPv6Address(ipv6))
+ elif nexthop_len == 24:
+ # RD + IPv6
+ (rd, ipv6) = struct.unpack_from('!8s16s', nexthop_data)
+ msg['nxhp_ip'] = str(ipaddress.IPv6Address(ipv6))
+ msg['nxhp_rd'] = str(RouteDistinguisher(rd))
+ elif nexthop_len == 32:
+ # IPv6 + IPv6 link-local
+ (ipv6, link_local)= struct.unpack_from('!16s16s', nexthop_data)
+ msg['nxhp_ip'] = str(ipaddress.IPv6Address(ipv6))
+ msg['nxhp_link-local'] = str(ipaddress.IPv6Address(link_local))
+ elif nexthop_len == 48:
+ # RD + IPv6 + RD + IPv6 link-local
+ u_str = '!8s16s8s16s'
+ (rd1, ipv6, rd2, link_local)= struct.unpack_from(u_str, nexthop_data)
+ msg['nxhp_rd1'] = str(RouteDistinguisher(rd1))
+ msg['nxhp_ip'] = str(ipaddress.IPv6Address(ipv6))
+ msg['nxhp_rd2'] = str(RouteDistinguisher(rd2))
+ msg['nxhp_link-local'] = str(ipaddress.IPv6Address(link_local))
+
+ return msg
+
+ @staticmethod
+ def dissect_snpa(snpa_data):
+ pass
+
+ @classmethod
+ def dissect(cls, data):
+ (afi, safi, nexthop_len) = struct.unpack_from(cls.UNPACK_STR, data)
+ offset = struct.calcsize(cls.UNPACK_STR)
+ msg = {'afi': afi, 'safi': safi}
+
+ # dissect nexthop
+ nexthop_data = data[offset: offset + nexthop_len]
+ nexthop = cls.dissect_nexthop(nexthop_data, nexthop_len)
+ msg.update(nexthop)
+
+ offset += nexthop_len
+ # dissect snpa or just reserved
+ offset += 1
+ # dissect nlri
+ nlri = NLRI.dissect_nlri(data[offset:], afi, safi)
+ msg.update(nlri)
+
+ return msg
+
+
+#------------------------------------------------------------------------------
+@PathAttribute.register_path_attr(PATH_ATTR_TYPE_MP_UNREACH_NLRI)
+class PathAttrMpUnReachNLRI:
+ """
+ +---------------------------------------------------------+
+ | Address Family Identifier (2 bytes) |
+ +---------------------------------------------------------+
+ | Subsequent Address Family Identifier (1 byte) |
+ +---------------------------------------------------------+
+ | Withdrawn Routes (variable) |
+ +---------------------------------------------------------+
+ """
+ UNPACK_STR = '!HB'
+
+ @classmethod
+ def dissect(cls, data):
+ (afi, safi) = struct.unpack_from(cls.UNPACK_STR, data)
+ offset = struct.calcsize(cls.UNPACK_STR)
+ msg = {'bmp_log_type': 'withdraw','afi': afi, 'safi': safi}
+
+ if data[offset:]:
+ # dissect withdrawn_routes
+ msg.update(NLRI.dissect_nlri(data[offset:], afi, safi))
+
+ return msg
+
+
+#------------------------------------------------------------------------------
+class PathAttrLocalPref:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrAtomicAgregate:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrAggregator:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrCommunities:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrOriginatorID:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrClusterList:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrExtendedCommunities:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrPMSITunnel:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrLinkState:
+ pass
+
+
+#------------------------------------------------------------------------------
+class PathAttrLargeCommunities:
+ pass
diff --git a/tests/topotests/lib/bmp_collector/bgp/update/rd.py b/tests/topotests/lib/bmp_collector/bgp/update/rd.py
new file mode 100644
index 0000000000..c382fa8340
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bgp/update/rd.py
@@ -0,0 +1,59 @@
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+import ipaddress
+import struct
+
+
+#------------------------------------------------------------------------------
+class RouteDistinguisher:
+ """
+ type 0:
+ +---------------------------------------------------------------------+
+ + type=0 (2 bytes)| Administrator subfield | Assigned number subfiled |
+ + | AS number (2 bytes) | Service Provider 4 bytes)|
+ +---------------------------------------------------------------------+
+
+ type 1:
+ +---------------------------------------------------------------------+
+ + type=1 (2 bytes)| Administrator subfield | Assigned number subfiled |
+ + | IPv4 (4 bytes) | Service Provider 2 bytes)|
+ +---------------------------------------------------------------------+
+
+ type 2:
+ +-------------------------------------------------------------------------+
+ + type=2 (2 bytes)| Administrator subfield | Assigned number subfiled |
+ + | 4-bytes AS number (4 bytes)| Service Provider 2 bytes)|
+ +-------------------------------------------------------------------------+
+ """
+ def __init__(self, rd):
+ self.rd = rd
+ self.as_number = None
+ self.admin_ipv4 = None
+ self.four_bytes_as = None
+ self.assigned_sp = None
+ self.repr_str = ''
+ self.dissect()
+
+ def dissect(self):
+ (rd_type,) = struct.unpack_from('!H', self.rd)
+ if rd_type == 0:
+ (self.as_number,
+ self.assigned_sp) = struct.unpack_from('!HI', self.rd[2:])
+ self.repr_str = f'{self.as_number}:{self.assigned_sp}'
+
+ elif rd_type == 1:
+ (self.admin_ipv4,
+ self.assigned_sp) = struct.unpack_from('!IH', self.rd[2:])
+ ipv4 = str(ipaddress.IPv4Address(self.admin_ipv4))
+ self.repr_str = f'{self.as_number}:{self.assigned_sp}'
+
+ elif rd_type == 2:
+ (self.four_bytes_as,
+ self.assigned_sp) = struct.unpack_from('!IH', self.rd[2:])
+ self.repr_str = f'{self.four_bytes_as}:{self.assigned_sp}'
+
+ def __str__(self):
+ return self.repr_str
diff --git a/tests/topotests/lib/bmp_collector/bmp.py b/tests/topotests/lib/bmp_collector/bmp.py
new file mode 100644
index 0000000000..b07329cd52
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bmp.py
@@ -0,0 +1,420 @@
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+"""
+BMP main module:
+ - dissect monitoring messages in the way to get updated/withdrawed prefixes
+ - XXX: missing RFCs references
+ - XXX: more bmp messages types to dissect
+ - XXX: complete bgp message dissection
+"""
+import datetime
+import ipaddress
+import json
+import os
+import struct
+
+from bgp.update import BGPUpdate
+from bgp.update.rd import RouteDistinguisher
+
+
+SEQ = 0
+LOG_DIR = "/var/log/"
+LOG_FILE = "/var/log/bmp.log"
+
+IS_ADJ_RIB_OUT = 1 << 4
+IS_AS_PATH = 1 << 5
+IS_POST_POLICY = 1 << 6
+IS_IPV6 = 1 << 7
+IS_FILTERED = 1 << 7
+
+if not os.path.exists(LOG_DIR):
+ os.makedirs(LOG_DIR)
+
+def bin2str_ipaddress(ip_bytes, is_ipv6=False):
+ if is_ipv6:
+ return str(ipaddress.IPv6Address(ip_bytes))
+ return str(ipaddress.IPv4Address(ip_bytes[-4:]))
+
+def log2file(logs):
+ """
+ XXX: extract the useful information and save it in a flat dictionnary
+ """
+ with open(LOG_FILE, 'a') as f:
+ f.write(json.dumps(logs) + "\n")
+
+
+#------------------------------------------------------------------------------
+class BMPCodes:
+ """
+ XXX: complete the list, provide RFCs.
+ """
+ VERSION = 0x3
+
+ BMP_MSG_TYPE_ROUTE_MONITORING = 0x00
+ BMP_MSG_TYPE_STATISTICS_REPORT = 0x01
+ BMP_MSG_TYPE_PEER_DOWN_NOTIFICATION = 0x02
+ BMP_MSG_TYPE_PEER_UP_NOTIFICATION = 0x03
+ BMP_MSG_TYPE_INITIATION = 0x04
+ BMP_MSG_TYPE_TERMINATION = 0x05
+ BMP_MSG_TYPE_ROUTE_MIRRORING = 0x06
+ BMP_MSG_TYPE_ROUTE_POLICY = 0x64
+
+ # initiation message types
+ BMP_INIT_INFO_STRING = 0x00
+ BMP_INIT_SYSTEM_DESCRIPTION = 0x01
+ BMP_INIT_SYSTEM_NAME = 0x02
+ BMP_INIT_VRF_TABLE_NAME = 0x03
+ BMP_INIT_ADMIN_LABEL = 0x04
+
+ # peer types
+ BMP_PEER_GLOBAL_INSTANCE = 0x00
+ BMP_PEER_RD_INSTANCE = 0x01
+ BMP_PEER_LOCAL_INSTANCE = 0x02
+ BMP_PEER_LOC_RIB_INSTANCE = 0x03
+
+ # peer header flags
+ BMP_PEER_FLAG_IPV6 = 0x80
+ BMP_PEER_FLAG_POST_POLICY = 0x40
+ BMP_PEER_FLAG_AS_PATH = 0x20
+ BMP_PEER_FLAG_ADJ_RIB_OUT = 0x10
+
+ # peer loc-rib flag
+ BMP_PEER_FLAG_LOC_RIB = 0x80
+ BMP_PEER_FLAG_LOC_RIB_RES = 0x7F
+
+ # statistics type
+ BMP_STAT_PREFIX_REJ = 0x00
+ BMP_STAT_PREFIX_DUP = 0x01
+ BMP_STAT_WITHDRAW_DUP = 0x02
+ BMP_STAT_CLUSTER_LOOP = 0x03
+ BMP_STAT_AS_LOOP = 0x04
+ BMP_STAT_INV_ORIGINATOR = 0x05
+ BMP_STAT_AS_CONFED_LOOP = 0x06
+ BMP_STAT_ROUTES_ADJ_RIB_IN = 0x07
+ BMP_STAT_ROUTES_LOC_RIB = 0x08
+ BMP_STAT_ROUTES_PER_ADJ_RIB_IN = 0x09
+ BMP_STAT_ROUTES_PER_LOC_RIB = 0x0A
+ BMP_STAT_UPDATE_TREAT = 0x0B
+ BMP_STAT_PREFIXES_TREAT = 0x0C
+ BMP_STAT_DUPLICATE_UPDATE = 0x0D
+ BMP_STAT_ROUTES_PRE_ADJ_RIB_OUT = 0x0E
+ BMP_STAT_ROUTES_POST_ADJ_RIB_OUT = 0x0F
+ BMP_STAT_ROUTES_PRE_PER_ADJ_RIB_OUT = 0x10
+ BMP_STAT_ROUTES_POST_PER_ADJ_RIB_OUT = 0x11
+
+ # peer down reason code
+ BMP_PEER_DOWN_LOCAL_NOTIFY = 0x01
+ BMP_PEER_DOWN_LOCAL_NO_NOTIFY = 0X02
+ BMP_PEER_DOWN_REMOTE_NOTIFY = 0X03
+ BMP_PEER_DOWN_REMOTE_NO_NOTIFY = 0X04
+ BMP_PEER_DOWN_INFO_NO_LONGER = 0x05
+ BMP_PEER_DOWN_SYSTEM_CLOSED = 0X06
+
+ # termincation message types
+ BMP_TERM_TYPE_STRING = 0x00
+ BMP_TERM_TYPE_REASON = 0X01
+
+ # termination reason code
+ BMP_TERM_REASON_ADMIN_CLOSE = 0x00
+ BMP_TERM_REASON_UNSPECIFIED = 0x01
+ BMP_TERM_REASON_RESOURCES = 0x02
+ BMP_TERM_REASON_REDUNDANT = 0x03
+ BMP_TERM_REASON_PERM_CLOSE = 0x04
+
+ # policy route tlv
+ BMP_ROUTE_POLICY_TLV_VRF = 0x00
+ BMP_ROUTE_POLICY_TLV_POLICY= 0x01
+ BMP_ROUTE_POLICY_TLV_PRE_POLICY = 0x02
+ BMP_ROUTE_POLICY_TLV_POST_POLICY = 0x03
+ BMP_ROUTE_POLICY_TLV_STRING = 0x04
+
+
+#------------------------------------------------------------------------------
+class BMPMsg:
+ """
+ XXX: should we move register_msg_type and look_msg_type
+ to generic Type class.
+ """
+ TYPES = {}
+ UNKNOWN_TYPE = None
+ HDR_STR = '!BIB'
+ MIN_LEN = struct.calcsize(HDR_STR)
+ TYPES_STR = {
+ BMPCodes.BMP_MSG_TYPE_INITIATION: 'initiation',
+ BMPCodes.BMP_MSG_TYPE_PEER_DOWN_NOTIFICATION: 'peer down notification',
+ BMPCodes.BMP_MSG_TYPE_PEER_UP_NOTIFICATION: 'peer up notification',
+ BMPCodes.BMP_MSG_TYPE_ROUTE_MONITORING: 'route monitoring',
+ BMPCodes.BMP_MSG_TYPE_STATISTICS_REPORT: 'statistics report',
+ BMPCodes.BMP_MSG_TYPE_TERMINATION: 'termination',
+ BMPCodes.BMP_MSG_TYPE_ROUTE_MIRRORING: 'route mirroring',
+ BMPCodes.BMP_MSG_TYPE_ROUTE_POLICY: 'route policy',
+ }
+
+ @classmethod
+ def register_msg_type(cls, msgtype):
+ def _register_type(subcls):
+ cls.TYPES[msgtype] = subcls
+ return subcls
+ return _register_type
+
+ @classmethod
+ def lookup_msg_type(cls, msgtype):
+ return cls.TYPES.get(msgtype, cls.UNKNOWN_TYPE)
+
+ @classmethod
+ def dissect_header(cls, data):
+ """
+ 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Version |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Message Length |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Message Type |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ """
+ if len(data) < cls.MIN_LEN:
+ pass
+ else:
+ _version, _len, _type = struct.unpack(cls.HDR_STR, data[0:cls.MIN_LEN])
+ return _version, _len, _type
+
+ @classmethod
+ def dissect(cls, data):
+ global SEQ
+ version, msglen, msgtype = cls.dissect_header(data)
+
+ msg_data = data[cls.MIN_LEN:msglen]
+ data = data[msglen:]
+
+ if version != BMPCodes.VERSION:
+ # XXX: log something
+ return data
+
+ msg_cls = cls.lookup_msg_type(msgtype)
+ if msg_cls == cls.UNKNOWN_TYPE:
+ # XXX: log something
+ return data
+
+ msg_cls.MSG_LEN = msglen - cls.MIN_LEN
+ logs = msg_cls.dissect(msg_data)
+ logs["seq"] = SEQ
+ log2file(logs)
+ SEQ += 1
+
+ return data
+
+
+#------------------------------------------------------------------------------
+class BMPPerPeerMessage:
+ """
+ 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Peer Type | Peer Flags |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Peer Address (16 bytes) |
+ ~ ~
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Peer AS |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Peer BGP ID |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Timestamp (seconds) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Timestamp (microseconds) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ """
+ PEER_UNPACK_STR = '!BB8s16sI4sII'
+ PEER_TYPE_STR = {
+ BMPCodes.BMP_PEER_GLOBAL_INSTANCE: 'global instance',
+ BMPCodes.BMP_PEER_RD_INSTANCE: 'route distinguisher instance',
+ BMPCodes.BMP_PEER_LOCAL_INSTANCE: 'local instance',
+ BMPCodes.BMP_PEER_LOC_RIB_INSTANCE: 'loc-rib instance',
+ }
+
+ @classmethod
+ def dissect(cls, data):
+ (peer_type,
+ peer_flags,
+ peer_distinguisher,
+ peer_address,
+ peer_asn,
+ peer_bgp_id,
+ timestamp_secs,
+ timestamp_microsecs) = struct.unpack_from(cls.PEER_UNPACK_STR, data)
+
+ msg = {'peer_type': cls.PEER_TYPE_STR[peer_type]}
+
+ if peer_type == 0x03:
+ msg['is_filtered'] = bool(peer_flags & IS_FILTERED)
+ else:
+ # peer_flags = 0x0000 0000
+ # ipv6, post-policy, as-path, adj-rib-out, reserverdx4
+ is_adj_rib_out = bool(peer_flags & IS_ADJ_RIB_OUT)
+ is_as_path = bool(peer_flags & IS_AS_PATH)
+ is_post_policy = bool(peer_flags & IS_POST_POLICY)
+ is_ipv6 = bool(peer_flags & IS_IPV6)
+ msg['post_policy'] = is_post_policy
+ msg['ipv6'] = is_ipv6
+ msg['peer_ip'] = bin2str_ipaddress(peer_address, is_ipv6)
+
+
+ peer_bgp_id = bin2str_ipaddress(peer_bgp_id)
+ timestamp = float(timestamp_secs) + timestamp_microsecs * (10 ** -6)
+
+ data = data[struct.calcsize(cls.PEER_UNPACK_STR):]
+ msg.update({
+ 'peer_distinguisher': str(RouteDistinguisher(peer_distinguisher)),
+ 'peer_asn': peer_asn,
+ 'peer_bgp_id': peer_bgp_id,
+ 'timestamp': str(datetime.datetime.fromtimestamp(timestamp)),
+ })
+
+ return data, msg
+
+
+#------------------------------------------------------------------------------
+@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_ROUTE_MONITORING)
+class BMPRouteMonitoring(BMPPerPeerMessage):
+
+ @classmethod
+ def dissect(cls, data):
+ data, peer_msg = super().dissect(data)
+ data, update_msg = BGPUpdate.dissect(data)
+ return {**peer_msg, **update_msg}
+
+
+#------------------------------------------------------------------------------
+class BMPStatisticsReport:
+ """
+ 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Stats Count |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Stat Type | Stat Len |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Stat Data |
+ ~ ~
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ """
+ pass
+
+
+#------------------------------------------------------------------------------
+class BMPPeerDownNotification:
+ """
+ 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Reason |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Data (present if Reason = 1, 2 or 3) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ """
+ pass
+
+
+#------------------------------------------------------------------------------
+@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_PEER_UP_NOTIFICATION)
+class BMPPeerUpNotification(BMPPerPeerMessage):
+ """
+ 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Local Address (16 bytes) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Local Port | Remote Port |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Sent OPEN Message #|
+ ~ ~
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Received OPEN Message |
+ ~ ~
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ """
+ UNPACK_STR = '!16sHH'
+ MIN_LEN = struct.calcsize(UNPACK_STR)
+ MSG_LEN = None
+
+ @classmethod
+ def dissect(cls, data):
+ data, peer_msg = super().dissect(data)
+
+ (local_addr,
+ local_port,
+ remote_port) = struct.unpack_from(cls.UNPACK_STR, data)
+
+ msg = {
+ **peer_msg,
+ **{
+ 'local_ip': bin2str_ipaddress(local_addr, peer_msg.get('ipv6')),
+ 'local_port': int(local_port),
+ 'remote_port': int(remote_port),
+ },
+ }
+
+ # XXX: dissect the bgp open message
+
+ return msg
+
+
+#------------------------------------------------------------------------------
+@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_INITIATION)
+class BMPInitiation:
+ """
+ 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Information Type | Information Length |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Information (variable) |
+ ~ ~
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ """
+ TLV_STR = '!HH'
+ MIN_LEN = struct.calcsize(TLV_STR)
+ FIELD_TO_STR = {
+ BMPCodes.BMP_INIT_INFO_STRING: 'information',
+ BMPCodes.BMP_INIT_ADMIN_LABEL: 'admin_label',
+ BMPCodes.BMP_INIT_SYSTEM_DESCRIPTION: 'system_description',
+ BMPCodes.BMP_INIT_SYSTEM_NAME: 'system_name',
+ BMPCodes.BMP_INIT_VRF_TABLE_NAME: 'vrf_table_name',
+ }
+
+ @classmethod
+ def dissect(cls, data):
+ msg = {}
+ while len(data) > cls.MIN_LEN:
+ _type, _len = struct.unpack_from(cls.TLV_STR, data[0:cls.MIN_LEN])
+ _value = data[cls.MIN_LEN: cls.MIN_LEN + _len].decode()
+
+ msg[cls.FIELD_TO_STR[_type]] = _value
+ data = data[cls.MIN_LEN + _len:]
+
+ return msg
+
+
+#------------------------------------------------------------------------------
+class BMPTermination:
+ """
+ 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Information Type | Information Length |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Information (variable) |
+ ~ ~
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ """
+ pass
+
+
+#------------------------------------------------------------------------------
+class BMPRouteMirroring:
+ pass
+
+
+#------------------------------------------------------------------------------
+class BMPRoutePolicy:
+ pass
diff --git a/tests/topotests/lib/bmp_collector/bmpserver b/tests/topotests/lib/bmp_collector/bmpserver
new file mode 100755
index 0000000000..25b4a52c5e
--- /dev/null
+++ b/tests/topotests/lib/bmp_collector/bmpserver
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: ISC
+
+# Copyright 2023 6WIND S.A.
+# Authored by Farid Mihoub <farid.mihoub@6wind.com>
+#
+import argparse
+# XXX: something more reliable should be used "Twisted" a great choice.
+import socket
+import sys
+
+from bmp import BMPMsg
+
+BGP_MAX_SIZE = 4096
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", "--address", type=str, default="0.0.0.0")
+parser.add_argument("-p", "--port", type=int, default=1789)
+
+def main():
+ args = parser.parse_args()
+ ADDRESS, PORT = args.address, args.port
+
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind((ADDRESS, PORT))
+ s.listen()
+ connection, _ = s.accept()
+
+ try:
+ while True:
+ data = connection.recv(BGP_MAX_SIZE)
+ while len(data) > BMPMsg.MIN_LEN:
+ data = BMPMsg.dissect(data)
+ except Exception as e:
+ # XXX: do something
+ pass
+ except KeyboardInterrupt:
+ # XXX: do something
+ pass
+ finally:
+ connection.close()
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tests/topotests/lib/topogen.py b/tests/topotests/lib/topogen.py
index 6ddd223e25..4d935b9538 100644
--- a/tests/topotests/lib/topogen.py
+++ b/tests/topotests/lib/topogen.py
@@ -363,6 +363,15 @@ class Topogen(object):
self.peern += 1
return self.gears[name]
+ def add_bmp_server(self, name, ip, defaultRoute, port=1789):
+ """Add the bmp collector gear"""
+ if name in self.gears:
+ raise KeyError("The bmp server already exists")
+
+ self.gears[name] = TopoBMPCollector(
+ self, name, ip=ip, defaultRoute=defaultRoute, port=port
+ )
+
def add_link(self, node1, node2, ifname1=None, ifname2=None):
"""
Creates a connection between node1 and node2. The nodes can be the
@@ -425,6 +434,13 @@ class Topogen(object):
"""
return self.get_gears(TopoExaBGP)
+ def get_bmp_servers(self):
+ """
+ Retruns the bmp servers dictionnary (the key is the bmp server the
+ value is the bmp server object itself).
+ """
+ return self.get_gears(TopoBMPCollector)
+
def start_topology(self):
"""Starts the topology class."""
logger.info("starting topology: {}".format(self.modname))
@@ -1204,6 +1220,33 @@ class TopoExaBGP(TopoHost):
return ""
+class TopoBMPCollector(TopoHost):
+ PRIVATE_DIRS = [
+ "/var/log",
+ ]
+
+ def __init__(self, tgen, name, **params):
+ params["private_mounts"] = self.PRIVATE_DIRS
+ self.port = params["port"]
+ self.ip = params["ip"]
+ super(TopoBMPCollector, self).__init__(tgen, name, **params)
+
+ def __str__(self):
+ gear = super(TopoBMPCollector, self).__str__()
+ gear += " TopoBMPCollector<>".format()
+ return gear
+
+ def start(self):
+ self.run(
+ "{}/bmp_collector/bmpserver -a {} -p {}&".format(CWD, self.ip, self.port),
+ stdout=None,
+ )
+
+ def stop(self):
+ self.run("pkill -9 -f bmpserver")
+ return ""
+
+
#
# Diagnostic function
#