Skip to content

Commit

Permalink
Merge pull request #1234 from jcpvdm/mvpn
Browse files Browse the repository at this point in the history
Implement RFC6514 MCAST-VPN (incomplete)
  • Loading branch information
thomas-mangin authored Sep 6, 2024
2 parents 11f0888 + 279a289 commit 82b21cd
Show file tree
Hide file tree
Showing 20 changed files with 924 additions and 7 deletions.
21 changes: 21 additions & 0 deletions etc/exabgp/api-mvpn.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
process mvpn {
run ./run/api-mvpn.run;
encoder json;
}

neighbor 127.0.0.1 {
router-id 32.32.32.32;
local-address 127.0.0.1;
local-as 65000;
peer-as 65000;
group-updates false;
auto-flush true;

family {
ipv4 mcast-vpn;
ipv6 mcast-vpn;
}
api {
processes [ mvpn ];
}
}
26 changes: 26 additions & 0 deletions etc/exabgp/conf-mvpn.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
neighbor 127.0.0.1 {
router-id 32.32.32.32;
local-address 127.0.0.1;
local-as 65000;
peer-as 65000;
group-updates true;
auto-flush true;

family {
ipv4 mcast-vpn;
ipv6 mcast-vpn;
}

announce {
ipv4 {
mcast-vpn shared-join rp 10.99.199.1 group 239.251.255.228 rd 65000:99999 source-as 65000 next-hop 10.10.6.3 extended-community [ target:192.168.94.12:5 ];
mcast-vpn source-join source 10.99.12.2 group 239.251.255.228 rd 65000:99999 source-as 65000 next-hop 10.10.6.3 extended-community [ target:192.168.94.12:5 ];
mcast-vpn source-ad source 10.99.12.4 group 239.251.255.228 rd 65000:99999 next-hop 10.10.6.4 extended-community [ target:65000:99999 ];
}
ipv6 {
mcast-vpn shared-join rp fd00::1 group ff0e::1 rd 65000:99999 source-as 65000 next-hop 10.10.6.3 extended-community [ target:192.168.94.12:5 ];
mcast-vpn source-join source fd12::2 group ff0e::1 rd 65000:99999 source-as 65000 next-hop 10.10.6.3 extended-community [ target:192.168.94.12:5 ];
mcast-vpn source-ad source fd12::4 group ff0e::1 rd 65000:99999 next-hop 10.10.6.4 extended-community [ target:65000:99999 ];
}
}
}
39 changes: 39 additions & 0 deletions etc/exabgp/run/api-mvpn.run
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/python3

import os
import sys
import time

time.sleep(2) # let the EOR pass


routes = [
'ipv4 mcast-vpn shared-join rp 10.99.199.1 group 239.251.255.228 rd 65000:99999 source-as 65000 next-hop 10.10.6.3 extended-community [ target:192.168.94.12:5 ]',
'ipv4 mcast-vpn source-join source 10.99.12.2 group 239.251.255.228 rd 65000:99999 source-as 65000 next-hop 10.10.6.3 extended-community [ target:192.168.94.12:5 ]',
'ipv6 mcast-vpn shared-join rp fd00::1 group ff0e::1 rd 65000:99999 source-as 65000 next-hop 10.10.6.3 extended-community [ target:192.168.94.12:5 ]',
'ipv6 mcast-vpn source-join source fd12::2 group ff0e::1 rd 65000:99999 source-as 65000 next-hop 10.10.6.3 extended-community [ target:192.168.94.12:5 ]',
'ipv6 mcast-vpn source-ad source fd12::4 group ff0e::1 rd 65000:99999 next-hop 10.10.6.4 extended-community [ target:65000:99999 ]',
'ipv4 mcast-vpn source-ad source 10.99.12.4 group 239.251.255.228 rd 65000:99999 next-hop 10.10.6.4 extended-community [ target:65000:99999 ]',
]

for r in routes:
sys.stdout.write('announce ' + r + '\n')
sys.stdout.flush()
time.sleep(0.3)

