#!/bin/bash # 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= 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" OUTSIDE_MAC=$(iface_macaddr "$NS") INSIDE_MAC=$(iface_macaddr veth0 "$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 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 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'" } usage() { local FULL=${1:-} echo "Usage: $0 [options] [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 Exec inside test environment" echo "enter Execute shell inside test environment" echo "ping Run ping inside test environment" 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 "" 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 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 " --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 " .." 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,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 ;; -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" ;; "exec") CMD=ns_exec ;; ping|tcpdump) CMD="run_$1" ;; "alias") print_alias exit 0 ;; "help") usage full >&2 ;; *) usage >&2 ;; esac shift trap cleanup EXIT check_prereq $CMD "$@"