diff --git a/etc/exabgp/api-mvpn.conf b/etc/exabgp/api-mvpn.conf new file mode 100644 index 000000000..d264a6da6 --- /dev/null +++ b/etc/exabgp/api-mvpn.conf @@ -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 ]; + } +} \ No newline at end of file diff --git a/etc/exabgp/conf-mvpn.conf b/etc/exabgp/conf-mvpn.conf new file mode 100644 index 000000000..f3f101ed0 --- /dev/null +++ b/etc/exabgp/conf-mvpn.conf @@ -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 ]; + } + } +} \ No newline at end of file diff --git a/etc/exabgp/run/api-mvpn.run b/etc/exabgp/run/api-mvpn.run new file mode 100755 index 000000000..8c44b269c --- /dev/null +++ b/etc/exabgp/run/api-mvpn.run @@ -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 diff --git a/qa/encoding/api-mvpn.ci b/qa/encoding/api-mvpn.ci new file mode 100644 index 000000000..e4c8c61ab --- /dev/null +++ b/qa/encoding/api-mvpn.ci @@ -0,0 +1 @@ +api-mvpn.conf \ No newline at end of file diff --git a/qa/encoding/api-mvpn.msg b/qa/encoding/api-mvpn.msg new file mode 100644 index 000000000..8e87d1a18 --- /dev/null +++ b/qa/encoding/api-mvpn.msg @@ -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 diff --git a/qa/encoding/conf-mvpn.ci b/qa/encoding/conf-mvpn.ci new file mode 100644 index 000000000..67e890526 --- /dev/null +++ b/qa/encoding/conf-mvpn.ci @@ -0,0 +1 @@ +conf-mvpn.conf \ No newline at end of file diff --git a/qa/encoding/conf-mvpn.msg b/qa/encoding/conf-mvpn.msg new file mode 100644 index 000000000..9b7378971 --- /dev/null +++ b/qa/encoding/conf-mvpn.msg @@ -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 \ No newline at end of file diff --git a/src/exabgp/bgp/message/update/nlri/__init__.py b/src/exabgp/bgp/message/update/nlri/__init__.py index 1903b2af6..a37b07c7a 100644 --- a/src/exabgp/bgp/message/update/nlri/__init__.py +++ b/src/exabgp/bgp/message/update/nlri/__init__.py @@ -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 diff --git a/src/exabgp/bgp/message/update/nlri/mvpn/__init__.py b/src/exabgp/bgp/message/update/nlri/mvpn/__init__.py new file mode 100644 index 000000000..a541a9605 --- /dev/null +++ b/src/exabgp/bgp/message/update/nlri/mvpn/__init__.py @@ -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 diff --git a/src/exabgp/bgp/message/update/nlri/mvpn/nlri.py b/src/exabgp/bgp/message/update/nlri/mvpn/nlri.py new file mode 100644 index 000000000..f5c277402 --- /dev/null +++ b/src/exabgp/bgp/message/update/nlri/mvpn/nlri.py @@ -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()) diff --git a/src/exabgp/bgp/message/update/nlri/mvpn/sharedjoin.py b/src/exabgp/bgp/message/update/nlri/mvpn/sharedjoin.py new file mode 100644 index 000000000..91da47c81 --- /dev/null +++ b/src/exabgp/bgp/message/update/nlri/mvpn/sharedjoin.py @@ -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 diff --git a/src/exabgp/bgp/message/update/nlri/mvpn/sourcead.py b/src/exabgp/bgp/message/update/nlri/mvpn/sourcead.py new file mode 100644 index 000000000..eb8569780 --- /dev/null +++ b/src/exabgp/bgp/message/update/nlri/mvpn/sourcead.py @@ -0,0 +1,113 @@ +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 + +# +-----------------------------------+ +# | RD (8 octets) | +# +-----------------------------------+ +# | Multicast Source Length (1 octet) | +# +-----------------------------------+ +# | Multicast Source (variable) | +# +-----------------------------------+ +# | Multicast Group Length (1 octet) | +# +-----------------------------------+ +# | Multicast Group (variable) | +# +-----------------------------------+ + + +@MVPN.register +class SourceAD(MVPN): + CODE = 5 + NAME = "Source Active A-D Route" + SHORT_NAME = "SourceAD" + + def __init__(self, rd, afi, source, group, packed=None, action=None, addpath=None): + MVPN.__init__(self, afi=afi, action=action, addpath=addpath) + self.rd = rd + self.source = source + self.group = group + self._pack(packed) + + def __eq__(self, other): + return ( + isinstance(other, SourceAD) + 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)}:{str(self.group)}' + + def __hash__(self): + return hash((self.rd, self.source, self.group)) + + def _pack(self, packed=None): + if self._packed: + return self._packed + + if packed: + self._packed = packed + return packed + self._packed = ( + self.rd.pack() + + 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 (18, 42): # IPv4 or IPv6 + raise Notify(3, 5, f"Unsupported Source Active A-D route length ({datalen} bytes).") + cursor = 0 + rd = RouteDistinguisher.unpack(data[cursor:8]) + cursor += 8 + sourceiplen = int(data[cursor] / 8) + cursor += 1 + if sourceiplen != 4 and sourceiplen != 16: + raise Notify( + 3, + 5, + f"Unsupported Source Active A-D Route Multicast Source IP 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"Unsupported Source Active A-D Route Multicast Group IP length ({groupiplen*8} bits). Expected 32 bits (IPv4) or 128 bits (IPv6).", + ) + groupip = IP.unpack(data[cursor : cursor + groupiplen]) + + # Missing implementation of this check from RFC 6514: + # Source Active A-D routes with a Multicast group belonging to the + # Source Specific Multicast (SSM) range (as defined in [RFC4607], and + # potentially extended locally on a router) MUST NOT be advertised by a + # router and MUST be discarded if received. + + return cls(afi=afi, rd=rd, source=sourceip, group=groupip, 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": "%s", ' % str(self.source) + content += '"group": "%s"' % str(self.group) + return '{%s}' % content diff --git a/src/exabgp/bgp/message/update/nlri/mvpn/sourcejoin.py b/src/exabgp/bgp/message/update/nlri/mvpn/sourcejoin.py new file mode 100644 index 000000000..ef1c45ca4 --- /dev/null +++ b/src/exabgp/bgp/message/update/nlri/mvpn/sourcejoin.py @@ -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 SourceJoin(MVPN): + CODE = 7 + NAME = "C-Multicast Source Tree Join route" + SHORT_NAME = "Source-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, SourceJoin) + 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 diff --git a/src/exabgp/configuration/announce/mvpn.py b/src/exabgp/configuration/announce/mvpn.py new file mode 100644 index 000000000..ff0924cf1 --- /dev/null +++ b/src/exabgp/configuration/announce/mvpn.py @@ -0,0 +1,111 @@ +# encoding: utf-8 + +from exabgp.rib.change import Change + +from exabgp.bgp.message import Action + +from exabgp.protocol.family import AFI +from exabgp.protocol.family import SAFI + +from exabgp.bgp.message.update.attribute import Attributes + +from exabgp.configuration.announce import ParseAnnounce +from exabgp.configuration.announce.ip import AnnounceIP + +from exabgp.configuration.static.mpls import mvpn_sourcead +from exabgp.configuration.static.mpls import mvpn_sourcejoin +from exabgp.configuration.static.mpls import mvpn_sharedjoin + + +class AnnounceMVPN(ParseAnnounce): + definition = [ + 'source-ad source group rd ', + 'shared-join rp group rd source-as ', + 'source-join source group rd source-as ', + ] + AnnounceIP.definition + + syntax = ' { ' '\n ' + ' ;\n '.join(definition) + '\n}' + + known = dict( + AnnounceIP.known, + ) + + action = dict( + AnnounceIP.action, + ) + + assign = dict( + AnnounceIP.assign, + ) + + name = 'mvpn' + afi = None + + def __init__(self, tokeniser, scope, error): + ParseAnnounce.__init__(self, tokeniser, scope, error) + + def clear(self): + pass + + def pre(self): + self.scope.to_context(self.name) + return True + + def post(self): + return ParseAnnounce.post() and self._check() + + @staticmethod + def check(change, afi): + if not AnnounceIP.check(change, afi): + return False + + return True + + +def mvpn_route(tokeniser, afi): + action = Action.ANNOUNCE if tokeniser.announce else Action.WITHDRAW + route_type = tokeniser() + if route_type == 'source-ad': + mvpn_nlri = mvpn_sourcead(tokeniser, afi, action) + elif route_type == 'source-join': + mvpn_nlri = mvpn_sourcejoin(tokeniser, afi, action) + elif route_type == 'shared-join': + mvpn_nlri = mvpn_sharedjoin(tokeniser, afi, action) + else: + raise ValueError("mup: unknown/unsupported mvpn route type: %s" % route_type) + + change = Change(mvpn_nlri, Attributes()) + + while True: + command = tokeniser() + + if not command: + break + + action = AnnounceMVPN.action.get(command, '') + + if action == 'attribute-add': + change.attributes.add(AnnounceMVPN.known[command](tokeniser)) + elif action == 'nlri-set': + change.nlri.assign(AnnounceMVPN.assign[command], AnnounceMVPN.known[command](tokeniser)) + elif action == 'nexthop-and-attribute': + nexthop, attribute = AnnounceMVPN.known[command](tokeniser) + change.nlri.nexthop = nexthop + change.attributes.add(attribute) + else: + raise ValueError('unknown command "%s"' % command) + + if not AnnounceMVPN.check(change, afi): + raise ValueError('invalid announcement (missing next-hop, label or rd ?)') + + return [change] + + +@ParseAnnounce.register('mcast-vpn', 'extend-name', 'ipv4') +def mcast_vpn_v4(tokeniser): + return mvpn_route(tokeniser, AFI.ipv4) + + +@ParseAnnounce.register('mcast-vpn', 'extend-name', 'ipv6') +def mcast_vpn_v6(tokeniser): + return mvpn_route(tokeniser, AFI.ipv6) diff --git a/src/exabgp/configuration/configuration.py b/src/exabgp/configuration/configuration.py index 8112ac0d9..6c6a23dab 100644 --- a/src/exabgp/configuration/configuration.py +++ b/src/exabgp/configuration/configuration.py @@ -50,6 +50,7 @@ from exabgp.configuration.announce.path import AnnouncePath # noqa: F401,E261,E501 from exabgp.configuration.announce.label import AnnounceLabel # noqa: F401,E261,E501 from exabgp.configuration.announce.vpn import AnnounceVPN # noqa: F401,E261,E501 +from exabgp.configuration.announce.mvpn import AnnounceMVPN # noqa: F401,E261,E501 from exabgp.configuration.announce.flow import AnnounceFlow # noqa: F401,E261,E501 from exabgp.configuration.announce.vpls import AnnounceVPLS # noqa: F401,E261,E501 from exabgp.configuration.announce.mup import AnnounceMup # noqa: F401,E261,E501 @@ -256,12 +257,12 @@ def __init__(self, configurations, text=False): }, self.announce_ipv4.name: { 'class': self.announce_ipv4, - 'commands': ['unicast', 'multicast', 'nlri-mpls', 'mpls-vpn', 'flow', 'flow-vpn', 'mup'], + 'commands': ['unicast', 'multicast', 'nlri-mpls', 'mpls-vpn', 'mcast-vpn', 'flow', 'flow-vpn', 'mup'], 'sections': {}, }, self.announce_ipv6.name: { 'class': self.announce_ipv6, - 'commands': ['unicast', 'multicast', 'nlri-mpls', 'mpls-vpn', 'flow', 'flow-vpn', 'mup'], + 'commands': ['unicast', 'multicast', 'nlri-mpls', 'mpls-vpn', 'mcast-vpn', 'flow', 'flow-vpn', 'mup'], 'sections': {}, }, self.announce_l2vpn.name: { diff --git a/src/exabgp/configuration/neighbor/family.py b/src/exabgp/configuration/neighbor/family.py index a8ab6e406..f45fae500 100644 --- a/src/exabgp/configuration/neighbor/family.py +++ b/src/exabgp/configuration/neighbor/family.py @@ -23,10 +23,12 @@ class ParseFamily(Section): ' ipv4 multicast;\n' ' ipv4 nlri-mpls;\n' ' ipv4 mpls-vpn;\n' + ' ipv4 mcast-vpn;\n' ' ipv4 mup;\n' ' ipv4 flow;\n' ' ipv4 flow-vpn;\n' ' ipv6 unicast;\n' + ' ipv6 mcast-vpn;\n' ' ipv6 mup;\n' ' ipv6 flow;\n' ' ipv6 flow-vpn;\n' @@ -41,6 +43,7 @@ class ParseFamily(Section): 'multicast': (AFI.ipv4, SAFI.multicast), 'nlri-mpls': (AFI.ipv4, SAFI.nlri_mpls), 'mpls-vpn': (AFI.ipv4, SAFI.mpls_vpn), + 'mcast-vpn': (AFI.ipv4, SAFI.mcast_vpn), 'flow': (AFI.ipv4, SAFI.flow_ip), 'flow-vpn': (AFI.ipv4, SAFI.flow_vpn), 'mup': (AFI.ipv4, SAFI.mup), @@ -49,6 +52,7 @@ class ParseFamily(Section): 'unicast': (AFI.ipv6, SAFI.unicast), 'nlri-mpls': (AFI.ipv6, SAFI.nlri_mpls), 'mpls-vpn': (AFI.ipv6, SAFI.mpls_vpn), + 'mcast-vpn': (AFI.ipv6, SAFI.mcast_vpn), 'mup': (AFI.ipv6, SAFI.mup), 'flow': (AFI.ipv6, SAFI.flow_ip), 'flow-vpn': (AFI.ipv6, SAFI.flow_vpn), diff --git a/src/exabgp/configuration/static/mpls.py b/src/exabgp/configuration/static/mpls.py index 18ff2592d..4ae31d980 100644 --- a/src/exabgp/configuration/static/mpls.py +++ b/src/exabgp/configuration/static/mpls.py @@ -28,6 +28,9 @@ from exabgp.bgp.message.update.nlri.mup import DirectSegmentDiscoveryRoute from exabgp.bgp.message.update.nlri.mup import Type1SessionTransformedRoute from exabgp.bgp.message.update.nlri.mup import Type2SessionTransformedRoute +from exabgp.bgp.message.update.nlri.mvpn import SourceAD +from exabgp.bgp.message.update.nlri.mvpn import SourceJoin +from exabgp.bgp.message.update.nlri.mvpn import SharedJoin def label(tokeniser): @@ -212,6 +215,85 @@ def parse_ip_prefix(tokeninser): return ip, int(length) +# shared-join rp group rd source-as +def mvpn_sharedjoin(tokeniser, afi, action): + if afi == AFI.ipv4: + tokeniser.consume("rp") + sourceip = IPv4.unpack(IPv4.pton(tokeniser())) + tokeniser.consume("group") + groupip = IPv4.unpack(IPv4.pton(tokeniser())) + elif afi == AFI.ipv6: + tokeniser.consume("rp") + sourceip = IPv6.unpack(IPv6.pton(tokeniser())) + tokeniser.consume("group") + groupip = IPv6.unpack(IPv6.pton(tokeniser())) + else: + raise Exception("unexpect afi: %s" % afi) + + tokeniser.consume("rd") + rd = route_distinguisher(tokeniser) + + tokeniser.consume("source-as") + value = tokeniser() + if not value.isdigit(): + raise Exception("expect source-as to be a integer in the range 0-4294967295, but received '%s'" % value) + asnum = int(value) + if asnum > 4294967295: + raise Exception("expect source-as to be a integer in the range 0-4294967295, but received '%s'" % value) + + return SharedJoin(rd=rd, afi=afi, source=sourceip, group=groupip, source_as=asnum, action=action) + + +# source-join source group rd source-as +def mvpn_sourcejoin(tokeniser, afi, action): + if afi == AFI.ipv4: + tokeniser.consume("source") + sourceip = IPv4.unpack(IPv4.pton(tokeniser())) + tokeniser.consume("group") + groupip = IPv4.unpack(IPv4.pton(tokeniser())) + elif afi == AFI.ipv6: + tokeniser.consume("source") + sourceip = IPv6.unpack(IPv6.pton(tokeniser())) + tokeniser.consume("group") + groupip = IPv6.unpack(IPv6.pton(tokeniser())) + else: + raise Exception("unexpect afi: %s" % afi) + + tokeniser.consume("rd") + rd = route_distinguisher(tokeniser) + + tokeniser.consume("source-as") + value = tokeniser() + if not value.isdigit(): + raise Exception("expect source-as to be a integer in the range 0-4294967295, but received '%s'" % value) + asnum = int(value) + if asnum > 4294967295: + raise Exception("expect source-as to be a integer in the range 0-4294967295, but received '%s'" % value) + + return SourceJoin(rd=rd, afi=afi, source=sourceip, group=groupip, source_as=asnum, action=action) + + +#'source-ad source group rd ' +def mvpn_sourcead(tokeniser, afi, action): + if afi == AFI.ipv4: + tokeniser.consume("source") + sourceip = IPv4.unpack(IPv4.pton(tokeniser())) + tokeniser.consume("group") + groupip = IPv4.unpack(IPv4.pton(tokeniser())) + elif afi == AFI.ipv6: + tokeniser.consume("source") + sourceip = IPv6.unpack(IPv6.pton(tokeniser())) + tokeniser.consume("group") + groupip = IPv6.unpack(IPv6.pton(tokeniser())) + else: + raise Exception("unexpect afi: %s" % afi) + + tokeniser.consume("rd") + rd = route_distinguisher(tokeniser) + + return SourceAD(rd=rd, afi=afi, source=sourceip, group=groupip, action=action) + + # 'mup-isd rd ', def srv6_mup_isd(tokeniser, afi): prefix_ip, prefix_len = parse_ip_prefix(tokeniser()) diff --git a/src/exabgp/protocol/family.py b/src/exabgp/protocol/family.py index 4558959f6..6f3c8b17e 100644 --- a/src/exabgp/protocol/family.py +++ b/src/exabgp/protocol/family.py @@ -97,9 +97,9 @@ def value(cls, name): @staticmethod def implemented_safi(afi): if afi == 'ipv4': - return ['unicast', 'multicast', 'nlri-mpls', 'mpls-vpn', 'flow', 'flow-vpn', 'mup'] + return ['unicast', 'multicast', 'nlri-mpls', 'mcast-vpn' 'mpls-vpn', 'flow', 'flow-vpn', 'mup'] if afi == 'ipv6': - return ['unicast', 'mpls-vpn', 'flow', 'flow-vpn', 'mup'] + return ['unicast', 'mpls-vpn', 'mcast-vpn', 'flow', 'flow-vpn', 'mup'] if afi == 'l2vpn': return ['vpls', 'evpn'] if afi == 'bgp-ls': @@ -130,6 +130,7 @@ class _SAFI(int): BGPLS_VPN = 72 # [RFC7752] MUP = 85 # [draft-mpmz-bess-mup-safi] MPLS_VPN = 128 # [RFC4364] + MCAST_VPN = 5 # [RFC6514] RTC = 132 # [RFC4684] FLOW_IP = 133 # [RFC5575] FLOW_VPN = 134 # [RFC5575] @@ -159,6 +160,7 @@ class _SAFI(int): BGPLS_VPN: 'bgp-ls-vpn', MUP: 'mup', MPLS_VPN: 'mpls-vpn', + MCAST_VPN: 'mcast-vpn', RTC: 'rtc', FLOW_IP: 'flow', FLOW_VPN: 'flow-vpn', @@ -171,10 +173,10 @@ def name(self): return self._names.get(self, 'unknown safi %d' % int(self)) def has_label(self): - return self in (SAFI.nlri_mpls, SAFI.mpls_vpn) + return self in (SAFI.nlri_mpls, SAFI.mpls_vpn, SAFI.mcast_vpn) def has_rd(self): - return self in (SAFI.mup, SAFI.mpls_vpn, SAFI.flow_vpn) + return self in (SAFI.mup, SAFI.mpls_vpn, SAFI.mcast_vpn, SAFI.flow_vpn) # technically self.flow_vpn and self.vpls has an RD but it is not an NLRI def has_path(self): @@ -199,6 +201,7 @@ class SAFI(Resource): bgp_ls_vpn = _SAFI(_SAFI.BGPLS_VPN) mup = _SAFI(_SAFI.MUP) mpls_vpn = _SAFI(_SAFI.MPLS_VPN) + mcast_vpn = _SAFI(_SAFI.MCAST_VPN) rtc = _SAFI(_SAFI.RTC) flow_ip = _SAFI(_SAFI.FLOW_IP) flow_vpn = _SAFI(_SAFI.FLOW_VPN) @@ -214,6 +217,7 @@ class SAFI(Resource): bgp_ls_vpn.pack(): bgp_ls_vpn, mup.pack(): mup, mpls_vpn.pack(): mpls_vpn, + mcast_vpn.pack(): mcast_vpn, rtc.pack(): rtc, flow_ip.pack(): flow_ip, flow_vpn.pack(): flow_vpn, @@ -231,6 +235,7 @@ class SAFI(Resource): 'bgp-ls-vpn': bgp_ls_vpn, 'mup': mup, 'mpls-vpn': mpls_vpn, + 'mcast-vpn': mcast_vpn, 'rtc': rtc, 'flow': flow_ip, 'flow-vpn': flow_vpn, @@ -269,6 +274,7 @@ class Family(object): (AFI.ipv4, SAFI.nlri_mpls): ((4,), 0), (AFI.ipv4, SAFI.mup): ((4, 16), 0), (AFI.ipv4, SAFI.mpls_vpn): ((12,), 8), + (AFI.ipv4, SAFI.mcast_vpn): ((4,), 0), (AFI.ipv4, SAFI.flow_ip): ((0, 4), 0), (AFI.ipv4, SAFI.flow_vpn): ((0, 4), 0), (AFI.ipv4, SAFI.rtc): ((4, 16), 0), @@ -276,6 +282,7 @@ class Family(object): (AFI.ipv6, SAFI.nlri_mpls): ((16, 32), 0), (AFI.ipv6, SAFI.mup): ((4, 16), 0), (AFI.ipv6, SAFI.mpls_vpn): ((24, 40), 8), + (AFI.ipv6, SAFI.mcast_vpn): ((4, 16), 0), (AFI.ipv6, SAFI.flow_ip): ((0, 16, 32), 0), (AFI.ipv6, SAFI.flow_vpn): ((0, 16, 32), 0), (AFI.l2vpn, SAFI.vpls): ((4,), 0), diff --git a/src/exabgp/rib/outgoing.py b/src/exabgp/rib/outgoing.py index 5da59c50a..0c749cae8 100644 --- a/src/exabgp/rib/outgoing.py +++ b/src/exabgp/rib/outgoing.py @@ -223,6 +223,15 @@ def updates(self, grouped): yield Update(nlris, attributes) continue + if family == (AFI.ipv4, SAFI.mcast_vpn) and grouped: + nlris = [change.nlri for change in changes.values()] + yield Update(nlris, attributes) + continue + if family == (AFI.ipv6, SAFI.mcast_vpn) and grouped: + nlris = [change.nlri for change in changes.values()] + yield Update(nlris, attributes) + continue + for change in changes.values(): yield Update([change.nlri], attributes) diff --git a/tests/nlri_tests.py b/tests/nlri_tests.py index cbdb3c59f..2129fcc2d 100755 --- a/tests/nlri_tests.py +++ b/tests/nlri_tests.py @@ -29,6 +29,11 @@ from exabgp.bgp.message.update.nlri.evpn.prefix import Prefix as EVPNPrefix from exabgp.bgp.message.update.nlri.evpn.nlri import EVPN +from exabgp.bgp.message.update.nlri.mvpn.nlri import MVPN +from exabgp.bgp.message.update.nlri.mvpn.sourcead import SourceAD as MVPN_SourceAD +from exabgp.bgp.message.update.nlri.mvpn.sourcejoin import SourceJoin as MVPN_SourceJoin +from exabgp.bgp.message.update.nlri.mvpn.sharedjoin import SharedJoin as MVPN_SharedJoin + from exabgp.bgp.message.update.nlri.qualifier.rd import RouteDistinguisher from exabgp.bgp.message.update.nlri.qualifier.labels import Labels from exabgp.bgp.message.update.nlri.qualifier.esi import ESI @@ -42,6 +47,129 @@ class TestNLRIs(unittest.TestCase): + # Tests on MVPN NLRIs + def test300_MVPNSourceAD_CreatePackUnpack(self): + '''Test pack/unpack for MVPN Source A-D route''' + nlri = MVPN_SourceAD( + afi=AFI.ipv4, + rd=RouteDistinguisher.fromElements("42.42.42.42", 5), + source=IP.create("1.2.3.0"), + group=IP.create("226.0.0.1"), + ) + + packed = nlri.pack() + unpacked, leftover = MVPN.unpack_nlri( + afi=AFI.ipv4, safi=SAFI.mcast_vpn, bgp=packed, action=Action.UNSET, addpath=None + ) + + self.assertEqual(0, len(leftover)) + self.assertTrue(isinstance(unpacked, MVPN_SourceAD)) + self.assertEqual("1.2.3.0", str(unpacked.source)) + self.assertEqual("42.42.42.42:5", unpacked.rd._str()) + self.assertEqual("226.0.0.1", str(unpacked.group)) + + nlri = MVPN_SourceAD( + afi=AFI.ipv6, + rd=RouteDistinguisher.fromElements("42.42.42.42", 5), + source=IP.create("fd12::2"), + group=IP.create("ff0e::1"), + ) + + packed = nlri.pack() + unpacked, leftover = MVPN.unpack_nlri( + afi=AFI.ipv6, safi=SAFI.mcast_vpn, bgp=packed, action=Action.UNSET, addpath=None + ) + + self.assertEqual(0, len(leftover)) + self.assertTrue(isinstance(unpacked, MVPN_SourceAD)) + self.assertEqual("fd12::2", str(unpacked.source)) + self.assertEqual("42.42.42.42:5", unpacked.rd._str()) + self.assertEqual("ff0e::1", str(unpacked.group)) + + def test300_MVPNSourceJoin_CreatePackUnpack(self): + '''Test pack/unpack for MVPN Source-Join route''' + nlri = MVPN_SourceJoin( + afi=AFI.ipv4, + rd=RouteDistinguisher.fromElements("42.42.42.42", 5), + source=IP.create("1.2.3.0"), + group=IP.create("226.0.0.1"), + source_as=1234, + ) + + packed = nlri.pack() + unpacked, leftover = MVPN.unpack_nlri( + afi=AFI.ipv4, safi=SAFI.mcast_vpn, bgp=packed, action=Action.UNSET, addpath=None + ) + + self.assertEqual(0, len(leftover)) + self.assertTrue(isinstance(unpacked, MVPN_SourceJoin)) + self.assertEqual("1.2.3.0", str(unpacked.source)) + self.assertEqual("42.42.42.42:5", unpacked.rd._str()) + self.assertEqual("226.0.0.1", str(unpacked.group)) + self.assertEqual(1234, unpacked.source_as) + + nlri = MVPN_SourceJoin( + afi=AFI.ipv6, + rd=RouteDistinguisher.fromElements("42.42.42.42", 5), + source=IP.create("fd12::2"), + group=IP.create("ff0e::1"), + source_as=1234, + ) + + packed = nlri.pack() + unpacked, leftover = MVPN.unpack_nlri( + afi=AFI.ipv6, safi=SAFI.mcast_vpn, bgp=packed, action=Action.UNSET, addpath=None + ) + + self.assertEqual(0, len(leftover)) + self.assertTrue(isinstance(unpacked, MVPN_SourceJoin)) + self.assertEqual("fd12::2", str(unpacked.source)) + self.assertEqual("42.42.42.42:5", unpacked.rd._str()) + self.assertEqual("ff0e::1", str(unpacked.group)) + self.assertEqual(1234, unpacked.source_as) + + def test300_MVPNSharedJoin_CreatePackUnpack(self): + '''Test pack/unpack for MVPN Shared-Join route''' + nlri = MVPN_SharedJoin( + afi=AFI.ipv4, + rd=RouteDistinguisher.fromElements("42.42.42.42", 5), + source=IP.create("1.2.3.0"), + group=IP.create("226.0.0.1"), + source_as=1234, + ) + + packed = nlri.pack() + unpacked, leftover = MVPN.unpack_nlri( + afi=AFI.ipv4, safi=SAFI.mcast_vpn, bgp=packed, action=Action.UNSET, addpath=None + ) + + self.assertEqual(0, len(leftover)) + self.assertTrue(isinstance(unpacked, MVPN_SharedJoin)) + self.assertEqual("1.2.3.0", str(unpacked.source)) + self.assertEqual("42.42.42.42:5", unpacked.rd._str()) + self.assertEqual("226.0.0.1", str(unpacked.group)) + self.assertEqual(1234, unpacked.source_as) + + nlri = MVPN_SharedJoin( + afi=AFI.ipv6, + rd=RouteDistinguisher.fromElements("42.42.42.42", 5), + source=IP.create("fd12::2"), + group=IP.create("ff0e::1"), + source_as=1234, + ) + + packed = nlri.pack() + unpacked, leftover = MVPN.unpack_nlri( + afi=AFI.ipv6, safi=SAFI.mcast_vpn, bgp=packed, action=Action.UNSET, addpath=None + ) + + self.assertEqual(0, len(leftover)) + self.assertTrue(isinstance(unpacked, MVPN_SharedJoin)) + self.assertEqual("fd12::2", str(unpacked.source)) + self.assertEqual("42.42.42.42:5", unpacked.rd._str()) + self.assertEqual("ff0e::1", str(unpacked.group)) + self.assertEqual(1234, unpacked.source_as) + # Tests on IPVPN NLRIs def test200_IPVPNCreatePackUnpack(self): @@ -207,7 +335,14 @@ def test101_EVPNHashEqual_somefieldsvary(self): # ESI nlri1 = EVPNMAC( RouteDistinguisher.fromElements("42.42.42.42", 5), - ESI(bytes([1,] * 10)), + ESI( + bytes( + [ + 1, + ] + * 10 + ) + ), EthernetTag(111), MAC("01:02:03:04:05:06"), 6 * 8,