time.sleep(5)

for r in routes:
sys.stdout.write('withdraw ' + r + '\n')
sys.stdout.flush()
time.sleep(0.3)

try:
now = time.time()
while os.getppid() != 1 and time.time() < now + 15:
line = sys.stdin.readline().strip()
if not line or 'shutdown' in line:
break
time.sleep(1)
except IOError:
pass
1 change: 1 addition & 0 deletions qa/encoding/api-mvpn.ci
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
api-mvpn.conf
16 changes: 16 additions & 0 deletions qa/encoding/api-mvpn.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:001E:02:00000007900F0003000105
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:001E:02:00000007900F0003000205

1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:005B:02:00000044400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800E21000105040A0A06030006160000FDE80001869F0000FDE8200A63C70120EFFBFFE4
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:005B:02:00000044400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800E21000105040A0A06030007160000FDE80001869F0000FDE8200A630C0220EFFBFFE4
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0073:02:0000005C400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800E39000205040A0A060300062E0000FDE80001869F0000FDE880FD00000000000000000000000000000180FF0E0000000000000000000000000001
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0073:02:0000005C400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800E39000205040A0A060300072E0000FDE80001869F0000FDE880FD12000000000000000000000000000280FF0E0000000000000000000000000001
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:006F:02:00000058400101004002004003040A0A060440050400000064C010080002FDE80001869F800E35000205040A0A060400052A0000FDE80001869F80FD12000000000000000000000000000480FF0E0000000000000000000000000001
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0057:02:00000040400101004002004003040A0A060440050400000064C010080002FDE80001869F800E1D000105040A0A06040005120000FDE80001869F200A630C0420EFFBFFE4

1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0055:02:0000003E400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800F1B00010506160000FDE80001869F0000FDE8200A63C70120EFFBFFE4
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0055:02:0000003E400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800F1B00010507160000FDE80001869F0000FDE8200A630C0220EFFBFFE4
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:006D:02:00000056400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800F33000205062E0000FDE80001869F0000FDE880FD00000000000000000000000000000180FF0E0000000000000000000000000001
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:006D:02:00000056400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800F33000205072E0000FDE80001869F0000FDE880FD12000000000000000000000000000280FF0E0000000000000000000000000001
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0069:02:00000052400101004002004003040A0A060440050400000064C010080002FDE80001869F800F2F000205052A0000FDE80001869F80FD12000000000000000000000000000480FF0E0000000000000000000000000001
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0051:02:0000003A400101004002004003040A0A060440050400000064C010080002FDE80001869F800F1700010505120000FDE80001869F200A630C0420EFFBFFE4
1 change: 1 addition & 0 deletions qa/encoding/conf-mvpn.ci
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
conf-mvpn.conf
4 changes: 4 additions & 0 deletions qa/encoding/conf-mvpn.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0073:02:0000005C400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800E39000105040A0A06030006160000FDE80001869F0000FDE8200A63C70120EFFBFFE407160000FDE80001869F0000FDE8200A630C0220EFFBFFE4
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:00A3:02:0000008C400101004002004003040A0A060340050400000064C010080102C0A85E0C0005800E69000205040A0A060300062E0000FDE80001869F0000FDE880FD00000000000000000000000000000180FF0E0000000000000000000000000001072E0000FDE80001869F0000FDE880FD12000000000000000000000000000280FF0E0000000000000000000000000001
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0057:02:00000040400101004002004003040A0A060440050400000064C010080002FDE80001869F800E1D000105040A0A06040005120000FDE80001869F200A630C0420EFFBFFE4
1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:006F:02:00000058400101004002004003040A0A060440050400000064C010080002FDE80001869F800E35000205040A0A060400052A0000FDE80001869F80FD12000000000000000000000000000480FF0E0000000000000000000000000001
1 change: 1 addition & 0 deletions src/exabgp/bgp/message/update/nlri/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@
from exabgp.bgp.message.update.nlri.rtc import RTC
from exabgp.bgp.message.update.nlri.bgpls import BGPLS
from exabgp.bgp.message.update.nlri.mup import MUP
from exabgp.bgp.message.update.nlri.mvpn import MVPN
10 changes: 10 additions & 0 deletions src/exabgp/bgp/message/update/nlri/mvpn/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Every MVPN should be imported from this file
# as it makes sure that all the registering decorator are run

