topotests: add basic bmp collector

Signed-off-by: Farid Mihoub <farid.mihoub@6wind.com>
This commit is contained in:
Farid Mihoub 2023-04-25 17:27:46 +02:00
parent 6934a1d31d
commit 875511c466
10 changed files with 1152 additions and 0 deletions

View file

@ -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,
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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())

View file

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