Merge pull request #12964 from FRIDM636/bmp-basic-test

Add basic bgp bmp test
This commit is contained in:
Russ White 2023-07-18 08:53:07 -04:00 committed by GitHub
commit a510c23641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1454 additions and 0 deletions

View File

View File

@ -0,0 +1,22 @@
router bgp 65501
bgp router-id 192.168.0.1
bgp log-neighbor-changes
no bgp ebgp-requires-policy
neighbor 192.168.0.2 remote-as 65502
neighbor 192:168::2 remote-as 65502
!
bmp targets bmp1
bmp connect 192.0.178.10 port 1789 min-retry 100 max-retry 10000
exit
!
address-family ipv4 unicast
neighbor 192.168.0.2 activate
neighbor 192.168.0.2 soft-reconfiguration inbound
no neighbor 192:168::2 activate
exit-address-family
!
address-family ipv6 unicast
neighbor 192:168::2 activate
neighbor 192:168::2 soft-reconfiguration inbound
exit-address-family
!

View File

@ -0,0 +1,7 @@
interface r1-eth0
ip address 192.0.178.1/24
!
interface r1-eth1
ip address 192.168.0.1/24
ipv6 address 192:168::1/64
!

View File

@ -0,0 +1,19 @@
router bgp 65502
bgp router-id 192.168.0.2
bgp log-neighbor-changes
no bgp ebgp-requires-policy
no bgp network import-check
neighbor 192.168.0.1 remote-as 65501
neighbor 192:168::1 remote-as 65501
!
address-family ipv4 unicast
neighbor 192.168.0.1 activate
no neighbor 192:168::1 activate
redistribute connected
exit-address-family
!
address-family ipv6 unicast
neighbor 192:168::1 activate
redistribute connected
exit-address-family
!

View File

@ -0,0 +1,8 @@
interface r2-eth0
ip address 192.168.0.2/24
ipv6 address 192:168::2/64
!
interface r2-eth1
ip address 172.31.0.2/24
ipv6 address 172:31::2/64
!

View File

@ -0,0 +1,246 @@
#!/usr/bin/env python
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
"""
test_bgp_bmp.py: Test BGP BMP functionalities
+------+ +------+ +------+
| | | | | |
| BMP1 |------------| R1 |---------------| R2 |
| | | | | |
+------+ +------+ +------+
Setup two routers R1 and R2 with one link configured with IPv4 and
IPv6 addresses.
Configure BGP in R1 and R2 to exchange prefixes from
the latter to the first router.
Setup a link between R1 and the BMP server, activate the BMP feature in R1
and ensure the monitored BGP sessions logs are well present on the BMP server.
"""
from functools import partial
from ipaddress import ip_network
import json
import os
import platform
import pytest
import sys
# Save the Current Working Directory to find configuration files.
CWD = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join("../"))
sys.path.append(os.path.join("../lib/"))
# pylint: disable=C0413
# Import topogen and topotest helpers
from lib import topotest
from lib.bgp import verify_bgp_convergence_from_running_config
from lib.topogen import Topogen, TopoRouter, get_topogen
from lib.topolog import logger
pytestmark = [pytest.mark.bgpd]
# remember the last sequence number of the logging messages
SEQ = 0
PRE_POLICY = "pre-policy"
POST_POLICY = "post-policy"
def build_topo(tgen):
tgen.add_router("r1")
tgen.add_router("r2")
tgen.add_bmp_server("bmp1", ip="192.0.178.10", defaultRoute="via 192.0.178.1")
switch = tgen.add_switch("s1")
switch.add_link(tgen.gears["r1"])
switch.add_link(tgen.gears["bmp1"])
tgen.add_link(tgen.gears["r1"], tgen.gears["r2"], "r1-eth1", "r2-eth0")
def setup_module(mod):
tgen = Topogen(build_topo, mod.__name__)
tgen.start_topology()
for rname, router in tgen.routers().items():
router.load_config(
TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
)
router.load_config(
TopoRouter.RD_BGP,
os.path.join(CWD, "{}/bgpd.conf".format(rname)),
"-M bmp",
)
tgen.start_router()
logger.info("starting BMP servers")
for _, server in tgen.get_bmp_servers().items():
server.start()
def teardown_module(_mod):
tgen = get_topogen()
tgen.stop_topology()
def test_bgp_convergence():
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
result = verify_bgp_convergence_from_running_config(tgen, dut="r1")
assert result is True, "BGP is not converging"
def get_bmp_messages():
"""
Read the BMP logging messages.
"""
messages = []
tgen = get_topogen()
text_output = tgen.gears["bmp1"].run("cat /var/log/bmp.log")
for m in text_output.splitlines():
# some output in the bash can break the message decoding
try:
messages.append(json.loads(m))
except Exception as e:
logger.warning(str(e) + " message: {}".format(str(m)))
continue
if not messages:
logger.error("Bad BMP log format, check your BMP server")
return messages
def check_for_prefixes(expected_prefixes, bmp_log_type, post_policy):
"""
Check for the presence of the given prefixes in the BMP server logs with
the given message type and the set policy.
"""
global SEQ
# we care only about the new messages
messages = [
m for m in sorted(get_bmp_messages(), key=lambda d: d["seq"]) if m["seq"] > SEQ
]
# get the list of pairs (prefix, policy, seq) for the given message type
prefixes = [
m["ip_prefix"]
for m in messages
if "ip_prefix" in m.keys()
and "bmp_log_type" in m.keys()
and m["bmp_log_type"] == bmp_log_type
and m["post_policy"] == post_policy
]
# check for prefixes
for ep in expected_prefixes:
if ep not in prefixes:
msg = "The prefix {} is not present in the {} log messages."
logger.debug(msg.format(ep, bmp_log_type))
return False
SEQ = messages[-1]["seq"]
return True
def set_bmp_policy(tgen, node, asn, target, safi, policy, vrf=None):
"""
Configure the bmp policy.
"""
vrf = " vrf {}" if vrf else ""
cmd = [
"con t\n",
"router bgp {}{}\n".format(asn, vrf),
"bmp targets {}\n".format(target),
"bmp monitor ipv4 {} {}\n".format(safi, policy),
"bmp monitor ipv6 {} {}\n".format(safi, policy),
"end\n",
]
tgen.gears[node].vtysh_cmd("".join(cmd))
def configure_prefixes(tgen, node, asn, safi, prefixes, vrf=None, update=True):
"""
Configure the bgp prefixes.
"""
withdraw = "no " if not update else ""
vrf = " vrf {}" if vrf else ""
for p in prefixes:
ip = ip_network(p)
cmd = [
"conf t\n",
"router bgp {}{}\n".format(asn, vrf),
"address-family ipv{} {}\n".format(ip.version, safi),
"{}network {}\n".format(withdraw, ip),
"exit-address-family\n",
]
logger.debug("setting prefix: ipv{} {} {}".format(ip.version, safi, ip))
tgen.gears[node].vtysh_cmd("".join(cmd))
def unicast_prefixes(policy):
"""
Setup the BMP monitor policy, Add and withdraw ipv4/v6 prefixes.
Check if the previous actions are logged in the BMP server with the right
message type and the right policy.
"""
tgen = get_topogen()
set_bmp_policy(tgen, "r1", 65501, "bmp1", "unicast", policy)
prefixes = ["172.31.0.15/32", "2111::1111/128"]
# add prefixes
configure_prefixes(tgen, "r2", 65502, "unicast", prefixes)
logger.info("checking for updated prefixes")
# check
test_func = partial(check_for_prefixes, prefixes, "update", policy == POST_POLICY)
success, _ = topotest.run_and_expect(test_func, True, wait=0.5)
assert success, "Checking the updated prefixes has been failed !."
# withdraw prefixes
configure_prefixes(tgen, "r2", 65502, "unicast", prefixes, update=False)
logger.info("checking for withdrawed prefxies")
# check
test_func = partial(check_for_prefixes, prefixes, "withdraw", policy == POST_POLICY)
success, _ = topotest.run_and_expect(test_func, True, wait=0.5)
assert success, "Checking the withdrawed prefixes has been failed !."
def test_bmp_server_logging():
"""
Assert the logging of the bmp server.
"""
def check_for_log_file():
tgen = get_topogen()
output = tgen.gears["bmp1"].run("ls /var/log/")
if "bmp.log" not in output:
return False
return True
success, _ = topotest.run_and_expect(check_for_log_file, True, wait=0.5)
assert success, "The BMP server is not logging"
def test_bmp_bgp_unicast():
"""
Add/withdraw bgp unicast prefixes and check the bmp logs.
"""
logger.info("*** Unicast prefixes pre-policy logging ***")
unicast_prefixes(PRE_POLICY)
logger.info("*** Unicast prefixes post-policy logging ***")
unicast_prefixes(POST_POLICY)
if __name__ == "__main__":
args = ["-s"] + sys.argv[1:]
sys.exit(pytest.main(args))

