Skip to content

Commit

Permalink
topotests: add basic bmp collector
Browse files Browse the repository at this point in the history
Signed-off-by: Farid Mihoub <farid.mihoub@6wind.com>
  • Loading branch information
Farid Mihoub committed Jul 12, 2023
1 parent 6934a1d commit 875511c
Show file tree
Hide file tree
Showing 10 changed files with 1,152 additions and 0 deletions.
Empty file.
34 changes: 34 additions & 0 deletions tests/topotests/lib/bmp_collector/bgp/open/__init__.py
Original file line number Diff line number Diff line change
@@ -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 changes: 54 additions & 0 deletions tests/topotests/lib/bmp_collector/bgp/update/__init__.py
Original file line number Diff line number Diff line change
@@ -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 changes: 53 additions & 0 deletions tests/topotests/lib/bmp_collector/bgp/update/af.py
Original file line number Diff line number Diff line change
@@ -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 changes: 140 additions & 0 deletions tests/topotests/lib/bmp_collector/bgp/update/nlri.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 875511c

Please sign in to comment.