# 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, 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