View File

@ -0,0 +1,34 @@
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
import ipaddress
import struct
class BGPOpen:
UNPACK_STR = '!16sHBBHH4sB'
@classmethod
def dissect(cls, data):
(marker,
length,
open_type,
version,
my_as,
hold_time,
bgp_id,
optional_params_len) = struct.unpack_from(cls.UNPACK_STR, data)
data = data[struct.calcsize(cls.UNPACK_STR) + optional_params_len:]
# XXX: parse optional parameters
return data, {
'version': version,
'my_as': my_as,
'hold_time': hold_time,
'bgp_id': ipaddress.ip_address(bgp_id),
'optional_params_len': optional_params_len,
}

View File

@ -0,0 +1,54 @@
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
import ipaddress
import struct
from .nlri import NlriIPv4Unicast
from .path_attributes import PathAttribute
#------------------------------------------------------------------------------
class BGPUpdate:
UNPACK_STR = '!16sHBH'
STATIC_SIZE = 23
@classmethod
def dissect(cls, data):
msg = {'bmp_log_type': 'update'}
common_size = struct.calcsize(cls.UNPACK_STR)
(marker,
length,
update_type,
withdrawn_routes_len) = struct.unpack_from(cls.UNPACK_STR, data)
# get withdrawn routes
withdrawn_routes = ''
if withdrawn_routes_len:
withdrawn_routes = NlriIPv4Unicast.parse(
data[common_size:common_size + withdrawn_routes_len]
)
msg['bmp_log_type'] = 'withdraw'
msg.update(withdrawn_routes)
# get path attributes
(total_path_attrs_len,) = struct.unpack_from(
'!H', data[common_size+withdrawn_routes_len:])
if total_path_attrs_len:
offset = cls.STATIC_SIZE + withdrawn_routes_len
path_attrs_data = data[offset:offset + total_path_attrs_len]
while path_attrs_data:
path_attrs_data, pattr = PathAttribute.dissect(path_attrs_data)
if pattr:
msg = {**msg, **pattr}
# get nlri
nlri_len = length - cls.STATIC_SIZE - withdrawn_routes_len - total_path_attrs_len
if nlri_len > 0:
nlri = NlriIPv4Unicast.parse(data[length - nlri_len:length])
msg.update(nlri)
return data[length:], msg

View File