# flake8: noqa: F401,E261

from exabgp.bgp.message.update.nlri.mvpn.nlri import MVPN

from exabgp.bgp.message.update.nlri.mvpn.sourcead import SourceAD
from exabgp.bgp.message.update.nlri.mvpn.sourcejoin import SourceJoin
from exabgp.bgp.message.update.nlri.mvpn.sharedjoin import SharedJoin
108 changes: 108 additions & 0 deletions src/exabgp/bgp/message/update/nlri/mvpn/nlri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from struct import pack

from exabgp.protocol.family import AFI
from exabgp.protocol.family import SAFI

from exabgp.bgp.message import Action

from exabgp.bgp.message.update.nlri import NLRI

# https://datatracker.ietf.org/doc/html/rfc6514

# +-----------------------------------+
# | Route Type (1 octet) |
# +-----------------------------------+
# | Length (1 octet) |
# +-----------------------------------+
# | Route Type specific (variable) |
# +-----------------------------------+

# ========================================================================= MVPN


@NLRI.register(AFI.ipv4, SAFI.mcast_vpn)
@NLRI.register(AFI.ipv6, SAFI.mcast_vpn)
class MVPN(NLRI):
registered_mvpn = dict()

# NEED to be defined in the subclasses
CODE = -1
NAME = 'Unknown'
SHORT_NAME = 'unknown'

def __init__(self, afi, action=Action.UNSET, addpath=None):
NLRI.__init__(self, afi=afi, safi=SAFI.mcast_vpn, action=action)
self._packed = b''

def __hash__(self):
return hash("%s:%s:%s:%s" % (self.afi, self.safi, self.CODE, self._packed))

def __len__(self):
return len(self._packed) + 2

def __eq__(self, other):
return NLRI.__eq__(self, other) and self.CODE == other.CODE

def __str__(self):
return "mvpn:%s:%s" % (
self.registered_mvpn.get(self.CODE, self).SHORT_NAME.lower(),
'0x' + ''.join('%02x' % _ for _ in self._packed),
)

def __repr__(self):
return str(self)

def feedback(self, action):
# if self.nexthop is None and action == Action.ANNOUNCE:
# return 'mvpn nlri next-hop is missing'
return ''

def _prefix(self):
return "mvpn:%s:" % (self.registered_mvpn.get(self.CODE, self).SHORT_NAME.lower())

def pack_nlri(self, negotiated=None):
# XXX: addpath not supported yet
return pack('!BB', self.CODE, len(self._packed)) + self._packed

@classmethod
def register(cls, klass):
if klass.CODE in cls.registered_mvpn:
raise RuntimeError('only one MVPN registration allowed')
cls.registered_mvpn[klass.CODE] = klass
return klass

@classmethod
def unpack_nlri(cls, afi, safi, bgp, action, addpath):
code = bgp[0]
length = bgp[1]

if code in cls.registered_mvpn:
klass = cls.registered_mvpn[code].unpack(bgp[2 : length + 2], afi)
else:
klass = GenericMVPN(afi, code, bgp[2 : length + 2])
klass.CODE = code
klass.action = action
klass.addpath = addpath

return klass, bgp[length + 2 :]

def _raw(self):
return ''.join('%02X' % _ for _ in self.pack_nlri())


class GenericMVPN(MVPN):
def __init__(self, afi, code, packed):
MVPN.__init__(self, afi)
self.CODE = code
self._pack(packed)

def _pack(self, packed=None):
if self._packed:
return self._packed

if packed:
self._packed = packed
return packed

