Files
podman/test/system/505-networking-pasta.bats
Ed Santiago 8c546cf7ee CI: bump VMs
All VMs have pasta 2023-12-04, so, remove a skip.

Signed-off-by: Ed Santiago <santiago@redhat.com>
2024-01-02 16:10:35 -07:00

792 lines
23 KiB
Bash

#!/usr/bin/env bats -*- bats -*-
#
# SPDX-License-Identifier: Apache-2.0
#
# Networking with pasta(1)
#
# Copyright (c) 2022 Red Hat GmbH
# Author: Stefano Brivio <sbrivio@redhat.com>
load helpers
load helpers.network
function setup() {
basic_setup
skip_if_not_rootless "pasta networking only available in rootless mode"
skip_if_no_pasta "pasta not found: install pasta(1) to run these tests"
XFER_FILE="${PODMAN_TMPDIR}/pasta.bin"
}
function default_ifname() {
local ip_ver="${1}"
local expr='[.[] | select(.dst == "default").dev] | .[0]'
ip -j -"${ip_ver}" route show | jq -rM "${expr}"
}
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}"
}
# _set_opt() - meta-helper for pasta_test_do.
#
# Sets an option, but panics if option is already set (e.g. UDP+TCP, IPv4/v6)
function _set_opt() {
local opt_name=$1
local -n opt_ref=$1
local newval=$2
if [[ -n "$opt_ref" ]]; then
# $kw sneakily inherited from caller
die "'$kw' in test name sets $opt_name='$newval', but $opt_name has already been set to '$opt_ref'"
fi
opt_ref=$newval
}
# pasta_test_do() - Run tests involving clients and servers
#
# This helper function is invoked without arguments; it determines what to do
# based on the @test name.
function pasta_test_do() {
local ip_ver iftype proto range delta bind_type bytes
# Normalize test name back to human-readable form. BATS gives us a
# sanitized string with non-alnum converted to '-XX' (dash-hexbyte)
# and spaces converted to underscores. Convert all of those to spaces.
# This then gives us only the important (mutable) part of the test:
#
# test_TCP_translated_..._forwarding-2c_IPv4-2c_loopback
# -> TCP translated ... forwarding IPv4 loopback
# -> TCP translated forwarding IPv4 loopback
local test_name=$(printf "$(sed \
-e 's/^test_//' \
-e 's/-\([0-9a-f]\{2\}\)/ /gI' \
-e 's/_/ /g' \
<<<"${BATS_TEST_NAME}")")
# We now have the @test name as specified in the script, minus punctuation.
# From each of the name components, determine an action.
#
# TCP translated port range forwarding IPv4 loopback
# | | | | | | \__ iftype=loopback
# | | | | | \________ ip_ver=4
# | | | | \____________________ bytes=1
# | | | \__________________________ range=3
# | | \_______________________________ (ignored)
# | \__________________________________________ delta=1
# \______________________________________________ proto=tcp
#
# Each keyword maps to one option. Conflicts ("TCP ... UDP") are fatal
# errors, as are unknown keywords.
for kw in $test_name; do
case $kw in
TCP|UDP) _set_opt proto ${kw,,} ;;
IPv*) _set_opt ip_ver $(expr "$kw" : "IPv\(.\)") ;;
Single) _set_opt range 1 ;;
range) _set_opt range 3 ;;
Address|Interface) _set_opt bind_type ${kw,,} ;;
bound) assert "$bind_type" != "" "WHAT-bound???" ;;
[Tt]ranslated) _set_opt delta 1 ;;
loopback|tap) _set_opt iftype $kw ;;
port) ;; # always occurs with 'forwarding'; ignore
forwarding) _set_opt bytes 1 ;;
large|small) _set_opt bytes $kw ;;
transfer) assert "$bytes" != "" "'transfer' must be preceded by 'large' or 'small'" ;;
*) die "cannot grok '$kw' in test name" ;;
esac
done
# Sanity checks: all test names must include IPv4/6 and TCP/UDP
test -n "$ip_ver" || die "Test name must include IPv4 or IPv6"
test -n "$proto" || die "Test name must include TCP or UDP"
test -n "$bytes" || die "Test name must include 'forwarding' or 'large/small transfer'"
# Major decision point: simple forwarding test, or multi-byte transfer?
if [[ $bytes -eq 1 ]]; then
# Simple forwarding check
# We can't always determine these from the test name. Use sane defaults.
range=${range:-1}
delta=${delta:-0}
bind_type=${bind_type:-port}
else
# Data transfer. Translate small/large to dd-recognizable sizes
case "$bytes" in
small) bytes="2k" ;;
large) case "$proto" in
tcp) bytes="10M" ;;
udp) bytes=$(($(cat /proc/sys/net/core/wmem_default) / 4)) ;;
*) die "Internal error: unknown proto '$proto'" ;;
esac
;;
*) die "Internal error: unknown transfer size '$bytes'" ;;
esac
# On data transfers, no other input args can be set in test name.
# Confirm that they are not defined, and set to a suitable default.
kw="something"
_set_opt range 1
_set_opt delta 0
_set_opt bind_type port
fi
# Dup check: make sure we haven't already run this combination of settings.
# This serves two purposes:
# 1) prevent developer from accidentally copy/pasting the same test
# 2) make sure our test-name-parsing code isn't missing anything important
local tests_run=${BATS_FILE_TMPDIR}/tests_run
touch ${tests_run}
local testid="IPv${ip_ver} $proto $iftype $bind_type range=$range delta=$delta bytes=$bytes"
if grep -q -F -- "$testid" ${tests_run}; then
die "Duplicate test! Have already run $testid"
fi
echo "$testid" >>${tests_run}
# Done figuring out test params. Now do the real work.
# Calculate and set addresses,
if [ ${ip_ver} -eq 4 ]; then
skip_if_no_ipv4 "IPv4 not routable on the host"
elif [ ${ip_ver} -eq 6 ]; then
skip_if_no_ipv6 "IPv6 not routable on the host"
else
skip "Unsupported IP version"
fi
if [ ${iftype} = "loopback" ]; then
local ifname="lo"
else
local ifname="$(default_ifname "${ip_ver}")"
fi
local addr="$(default_addr "${ip_ver}" "${ifname}")"
# ports,
if [ ${range} -gt 1 ]; then
local port="$(random_free_port_range ${range} ${addr} ${proto})"
local xport="$((${port%-*} + delta))-$((${port#*-} + delta))"
local seq="$(echo ${port} | tr '-' ' ')"
local xseq="$(echo ${xport} | tr '-' ' ')"
else
local port=$(random_free_port "" ${address} ${proto})
local xport="$((port + delta))"
local seq="${port} ${port}"
local xseq="${xport} ${xport}"
fi
local proto_upper="$(echo "${proto}" | tr [:lower:] [:upper:])"
# socat options for first <address> in server ("LISTEN" address types),
local bind="${proto_upper}${ip_ver}-LISTEN:\${port}"
# For IPv6 via tap, we can pick either link-local or global unicast
if [ ${ip_ver} -eq 4 ] || [ ${iftype} = "loopback" ]; then
bind="${bind},bind=[${addr}]"
fi
if [ "${proto}" = "udp" ]; then
bind="${bind},null-eof"
fi
# socat options for second <address> in server ("STDOUT" or "EXEC"),
if [ "${bytes}" = "1" ]; then
recv="STDOUT"
else
recv="EXEC:md5sum"
fi
# and port forwarding configuration for Podman and pasta.
#
# TODO: Use Podman options once/if
# https://github.com/containers/podman/issues/14425 is solved
case ${bind_type} in
"interface")
local pasta_spec=":--${proto}-ports,${addr}%${ifname}/${port}:${xport}"
local podman_spec=
;;
"address")
local pasta_spec=
local podman_spec="[${addr}]:${port}:${xport}/${proto}"
;;
*)
local pasta_spec=
local podman_spec="[${addr}]:${port}:${xport}/${proto}"
;;
esac
# Fill in file for data transfer tests, and expected output strings
if [ "${bytes}" != "1" ]; then
dd if=/dev/urandom bs=${bytes} count=1 of="${XFER_FILE}"
local expect="$(cat "${XFER_FILE}" | md5sum)"
else
printf "x" > "${XFER_FILE}"
local expect="$(for i in $(seq ${seq}); do printf "x"; done)"
fi
# Set retry/initial delay for client to connect
local delay=1
if [ ${ip_ver} -eq 6 ] && [ "${addr}" != "::1" ]; then
# Duplicate Address Detection on link-local
delay=3
elif [ "${proto}" = "udp" ]; then
# With Podman up, and socat still starting, UDP clients send to nowhere
delay=2
fi
# Now actually run the test: client,
for one_port in $(seq ${seq}); do
local connect="${proto_upper}${ip_ver}:[${addr}]:${one_port}"
[ "${proto}" = "udp" ] && connect="${connect},shut-null"
local retries=10
(while sleep ${delay} && test $((retries--)) -gt 0 && ! timeout --foreground -v --kill=5 90 socat -u "OPEN:${XFER_FILE}" "${connect}"; do :
done) &
done
# and server,
run_podman run --net=pasta"${pasta_spec}" -p "${podman_spec}" "${IMAGE}" \
sh -c 'for port in $(seq '"${xseq}"'); do '\
' socat -u '"${bind}"' '"${recv}"' & '\
' done; wait'
# which should give us the expected output back.
assert "${output}" = "${expect}" "Mismatch between data sent and received"
}
### Addresses ##################################################################
@test "IPv4 default address assignment" {
skip_if_no_ipv4 "IPv4 not routable on the host"
run_podman run --net=pasta $IMAGE ip -j -4 address show
local container_address="$(ipv4_get_addr_global "${output}")"
local host_address="$(default_addr 4)"
assert "${container_address}" = "${host_address}" \
"Container address not matching host"
}
@test "IPv4 address assignment" {
skip_if_no_ipv4 "IPv4 not routable on the host"
run_podman run --net=pasta:-a,192.0.2.1 $IMAGE ip -j -4 address show
local container_address="$(ipv4_get_addr_global "${output}")"
assert "${container_address}" = "192.0.2.1" \
"Container address not matching configured value"
}
@test "No IPv4" {
skip_if_no_ipv4 "IPv4 not routable on the host"
skip_if_no_ipv6 "IPv6 not routable on the host"
run_podman run --net=pasta:-6 $IMAGE ip -j -4 address show
local container_address="$(ipv4_get_addr_global "${output}")"
assert "${container_address}" = "null" \
"Container has IPv4 global address with IPv4 disabled"
}
@test "IPv6 default address assignment" {
skip_if_no_ipv6 "IPv6 not routable on the host"
run_podman run --net=pasta $IMAGE ip -j -6 address show
local container_address="$(ipv6_get_addr_global "${output}")"
local host_address="$(default_addr 6)"
assert "${container_address}" = "${host_address}" \
"Container address not matching host"
}
@test "IPv6 address assignment" {
skip_if_no_ipv6 "IPv6 not routable on the host"
run_podman run --net=pasta:-a,2001:db8::1 $IMAGE ip -j -6 address show
local container_address="$(ipv6_get_addr_global "${output}")"
assert "${container_address}" = "2001:db8::1" \
"Container address not matching configured value"
}
@test "No IPv6" {
skip_if_no_ipv6 "IPv6 not routable on the host"
skip_if_no_ipv4 "IPv4 not routable on the host"
run_podman run --net=pasta:-4 $IMAGE ip -j -6 address show
local container_address="$(ipv6_get_addr_global "${output}")"
assert "${container_address}" = "null" \
"Container has IPv6 global address with IPv6 disabled"
}
@test "podman puts pasta IP in /etc/hosts" {
skip_if_no_ipv4 "IPv4 not routable on the host"
pname="p$(random_string 30)"
ip="$(default_addr 4)"
run_podman pod create --net=pasta --name "${pname}"
run_podman run --pod="${pname}" "${IMAGE}" getent hosts "${pname}"
assert "$(echo ${output} | cut -f1 -d' ')" = "${ip}" "Correct /etc/hosts entry missing"
run_podman pod rm "${pname}"
run_podman rmi $(pause_image)
}
### Routes #####################################################################
@test "IPv4 default route" {
skip_if_no_ipv4 "IPv4 not routable on the host"
run_podman run --net=pasta $IMAGE ip -j -4 route show
local container_route="$(ipv4_get_route_default "${output}")"
local host_route="$(ipv4_get_route_default)"
assert "${container_route}" = "${host_route}" \
"Container route not matching host"
}
@test "IPv4 default route assignment" {
skip_if_no_ipv4 "IPv4 not routable on the host"
run_podman run --net=pasta:-a,192.0.2.2,-g,192.0.2.1 $IMAGE \
ip -j -4 route show
local container_route="$(ipv4_get_route_default "${output}")"
assert "${container_route}" = "192.0.2.1" \
"Container route not matching configured value"
}
@test "IPv6 default route" {
skip_if_no_ipv6 "IPv6 not routable on the host"
run_podman run --net=pasta $IMAGE ip -j -6 route show
local container_route="$(ipv6_get_route_default "${output}")"
local host_route="$(ipv6_get_route_default)"
assert "${container_route}" = "${host_route}" \
"Container route not matching host"
}
@test "IPv6 default route assignment" {
skip_if_no_ipv6 "IPv6 not routable on the host"
run_podman run --net=pasta:-a,2001:db8::2,-g,2001:db8::1 $IMAGE \
ip -j -6 route show
local container_route="$(ipv6_get_route_default "${output}")"
assert "${container_route}" = "2001:db8::1" \
"Container route not matching configured value"
}
### Interfaces #################################################################
@test "Default MTU" {
run_podman run --net=pasta $IMAGE ip -j link show
container_tap_mtu="$(ether_get_mtu "${output}")"
assert "${container_tap_mtu}" = "65520" \
"Container's default MTU not 65220 bytes by default"
}
@test "MTU assignment" {
run_podman run --net=pasta:-m,1280 $IMAGE ip -j link show
container_tap_mtu="$(ether_get_mtu "${output}")"
assert "${container_tap_mtu}" = "1280" \
"Container's default MTU not matching configured 1280 bytes"
}
@test "Loopback interface state" {
run_podman run --net=pasta $IMAGE ip -j link show
local jq_expr='.[] | select(.link_type == "loopback").flags | '\
' contains(["UP"])'
container_loopback_up="$(printf '%s' "${output}" | jq -rM "${jq_expr}")"
assert "${container_loopback_up}" = "true" \
"Container's loopback interface not up"
}
### DNS ########################################################################
@test "External resolver, IPv4" {
skip_if_no_ipv4 "IPv4 not routable on the host"
run_podman '?' run --net=pasta $IMAGE nslookup 127.0.0.1
assert "$output" =~ "1.0.0.127.in-addr.arpa" \
"127.0.0.1 not resolved"
}
@test "External resolver, IPv6" {
skip_if_no_ipv6 "IPv6 not routable on the host"
run_podman run --net=pasta $IMAGE nslookup ::1 || :
assert "$output" =~ "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" \
"::1 not resolved"
}
@test "Local forwarder, IPv4" {
skip_if_no_ipv4 "IPv4 not routable on the host"
run_podman run --dns 198.51.100.1 \
--net=pasta:--dns-forward,198.51.100.1 $IMAGE nslookup 127.0.0.1 || :
assert "$output" =~ "1.0.0.127.in-addr.arpa" "No answer from resolver"
}
@test "Local forwarder, IPv6" {
skip_if_no_ipv6 "IPv6 not routable on the host"
# TODO: Two issues here:
skip "Currently unsupported"
# run_podman run --dns 2001:db8::1 \
# --net=pasta:--dns-forward,2001:db8::1 $IMAGE nslookup ::1
#
# 1. With this, Podman writes "nameserver 2001:db8::1" to
# /etc/resolv.conf, without zone, and the query originates from ::1.
# Passing:
# --dns "2001:db8::2%eth0"
# results in:
# Error: 2001:db8::2%eth0 is not an ip address
# Fix the issue in Podman once we figure out 2. below.
#
#
# run_podman run --dns 2001:db8::1 \
# --net=pasta:--dns-forward,2001:db8::1 $IMAGE \
# sh -c 'echo 2001:db8::1%eth0 >/etc/resolv.conf; nslookup ::1'
#
# 2. This fixes the source address of the query, but the answer is
# discarded. Figure out if it's an issue in Busybox, in musl, if we
# should just include a full-fledged resolver in the test image, etc.
}
### TCP/IPv4 Port Forwarding ###################################################
@test "Single TCP port forwarding, IPv4, tap" {
pasta_test_do
}
@test "Single TCP port forwarding, IPv4, loopback" {
pasta_test_do
}
@test "TCP port range forwarding, IPv4, tap" {
pasta_test_do
}
@test "TCP port range forwarding, IPv4, loopback" {
pasta_test_do
}
@test "Translated TCP port forwarding, IPv4, tap" {
pasta_test_do
}
@test "Translated TCP port forwarding, IPv4, loopback" {
pasta_test_do
}
@test "TCP translated port range forwarding, IPv4, tap" {
pasta_test_do
}
@test "TCP translated port range forwarding, IPv4, loopback" {
pasta_test_do
}
@test "Address-bound TCP port forwarding, IPv4, tap" {
pasta_test_do
}
@test "Address-bound TCP port forwarding, IPv4, loopback" {
pasta_test_do
}
@test "Interface-bound TCP port forwarding, IPv4, tap" {
pasta_test_do
}
@test "Interface-bound TCP port forwarding, IPv4, loopback" {
pasta_test_do
}
### TCP/IPv6 Port Forwarding ###################################################
@test "Single TCP port forwarding, IPv6, tap" {
pasta_test_do
}
@test "Single TCP port forwarding, IPv6, loopback" {
pasta_test_do
}
@test "TCP port range forwarding, IPv6, tap" {
pasta_test_do
}
@test "TCP port range forwarding, IPv6, loopback" {
pasta_test_do
}
@test "Translated TCP port forwarding, IPv6, tap" {
pasta_test_do
}
@test "Translated TCP port forwarding, IPv6, loopback" {
pasta_test_do
}
@test "TCP translated port range forwarding, IPv6, tap" {
pasta_test_do
}
@test "TCP translated port range forwarding, IPv6, loopback" {
pasta_test_do
}
@test "Address-bound TCP port forwarding, IPv6, tap" {
pasta_test_do
}
@test "Address-bound TCP port forwarding, IPv6, loopback" {
pasta_test_do
}
@test "Interface-bound TCP port forwarding, IPv6, tap" {
pasta_test_do
}
@test "Interface-bound TCP port forwarding, IPv6, loopback" {
pasta_test_do
}
### UDP/IPv4 Port Forwarding ###################################################
@test "Single UDP port forwarding, IPv4, tap" {
pasta_test_do
}
@test "Single UDP port forwarding, IPv4, loopback" {
pasta_test_do
}
@test "UDP port range forwarding, IPv4, tap" {
pasta_test_do
}
@test "UDP port range forwarding, IPv4, loopback" {
pasta_test_do
}
@test "Translated UDP port forwarding, IPv4, tap" {
pasta_test_do
}
@test "Translated UDP port forwarding, IPv4, loopback" {
pasta_test_do
}
@test "UDP translated port range forwarding, IPv4, tap" {
pasta_test_do
}
@test "UDP translated port range forwarding, IPv4, loopback" {
pasta_test_do
}
@test "Address-bound UDP port forwarding, IPv4, tap" {
pasta_test_do
}
@test "Address-bound UDP port forwarding, IPv4, loopback" {
pasta_test_do
}
@test "Interface-bound UDP port forwarding, IPv4, tap" {
pasta_test_do
}
@test "Interface-bound UDP port forwarding, IPv4, loopback" {
pasta_test_do
}
### UDP/IPv6 Port Forwarding ###################################################
@test "Single UDP port forwarding, IPv6, tap" {
pasta_test_do
}
@test "Single UDP port forwarding, IPv6, loopback" {
pasta_test_do
}
@test "UDP port range forwarding, IPv6, tap" {
pasta_test_do
}
@test "UDP port range forwarding, IPv6, loopback" {
pasta_test_do
}
@test "Translated UDP port forwarding, IPv6, tap" {
pasta_test_do
}
@test "Translated UDP port forwarding, IPv6, loopback" {
pasta_test_do
}
@test "UDP translated port range forwarding, IPv6, tap" {
pasta_test_do
}
@test "UDP translated port range forwarding, IPv6, loopback" {
pasta_test_do
}
@test "Address-bound UDP port forwarding, IPv6, tap" {
pasta_test_do
}
@test "Address-bound UDP port forwarding, IPv6, loopback" {
pasta_test_do
}
@test "Interface-bound UDP port forwarding, IPv6, tap" {
pasta_test_do
}
@test "Interface-bound UDP port forwarding, IPv6, loopback" {
pasta_test_do
}
### TCP/IPv4 transfer ##########################################################
@test "TCP/IPv4 small transfer, tap" {
pasta_test_do
}
@test "TCP/IPv4 small transfer, loopback" {
pasta_test_do
}
@test "TCP/IPv4 large transfer, tap" {
pasta_test_do
}
@test "TCP/IPv4 large transfer, loopback" {
pasta_test_do
}
### TCP/IPv6 transfer ##########################################################
@test "TCP/IPv6 small transfer, tap" {
pasta_test_do
}
@test "TCP/IPv6 small transfer, loopback" {
pasta_test_do
}
@test "TCP/IPv6 large transfer, tap" {
pasta_test_do
}
@test "TCP/IPv6 large transfer, loopback" {
pasta_test_do
}
### UDP/IPv4 transfer ##########################################################
@test "UDP/IPv4 small transfer, tap" {
pasta_test_do
}
@test "UDP/IPv4 small transfer, loopback" {
pasta_test_do
}
@test "UDP/IPv4 large transfer, tap" {
pasta_test_do
}
@test "UDP/IPv4 large transfer, loopback" {
pasta_test_do
}
### UDP/IPv6 transfer ##########################################################
@test "UDP/IPv6 small transfer, tap" {
pasta_test_do
}
@test "UDP/IPv6 small transfer, loopback" {
pasta_test_do
}
@test "UDP/IPv6 large transfer, tap" {
pasta_test_do
}
@test "UDP/IPv6 large transfer, loopback" {
pasta_test_do
}
### Lifecycle ##################################################################
@test "pasta(1) quits when the namespace is gone" {
local pidfile="${PODMAN_TMPDIR}/pasta.pid"
run_podman run "--net=pasta:--pid,${pidfile}" $IMAGE true
sleep 1
! ps -p $(cat "${pidfile}") && rm "${pidfile}"
}
### Options ####################################################################
@test "Unsupported protocol in port forwarding" {
local port=$(random_free_port "" "" tcp)
run_podman 126 run --net=pasta -p "${port}:${port}/sctp" $IMAGE true
is "$output" "Error: .*can't forward protocol: sctp"
}
@test "Use options from containers.conf" {
skip_if_remote "containers.conf must be set for the server"
containersconf=$PODMAN_TMPDIR/containers.conf
mac="9a:dd:31:ea:92:98"
cat >$containersconf <<EOF
[network]
default_rootless_network_cmd = "pasta"
pasta_options = ["-I", "myname", "--ns-mac-addr", "$mac"]
EOF
# 2023-06-29 DO NOT INCLUDE "--net=pasta" on this line!
# This tests containers.conf:default_rootless_network_cmd (pr #19032)
CONTAINERS_CONF_OVERRIDE=$containersconf run_podman run $IMAGE ip link show myname
assert "$output" =~ "$mac" "mac address is set on custom interface"
# now, again but this time overwrite a option on the cli.
mac2="aa:bb:cc:dd:ee:ff"
CONTAINERS_CONF_OVERRIDE=$containersconf run_podman run --net=pasta:--ns-mac-addr,"$mac2" $IMAGE ip link show myname
assert "$output" =~ "$mac2" "mac address from cli is set on custom interface"
}