@ -0,0 +1,53 @@
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
# IANA Address Family Identifier
AFI_IP = 1
AFI_IP6 = 2
AFI_L2VPN = 25
# IANA Subsequent Address Family Idenitifier
SAFI_UNICAST = 1
SAFI_MULTICAST = 2
SAFI_MPLS_LABEL = 4
SAFI_EVPN = 70
SAFI_MPLS_VPN = 128
SAFI_IP_FLOWSPEC = 133
SAFI_VPN_FLOWSPEC = 134
#------------------------------------------------------------------------------
class AddressFamily:
def __init__(self, afi, safi):
self.afi = afi
self.safi = safi
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return (self.afi, self.safi) == (other.afi, other.safi)
def __str__(self):
return f'afi: {self.afi}, safi: {self.safi}'
def __hash__(self):
return hash((self.afi, self.safi))
#------------------------------------------------------------------------------
class AF:
IPv4_UNICAST = AddressFamily(AFI_IP, SAFI_UNICAST)
IPv6_UNICAST = AddressFamily(AFI_IP6, SAFI_UNICAST)
IPv4_VPN = AddressFamily(AFI_IP, SAFI_MPLS_VPN)
IPv6_VPN = AddressFamily(AFI_IP6, SAFI_MPLS_VPN)
IPv4_MPLS = AddressFamily(AFI_IP, SAFI_MPLS_LABEL)
IPv6_MPLS = AddressFamily(AFI_IP6, SAFI_MPLS_LABEL)
IPv4_FLOWSPEC = AddressFamily(AFI_IP, SAFI_IP_FLOWSPEC)
IPv6_FLOWSPEC = AddressFamily(AFI_IP6, SAFI_IP_FLOWSPEC)
VPNv4_FLOWSPEC = AddressFamily(AFI_IP, SAFI_VPN_FLOWSPEC)
VPNv6_FLOWSPEC = AddressFamily(AFI_IP6, SAFI_VPN_FLOWSPEC)
L2EVPN = AddressFamily(AFI_L2VPN, SAFI_EVPN)
L2VPN_FLOWSPEC = AddressFamily(AFI_L2VPN, SAFI_VPN_FLOWSPEC)

View File

@ -0,0 +1,140 @@
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
import ipaddress
import struct
from .af import AddressFamily, AF
from .rd import RouteDistinguisher
def decode_label(label):
# from frr
# frr encode just one label
return (label[0] << 12) | (label[1] << 4) | (label[2] & 0xf0) >> 4
def padding(databin, len_):
"""
Assumption:
One nlri per update/withdraw message, so we can add
a padding to the prefix without worrying about its length
"""
if len(databin) >= len_:
return databin
return databin + b'\0' * (len_ - len(databin))
def dissect_nlri(nlri_data, afi, safi):
"""
Exract nlri information based on the address family
"""
addr_family = AddressFamily(afi, safi)
if addr_family == AF.IPv6_VPN:
return NlriIPv6Vpn.parse(nlri_data)
elif addr_family == AF.IPv4_VPN:
return NlriIPv4Vpn.parse(nlri_data)
elif addr_family == AF.IPv6_UNICAST:
return NlriIPv6Unicast.parse(nlri_data)
return {'ip_prefix': 'Unknown'}
#------------------------------------------------------------------------------
class NlriIPv4Unicast:
@staticmethod
def parse(data):
"""parses prefixes from withdrawn_routes or nrli data"""
(prefix_len,) = struct.unpack_from('!B', data)
prefix = padding(data[1:], 4)
return {'ip_prefix': f'{ipaddress.IPv4Address(prefix)}/{prefix_len}'}
#------------------------------------------------------------------------------
class NlriIPv6Unicast:
@staticmethod
def parse(data):
"""parses prefixes from withdrawn_routes or nrli data"""
(prefix_len,) = struct.unpack_from('!B', data)
prefix = padding(data[1:], 16)
return {'ip_prefix': f'{ipaddress.IPv6Address(prefix)}/{prefix_len}'}
#------------------------------------------------------------------------------
class NlriIPv4Vpn:
UNPACK_STR = '!B3s8s'
@classmethod
def parse(cls, data):
(bit_len, label, rd) = struct.unpack_from(cls.UNPACK_STR, data)
offset = struct.calcsize(cls.UNPACK_STR)
ipv4 = padding(data[offset:], 4)
# prefix_len = total_bits_len - label_bits_len - rd_bits_len
prefix_len = bit_len - 3*8 - 8*8
return {
'label': decode_label(label),
'rd': str(RouteDistinguisher(rd)),
'ip_prefix': f'{ipaddress.IPv4Address(ipv4)}/{prefix_len}',
}
#------------------------------------------------------------------------------
class NlriIPv6Vpn:
UNPACK_STR = '!B3s8s'
@classmethod
def parse(cls, data):
# rfc 3107, 8227
(bit_len, label, rd) = struct.unpack_from(cls.UNPACK_STR, data)
offset = struct.calcsize(cls.UNPACK_STR)
ipv6 = padding(data[offset:], 16)
prefix_len = bit_len - 3*8 - 8*8
return {
'label': decode_label(label),
'rd': str(RouteDistinguisher(rd)),
'ip_prefix': f'{ipaddress.IPv6Address(ipv6)}/{prefix_len}',
}
#------------------------------------------------------------------------------
class NlriIPv4Mpls:
pass
#------------------------------------------------------------------------------
class NlriIPv6Mpls:
pass
#------------------------------------------------------------------------------
class NlriIPv4FlowSpec:
pass
#------------------------------------------------------------------------------
class NlriIPv6FlowSpec:
pass
#------------------------------------------------------------------------------
class NlriVpn4FlowSpec:
pass
#------------------------------------------------------------------------------
class NlriVpn6FlowSpec:
pass
#------------------------------------------------------------------------------
class NlriL2EVPN:
pass
#------------------------------------------------------------------------------
class NlriL2VPNFlowSpec:
pass

