Skip to content

Instantly share code, notes, and snippets.

@Forsworns
Last active December 28, 2021 02:00
Show Gist options
  • Save Forsworns/33c48ddf1a6cc03e83d8de4b059fc13c to your computer and use it in GitHub Desktop.
Save Forsworns/33c48ddf1a6cc03e83d8de4b059fc13c to your computer and use it in GitHub Desktop.
test network environment script in xdp-turorial
#!/bin/bash
# These are the config options for the testlab
SETUP_SCRIPT="$(dirname "$0")/setup-env.sh"
STATEDIR="${TMPDIR:-/tmp}/xdp-tutorial-testlab"
IP6_SUBNET=fc00:dead:cafe # must have exactly three :-separated elements
IP6_PREFIX_SIZE=64 # Size of assigned prefixes
IP6_FULL_PREFIX_SIZE=48 # Size of IP6_SUBNET
IP4_SUBNET=10.11
IP4_PREFIX_SIZE=24 # Size of assigned prefixes
IP4_FULL_PREFIX_SIZE=16 # Size of IP4_SUBNET
VLAN_IDS=(1 2)
GENERATED_NAME_PREFIX="xdptut"
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Script to setup things inside a test environment, used by testenv.sh for
# executing commands.
#
# Author: Toke Høiland-Jørgensen (toke@redhat.com)
# Date: 7 March 2019
# Copyright (c) 2019 Red Hat
die()
{
echo "$1" >&2
exit 1
}
[ -n "$TESTENV_NAME" ] || die "TESTENV_NAME missing from environment"
[ -n "$1" ] || die "Usage: $0 <command to execute>"
set -o nounset
mount -t bpf bpf /sys/fs/bpf/ || die "Unable to mount /sys/fs/bpf inside test environment"
exec "$@"
#!/bin/bash
# This is copied from https://github.com/xdp-project/xdp-tutorial/blob/master/testenv/testenv.sh
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Script to setup and manage test environment for the XDP tutorial.
# See README.org for instructions on how to use.
#
# Author: Toke Høiland-Jørgensen (toke@redhat.com)
# Date: 6 March 2019
# Copyright (c) 2019 Red Hat
set -o errexit
set -o nounset
umask 077
source "$(dirname "$0")/config.sh"
NEEDED_TOOLS="ethtool ip tc ping"
MAX_NAMELEN=15
# Global state variables that will be set by options etc below
GENERATE_NEW=0
CLEANUP_FUNC=
STATEFILE=
CMD=
NS=
XDP_LOADER=./xdp_loader
XDP_STATS=./xdp_stats
LEGACY_IP=0
USE_VLAN=0
RUN_ON_INNER=0
# State variables that are written to and read from statefile
STATEVARS=(IP6_PREFIX IP4_PREFIX
INSIDE_IP6 INSIDE_IP4 INSIDE_MAC
OUTSIDE_IP6 OUTSIDE_IP4 OUTSIDE_MAC
ENABLE_IPV4 ENABLE_VLAN)
IP6_PREFIX=
IP4_PREFIX=
INSIDE_IP6=
INSIDE_IP4=
INSIDE_MAC=
OUTSIDE_IP6=
OUTSIDE_IP4=
OUTSIDE_MAC=
ENABLE_IPV4=0
ENABLE_VLAN=0
die()
{
echo "$1" >&2
exit 1
}
check_prereq()
{
local max_locked_mem=$(ulimit -l)
for t in $NEEDED_TOOLS; do
which "$t" > /dev/null || die "Missing required tools: $t"
done
if [ "$EUID" -ne "0" ]; then
die "This script needs root permissions to run."
fi
[ -d "$STATEDIR" ] || mkdir -p "$STATEDIR" || die "Unable to create state dir $STATEDIR"
if [ "$max_locked_mem" != "unlimited" ]; then
ulimit -l unlimited || die "Unable to set ulimit"
fi
}
get_nsname()
{
local GENERATE=${1:-0}
if [ -z "$NS" ]; then
[ -f "$STATEDIR/current" ] && NS=$(< "$STATEDIR/current")
if [ "$GENERATE" -eq "1" ] && [ -z "$NS" -o "$GENERATE_NEW" -eq "1" ]; then
NS=$(printf "%s-%04x" "$GENERATED_NAME_PREFIX" $RANDOM)
fi
fi
if [ "${#NS}" -gt "$MAX_NAMELEN" ]; then
die "Environment name '$NS' is too long (max $MAX_NAMELEN)"
fi
STATEFILE="$STATEDIR/${NS}.state"
}
ensure_nsname()
{
[ -z "$NS" ] && die "No environment selected; use --name to select one or 'setup' to create one"
[ -e "$STATEFILE" ] || die "Environment for $NS doesn't seem to exist"
echo "$NS" > "$STATEDIR/current"
read_statefile
}
get_num()
{
local num=1
if [ -f "$STATEDIR/highest_num" ]; then
num=$(( 1 + $(< "$STATEDIR/highest_num" )))
fi
echo $num > "$STATEDIR/highest_num"
printf "%x" $num
}
write_statefile()
{
[ -z "$STATEFILE" ] && return 1
echo > "$STATEFILE"
for var in "${STATEVARS[@]}"; do
echo "${var}='$(eval echo '$'$var)'" >> "$STATEFILE"
done
}
read_statefile()
{
local value
for var in "${STATEVARS[@]}"; do
value=$(source "$STATEFILE"; eval echo '$'$var)
eval "$var=\"$value\""
done
}
cleanup_setup()
{
echo "Error during setup, removing partially-configured environment '$NS'" >&2
set +o errexit
ip netns del "$NS" 2>/dev/null
ip link del dev "$NS" 2>/dev/null
rm -f "$STATEFILE"
}
cleanup_teardown()
{
echo "Warning: Errors during teardown, partial environment may be left" >&2
}
cleanup()
{
[ -n "$CLEANUP_FUNC" ] && $CLEANUP_FUNC
[ -d "$STATEDIR" ] || return 0
local statefiles=("$STATEDIR"/*.state)
if [ "${#statefiles[*]}" -eq 1 ] && [ ! -e "${statefiles[0]}" ]; then
rm -f "${STATEDIR}/highest_num" "${STATEDIR}/current"
rmdir "$STATEDIR"
fi
}
iface_macaddr()
{
local iface="$1"
local ns="${2:-}"
local output
if [ -n "$ns" ]; then
output=$(ip -br -n "$ns" link show dev "$iface")
else
output=$(ip -br link show dev "$iface")
fi
echo "$output" | awk '{print $3}'
}
set_sysctls()
{
local iface="$1"
local in_ns="${2:-}"
local nscmd=
[ -n "$in_ns" ] && nscmd="ip netns exec $in_ns"
local sysctls=(accept_dad
accept_ra
mldv1_unsolicited_report_interval
mldv2_unsolicited_report_interval)
for s in ${sysctls[*]}; do
$nscmd sysctl -w net.ipv6.conf.$iface.${s}=0 >/dev/null
done
}
wait_for_dev()
{
local iface="$1"
local in_ns="${2:-}"
local retries=5 # max retries
local nscmd=
[ -n "$in_ns" ] && nscmd="ip netns exec $in_ns"
while [ "$retries" -gt "0" ]; do
if ! $nscmd ip addr show dev $iface | grep -q tentative; then return 0; fi
sleep 0.5
retries=$((retries -1))
done
}
get_vlan_prefix()
{
# Split the IPv6 prefix, and add the VLAN ID to the upper byte of the fourth
# element in the prefix. This will break if the global prefix config doesn't
# have exactly three elements in it.
local prefix="$1"
local vid="$2"
(IFS=:; set -- $prefix; printf "%s:%s:%s:%x::" "$1" "$2" "$3" $(($4 + $vid * 4096)))
}
setup()
{
get_nsname 1
echo "Setting up new environment '$NS'"
[ -e "$STATEFILE" ] && die "Environment for '$NS' already exists"
local NUM=$(get_num "$NS")
local PEERNAME="testl-ve-$NUM"
[ -z "$IP6_PREFIX" ] && IP6_PREFIX="${IP6_SUBNET}:${NUM}::"
[ -z "$IP4_PREFIX" ] && IP4_PREFIX="${IP4_SUBNET}.$((0x$NUM))."
INSIDE_IP6="${IP6_PREFIX}2"
INSIDE_IP4="${IP4_PREFIX}2"
OUTSIDE_IP6="${IP6_PREFIX}1"
OUTSIDE_IP4="${IP4_PREFIX}1"
CLEANUP_FUNC=cleanup_setup
if ! mount | grep -q /sys/fs/bpf; then
mount -t bpf bpf /sys/fs/bpf/
fi
ip netns add "$NS"
ip link add dev "$NS" type veth peer name veth0 netns "$NS"
set_sysctls $NS
ip link set dev "$NS" up
ip addr add dev "$NS" "${OUTSIDE_IP6}/${IP6_PREFIX_SIZE}"
ethtool -K "$NS" rxvlan off txvlan off
# Prevent neighbour queries on the link
INSIDE_MAC=$(iface_macaddr veth0 "$NS")
ip neigh add "$INSIDE_IP6" lladdr "$INSIDE_MAC" dev "$NS" nud permanent
set_sysctls veth0 "$NS"
ip -n "$NS" link set dev lo up
ip -n "$NS" link set dev veth0 up
ip -n "$NS" addr add dev veth0 "${INSIDE_IP6}/${IP6_PREFIX_SIZE}"
ip netns exec "$NS" ethtool -K veth0 rxvlan off txvlan off
# Prevent neighbour queries on the link
OUTSIDE_MAC=$(iface_macaddr "$NS")
ip -n "$NS" neigh add "$OUTSIDE_IP6" lladdr "$OUTSIDE_MAC" dev veth0 nud permanent
# Add route for whole test subnet, to make it easier to communicate between
# namespaces
ip -n "$NS" route add "${IP6_SUBNET}::/$IP6_FULL_PREFIX_SIZE" via "$OUTSIDE_IP6" dev veth0
if [ "$LEGACY_IP" -eq "1" ]; then
ip addr add dev "$NS" "${OUTSIDE_IP4}/${IP4_PREFIX_SIZE}"
ip -n "$NS" addr add dev veth0 "${INSIDE_IP4}/${IP4_PREFIX_SIZE}"
ip neigh add "$INSIDE_IP4" lladdr "$INSIDE_MAC" dev "$NS" nud permanent
ip -n "$NS" neigh add "$OUTSIDE_IP4" lladdr "$OUTSIDE_MAC" dev veth0 nud permanent
ip -n "$NS" route add "${IP4_SUBNET}/${IP4_FULL_PREFIX_SIZE}" via "$OUTSIDE_IP4" dev veth0
ENABLE_IPV4=1
else
ENABLE_IPV4=0
fi
if [ "$USE_VLAN" -eq "1" ]; then
ENABLE_VLAN=1
for vid in "${VLAN_IDS[@]}"; do
local vlpx="$(get_vlan_prefix "$IP6_PREFIX" "$vid")"
local inside_ip="${vlpx}2"
local outside_ip="${vlpx}1"
ip link add dev "${NS}.$vid" link "$NS" type vlan id "$vid"
ip link set dev "${NS}.$vid" up
ip addr add dev "${NS}.$vid" "${outside_ip}/${IP6_PREFIX_SIZE}"
ip neigh add "$inside_ip" lladdr "$INSIDE_MAC" dev "${NS}.$vid" nud permanent
set_sysctls "${NS}/$vid"
ip -n "$NS" link add dev "veth0.$vid" link "veth0" type vlan id "$vid"
ip -n "$NS" link set dev "veth0.$vid" up
ip -n "$NS" addr add dev "veth0.$vid" "${inside_ip}/${IP6_PREFIX_SIZE}"
ip -n "$NS" neigh add "$outside_ip" lladdr "$OUTSIDE_MAC" dev "veth0.$vid" nud permanent
set_sysctls "veth0/$vid" "$NS"
done
else
ENABLE_VLAN=0
fi
write_statefile
CLEANUP_FUNC=
echo -n "Setup environment '$NS' with peer ip ${INSIDE_IP6}"
[ "$ENABLE_IPV4" -eq "1" ] && echo " and ${INSIDE_IP4}." || echo "."
echo "Waiting for interface configuration to settle..."
echo ""
wait_for_dev "$NS" && wait_for_dev veth0 "$NS"
LEGACY_IP=0 USE_VLAN=0 run_ping -c 1
echo "$NS" > "$STATEDIR/current"
}
teardown()
{
get_nsname && ensure_nsname "$NS"
echo "Tearing down environment '$NS'"
CLEANUP_FUNC=cleanup_teardown
ip link del dev "$NS"
ip netns del "$NS"
rm -f "$STATEFILE"
[ -d "/sys/fs/bpf/$NS" ] && rmdir "/sys/fs/bpf/$NS" || true
if [ -f "$STATEDIR/current" ]; then
local CUR=$(< "$STATEDIR/current" )
[[ "$CUR" == "$NS" ]] && rm -f "$STATEDIR/current"
fi
CLEANUP_FUNC=
}
reset()
{
teardown && setup
}
ns_exec()
{
get_nsname && ensure_nsname "$NS"
ip netns exec "$NS" env TESTENV_NAME="$NS" "$SETUP_SCRIPT" "$@"
}
enter()
{
ns_exec "${SHELL:-bash}"
}
run_ping()
{
local PING
local IP
get_nsname && ensure_nsname "$NS"
echo "Running ping from inside test environment:"
echo ""
if [ "$LEGACY_IP" -eq "1" ]; then
PING=$(which ping)
IP="${OUTSIDE_IP4}"
[ "$USE_VLAN" -eq "0" ] || die "Can't use --legacy-ip and --vlan at the same time."
[ "$ENABLE_IPV4" -eq "1" ] || die "No legacy IP addresses configured in environment."
else
PING=$(which ping6 2>/dev/null || which ping)
if [ "$USE_VLAN" -eq "0" ]; then
IP="${OUTSIDE_IP6}"
else
[ "$ENABLE_VLAN" -eq "1" ] || die "No VLANs configured in environment."
IP="$(get_vlan_prefix "$IP6_PREFIX" "${VLAN_IDS[0]}")1"
fi
fi
ns_exec "$PING" "$IP" "$@"
}
run_tcpdump()
{
get_nsname && ensure_nsname "$NS"
if [ "$RUN_ON_INNER" -eq "1" ]; then
ns_exec tcpdump -nei veth0 "$@"
else
tcpdump -nei "$NS" "$@"
fi
}
status()
{
get_nsname
echo "Currently selected environment: ${NS:-None}"
if [ -n "$NS" ] && [ -e "$STATEFILE" ]; then
read_statefile
echo -n " Namespace: "; ip netns | grep "^$NS"
echo " Prefix: ${IP6_PREFIX}/${IP6_PREFIX_SIZE}"
[ "$ENABLE_IPV4" -eq "1" ] && echo " Legacy prefix: ${IP4_PREFIX}0/${IP4_PREFIX_SIZE}"
echo -n " Iface: "; ip -br a show dev "$NS" | sed 's/\s\+/ /g'
fi
echo ""
echo "All existing environments:"
for f in "$STATEDIR"/*.state; do
if [ ! -e "$f" ]; then
echo " No environments exist"
break
fi
NAME=$(basename "$f" .state)
echo " $NAME"
done
}
print_alias()
{
local scriptname="$(readlink -e "$0")"
local sudo=
[ -t 1 ] && echo "Eval this with \`eval \$($0 alias)\` to create shell alias" >&2
if [ "$EUID" -ne "0" ]; then
sudo="sudo "
echo "WARNING: Creating sudo alias; be careful, this script WILL execute arbitrary programs" >&2
fi
echo "" >&2
echo "alias t='$sudo$scriptname'"
}
#
# This command can be used to populate maps for the assignment 3 of the
# packet03-redirecting lesson. It takes two arguments: the source and the
# destination environment names.
#
populate_redirect_map()
{
local src="$1"
local dest="$2"
local src_mac=$(ip netns exec $src cat /sys/class/net/veth0/address)
local dest_mac=$(ip netns exec $dest cat /sys/class/net/veth0/address)
# set bidirectional forwarding
./xdp_prog_user -d $src -r $dest --src-mac $src_mac --dest-mac $dest_mac
./xdp_prog_user -d $dest -r $src --src-mac $dest_mac --dest-mac $src_mac
}
xdp_load()
{
get_nsname && ensure_nsname
[ -x "$XDP_LOADER" ] || die "Loader '$XDP_LOADER' is not executable"
$XDP_LOADER --dev "$NS" "$@"
}
xdp_unload()
{
get_nsname && ensure_nsname
[ -x "$XDP_LOADER" ] || die "Loader '$XDP_LOADER' is not executable"
$XDP_LOADER --dev "$NS" --unload "$@"
}
xdp_stats()
{
get_nsname && ensure_nsname
[ -x "$XDP_STATS" ] || die "Stats tool '$XDP_STATS' is not executable"
$XDP_STATS --dev "$NS" "$@"
}
usage()
{
local FULL=${1:-}
echo "Usage: $0 [options] <command> [param]"
echo ""
echo "Commands:"
echo "setup Setup and initialise new environment"
echo "teardown Tear down existing environment"
echo "reset Reset environment to original state"
echo "exec <command> Exec <command> inside test environment"
echo "enter Execute shell inside test environment"
echo "ping Run ping inside test environment"
echo "alias Print shell alias for easy access to this script"
echo "status (or st) Show status of test environment"
echo "load Load XDP program on outer interface"
echo "unload Unload XDP program on outer interface"
echo "tcpdump Run on outer interface (or inner with --inner)"
echo "stats Run the XDP statistics program"
echo "redirect <env1> <env2> Setup redirects for packet03 lessons"
echo ""
if [ -z "$FULL" ] ; then
echo "Use --help to see the list of options."
exit 1
fi
echo "Options:"
echo "-h, --help Show this usage text"
echo ""
echo "-n, --name <name> Set name of test environment. If not set, the last used"
echo " name will be used, or a new one generated."
echo ""
echo "-g, --gen-new Generate a new test environment name even though an existing"
echo " environment is selected as the current one."
echo ""
echo "-l, --loader <prog> Specify program to use for loading XDP programs."
echo " Device name will be passed to it, along with any additional"
echo " command line options passed after --."
echo " Default: '$XDP_LOADER'"
echo ""
echo "-s, --stats <prog> Specify program to use for getting statistics ('stats' command)."
echo " Device name will be passed to it, along with any additional"
echo " command line options passed after --."
echo " Default: '$XDP_STATS'"
echo ""
echo " --legacy-ip Enable legacy IP (IPv4) support."
echo " For setup and reset commands this enables configuration of legacy"
echo " IP addresses on the interface, for the ping command it switches to"
echo " legacy ping."
echo ""
echo " --vlan Enable VLAN support."
echo " When used with the setup and reset commands, these VLAN IDs will"
echo " be configured: ${VLAN_IDS[*]}. The VLAN interfaces are named as"
echo " <ifname>.<vlid>."
echo " When used with the ping command, the pings will be sent on the"
echo " first VLAN ID (${VLAN_IDS[0]})."
echo ""
echo " --inner Use with tcpdump command to run on inner interface."
echo ""
exit 1
}
OPTS="hn:gl:s:"
LONGOPTS="help,name:,gen-new,loader:,stats:,legacy-ip,vlan,inner"
OPTIONS=$(getopt -o "$OPTS" --long "$LONGOPTS" -- "$@")
[ "$?" -ne "0" ] && usage >&2 || true
eval set -- "$OPTIONS"
while true; do
arg="$1"
shift
case "$arg" in
-h | --help)
usage full >&2
;;
-n | --name)
NS="$1"
shift
;;
-l | --loader)
XDP_LOADER="$1"
shift
;;
-s | --stats)
XDP_STATS="$1"
shift
;;
-g | --gen-new)
GENERATE_NEW=1
;;
--legacy-ip)
LEGACY_IP=1
;;
--vlan)
USE_VLAN=1
;;
--inner)
RUN_ON_INNER=1
;;
-- )
break
;;
esac
done
[ "$#" -eq 0 ] && usage >&2
case "$1" in
st|sta|status)
CMD=status
;;
setup|teardown|reset|enter)
CMD="$1"
;;
load|unload|stats)
CMD="xdp_$1"
;;
"exec")
CMD=ns_exec
;;
ping|tcpdump)
CMD="run_$1"
;;
redirect)
CMD=populate_redirect_map
;;
"alias")
print_alias
exit 0
;;
"help")
usage full >&2
;;
*)
usage >&2
;;
esac
shift
trap cleanup EXIT
check_prereq
$CMD "$@"

Test environment script

This directory contains a setup script that you can use to create test environments for testing your XDP programs. It works by creating virtual ethernet (veth) interface pairs and moving one end of each pair to another network namespace. You can load the XDP program in the other namespace and send traffic to it through the interface that is visible in the root namespace.

Run ./testenv.sh with no parameter to get a list of available commands, or run ./testenv.sh --help to get the full help listing with all options. The script can maintain several environments active at the same time, and you can switch between them using the --name option.

If you don’t specify a name, the most recently used environment will be used. If you don’t specify a name when setting up a new environment, a random name will be generated for you.

Examples:

Setup new environment named “test”: ./testenv.sh setup --name=test

Create a shell alias for easy use of script from anywhere: eval $(./testenv.sh alias)

See the currently active environment, and a list of all active environment names (with alias defined as above): t status

Enter the currently active environment: t enter

Execute a command inside the environment: t exec -- ip a

Teardown the environment: t teardown

Understanding the network topology

When setting up a test environment, there will be a virtual link between the environment inside the new namespace, and the interface visible from the host system root namespace. The new namespace will be named after the environment name passed to the script, as will the interface visible in the outer namespace. The interface inside the namespace will always be named ‘veth0’.

To illustrate this, creating a test environment with the name ‘test01’ (with t setup --name test01 will result in the following environment being set up:

+-----------------------------+                          +-----------------------------+
| Root namespace              |                          | Testenv namespace 'test01'  |
|                             |      From 'test01'       |                             |
|                    +--------+ TX->                RX-> +--------+                    |
|                    | test01 +--------------------------+  veth0 |                    |
|                    +--------+ <-RX                <-TX +--------+                    |
|                             |       From 'veth0'       |                             |
+-----------------------------+                          +-----------------------------+

The ‘test01’ interface visible in the root namespace is the one we will be installing XDP programs on in the tutorial lessons. The XDP program will see packets being received on this interface; as you can see from the diagram, this means all packets being transmitted from inside the new namespace.

The setup is created this way to simulate the case where the host machine have physical interfaces; but instead of the traffic arriving from outside hosts on physical interfaces, they will arrive from inside the namespace on the virtual interface. This also means that when you generate traffic to test your XDP programs, you need to generate it from inside the test environment. The t ping command will start the ping inside the test environment by default, and you can run arbitrary programs inside the environment by using t exec -- <command>, or simply spawning a shell with t enter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment