Add tests for guest vsock device namespace assignment, including ns
destruction and the device moving between namespaces.

The tests drive the device through ynl's cli.py and create guest
namespaces with unshare, so they need python3, PyYAML and unshare. A
missing one skips only these tests, not the whole script.

Signed-off-by: Bobby Eshleman <[email protected]>
---
Changes in v3:
- Replace reset_on_ns_delete_ok with pins_ns_ok, to cover the netns
  pinning
- Skip only the ns_guest_ tests when PyYAML or the ynl CLI or spec is
  missing, and ship the ynl bits via TEST_FILES for installed trees
  (Sashiko)
- Also skip only the ns_guest_ tests when unshare or python3 is missing
- Make vm_ns_start() wait for the namespace and report failure (Sashiko)
- Poll for FIRST in old_conn_send_fails instead of sleeping, and report
  an over-long assign as a skip (Sashiko)
- Kill the guest-side socat by name, $! names a local subshell (Sashiko)
- Wait for the host listener to exit before the next test binds its
  port (Sashiko)

Changes in v2:
- Drive the assign with ynl cli.py, drop the vsock_assign_g2h_netns
  ioctl helper and its patch
- Terminate the guest socat sender too, it sometimes outlived the test
  and kept holding on to a port the next test binds
---
 tools/testing/selftests/vsock/Makefile  |   5 +
 tools/testing/selftests/vsock/vmtest.sh | 502 +++++++++++++++++++++++++++++++-
 2 files changed, 501 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/vsock/Makefile 
b/tools/testing/selftests/vsock/Makefile
index c407c0afd938..2b066ba91bcd 100644
--- a/tools/testing/selftests/vsock/Makefile
+++ b/tools/testing/selftests/vsock/Makefile
@@ -13,5 +13,10 @@ $(VSOCK_TEST_DIR)/vsock_test: $(VSOCK_TEST_SRCS)
 TEST_PROGS += vmtest.sh
 TEST_GEN_FILES := vsock_test
 
+TEST_FILES := \
+       ../../../net/ynl/pyynl/cli.py \
+       ../../../net/ynl/pyynl/lib \
+       ../../../../Documentation/netlink/specs/vsock.yaml
+
 include ../lib.mk
 
diff --git a/tools/testing/selftests/vsock/vmtest.sh 
b/tools/testing/selftests/vsock/vmtest.sh
index 310dfc2a39ad..7693d5e0a1d0 100755
--- a/tools/testing/selftests/vsock/vmtest.sh
+++ b/tools/testing/selftests/vsock/vmtest.sh
@@ -17,6 +17,20 @@ readonly KERNEL_CHECKOUT=$(realpath 
"${SCRIPT_DIR}"/../../../../)
 source "${SCRIPT_DIR}"/../kselftest/ktap_helpers.sh
 
 readonly VSOCK_TEST="${SCRIPT_DIR}"/vsock_test
+
+# "make install" copies the ynl CLI, its lib package and the spec next to this
+# script (see TEST_FILES); in a source tree they stay where they are built.
+if [[ -r "${SCRIPT_DIR}"/cli.py ]]; then
+       readonly YNL_CLI="${SCRIPT_DIR}"/cli.py
+       readonly YNL_LIB="${SCRIPT_DIR}"/lib
+       readonly VSOCK_SPEC="${SCRIPT_DIR}"/vsock.yaml
+else
+       readonly YNL_DIR="${KERNEL_CHECKOUT}"/tools/net/ynl/pyynl
+       readonly YNL_CLI="${YNL_DIR}"/cli.py
+       readonly YNL_LIB="${YNL_DIR}"/lib
+       readonly SPEC_DIR="${KERNEL_CHECKOUT}"/Documentation/netlink/specs
+       readonly VSOCK_SPEC="${SPEC_DIR}"/vsock.yaml
+fi
 readonly TEST_GUEST_PORT=51000
 readonly TEST_HOST_PORT=50000
 readonly TEST_HOST_PORT_LISTENER=50001
@@ -73,6 +87,12 @@ readonly TEST_NAMES=(
        ns_delete_vm_ok
        ns_delete_host_ok
        ns_delete_both_ok
+       ns_guest_local_connect_to_host_fails
+       ns_guest_assign_g2h_netns_connect_to_host_ok
+       ns_guest_assign_g2h_netns_init_ns_connect_fails
+       ns_guest_assign_g2h_netns_host_connect_ok
+       ns_guest_assign_g2h_netns_pins_ns_ok
+       ns_guest_assign_g2h_netns_old_conn_send_fails
 )
 readonly TEST_DESCS=(
        # vm_server_host_client
@@ -149,12 +169,36 @@ readonly TEST_DESCS=(
 
        # ns_delete_both_ok
        "Check that deleting the VM and host's namespaces does not break the 
socket connection"
+
+       # ns_guest_local_connect_to_host_fails
+       "Check a guest process in a local ns cannot reach the host without the 
netns assign."
+
+       # ns_guest_assign_g2h_netns_connect_to_host_ok
+       "Check a guest process in a local ns reaches the host once the vsock 
device is assigned to it."
+
+       # ns_guest_assign_g2h_netns_init_ns_connect_fails
+       "Check the guest's initial ns loses vsock once the device is assigned 
to another ns."
+
+       # ns_guest_assign_g2h_netns_host_connect_ok
+       "Check the host reaches a guest listener in the ns the vsock device is 
assigned to."
+
+       # ns_guest_assign_g2h_netns_pins_ns_ok
+       "Check the assignment keeps the ns alive when deleted, and that 
reassigning releases it."
+
+       # ns_guest_assign_g2h_netns_old_conn_send_fails
+       "Check connections made before the assign stop sending once they lose 
the device."
 )
 
 readonly USE_SHARED_VM=(
        vm_server_host_client
        vm_client_host_server
        vm_loopback
+       ns_guest_local_connect_to_host_fails
+       ns_guest_assign_g2h_netns_connect_to_host_ok
+       ns_guest_assign_g2h_netns_init_ns_connect_fails
+       ns_guest_assign_g2h_netns_host_connect_ok
+       ns_guest_assign_g2h_netns_pins_ns_ok
+       ns_guest_assign_g2h_netns_old_conn_send_fails
 )
 readonly NS_MODES=("local" "global")
 
@@ -316,6 +360,34 @@ check_deps() {
        fi
 }
 
+check_ns_guest_deps() {
+       local tname=$1
+       local dep
+
+       if [[ ! "${tname}" =~ ^ns_guest_ ]]; then
+               return 0
+       fi
+
+       for dep in unshare python3; do
+               if [[ ! -x $(command -v "${dep}") ]]; then
+                       log_host "no ${dep} for test ${tname}"
+                       return 1
+               fi
+       done
+
+       if ! python3 -c "import yaml" &>/dev/null; then
+               log_host "no python3 yaml module for test ${tname}"
+               return 1
+       fi
+
+       if [[ ! -r "${YNL_CLI}" ]] || [[ ! -r "${VSOCK_SPEC}" ]]; then
+               log_host "no ynl cli or vsock spec for test ${tname}"
+               return 1
+       fi
+
+       return 0
+}
+
 check_netns() {
        local tname=$1
 
@@ -401,6 +473,19 @@ setup_home() {
        mkdir -p "$(dirname "${SSH_KEY_PATH}")"
        ssh-keygen -t ed25519 -f "${SSH_KEY_PATH}" -N "" -q
        cp "${VSOCK_TEST}" "${TEST_HOME}"/vsock_test
+
+       mkdir -p "${TEST_HOME}"/ynl
+       if [[ -r "${YNL_CLI}" ]]; then
+               cp "${YNL_CLI}" "${TEST_HOME}"/ynl/
+               cp -r "${YNL_LIB}" "${TEST_HOME}"/ynl/
+               cp "${VSOCK_SPEC}" "${TEST_HOME}"/ynl/
+       fi
+
+       # One of the tests runs the CLI as an unprivileged user, so the CLI
+       # has to be reachable by one.
+       chmod -R a+rX "${TEST_HOME}"/ynl
+
+       chmod 755 "${TEST_HOME}"
 }
 
 create_pidfile() {
@@ -528,6 +613,82 @@ vm_wait_for_ssh() {
        done
 }
 
+# Create a local mode namespace in the VM and echo the pid holding it open.
+vm_ns_start() {
+       local ns=$1
+       local initns
+       local nspid
+       local newns
+       local i
+
+       local mode=/proc/sys/net/vsock/child_ns_mode
+
+       if ! vm_ssh "${ns}" -- "echo local > ${mode}" &>/dev/null; then
+               return
+       fi
+
+       nspid=$(vm_ssh "${ns}" -- "unshare -n sleep infinity" \
+               '>/dev/null 2>&1 & echo $!')
+       if [[ -z "${nspid}" ]]; then
+               return
+       fi
+
+       initns=$(vm_ssh "${ns}" -- "readlink /proc/self/ns/net")
+       for ((i = 0; i < WAIT_PERIOD_MAX; i++)); do
+               newns=$(vm_ssh "${ns}" -- "readlink /proc/${nspid}/ns/net" \
+                       2>/dev/null)
+               if [[ -n "${newns}" ]] && [[ "${newns}" != "${initns}" ]]; then
+                       echo "${nspid}"
+                       return
+               fi
+               sleep 1
+       done
+
+       vm_ns_stop "${ns}" "${nspid}"
+}
+
+# Returns once the holder is gone, so that the namespace is unreferenced and
+# the kernel can start tearing it down.
+vm_ns_stop() {
+       local ns=$1
+       local nspid=$2
+
+       vm_ssh "${ns}" <<-EOF &>/dev/null
+               kill ${nspid}
+               for ((i = 0; i < ${WAIT_PERIOD_MAX}; i++)); do
+                       kill -0 ${nspid} 2>/dev/null || break
+                       sleep 1
+               done
+       EOF
+}
+
+# Runs in the guest's initial namespace when <nspid> is empty. The command must
+# not contain single quotes.
+vm_ns_exec() {
+       local ns=$1
+       local nspid=$2
+       local cmd=$3
+
+       if [[ -z "${nspid}" ]]; then
+               vm_ssh "${ns}" -- "${cmd}"
+               return
+       fi
+
+       vm_ssh "${ns}" -- nsenter -t "${nspid}" -n sh -c "'${cmd}'"
+}
+
+vm_ns_assign_g2h() {
+       local ns=$1
+       local nspid=$2
+
+       vm_ns_exec "${ns}" "${nspid}" "python3 /root/ynl/cli.py --no-schema \
+               --spec /root/ynl/vsock.yaml --do dev-netns-set"
+}
+
+vm_reset_g2h() {
+       vm_ns_assign_g2h "init_ns" "" &>/dev/null
+}
+
 # derived from selftests/net/net_helper.sh
 wait_for_listener()
 {
@@ -564,17 +725,33 @@ wait_for_listener()
        done
 }
 
-vm_wait_for_listener() {
+# Runs in the guest's initial namespace when <nspid> is empty.
+vm_ns_wait_for_listener() {
        local ns=$1
-       local port=$2
-       local protocol=$3
+       local nspid=$2
+       local port=$3
+       local protocol=$4
+       local nsenter=
+       local args
+
+       [[ -n "${nspid}" ]] && nsenter="nsenter -t ${nspid} -n"
+       args="${port} ${WAIT_PERIOD} ${WAIT_PERIOD_MAX} ${protocol}"
 
        vm_ssh "${ns}" <<EOF
 $(declare -f wait_for_listener)
-wait_for_listener ${port} ${WAIT_PERIOD} ${WAIT_PERIOD_MAX} ${protocol}
+export -f wait_for_listener
+${nsenter} bash -c "wait_for_listener ${args}"
 EOF
 }
 
+vm_wait_for_listener() {
+       local ns=$1
+       local port=$2
+       local protocol=$3
+
+       vm_ns_wait_for_listener "${ns}" "" "${port}" "${protocol}"
+}
+
 host_wait_for_listener() {
        local ns=$1
        local port=$2
@@ -1421,6 +1598,317 @@ test_ns_delete_both_ok() {
        check_ns_delete_doesnt_break_connection "both"
 }
 
+# Send a string from the guest to a host listener and leave what the host
+# received in <outfile>.
+guest_send_to_host() {
+       local ns=$1
+       local nspid=$2
+       local port=$3
+       local outfile=$4
+       local cmd="echo TEST | socat -u STDIN VSOCK-CONNECT:2:${port}"
+       local pid
+
+       socat -u VSOCK-LISTEN:"${port}" STDOUT > "${outfile}" 2>/dev/null &
+       pid=$!
+       host_wait_for_listener "${ns}" "${port}" "vsock"
+
+       vm_ns_exec "${ns}" "${nspid}" "${cmd}" 2>/dev/null
+
+       timeout "${WAIT_PERIOD}" \
+               bash -c 'while [[ ! -s '"${outfile}"' ]]; do sleep 1; done'
+
+       # Wait for the listener to actually go, or the next test binding the
+       # same port races it and silently reads nothing.
+       terminate_pids "${pid}"
+       wait "${pid}" 2>/dev/null
+}
+
+# Send a string from the host to a listener in the guest and leave what the
+# guest received in <outfile>.
+host_send_to_guest() {
+       local ns=$1
+       local nspid=$2
+       local port=$3
+       local outfile=$4
+       local cmd="socat -u VSOCK-LISTEN:${port} STDOUT"
+       local dst="VSOCK-CONNECT:${VSOCK_CID}:${port}"
+       local pid
+
+       vm_ns_exec "${ns}" "${nspid}" "${cmd}" > "${outfile}" 2>/dev/null &
+       pid=$!
+       vm_ns_wait_for_listener "${ns}" "${nspid}" "${port}" "vsock"
+
+       echo TEST | socat -u STDIN "${dst}" 2>/dev/null
+
+       timeout "${WAIT_PERIOD}" \
+               bash -c 'while [[ ! -s '"${outfile}"' ]]; do sleep 1; done'
+
+       vm_ns_exec "${ns}" "${nspid}" \
+               "pkill -f VSOCK-LISTEN:${port}" &>/dev/null
+       terminate_pids "${pid}"
+       wait "${pid}" 2>/dev/null
+}
+
+ns_old_conn_cleanup() {
+       local port=$1
+       local pid=$2
+       local sender=$3
+       local outfile=$4
+       local nspid=$5
+
+       vm_ns_exec "init_ns" "" "pkill -f VSOCK-CONNECT:2:${port}" &>/dev/null
+       terminate_pids "${pid}" "${sender}"
+       wait "${pid}" "${sender}" 2>/dev/null
+       rm -f "${outfile}"
+       vm_ns_stop "init_ns" "${nspid}"
+       vm_reset_g2h
+}
+
+test_ns_guest_assign_g2h_netns_old_conn_send_fails() {
+       local gap=$(( WAIT_PERIOD * 3 ))
+       local port=12346
+       local elapsed
+       local started
+       local outfile
+       local result
+       local sender
+       local nspid
+       local pid
+
+       nspid=$(vm_ns_start "init_ns")
+       if [[ -z "${nspid}" ]]; then
+               log_host "failed to create a namespace inside the guest"
+               return "${KSFT_FAIL}"
+       fi
+
+       outfile=$(mktemp)
+       socat -u VSOCK-LISTEN:"${port}" STDOUT > "${outfile}" 2>/dev/null &
+       pid=$!
+       host_wait_for_listener "init_ns" "${port}" "vsock"
+
+       # Send a message, wait, then send another. While waiting, assign the
+       # device to a namespace. Confirm the second message does not arrive.
+       vm_ssh "init_ns" -- \
+               "(echo FIRST; sleep ${gap}; echo SECOND) |" \
+               "socat -u STDIN VSOCK-CONNECT:2:${port}" &>/dev/null &
+       sender=$!
+
+       timeout "$(( WAIT_PERIOD * WAIT_PERIOD_MAX ))" \
+               bash -c 'while [[ ! -s '"${outfile}"' ]]; do sleep 1; done'
+
+       started=${SECONDS}
+       if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then
+               log_host "failed to assign the vsock device to the guest ns"
+               ns_old_conn_cleanup "${port}" "${pid}" "${sender}" \
+                       "${outfile}" "${nspid}"
+               return "${KSFT_FAIL}"
+       fi
+       elapsed=$(( SECONDS - started ))
+
+       sleep $(( gap + WAIT_PERIOD ))
+
+       result=$(cat "${outfile}")
+       ns_old_conn_cleanup "${port}" "${pid}" "${sender}" \
+               "${outfile}" "${nspid}"
+
+       if [[ "${result}" != *FIRST* ]]; then
+               log_host "no connection before the assign: [${result}]"
+               return "${KSFT_FAIL}"
+       fi
+
+       if [[ ${elapsed} -ge ${gap} ]]; then
+               log_host "assign took ${elapsed}s, longer than the ${gap}s gap"
+               return "${KSFT_SKIP}"
+       fi
+
+       if [[ "${result}" == *SECOND* ]]; then
+               log_host "old connection still delivered after the assign"
+               return "${KSFT_FAIL}"
+       fi
+
+       return "${KSFT_PASS}"
+}
+
+test_ns_guest_local_connect_to_host_fails() {
+       local port=12345
+       local outfile
+       local result
+       local nspid
+
+       nspid=$(vm_ns_start "init_ns")
+       if [[ -z "${nspid}" ]]; then
+               log_host "failed to create a namespace inside the guest"
+               return "${KSFT_FAIL}"
+       fi
+
+       outfile=$(mktemp)
+       guest_send_to_host "init_ns" "${nspid}" "${port}" "${outfile}"
+
+       vm_ns_stop "init_ns" "${nspid}"
+       vm_reset_g2h
+
+       result=$(cat "${outfile}")
+       rm -f "${outfile}"
+
+       if [[ "${result}" == TEST ]]; then
+               return "${KSFT_FAIL}"
+       fi
+
+       return "${KSFT_PASS}"
+}
+
+test_ns_guest_assign_g2h_netns_connect_to_host_ok() {
+       local port=12345
+       local outfile
+       local result
+       local nspid
+
+       nspid=$(vm_ns_start "init_ns")
+       if [[ -z "${nspid}" ]]; then
+               log_host "failed to create a namespace inside the guest"
+               return "${KSFT_FAIL}"
+       fi
+
+       if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then
+               log_host "failed to assign the vsock device to the guest ns"
+               vm_ns_stop "init_ns" "${nspid}"
+               vm_reset_g2h
+               return "${KSFT_FAIL}"
+       fi
+
+       outfile=$(mktemp)
+       guest_send_to_host "init_ns" "${nspid}" "${port}" "${outfile}"
+
+       vm_ns_stop "init_ns" "${nspid}"
+       vm_reset_g2h
+
+       result=$(cat "${outfile}")
+       rm -f "${outfile}"
+
+       if [[ "${result}" != TEST ]]; then
+               return "${KSFT_FAIL}"
+       fi
+
+       return "${KSFT_PASS}"
+}
+
+test_ns_guest_assign_g2h_netns_init_ns_connect_fails() {
+       local port=12345
+       local outfile
+       local result
+       local nspid
+
+       nspid=$(vm_ns_start "init_ns")
+       if [[ -z "${nspid}" ]]; then
+               log_host "failed to create a namespace inside the guest"
+               return "${KSFT_FAIL}"
+       fi
+
+       if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then
+               log_host "failed to assign the vsock device to the guest ns"
+               vm_ns_stop "init_ns" "${nspid}"
+               vm_reset_g2h
+               return "${KSFT_FAIL}"
+       fi
+
+       outfile=$(mktemp)
+       guest_send_to_host "init_ns" "" "${port}" "${outfile}"
+
+       vm_ns_stop "init_ns" "${nspid}"
+       vm_reset_g2h
+
+       result=$(cat "${outfile}")
+       rm -f "${outfile}"
+
+       if [[ "${result}" == TEST ]]; then
+               return "${KSFT_FAIL}"
+       fi
+
+       return "${KSFT_PASS}"
+}
+
+test_ns_guest_assign_g2h_netns_host_connect_ok() {
+       local port=12345
+       local outfile
+       local result
+       local nspid
+
+       nspid=$(vm_ns_start "init_ns")
+       if [[ -z "${nspid}" ]]; then
+               log_host "failed to create a namespace inside the guest"
+               return "${KSFT_FAIL}"
+       fi
+
+       if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then
+               log_host "failed to assign the vsock device to the guest ns"
+               vm_ns_stop "init_ns" "${nspid}"
+               vm_reset_g2h
+               return "${KSFT_FAIL}"
+       fi
+
+       outfile=$(mktemp)
+       host_send_to_guest "init_ns" "${nspid}" "${port}" "${outfile}"
+
+       vm_ns_stop "init_ns" "${nspid}"
+       vm_reset_g2h
+
+       result=$(cat "${outfile}")
+       rm -f "${outfile}"
+
+       if [[ "${result}" != TEST ]]; then
+               return "${KSFT_FAIL}"
+       fi
+
+       return "${KSFT_PASS}"
+}
+
+test_ns_guest_assign_g2h_netns_pins_ns_ok() {
+       local port=12345
+       local outfile
+       local result
+       local nspid
+
+       nspid=$(vm_ns_start "init_ns")
+       if [[ -z "${nspid}" ]]; then
+               log_host "failed to create a namespace inside the guest"
+               return "${KSFT_FAIL}"
+       fi
+
+       if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then
+               log_host "failed to assign the vsock device to the guest ns"
+               vm_ns_stop "init_ns" "${nspid}"
+               vm_reset_g2h
+               return "${KSFT_FAIL}"
+       fi
+
+       vm_ns_stop "init_ns" "${nspid}"
+       sleep "${WAIT_PERIOD}"
+
+       outfile=$(mktemp)
+       guest_send_to_host "init_ns" "" "${port}" "${outfile}"
+       result=$(cat "${outfile}")
+       if [[ "${result}" == TEST ]]; then
+               log_host "device returned to the init ns instead of pinning it"
+               rm -f "${outfile}"
+               vm_reset_g2h
+               return "${KSFT_FAIL}"
+       fi
+
+       # Assigning it back is what releases the deleted namespace.
+       vm_reset_g2h
+
+       guest_send_to_host "init_ns" "" "$(( port + 1 ))" "${outfile}"
+       result=$(cat "${outfile}")
+       rm -f "${outfile}"
+
+       if [[ "${result}" != TEST ]]; then
+               log_host "device did not come back on reassign: [${result}]"
+               return "${KSFT_FAIL}"
+       fi
+
+       return "${KSFT_PASS}"
+}
+
 shared_vm_test() {
        local tname
 
@@ -1453,7 +1941,8 @@ run_shared_vm_tests() {
                        continue
                fi
 
-               if ! check_netns "${arg}"; then
+               if ! check_netns "${arg}" ||
+                  ! check_ns_guest_deps "${arg}"; then
                        check_result "${KSFT_SKIP}" "${arg}"
                        continue
                fi
@@ -1517,7 +2006,8 @@ run_ns_tests() {
                        continue
                fi
 
-               if ! check_netns "${arg}"; then
+               if ! check_netns "${arg}" ||
+                  ! check_ns_guest_deps "${arg}"; then
                        check_result "${KSFT_SKIP}" "${arg}"
                        continue
                fi

-- 
2.53.0-Meta


Reply via email to