View File

@ -0,0 +1,304 @@
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
import struct
import ipaddress
from . import nlri as NLRI
from .af import AddressFamily, AF
from .rd import RouteDistinguisher
PATH_ATTR_FLAG_OPTIONAL = 1 << 7
PATH_ATTR_FLAG_TRANSITIVE = 1 << 6
PATH_ATTR_FLAG_PARTIAL = 1 << 5
PATH_ATTR_FLAG_EXTENDED_LENGTH = 1 << 4
PATH_ATTR_TYPE_ORIGIN = 1
PATH_ATTR_TYPE_AS_PATH = 2
PATH_ATTR_TYPE_NEXT_HOP = 3
PATH_ATTR_TYPE_MULTI_EXIT_DISC = 4
PATH_ATTR_TYPE_LOCAL_PREF = 5
PATH_ATTR_TYPE_ATOMIC_AGGREGATE = 6
PATH_ATTR_TYPE_AGGREGATOR = 7
PATH_ATTR_TYPE_COMMUNITIES = 8
PATH_ATTR_TYPE_ORIGINATOR_ID = 9
PATH_ATTR_TYPE_CLUSTER_LIST = 10
PATH_ATTR_TYPE_MP_REACH_NLRI = 14
PATH_ATTR_TYPE_MP_UNREACH_NLRI = 15
PATH_ATTR_TYPE_EXTENDED_COMMUNITIES = 16
PATH_ATTR_TYPE_AS4_PATH = 17
PATH_ATTR_TYPE_AS4_AGGREGATOR = 18
PATH_ATTR_TYEP_PMSI_TUNNEL_ATTRIBUTE = 22
ORIGIN_IGP = 0x00
ORIGIN_EGP = 0x01
ORIGIN_INCOMPLETE = 0x02
#------------------------------------------------------------------------------
class PathAttribute:
PATH_ATTRS = {}
UNKNOWN_ATTR = None
UNPACK_STR = '!BB'
@classmethod
def register_path_attr(cls, path_attr):
def _register_path_attr(subcls):
cls.PATH_ATTRS[path_attr] = subcls
return subcls
return _register_path_attr
@classmethod
def lookup_path_attr(cls, type_code):
return cls.PATH_ATTRS.get(type_code, cls.UNKNOWN_ATTR)
@classmethod
def dissect(cls, data):
flags, type_code = struct.unpack_from(cls.UNPACK_STR, data)
offset = struct.calcsize(cls.UNPACK_STR)
# get attribute length
attr_len_str = '!H' if (flags & PATH_ATTR_FLAG_EXTENDED_LENGTH) else '!B'
(attr_len,) = struct.unpack_from(attr_len_str, data[offset:])
offset += struct.calcsize(attr_len_str)
path_attr_cls = cls.lookup_path_attr(type_code)
if path_attr_cls == cls.UNKNOWN_ATTR:
return data[offset + attr_len:], None
return data[offset+attr_len:], path_attr_cls.dissect(data[offset:offset+attr_len])
#------------------------------------------------------------------------------
@PathAttribute.register_path_attr(PATH_ATTR_TYPE_ORIGIN)
class PathAttrOrigin:
ORIGIN_STR = {
ORIGIN_IGP: 'IGP',
ORIGIN_EGP: 'EGP',
ORIGIN_INCOMPLETE: 'INCOMPLETE',
}
@classmethod
def dissect(cls, data):
(origin,) = struct.unpack_from('!B', data)
return {'origin': cls.ORIGIN_STR.get(origin, 'UNKNOWN')}
#------------------------------------------------------------------------------
@PathAttribute.register_path_attr(PATH_ATTR_TYPE_AS_PATH)
class PathAttrAsPath:
AS_PATH_TYPE_SET = 0x01
AS_PATH_TYPE_SEQUENCE= 0x02
@staticmethod
def get_asn_len(asns):
"""XXX: Add this nightmare to determine the ASN length"""
pass
@classmethod
def dissect(cls, data):
(_type, _len) = struct.unpack_from('!BB', data)
data = data[2:]
_type_str = 'Ordred' if _type == cls.AS_PATH_TYPE_SEQUENCE else 'Raw'
segment = []
while data:
(asn,) = struct.unpack_from('!I', data)
segment.append(asn)
data = data[4:]
return {'as_path': ' '.join(str(a) for a in segment)}
#------------------------------------------------------------------------------
@PathAttribute.register_path_attr(PATH_ATTR_TYPE_NEXT_HOP)
class PathAttrNextHop:
@classmethod
def dissect(cls, data):
(nexthop,) = struct.unpack_from('!4s', data)
return {'bgp_nexthop': str(ipaddress.IPv4Address(nexthop))}
#------------------------------------------------------------------------------
class PathAttrMultiExitDisc:
pass
#------------------------------------------------------------------------------
@PathAttribute.register_path_attr(PATH_ATTR_TYPE_MP_REACH_NLRI)
class PathAttrMpReachNLRI:
"""
+---------------------------------------------------------+
| Address Family Identifier (2 octets) |
+---------------------------------------------------------+
| Subsequent Address Family Identifier (1 octet) |
+---------------------------------------------------------+
| Length of Next Hop Network Address (1 octet) |
+---------------------------------------------------------+
| Network Address of Next Hop (variable) |
+---------------------------------------------------------+
| Number of SNPAs (1 octet) |
+---------------------------------------------------------+
| Length of first SNPA(1 octet) |
+---------------------------------------------------------+
| First SNPA (variable) |
+---------------------------------------------------------+
| Length of second SNPA (1 octet) |
+---------------------------------------------------------+
| Second SNPA (variable) |
+---------------------------------------------------------+
| ... |
+---------------------------------------------------------+
| Length of Last SNPA (1 octet) |
+---------------------------------------------------------+
| Last SNPA (variable) |
+---------------------------------------------------------+
| Network Layer Reachability Information (variable) |
+---------------------------------------------------------+
"""
UNPACK_STR = '!HBB'
NLRI_RESERVED_LEN = 1
@staticmethod
def dissect_nexthop(nexthop_data, nexthop_len):
msg = {}
if nexthop_len == 4:
# IPv4
(ipv4,) = struct.unpack_from('!4s', nexthop_data)
msg['nxhp_ip'] = str(ipaddress.IPv4Address(ipv4))
elif nexthop_len == 12:
# RD + IPv4
(rd, ipv4) = struct.unpack_from('!8s4s', nexthop_data)
msg['nxhp_ip'] = str(ipaddress.IPv4Address(ipv4))
msg['nxhp_rd'] = str(RouteDistinguisher(rd))
elif nexthop_len == 16:
# IPv6
(ipv6,) = struct.unpack_from('!16s', nexthop_data)
msg['nxhp_ip'] = str(ipaddress.IPv6Address(ipv6))
elif nexthop_len == 24:
# RD + IPv6
(rd, ipv6) = struct.unpack_from('!8s16s', nexthop_data)
msg['nxhp_ip'] = str(ipaddress.IPv6Address(ipv6))
msg['nxhp_rd'] = str(RouteDistinguisher(rd))
elif nexthop_len == 32:
# IPv6 + IPv6 link-local
(ipv6, link_local)= struct.unpack_from('!16s16s', nexthop_data)
msg['nxhp_ip'] = str(ipaddress.IPv6Address(ipv6))
msg['nxhp_link-local'] = str(ipaddress.IPv6Address(link_local))
elif nexthop_len == 48:
# RD + IPv6 + RD + IPv6 link-local
u_str = '!8s16s8s16s'
(rd1, ipv6, rd2, link_local)= struct.unpack_from(u_str, nexthop_data)
msg['nxhp_rd1'] = str(RouteDistinguisher(rd1))
msg['nxhp_ip'] = str(ipaddress.IPv6Address(ipv6))
msg['nxhp_rd2'] = str(RouteDistinguisher(rd2))
msg['nxhp_link-local'] = str(ipaddress.IPv6Address(link_local))
return msg
@staticmethod
def dissect_snpa(snpa_data):
pass
@classmethod
def dissect(cls, data):
(afi, safi, nexthop_len) = struct.unpack_from(cls.UNPACK_STR, data)
offset = struct.calcsize(cls.UNPACK_STR)
msg = {'afi': afi, 'safi': safi}
# dissect nexthop
nexthop_data = data[offset: offset + nexthop_len]
nexthop = cls.dissect_nexthop(nexthop_data, nexthop_len)
msg.update(nexthop)
offset += nexthop_len
# dissect snpa or just reserved
offset += 1
# dissect nlri
nlri = NLRI.dissect_nlri(data[offset:], afi, safi)
msg.update(nlri)
return msg
#------------------------------------------------------------------------------
@PathAttribute.register_path_attr(PATH_ATTR_TYPE_MP_UNREACH_NLRI)
class PathAttrMpUnReachNLRI:
"""
+---------------------------------------------------------+
| Address Family Identifier (2 bytes) |
+---------------------------------------------------------+
| Subsequent Address Family Identifier (1 byte) |
+---------------------------------------------------------+
| Withdrawn Routes (variable) |
+---------------------------------------------------------+
"""
UNPACK_STR = '!HB'
@classmethod
def dissect(cls, data):
(afi, safi) = struct.unpack_from(cls.UNPACK_STR, data)
offset = struct.calcsize(cls.UNPACK_STR)
msg = {'bmp_log_type': 'withdraw','afi': afi, 'safi': safi}
if data[offset:]:
# dissect withdrawn_routes
msg.update(NLRI.dissect_nlri(data[offset:], afi, safi))
return msg
#------------------------------------------------------------------------------
class PathAttrLocalPref:
pass
#------------------------------------------------------------------------------
class PathAttrAtomicAgregate:
pass
#------------------------------------------------------------------------------
class PathAttrAggregator:
pass
#------------------------------------------------------------------------------
class PathAttrCommunities:
pass
#------------------------------------------------------------------------------
class PathAttrOriginatorID:
pass
#------------------------------------------------------------------------------
class PathAttrClusterList:
pass
#------------------------------------------------------------------------------
class PathAttrExtendedCommunities:
pass
#------------------------------------------------------------------------------
class PathAttrPMSITunnel:
pass
#------------------------------------------------------------------------------
class PathAttrLinkState:
pass
#------------------------------------------------------------------------------
class PathAttrLargeCommunities:
pass