def json(self, compact=None):
return '{ "code": %d, "parsed": false, "raw": "%s" }' % (self.CODE, self._raw())
114 changes: 114 additions & 0 deletions src/exabgp/bgp/message/update/nlri/mvpn/sharedjoin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from exabgp.protocol.family import AFI
from exabgp.protocol.family import SAFI

from exabgp.bgp.message.update.nlri.qualifier import RouteDistinguisher
from exabgp.bgp.message.update.nlri.mvpn.nlri import MVPN
from exabgp.bgp.message.notification import Notify
from exabgp.protocol.ip import IP
from struct import pack

# +-----------------------------------+
# | RD (8 octets) |
# +-----------------------------------+
# | Source AS (4 octets) |
# +-----------------------------------+
# | Multicast Source Length (1 octet) |
# +-----------------------------------+
# | Multicast Source (variable) |
# +-----------------------------------+
# | Multicast Group Length (1 octet) |
# +-----------------------------------+
# | Multicast Group (variable) |
# +-----------------------------------+


@MVPN.register
class SharedJoin(MVPN):
CODE = 6
NAME = "C-Multicast Shared Tree Join route"
SHORT_NAME = "Shared-Join"

def __init__(self, rd, afi, source, group, source_as, packed=None, action=None, addpath=None):
MVPN.__init__(self, afi=afi, action=action, addpath=addpath)
self.rd = rd
self.group = group
self.source = source
self.source_as = source_as
self._pack(packed)

def __eq__(self, other):
return (
isinstance(other, SharedJoin)
and self.CODE == other.CODE
and self.rd == other.rd
and self.source == other.source
and self.group == other.group
)

def __ne__(self, other):
return not self.__eq__(other)

def __str__(self):
return f'{self._prefix()}:{self.rd._str()}:{str(self.source_as)}:{str(self.source)}:{str(self.group)}'

def __hash__(self):
return hash((self.rd, self.source, self.group, self.source_as))

def _pack(self, packed=None):
if self._packed:
return self._packed

if packed:
self._packed = packed
return packed
self._packed = (
self.rd.pack()
+ pack('!I', self.source_as)
+ bytes([len(self.source) * 8])
+ self.source.pack()
+ bytes([len(self.group) * 8])
+ self.group.pack()
)
return self._packed

@classmethod
def unpack(cls, data, afi):
datalen = len(data)
if datalen not in (22, 46): # IPv4 or IPv6
raise Notify(3, 5, f"Invalid C-Multicast Route length ({datalen} bytes).")
cursor = 0
rd = RouteDistinguisher.unpack(data[cursor:8])
cursor += 8
source_as = int.from_bytes(data[cursor : cursor + 4], "big")
cursor += 4
sourceiplen = int(data[cursor] / 8)
cursor += 1
if sourceiplen != 4 and sourceiplen != 16:
raise Notify(
3,
5,
f"Invalid C-Multicast Route length ({sourceiplen*8} bits). Expected 32 bits (IPv4) or 128 bits (IPv6).",
)
sourceip = IP.unpack(data[cursor : cursor + sourceiplen])
cursor += sourceiplen
groupiplen = int(data[cursor] / 8)
cursor += 1
if groupiplen != 4 and groupiplen != 16:
raise Notify(
3,
5,
f"Invalid C-Multicast Route length ({groupiplen*8} bits). Expected 32 bits (IPv4) or 128 bits (IPv6).",
)
groupip = IP.unpack(data[cursor : cursor + groupiplen])
return cls(afi=afi, rd=rd, source=sourceip, group=groupip, source_as=source_as, packed=data)

def json(self, compact=None):
content = ' "code": %d, ' % self.CODE
content += '"parsed": true, '
content += '"raw": "%s", ' % self._raw()
content += '"name": "%s", ' % self.NAME
content += '%s, ' % self.rd.json()
content += '"source-as": "%s", ' % str(self.source_as)
content += '"source": "%s", ' % str(self.source)
content += '"group": "%s"' % str(self.group)
return '{%s}' % content
Loading

0 comments on commit 82b21cd

Please sign in to comment.