From 41ac7d146034c2dec9c022f9e7dcca485840d4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irving=20Mondrag=C3=B3n?= Date: Sun, 8 Mar 2026 04:46:57 +0100 Subject: [PATCH] Replace test-port-forwarding.pl with BATS test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Irving MondragΓ³n --- .github/workflows/test.yml | 12 + hack/bats/helpers/limactl.bash | 18 + hack/bats/tests/port-forwarding-config.bash | 219 +++++++++ hack/bats/tests/port-forwarding.bats | 450 +++++++++++++++++ hack/test-port-forwarding.pl | 505 -------------------- hack/test-templates.sh | 67 +-- 6 files changed, 717 insertions(+), 554 deletions(-) create mode 100644 hack/bats/tests/port-forwarding-config.bash create mode 100644 hack/bats/tests/port-forwarding.bats delete mode 100755 hack/test-port-forwarding.pl diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6929fbacbf4..b34215d2f7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -171,6 +171,8 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable @@ -199,6 +201,8 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable @@ -224,6 +228,8 @@ jobs: timeout-minutes: 120 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable @@ -296,6 +302,8 @@ jobs: - ../hack/test-templates/test-misc.yaml # TODO: merge net-user-v2 into test-misc steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable @@ -430,6 +438,8 @@ jobs: timeout-minutes: 120 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable @@ -532,6 +542,8 @@ jobs: # --cpus=1 is needed for running vz on GHA: https://github.com/lima-vm/lima/pull/1511#issuecomment-1574937888 run: echo "LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --cpus 1 --memory 1" >>$GITHUB_ENV - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable diff --git a/hack/bats/helpers/limactl.bash b/hack/bats/helpers/limactl.bash index f3e98d9d044..ca2b5378e16 100644 --- a/hack/bats/helpers/limactl.bash +++ b/hack/bats/helpers/limactl.bash @@ -31,6 +31,24 @@ ensure_instance() { bats) limactl start --yes --name "$instance" template:default 3>&- 4>&- ;; bats-nomount) limactl start --yes --name "$instance" --mount-none template:default 3>&- 4>&- ;; bats-dummy) create_dummy_instance "$instance" '.disk = "1M"' ;; + bats-portfwd) + local tmpconfig="$BATS_FILE_TMPDIR/config" + mkdir -p "$tmpconfig" + local tmpfile="$tmpconfig/bats-portfwd.yaml" + limactl tmpl copy "template:default" "$tmpfile" + # Remove existing portForwards section: skip from "portForwards:" until the next top-level key + local cleaned + cleaned=$(awk ' + /^portForwards:/ { skip=1; next } + skip && /^[a-z]/ { skip=0 } + !skip { print } + ' "$tmpfile") + echo "$cleaned" >"$tmpfile" + # shellcheck source=../tests/port-forwarding-config.bash + source "$PATH_BATS_ROOT/tests/port-forwarding-config.bash" + generate_port_forwards_yaml "$(get_host_ipv4)" >>"$tmpfile" + limactl start --yes --name "$instance" "$tmpfile" 3>&- 4>&- + ;; *) echo "ensure_instance: unknown instance name '$instance'" >&2 return 1 diff --git a/hack/bats/tests/port-forwarding-config.bash b/hack/bats/tests/port-forwarding-config.bash new file mode 100644 index 00000000000..84bfe068eb7 --- /dev/null +++ b/hack/bats/tests/port-forwarding-config.bash @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + +# Port forwarding configuration for the BATS port-forwarding test. +# Sourced by port-forwarding.bats and helpers/limactl.bash (for instance creation). + +# shellcheck shell=bash + +# Detect the host's external IPv4 address. +get_host_ipv4() { + local ipv4="" + if [[ $(uname -s) == "Darwin" ]]; then + ipv4=$(system_profiler SPNetworkDataType -json 2>/dev/null | + jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first') + elif [[ $(uname -o 2>/dev/null) == "Msys" ]]; then + # hostname -I doesn't exist on MSYS2; use .NET DNS resolver (same as old Perl gethostbyname) + # shellcheck disable=SC2016 # $_ is PowerShell syntax, not bash + ipv4=$(powershell.exe -NoProfile -Command \ + '[System.Net.Dns]::GetHostAddresses((hostname)) | Where-Object {$_.AddressFamily -eq "InterNetwork" -and $_.IPAddressToString -ne "127.0.0.1"} | Select-Object -First 1 -ExpandProperty IPAddressToString' \ + 2>/dev/null | tr -d '\r') + else + # Linux: first non-loopback IPv4 from hostname -I + ipv4=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v ':' | grep -v '^127\.' | head -1) + fi + # Fallback + if [[ -z $ipv4 || $ipv4 == "null" ]]; then + ipv4="127.0.0.1" + fi + echo "$ipv4" +} + +# Port forwarding rules (YAML) with interleaved test specs (comments). +# Placeholders "HOST_IPV4" and "SOCK_DIR/" are replaced at runtime. +# +# Test spec comment format: +# # forward: -> +# # forward: -> +# # ignore: +# # skip: +port_forwards_block() { + cat <<'RULES' +portForwards: +# skip: 127.0.0.1 22 -> 127.0.0.1 2222 +# skip: 127.0.0.1 SSH_LOCAL_PORT + +- guestIP: 127.0.0.2 + guestPortRange: [3000, 3009] + hostPortRange: [2000, 2009] + ignore: true + +- guestIP: 0.0.0.0 + guestIPMustBeZero: false + guestPortRange: [3010, 3019] + hostPortRange: [2010, 2019] + ignore: true + +- guestIP: 0.0.0.0 + guestIPMustBeZero: false + guestPortRange: [3000, 3029] + hostPortRange: [2000, 2029] + +# The following rule is completely shadowed by the previous one and has no effect +- guestIP: 0.0.0.0 + guestIPMustBeZero: false + guestPortRange: [3020, 3029] + hostPortRange: [2020, 2029] + ignore: true + + # ignore: 127.0.0.2 3000 + # forward: 127.0.0.3 3001 -> 127.0.0.1 2001 + + # Blocking 127.0.0.2 cannot block forwarding from 0.0.0.0 + # forward: 0.0.0.0 3002 -> 127.0.0.1 2002 + + # Blocking 0.0.0.0 will block forwarding from any interface because guestIPMustBeZero is false + # ignore: 0.0.0.0 3010 + # ignore: 127.0.0.1 3011 + + # Forwarding from 0.0.0.0 works for any interface (including IPv6) + # The "ignore" rule above has no effect because the previous rule already matched. + # forward: 127.0.0.2 3020 -> 127.0.0.1 2020 + # forward: 127.0.0.1 3021 -> 127.0.0.1 2021 + # forward: 0.0.0.0 3022 -> 127.0.0.1 2022 + # forward: :: 3023 -> 127.0.0.1 2023 + # forward: ::1 3024 -> 127.0.0.1 2024 + +- guestPortRange: [3030, 3039] + hostPortRange: [2030, 2039] + hostIP: HOST_IPV4 + + # forward: 127.0.0.1 3030 -> HOST_IPV4 2030 + # forward: 0.0.0.0 3031 -> HOST_IPV4 2031 + # forward: :: 3032 -> HOST_IPV4 2032 + # forward: ::1 3033 -> HOST_IPV4 2033 + +- guestPortRange: [300, 304] + + # forward: 127.0.0.1 300 -> 127.0.0.1 300 + # forward: 0.0.0.0 301 -> 127.0.0.1 301 + # forward: :: 302 -> 127.0.0.1 302 + # forward: ::1 303 -> 127.0.0.1 303 + # ignore: 192.168.5.15 304 -> 127.0.0.1 304 + +- guestPortRange: [305, 309] + guestIPMustBeZero: false + + # forward: 127.0.0.1 325 -> 127.0.0.1 325 + # forward: 0.0.0.0 326 -> 127.0.0.1 326 + # forward: :: 327 -> 127.0.0.1 327 + # forward: ::1 328 -> 127.0.0.1 328 + # ignore: 192.168.5.15 329 -> 127.0.0.1 329 + +- guestPortRange: [310, 314] + hostIP: 0.0.0.0 + + # forward: 127.0.0.1 310 -> 0.0.0.0 310 + # forward: 0.0.0.0 311 -> 0.0.0.0 311 + # forward: :: 312 -> 0.0.0.0 312 + # forward: ::1 313 -> 0.0.0.0 313 + # ignore: 192.168.5.15 314 -> 0.0.0.0 314 + +- guestPortRange: [315, 319] + guestIPMustBeZero: false + hostIP: 0.0.0.0 + + # forward: 127.0.0.1 315 -> 0.0.0.0 315 + # forward: 0.0.0.0 316 -> 0.0.0.0 316 + # forward: :: 317 -> 0.0.0.0 317 + # forward: ::1 318 -> 0.0.0.0 318 + # ignore: 192.168.5.15 319 -> 0.0.0.0 319 + + # Things we can't test: + # - Accessing a forward from a different interface (e.g. connect to HOST_IPV4 to connect to 0.0.0.0) + # - failed forward to privileged port + +- guestIP: "192.168.5.15" + guestPortRange: [4000, 4009] + hostIP: "HOST_IPV4" + + # forward: 192.168.5.15 4000 -> HOST_IPV4 4000 + +- guestIP: "::1" + guestPortRange: [4010, 4019] + hostIP: "::" + + # forward: ::1 4010 -> :: 4010 + +- guestIP: "::" + guestPortRange: [4020, 4029] + hostIP: "HOST_IPV4" + + # forward: 127.0.0.1 4020 -> HOST_IPV4 4020 + # forward: 127.0.0.2 4021 -> HOST_IPV4 4021 + # forward: 192.168.5.15 4022 -> HOST_IPV4 4022 + # forward: 0.0.0.0 4023 -> HOST_IPV4 4023 + # forward: :: 4024 -> HOST_IPV4 4024 + # forward: ::1 4025 -> HOST_IPV4 4025 + +- guestIP: "0.0.0.0" + guestIPMustBeZero: false + guestPortRange: [4030, 4039] + hostIP: "HOST_IPV4" + + # forward: 127.0.0.1 4030 -> HOST_IPV4 4030 + # forward: 127.0.0.2 4031 -> HOST_IPV4 4031 + # forward: 192.168.5.15 4032 -> HOST_IPV4 4032 + # forward: 0.0.0.0 4033 -> HOST_IPV4 4033 + # forward: :: 4034 -> HOST_IPV4 4034 + # forward: ::1 4035 -> HOST_IPV4 4035 + +- guestIPMustBeZero: true + guestPortRange: [4040, 4049] + +- guestIP: "0.0.0.0" + guestIPMustBeZero: false + guestPortRange: [4040, 4049] + ignore: true + + # forward: 0.0.0.0 4040 -> 127.0.0.1 4040 + # forward: :: 4041 -> 127.0.0.1 4041 + # ignore: 127.0.0.1 4043 -> 127.0.0.1 4043 + # ignore: 192.168.5.15 4044 -> 127.0.0.1 4044 + +# This rule exists to test `nerdctl run` binding to 0.0.0.0 by default, +# and making sure it gets forwarded to the external host IP. +# The actual test code is in test-templates.sh in the "container-engine" block. +- guestIPMustBeZero: true + guestPort: 8888 + hostIP: 0.0.0.0 + +- guestPort: 5000 + hostSocket: port5000.sock + + # forward: 127.0.0.1 5000 -> SOCK_DIR/port5000.sock + +- guestPort: 5001 + hostSocket: port5001.sock + + # ignore: 192.168.5.15 5001 -> SOCK_DIR/port5001.sock + +- guestPort: 5002 + guestIPMustBeZero: false + hostSocket: port5002.sock + + # forward: 127.0.0.1 5002 -> SOCK_DIR/port5002.sock + +- guestPort: 5003 + guestIPMustBeZero: false + hostSocket: port5003.sock + + # ignore: 192.168.5.15 5003 -> SOCK_DIR/port5003.sock +RULES +} + +# Generate port forwards YAML with HOST_IPV4 placeholder replaced. +generate_port_forwards_yaml() { + local host_ipv4=$1 + port_forwards_block | sed "s/HOST_IPV4/${host_ipv4}/g" +} diff --git a/hack/bats/tests/port-forwarding.bats b/hack/bats/tests/port-forwarding.bats new file mode 100644 index 00000000000..ae90e578693 --- /dev/null +++ b/hack/bats/tests/port-forwarding.bats @@ -0,0 +1,450 @@ +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + +# This test verifies Lima's port forwarding rules by starting listeners in the guest, +# sending data from the host, and checking both event logs and data delivery. +# +# Usage: +# bats --formatter tap hack/bats/tests/port-forwarding.bats +# Creates and deletes its own "bats-portfwd" instance. + +load "../helpers/load" + +NAME="bats-portfwd" +INSTANCE="$NAME" +CONNECTION_TIMEOUT=1 + +# Source the port forwarding config (YAML rules + get_host_ipv4). +# The test specs are embedded as YAML comments (e.g. "# forward:", "# ignore:") +# which cannot be parsed by yq, so we use bash regex in parse_test_cases() instead. +# shellcheck source=port-forwarding-config.bash +source "$(cd "$(dirname "${BATS_TEST_FILENAME}")" && pwd)/port-forwarding-config.bash" + +join_host_port() { + local ip=$1 port=$2 + if [[ $ip == *:* ]]; then + echo "[$ip]:$port" + else + echo "$ip:$port" + fi +} + +parse_test_cases() { + local host_ipv4=$1 sock_dir=$2 ssh_local_port=$3 + while IFS= read -r line; do + # Skip non-test lines + [[ $line =~ ^(forward|ignore): ]] || continue + # Replace placeholders + line="${line//HOST_IPV4/$host_ipv4}" + line="${line//SOCK_DIR\//$sock_dir}" + line="${line//SSH_LOCAL_PORT/$ssh_local_port}" + + local mode="" guest_ip="" guest_port="" host_ip="" host_port="" host_socket="" + if [[ $line =~ ^(forward|ignore):[[:space:]]+([0-9.:]+)[[:space:]]+([0-9]+)([[:space:]]+-\>[[:space:]]+(.+))? ]]; then + mode="${BASH_REMATCH[1]}" + guest_ip="${BASH_REMATCH[2]}" + guest_port="${BASH_REMATCH[3]}" + local rest="${BASH_REMATCH[5]}" + if [[ -n $rest ]]; then + if [[ $rest == */* ]]; then + # Unix socket path + host_socket="$rest" + elif [[ $rest =~ ^([0-9.:]+)[[:space:]]+([0-9]+)$ ]]; then + host_ip="${BASH_REMATCH[1]}" + host_port="${BASH_REMATCH[2]}" + fi + fi + # Defaults for non-socket cases + if [[ -z $host_socket ]]; then + host_ip="${host_ip:-127.0.0.1}" + host_port="${host_port:-$guest_port}" + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$mode" "$guest_ip" "$guest_port" "${host_ip:--}" "${host_port:--}" "${host_socket:--}" + fi + done < <(port_forwards_block | sed -n 's/^[[:space:]]*#[[:space:]]*//p') +} + +normalize_fields() { + if [[ $host_ip == "-" ]]; then host_ip=""; fi + if [[ $host_port == "-" ]]; then host_port=""; fi + if [[ $host_socket == "-" ]]; then host_socket=""; fi +} + +build_log_msg() { + local mode=$1 guest_ip=$2 guest_port=$3 host_ip=$4 host_port=$5 host_socket=$6 + local remote + remote=$(join_host_port "$guest_ip" "$guest_port") + if [[ $mode == "forward" ]]; then + local local_addr + if [[ -n $host_socket ]]; then + local_addr="$host_socket" + else + local_addr=$(join_host_port "$host_ip" "$host_port") + fi + echo "Forwarding TCP from $remote to $local_addr" + else + echo "Not forwarding TCP $remote" + fi +} + +skip_reason() { + local mode=$1 guest_ip=$2 guest_port=$3 host_ip=$4 host_port=$5 host_socket=$6 + + if [[ $guest_ip == *:* ]] && ! ip -6 addr show lo >/dev/null 2>&1; then + echo "IPv6 not available on host" + return 0 + fi + if [[ $mode == "forward" && -z $host_socket && $host_port -lt 1024 && "$(uname -s)" != "Darwin" ]]; then + echo "Privileged port ($host_port) forwarding requires macOS" + return 0 + fi + if [[ $VM_TYPE == "wsl2" ]]; then + if [[ $mode == "ignore" && ($guest_ip == "0.0.0.0" || $guest_ip == "127.0.0.1") ]]; then + echo "Ignore rules for 0.0.0.0/127.0.0.1 not testable on $VM_TYPE" + return 0 + fi + # 192.168.5.15 is the SLIRP (user-mode networking) guest IP (see pkg/networks/const.go) + if [[ $guest_ip == "192.168.5.15" ]]; then + echo "SLIRP IP 192.168.5.15 not available on $VM_TYPE" + return 0 + fi + fi + case "$(uname -o 2>/dev/null)" in + Cygwin | Msys) + if [[ -n $host_socket ]]; then + echo "Unix socket forwarding not supported on Windows" + return 0 + fi + ;; + esac + return 1 +} + +assert_watch_event() { + local type=$1 guest_addr=$2 host_addr=${3:-} + local filter + if [[ -n $host_addr ]]; then + filter="select(.event.status.portForward.type == \"$type\" + and .event.status.portForward.guestAddr == \"$guest_addr\" + and .event.status.portForward.hostAddr == \"$host_addr\")" + else + filter="select(.event.status.portForward.type == \"$type\" + and .event.status.portForward.guestAddr == \"$guest_addr\")" + fi + jq -e "$filter" "$EVENTS_FILE" >/dev/null 2>&1 +} + +# check_test_cases runs assertions for test cases matching the given port range. +check_test_cases() { + local min_port=$1 max_port=$2 + local id=0 failures=0 + while IFS=$'\t' read -r mode guest_ip guest_port host_ip host_port host_socket; do + [[ -n $mode ]] || continue + normalize_fields + + if [[ $guest_port -lt $min_port || $guest_port -gt $max_port ]]; then + id=$((id + 1)) + continue + fi + + local log_msg + log_msg=$(build_log_msg "$mode" "$guest_ip" "$guest_port" "$host_ip" "$host_port" "$host_socket") + + local reason + if reason=$(skip_reason "$mode" "$guest_ip" "$guest_port" "$host_ip" "$host_port" "$host_socket"); then + echo "# skipped ($reason): $log_msg" >&3 + id=$((id + 1)) + continue + fi + + local remote + remote=$(join_host_port "$guest_ip" "$guest_port") + + local event_err="" data_err="" + if [[ -s $EVENTS_FILE ]]; then + if [[ $mode == "forward" ]]; then + if ! assert_watch_event "forwarding" "$remote" "${host_socket:-$(join_host_port "$host_ip" "$host_port")}"; then + event_err="Event missing from watch --json output" + fi + else + if ! assert_watch_event "not-forwarding" "$remote"; then + event_err="Event missing from watch --json output" + fi + fi + fi + if [[ $host_port != "$SSH_LOCAL_PORT" ]]; then + local got + got=$(tr -d '\r' <"$RESULTS_DIR/socat.${id}" 2>/dev/null | sed '/^$/d') + if [[ $mode == "forward" && $got != "$log_msg" ]]; then + data_err="Guest received: '${got}'" + fi + if [[ $mode == "ignore" && -n $got ]]; then + data_err="Guest received: '${got}' (instead of nothing)" + fi + fi + + if [[ -n $data_err ]]; then + local full_err="" + [[ -z $event_err ]] || full_err=$'\n'" $event_err" + full_err="${full_err}"$'\n'" $data_err" + echo "# FAIL: $log_msg$full_err" >&3 + failures=$((failures + 1)) + elif [[ -n $event_err ]]; then + echo "# warning: $log_msg - $event_err" >&3 + else + echo "# ok: $log_msg" >&3 + fi + id=$((id + 1)) + done <"$BATS_FILE_TMPDIR/test_cases.tsv" + + [[ $failures -eq 0 ]] +} + +local_setup_file() { + HOST_IPV4=$(get_host_ipv4) + export HOST_IPV4 + + EVENTS_FILE="$BATS_FILE_TMPDIR/events.jsonl" + export EVENTS_FILE + RESULTS_DIR="$BATS_FILE_TMPDIR/results" + mkdir -p "$RESULTS_DIR" + export RESULTS_DIR + + local inst_dir + inst_dir=$(limactl list "$NAME" --yq .dir) + SOCK_DIR="${inst_dir}/sock/" + export SOCK_DIR + + SSH_LOCAL_PORT=$(limactl ls --json "$NAME" | jq -r '.sshLocalPort') + export SSH_LOCAL_PORT + + VM_TYPE=$(limactl ls --json "$NAME" | jq -r '.vmType') + export VM_TYPE + + limactl shell "$NAME" sudo apt-get install -y socat >/dev/null 2>&1 + + limactl shell "$NAME" bash -c 'sudo pkill -x socat 2>/dev/null || true; rm -f ~/socat.*' + sleep 5 + + local test_cases + test_cases=$(parse_test_cases "$HOST_IPV4" "$SOCK_DIR" "$SSH_LOCAL_PORT") + echo "$test_cases" >"$BATS_FILE_TMPDIR/test_cases.tsv" + + timeout 120 limactl watch --json "$NAME" >"$EVENTS_FILE" 2>/dev/null 3>&- 4>&- & + WATCH_PID=$! + export WATCH_PID + sleep 1 + + local listener_script="$BATS_FILE_TMPDIR/start-listeners.sh" + { + echo '#!/bin/bash' + echo 'cd $HOME' + echo 'rm -f socat.*' + local id=0 + while IFS=$'\t' read -r mode guest_ip guest_port host_ip host_port host_socket; do + [[ -n $mode ]] || continue + normalize_fields + local proto="TCP" + if [[ $guest_ip == *:* ]]; then proto="TCP6"; fi + local sudo="" + if [[ $guest_port -lt 1024 ]]; then sudo="sudo "; fi + echo "${sudo}socat -u ${proto}-LISTEN:${guest_port},bind=${guest_ip},reuseaddr OPEN:\$HOME/socat.${id},creat /dev/null 2>&1 &" + id=$((id + 1)) + done <"$BATS_FILE_TMPDIR/test_cases.tsv" + } >"$listener_script" + + local listener_script_host="$listener_script" + if command -v cygpath >/dev/null 2>&1; then + listener_script_host="$(cygpath -w "$listener_script")" + fi + limactl cp "$listener_script_host" "${NAME}:/tmp/start-listeners.sh" + limactl shell "$NAME" bash /tmp/start-listeners.sh /dev/null && continue + expected_fwd=$((expected_fwd + 1)) + done <"$BATS_FILE_TMPDIR/test_cases.tsv" + + # Wait for all forwarding events to appear (up to 15s) + local wait_deadline=$((SECONDS + 15)) + while [[ $SECONDS -lt $wait_deadline ]]; do + local got_fwd=0 + if [[ -s $EVENTS_FILE ]]; then + got_fwd=$(jq -s '[.[] | select(.event.status.portForward.type == "forwarding")] | length' "$EVENTS_FILE" 2>/dev/null || echo 0) + fi + if [[ $got_fwd -ge $expected_fwd ]]; then + break + fi + sleep 1 + done + # Pause for host-side listeners to finish starting after events are emitted. + # The hostagent emits the forwarding event before spawning the listener goroutine, + # so we need to wait for the actual listeners to be ready. + sleep 5 + + id=0 + while IFS=$'\t' read -r mode guest_ip guest_port host_ip host_port host_socket; do + [[ -n $mode ]] || continue + normalize_fields + + local msg + msg=$(build_log_msg "$mode" "$guest_ip" "$guest_port" "$host_ip" "$host_port" "$host_socket") + + if [[ $host_port == "$SSH_LOCAL_PORT" ]]; then + id=$((id + 1)) + continue + fi + + if skip_reason "$mode" "$guest_ip" "$guest_port" "$host_ip" "$host_port" "$host_socket" >/dev/null; then + id=$((id + 1)) + continue + fi + + local socat_cmd + if [[ -n $host_socket ]]; then + socat_cmd="socat -u STDIN UNIX-CONNECT:${host_socket}" + elif [[ $host_ip == *:* ]]; then + socat_cmd="socat -u STDIN TCP6:[${host_ip}]:${host_port},connect-timeout=${CONNECTION_TIMEOUT}" + else + socat_cmd="socat -u STDIN TCP:${host_ip}:${host_port},connect-timeout=${CONNECTION_TIMEOUT}" + fi + echo "$msg" | $socat_cmd 2>/dev/null || true + + id=$((id + 1)) + done <"$BATS_FILE_TMPDIR/test_cases.tsv" + + sleep 3 + kill "$WATCH_PID" 2>/dev/null || true + wait "$WATCH_PID" 2>/dev/null || true + + local num_cases + num_cases=$(wc -l <"$BATS_FILE_TMPDIR/test_cases.tsv") + local fetch_script='cd $HOME' + for ((i = 0; i < num_cases; i++)); do + fetch_script="${fetch_script}; echo '===${i}==='; cat socat.${i} 2>/dev/null || true" + done + local all_output + all_output=$(limactl shell --workdir / "$NAME" bash -c "$fetch_script" 2>/dev/null) || true + local current_id="" + while IFS= read -r line; do + if [[ $line =~ ^===([0-9]+)=== ]]; then + current_id="${BASH_REMATCH[1]}" + : >"$RESULTS_DIR/socat.${current_id}" + elif [[ -n $current_id ]]; then + echo "$line" >>"$RESULTS_DIR/socat.${current_id}" + fi + done <<<"$all_output" +} + +local_teardown_file() { + limactl shell "$NAME" bash -c 'sudo pkill -x socat 2>/dev/null || true' 2>/dev/null || true +} + +@test "ignore for specific guest IP" { + check_test_cases 3000 3009 +} + +@test "ignore with guestIPMustBeZero false" { + check_test_cases 3010 3019 +} + +@test "forward from any interface" { + check_test_cases 3000 3029 +} + +@test "forward to host external IP" { + check_test_cases 3030 3039 +} + +@test "forward privileged ports" { + check_test_cases 300 304 +} + +@test "forward privileged ports guestIPMustBeZero false" { + check_test_cases 305 309 +} + +@test "forward to hostIP 0.0.0.0" { + check_test_cases 310 314 +} + +@test "forward to hostIP 0.0.0.0 guestIPMustBeZero false" { + check_test_cases 315 319 +} + +@test "forward from specific guest IP" { + check_test_cases 4000 4009 +} + +@test "forward IPv6 loopback" { + check_test_cases 4010 4019 +} + +@test "forward from IPv6 any" { + check_test_cases 4020 4029 +} + +@test "forward from IPv4 any guestIPMustBeZero false" { + check_test_cases 4030 4039 +} + +@test "guestIPMustBeZero true with ignore fallback" { + check_test_cases 4040 4049 +} + +@test "socket forwarding" { + check_test_cases 5000 5003 +} + +@test "no unexpected or failed watch events" { + [[ -s $EVENTS_FILE ]] || skip "no events collected" + + local -A expected_events + while IFS=$'\t' read -r mode guest_ip guest_port host_ip host_port host_socket; do + [[ -n $mode ]] || continue + normalize_fields + local r + r=$(join_host_port "$guest_ip" "$guest_port") + if [[ $mode == "forward" ]]; then + local la + if [[ -n $host_socket ]]; then la="$host_socket"; else la=$(join_host_port "$host_ip" "$host_port"); fi + expected_events["forwarding:${r}:${la}"]=1 + else + expected_events["not-forwarding:${r}:"]=1 + fi + done <"$BATS_FILE_TMPDIR/test_cases.tsv" + expected_events["forwarding:127.0.0.1:22:127.0.0.1:${SSH_LOCAL_PORT}"]=1 + + local failures=0 + while IFS= read -r line; do + local type guest_addr host_addr + type=$(echo "$line" | jq -r '.event.status.portForward.type // empty') + [[ -n $type ]] || continue + [[ $type == "forwarding" || $type == "not-forwarding" ]] || continue + guest_addr=$(echo "$line" | jq -r '.event.status.portForward.guestAddr // empty') + host_addr=$(echo "$line" | jq -r '.event.status.portForward.hostAddr // empty') + local key="${type}:${guest_addr}:${host_addr}" + if [[ -z ${expected_events[$key]:-} ]]; then + echo "# Unexpected: $type $guest_addr -> $host_addr" >&3 + fi + done <"$EVENTS_FILE" + + # Check for failed forward events + while IFS= read -r line; do + local ftype ferror fhost_addr + ftype=$(echo "$line" | jq -r '.event.status.portForward.type // empty') + [[ $ftype == "failed" ]] || continue + ferror=$(echo "$line" | jq -r '.event.status.portForward.error // empty') + fhost_addr=$(echo "$line" | jq -r '.event.status.portForward.hostAddr // empty') + echo "# Failed: $fhost_addr ($ferror)" >&3 + failures=$((failures + 1)) + done <"$EVENTS_FILE" + + [[ $failures -eq 0 ]] +} diff --git a/hack/test-port-forwarding.pl b/hack/test-port-forwarding.pl deleted file mode 100755 index 7f0d275973f..00000000000 --- a/hack/test-port-forwarding.pl +++ /dev/null @@ -1,505 +0,0 @@ -#!/usr/bin/env perl - -# This script tests the port forwarding settings of lima. It has to be run -# twice: once to update the instance yaml file with the port forwarding -# rules (before the instance is started). And once when the instance is -# running to perform the tests: -# -# ./hack/test-port-forwarding.pl templates/default.yaml -# limactl --tty=false start templates/default.yaml -# git restore templates/default.yaml -# ./hack/test-port-forwarding.pl default [nc|socat [nc|socat]] [timeout] -# -# TODO: support for ipv6 host addresses - -use strict; -use warnings; - -use Config qw(%Config); -use File::Spec::Functions qw(catfile); -use IO::Handle qw(); -use JSON::PP; -use Socket qw(inet_ntoa); -use Sys::Hostname qw(hostname); - -my $connectionTimeout = 1; # seconds - -my $instance = shift; -my $listener; -my $writer; -while (my $arg = shift) { - if ($arg eq "nc" || $arg eq "socat") { - $listener = $arg unless defined $listener; - $writer = $arg if defined $listener && !defined $writer; - } elsif ($arg =~ /^\d+$/) { - $connectionTimeout = $arg; - } else { - die "Usage: $0 [instance|yaml-file] [nc|socat [nc|socat]] [timeout]\n"; - } -} -$listener ||= "nc"; -$writer ||= $listener; - -my $addr = scalar gethostbyname(hostname()); -# If hostname address cannot be determines, use localhost to trigger fallback to system_profiler lookup -my $ipv4 = length $addr ? inet_ntoa($addr) : "127.0.0.1"; -my $ipv6 = ""; # todo - -# macOS GitHub runners seem to use "localhost" as the hostname -if ($ipv4 eq "127.0.0.1" && $Config{osname} eq "darwin") { - $ipv4 = qx(system_profiler SPNetworkDataType -json | jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first'); - chomp $ipv4; -} - -my $instDir = qx(limactl list "$instance" --yq .dir); -chomp $instDir; -# platform independent way to add trailing path separator -my $sockDir = catfile($instDir, "sock", ""); - -# If $instance is a filename, add our portForwards to it to enable testing -if (-f $instance) { - open(my $fh, "+< $instance") or die "Can't open $instance for read/write: $!"; - my @yaml; - while (<$fh>) { - # Remove existing "portForwards:" section from the config file - my $seq = /^portForwards:/ ... /^[a-z]/; - next if $seq && $seq !~ /E0$/; - push @yaml, $_; - } - seek($fh, 0, 0); - truncate($fh, 0); - print $fh $_ for @yaml; - while () { - s/ipv4/$ipv4/g; - s/ipv6/$ipv6/g; - print $fh $_; - } - exit; -} - -# Check if netcat is available before running tests -my $nc_path = `command -v nc 2>/dev/null`; -chomp $nc_path; -unless ($nc_path) { - die "Error: 'nc' (netcat) is not installed on the host system.\n" . - "Please install netcat to run this test script:\n" . - " - On macOS: brew install netcat\n" . - " - On Ubuntu/Debian: sudo apt-get install netcat\n" . - " - On RHEL/CentOS: sudo yum install nmap-ncat\n"; -} - -# Otherwise $instance must be the name of an already running instance that has been -# configured with our portForwards settings. - -my $instanceType = qx(limactl ls --json "$instance" | jq -r '.vmType' | sed s/x/x/); -chomp $instanceType; - -# Get sshLocalPort for lima instance -my $sshLocalPort; -open(my $ls, "limactl ls --json |") or die; -while (<$ls>) { - next unless /"name":"$instance"/; - ($sshLocalPort) = /"sshLocalPort":(\d+)/ or die; - last; -} -die "Cannot determine sshLocalPort" unless $sshLocalPort; - -# Extract forwarding tests from the "portForwards" section -my @test; -while () { - chomp; - s/^\s+#\s*//; - next unless /^(forward|ignore)/; - if (/ipv6/ && !$ipv6) { - printf "🚧 Not yet: # $_\n"; - next; - } - s/sshLocalPort/$sshLocalPort/g; - s/ipv4/$ipv4/g; - s/ipv6/$ipv6/g; - s/sockDir\//$sockDir/g; - # forward: 127.0.0.1 899 β†’ 127.0.0.1 799 - # ignore: 127.0.0.2 8888 - /^(forward|ignore):\s+([0-9.:]+)\s+(\d+)(?:\s+β†’)?(?:\s+(?:([0-9.:]+)(?:\s+(\d+))|(\S+))?)?/; - die "Cannot parse test '$_'" unless $1; - my %test; @test{qw(mode guest_ip guest_port host_ip host_port host_socket)} = ($1, $2, $3, $4, $5, $6); - - $test{host_ip} ||= "127.0.0.1"; - $test{host_port} ||= $test{guest_port}; - $test{host_socket} ||= ""; - if ($test{mode} eq "forward" && $test{host_socket} eq "" && $test{host_port} < 1024 && $Config{osname} ne "darwin") { - printf "🚧 Not supported on $Config{osname}: # $_\n"; - next; - } - if ($test{mode} eq "ignore" && ($test{guest_ip} eq "0.0.0.0" || $test{guest_ip} eq "127.0.0.1") && "$instanceType" eq "wsl2") { - printf "🚧 Not supported for $instanceType machines: # $_\n"; - next; - } - if ($test{guest_ip} eq "192.168.5.15" && "$instanceType" eq "wsl2") { - printf "🚧 Not supported for $instanceType machines: # $_\n"; - next; - } - if ($test{host_socket} ne "" && $Config{osname} eq "cygwin") { - printf "🚧 Not supported on $Config{osname}: # $_\n"; - next; - } - - my $remote = JoinHostPort($test{guest_ip},$test{guest_port}); - my $local = $test{host_socket} eq "" ? JoinHostPort($test{host_ip},$test{host_port}) : $test{host_socket}; - if ($test{mode} eq "ignore") { - $test{log_msg} = "Not forwarding TCP $remote"; - } - else { - $test{log_msg} = "Forwarding TCP from $remote to $local"; - } - push @test, \%test; -} - -open(my $lima, "| limactl shell --workdir / $instance") - or die "Can't run lima shell on $instance: $!"; -$lima->autoflush; - -print $lima <<'EOF'; -set -e -cd $HOME -sudo pkill -x nc || true -sudo pkill -x socat || true -rm -f nc.* socat.* -EOF - -# Give the hostagent some time to remove any port forwards from a previous (crashed?) test run -sleep 5; - -# Record current log size, so we can skip prior output -$ENV{HOME_HOST} ||= "$ENV{HOME}"; -$ENV{LIMA_HOME} ||= "$ENV{HOME_HOST}/.lima"; -my $ha_stdout_log = "$ENV{LIMA_HOME}/$instance/ha.stdout.log"; -my $ha_stderr_log = "$ENV{LIMA_HOME}/$instance/ha.stderr.log"; -my $ha_stdout_log_size = -s $ha_stdout_log or die; -my $ha_stderr_log_size = -s $ha_stderr_log or die; - -# Setup a netcat listener on the guest for each test -foreach my $id (0..@test-1) { - my $test = $test[$id]; - my $cmd; - if ($listener eq "nc") { - $cmd = "nc -l $test->{guest_ip} $test->{guest_port}"; - if ($instance =~ /^alpine/) { - $cmd = "nc -l -s $test->{guest_ip} -p $test->{guest_port}"; - } - } elsif ($listener eq "socat") { - my $proto = $test->{guest_ip} =~ /:/ ? "TCP6" : "TCP"; - $cmd = "socat -u $proto-LISTEN:$test->{guest_port},bind=$test->{guest_ip} STDOUT"; - } - - my $sudo = $test->{guest_port} < 1024 ? "sudo " : ""; - print $lima "${sudo}${cmd} >$listener.${id} 2>/dev/null &\n"; -} - -# Make sure the guest- and hostagents had enough time to set up the forwards -sleep 5; - -# Try to reach each listener from the host -foreach my $test (@test) { - next if $test->{host_port} == $sshLocalPort; - my $cmd; - if ($writer eq "nc") { - if ($Config{osname} eq "darwin") { - # macOS nc doesn't support -w for connection timeout, so use -G instead - $cmd = $test->{host_socket} eq "" ? "nc -G $connectionTimeout $test->{host_ip} $test->{host_port}" : "nc -G $connectionTimeout -U $test->{host_socket}"; - } else { - $cmd = $test->{host_socket} eq "" ? "nc -w $connectionTimeout $test->{host_ip} $test->{host_port}" : "nc -w $connectionTimeout -U $test->{host_socket}"; - } - } elsif ($writer eq "socat") { - my $tcp_dest = $test->{host_ip} =~ /:/ ? "TCP6:[$test->{host_ip}]:$test->{host_port}" : "TCP:$test->{host_ip}:$test->{host_port}"; - $cmd = $test->{host_socket} eq "" ? "socat -u STDIN $tcp_dest,connect-timeout=$connectionTimeout" : "socat -u STDIN UNIX-CONNECT:$test->{host_socket}"; - } - print "Running: $cmd\n"; - open(my $netcat, "| $cmd") or die "Can't run '$cmd': $!"; - print $netcat "$test->{log_msg}\n"; - # Don't check for errors on close; macOS nc seems to return non-zero exit code even on success - close($netcat); -} - -# Extract forwarding log messages from hostagent JSON event log -my $json_parser = JSON::PP->new->utf8->relaxed; - -open(my $log, "< $ha_stdout_log") or die "Can't read $ha_stdout_log: $!"; -seek($log, $ha_stdout_log_size, 0) or die "Can't seek $ha_stdout_log to $ha_stdout_log_size: $!"; -my %seen; -my %failed_to_listen_tcp; - -while (<$log>) { - chomp; - next unless /^\s*\{/; # Skip non-JSON lines - - my $event = eval { $json_parser->decode($_) }; - next unless $event; - - my $pf = $event->{status}{portForward}; - next unless $pf && $pf->{type}; - - my $type = $pf->{type}; - my $protocol = uc($pf->{protocol} || "tcp"); - my $guest_addr = $pf->{guestAddr} || ""; - my $host_addr = $pf->{hostAddr} || ""; - my $error = $pf->{error} || ""; - - if ($type eq "forwarding") { - my $msg = "Forwarding $protocol from $guest_addr to $host_addr"; - $seen{$msg}++; - } elsif ($type eq "not-forwarding") { - my $msg = "Not forwarding $protocol $guest_addr"; - $seen{$msg}++; - } elsif ($type eq "failed" && $error =~ /listen tcp/) { - # Extract the address from the error message - if ($error =~ /listen tcp (.*?:\d+):/) { - my $addr = $1; - $failed_to_listen_tcp{$addr} = "failed to listen tcp: $error"; - } - } -} -close $log or die; - -# Also check stderr log for failed_to_listen_tcp messages (these may not be in JSON events) -open(my $stderr_log, "< $ha_stderr_log") or die "Can't read $ha_stderr_log: $!"; -seek($stderr_log, $ha_stderr_log_size, 0) or die "Can't seek $ha_stderr_log to $ha_stderr_log_size: $!"; -while (<$stderr_log>) { - $failed_to_listen_tcp{$2}=$1 if /(failed to listen tcp: listen tcp (.*?:\d+):[^"]+)/; -} -close $stderr_log or die; - -my $rc = 0; -my %expected; -foreach my $id (0..@test-1) { - my $test = $test[$id]; - my $err = ""; - $expected{$test->{log_msg}}++; - unless ($seen{$test->{log_msg}}) { - $err .= "\n Message missing from ha.stdout.log (JSON events)"; - } - my $log = qx(limactl shell --workdir / $instance sh -c "cd; cat $listener.$id"); - chomp $log; - if ($test->{mode} eq "forward" && $test->{log_msg} ne $log) { - $err .= "\n Guest received: '$log'"; - } - if ($test->{mode} eq "ignore" && $log) { - $err .= "\n Guest received: '$log' (instead of nothing)"; - } - printf "%s %s%s\n", ($err ? "❌" : "βœ…"), $test->{log_msg}, $err; - $rc = 1 if $err; -} - -foreach (keys %seen) { - next if $expected{$_}; - # Should this be an error? Really should only happen if something else failed as well. - print "πŸ˜• Unexpected log message: $_\n"; -} - -if (%failed_to_listen_tcp) { - foreach (keys %failed_to_listen_tcp) { - print "⚠️ $failed_to_listen_tcp{$_}\n"; - } - my @tcp_list = keys %failed_to_listen_tcp; - if ($Config{osname} eq "darwin") { - my @lsof_args = map { "-iTCP\@$_" } @tcp_list; - print `lsof -P @lsof_args`; - } elsif ($Config{osname} eq "linux") { - my @lss_args = map { "src = $_" } @tcp_list; - my $ss_expression = join(" or ", @lss_args); - print `sudo ss -lnpt "$ss_expression"`; - } elsif ($Config{osname} eq "cygwin") { - my @awk_args = map { "-e'/$_/'" } @tcp_list; - print `netstat -aon | awk -e'/^ +Proto/' @awk_args`; - } -} - -# Cleanup remaining netcat instances (and port forwards) -print $lima "sudo pkill -x $listener"; - -exit $rc; - -sub JoinHostPort { - my($host,$port) = @_; - $host = "[$host]" if $host =~ /:/; - return "$host:$port"; -} - -# This YAML section includes port forwarding `rules` for the guest- and hostagents, -# with interleaved `tests` (in comments) that are executed by this script. The strings -# "ipv4" and "ipv6" will be replaced by the actual host ipv4 and ipv6 addresses. -__DATA__ -portForwards: -# We can't test that port 22 will be blocked because the guestagent has -# been ignoring it since startup, so the log message is in the part of -# the log we skipped. -# skip: 127.0.0.1 22 β†’ 127.0.0.1 2222 -# ignore: 127.0.0.1 sshLocalPort - -- guestIP: 127.0.0.2 - guestPortRange: [3000, 3009] - hostPortRange: [2000, 2009] - ignore: true - -- guestIP: 0.0.0.0 - guestIPMustBeZero: false - guestPortRange: [3010, 3019] - hostPortRange: [2010, 2019] - ignore: true - -- guestIP: 0.0.0.0 - guestIPMustBeZero: false - guestPortRange: [3000, 3029] - hostPortRange: [2000, 2029] - -# The following rule is completely shadowed by the previous one and has no effect -- guestIP: 0.0.0.0 - guestIPMustBeZero: false - guestPortRange: [3020, 3029] - hostPortRange: [2020, 2029] - ignore: true - - # ignore: 127.0.0.2 3000 - # forward: 127.0.0.3 3001 β†’ 127.0.0.1 2001 - - # Blocking 127.0.0.2 cannot block forwarding from 0.0.0.0 - # forward: 0.0.0.0 3002 β†’ 127.0.0.1 2002 - - # Blocking 0.0.0.0 will block forwarding from any interface because guestIPMustBeZero is false - # ignore: 0.0.0.0 3010 - # ignore: 127.0.0.1 3011 - - # Forwarding from 0.0.0.0 works for any interface (including IPv6) - # The "ignore" rule above has no effect because the previous rule already matched. - # forward: 127.0.0.2 3020 β†’ 127.0.0.1 2020 - # forward: 127.0.0.1 3021 β†’ 127.0.0.1 2021 - # forward: 0.0.0.0 3022 β†’ 127.0.0.1 2022 - # forward: :: 3023 β†’ 127.0.0.1 2023 - # forward: ::1 3024 β†’ 127.0.0.1 2024 - -- guestPortRange: [3030, 3039] - hostPortRange: [2030, 2039] - hostIP: ipv4 - - # forward: 127.0.0.1 3030 β†’ ipv4 2030 - # forward: 0.0.0.0 3031 β†’ ipv4 2031 - # forward: :: 3032 β†’ ipv4 2032 - # forward: ::1 3033 β†’ ipv4 2033 - -- guestPortRange: [300, 304] - - # forward: 127.0.0.1 300 β†’ 127.0.0.1 300 - # forward: 0.0.0.0 301 β†’ 127.0.0.1 301 - # forward: :: 302 β†’ 127.0.0.1 302 - # forward: ::1 303 β†’ 127.0.0.1 303 - # ignore: 192.168.5.15 304 β†’ 127.0.0.1 304 - -- guestPortRange: [305, 309] - guestIPMustBeZero: false - - # forward: 127.0.0.1 325 β†’ 127.0.0.1 325 - # forward: 0.0.0.0 326 β†’ 127.0.0.1 326 - # forward: :: 327 β†’ 127.0.0.1 327 - # forward: ::1 328 β†’ 127.0.0.1 328 - # ignore: 192.168.5.15 329 β†’ 127.0.0.1 329 - -- guestPortRange: [310, 314] - hostIP: 0.0.0.0 - - # forward: 127.0.0.1 310 β†’ 0.0.0.0 310 - # forward: 0.0.0.0 311 β†’ 0.0.0.0 311 - # forward: :: 312 β†’ 0.0.0.0 312 - # forward: ::1 313 β†’ 0.0.0.0 313 - # ignore: 192.168.5.15 314 β†’ 0.0.0.0 314 - -- guestPortRange: [315, 319] - guestIPMustBeZero: false - hostIP: 0.0.0.0 - - # forward: 127.0.0.1 315 β†’ 0.0.0.0 315 - # forward: 0.0.0.0 316 β†’ 0.0.0.0 316 - # forward: :: 317 β†’ 0.0.0.0 317 - # forward: ::1 318 β†’ 0.0.0.0 318 - # ignore: 192.168.5.15 319 β†’ 0.0.0.0 319 - - # Things we can't test: - # - Accessing a forward from a different interface (e.g. connect to ipv4 to connect to 0.0.0.0) - # - failed forward to privileged port - - -- guestIP: "192.168.5.15" - guestPortRange: [4000, 4009] - hostIP: "ipv4" - - # forward: 192.168.5.15 4000 β†’ ipv4 4000 - -- guestIP: "::1" - guestPortRange: [4010, 4019] - hostIP: "::" - - # forward: ::1 4010 β†’ :: 4010 - -- guestIP: "::" - guestPortRange: [4020, 4029] - hostIP: "ipv4" - - # forward: 127.0.0.1 4020 β†’ ipv4 4020 - # forward: 127.0.0.2 4021 β†’ ipv4 4021 - # forward: 192.168.5.15 4022 β†’ ipv4 4022 - # forward: 0.0.0.0 4023 β†’ ipv4 4023 - # forward: :: 4024 β†’ ipv4 4024 - # forward: ::1 4025 β†’ ipv4 4025 - -- guestIP: "0.0.0.0" - guestIPMustBeZero: false - guestPortRange: [4030, 4039] - hostIP: "ipv4" - - # forward: 127.0.0.1 4030 β†’ ipv4 4030 - # forward: 127.0.0.2 4031 β†’ ipv4 4031 - # forward: 192.168.5.15 4032 β†’ ipv4 4032 - # forward: 0.0.0.0 4033 β†’ ipv4 4033 - # forward: :: 4034 β†’ ipv4 4034 - # forward: ::1 4035 β†’ ipv4 4035 - -- guestIPMustBeZero: true - guestPortRange: [4040, 4049] - -- guestIP: "0.0.0.0" - guestIPMustBeZero: false - guestPortRange: [4040, 4049] - ignore: true - - # forward: 0.0.0.0 4040 β†’ 127.0.0.1 4040 - # forward: :: 4041 β†’ 127.0.0.1 4041 - # ignore: 127.0.0.1 4043 β†’ 127.0.0.1 4043 - # ignore: 192.168.5.15 4044 β†’ 127.0.0.1 4044 - -# This rule exist to test `nerdctl run` binding to 0.0.0.0 by default, -# and making sure it gets forwarded to the external host IP. -# The actual test code is in test-example.sh in the "port-forwarding" block. -- guestIPMustBeZero: true - guestPort: 8888 - hostIP: 0.0.0.0 - -- guestPort: 5000 - hostSocket: port5000.sock - - # forward: 127.0.0.1 5000 β†’ sockDir/port5000.sock - -- guestPort: 5001 - hostSocket: port5001.sock - - # ignore: 192.168.5.15 5001 β†’ sockDir/port5001.sock - -- guestPort: 5002 - guestIPMustBeZero: false - hostSocket: port5002.sock - - # forward: 127.0.0.1 5002 β†’ sockDir/port5002.sock - -- guestPort: 5003 - guestIPMustBeZero: false - hostSocket: port5003.sock - - # ignore: 192.168.5.15 5003 β†’ sockDir/port5003.sock diff --git a/hack/test-templates.sh b/hack/test-templates.sh index 3275cba3c93..0032a7231c0 100755 --- a/hack/test-templates.sh +++ b/hack/test-templates.sh @@ -116,21 +116,8 @@ case "$(limactl tmpl yq "$FILE_HOST" '.networks[].lima')" in esac if [[ -n ${CHECKS["port-forwards"]} ]]; then - tmpconfig="$HOME_HOST/lima-config-tmp" - mkdir -p "${tmpconfig}" - defer "rm -rf \"$tmpconfig\"" - tmpfile="${tmpconfig}/${NAME}.yaml" - cp "$FILE" "${tmpfile}" - FILE="${tmpfile}" - FILE_HOST=$FILE - if [ "${OS_HOST}" = "Msys" ]; then - FILE_HOST="$(cygpath -w "$FILE")" - fi - - INFO "Setup port forwarding rules for testing in \"${FILE}\"" - "${scriptdir}/test-port-forwarding.pl" "${FILE}" - INFO "Validating \"$FILE_HOST\"" - limactl validate "$FILE_HOST" + INFO "Adding port 8888 forwarding rule" + LIMACTL_CREATE+=(--set '.portForwards += [{"guestIPMustBeZero": true, "guestPort": 8888, "hostIP": "0.0.0.0"}]') fi INFO "Make sure template embedding copies \"$FILE_HOST\" exactly" @@ -432,42 +419,24 @@ if [[ -n ${CHECKS["container-engine"]} ]]; then fi if [[ -n ${CHECKS["port-forwards"]} ]]; then - PORT_FORWARDING_CONNECTION_TIMEOUT=1 - INFO "Testing port forwarding rules using netcat and socat with connection timeout ${PORT_FORWARDING_CONNECTION_TIMEOUT}s" - set -x - if [[ ${NAME} == "alpine"* ]]; then - limactl shell "${NAME}" sudo apk add socat - fi - if [[ ${NAME} == "archlinux" ]]; then - limactl shell "${NAME}" sudo pacman -Syu --noconfirm openbsd-netcat socat - fi - if [[ ${NAME} == "debian" || ${NAME} == "default" || ${NAME} == "docker" || ${NAME} == "test-misc" ]]; then - limactl shell "${NAME}" sudo apt-get install -y netcat-openbsd socat - fi - if [[ ${NAME} == "fedora" || ${NAME} == "wsl2" ]]; then - limactl shell "${NAME}" sudo dnf install -y nc socat - fi - if [[ ${NAME} == "opensuse" ]]; then - limactl shell "${NAME}" sudo zypper in -y netcat-openbsd socat - fi - if limactl shell "${NAME}" command -v dnf; then - limactl shell "${NAME}" sudo dnf install -y nc socat - fi - if "${scriptdir}/test-port-forwarding.pl" "${NAME}" socat $PORT_FORWARDING_CONNECTION_TIMEOUT; then - INFO "Port forwarding rules work" - else - ERROR "Port forwarding rules do not work with socat" - diagnose "$NAME" - exit 1 - fi - if [[ -n ${CHECKS["container-engine"]} || ${NAME} == "alpine"* ]]; then - INFO "Testing that \"${CONTAINER_ENGINE} run\" binds to 0.0.0.0 and is forwarded to the host (non-default behavior, configured via test-port-forwarding.pl)" - if [ "$(uname)" = "Darwin" ]; then - # macOS runners seem to use `localhost` as the hostname, so the perl lookup just returns `127.0.0.1` - hostip=$(system_profiler SPNetworkDataType -json | jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first') + INFO "Testing that \"${CONTAINER_ENGINE} run\" binds to 0.0.0.0 and is forwarded to the host" + set -x + # Detect the host's external IPv4 address + hostip="" + if [[ $(uname -s) == "Darwin" ]]; then + hostip=$(system_profiler SPNetworkDataType -json 2>/dev/null | + jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first') + elif [[ $(uname -o 2>/dev/null) == "Msys" ]]; then + # shellcheck disable=SC2016 # $_ is PowerShell syntax, not bash + hostip=$(powershell.exe -NoProfile -Command \ + '[System.Net.Dns]::GetHostAddresses((hostname)) | Where-Object {$_.AddressFamily -eq "InterNetwork" -and $_.IPAddressToString -ne "127.0.0.1"} | Select-Object -First 1 -ExpandProperty IPAddressToString' \ + 2>/dev/null | tr -d '\r') else - hostip=$(perl -MSocket -MSys::Hostname -E 'say inet_ntoa(scalar gethostbyname(hostname()))') + hostip=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v ':' | grep -v '^127\.' | head -1) + fi + if [[ -z $hostip || $hostip == "null" ]]; then + hostip="127.0.0.1" fi if [ -n "${hostip}" ]; then sudo=""