View File

@ -0,0 +1,59 @@
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
import ipaddress
import struct
#------------------------------------------------------------------------------
class RouteDistinguisher:
"""
type 0:
+---------------------------------------------------------------------+
+ type=0 (2 bytes)| Administrator subfield | Assigned number subfiled |
+ | AS number (2 bytes) | Service Provider 4 bytes)|
+---------------------------------------------------------------------+
type 1:
+---------------------------------------------------------------------+
+ type=1 (2 bytes)| Administrator subfield | Assigned number subfiled |
+ | IPv4 (4 bytes) | Service Provider 2 bytes)|
+---------------------------------------------------------------------+
type 2:
+-------------------------------------------------------------------------+
+ type=2 (2 bytes)| Administrator subfield | Assigned number subfiled |
+ | 4-bytes AS number (4 bytes)| Service Provider 2 bytes)|
+-------------------------------------------------------------------------+
"""
def __init__(self, rd):
self.rd = rd
self.as_number = None
self.admin_ipv4 = None
self.four_bytes_as = None
self.assigned_sp = None
self.repr_str = ''
self.dissect()
def dissect(self):
(rd_type,) = struct.unpack_from('!H', self.rd)
if rd_type == 0:
(self.as_number,
self.assigned_sp) = struct.unpack_from('!HI', self.rd[2:])
self.repr_str = f'{self.as_number}:{self.assigned_sp}'
elif rd_type == 1:
(self.admin_ipv4,
self.assigned_sp) = struct.unpack_from('!IH', self.rd[2:])
ipv4 = str(ipaddress.IPv4Address(self.admin_ipv4))
self.repr_str = f'{self.as_number}:{self.assigned_sp}'
elif rd_type == 2:
(self.four_bytes_as,
self.assigned_sp) = struct.unpack_from('!IH', self.rd[2:])
self.repr_str = f'{self.four_bytes_as}:{self.assigned_sp}'
def __str__(self):
return self.repr_str

