forked from Mirror/frr
topotests: add basic bmp collector
Signed-off-by: Farid Mihoub <farid.mihoub@6wind.com>
This commit is contained in:
parent
6934a1d31d
commit
875511c466
0
tests/topotests/lib/bmp_collector/bgp/__init__.py
Normal file
0
tests/topotests/lib/bmp_collector/bgp/__init__.py
Normal file
34
tests/topotests/lib/bmp_collector/bgp/open/__init__.py
Normal file
34
tests/topotests/lib/bmp_collector/bgp/open/__init__.py
Normal 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,
|
||||
}
|
54
tests/topotests/lib/bmp_collector/bgp/update/__init__.py
Normal file
54
tests/topotests/lib/bmp_collector/bgp/update/__init__.py
Normal 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
|
53
tests/topotests/lib/bmp_collector/bgp/update/af.py
Normal file
53
tests/topotests/lib/bmp_collector/bgp/update/af.py
Normal 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)
|
140
tests/topotests/lib/bmp_collector/bgp/update/nlri.py
Normal file
140
tests/topotests/lib/bmp_collector/bgp/update/nlri.py
Normal 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
|
304
tests/topotests/lib/bmp_collector/bgp/update/path_attributes.py
Normal file
304
tests/topotests/lib/bmp_collector/bgp/update/path_attributes.py
Normal 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
|
59
tests/topotests/lib/bmp_collector/bgp/update/rd.py
Normal file
59
tests/topotests/lib/bmp_collector/bgp/update/rd.py
Normal 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
|
420
tests/topotests/lib/bmp_collector/bmp.py
Normal file
420
tests/topotests/lib/bmp_collector/bmp.py
Normal 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
|
45
tests/topotests/lib/bmp_collector/bmpserver
Executable file
45
tests/topotests/lib/bmp_collector/bmpserver
Executable 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())
|
|
@ -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
|
||||
#
|
||||
|
|
Loading…
Reference in a new issue