From c205b25c0e390703ac2da83bb83b2d6765474aaf Mon Sep 17 00:00:00 2001 From: Volodymyr Huti Date: Thu, 4 Jan 2024 01:09:28 +0200 Subject: [PATCH] tests: implement dscp -> vrf packet switching demo Signed-off-by: Volodymyr Huti --- tests/topotests/bgp_qppb_flow/__init__.py | 40 ++++ .../topotests/bgp_qppb_flow/test_bgp_qppb.py | 39 +--- tests/topotests/bgp_qppb_flow/test_vrf.py | 211 ++++++++++++++++++ tests/topotests/bgp_qppb_flow/topo_vrf.json | 65 ++++++ tests/topotests/bgp_qppb_flow/xdp_vrf.c | 42 ++++ 5 files changed, 360 insertions(+), 37 deletions(-) create mode 100644 tests/topotests/bgp_qppb_flow/test_vrf.py create mode 100644 tests/topotests/bgp_qppb_flow/topo_vrf.json create mode 100644 tests/topotests/bgp_qppb_flow/xdp_vrf.c diff --git a/tests/topotests/bgp_qppb_flow/__init__.py b/tests/topotests/bgp_qppb_flow/__init__.py index c11320e9d5de..177dfe83b79a 100755 --- a/tests/topotests/bgp_qppb_flow/__init__.py +++ b/tests/topotests/bgp_qppb_flow/__init__.py @@ -8,8 +8,11 @@ import os import sys import json +import time import pytest +import functools +from lib import topotest from lib.topolog import logger from lib.common_config import ( start_router_daemons, @@ -192,3 +195,40 @@ def tc_log_stats(host, iface): tc_flags + " qdisc ls ", ], ) + +def check_ping4(rnode, dst, connected=True, src=None, tos=None, count=10, timeout=0): + ping = "" + if timeout: + ping = "timeout {} ".format(timeout) + ping += "ping {} -c{}".format(dst, count) + if src: + ping = "{} -I{}".format(ping, src) + if tos: + ping = "{} -Q{}".format(ping, tos) + + match = ", {} packet loss".format("100%" if connected else "0%") + logger.info( + "[+] {} ping -> {}, connection expected -> {}".format( + rnode, dst, "up" if connected else "down" + ) + ) + logger.debug("Executing the ping -> {}".format(ping)) + + def _match_missing(rnode, dst, match): + output = rnode.run(ping) + logger.info(output) + return match not in output + + func = functools.partial(_match_missing, rnode, dst, match) + success, result = topotest.run_and_expect(func, True, count, wait=1) + assert result is True + +def _check(tcpdump, sender, receiver, dst, cap_iface, tos, src=None, ping_tos=None): + PINGS = 10 + p1 = tcpdump.capture_start(receiver, cap_iface, background=True, timeout=PINGS) + assert p1, "Failed to run tcpdump on {}:\n{}".format(sender.name, p1) + + check_ping4(sender, dst, src=src, count=PINGS, timeout=PINGS, tos=ping_tos) + time.sleep(1.5) + return tcpdump.find_msg(receiver, "tos 0x%x" % tos.value) + diff --git a/tests/topotests/bgp_qppb_flow/test_bgp_qppb.py b/tests/topotests/bgp_qppb_flow/test_bgp_qppb.py index a72fc061b711..0295cae76384 100644 --- a/tests/topotests/bgp_qppb_flow/test_bgp_qppb.py +++ b/tests/topotests/bgp_qppb_flow/test_bgp_qppb.py @@ -101,34 +101,6 @@ def setup_test_hosts(tgen, router): router.cmd_raises("sysctl -w net.ipv4.conf.all.proxy_arp=1") -def check_ping4(rnode, dst, connected=True, src=None, tos=None, count=10, timeout=0): - ping = "" - if timeout: - ping = "timeout {} ".format(timeout) - ping += "ping {} -c{}".format(dst, count) - if src: - ping = "{} -I{}".format(ping, src) - if tos: - ping = "{} -Q{}".format(ping, src) - - match = ", {} packet loss".format("100%" if connected else "0%") - logger.info( - "[+] {} ping -> {}, connection expected -> {}".format( - rnode, dst, "up" if connected else "down" - ) - ) - logger.debug("Executing the ping -> {}".format(ping)) - - def _match_missing(rnode, dst, match): - output = rnode.run(ping) - logger.info(output) - return match not in output - - func = functools.partial(_match_missing, rnode, dst, match) - success, result = topotest.run_and_expect(func, True, count, wait=1) - assert result is True - - # Module # ------------------------------------------------------- def teardown_module(_mod): @@ -258,15 +230,8 @@ def test_xdp_lpm(tgen): r4_lo_ip = "1.0.4.17" PINGS = 10 - def _check(sender, receiver, dst, cap_iface, tos, src=None, ping_tos=None): - p1 = tcpdump.capture_start(receiver, cap_iface, background=True, timeout=PINGS) - assert p1, "Failed to run tcpdump on {}:\n{}".format(sender.name, p1) - - check_ping4(sender, dst, src=src, count=PINGS, timeout=PINGS, tos=ping_tos) - time.sleep(1.5) - return tcpdump.find_msg(receiver, "tos 0x%x" % tos.value) - - check_connection = functools.partial(_check, h1, r4, r4_lo_ip, "r4-r3-eth0") + from . import _check + check_connection = functools.partial(_check, tcpdump, h1, r4, r4_lo_ip, "r4-r3-eth0") h1.run("ping -c 3 -w 3 " + r4_lo_ip) # refresh arp cache, etc ... time.sleep(2) # -------------------------------------------------------------------------------- diff --git a/tests/topotests/bgp_qppb_flow/test_vrf.py b/tests/topotests/bgp_qppb_flow/test_vrf.py new file mode 100644 index 000000000000..d372061693ed --- /dev/null +++ b/tests/topotests/bgp_qppb_flow/test_vrf.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# +# SPDX-License-Identifier: ISC +# Copyright (c) 2023 VyOS Inc. +# Volodymyr Huti +# + + +import os +import re +import sys +import time +import functools +import subprocess + +from lib import topotest +from lib.topolog import logger +from lib.topojson import build_config_from_json +from lib.topogen import Topogen, TopoRouter, get_topogen +from lib.bgp import verify_bgp_convergence +from lib.common_config import ( + create_debug_log_config, + apply_raw_config, + start_topology, + TcpDumpHelper, + IPerfHelper, + step, +) + +from lib.topotest import version_cmp, interface_to_ifindex +from bcc import BPF, DEBUG_PREPROCESSOR, DEBUG_SOURCE, DEBUG_BPF, DEBUG_BTF +from ctypes import Structure, c_int, c_uint, c_ubyte, c_uint32 + +import pyroute2 +from pyroute2.netns import pushns, popns + +# Module +# ------------------------------------------------------- +def teardown_module(_mod): + "Teardown the pytest environment" + tgen = get_topogen() + tgen.stop_topology() + +DEV_DEBUG = True +CWD = os.path.dirname(os.path.realpath(__file__)) + +def setup_module(mod): + testsuite_run_time = time.asctime(time.localtime(time.time())) + logger.info("Testsuite start time: {}".format(testsuite_run_time)) + logger.info("=" * 40) + + logger.info("Running setup_module to create topology") + + json_file = f"{CWD}/topo_vrf.json" + tgen = Topogen(json_file, mod.__name__) + global topo + topo = tgen.json_topo + + start_topology(tgen) + build_config_from_json(tgen, topo) + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + # ----------------------------------------------------------------------- + r1 = tgen.gears["r1"] + debug_rmap_dict = {"r1": {"raw_config": ["end", "debug route-map"]}} + debug_config_dict = { + "r1": {"debug": {"log_file": "debug.log", "enable": ["bgpd", "zebra"]}} + } + if DEV_DEBUG: + create_debug_log_config(tgen, debug_config_dict) + apply_raw_config(tgen, debug_rmap_dict) + + +# Test Cases +# ------------------------------------------------------- +xdp_ifindex = lambda host, iface: c_uint(interface_to_ifindex(host, iface)) +xdp_dscp = lambda x: c_ubyte(dscp_tos(x)) +dscp_tos = lambda x: x << 2 +tag10 = c_uint32(10) +tag20 = c_uint32(20) + +def tc_bpf_filter(rnode, ifid): + "Attach tc bpf filter, depends on pyroute2 package" + + tc_fn = rnode.bpf.funcs[b"xdp_vrf"] + rnode_ns = "/proc/{}/ns/net".format(rnode.net.pid) + + logger.debug("Attach TC-BPF handler '{}'\nNetNS --> {})".format(ifid, rnode_ns)) + # ip.tc("add", "clsact", ifid, "1:") + pushns(rnode_ns) + ip = pyroute2.IPRoute() + ip.tc("add", "ingress", ifid, "ffff:") + ip.tc("add-filter", "bpf", ifid, 20, fd=tc_fn.fd, name=tc_fn.name, + parent="ffff:", classid=1, action="drop", direct_action=True) + popns() + + +def test_dscp_vrf(tgen): + r1 = tgen.gears['r1'] + r2 = tgen.gears['r2'] + r3 = tgen.gears['r3'] + r4 = tgen.gears['r4'] + + red = xdp_ifindex(r1, "RED") + blue = xdp_ifindex(r1, "BLUE") + # red = xdp_ifindex(r1, "r1-r2-eth0") + # blue = xdp_ifindex(r1, "r1-r3-eth1") + + r1.cmd_raises("sysctl -w net.ipv4.conf.all.proxy_arp=1") + r1.cmd_raises(""" + ip rule add table 1 + ip rule add pref 32765 table local + ip rule del pref 0 + ip rule show + """) + + for r in tgen.gears.values(): + setup_bpf(tgen, r) + + for r in [r2, r3, r4]: + router_attach_xdp(r, "%s-r1-eth0" % r.name, b"xdp_dummy") + r.cmd_raises("ip route add default dev %s-r1-eth0" % r.name) + + for iface in ["r1-r2-eth0", "r1-r3-eth1"]: + router_attach_xdp(r1, iface, b"xdp_dummy"); + + ingress_if = interface_to_ifindex(r1, "r1-r4-eth2") + tc_bpf_filter(r1, ingress_if) + # router_attach_xdp(r1, "r1-r4-eth2") + # r1.cmd_raises("ip l set r1-r4-eth2 master RED") + + dscp_iface_map = r1.bpf['dscp_iface_map'] + dscp_iface_map[tag10] = red + dscp_iface_map[tag20] = blue + + # let kernel initialize caches & remove rule + r4.cmd("ping 192.168.1.1 -c 5") + r1.cmd("ip rule del lookup 1") + + from . import _check + lo_ip = "192.168.1.1" + tcpdump = TcpDumpHelper(tgen, "icmp[0] == 8") # ICMP Echo requst + r2_check_conn = functools.partial(_check, tcpdump, r4, r2, lo_ip, "r2-r1-eth0") + r3_check_conn = functools.partial(_check, tcpdump, r4, r3, lo_ip, "r3-r1-eth0") + + found, matches = r2_check_conn(tag10, ping_tos=10) + logger.info("{} {}".format(found, matches)) + + found, matches = r3_check_conn(tag20, ping_tos=20) + logger.info("{} {}".format(found, matches)) + + breakpoint() + + +def router_attach_xdp(rnode, iface, fn=b"xdp_vrf"): + """ + - swap netns to rnode, + - attach `xdp_qppb` to `iface` + - switch back to root ns + """ + ns = "/proc/%d/ns/net" % rnode.net.pid + vrf_fn = rnode.bpf.funcs[fn] + + pushns(ns) + logger.debug("Attach XDP handler '{}|{}'\nNetNS --> {})".format(iface, fn, ns)) + rnode.bpf.attach_xdp(iface, vrf_fn, 0) + popns() + + +def setup_bpf(tgen, rnode, debug_on=True): + """ + - mount bpf fs + - compile classifier hook + """ + debug_flags = DEBUG_BPF | DEBUG_PREPROCESSOR | DEBUG_SOURCE | DEBUG_BTF + debug = debug_flags if debug_on else 0 + src_file = CWD + "/xdp_vrf.c" + bpf_flags = [ "-w" ] + + ns = "/proc/%d/ns/mnt" % rnode.net.pid + nsfd = os.open(ns, os.O_RDONLY) + + import ctypes + libc = ctypes.CDLL("libc.so.6", use_errno=True) + libc.setns(nsfd, 0) + + tgen.qppb_nodes.append(rnode) + rnode.cmd_raises( + """ + mkdir -p /sys/fs/bpf + mount -t bpf bpf /sys/fs/bpf + """ + ) + + try: + logger.info("Preparing the XDP src: " + src_file) + b = BPF(src_file=src_file.encode(), cflags=bpf_flags, debug=debug) + + logger.info("Loading XDP hooks -- xdp_vrf") + b.load_func(b"xdp_vrf", BPF.SCHED_CLS) + b.load_func(b"xdp_dummy", BPF.XDP) + rnode.bpf = b + except Exception as e: + pytest.skip("Failed to configure XDP environment -- \n" + str(e)) + # breakpoint() + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) diff --git a/tests/topotests/bgp_qppb_flow/topo_vrf.json b/tests/topotests/bgp_qppb_flow/topo_vrf.json new file mode 100644 index 000000000000..cfa3b069c794 --- /dev/null +++ b/tests/topotests/bgp_qppb_flow/topo_vrf.json @@ -0,0 +1,65 @@ +{ + "address_types": ["ipv4"], + "ipv4base": "10.0.0.0", + "ipv4mask": 30, + "ipv6base": "fd00::", + "ipv6mask": 64, + "link_ip_start": { + "ipv4": "10.0.0.0", + "v4mask": 30, + "ipv6": "fd00::", + "v6mask": 64 + }, + "lo_prefix": { + "ipv4": "1.0.", + "v4mask": 32, + "ipv6": "2001:DB8:F::", + "v6mask": 128 + }, + "routers": { + "r1": { + "links": { + "r2": { + "ipv4": "192.168.1.253/24", "vrf": "RED", + "ipv6": "auto" + }, + "r3": { + "ipv4": "192.168.1.253/24", "vrf": "BLUE", + "ipv6": "auto" + }, + "r4": { + "ipv4": "20.0.0.2/24", + "ipv6": "auto" + } + }, + "vrfs":[ + {"name": "RED", "id": "1"}, + {"name": "BLUE", "id": "2"} + ] + }, + "r2": { + "links": { + "r1": { + "ipv4": "192.168.1.1/24", + "ipv6": "auto" + } + } + }, + "r3": { + "links": { + "r1": { + "ipv4": "192.168.1.1/24", + "ipv6": "auto" + } + } + }, + "r4": { + "links": { + "r1": { + "ipv4": "20.0.0.1/24", + "ipv6": "auto" + } + } + } + } +} diff --git a/tests/topotests/bgp_qppb_flow/xdp_vrf.c b/tests/topotests/bgp_qppb_flow/xdp_vrf.c new file mode 100644 index 000000000000..5f824a6a032e --- /dev/null +++ b/tests/topotests/bgp_qppb_flow/xdp_vrf.c @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * XDP handler from mark/classifing traffic + * Copyright (C) 2023 VyOS Inc. + * Volodymyr Huti + */ + +#include + +int xdp_dummy(struct xdp_md *ctx) { + return XDP_PASS; +} + +#if !defined(BPF_PIN_DIR) +#define BPF_PIN_DIR "/sys/fs/bpf" +#endif + +#define VRF_PIN BPF_PIN_DIR "/vrf_map" +BPF_TABLE_PINNED("array", u32 /*dscp*/, u32 /*vrf*/, dscp_iface_map, 100, VRF_PIN); +int xdp_vrf(struct __sk_buff *skb) +{ + u8 *cursor = 0; + struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet)); + struct ip_t *ip = cursor_advance(cursor, sizeof(*ip)); + u32 tos, eif, *eifp; + int rc = 0; + + tos = ip->tos; + eifp = dscp_iface_map.lookup(&tos); + if (!eifp) + goto out; + + eif = *eifp; + if (eif <= 0 || eif >= 100) + // XXX: validate that ifid exists + goto out; + + rc = bpf_redirect(*eifp, 0); + // bpf_trace_printk("[eif=%d|act=%d] Redir", eif, rc); +out: return rc; +} +