Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RFC6514 MCAST-VPN (incomplete) #1234

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name may be used when the route is printed and may need to be lower case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean NAME, SHORT_NAME or both? I tried to follow example from other nlri code (like mup and evpn), but didn't notice a pattern regarding lower-case use.

Notice SHORT_NAME is already lower-cased here when printing by _prefix():

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

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

This is the print output you mean right?

reactor       connected to peer-1 with outgoing-1 10.99.12.6-10.99.12.5
processes     ipv4 added to neighbor 10.99.12.5 local-ip 10.99.12.6 local-as 65000 peer-as 65000 router-id 32.32.32.32 family-allowed in-open : mvpn:shared-join::65000:99999:65000:10.99.199.1:239.251.255.228 extended-community target:192.168.94.12:5
processes     ipv4 added to neighbor 10.99.12.5 local-ip 10.99.12.6 local-as 65000 peer-as 65000 router-id 32.32.32.32 family-allowed in-open : mvpn:source-join::65000:99999:65000:10.99.12.2:239.251.255.228 extended-community target:192.168.94.12:5
processes     ipv6 added to neighbor 10.99.12.5 local-ip 10.99.12.6 local-as 65000 peer-as 65000 router-id 32.32.32.32 family-allowed in-open : mvpn:shared-join::65000:99999:65000:fd00::1:ff0e::1 extended-community target:192.168.94.12:5
processes     ipv6 added to neighbor 10.99.12.5 local-ip 10.99.12.6 local-as 65000 peer-as 65000 router-id 32.32.32.32 family-allowed in-open : mvpn:source-join::65000:99999:65000:fd12::2:ff0e::1 extended-community target:192.168.94.12:5
processes     ipv6 added to neighbor 10.99.12.5 local-ip 10.99.12.6 local-as 65000 peer-as 65000 router-id 32.32.32.32 family-allowed in-open : mvpn:sourcead::65000:99999:fd12::4:ff0e::1 extended-community target:65000:99999
processes     ipv4 added to neighbor 10.99.12.5 local-ip 10.99.12.6 local-as 65000 peer-as 65000 router-id 32.32.32.32 family-allowed in-open : mvpn:sourcead::65000:99999:10.99.12.4:239.251.255.228 extended-community target:65000:99999


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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this represent the route in a way which could be parsed by the configuration code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get this one. can you share an example?

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