summaryrefslogtreecommitdiff
path: root/tests/topotests/lib/bmp_collector/bmp.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/topotests/lib/bmp_collector/bmp.py')
-rw-r--r--tests/topotests/lib/bmp_collector/bmp.py432
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