mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-08-09 07:56:29 +00:00
topotests: add bgp_rpki_topo1
Add bgp_rpki_topo1 topotest to validate the RPKI feature. Use a RTR RPKI server from the above link with a black cleaning. Link: https://raw.githubusercontent.com/tmshlvck/pyrtr/90df586375396aae08b07069187308b5b7b8823b/pyrtr/__init__.py Signed-off-by: Louis Scalbert <louis.scalbert@6wind.com>
This commit is contained in:
parent
f8875f53d6
commit
751500acdb
0
tests/topotests/bgp_rpki_topo1/__init__.py
Normal file
0
tests/topotests/bgp_rpki_topo1/__init__.py
Normal file
14
tests/topotests/bgp_rpki_topo1/r1/bgpd.conf
Normal file
14
tests/topotests/bgp_rpki_topo1/r1/bgpd.conf
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
router bgp 65530
|
||||||
|
no bgp ebgp-requires-policy
|
||||||
|
no bgp network import-check
|
||||||
|
neighbor 192.0.2.2 remote-as 65002
|
||||||
|
neighbor 192.0.2.2 timers 1 3
|
||||||
|
neighbor 192.0.2.2 timers connect 1
|
||||||
|
neighbor 192.0.2.2 ebgp-multihop 3
|
||||||
|
neighbor 192.0.2.2 update-source 192.0.2.1
|
||||||
|
address-family ipv4 unicast
|
||||||
|
network 198.51.100.0/24
|
||||||
|
network 203.0.113.0/24
|
||||||
|
network 10.0.0.0/24
|
||||||
|
exit-address-family
|
||||||
|
!
|
319
tests/topotests/bgp_rpki_topo1/r1/rtrd.py
Executable file
319
tests/topotests/bgp_rpki_topo1/r1/rtrd.py
Executable file
@ -0,0 +1,319 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
# Copyright (C) 2023 Tomas Hlavacek (tmshlvck@gmail.com)
|
||||||
|
|
||||||
|
from typing import List, Tuple, Callable, Type
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import socketserver
|
||||||
|
import struct
|
||||||
|
import ipaddress
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
LISTEN_HOST, LISTEN_PORT = "0.0.0.0", 15432
|
||||||
|
VRPS_FILE = os.path.join(sys.path[0], "vrps.csv")
|
||||||
|
|
||||||
|
|
||||||
|
def dbg(m: str):
|
||||||
|
print(m)
|
||||||
|
|
||||||
|
|
||||||
|
class RTRDatabase(object):
|
||||||
|
def __init__(self, vrps_file: str) -> None:
|
||||||
|
self.last_serial = 0
|
||||||
|
self.ann4 = []
|
||||||
|
self.ann6 = []
|
||||||
|
self.withdraw4 = []
|
||||||
|
self.withdraw6 = []
|
||||||
|
|
||||||
|
with open(vrps_file, "r") as fh:
|
||||||
|
for rasn, rnet, rmaxlen, _ in csv.reader(fh):
|
||||||
|
try:
|
||||||
|
net = ipaddress.ip_network(rnet)
|
||||||
|
asn = int(rasn[2:])
|
||||||
|
maxlen = int(rmaxlen)
|
||||||
|
if net.version == 6:
|
||||||
|
self.ann6.append((asn, str(net), maxlen))
|
||||||
|
elif net.version == 4:
|
||||||
|
self.ann4.append((asn, str(net), maxlen))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown AFI: {net.version}")
|
||||||
|
except Exception as e:
|
||||||
|
dbg(
|
||||||
|
f"VRPS load: ignoring {str((rasn, rnet,rmaxlen))} because {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_serial(self) -> int:
|
||||||
|
return self.last_serial
|
||||||
|
|
||||||
|
def set_serial(self, serial: int) -> None:
|
||||||
|
self.last_serial = serial
|
||||||
|
|
||||||
|
def get_announcements4(self, serial: int = 0) -> List[Tuple[int, str, int]]:
|
||||||
|
if serial > self.last_serial:
|
||||||
|
return self.ann4
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_withdrawals4(self, serial: int = 0) -> List[Tuple[int, str, int]]:
|
||||||
|
if serial > self.last_serial:
|
||||||
|
return self.withdraw4
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_announcements6(self, serial: int = 0) -> List[Tuple[int, str, int]]:
|
||||||
|
if serial > self.last_serial:
|
||||||
|
return self.ann6
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_withdrawals6(self, serial: int = 0) -> List[Tuple[int, str, int]]:
|
||||||
|
if serial > self.last_serial:
|
||||||
|
return self.withdraw6
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class RTRConnHandler(socketserver.BaseRequestHandler):
|
||||||
|
PROTO_VERSION = 0
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
self.session_id = 2345
|
||||||
|
self.serial = 1024
|
||||||
|
|
||||||
|
dbg(f"New connection from: {str(self.client_address)} ")
|
||||||
|
# TODO: register for notifies
|
||||||
|
|
||||||
|
def finish(self) -> None:
|
||||||
|
pass
|
||||||
|
# TODO: de-register
|
||||||
|
|
||||||
|
HEADER_LEN = 8
|
||||||
|
|
||||||
|
def decode_header(self, buf: bytes) -> Tuple[int, int, int, int]:
|
||||||
|
# common header in all received packets
|
||||||
|
return struct.unpack("!BBHI", buf)
|
||||||
|
# reutnrs (proto_ver, pdu_type, sess_id, length)
|
||||||
|
|
||||||
|
SERNOTIFY_TYPE = 0
|
||||||
|
SERNOTIFY_LEN = 12
|
||||||
|
|
||||||
|
def send_sernotify(self, serial: int) -> None:
|
||||||
|
# serial notify PDU
|
||||||
|
dbg(f"<Serial Notify session_id={self.session_id} serial={serial}")
|
||||||
|
self.request.send(
|
||||||
|
struct.pack(
|
||||||
|
"!BBHII",
|
||||||
|
self.PROTO_VERSION,
|
||||||
|
self.SERNOTIFY_TYPE,
|
||||||
|
self.session_id,
|
||||||
|
self.SERNOTIFY_LEN,
|
||||||
|
serial,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
CACHERESPONSE_TYPE = 3
|
||||||
|
CACHERESPONSE_LEN = 8
|
||||||
|
|
||||||
|
def send_cacheresponse(self) -> None:
|
||||||
|
# cache response PDU
|
||||||
|
dbg(f"<Cache response session_id={self.session_id}")
|
||||||
|
self.request.send(
|
||||||
|
struct.pack(
|
||||||
|
"!BBHI",
|
||||||
|
self.PROTO_VERSION,
|
||||||
|
self.CACHERESPONSE_TYPE,
|
||||||
|
self.session_id,
|
||||||
|
self.CACHERESPONSE_LEN,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
FLAGS_ANNOUNCE = 1
|
||||||
|
FLAGS_WITHDRAW = 0
|
||||||
|
|
||||||
|
IPV4_TYPE = 4
|
||||||
|
IPV4_LEN = 20
|
||||||
|
|
||||||
|
def send_ipv4(self, ipnet: str, asn: int, maxlen: int, flags: int):
|
||||||
|
# IPv4 PDU
|
||||||
|
dbg(f"<IPv4 net={ipnet} asn={asn} maxlen={maxlen} flags={flags}")
|
||||||
|
ip = ipaddress.IPv4Network(ipnet)
|
||||||
|
self.request.send(
|
||||||
|
struct.pack(
|
||||||
|
"!BBHIBBBB4sI",
|
||||||
|
self.PROTO_VERSION,
|
||||||
|
self.IPV4_TYPE,
|
||||||
|
0,
|
||||||
|
self.IPV4_LEN,
|
||||||
|
flags,
|
||||||
|
ip.prefixlen,
|
||||||
|
maxlen,
|
||||||
|
0,
|
||||||
|
ip.network_address.packed,
|
||||||
|
asn,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def announce_ipv4(self, ipnet, asn, maxlen):
|
||||||
|
self.send_ipv4(ipnet, asn, maxlen, self.FLAGS_ANNOUNCE)
|
||||||
|
|
||||||
|
def withdraw_ipv4(self, ipnet, asn, maxlen):
|
||||||
|
self.send_ipv4(ipnet, asn, maxlen, self.FLAGS_WITHDRAW)
|
||||||
|
|
||||||
|
IPV6_TYPE = 6
|
||||||
|
IPV6_LEN = 32
|
||||||
|
|
||||||
|
def send_ipv6(self, ipnet: str, asn: int, maxlen: int, flags: int):
|
||||||
|
# IPv6 PDU
|
||||||
|
dbg(f"<IPv6 net={ipnet} asn={asn} maxlen={maxlen} flags={flags}")
|
||||||
|
ip = ipaddress.IPv6Network(ipnet)
|
||||||
|
self.request.send(
|
||||||
|
struct.pack(
|
||||||
|
"!BBHIBBBB16sI",
|
||||||
|
self.PROTO_VERSION,
|
||||||
|
self.IPV6_TYPE,
|
||||||
|
0,
|
||||||
|
self.IPV6_LEN,
|
||||||
|
flags,
|
||||||
|
ip.prefixlen,
|
||||||
|
maxlen,
|
||||||
|
0,
|
||||||
|
ip.network_address.packed,
|
||||||
|
asn,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def announce_ipv6(self, ipnet: str, asn: int, maxlen: int):
|
||||||
|
self.send_ipv6(ipnet, asn, maxlen, self.FLAGS_ANNOUNCE)
|
||||||
|
|
||||||
|
def withdraw_ipv6(self, ipnet: str, asn: int, maxlen: int):
|
||||||
|
self.send_ipv6(ipnet, asn, maxlen, self.FLAGS_WITHDRAW)
|
||||||
|
|
||||||
|
EOD_TYPE = 7
|
||||||
|
EOD_LEN = 12
|
||||||
|
|
||||||
|
def send_endofdata(self, serial: int):
|
||||||
|
# end of data PDU
|
||||||
|
dbg(f"<End of Data session_id={self.session_id} serial={serial}")
|
||||||
|
self.server.db.set_serial(serial)
|
||||||
|
self.request.send(
|
||||||
|
struct.pack(
|
||||||
|
"!BBHII",
|
||||||
|
self.PROTO_VERSION,
|
||||||
|
self.EOD_TYPE,
|
||||||
|
self.session_id,
|
||||||
|
self.EOD_LEN,
|
||||||
|
serial,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
CACHERESET_TYPE = 8
|
||||||
|
CACHERESET_LEN = 8
|
||||||
|
|
||||||
|
def send_cachereset(self):
|
||||||
|
# cache reset PDU
|
||||||
|
dbg("<Cache Reset")
|
||||||
|
self.request.send(
|
||||||
|
struct.pack(
|
||||||
|
"!BBHI",
|
||||||
|
self.PROTO_VERSION,
|
||||||
|
self.CACHERESET_TYPE,
|
||||||
|
0,
|
||||||
|
self.CACHERESET_LEN,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
SERIAL_QUERY_TYPE = 1
|
||||||
|
SERIAL_QUERY_LEN = 12
|
||||||
|
|
||||||
|
def handle_serial_query(self, buf: bytes, sess_id: int):
|
||||||
|
serial = struct.unpack("!I", buf)[0]
|
||||||
|
dbg(f">Serial query: {serial}")
|
||||||
|
if sess_id:
|
||||||
|
self.server.db.set_serial(serial)
|
||||||
|
else:
|
||||||
|
self.server.db.set_serial(0)
|
||||||
|
self.send_cacheresponse()
|
||||||
|
|
||||||
|
for asn, ipnet, maxlen in self.server.db.get_announcements4(serial):
|
||||||
|
self.announce_ipv4(ipnet, asn, maxlen)
|
||||||
|
|
||||||
|
for asn, ipnet, maxlen in self.server.db.get_withdrawals4(serial):
|
||||||
|
self.withdraw_ipv4(ipnet, asn, maxlen)
|
||||||
|
|
||||||
|
for asn, ipnet, maxlen in self.server.db.get_announcements6(serial):
|
||||||
|
self.announce_ipv6(ipnet, asn, maxlen)
|
||||||
|
|
||||||
|
for asn, ipnet, maxlen in self.server.db.get_withdrawals6(serial):
|
||||||
|
self.withdraw_ipv6(ipnet, asn, maxlen)
|
||||||
|
|
||||||
|
self.send_endofdata(self.serial)
|
||||||
|
|
||||||
|
RESET_TYPE = 2
|
||||||
|
|
||||||
|
def handle_reset(self):
|
||||||
|
dbg(">Reset")
|
||||||
|
self.session_id += 1
|
||||||
|
self.server.db.set_serial(0)
|
||||||
|
self.send_cacheresponse()
|
||||||
|
|
||||||
|
for asn, ipnet, maxlen in self.server.db.get_announcements4(self.serial):
|
||||||
|
self.announce_ipv4(ipnet, asn, maxlen)
|
||||||
|
|
||||||
|
for asn, ipnet, maxlen in self.server.db.get_announcements6(self.serial):
|
||||||
|
self.announce_ipv6(ipnet, asn, maxlen)
|
||||||
|
|
||||||
|
self.send_endofdata(self.serial)
|
||||||
|
|
||||||
|
ERROR_TYPE = 10
|
||||||
|
|
||||||
|
def handle_error(self, buf: bytes):
|
||||||
|
dbg(f">Error: {str(buf)}")
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
while True:
|
||||||
|
b = self.request.recv(self.HEADER_LEN, socket.MSG_WAITALL)
|
||||||
|
if len(b) == 0:
|
||||||
|
break
|
||||||
|
proto_ver, pdu_type, sess_id, length = self.decode_header(b)
|
||||||
|
dbg(
|
||||||
|
f">Header proto_ver={proto_ver} pdu_type={pdu_type} sess_id={sess_id} length={length}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if sess_id:
|
||||||
|
self.session_id = sess_id
|
||||||
|
|
||||||
|
if pdu_type == self.SERIAL_QUERY_TYPE:
|
||||||
|
b = self.request.recv(
|
||||||
|
self.SERIAL_QUERY_LEN - self.HEADER_LEN, socket.MSG_WAITALL
|
||||||
|
)
|
||||||
|
self.handle_serial_query(b, sess_id)
|
||||||
|
|
||||||
|
elif pdu_type == self.RESET_TYPE:
|
||||||
|
self.handle_reset()
|
||||||
|
|
||||||
|
elif pdu_type == self.ERROR_TYPE:
|
||||||
|
b = self.request.recv(length - self.HEADER_LEN, socket.MSG_WAITALL)
|
||||||
|
self.handle_error(b)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||||
|
def __init__(
|
||||||
|
self, bind: Tuple[str, int], handler: Type[RTRConnHandler], db: RTRDatabase
|
||||||
|
) -> None:
|
||||||
|
super().__init__(bind, handler)
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
db = RTRDatabase(VRPS_FILE)
|
||||||
|
server = ThreadedTCPServer((LISTEN_HOST, LISTEN_PORT), RTRConnHandler, db)
|
||||||
|
dbg(f"Server listening on {LISTEN_HOST} port {LISTEN_PORT}")
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
1
tests/topotests/bgp_rpki_topo1/r1/staticd.conf
Normal file
1
tests/topotests/bgp_rpki_topo1/r1/staticd.conf
Normal file
@ -0,0 +1 @@
|
|||||||
|
ip route 192.0.2.2/32 192.168.1.2
|
3
tests/topotests/bgp_rpki_topo1/r1/vrps.csv
Normal file
3
tests/topotests/bgp_rpki_topo1/r1/vrps.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ASN,IP Prefix,Max Length,Trust Anchor
|
||||||
|
AS65530,198.51.100.0/24,24,private
|
||||||
|
AS65530,203.0.113.0/24,24,private
|
|
6
tests/topotests/bgp_rpki_topo1/r1/zebra.conf
Normal file
6
tests/topotests/bgp_rpki_topo1/r1/zebra.conf
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
interface lo
|
||||||
|
ip address 192.0.2.1/32
|
||||||
|
!
|
||||||
|
interface r1-eth0
|
||||||
|
ip address 192.168.1.1/24
|
||||||
|
!
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"routerId": "192.0.2.2",
|
||||||
|
"defaultLocPrf": 100,
|
||||||
|
"localAS": 65002,
|
||||||
|
"routes": {
|
||||||
|
"198.51.100.0/24": [
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"bestpath": true,
|
||||||
|
"selectionReason": "First path received",
|
||||||
|
"pathFrom": "external",
|
||||||
|
"prefix": "198.51.100.0",
|
||||||
|
"prefixLen": 24,
|
||||||
|
"network": "198.51.100.0/24",
|
||||||
|
"metric": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"path": "65530",
|
||||||
|
"origin": "IGP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"203.0.113.0/24": [
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"bestpath": true,
|
||||||
|
"selectionReason": "First path received",
|
||||||
|
"pathFrom": "external",
|
||||||
|
"prefix": "203.0.113.0",
|
||||||
|
"prefixLen": 24,
|
||||||
|
"network": "203.0.113.0/24",
|
||||||
|
"metric": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"path": "65530",
|
||||||
|
"origin": "IGP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"routerId": "192.0.2.2",
|
||||||
|
"defaultLocPrf": 100,
|
||||||
|
"localAS": 65002,
|
||||||
|
"routes": {
|
||||||
|
}
|
||||||
|
}
|
1
tests/topotests/bgp_rpki_topo1/r2/bgp_table_rmap_rpki_valid.json
Symbolic link
1
tests/topotests/bgp_rpki_topo1/r2/bgp_table_rmap_rpki_valid.json
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
bgp_table_rpki_valid.json
|
52
tests/topotests/bgp_rpki_topo1/r2/bgp_table_rpki_any.json
Normal file
52
tests/topotests/bgp_rpki_topo1/r2/bgp_table_rpki_any.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"routerId": "192.0.2.2",
|
||||||
|
"defaultLocPrf": 100,
|
||||||
|
"localAS": 65002,
|
||||||
|
"routes": {
|
||||||
|
"10.0.0.0/24": [
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"bestpath": true,
|
||||||
|
"selectionReason": "First path received",
|
||||||
|
"pathFrom": "external",
|
||||||
|
"prefix": "10.0.0.0",
|
||||||
|
"prefixLen": 24,
|
||||||
|
"network": "10.0.0.0/24",
|
||||||
|
"metric": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"path": "65530",
|
||||||
|
"origin": "IGP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"198.51.100.0/24": [
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"bestpath": true,
|
||||||
|
"selectionReason": "First path received",
|
||||||
|
"pathFrom": "external",
|
||||||
|
"prefix": "198.51.100.0",
|
||||||
|
"prefixLen": 24,
|
||||||
|
"network": "198.51.100.0/24",
|
||||||
|
"metric": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"path": "65530",
|
||||||
|
"origin": "IGP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"203.0.113.0/24": [
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"bestpath": true,
|
||||||
|
"selectionReason": "First path received",
|
||||||
|
"pathFrom": "external",
|
||||||
|
"prefix": "203.0.113.0",
|
||||||
|
"prefixLen": 24,
|
||||||
|
"network": "203.0.113.0/24",
|
||||||
|
"metric": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"path": "65530",
|
||||||
|
"origin": "IGP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"routerId": "192.0.2.2",
|
||||||
|
"defaultLocPrf": 100,
|
||||||
|
"localAS": 65002,
|
||||||
|
"routes": {
|
||||||
|
"10.0.0.0/24": [
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"bestpath": true,
|
||||||
|
"selectionReason": "First path received",
|
||||||
|
"pathFrom": "external",
|
||||||
|
"prefix": "10.0.0.0",
|
||||||
|
"prefixLen": 24,
|
||||||
|
"network": "10.0.0.0/24",
|
||||||
|
"metric": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"path": "65530",
|
||||||
|
"origin": "IGP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
35
tests/topotests/bgp_rpki_topo1/r2/bgp_table_rpki_valid.json
Normal file
35
tests/topotests/bgp_rpki_topo1/r2/bgp_table_rpki_valid.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"routerId": "192.0.2.2",
|
||||||
|
"defaultLocPrf": 100,
|
||||||
|
"localAS": 65002,
|
||||||
|
"routes": {
|
||||||
|
"198.51.100.0/24": [
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"bestpath": true,
|
||||||
|
"pathFrom": "external",
|
||||||
|
"prefix": "198.51.100.0",
|
||||||
|
"prefixLen": 24,
|
||||||
|
"network": "198.51.100.0/24",
|
||||||
|
"metric": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"path": "65530",
|
||||||
|
"origin": "IGP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"203.0.113.0/24": [
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"bestpath": true,
|
||||||
|
"pathFrom": "external",
|
||||||
|
"prefix": "203.0.113.0",
|
||||||
|
"prefixLen": 24,
|
||||||
|
"network": "203.0.113.0/24",
|
||||||
|
"metric": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"path": "65530",
|
||||||
|
"origin": "IGP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
19
tests/topotests/bgp_rpki_topo1/r2/bgpd.conf
Normal file
19
tests/topotests/bgp_rpki_topo1/r2/bgpd.conf
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
router bgp 65002
|
||||||
|
no bgp ebgp-requires-policy
|
||||||
|
neighbor 192.0.2.1 remote-as 65530
|
||||||
|
neighbor 192.0.2.1 timers connect 1
|
||||||
|
neighbor 192.0.2.1 ebgp-multihop 3
|
||||||
|
neighbor 192.0.2.1 update-source 192.0.2.2
|
||||||
|
!
|
||||||
|
router bgp 65002 vrf vrf10
|
||||||
|
no bgp ebgp-requires-policy
|
||||||
|
neighbor 192.0.2.3 remote-as 65530
|
||||||
|
neighbor 192.0.2.3 timers 1 3
|
||||||
|
neighbor 192.0.2.3 timers connect 1
|
||||||
|
neighbor 192.0.2.3 ebgp-multihop 3
|
||||||
|
neighbor 192.0.2.3 update-source 192.0.2.2
|
||||||
|
!
|
||||||
|
rpki
|
||||||
|
rpki retry_interval 5
|
||||||
|
rpki cache 192.0.2.1 15432 preference 1
|
||||||
|
exit
|
18
tests/topotests/bgp_rpki_topo1/r2/rpki_prefix_table.json
Normal file
18
tests/topotests/bgp_rpki_topo1/r2/rpki_prefix_table.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"prefixes":[
|
||||||
|
{
|
||||||
|
"prefix":"198.51.100.0",
|
||||||
|
"prefixLenMin":24,
|
||||||
|
"prefixLenMax":24,
|
||||||
|
"asn":65530
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prefix":"203.0.113.0",
|
||||||
|
"prefixLenMin":24,
|
||||||
|
"prefixLenMax":24,
|
||||||
|
"asn":65530
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ipv4PrefixCount":2,
|
||||||
|
"ipv6PrefixCount":0
|
||||||
|
}
|
1
tests/topotests/bgp_rpki_topo1/r2/staticd.conf
Normal file
1
tests/topotests/bgp_rpki_topo1/r2/staticd.conf
Normal file
@ -0,0 +1 @@
|
|||||||
|
ip route 192.0.2.1/32 192.168.1.1
|
9
tests/topotests/bgp_rpki_topo1/r2/zebra.conf
Normal file
9
tests/topotests/bgp_rpki_topo1/r2/zebra.conf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
interface lo
|
||||||
|
ip address 192.0.2.2/32
|
||||||
|
!
|
||||||
|
interface vrf10 vrf vrf10
|
||||||
|
ip address 192.0.2.2/32
|
||||||
|
!
|
||||||
|
interface r2-eth0
|
||||||
|
ip address 192.168.1.2/24
|
||||||
|
!
|
264
tests/topotests/bgp_rpki_topo1/test_bgp_rpki_topo1.py
Normal file
264
tests/topotests/bgp_rpki_topo1/test_bgp_rpki_topo1.py
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# SPDX-License-Identifier: ISC
|
||||||
|
|
||||||
|
# Copyright 2023 6WIND S.A.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
import functools
|
||||||
|
|
||||||
|
CWD = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
sys.path.append(os.path.join(CWD, "../"))
|
||||||
|
|
||||||
|
# pylint: disable=C0413
|
||||||
|
from lib import topotest
|
||||||
|
from lib.topogen import Topogen, TopoRouter, get_topogen
|
||||||
|
from lib.common_config import step
|
||||||
|
from lib.topolog import logger
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.bgpd]
|
||||||
|
|
||||||
|
|
||||||
|
def build_topo(tgen):
|
||||||
|
for routern in range(1, 3):
|
||||||
|
tgen.add_router("r{}".format(routern))
|
||||||
|
|
||||||
|
switch = tgen.add_switch("s1")
|
||||||
|
switch.add_link(tgen.gears["r1"])
|
||||||
|
switch.add_link(tgen.gears["r2"])
|
||||||
|
|
||||||
|
|
||||||
|
def setup_module(mod):
|
||||||
|
tgen = Topogen(build_topo, mod.__name__)
|
||||||
|
tgen.start_topology()
|
||||||
|
|
||||||
|
router_list = tgen.routers()
|
||||||
|
|
||||||
|
for i, (rname, router) in enumerate(router_list.items(), 1):
|
||||||
|
router.load_config(
|
||||||
|
TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
|
||||||
|
)
|
||||||
|
router.load_config(
|
||||||
|
TopoRouter.RD_STATIC, os.path.join(CWD, "{}/staticd.conf".format(rname))
|
||||||
|
)
|
||||||
|
router.load_config(
|
||||||
|
TopoRouter.RD_BGP,
|
||||||
|
os.path.join(CWD, "{}/bgpd.conf".format(rname)),
|
||||||
|
" -M bgpd_rpki" if rname == "r2" else "",
|
||||||
|
)
|
||||||
|
|
||||||
|
tgen.start_router()
|
||||||
|
|
||||||
|
r1_path = os.path.join(CWD, "r1")
|
||||||
|
|
||||||
|
global rtrd_process
|
||||||
|
|
||||||
|
tgen.gears["r1"].cmd("chmod u+x {}/rtrd.py".format(r1_path))
|
||||||
|
rtrd_process = tgen.gears["r1"].popen("python3 {}/rtrd.py".format(r1_path))
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_module(mod):
|
||||||
|
tgen = get_topogen()
|
||||||
|
|
||||||
|
logger.info("r1: sending SIGTERM to rtrd RPKI server")
|
||||||
|
rtrd_process.kill()
|
||||||
|
tgen.stop_topology()
|
||||||
|
|
||||||
|
|
||||||
|
def show_rpki_prefixes(rname, expected, vrf=None):
|
||||||
|
tgen = get_topogen()
|
||||||
|
|
||||||
|
if vrf:
|
||||||
|
cmd = "show rpki prefix-table vrf {} json".format(vrf)
|
||||||
|
else:
|
||||||
|
cmd = "show rpki prefix-table json"
|
||||||
|
|
||||||
|
output = json.loads(tgen.gears[rname].vtysh_cmd(cmd))
|
||||||
|
|
||||||
|
return topotest.json_cmp(output, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def show_bgp_ipv4_table_rpki(rname, rpki_state, expected, vrf=None):
|
||||||
|
tgen = get_topogen()
|
||||||
|
|
||||||
|
cmd = "show bgp"
|
||||||
|
if vrf:
|
||||||
|
cmd += " vrf {}".format(vrf)
|
||||||
|
cmd += " ipv4 unicast"
|
||||||
|
if rpki_state:
|
||||||
|
cmd += " rpki {}".format(rpki_state)
|
||||||
|
cmd += " json"
|
||||||
|
|
||||||
|
output = json.loads(tgen.gears[rname].vtysh_cmd(cmd))
|
||||||
|
|
||||||
|
expected_nb = len(expected.get("routes"))
|
||||||
|
output_nb = len(output.get("routes", {}))
|
||||||
|
|
||||||
|
if expected_nb != output_nb:
|
||||||
|
return {"error": "expected {} prefixes. Got {}".format(expected_nb, output_nb)}
|
||||||
|
|
||||||
|
return topotest.json_cmp(output, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_bgp_rpki_prefixes():
|
||||||
|
tgen = get_topogen()
|
||||||
|
|
||||||
|
if tgen.routers_have_failure():
|
||||||
|
pytest.skip(tgen.errors)
|
||||||
|
|
||||||
|
rname = "r2"
|
||||||
|
|
||||||
|
step("Check RPKI prefix table")
|
||||||
|
|
||||||
|
expected = open(os.path.join(CWD, "{}/rpki_prefix_table.json".format(rname))).read()
|
||||||
|
expected_json = json.loads(expected)
|
||||||
|
test_func = functools.partial(show_rpki_prefixes, rname, expected_json)
|
||||||
|
_, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5)
|
||||||
|
assert result is None, "Failed to see RPKI prefixes on {}".format(rname)
|
||||||
|
|
||||||
|
for rpki_state in ["valid", "notfound", None]:
|
||||||
|
if rpki_state:
|
||||||
|
step("Check RPKI state of prefixes in BGP table: {}".format(rpki_state))
|
||||||
|
else:
|
||||||
|
step("Check prefixes in BGP table")
|
||||||
|
expected = open(
|
||||||
|
os.path.join(
|
||||||
|
CWD,
|
||||||
|
"{}/bgp_table_rpki_{}.json".format(
|
||||||
|
rname, rpki_state if rpki_state else "any"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).read()
|
||||||
|
expected_json = json.loads(expected)
|
||||||
|
test_func = functools.partial(
|
||||||
|
show_bgp_ipv4_table_rpki, rname, rpki_state, expected_json
|
||||||
|
)
|
||||||
|
_, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5)
|
||||||
|
assert result is None, "Unexpected prefixes RPKI state on {}".format(rname)
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_bgp_rpki_prefixes_no_rpki_cache():
|
||||||
|
tgen = get_topogen()
|
||||||
|
|
||||||
|
if tgen.routers_have_failure():
|
||||||
|
pytest.skip(tgen.errors)
|
||||||
|
|
||||||
|
def _show_rpki_no_connection(rname):
|
||||||
|
output = json.loads(
|
||||||
|
tgen.gears[rname].vtysh_cmd("show rpki cache-connection json")
|
||||||
|
)
|
||||||
|
|
||||||
|
return output == {"error": "No connection to RPKI cache server."}
|
||||||
|
|
||||||
|
step("Remove RPKI server from configuration")
|
||||||
|
rname = "r2"
|
||||||
|
tgen.gears[rname].vtysh_cmd(
|
||||||
|
"""
|
||||||
|
configure
|
||||||
|
rpki
|
||||||
|
no rpki cache 192.0.2.1 15432 preference 1
|
||||||
|
exit
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
step("Check RPKI connection state")
|
||||||
|
|
||||||
|
test_func = functools.partial(_show_rpki_no_connection, rname)
|
||||||
|
_, result = topotest.run_and_expect(test_func, True, count=60, wait=0.5)
|
||||||
|
assert result, "RPKI is still connected on {}".format(rname)
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_bgp_rpki_prefixes_reconnect():
|
||||||
|
tgen = get_topogen()
|
||||||
|
|
||||||
|
if tgen.routers_have_failure():
|
||||||
|
pytest.skip(tgen.errors)
|
||||||
|
|
||||||
|
step("Restore RPKI server configuration")
|
||||||
|
|
||||||
|
rname = "r2"
|
||||||
|
tgen.gears[rname].vtysh_cmd(
|
||||||
|
"""
|
||||||
|
configure
|
||||||
|
rpki
|
||||||
|
rpki cache 192.0.2.1 15432 preference 1
|
||||||
|
exit
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
step("Check RPKI prefix table")
|
||||||
|
|
||||||
|
expected = open(os.path.join(CWD, "{}/rpki_prefix_table.json".format(rname))).read()
|
||||||
|
expected_json = json.loads(expected)
|
||||||
|
test_func = functools.partial(show_rpki_prefixes, rname, expected_json)
|
||||||
|
_, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5)
|
||||||
|
assert result is None, "Failed to see RPKI prefixes on {}".format(rname)
|
||||||
|
|
||||||
|
for rpki_state in ["valid", "notfound", None]:
|
||||||
|
if rpki_state:
|
||||||
|
step("Check RPKI state of prefixes in BGP table: {}".format(rpki_state))
|
||||||
|
else:
|
||||||
|
step("Check prefixes in BGP table")
|
||||||
|
expected = open(
|
||||||
|
os.path.join(
|
||||||
|
CWD,
|
||||||
|
"{}/bgp_table_rpki_{}.json".format(
|
||||||
|
rname, rpki_state if rpki_state else "any"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).read()
|
||||||
|
expected_json = json.loads(expected)
|
||||||
|
_, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5)
|
||||||
|
assert result is None, "Unexpected prefixes RPKI state on {}".format(rname)
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_bgp_rpki_route_map():
|
||||||
|
tgen = get_topogen()
|
||||||
|
|
||||||
|
if tgen.routers_have_failure():
|
||||||
|
pytest.skip(tgen.errors)
|
||||||
|
|
||||||
|
step("Apply RPKI valid route-map on neighbor")
|
||||||
|
|
||||||
|
rname = "r2"
|
||||||
|
tgen.gears[rname].vtysh_cmd(
|
||||||
|
"""
|
||||||
|
configure
|
||||||
|
route-map RPKI permit 10
|
||||||
|
match rpki valid
|
||||||
|
!
|
||||||
|
router bgp 65002
|
||||||
|
address-family ipv4 unicast
|
||||||
|
neighbor 192.0.2.1 route-map RPKI in
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
for rpki_state in ["valid", "notfound", None]:
|
||||||
|
if rpki_state:
|
||||||
|
step("Check RPKI state of prefixes in BGP table: {}".format(rpki_state))
|
||||||
|
else:
|
||||||
|
step("Check prefixes in BGP table")
|
||||||
|
expected = open(
|
||||||
|
os.path.join(
|
||||||
|
CWD,
|
||||||
|
"{}/bgp_table_rmap_rpki_{}.json".format(
|
||||||
|
rname, rpki_state if rpki_state else "any"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).read()
|
||||||
|
expected_json = json.loads(expected)
|
||||||
|
test_func = functools.partial(
|
||||||
|
show_bgp_ipv4_table_rpki,
|
||||||
|
rname,
|
||||||
|
rpki_state,
|
||||||
|
expected_json,
|
||||||
|
)
|
||||||
|
_, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5)
|
||||||
|
assert result is None, "Unexpected prefixes RPKI state on {}".format(rname)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = ["-s"] + sys.argv[1:]
|
||||||
|
sys.exit(pytest.main(args))
|
Loading…
Reference in New Issue
Block a user