View File

@ -0,0 +1,420 @@
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
"""
BMP main module:
- dissect monitoring messages in the way to get updated/withdrawed prefixes
- XXX: missing RFCs references
- XXX: more bmp messages types to dissect
- XXX: complete bgp message dissection
"""
import datetime
import ipaddress
import json
import os
import struct
from bgp.update import BGPUpdate
from bgp.update.rd import RouteDistinguisher
SEQ = 0
LOG_DIR = "/var/log/"
LOG_FILE = "/var/log/bmp.log"
IS_ADJ_RIB_OUT = 1 << 4
IS_AS_PATH = 1 << 5
IS_POST_POLICY = 1 << 6
IS_IPV6 = 1 << 7
IS_FILTERED = 1 << 7
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
def bin2str_ipaddress(ip_bytes, is_ipv6=False):
if is_ipv6:
return str(ipaddress.IPv6Address(ip_bytes))
return str(ipaddress.IPv4Address(ip_bytes[-4:]))
def log2file(logs):
"""
XXX: extract the useful information and save it in a flat dictionnary
"""
with open(LOG_FILE, 'a') as f:
f.write(json.dumps(logs) + "\n")
#------------------------------------------------------------------------------
class BMPCodes:
"""
XXX: complete the list, provide RFCs.
"""
VERSION = 0x3
BMP_MSG_TYPE_ROUTE_MONITORING = 0x00
BMP_MSG_TYPE_STATISTICS_REPORT = 0x01
BMP_MSG_TYPE_PEER_DOWN_NOTIFICATION = 0x02
BMP_MSG_TYPE_PEER_UP_NOTIFICATION = 0x03
BMP_MSG_TYPE_INITIATION = 0x04
BMP_MSG_TYPE_TERMINATION = 0x05
BMP_MSG_TYPE_ROUTE_MIRRORING = 0x06
BMP_MSG_TYPE_ROUTE_POLICY = 0x64
# initiation message types
BMP_INIT_INFO_STRING = 0x00
BMP_INIT_SYSTEM_DESCRIPTION = 0x01
BMP_INIT_SYSTEM_NAME = 0x02
BMP_INIT_VRF_TABLE_NAME = 0x03
BMP_INIT_ADMIN_LABEL = 0x04
# peer types
BMP_PEER_GLOBAL_INSTANCE = 0x00
BMP_PEER_RD_INSTANCE = 0x01
BMP_PEER_LOCAL_INSTANCE = 0x02
BMP_PEER_LOC_RIB_INSTANCE = 0x03
# peer header flags
BMP_PEER_FLAG_IPV6 = 0x80
BMP_PEER_FLAG_POST_POLICY = 0x40
BMP_PEER_FLAG_AS_PATH = 0x20
BMP_PEER_FLAG_ADJ_RIB_OUT = 0x10
# peer loc-rib flag
BMP_PEER_FLAG_LOC_RIB = 0x80
BMP_PEER_FLAG_LOC_RIB_RES = 0x7F
# statistics type
BMP_STAT_PREFIX_REJ = 0x00
BMP_STAT_PREFIX_DUP = 0x01
BMP_STAT_WITHDRAW_DUP = 0x02
BMP_STAT_CLUSTER_LOOP = 0x03
BMP_STAT_AS_LOOP = 0x04
BMP_STAT_INV_ORIGINATOR = 0x05
BMP_STAT_AS_CONFED_LOOP = 0x06
BMP_STAT_ROUTES_ADJ_RIB_IN = 0x07
BMP_STAT_ROUTES_LOC_RIB = 0x08
BMP_STAT_ROUTES_PER_ADJ_RIB_IN = 0x09
BMP_STAT_ROUTES_PER_LOC_RIB = 0x0A
BMP_STAT_UPDATE_TREAT = 0x0B
BMP_STAT_PREFIXES_TREAT = 0x0C
BMP_STAT_DUPLICATE_UPDATE = 0x0D
BMP_STAT_ROUTES_PRE_ADJ_RIB_OUT = 0x0E
BMP_STAT_ROUTES_POST_ADJ_RIB_OUT = 0x0F
BMP_STAT_ROUTES_PRE_PER_ADJ_RIB_OUT = 0x10
BMP_STAT_ROUTES_POST_PER_ADJ_RIB_OUT = 0x11
# peer down reason code
BMP_PEER_DOWN_LOCAL_NOTIFY = 0x01
BMP_PEER_DOWN_LOCAL_NO_NOTIFY = 0X02
BMP_PEER_DOWN_REMOTE_NOTIFY = 0X03
BMP_PEER_DOWN_REMOTE_NO_NOTIFY = 0X04
BMP_PEER_DOWN_INFO_NO_LONGER = 0x05
BMP_PEER_DOWN_SYSTEM_CLOSED = 0X06
# termincation message types
BMP_TERM_TYPE_STRING = 0x00
BMP_TERM_TYPE_REASON = 0X01
# termination reason code
BMP_TERM_REASON_ADMIN_CLOSE = 0x00
BMP_TERM_REASON_UNSPECIFIED = 0x01
BMP_TERM_REASON_RESOURCES = 0x02
BMP_TERM_REASON_REDUNDANT = 0x03
BMP_TERM_REASON_PERM_CLOSE = 0x04
# policy route tlv
BMP_ROUTE_POLICY_TLV_VRF = 0x00
BMP_ROUTE_POLICY_TLV_POLICY= 0x01
BMP_ROUTE_POLICY_TLV_PRE_POLICY = 0x02
BMP_ROUTE_POLICY_TLV_POST_POLICY = 0x03
BMP_ROUTE_POLICY_TLV_STRING = 0x04
#------------------------------------------------------------------------------
class BMPMsg:
"""
XXX: should we move register_msg_type and look_msg_type
to generic Type class.
"""
TYPES = {}
UNKNOWN_TYPE = None
HDR_STR = '!BIB'
MIN_LEN = struct.calcsize(HDR_STR)
TYPES_STR = {
BMPCodes.BMP_MSG_TYPE_INITIATION: 'initiation',
BMPCodes.BMP_MSG_TYPE_PEER_DOWN_NOTIFICATION: 'peer down notification',
BMPCodes.BMP_MSG_TYPE_PEER_UP_NOTIFICATION: 'peer up notification',
BMPCodes.BMP_MSG_TYPE_ROUTE_MONITORING: 'route monitoring',
BMPCodes.BMP_MSG_TYPE_STATISTICS_REPORT: 'statistics report',
BMPCodes.BMP_MSG_TYPE_TERMINATION: 'termination',
BMPCodes.BMP_MSG_TYPE_ROUTE_MIRRORING: 'route mirroring',
BMPCodes.BMP_MSG_TYPE_ROUTE_POLICY: 'route policy',
}
@classmethod
def register_msg_type(cls, msgtype):
def _register_type(subcls):
cls.TYPES[msgtype] = subcls
return subcls
return _register_type
@classmethod
def lookup_msg_type(cls, msgtype):
return cls.TYPES.get(msgtype, cls.UNKNOWN_TYPE)
@classmethod
def dissect_header(cls, data):
"""
0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Message Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Message Type |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"""
if len(data) < cls.MIN_LEN:
pass
else:
_version, _len, _type = struct.unpack(cls.HDR_STR, data[0:cls.MIN_LEN])
return _version, _len, _type
@classmethod
def dissect(cls, data):
global SEQ
version, msglen, msgtype = cls.dissect_header(data)
msg_data = data[cls.MIN_LEN:msglen]
data = data[msglen:]
if version != BMPCodes.VERSION:
# XXX: log something
return data
msg_cls = cls.lookup_msg_type(msgtype)
if msg_cls == cls.UNKNOWN_TYPE:
# XXX: log something
return data
msg_cls.MSG_LEN = msglen - cls.MIN_LEN
logs = msg_cls.dissect(msg_data)
logs["seq"] = SEQ
log2file(logs)
SEQ += 1
return data
#------------------------------------------------------------------------------
class BMPPerPeerMessage:
"""
0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Peer Type | Peer Flags |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Peer Address (16 bytes) |
~ ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Peer AS |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Peer BGP ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Timestamp (seconds) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Timestamp (microseconds) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"""
PEER_UNPACK_STR = '!BB8s16sI4sII'
PEER_TYPE_STR = {
BMPCodes.BMP_PEER_GLOBAL_INSTANCE: 'global instance',
BMPCodes.BMP_PEER_RD_INSTANCE: 'route distinguisher instance',
BMPCodes.BMP_PEER_LOCAL_INSTANCE: 'local instance',
BMPCodes.BMP_PEER_LOC_RIB_INSTANCE: 'loc-rib instance',
}
@classmethod
def dissect(cls, data):
(peer_type,
peer_flags,
peer_distinguisher,
peer_address,
peer_asn,
peer_bgp_id,
timestamp_secs,
timestamp_microsecs) = struct.unpack_from(cls.PEER_UNPACK_STR, data)
msg = {'peer_type': cls.PEER_TYPE_STR[peer_type]}
if peer_type == 0x03:
msg['is_filtered'] = bool(peer_flags & IS_FILTERED)
else:
# peer_flags = 0x0000 0000
# ipv6, post-policy, as-path, adj-rib-out, reserverdx4
is_adj_rib_out = bool(peer_flags & IS_ADJ_RIB_OUT)
is_as_path = bool(peer_flags & IS_AS_PATH)
is_post_policy = bool(peer_flags & IS_POST_POLICY)
is_ipv6 = bool(peer_flags & IS_IPV6)
msg['post_policy'] = is_post_policy
msg['ipv6'] = is_ipv6
msg['peer_ip'] = bin2str_ipaddress(peer_address, is_ipv6)
peer_bgp_id = bin2str_ipaddress(peer_bgp_id)
timestamp = float(timestamp_secs) + timestamp_microsecs * (10 ** -6)
data = data[struct.calcsize(cls.PEER_UNPACK_STR):]
msg.update({
'peer_distinguisher': str(RouteDistinguisher(peer_distinguisher)),
'peer_asn': peer_asn,
'peer_bgp_id': peer_bgp_id,
'timestamp': str(datetime.datetime.fromtimestamp(timestamp)),
})
return data, msg
#------------------------------------------------------------------------------
@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_ROUTE_MONITORING)
class BMPRouteMonitoring(BMPPerPeerMessage):
@classmethod
def dissect(cls, data):
data, peer_msg = super().dissect(data)
data, update_msg = BGPUpdate.dissect(data)
return {**peer_msg, **update_msg}
#------------------------------------------------------------------------------
class BMPStatisticsReport:
"""
0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stats Count |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stat Type | Stat Len |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stat Data |
~ ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"""
pass
#------------------------------------------------------------------------------
class BMPPeerDownNotification:
"""
0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reason |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data (present if Reason = 1, 2 or 3) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"""
pass
#------------------------------------------------------------------------------
@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_PEER_UP_NOTIFICATION)
class BMPPeerUpNotification(BMPPerPeerMessage):
"""
0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Local Address (16 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Local Port | Remote Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sent OPEN Message #|
~ ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Received OPEN Message |
~ ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"""
UNPACK_STR = '!16sHH'
MIN_LEN = struct.calcsize(UNPACK_STR)
MSG_LEN = None
@classmethod
def dissect(cls, data):
data, peer_msg = super().dissect(data)
(local_addr,
local_port,
remote_port) = struct.unpack_from(cls.UNPACK_STR, data)
msg = {
**peer_msg,
**{
'local_ip': bin2str_ipaddress(local_addr, peer_msg.get('ipv6')),
'local_port': int(local_port),
'remote_port': int(remote_port),
},
}
# XXX: dissect the bgp open message
return msg
#------------------------------------------------------------------------------
@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_INITIATION)
class BMPInitiation:
"""
0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Information Type | Information Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Information (variable) |
~ ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"""
TLV_STR = '!HH'
MIN_LEN = struct.calcsize(TLV_STR)
FIELD_TO_STR = {
BMPCodes.BMP_INIT_INFO_STRING: 'information',
BMPCodes.BMP_INIT_ADMIN_LABEL: 'admin_label',
BMPCodes.BMP_INIT_SYSTEM_DESCRIPTION: 'system_description',
BMPCodes.BMP_INIT_SYSTEM_NAME: 'system_name',
BMPCodes.BMP_INIT_VRF_TABLE_NAME: 'vrf_table_name',
}
@classmethod
def dissect(cls, data):
msg = {}
while len(data) > cls.MIN_LEN:
_type, _len = struct.unpack_from(cls.TLV_STR, data[0:cls.MIN_LEN])
_value = data[cls.MIN_LEN: cls.MIN_LEN + _len].decode()
msg[cls.FIELD_TO_STR[_type]] = _value
data = data[cls.MIN_LEN + _len:]
return msg
#------------------------------------------------------------------------------
class BMPTermination:
"""
0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Information Type | Information Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Information (variable) |
~ ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"""
pass
#------------------------------------------------------------------------------
class BMPRouteMirroring:
pass
#------------------------------------------------------------------------------
class BMPRoutePolicy:
pass

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: ISC
# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
import argparse
# XXX: something more reliable should be used "Twisted" a great choice.
import socket
import sys
from bmp import BMPMsg
BGP_MAX_SIZE = 4096
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--address", type=str, default="0.0.0.0")
parser.add_argument("-p", "--port", type=int, default=1789)
def main():
args = parser.parse_args()
ADDRESS, PORT = args.address, args.port
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((ADDRESS, PORT))
s.listen()
connection, _ = s.accept()
try:
while True:
data = connection.recv(BGP_MAX_SIZE)
while len(data) > BMPMsg.MIN_LEN:
data = BMPMsg.dissect(data)
except Exception as e:
# XXX: do something
pass
except KeyboardInterrupt:
# XXX: do something
pass
finally:
connection.close()
if __name__ == "__main__":
sys.exit(main())

