mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-06-07 16:51:17 +00:00
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
|
self.peern += 1
|
||||||
return self.gears[name]
|
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):
|
def add_link(self, node1, node2, ifname1=None, ifname2=None):
|
||||||
"""
|
"""
|
||||||
Creates a connection between node1 and node2. The nodes can be the
|
Creates a connection between node1 and node2. The nodes can be the
|
||||||
@ -425,6 +434,13 @@ class Topogen(object):
|
|||||||
"""
|
"""
|
||||||
return self.get_gears(TopoExaBGP)
|
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):
|
def start_topology(self):
|
||||||
"""Starts the topology class."""
|
"""Starts the topology class."""
|
||||||
logger.info("starting topology: {}".format(self.modname))
|
logger.info("starting topology: {}".format(self.modname))
|
||||||
@ -1204,6 +1220,33 @@ class TopoExaBGP(TopoHost):
|
|||||||
return ""
|
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
|
# Diagnostic function
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user