From a0e6cd4c8fb954f66546b056b0016b4ca7a0a430 Mon Sep 17 00:00:00 2001 From: Louis Scalbert Date: Mon, 16 Jan 2023 15:43:54 +0100 Subject: [PATCH] tests: add bgp_linkstate_topo1 Use an external BGP injector tool in router r1. Check that bgpd on r2 is able to decode BGP-LS prefixes and re-encode to the r3 instance. Link: https://github.com/louis-6wind/bgp_injector Signed-off-by: Louis Scalbert --- .../topotests/bgp_linkstate_topo1/__init__.py | 0 .../bgp_linkstate_topo1/r1/bgp_injector.cfg | 202 ++++++ .../bgp_linkstate_topo1/r1/bgp_injector.py | 596 ++++++++++++++++++ .../bgp_linkstate_topo1/r1/staticd.conf | 1 + .../bgp_linkstate_topo1/r1/zebra.conf | 7 + .../bgp_linkstate_topo1/r2/bgpd.conf | 20 + .../bgp_linkstate_topo1/r2/linkstate.json | 189 ++++++ .../bgp_linkstate_topo1/r2/staticd.conf | 2 + .../bgp_linkstate_topo1/r2/zebra.conf | 11 + .../bgp_linkstate_topo1/r3/bgpd.conf | 14 + .../bgp_linkstate_topo1/r3/linkstate.json | 1 + .../bgp_linkstate_topo1/r3/staticd.conf | 1 + .../bgp_linkstate_topo1/r3/zebra.conf | 7 + .../test_bgp_linkstate_topo1.py | 113 ++++ 14 files changed, 1164 insertions(+) create mode 100644 tests/topotests/bgp_linkstate_topo1/__init__.py create mode 100644 tests/topotests/bgp_linkstate_topo1/r1/bgp_injector.cfg create mode 100755 tests/topotests/bgp_linkstate_topo1/r1/bgp_injector.py create mode 100644 tests/topotests/bgp_linkstate_topo1/r1/staticd.conf create mode 100644 tests/topotests/bgp_linkstate_topo1/r1/zebra.conf create mode 100644 tests/topotests/bgp_linkstate_topo1/r2/bgpd.conf create mode 100644 tests/topotests/bgp_linkstate_topo1/r2/linkstate.json create mode 100644 tests/topotests/bgp_linkstate_topo1/r2/staticd.conf create mode 100644 tests/topotests/bgp_linkstate_topo1/r2/zebra.conf create mode 100644 tests/topotests/bgp_linkstate_topo1/r3/bgpd.conf create mode 120000 tests/topotests/bgp_linkstate_topo1/r3/linkstate.json create mode 100644 tests/topotests/bgp_linkstate_topo1/r3/staticd.conf create mode 100644 tests/topotests/bgp_linkstate_topo1/r3/zebra.conf create mode 100644 tests/topotests/bgp_linkstate_topo1/test_bgp_linkstate_topo1.py diff --git a/tests/topotests/bgp_linkstate_topo1/__init__.py b/tests/topotests/bgp_linkstate_topo1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/topotests/bgp_linkstate_topo1/r1/bgp_injector.cfg b/tests/topotests/bgp_linkstate_topo1/r1/bgp_injector.cfg new file mode 100644 index 0000000000..30fbc7d847 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r1/bgp_injector.cfg @@ -0,0 +1,202 @@ +// Check content with +// cat bgp_injector.cfg | sed -e 's|//.*||g' | jq . +{ +"my_as": 65001, +"hold_time": 30, +"bgp_identifier": "192.0.2.1", +"local_address": "192.0.2.1", +"peer_address": "192.0.2.2", +"mss": 4000, +"port": 179, +"path_attributes": +{ + "as-path": "65001", + "next-hop": "192.0.2.1", + "origin": 0 +}, +"link_states": +[ + { + "nlri": + { + "proto": "01", // IS-IS L1 + "id": "0000000000000020", + "type": "0002", // Link-NLRI + "256": { // Local Link-Node Descriptor TLV + "512": "0000fde9", // AS 65001 + "513": "00000000", // BGP-LS ID + "515": "000000001001" // router-id: 0000.0000.1001 + }, + "257": { // Remote Link-Node Descriptor TLV + "512": "0000fde9", // AS 65001 + "513": "00000000", // BGP-LS ID + "515": "000000001000" // router-id: 0000.0000.1000 + }, + "259": "0a010001", // IPv4 interface address TLV + "260": "0a010002", // IPv4 Neighbor address TLV + "261": "20010000000000000000000000000001", // IPv6 interface address TLV + "262": "20010000000000000000000000000002", // IPv6 Neighbor address TLV + "263": "00000002" // MT-ID + }, + "attr": + { + "1028": "01010101", //IPv4 Router-ID of Local Node TLV + "1030": "0a0a0a0a", //IPv4 Router-ID of Remote Node TLV + "1089": "4d2817c8", // Maximum link bandwidth TLV 1410.07 Mbps + "1090": "4d2817c8", // Maximum reservable link bandwidth TLV 1410.07 Mbps + "1091": "4d2817c84d2817c84d2817c84d2817c84d2817c84d2817c84d2817c84d2817c8", // Unreserved bandwidth TLV + "1092": "00000064", // TE Default Metric TLV + "1095": "00000a", // Metric TLV + // Adjacency SID TLV + // Flags: 0x30, Value Flag (V), Local Flag (L) + // Weight: 0 + // .... 0000 0011 1010 1001 1000 = SID/Label: 15000 + "1099": "30000000003a98", + //Unidirectional Link Delay TLV + // TE Metric Flags: 0x00 + // Delay: 8500 + "1114": "00002134", + //Min/Max Unidirectional Link Delay TLV + // TE Metric Flags: 0x00 + // Min Delay: 8000 + // Reserved: 0x00 + // Max Delay: 9000 + "1115": "00001f4000002328", + "1122": { //Application-Specific Link Attributes TLV + // Type: 1122 + // Length: 48 + // SABM Length: 4 + // UDABM Length: 4 + // Reserved: 0x0000 + // Standard Application Identifier Bit Mask: 0x10000000, Flexible Algorithm (X) + // User-Defined Application Identifier Bit Mask: 00 00 00 00 + "0": "040400001000000000000000", // 0 means encode data directly + "1088": "00000001", // Administrative group (color) TLV + "1092": "00000064", // TE Default Metric TLV + "1115": "00001f4000002328", // Min/Max Unidirectional Link Delay TLV + "1173": "00000001"// Extended Administrative Group TLV + } + } + }, + { + "nlri": + { + "proto": "01", // IS-IS L1 + "id": "0000000000000020", + "type": "0001", // Node-NLRI + "256": { // Local Link-Node Descriptor TLV + "512": "0000fde9", // AS 65001 + "513": "00000000", // BGP-LS ID + "515": "00000000100300" // router-id: 0000.0000.1003.00 + } + }, + "attr": + { + "0": "0107000400000002010a00020108040200027233040300034910000404000403030303040a000cc000000fa004890003004e20040b0003008082040c000c00000003e804890003003a98" + } + }, + { + "nlri": + { + "proto": "03", // OSPFv2 + "id": "0000000000000020", + "type": "0001", // Node-NLRI + "256": { // Local Link-Node Descriptor TLV + "512": "0000fde9", // AS 65001 + "513": "00000000", // BGP-LS ID + "514": "00000000", // Area 0 + "515": "0a0a0a0a" // router-id: 10.10.10.10 + } + } + }, + { + "nlri": + { + "proto": "03", // OSPFv2 + "id": "0000000000000020", + "type": "0001", // Node-NLRI + "256": { // Local Link-Node Descriptor TLV + "512": "0000fde9", // AS 65001 + "513": "00000000", // BGP-LS ID + "514": "00000000", // Area 0 + "515": "0a0a0a0a01010101" // router-id: 10.10.10.10:1.1.1.1 + } + } + }, + { + "nlri": + { + "proto": "03", // OSPFv2 + "id": "0000000000000020", + "type": "0003", // IPv4-topo-prefix-NLRI + "256": { // Local Link-Node Descriptor TLV + "512": "0000fde9", // AS 65001 + "513": "00000000", // BGP-LS ID + "514": "00000000", // Area 0 + "515": "0a0a0a0a01010101" // router-id: 10.10.10.10:1.1.1.1 + }, + "265": "18590a0b" // IP Reachability Information TLV (89.10.11.0/24) + } + }, + { + "nlri": + { + "proto": "02", // IS-IS L2 + "id": "0000000000000020", + "type": "0004", // IPv6-topo-prefix-NLRI + "256": { // Local Link-Node Descriptor TLV + "512": "0000fde9", // AS 65001 + "513": "00000000", // BGP-LS ID + "515": "00000000100300" // router-id: 0000.0000.1003.00 + }, + "263": "0002", // MT-ID + // IP Reachability Information TLV (12:12::12:12/128) + "265": "8000120012000000000000000000120012" + } + }, + { + "nlri": + { + "proto": "06", // OSPFv3 + "id": "0000000000000020", + "type": "0004", // IPv6-topo-prefix-NLRI + "256": { // Local Link-Node Descriptor TLV + "512": "0000fde9", // AS 65001 + "513": "00000000", // BGP-LS ID + "514": "00000000", // Area 0 + "515": "0a0a0a0a" // router-id: 10.10.10.10 + }, + "263": "0002", // MT-ID + "264": "01", // OSPF: route-type Intra-Area (0x1) + // IP Reachability Information TLV (12:12::12:12/128) + "265": "8000120012000000000000000000120012" + } + }, + { + "nlri": + { + "proto": "06", // OSPFv3 + "id": "ffffffffffffffff", + "type": "0002", // Link-NLRI + "256": { // Local Link-Node Descriptor TLV + "512": "ffffffff", // AS + "513": "ffffffff", // BGP-LS ID + "514": "ffffffff", // OSPF area ID + "515": "0a0a0a0b02020202" // router-id: 10.10.10.11:2.2.2.2 + }, + "257": { // Remote Link-Node Descriptor TLV + "512": "ffffffff", // AS + "513": "ffffffff", // BGP-LS ID + "514": "ffffffff", // OSPF area ID + "515": "0a0a0a0a01010101" // router-id: 10.10.10.10:1.1.1.1 + }, + "259": "0a010001", // IPv4 interface address TLV + "260": "0a010002", // IPv4 Neighbor address TLV + "261": "20010000000000000000000000000001", // IPv6 interface address TLV + "262": "20010000000000000000000000000002", // IPv6 Neighbor address TLV + "263": "00000002", // MT-ID + "424": "200100000000000001" // unknown TLV + } + } +] +} diff --git a/tests/topotests/bgp_linkstate_topo1/r1/bgp_injector.py b/tests/topotests/bgp_linkstate_topo1/r1/bgp_injector.py new file mode 100755 index 0000000000..314b8fe44c --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r1/bgp_injector.py @@ -0,0 +1,596 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT + +# +# Copyright 2018 Jorge Borreicho +# Copyright 2023 6WIND S.A. + +""" + BGP prefix injection tool +""" + +import socket +import sys +import time +from datetime import datetime +import struct +import threading +import json +import os +import re +import signal +import errno + + +AFI_IPV4 = 1 +SAFI_UNICAST = 1 + +AFI_LINKSTATE = 16388 +SAFI_LINKSTATE = 71 + +saved_pid = False +global pid_file + +class Unbuffered(object): + def __init__(self, stream): + self.stream = stream + def write(self, data): + self.stream.write(data) + self.stream.flush() + def writelines(self, datas): + self.stream.writelines(datas) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +def keepalive_thread(conn, interval): + + # infinite loop so that function do not terminate and thread do not end. + while True: + time.sleep(interval) + keepalive_bgp(conn) + + +def receive_thread(conn): + + # infinite loop so that function do not terminate and thread do not end. + while True: + + # Receiving from client + r = conn.recv(1500) + while True: + start_ptr = ( + r.find( + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + ) + + 16 + ) + end_ptr = ( + r[16:].find( + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + ) + + 16 + ) + if ( + start_ptr >= end_ptr + ): # a single message was sent in the BGP packet OR it is the last message of the BGP packet + decode_bgp(r[start_ptr:]) + break + else: # more messages left to decode + decode_bgp(r[start_ptr:end_ptr]) + r = r[end_ptr:] + + +def decode_bgp(msg): + if len(msg) < 3: + return + msg_length, msg_type = struct.unpack("!HB", msg[0:3]) + if msg_type == 4: + # print(timestamp + " - " + "Received KEEPALIVE") #uncomment to debug + pass + elif msg_type == 2: + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Received UPDATE") + elif msg_type == 1: + version, remote_as, holdtime, i1, i2, i3, i4, opt_length = struct.unpack( + "!BHHBBBBB", msg[3:13] + ) + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Received OPEN") + print() + print( + "--> Version:" + + str(version) + + ", Remote AS: " + + str(remote_as) + + ", Hold Time:" + + str(holdtime) + + ", Remote ID: " + + str(i1) + + "." + + str(i2) + + "." + + str(i3) + + "." + + str(i4) + ) + print() + elif msg_type == 3: + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Received NOTIFICATION") + + +def multiprotocol_capability(afi, safi): + hexstream = bytes.fromhex("02060104") + hexstream += struct.pack("!H", afi) + hexstream += struct.pack("!B", 0) + hexstream += struct.pack("!B", safi) + + return hexstream + + +def open_bgp(conn, config): + + # Build the BGP Message + bgp_version = b"\x04" + bgp_as = struct.pack("!H", config["my_as"]) + bgp_hold_time = struct.pack("!H", config["hold_time"]) + + octet = config["bgp_identifier"].split(".") + bgp_identifier = struct.pack( + "!BBBB", int(octet[0]), int(octet[1]), int(octet[2]), int(octet[3]) + ) + + bgp_opt = b"" + bgp_opt += multiprotocol_capability(AFI_IPV4, SAFI_UNICAST) + bgp_opt += multiprotocol_capability(AFI_LINKSTATE, SAFI_LINKSTATE) + + bgp_opt_lenght = struct.pack("!B", len(bgp_opt)) + + bgp_message = ( + bgp_version + bgp_as + bgp_hold_time + bgp_identifier + bgp_opt_lenght + bgp_opt + ) + + # Build the BGP Header + total_length = len(bgp_message) + 16 + 2 + 1 + bgp_marker = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + bgp_length = struct.pack("!H", total_length) + bgp_type = b"\x01" + bgp_header = bgp_marker + bgp_length + bgp_type + + bgp_packet = bgp_header + bgp_message + + conn.send(bgp_packet) + return 0 + + +def keepalive_bgp(conn): + + # Build the BGP Header + total_length = 16 + 2 + 1 + bgp_marker = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + bgp_length = struct.pack("!H", total_length) + bgp_type = b"\x04" + bgp_header = bgp_marker + bgp_length + bgp_type + + bgp_packet = bgp_header + + conn.send(bgp_packet) + return 0 + + +def encode_ipv4_prefix(address, netmask): + + octet = address.split(".") + length = struct.pack("!B", int(netmask)) + + if int(netmask) <= 8: + prefix = struct.pack("!B", int(octet[0])) + elif int(netmask) <= 16: + prefix = struct.pack("!BB", int(octet[0]), int(octet[1])) + elif int(netmask) <= 24: + prefix = struct.pack("!BBB", int(octet[0]), int(octet[1]), int(octet[2])) + else: + prefix = struct.pack( + "!BBBB", int(octet[0]), int(octet[1]), int(octet[2]), int(octet[3]) + ) + + return length + prefix + + +def encode_path_attribute_mp_reach_nrli(afi, safi, data, config): + hexstream = b"" + hexstream += b"\x90" # flags optional, extended + hexstream += struct.pack("!B", 14) # type code MP_REACH_NLRI + + hexstream2 = b"" + hexstream2 += struct.pack("!H", afi) + hexstream2 += struct.pack("!B", safi) + hexstream2 += struct.pack("!B", 4) # nexthop length + hexstream2 += socket.inet_aton(config["local_address"]) # nexthop IPv4 + hexstream2 += b"\x00" # SNPA + hexstream2 += data + + hexstream += struct.pack("!H", len(hexstream2)) # length + hexstream += hexstream2 + + return hexstream + + +def encode_path_attribute_linkstate(data): + hexstream = b"" + hexstream += b"\x80" # flags optional + hexstream += struct.pack("!B", 29) # type code BGP-LS + hexstream += struct.pack("!B", len(data)) # length + hexstream += data + + return hexstream + + +def encode_path_attribute(type, value): + + path_attributes = { + "origin": [b"\x40", 1], + "as-path": [b"\x40", 2], + "next-hop": [b"\x40", 3], + "med": [b"\x80", 4], + "local_pref": [b"\x40", 5], + "communities": [b"\xc0", 8], + } + + attribute_flag = path_attributes[type][0] + attribute_type_code = struct.pack("!B", int(path_attributes[type][1])) + + if type == "origin": + attribute_value = struct.pack("!B", value) + elif type == "as-path": + as_number_list = value.split(" ") + attribute_value = struct.pack("!BB", 2, len(as_number_list)) + for as_number in as_number_list: + attribute_value += struct.pack("!H", int(as_number)) + elif type == "next-hop": + octet = value.split(".") + attribute_value = struct.pack( + "!BBBB", int(octet[0]), int(octet[1]), int(octet[2]), int(octet[3]) + ) + elif type == "med": + attribute_value = struct.pack("!I", value) + elif type == "local_pref": + attribute_value = struct.pack("!I", value) + elif type == "communities": + communities_list = value.split(" ") + attribute_value = b"" + for community in communities_list: + aux = community.split(":") + attribute_value += struct.pack("!HH", int(aux[0]), int(aux[1])) + + attribute_length = struct.pack("!B", len(attribute_value)) + + return attribute_flag + attribute_type_code + attribute_length + attribute_value + + +def encode_tlvs(tlvs): + stream = b"" + for key, tlv_data in tlvs.items(): + if isinstance(key, str) and key.isdigit(): + tlv_type = int(key) + else: + # key is not a TLV + continue + if isinstance(tlv_data, str): + if tlv_type != 0: + # TLV type 0 is fake TLV + stream += struct.pack("!H", tlv_type) + stream += struct.pack("!H", len(bytes.fromhex(tlv_data))) + stream += bytes.fromhex(tlv_data) + elif isinstance(tlv_data, dict): + # TLV contains sub-TLV + stream += struct.pack("!H", tlv_type) + + stream_subtlv = encode_tlvs(tlv_data) + stream += struct.pack("!H", len(stream_subtlv)) + stream += stream_subtlv + else: + # invalid input + assert 0 + + return stream + + +def encode_linkstate_nrli_tlv(nlri): + stream = b"" + stream += bytes.fromhex(nlri["type"]) + + stream2 = b"" + stream2 += bytes.fromhex(nlri["proto"]) + stream2 += bytes.fromhex(nlri["id"]) + stream2 += encode_tlvs(nlri) + + stream += struct.pack("!H", len(stream2)) + stream += stream2 + + return stream + + +def update_bgp(conn, link_state, config): + + # Build the BGP Message + + # Expired Routes + # 1 - Withdrawn Routes + + bgp_withdrawn_routes = b"" + max_length_reached = False + + bgp_withdrawn_routes_length = struct.pack("!H", len(bgp_withdrawn_routes)) + bgp_withdrawn_routes = bgp_withdrawn_routes_length + bgp_withdrawn_routes + + # New Routes + # 2 - Path Attributes + + path_attributes = config["path_attributes"] + bgp_mss = config["mss"] + + bgp_total_path_attributes = b"" + + # encode link-state MP_REACH NLRI + data = encode_linkstate_nrli_tlv(link_state["nlri"]) + bgp_total_path_attributes += encode_path_attribute_mp_reach_nrli( + AFI_LINKSTATE, SAFI_LINKSTATE, data, config + ) + + # encode classic attributes + for key in path_attributes.keys(): + bgp_total_path_attributes += encode_path_attribute(key, path_attributes[key]) + + # encode link-state attributes + if "attr" in link_state: + data = encode_tlvs(link_state["attr"]) + else: + data = b"" + bgp_total_path_attributes += encode_path_attribute_linkstate(data) + + bgp_total_path_attributes_length = struct.pack("!H", len(bgp_total_path_attributes)) + bgp_total_path_attributes = ( + bgp_total_path_attributes_length + bgp_total_path_attributes + ) + + # 3- Network Layer Reachability Information (NLRI) + + bgp_new_routes = b"" + + bgp_message = bgp_withdrawn_routes + bgp_total_path_attributes + bgp_new_routes + + # Build the BGP Header + total_length = len(bgp_message) + 16 + 2 + 1 + bgp_marker = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + bgp_length = struct.pack("!H", total_length) + bgp_type = b"\x02" + bgp_header = bgp_marker + bgp_length + bgp_type + + bgp_packet = bgp_header + bgp_message + + conn.send(bgp_packet) + + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Sent UPDATE") + + return 0 + + +def str2ip(ip_str): + s_octet = ip_str.split(".") + ip_addr = struct.pack( + "!BBBB", int(s_octet[0]), int(s_octet[1]), int(s_octet[2]), int(s_octet[3]) + ) + return ip_addr + + +def check_pid(pid): + if pid < 0: # user input error + return False + if pid == 0: # all processes + return False + try: + os.kill(pid, 0) + return True + except OSError as err: + if err.errno == errno.EPERM: # a process we were denied access to + return True + if err.errno == errno.ESRCH: # No such process + return False + # should never happen + return False + + +def savepid(): + ownid = os.getpid() + + flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY + mode = ((os.R_OK | os.W_OK) << 6) | (os.R_OK << 3) | os.R_OK + + try: + fd = os.open(pid_file, flags, mode) + except OSError: + try: + pid = open(pid_file, "r").readline().strip() + if check_pid(int(pid)): + sys.stderr.write( + "PIDfile already exists and program still running %s\n" % pid_file + ) + return False + else: + # If pid is not running, reopen file without O_EXCL + fd = os.open(pid_file, flags ^ os.O_EXCL, mode) + except (OSError, IOError, ValueError): + sys.stderr.write( + "issue accessing PID file %s (most likely permission or ownership)\n" + % pid_file + ) + return False + + try: + f = os.fdopen(fd, "w") + line = "%d\n" % ownid + f.write(line) + f.close() + saved_pid = True + except IOError: + sys.stderr.write("Can not create PIDfile %s\n" % pid_file) + return False + print("Created PIDfile %s with value %d\n" % (pid_file, ownid)) + return True + + +def removepid(): + if not saved_pid: + return + try: + os.remove(pid_file) + except OSError as exc: + if exc.errno == errno.ENOENT: + pass + else: + sys.stderr.write("Can not remove PIDfile %s\n" % pid_file) + return + sys.stderr.write("Removed PIDfile %s\n" % pid_file) + + +def daemonize(): + try: + pid = os.fork() + if pid > 0: + # Exit first parent + sys.exit(0) + except OSError as e: + print("Fork #1 failed: %d (%s)" % (e.errno, e.strerror)) + sys.exit(1) + + # Decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # Do second fork + try: + pid = os.fork() + if pid > 0: + # Exit from second parent + sys.exit(0) + except OSError as e: + print("Fork #2 failed: %d (%s)" % (e.errno, e.strerror)) + sys.exit(1) + + # Redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, "r") + so = open(os.devnull, "a+") + se = open(os.devnull, "a+") + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + +def term(signal, frame): + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "^C received, shutting down.\n") + bgp_socket.close() + removepid() + exit() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + # daemonize and log to file + daemonize() + pid_file = os.path.join(sys.argv[1], "bgp_injector.pid") + savepid() + # deal with daemon termination + signal.signal(signal.SIGTERM, term) + signal.signal(signal.SIGINT, term) # CTRL + C + + log_dir = os.path.join(sys.argv[1], "bgp_injector.log") + f = open(log_dir, 'w') + sys.stdout = Unbuffered(f) + sys.stderr = Unbuffered(f) + + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Starting BGP injector ") + + CONFIG_FILENAME = os.path.join(sys.path[0], "bgp_injector.cfg") + + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Reading config file " + CONFIG_FILENAME) + + input_file = open(CONFIG_FILENAME, "r") + + input = input_file.read() + # cleanup comments that are not supported by JSON + json_input = re.sub(r"//.*\n", "", input, flags=re.MULTILINE) + + config = json.loads(json_input) + + bgp_peer = config["peer_address"] + bgp_local = config["local_address"] + bgp_mss = config["mss"] + bgp_port = config["port"] + rib = dict() + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Starting BGP... (peer: " + str(bgp_peer) + ")") + + retry = 30 + while retry: + retry -= 1 + try: + bgp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + bgp_socket.bind((bgp_local, 0)) + bgp_socket.connect((bgp_peer, bgp_port)) + open_bgp(bgp_socket, config) + break + except TimeoutError: + if retry == 0: + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Error: timeout connecting to the peer.") + exit() + time.sleep(1) + except OSError as e: + if retry == 0: + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "Error: cannot connect to the peer: " + str(e)) + exit() + time.sleep(1) + + receive_worker = threading.Thread( + target=receive_thread, args=(bgp_socket,) + ) # wait from BGP msg from peer and process them + receive_worker.setDaemon(True) + receive_worker.start() + + keepalive_worker = threading.Thread( + target=keepalive_thread, + args=( + bgp_socket, + (config["hold_time"]) / 3, + ), + ) # send keep alives every 10s by default + keepalive_worker.setDaemon(True) + keepalive_worker.start() + + # send a first keepalive packet before sending the initial UPDATE packet + keepalive_bgp(bgp_socket) + + timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print(timestamp + " - " + "BGP is up.") + + time.sleep(3) + for link_state in config["link_states"]: + update_bgp( + bgp_socket, + link_state, + config, + ) + + while True: + time.sleep(60) diff --git a/tests/topotests/bgp_linkstate_topo1/r1/staticd.conf b/tests/topotests/bgp_linkstate_topo1/r1/staticd.conf new file mode 100644 index 0000000000..7f2f057bfe --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r1/staticd.conf @@ -0,0 +1 @@ +ip route 192.0.2.2/32 192.168.1.2 diff --git a/tests/topotests/bgp_linkstate_topo1/r1/zebra.conf b/tests/topotests/bgp_linkstate_topo1/r1/zebra.conf new file mode 100644 index 0000000000..3307c123f8 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r1/zebra.conf @@ -0,0 +1,7 @@ +! +interface lo + ip address 192.0.2.1/32 +! +interface r1-eth0 + ip address 192.168.1.1/24 +! \ No newline at end of file diff --git a/tests/topotests/bgp_linkstate_topo1/r2/bgpd.conf b/tests/topotests/bgp_linkstate_topo1/r2/bgpd.conf new file mode 100644 index 0000000000..26ffee9c1b --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r2/bgpd.conf @@ -0,0 +1,20 @@ +router bgp 65002 + no bgp ebgp-requires-policy + neighbor 192.0.2.1 remote-as 65001 + 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 + neighbor 192.0.2.3 remote-as 65003 + 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 + address-family ipv4 unicast + no neighbor 192.0.2.1 activate + no neighbor 192.0.2.3 activate + exit-address-family + address-family link-state link-state + neighbor 192.0.2.1 activate + neighbor 192.0.2.3 activate + exit-address-family +! \ No newline at end of file diff --git a/tests/topotests/bgp_linkstate_topo1/r2/linkstate.json b/tests/topotests/bgp_linkstate_topo1/r2/linkstate.json new file mode 100644 index 0000000000..1e653c5531 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r2/linkstate.json @@ -0,0 +1,189 @@ +{ + "routes": { + "Link OSPFv3 ID:0xffffffffffffffff {Local {AS:4294967295 ID:4294967295 Area:4294967295 Rtr:10.10.10.11:2.2.2.2} Remote {AS:4294967295 ID:4294967295 Area:4294967295 Rtr:10.10.10.10:1.1.1.1} IPv4:10.1.0.1 Neigh-IPv4:10.1.0.2 IPv6:2001::1 Neigh-IPv6:2001::2 MT:0,2 424:0x200100000000000001}/XX": [ + { + "valid": true, + "bestpath": true, + "pathFrom": "external", + "linkStateNLRI": { + "nlriType": "Link", + "protocol": "OSPFv3", + "identifier": "0xffffffffffffffff", + "localNode": { + "as": 4294967295, + "identifier": 4294967295, + "area": 4294967295, + "routerID": "10.10.10.11:2.2.2.2" + }, + "remoteNode": { + "as": 4294967295, + "identifier": 4294967295, + "area": 4294967295, + "routerID": "10.10.10.10:1.1.1.1" + }, + "interfaceIPv4": "10.1.0.1", + "neighborIPv4": "10.1.0.2", + "interfaceIPv6": "2001::1", + "neighborIPv6": "2001::2", + "mtID": [0, 2], + "424": ["0x2001000000000000", "0x01"] + }, + "weight": 0, + "origin": "IGP" + } + ], + "IPv6-Prefix OSPFv3 ID:0x20 {Local {AS:65001 ID:0 Area:0 Rtr:10.10.10.10} MT:2 OSPF-Route-Type:1 IPv6:12:12::12:12/128}/XX": [ + { + "valid": true, + "bestpath": true, + "pathFrom": "external", + "linkStateNLRI": { + "nlriType": "IPv6-Prefix", + "protocol": "OSPFv3", + "identifier": "0x20", + "localNode": { + "as": 65001, + "identifier": 0, + "area": 0, + "routerID": "10.10.10.10" + }, + "ospfRouteType": 1, + "ipReachability": "12:12::12:12/128", + "mtID": [2] + }, + "weight": 0, + "origin": "IGP" + } + ], + "IPv6-Prefix ISIS-L2 ID:0x20 {Local {AS:65001 ID:0 Rtr:0000.0000.1003.00} MT:2 IPv6:12:12::12:12/128}/XX": [ + { + "valid": true, + "bestpath": true, + "pathFrom": "external", + "linkStateNLRI": { + "nlriType": "IPv6-Prefix", + "protocol": "ISIS-L2", + "identifier": "0x20", + "localNode": { + "as": 65001, + "identifier": 0, + "routerID": "0000.0000.1003.00" + }, + "ipReachability": "12:12::12:12/128", + "mtID": [2] + }, + "weight": 0, + "origin": "IGP" + } + ], + "IPv4-Prefix OSPFv2 ID:0x20 {Local {AS:65001 ID:0 Area:0 Rtr:10.10.10.10:1.1.1.1} IPv4:89.10.11.0/24}/XX": [ + { + "valid": true, + "bestpath": true, + "pathFrom": "external", + "linkStateNLRI": { + "nlriType": "IPv4-Prefix", + "protocol": "OSPFv2", + "identifier": "0x20", + "localNode": { + "as": 65001, + "identifier": 0, + "area": 0, + "routerID": "10.10.10.10:1.1.1.1" + }, + "ipReachability": "89.10.11.0/24" + }, + "weight": 0, + "origin": "IGP" + } + ], + "Node OSPFv2 ID:0x20 {Local {AS:65001 ID:0 Area:0 Rtr:10.10.10.10:1.1.1.1}}/XX": [ + { + "valid": true, + "bestpath": true, + "pathFrom": "external", + "linkStateNLRI": { + "nlriType": "Node", + "protocol": "OSPFv2", + "identifier": "0x20", + "localNode": { + "as": 65001, + "identifier": 0, + "area": 0, + "routerID": "10.10.10.10:1.1.1.1" + } + }, + "weight": 0, + "origin": "IGP" + } + ], + "Node OSPFv2 ID:0x20 {Local {AS:65001 ID:0 Area:0 Rtr:10.10.10.10}}/XX": [ + { + "valid": true, + "bestpath": true, + "pathFrom": "external", + "linkStateNLRI": { + "nlriType": "Node", + "protocol": "OSPFv2", + "identifier": "0x20", + "localNode": { + "as": 65001, + "identifier": 0, + "area": 0, + "routerID": "10.10.10.10" + } + }, + "weight": 0, + "origin": "IGP" + } + ], + "Node ISIS-L1 ID:0x20 {Local {AS:65001 ID:0 Rtr:0000.0000.1003.00}}/XX": [ + { + "valid": true, + "bestpath": true, + "pathFrom": "external", + "linkStateNLRI": { + "nlriType": "Node", + "protocol": "ISIS-L1", + "identifier": "0x20", + "localNode": { + "as": 65001, + "identifier": 0, + "routerID": "0000.0000.1003.00" + } + }, + "weight": 0, + "origin": "IGP" + } + ], + "Link ISIS-L1 ID:0x20 {Local {AS:65001 ID:0 Rtr:0000.0000.1001} Remote {AS:65001 ID:0 Rtr:0000.0000.1000} IPv4:10.1.0.1 Neigh-IPv4:10.1.0.2 IPv6:2001::1 Neigh-IPv6:2001::2 MT:0,2}/XX": [ + { + "valid": true, + "bestpath": true, + "pathFrom": "external", + "linkStateNLRI": { + "nlriType": "Link", + "protocol": "ISIS-L1", + "identifier": "0x20", + "localNode": { + "as": 65001, + "identifier": 0, + "routerID": "0000.0000.1001" + }, + "remoteNode": { + "as": 65001, + "identifier": 0, + "routerID": "0000.0000.1000" + }, + "interfaceIPv4": "10.1.0.1", + "neighborIPv4": "10.1.0.2", + "interfaceIPv6": "2001::1", + "neighborIPv6": "2001::2", + "mtID": [0, 2] + }, + "weight": 0, + "origin": "IGP" + } + ] + } +} diff --git a/tests/topotests/bgp_linkstate_topo1/r2/staticd.conf b/tests/topotests/bgp_linkstate_topo1/r2/staticd.conf new file mode 100644 index 0000000000..4ce3f7b9b1 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r2/staticd.conf @@ -0,0 +1,2 @@ +ip route 192.0.2.1/32 192.168.1.1 +ip route 192.0.2.3/32 192.168.2.3 \ No newline at end of file diff --git a/tests/topotests/bgp_linkstate_topo1/r2/zebra.conf b/tests/topotests/bgp_linkstate_topo1/r2/zebra.conf new file mode 100644 index 0000000000..586dc0acad --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r2/zebra.conf @@ -0,0 +1,11 @@ +! +int lo + ip address 192.0.2.2/32 +! +interface r2-eth0 + ip address 192.168.1.2/24 +! +interface r2-eth1 + ip address 192.168.2.2/24 +! + diff --git a/tests/topotests/bgp_linkstate_topo1/r3/bgpd.conf b/tests/topotests/bgp_linkstate_topo1/r3/bgpd.conf new file mode 100644 index 0000000000..88693a3750 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r3/bgpd.conf @@ -0,0 +1,14 @@ +router bgp 65003 + no bgp ebgp-requires-policy + 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.3 + address-family ipv4 unicast + no neighbor 192.0.2.2 activate + exit-address-family + address-family link-state link-state + neighbor 192.0.2.2 activate + exit-address-family +! \ No newline at end of file diff --git a/tests/topotests/bgp_linkstate_topo1/r3/linkstate.json b/tests/topotests/bgp_linkstate_topo1/r3/linkstate.json new file mode 120000 index 0000000000..588691dcf7 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r3/linkstate.json @@ -0,0 +1 @@ +../r2/linkstate.json \ No newline at end of file diff --git a/tests/topotests/bgp_linkstate_topo1/r3/staticd.conf b/tests/topotests/bgp_linkstate_topo1/r3/staticd.conf new file mode 100644 index 0000000000..e8a3198ba7 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r3/staticd.conf @@ -0,0 +1 @@ +ip route 192.0.2.2/32 192.168.2.2 \ No newline at end of file diff --git a/tests/topotests/bgp_linkstate_topo1/r3/zebra.conf b/tests/topotests/bgp_linkstate_topo1/r3/zebra.conf new file mode 100644 index 0000000000..532aede2e8 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/r3/zebra.conf @@ -0,0 +1,7 @@ +! +int lo + ip address 192.0.2.3/32 +! +interface r3-eth0 + ip address 192.168.2.3/24 +! diff --git a/tests/topotests/bgp_linkstate_topo1/test_bgp_linkstate_topo1.py b/tests/topotests/bgp_linkstate_topo1/test_bgp_linkstate_topo1.py new file mode 100644 index 0000000000..805159ca90 --- /dev/null +++ b/tests/topotests/bgp_linkstate_topo1/test_bgp_linkstate_topo1.py @@ -0,0 +1,113 @@ +#!/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, 4): + tgen.add_router("r{}".format(routern)) + + switch = tgen.add_switch("s1") + switch.add_link(tgen.gears["r1"]) + switch.add_link(tgen.gears["r2"]) + + switch = tgen.add_switch("s2") + switch.add_link(tgen.gears["r2"]) + switch.add_link(tgen.gears["r3"]) + + +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)) + ) + if rname == "r1": + # use bgp_injector.py to inject BGP prefixes + continue + router.load_config( + TopoRouter.RD_BGP, os.path.join(CWD, "{}/bgpd.conf".format(rname)) + ) + + tgen.start_router() + + r1_path = os.path.join(CWD, "r1") + log_dir = os.path.join(tgen.logdir, "r1") + tgen.gears["r1"].cmd("chmod u+x {}/bgp_injector.py".format(r1_path)) + tgen.gears["r1"].run("{}/bgp_injector.py {}".format(r1_path, log_dir)) + + +def teardown_module(mod): + tgen = get_topogen() + + log_dir = os.path.join(tgen.logdir, "r1") + pid_file = os.path.join(log_dir, "bgp_injector.pid") + + logger.info("r1: sending SIGTERM to bgp_injector") + tgen.gears["r1"].cmd("kill $(cat {})".format(pid_file)) + tgen.stop_topology() + + +def test_show_bgp_link_state(): + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + def _remove_prefixlen(tmp_json): + new_json = { + prefix.split("}/")[0] + "}/XX": data + for prefix, data in tmp_json["routes"].items() + } + + return new_json + + def _show_bgp_link_state_json(rname, tmp_expected): + tmp_output = json.loads( + tgen.gears[rname].vtysh_cmd("show bgp link-state link-state json") + ) + # prefix length is the size of prefix in memory + # which differs on 32 and 64 bits. + # do not compare the prefix length + output = _remove_prefixlen(tmp_output) + expected = _remove_prefixlen(tmp_expected) + + return topotest.json_cmp(output, expected) + + step("Check BGP Link-State tables") + for rname in ["r2", "r3"]: + expected = open(os.path.join(CWD, "{}/linkstate.json".format(rname))).read() + expected_json = json.loads(expected) + test_func = functools.partial(_show_bgp_link_state_json, rname, expected_json) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) + assert result is None, "Failed to see BGP prefixes on {}".format(rname) + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) -- 2.39.5