View File

@ -363,6 +363,15 @@ class Topogen(object):
self.peern += 1
return self.gears[name]
def add_bmp_server(self, name, ip, defaultRoute, port=1789):
"""Add the bmp collector gear"""
if name in self.gears:
raise KeyError("The bmp server already exists")
self.gears[name] = TopoBMPCollector(
self, name, ip=ip, defaultRoute=defaultRoute, port=port
)
def add_link(self, node1, node2, ifname1=None, ifname2=None):
"""
Creates a connection between node1 and node2. The nodes can be the
@ -425,6 +434,13 @@ class Topogen(object):
"""
return self.get_gears(TopoExaBGP)
def get_bmp_servers(self):
"""
Retruns the bmp servers dictionnary (the key is the bmp server the
value is the bmp server object itself).
"""
return self.get_gears(TopoBMPCollector)
def start_topology(self):
"""Starts the topology class."""
logger.info("starting topology: {}".format(self.modname))
@ -1204,6 +1220,33 @@ class TopoExaBGP(TopoHost):
return ""
class TopoBMPCollector(TopoHost):
PRIVATE_DIRS = [
"/var/log",
]
def __init__(self, tgen, name, **params):
params["private_mounts"] = self.PRIVATE_DIRS
self.port = params["port"]
self.ip = params["ip"]
super(TopoBMPCollector, self).__init__(tgen, name, **params)
def __str__(self):
gear = super(TopoBMPCollector, self).__str__()
gear += " TopoBMPCollector<>".format()
return gear
def start(self):
self.run(
"{}/bmp_collector/bmpserver -a {} -p {}&".format(CWD, self.ip, self.port),
stdout=None,
)
def stop(self):
self.run("pkill -9 -f bmpserver")
return ""
#
# Diagnostic function
#