|
#!/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 "$@" |