mirror of
https://github.com/containers/podman.git
synced 2025-12-02 11:08:36 +08:00
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>
412 lines
13 KiB
Bash
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}"
|
|
}
|