diff options
Diffstat (limited to 'tests/topotests/lib/bmp_collector/bmp.py')
| -rw-r--r-- | tests/topotests/lib/bmp_collector/bmp.py | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/tests/topotests/lib/bmp_collector/bmp.py b/tests/topotests/lib/bmp_collector/bmp.py new file mode 100644 index 0000000000..237decdd5e --- /dev/null +++ b/tests/topotests/lib/bmp_collector/bmp.py @@ -0,0 +1,432 @@ +# 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, log_file): + """ + 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, log_file=None): + 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, log_file if log_file else LOG_FILE) + 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) + msg["policy"] = "loc-rib" + 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["policy"] = "post-policy" if is_post_policy else "pre-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 |
