diff --git a/tests/topotests/bgp_bmp/__init__.py b/tests/topotests/bgp_bmp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/topotests/bgp_bmp/r1/bgpd.conf b/tests/topotests/bgp_bmp/r1/bgpd.conf new file mode 100644 index 000000000..69acf6e75 --- /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 000000000..6a25a6f4c --- /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 000000000..7c8255a17 --- /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 000000000..9d82bfe2d --- /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 000000000..65f191b33 --- /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 +# + +""" +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 000000000..e69de29bb 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 000000000..6c814ee9a --- /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 +# +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 000000000..d079b3511 --- /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 +# +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 000000000..01af1ae2b --- /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 +# + +# 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 000000000..c1720f126 --- /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 +# +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 000000000..6e82e9c17 --- /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 +# +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 000000000..c382fa834 --- /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 +# +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 000000000..b07329cd5 --- /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 +# +""" +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 000000000..25b4a52c5 --- /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 +# +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 6ddd223e2..4d935b953 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 #