Files
podman/test/system/helpers.network.bash
Stefano Brivio 23433ec7bd test/system: Add support for multipath routes in pasta networking tests
In some environments, such as the one described in
https://github.com/containers/podman/issues/20927, the default route
is given as nexthop gateways. That is, it's a multipath routes with
multiple gateways.

That means that pasta(1), after commit 6c7623d07bbd ("netlink: Add
support to fetch default gateway from multipath routes"), can start
and use a default gateway from that route.

Just like in pasta(1), in these tests, the default route indicates
which upstream interface we should pick. If we ignore multipath
routes, IPv6 addresses and gateway addresses themselves won't be
available, so, while pasta is now able to configure the container,
IPv6 tests will expect to find no address and no gateway, hence fail
due to the mismatch.

Try to get routes, including gateway addresses and interface names,
from nexthop objects, in case the selection of a regular default
route yields no results.

Link: https://github.com/containers/podman/issues/20927
Closes: #20927
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
2024-03-18 14:29:38 +01:00

412 lines
13 KiB
Bash

# -*- bash -*-
### Feature Checks #############################################################
# has_ipv4() - Check if one default route is available for IPv4
function has_ipv4() {
[ -n "$(ip -j -4 route show | jq -rM '.[] | select(.dst == "default")')" ]
}
# has_ipv6() - Check if one default route is available for IPv6
function has_ipv6() {
[ -n "$(ip -j -6 route show | jq -rM '.[] | select(.dst == "default")')" ]
}
# skip_if_no_ipv4() - Skip current test if IPv4 traffic can't be routed
# $1: Optional message to display
function skip_if_no_ipv4() {
if ! has_ipv4; then
local msg=$(_add_label_if_missing "$1" "IPv4")
skip "${msg:-not applicable with no routable IPv4}"
fi
}
# skip_if_no_ipv6() - Skip current test if IPv6 traffic can't be routed
# $1: Optional message to display
function skip_if_no_ipv6() {
if ! has_ipv6; then
local msg=$(_add_label_if_missing "$1" "IPv6")
skip "${msg:-not applicable with no routable IPv6}"
fi
}
# has_pasta() - Check if the pasta(1) command is available
function has_pasta() {
command -v pasta >/dev/null
}
# skip_if_no_pasta() - Skip current test if pasta(1) is not available
# $1: Optional message to display
function skip_if_no_pasta() {
if ! has_pasta; then
local msg=$(_add_label_if_missing "$1" "pasta")
skip "${msg:-not applicable with no pasta binary}"
fi
}
### procfs access ##############################################################
# ipv6_to_procfs() - RFC 5952 IPv6 address text representation to procfs format
# $1: Address in any notation described by RFC 5952
function ipv6_to_procfs() {
local addr="${1}"
# Add leading zero if missing
case ${addr} in
"::"*) addr=0"${addr}" ;;
esac
# Double colon can mean any number of all-zero fields. Expand to fill
# as many colons as are missing. (This will not be a valid IPv6 form,
# but we don't need it for long). E.g., 0::1 -> 0:::::::1
case ${addr} in
*"::"*)
# All the colons in the address
local colons
colons=$(tr -dc : <<<$addr)
# subtract those from a string of eight colons; this gives us
# a string of two to six colons...
local pad
pad=$(sed -e "s/$colons//" <<<":::::::")
# ...which we then inject in place of the double colon.
addr=$(sed -e "s/::/::$pad/" <<<$addr)
;;
esac
# Print as a contiguous string of zero-filled 16-bit words
# (The additional ":" below is needed because 'read -d x' actually
# means "x is a TERMINATOR, not a delimiter")
local group
while read -d : group; do
printf "%04X" "0x${group:-0}"
done <<<"${addr}:"
}
# __ipv4_to_procfs() - Print bytes in hexadecimal notation reversing arguments
# $@: IPv4 address as separate bytes
function __ipv4_to_procfs() {
printf "%02X%02X%02X%02X" ${4} ${3} ${2} ${1}
}
# ipv4_to_procfs() - IPv4 address representation to big-endian procfs format
# $1: Text representation of IPv4 address
function ipv4_to_procfs() {
IFS='.' read -r o1 o2 o3 o4 <<< $1
__ipv4_to_procfs $o1 $o2 $o3 $o4
}
### Addresses, Routes, Links ###################################################
# ipv4_get_addr_global() - Print first global IPv4 address reported by netlink
# $1: Optional output of 'ip -j -4 address show' from a different context
function ipv4_get_addr_global() {
local expr='[.[].addr_info[] | select(.scope=="global")] | .[0].local'
echo "${1:-$(ip -j -4 address show)}" | jq -rM "${expr}"
}
# ipv6_get_addr_global() - Print first global IPv6 address reported by netlink
# $1: Optional output of 'ip -j -6 address show' from a different context
function ipv6_get_addr_global() {
local expr='[.[].addr_info[] | select(.scope=="global")] | .[0].local'
echo "${1:-$(ip -j -6 address show)}" | jq -rM "${expr}"
}
# random_rfc1918_subnet() - Pseudorandom unused subnet in 172.16/12 prefix
#
# Use the class B set, because much of our CI environment (Google, RH)
# already uses up much of the class A, and it's really hard to test
# if a block is in use.
#
# This returns THREE OCTETS! It is up to our caller to append .0/24, .255, &c.
#
function random_rfc1918_subnet() {
local retries=1024
while [ "$retries" -gt 0 ];do
# 172.16.0.0 -> 172.31.255.255
local n1=172
local n2=$(( 16 + $RANDOM & 15 ))
local n3=$(( $RANDOM & 255 ))
if ! subnet_in_use $n1 $n2 $n3; then
echo "$n1.$n2.$n3"
return
fi
retries=$(( retries - 1 ))
done
die "Could not find a random not-in-use rfc1918 subnet"
}
# subnet_in_use() - true if subnet already routed on host
function subnet_in_use() {
local subnet_script=${PODMAN_TMPDIR-/var/tmp}/subnet-in-use
rm -f $subnet_script
# This would be a nightmare to do in bash. ipcalc, ipcalc-ng, sipcalc
# would be nice but are unavailable some environments (cough RHEL).
# Likewise python/perl netmask modules. So, use bare-bones perl.
cat >$subnet_script <<"EOF"
#!/usr/bin/env perl
use strict;
use warnings;
# 3 octets, in binary: 172.16.x -> 1010 1100 0000 1000 xxxx xxxx ...
my $subnet_to_check = sprintf("%08b%08b%08b", @ARGV);
my $found = 0;
# Input is "ip route list", one or more lines like '10.0.0.0/8 via ...'
while (<STDIN>) {
# Only interested in x.x.x.x/n lines
if (m!^([\d.]+)/(\d+)!) {
my ($ip, $bits) = ($1, $2);
# Our caller has /24 granularity, so treat /30 on host as /24.
$bits = 24 if $bits > 24;
# Temporary: entire subnet as binary string. 4 octets, split,
# then represented as a 32-bit binary string.
my $net = sprintf("%08b%08b%08b%08b", split(/\./, $ip));
# Now truncate those 32 bits down to the route's netmask size.
# This is the actual subnet range in use on the host.
my $net_truncated = sprintf("%.*s", $bits, $net);
# Desired subnet is in use if it matches a host route prefix
# print STDERR "--- $subnet_to_check in $net_truncated (@ARGV in $ip/$bits)\n";
$found = 1 if $subnet_to_check =~ /^$net_truncated/;
}
}
# Convert to shell exit status (0 = success)
exit !$found;
EOF
chmod 755 $subnet_script
# This runs 'ip route list', converts x.x.x.x/n to its binary prefix,
# then checks if our desired subnet matches that prefix (i.e. is in
# that range). Existing routes with size greater than 24 are
# normalized to /24 because that's the granularity of our
# random_rfc1918_subnet code.
#
# Contrived examples:
# 127.0.0.0/1 -> 0
# 128.0.0.0/1 -> 1
# 10.0.0.0/8 -> 00001010
#
# I'm so sorry for the ugliness.
ip route list | $subnet_script $*
}
# ipv4_get_route_default() - Print first default IPv4 route reported by netlink
# $1: Optional output of 'ip -j -4 route show' from a different context
function ipv4_get_route_default() {
local jq_gw='[.[] | select(.dst == "default").gateway] | .[0]'
local jq_nh='[.[] | select(.dst == "default").nexthops[0].gateway] | .[0]'
local out
out="$(echo "${1:-$(ip -j -4 route show)}" | jq -rM "${jq_gw}")"
if [ "${out}" = "null" ]; then
out="$(echo "${1:-$(ip -j -4 route show)}" | jq -rM "${jq_nh}")"
fi
echo "${out}"
}
# ipv6_get_route_default() - Print first default IPv6 route reported by netlink
# $1: Optional output of 'ip -j -6 route show' from a different context
function ipv6_get_route_default() {
local jq_gw='[.[] | select(.dst == "default").gateway] | .[0]'
local jq_nh='[.[] | select(.dst == "default").nexthops[0].gateway] | .[0]'
local out
out="$(echo "${1:-$(ip -j -6 route show)}" | jq -rM "${jq_gw}")"
if [ "${out}" = "null" ]; then
out="$(echo "${1:-$(ip -j -6 route show)}" | jq -rM "${jq_nh}")"
fi
echo "${out}"
}
# ether_get_mtu() - Get MTU of first Ethernet-like link
# $1: Optional output of 'ip -j link show' from a different context
function ether_get_mtu() {
local jq_expr='[.[] | select(.link_type == "ether").mtu] | .[0]'
echo "${1:-$(ip -j link show)}" | jq -rM "${jq_expr}"
}
# ether_get_name() - Get name of first Ethernet-like interface
# $1: Optional output of 'ip -j link show' from a different context
function ether_get_name() {
local jq_expr='[.[] | select(.link_type == "ether").ifname] | .[0]'
echo "${1:-$(ip -j link show)}" | jq -rM "${jq_expr}"
}
### Ports and Ranges ###########################################################
# random_free_port() - Get unbound port with pseudorandom number
# $1: Optional, dash-separated interval, [5000, 5999] by default
# $2: Optional binding address, any IPv4 address by default
# $3: Optional protocol, tcp or udp
function random_free_port() {
local range=${1:-5000-5999}
local address=${2:-0.0.0.0}
local protocol=${3:-tcp}
local port
for port in $(shuf -i ${range}); do
if port_is_free $port $address $protocol; then
echo $port
return
fi
done
die "Could not find open port in range $range"
}
# random_free_port_range() - Get range of unbound ports with pseudorandom start
# $1: Size of range (i.e. number of ports)
# $2: Optional binding address, any IPv4 address by default
# $3: Optional protocol, tcp or udp
function random_free_port_range() {
local size=${1?Usage: random_free_port_range SIZE [ADDRESS [tcp|udp]]}
local address=${2:-0.0.0.0}
local protocol=${3:-tcp}
local maxtries=10
while [[ $maxtries -gt 0 ]]; do
local firstport=$(random_free_port)
local lastport=
for i in $(seq 1 $((size - 1))); do
lastport=$((firstport + i))
if ! port_is_free $lastport $address $protocol; then
echo "# port $lastport is in use; trying another." >&3
lastport=
break
fi
done
if [[ -n "$lastport" ]]; then
echo "$firstport-$lastport"
return
fi
maxtries=$((maxtries - 1))
done
die "Could not find free port range with size $size"
}
# port_is_bound() - Check if TCP or UDP port is bound for a given address
# $1: Port number
# $2: Optional protocol, or optional IPv4 or IPv6 address, default: tcp
# $3: Optional IPv4 or IPv6 address, or optional protocol, default: any
function port_is_bound() {
local port=${1?Usage: port_is_bound PORT [tcp|udp] [ADDRESS]}
if [ "${2}" = "tcp" ] || [ "${2}" = "udp" ]; then
local address="${3}"
local proto="${2}"
elif [ "${3}" = "tcp" ] || [ "${3}" = "udp" ]; then
local address="${2}"
local proto="${3}"
else
local address="${2}" # Might be empty
local proto="tcp"
fi
# /proc/net/tcp is insufficient: it does not show some rootless ports.
# ss does, so check it first.
run ss -${proto:0:1}nlH sport = $port
if [[ -n "$output" ]]; then
return
fi
port=$(printf %04X ${port})
case "${address}" in
*":"*)
grep -e "^[^:]*: $(ipv6_to_procfs "${address}"):${port} .*" \
-e "^[^:]*: $(ipv6_to_procfs "::0"):${port} .*" \
-q "/proc/net/${proto}6"
;;
*"."*)
grep -e "^[^:]*: $(ipv4_to_procfs "${address}"):${port}" \
-e "^[^:]*: $(ipv4_to_procfs "0.0.0.0"):${port}" \
-e "^[^:]*: $(ipv4_to_procfs "127.0.0.1"):${port}" \
-q "/proc/net/${proto}"
;;
*)
# No address: check both IPv4 and IPv6, for any bound address
grep "^[^:]*: [^:]*:${port} .*" -q "/proc/net/${proto}6" || \
grep "^[^:]*: [^:]*:${port} .*" -q "/proc/net/${proto}"
;;
esac
}
# port_is_free() - Check if TCP or UDP port is free to bind for a given address
# $1: Port number
# $2: Optional protocol, or optional IPv4 or IPv6 address, default: tcp
# $3: Optional IPv4 or IPv6 address, or optional protocol, default: any
function port_is_free() {
! port_is_bound ${@}
}
# wait_for_port() - Return once port is bound (available for use by clients)
# $1: Host or address to check for possible binding
# $2: Port number
# $3: Optional timeout, 5 seconds if not given
function wait_for_port() {
local host=$1
local port=$2
local _timeout=${3:-5}
# Wait
while [ $_timeout -gt 0 ]; do
port_is_bound ${port} "${host}" && return
sleep 1
_timeout=$(( $_timeout - 1 ))
done
die "Timed out waiting for $host:$port"
}
# tcp_port_probe() - Check if a TCP port has an active listener
# $1: Port number
# $2: Optional address, 0.0.0.0 by default
function tcp_port_probe() {
local address="${2:-0.0.0.0}"
: | nc "${address}" "${1}"
}
### Pasta Helpers ##############################################################
function default_ifname() {
local jq_expr='[.[] | select(.dst == "default").dev] | .[0]'
local jq_expr_nh='[.[] | select(.dst == "default").nexthops[0].dev] | .[0]'
local ip_ver="${1}"
local out
out="$(ip -j -"${ip_ver}" route show | jq -rM "${jq_expr}")"
if [ "${out}" = "null" ]; then
out="$(ip -j -"${ip_ver}" route show | jq -rM "${jq_expr_nh}")"
fi
echo "${out}"
}
function default_addr() {
local ip_ver="${1}"
local ifname="${2:-$(default_ifname "${ip_ver}")}"
local expr='.[0] | .addr_info[0].local'
ip -j -"${ip_ver}" addr show "${ifname}" | jq -rM "${expr}"
}