#!/bin/bash -e # SPDX-License-Identifier: GPL-2.0 # # xfrm/IPsec tests. # Currently implemented: # - ICMP error source address verification (IETF RFC 4301 section 6) # - ICMP MTU exceeded handling over IPsec tunnels. # # Addresses and topology: # IPv4 prefix 10.1.c.d IPv6 prefix fc00:c::d/64 where c is the segment number # and d is the interface identifier. # IPv6 uses the same c:d as IPv4, and start with IPv6 prefix instead ipv4 prefix # # Network topology default: ns_set_v4 or ns_set_v6 # 1.1 1.2 2.1 2.2 3.1 3.2 4.1 4.2 5.1 5.2 6.1 6.2 # eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 # a -------- r1 -------- s1 -------- r2 -------- s2 -------- r3 -------- b # a, b = Alice and Bob hosts without IPsec. # r1, r2, r3 routers, without IPsec # s1, s2, IPsec gateways/routers that setup tunnel(s). # Network topology x: IPsec gateway that generates ICMP response - ns_set_v4x or ns_set_v6x # 1.1 1.2 2.1 2.2 3.1 3.2 4.1 4.2 5.1 5.2 # eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 # a -------- r1 -------- s1 -------- r2 -------- s2 -------- b . lib.sh EXIT_ON_TEST_FAIL=no PAUSE=no VERBOSE=${VERBOSE:-0} DEBUG=0 # Name Description tests=" unreachable_ipv4 IPv4 unreachable from router r3 unreachable_ipv6 IPv6 unreachable from router r3 unreachable_gw_ipv4 IPv4 unreachable from IPsec gateway s2 unreachable_gw_ipv6 IPv6 unreachable from IPsec gateway s2 mtu_ipv4_s2 IPv4 MTU exceeded from IPsec gateway s2 mtu_ipv6_s2 IPv6 MTU exceeded from IPsec gateway s2 mtu_ipv4_r2 IPv4 MTU exceeded from ESP router r2 mtu_ipv6_r2 IPv6 MTU exceeded from ESP router r2 mtu_ipv4_r3 IPv4 MTU exceeded from router r3 mtu_ipv6_r3 IPv6 MTU exceeded from router r3" prefix4="10.1" prefix6="fc00" run_cmd_err() { cmd="$*" if [ "$VERBOSE" -gt 0 ]; then printf " COMMAND: %s\n" "$cmd" fi out="$($cmd 2>&1)" && rc=0 || rc=$? if [ "$VERBOSE" -gt 1 ] && [ -n "$out" ]; then echo " $out" echo fi return 0 } run_cmd() { run_cmd_err "$@" || exit 1 } run_test() { # If errexit is set, unset it for sub-shell and restore after test errexit=0 if [[ $- =~ "e" ]]; then errexit=1 set +e fi ( unset IFS # shellcheck disable=SC2030 # fail is read by trap/cleanup within this subshell fail="yes" # Since cleanup() relies on variables modified by this sub shell, # it has to run in this context. trap 'log_test_error $?; cleanup' EXIT INT TERM if [ "$VERBOSE" -gt 0 ]; then printf "\n#############################################################\n\n" fi ret=0 case "${name}" in # can't use eval and test names shell check will complain about unused code unreachable_ipv4) test_unreachable_ipv4 ;; unreachable_ipv6) test_unreachable_ipv6 ;; unreachable_gw_ipv4) test_unreachable_gw_ipv4 ;; unreachable_gw_ipv6) test_unreachable_gw_ipv6 ;; mtu_ipv4_s2) test_mtu_ipv4_s2 ;; mtu_ipv6_s2) test_mtu_ipv6_s2 ;; mtu_ipv4_r2) test_mtu_ipv4_r2 ;; mtu_ipv6_r2) test_mtu_ipv6_r2 ;; mtu_ipv4_r3) test_mtu_ipv4_r3 ;; mtu_ipv6_r3) test_mtu_ipv6_r3 ;; esac ret=$? if [ $ret -eq 0 ]; then fail="no" if [ "$VERBOSE" -gt 1 ]; then show_icmp_filter fi printf "TEST: %-60s [ PASS ]\n" "${desc}" elif [ $ret -eq "$ksft_skip" ]; then fail="no" printf "TEST: %-60s [SKIP]\n" "${desc}" fi return $ret ) ret=$? [ $errexit -eq 1 ] && set -e case $ret in 0) all_skipped=false [ "$exitcode" -eq "$ksft_skip" ] && exitcode=0 ;; "$ksft_skip") [ $all_skipped = true ] && exitcode=$ksft_skip ;; *) all_skipped=false exitcode=1 ;; esac return 0 # don't trigger errexit (-e); actual status in exitcode } setup_namespaces() { local namespaces="" NS_A="" NS_B="" NS_R1="" NS_R2="" NS_R3="" NS_S1="" NS_S2="" for ns in ${ns_set}; do namespaces="$namespaces NS_${ns^^}" done # shellcheck disable=SC2086 # setup_ns expects unquoted list setup_ns $namespaces ns_active= #ordered list of namespaces for this test. [ -n "${NS_A}" ] && ns_a=(ip netns exec "${NS_A}") && ns_active="${ns_active} $NS_A" [ -n "${NS_R1}" ] && ns_active="${ns_active} $NS_R1" [ -n "${NS_S1}" ] && ns_s1=(ip netns exec "${NS_S1}") && ns_active="${ns_active} $NS_S1" [ -n "${NS_R2}" ] && ns_r2=(ip netns exec "${NS_R2}") && ns_active="${ns_active} $NS_R2" [ -n "${NS_S2}" ] && ns_s2=(ip netns exec "${NS_S2}") && ns_active="${ns_active} $NS_S2" [ -n "${NS_R3}" ] && ns_r3=(ip netns exec "${NS_R3}") && ns_active="${ns_active} $NS_R3" [ -n "${NS_B}" ] && ns_active="${ns_active} $NS_B" } addr_add() { local -a ns_cmd=(ip netns exec "$1") local addr="$2" local dev="$3" run_cmd "${ns_cmd[@]}" ip addr add "${addr}" dev "${dev}" run_cmd "${ns_cmd[@]}" ip link set up "${dev}" } veth_add() { local ns=$2 local pns=$1 local -a ns_cmd=(ip netns exec "${pns}") local ln="eth0" local rn="eth1" run_cmd "${ns_cmd[@]}" ip link add "${ln}" type veth peer name "${rn}" netns "${ns}" } show_icmp_filter() { run_cmd "${ns_r2[@]}" nft list ruleset echo "$out" } setup_icmp_filter() { run_cmd "${ns_r2[@]}" nft add table inet filter run_cmd "${ns_r2[@]}" nft add chain inet filter FORWARD \ '{ type filter hook forward priority filter; policy drop ; }' run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD counter ip protocol esp \ counter log accept run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD counter ip protocol \ icmp counter log drop if [ "$VERBOSE" -gt 0 ]; then run_cmd "${ns_r2[@]}" nft list ruleset echo "$out" fi } setup_icmpv6_filter() { run_cmd "${ns_r2[@]}" nft add table inet filter run_cmd "${ns_r2[@]}" nft add chain inet filter FORWARD \ '{ type filter hook forward priority filter; policy drop ; }' run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD ip6 nexthdr \ ipv6-icmp icmpv6 type echo-request counter log drop run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD ip6 nexthdr esp \ counter log accept run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD ip6 nexthdr \ ipv6-icmp icmpv6 type \ '{nd-neighbor-solicit,nd-neighbor-advert,nd-router-solicit,nd-router-advert}' \ counter log drop if [ "$VERBOSE" -gt 0 ]; then run_cmd "${ns_r2[@]}" nft list ruleset echo "$out" fi } set_xfrm_params() { s1_src=${src} s1_dst=${dst} s1_src_net=${src_net} s1_dst_net=${dst_net} } setup_ns_set_v4() { ns_set="a r1 s1 r2 s2 r3 b" # Network topology default imax=$(echo "$ns_set" | wc -w) # number of namespaces in this topology src="10.1.3.1" dst="10.1.4.2" src_net="10.1.1.0/24" dst_net="10.1.6.0/24" prefix=${prefix4} prefix_len=24 s="." S="." set_xfrm_params } setup_ns_set_v4x() { ns_set="a r1 s1 r2 s2 b" # Network topology: x imax=$(echo "$ns_set" | wc -w) # number of namespaces in this topology prefix=${prefix4} s="." S="." src="10.1.3.1" dst="10.1.4.2" src_net="10.1.1.0/24" dst_net="10.1.5.0/24" prefix_len=24 set_xfrm_params } setup_ns_set_v6() { ns_set="a r1 s1 r2 s2 r3 b" # Network topology default imax=$(echo "$ns_set" | wc -w) # number of namespaces in this topology prefix=${prefix6} s=":" S="::" src="fc00:3::1" dst="fc00:4::2" src_net="fc00:1::0/64" dst_net="fc00:6::0/64" prefix_len=64 set_xfrm_params } setup_ns_set_v6x() { ns_set="a r1 s1 r2 s2 b" # Network topology: x imax=$(echo "$ns_set" | wc -w) prefix=${prefix6} s=":" S="::" src="fc00:3::1" dst="fc00:4::2" src_net="fc00:1::0/64" dst_net="fc00:5::0/64" prefix_len=64 set_xfrm_params } setup_network() { # Create veths and add addresses local -a ns_cmd i=1 p="" for ns in ${ns_active}; do ns_cmd=(ip netns exec "${ns}") if [ "${i}" -ne 1 ]; then # Create veth between previous and current namespace veth_add "${p}" "${ns}" # Add addresses: previous gets .1 on eth0, current gets .2 on eth1 addr_add "${p}" "${prefix}${s}$((i-1))${S}1/${prefix_len}" eth0 addr_add "${ns}" "${prefix}${s}$((i-1))${S}2/${prefix_len}" eth1 fi # Enable forwarding run_cmd "${ns_cmd[@]}" sysctl -q net/ipv4/ip_forward=1 run_cmd "${ns_cmd[@]}" sysctl -q net/ipv6/conf/all/forwarding=1 run_cmd "${ns_cmd[@]}" sysctl -q net/ipv6/conf/default/accept_dad=0 p=${ns} i=$((i + 1)) done # Add routes (needs all addresses to exist first) i=1 for ns in ${ns_active}; do ns_cmd=(ip netns exec "${ns}") # Forward routes to networks beyond this node if [ "${i}" -ne "${imax}" ]; then nhf="${prefix}${s}${i}${S}2" # nexthop forward for j in $(seq $((i + 1)) "${imax}"); do run_cmd "${ns_cmd[@]}" ip route replace \ "${prefix}${s}${j}${S}0/${prefix_len}" via "${nhf}" done fi # Reverse routes to networks before this node if [ "${i}" -gt 1 ]; then nhr="${prefix}${s}$((i-1))${S}1" # nexthop reverse for j in $(seq 1 $((i - 2))); do run_cmd "${ns_cmd[@]}" ip route replace \ "${prefix}${s}${j}${S}0/${prefix_len}" via "${nhr}" done fi i=$((i + 1)) done } setup_xfrm_mode() { local MODE=${1:-tunnel} if [ "${MODE}" != "tunnel" ] && [ "${MODE}" != "beet" ]; then echo "xfrm mode ${MODE} not supported" log_test_error return 1 fi run_cmd "${ns_s1[@]}" ip xfrm policy add src "${s1_src_net}" dst "${s1_dst_net}" dir out \ tmpl src "${s1_src}" dst "${s1_dst}" proto esp reqid 1 mode "${MODE}" # no "input" policies. we are only doing forwarding so far run_cmd "${ns_s1[@]}" ip xfrm policy add src "${s1_dst_net}" dst "${s1_src_net}" dir fwd \ flag icmp tmpl src "${s1_dst}" dst "${s1_src}" proto esp reqid 2 mode "${MODE}" run_cmd "${ns_s1[@]}" ip xfrm state add src "${s1_src}" dst "${s1_dst}" proto esp spi 1 \ reqid 1 mode "${MODE}" aead 'rfc4106(gcm(aes))' \ 0x1111111111111111111111111111111111111111 96 \ sel src "${s1_src_net}" dst "${s1_dst_net}" dir out run_cmd "${ns_s1[@]}" ip xfrm state add src "${s1_dst}" dst "${s1_src}" proto esp spi 2 \ reqid 2 flag icmp replay-window 8 mode "${MODE}" aead 'rfc4106(gcm(aes))' \ 0x2222222222222222222222222222222222222222 96 \ sel src "${s1_dst_net}" dst "${s1_src_net}" dir in run_cmd "${ns_s2[@]}" ip xfrm policy add src "${s1_dst_net}" dst "${s1_src_net}" dir out \ flag icmp tmpl src "${s1_dst}" dst "${s1_src}" proto esp reqid 2 mode "${MODE}" run_cmd "${ns_s2[@]}" ip xfrm policy add src "${s1_src_net}" dst "${s1_dst_net}" dir fwd \ tmpl src "${s1_src}" dst "${s1_dst}" proto esp reqid 1 mode "${MODE}" run_cmd "${ns_s2[@]}" ip xfrm state add src "${s1_dst}" dst "${s1_src}" proto esp spi 2 \ reqid 2 mode "${MODE}" aead 'rfc4106(gcm(aes))' \ 0x2222222222222222222222222222222222222222 96 \ sel src "${s1_dst_net}" dst "${s1_src_net}" dir out run_cmd "${ns_s2[@]}" ip xfrm state add src "${s1_src}" dst "${s1_dst}" proto esp spi 1 \ reqid 1 flag icmp replay-window 8 mode "${MODE}" aead 'rfc4106(gcm(aes))' \ 0x1111111111111111111111111111111111111111 96 \ sel src "${s1_src_net}" dst "${s1_dst_net}" dir in } setup_xfrm() { setup_xfrm_mode tunnel } setup() { [ "$(id -u)" -ne 0 ] && echo " need to run as root" && return "$ksft_skip" for arg; do case "${arg}" in ns_set_v4) setup_ns_set_v4 ;; ns_set_v4x) setup_ns_set_v4x ;; ns_set_v6) setup_ns_set_v6 ;; ns_set_v6x) setup_ns_set_v6x ;; namespaces) setup_namespaces ;; network) setup_network ;; xfrm) setup_xfrm ;; icmp_filter) setup_icmp_filter ;; icmpv6_filter) setup_icmpv6_filter ;; *) echo " ${arg} not supported"; return 1 ;; esac || return 1 done } # shellcheck disable=SC2317 # called via trap pause() { echo echo "Pausing. Hit enter to continue" read -r _ } # shellcheck disable=SC2317 # called via trap log_test_error() { # shellcheck disable=SC2031 # fail is set in subshell, read via trap if [ "${fail}" = "yes" ] && [ -n "${desc}" ]; then if [ "$VERBOSE" -gt 0 ]; then show_icmp_filter fi printf "TEST: %-60s [ FAIL ] %s\n" "${desc}" "${name}" [ -n "${cmd}" ] && printf '%s\n\n' "${cmd}" [ -n "${out}" ] && printf '%s\n\n' "${out}" fi } # shellcheck disable=SC2317 # called via trap cleanup() { # shellcheck disable=SC2031 # fail is set in subshell, read via trap [[ "$PAUSE" = "always" || ( "$PAUSE" = "fail" && "$fail" = "yes" ) ]] && pause cleanup_all_ns # shellcheck disable=SC2031 # fail is set in subshell, read via trap [ "${EXIT_ON_TEST_FAIL}" = "yes" ] && [ "${fail}" = "yes" ] && exit 1 } test_unreachable_ipv6() { setup ns_set_v6 namespaces network xfrm icmpv6_filter || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:6::2 run_cmd_err "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:6::3 rc=0 echo -e "$out" | grep -q -E 'From fc00:5::2 icmp_seq.* Destination' || rc=1 return "${rc}" } test_unreachable_gw_ipv6() { setup ns_set_v6x namespaces network xfrm icmpv6_filter || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:5::2 run_cmd_err "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:5::3 rc=0 echo -e "$out" | grep -q -E 'From fc00:4::2 icmp_seq.* Destination' || rc=1 return "${rc}" } test_unreachable_ipv4() { setup ns_set_v4 namespaces network icmp_filter xfrm || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.6.2 run_cmd_err "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.6.3 rc=0 echo -e "$out" | grep -q -E 'From 10.1.5.2 icmp_seq.* Destination' || rc=1 return "${rc}" } test_unreachable_gw_ipv4() { setup ns_set_v4x namespaces network icmp_filter xfrm || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.5.2 run_cmd_err "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.5.3 rc=0 echo -e "$out" | grep -q -E 'From 10.1.4.2 icmp_seq.* Destination' || rc=1 return "${rc}" } test_mtu_ipv4_r2() { setup ns_set_v4 namespaces network icmp_filter xfrm || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.6.2 run_cmd "${ns_r2[@]}" ip route replace 10.1.3.0/24 dev eth1 src 10.1.3.2 mtu 1300 run_cmd "${ns_r2[@]}" ip route replace 10.1.4.0/24 dev eth0 src 10.1.4.1 mtu 1300 # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword run_cmd "${ns_a[@]}" ping -M do -s 1300 -W 5 -w 4 -c 1 10.1.6.2 || true rc=0 echo -e "$out" | grep -q -E "From 10.1.2.2 icmp_seq=.* Frag needed and DF set" || rc=1 return "${rc}" } test_mtu_ipv6_r2() { setup ns_set_v6 namespaces network xfrm icmpv6_filter || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:6::2 run_cmd "${ns_r2[@]}" ip -6 route replace fc00:3::/64 \ dev eth1 metric 256 src fc00:3::2 mtu 1300 run_cmd "${ns_r2[@]}" ip -6 route replace fc00:4::/64 \ dev eth0 metric 256 src fc00:4::1 mtu 1300 # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword run_cmd "${ns_a[@]}" ping -M do -s 1300 -W 5 -w 4 -c 1 fc00:6::2 || true rc=0 echo -e "$out" | grep -q -E "From fc00:2::2 icmp_seq=.* Packet too big: mtu=1230" || rc=1 return "${rc}" } test_mtu_ipv4_r3() { setup ns_set_v4 namespaces network icmp_filter xfrm || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.6.2 run_cmd "${ns_r3[@]}" ip route replace 10.1.6.0/24 dev eth0 mtu 1300 # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword run_cmd "${ns_a[@]}" ping -M do -s 1350 -W 5 -w 4 -c 1 10.1.6.2 || true rc=0 echo -e "$out" | grep -q -E "From 10.1.5.2 .* Frag needed and DF set \(mtu = 1300\)" || rc=1 return "${rc}" } test_mtu_ipv4_s2() { setup ns_set_v4x namespaces network icmp_filter xfrm || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.5.2 run_cmd "${ns_s2[@]}" ip route replace 10.1.5.0/24 dev eth0 src 10.1.5.1 mtu 1300 # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword run_cmd "${ns_a[@]}" ping -M do -s 1350 -W 5 -w 4 -c 1 10.1.5.2 || true rc=0 echo -e "$out" | grep -q -E "From 10.1.4.2.*Frag needed and DF set \(mtu = 1300\)" || rc=1 return "${rc}" } test_mtu_ipv6_s2() { setup ns_set_v6x namespaces network xfrm icmpv6_filter || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:5::2 run_cmd "${ns_s2[@]}" ip -6 route replace fc00:5::/64 dev eth0 metric 256 mtu 1300 # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword run_cmd "${ns_a[@]}" ping -M do -s 1350 -W 5 -w 4 -c 1 fc00:5::2 || true rc=0 echo -e "$out" | grep -q -E "From fc00:4::2.*Packet too big: mtu=1300" || rc=1 return "${rc}" } test_mtu_ipv6_r3() { setup ns_set_v6 namespaces network xfrm icmpv6_filter || return "$ksft_skip" run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:6::2 run_cmd "${ns_r3[@]}" ip -6 route replace fc00:6::/64 dev eth1 metric 256 mtu 1300 # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword run_cmd "${ns_a[@]}" ping -M do -s 1300 -W 5 -w 4 -c 1 fc00:6::2 || true rc=0 echo -e "$out" | grep -q -E "From fc00:5::2 icmp_seq=.* Packet too big: mtu=1300" || rc=1 return "${rc}" } ################################################################################ # usage() { echo echo "$0 [OPTIONS] [TEST]..." echo "If no TEST argument is given, all tests will be run." echo echo -e "\t-p Pause on fail. Namespaces are kept for diagnostics" echo -e "\t-P Pause after the test. Namespaces are kept for diagnostics" echo -e "\t-v Verbose output. Show commands; -vv Show output and nft rules also" echo "Available tests${tests}" exit 1 } ################################################################################ # exitcode=0 all_skipped=true out= cmd= while getopts :epPv o; do case $o in e) EXIT_ON_TEST_FAIL=yes ;; P) PAUSE=always ;; p) PAUSE=fail ;; v) VERBOSE=$((VERBOSE + 1)) ;; *) usage ;; esac done shift $((OPTIND - 1)) IFS=$'\t\n' for arg; do # Check first that all requested tests are available before running any command -v "test_${arg}" >/dev/null || { echo "=== Test ${arg} not found" usage } done name="" desc="" fail="no" for t in ${tests}; do [ "${name}" = "" ] && name="${t}" && continue [ "${desc}" = "" ] && desc="${t}" run_this=1 for arg; do [ "${arg}" = "${name}" ] && run_this=1 && break run_this=0 done if [ $run_this -eq 1 ]; then run_test fi name="" desc="" done exit ${exitcode}