Skip to content

Commit

Permalink
tests: implement dscp -> vrf packet switching demo
Browse files Browse the repository at this point in the history
Signed-off-by: Volodymyr Huti <[email protected]>
  • Loading branch information
Volodymyr Huti committed Jan 8, 2024
1 parent c985170 commit b06c51a
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 37 deletions.
40 changes: 40 additions & 0 deletions tests/topotests/bgp_qppb_flow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

39 changes: 2 additions & 37 deletions tests/topotests/bgp_qppb_flow/test_bgp_qppb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
# --------------------------------------------------------------------------------
Expand Down
226 changes: 226 additions & 0 deletions tests/topotests/bgp_qppb_flow/test_vrf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#!/usr/bin/env python
#
# SPDX-License-Identifier: ISC
# Copyright (c) 2023 VyOS Inc.
# Volodymyr Huti <[email protected]>
#


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():
load_vrf_plugin(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")
# breakpoint()

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 load_vrf_plugin(tgen, rnode, debug_on=True):
"""
Initialize rnode XDP hooks and BPF mapping handlers
- compile xdp handlers from `xdp_qppb.c` in specified `mode`
- load `xdp_qppb` and `xdp_tc_mark` hooks
- restart router with QPPB plugin
Parameters
----------
* `tgen`: topogen object
* `rnode`: router object
* `mode`: xdp processing mode required
* `debug_on`: enable debug logs for bpf compilation / xdp handlers
Usage
---------
load_qppb_plugin(tgen, r1, mode=XdpMode.META)
Returns -> None (XXX)
"""
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:
breakpoint()
pytest.skip("Failed to configure XDP environment -- \n" + str(e))


if __name__ == "__main__":
args = ["-s"] + sys.argv[1:]
sys.exit(pytest.main(args))
65 changes: 65 additions & 0 deletions tests/topotests/bgp_qppb_flow/topo_vrf.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
Loading

0 comments on commit b06c51a

Please sign in to comment.