diff options
-rw-r--r-- | test/packetdrill/BUILD | 8 | ||||
-rw-r--r-- | test/packetdrill/Dockerfile | 9 | ||||
-rw-r--r-- | test/packetdrill/defs.bzl | 85 | ||||
-rw-r--r-- | test/packetdrill/fin_wait2_timeout.pkt | 23 | ||||
-rwxr-xr-x | test/packetdrill/packetdrill_setup.sh | 26 | ||||
-rwxr-xr-x | test/packetdrill/packetdrill_test.sh | 213 |
6 files changed, 364 insertions, 0 deletions
diff --git a/test/packetdrill/BUILD b/test/packetdrill/BUILD new file mode 100644 index 000000000..d113555b1 --- /dev/null +++ b/test/packetdrill/BUILD @@ -0,0 +1,8 @@ +load("defs.bzl", "packetdrill_test") + +package(licenses = ["notice"]) + +packetdrill_test( + name = "fin_wait2_timeout", + scripts = ["fin_wait2_timeout.pkt"], +) diff --git a/test/packetdrill/Dockerfile b/test/packetdrill/Dockerfile new file mode 100644 index 000000000..bd4451355 --- /dev/null +++ b/test/packetdrill/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:bionic + +RUN apt-get update +RUN apt-get install -y net-tools git iptables iputils-ping netcat tcpdump jq tar +RUN hash -r +RUN git clone --branch packetdrill-v2.0 \ + https://github.com/google/packetdrill.git +RUN cd packetdrill/gtests/net/packetdrill && ./configure && \ + apt-get install -y bison flex make && make diff --git a/test/packetdrill/defs.bzl b/test/packetdrill/defs.bzl new file mode 100644 index 000000000..582f97e0c --- /dev/null +++ b/test/packetdrill/defs.bzl @@ -0,0 +1,85 @@ +"""Defines a rule for packetdrill test targets.""" + +def _packetdrill_test_impl(ctx): + test_runner = ctx.executable._test_runner + runner = ctx.actions.declare_file("%s-runner" % ctx.label.name) + + script_paths = [] + for script in ctx.files.scripts: + script_paths.append(script.short_path) + runner_content = "\n".join([ + "#!/bin/bash", + # This test will run part in a distinct user namespace. This can cause + # permission problems, because all runfiles may not be owned by the + # current user, and no other users will be mapped in that namespace. + # Make sure that everything is readable here. + "find . -type f -exec chmod a+rx {} \\;", + "find . -type d -exec chmod a+rx {} \\;", + "%s %s --init_script %s -- %s\n" % ( + test_runner.short_path, + " ".join(ctx.attr.flags), + ctx.files._init_script[0].short_path, + " ".join(script_paths), + ), + ]) + ctx.actions.write(runner, runner_content, is_executable = True) + + transitive_files = depset() + if hasattr(ctx.attr._test_runner, "data_runfiles"): + transitive_files = depset(ctx.attr._test_runner.data_runfiles.files) + runfiles = ctx.runfiles( + files = [test_runner] + ctx.files._init_script + ctx.files.scripts, + transitive_files = transitive_files, + collect_default = True, + collect_data = True, + ) + return [DefaultInfo(executable = runner, runfiles = runfiles)] + +_packetdrill_test = rule( + attrs = { + "_test_runner": attr.label( + executable = True, + cfg = "host", + allow_files = True, + default = "packetdrill_test.sh", + ), + "_init_script": attr.label( + allow_single_file = True, + default = "packetdrill_setup.sh", + ), + "flags": attr.string_list( + mandatory = False, + default = [], + ), + "scripts": attr.label_list( + mandatory = True, + allow_files = True, + ), + }, + test = True, + implementation = _packetdrill_test_impl, +) + +_PACKETDRILL_TAGS = ["local", "manual"] + +def packetdrill_linux_test(name, **kwargs): + if "tags" not in kwargs: + kwargs["tags"] = _PACKETDRILL_TAGS + _packetdrill_test( + name = name + "_linux_test", + flags = ["--dut_platform", "linux"], + **kwargs + ) + +def packetdrill_netstack_test(name, **kwargs): + if "tags" not in kwargs: + kwargs["tags"] = _PACKETDRILL_TAGS + _packetdrill_test( + name = name + "_netstack_test", + flags = ["--dut_platform", "netstack"], + **kwargs + ) + +def packetdrill_test(**kwargs): + packetdrill_linux_test(**kwargs) + packetdrill_netstack_test(**kwargs) diff --git a/test/packetdrill/fin_wait2_timeout.pkt b/test/packetdrill/fin_wait2_timeout.pkt new file mode 100644 index 000000000..613f0bec9 --- /dev/null +++ b/test/packetdrill/fin_wait2_timeout.pkt @@ -0,0 +1,23 @@ +// Test that a socket in FIN_WAIT_2 eventually times out and a subsequent +// packet generates a RST. + +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0 bind(3, ..., ...) = 0 + ++0 listen(3, 1) = 0 + +// Establish a connection without timestamps. ++0 < S 0:0(0) win 32792 <mss 1460,sackOK,nop,nop,nop,wscale 7> ++0 > S. 0:0(0) ack 1 <...> ++0 < P. 1:1(0) ack 1 win 257 + ++0.100 accept(3, ..., ...) = 4 +// set FIN_WAIT2 timeout to 1 seconds. ++0.100 setsockopt(4, SOL_TCP, TCP_LINGER2, [1], 4) = 0 ++0 close(4) = 0 + ++0 > F. 1:1(0) ack 1 <...> ++0 < . 1:1(0) ack 2 win 257 + ++1.1 < . 1:1(0) ack 2 win 257 ++0 > R 2:2(0) win 0 diff --git a/test/packetdrill/packetdrill_setup.sh b/test/packetdrill/packetdrill_setup.sh new file mode 100755 index 000000000..b858072f0 --- /dev/null +++ b/test/packetdrill/packetdrill_setup.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Copyright 2018 The gVisor Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script runs both within the sentry context and natively. It should tweak +# TCP parameters to match expectations found in the script files. +sysctl -q net.ipv4.tcp_sack=1 +sysctl -q net.ipv4.tcp_rmem="4096 2097152 $((8*1024*1024))" +sysctl -q net.ipv4.tcp_wmem="4096 2097152 $((8*1024*1024))" + +# There may be errors from the above, but they will show up in the test logs and +# we always want to proceed from this point. It's possible that values were +# already set correctly and the nodes were not available in the namespace. +exit 0 diff --git a/test/packetdrill/packetdrill_test.sh b/test/packetdrill/packetdrill_test.sh new file mode 100755 index 000000000..614d94d74 --- /dev/null +++ b/test/packetdrill/packetdrill_test.sh @@ -0,0 +1,213 @@ +#!/bin/bash + +# Copyright 2020 The gVisor Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Run a packetdrill test. Two docker containers are made, one for the +# Device-Under-Test (DUT) and one for the test runner. Each is attached with +# two networks, one for control packets that aid the test and one for test +# packets which are sent as part of the test and observed for correctness. + +set -euxo pipefail + +function failure() { + local lineno=$1 + local msg=$2 + local filename="$0" + echo "FAIL: $filename:$lineno: $msg" +} +trap 'failure ${LINENO} "$BASH_COMMAND"' ERR + +declare -r LONGOPTS="dut_platform:,init_script:" + +# Don't use declare below so that the error from getopt will end the script. +PARSED=$(getopt --options "" --longoptions=$LONGOPTS --name "$0" -- "$@") + +eval set -- "$PARSED" + +while true; do + case "$1" in + --dut_platform) + declare -r DUT_PLATFORM="$2" + shift 2 + ;; + --init_script) + declare -r INIT_SCRIPT="$2" + shift 2 + ;; + --) + shift + break + ;; + *) + echo "Programming error" + exit 3 + esac +done + +# All the other arguments are scripts. +declare -r scripts="$@" + +# Check that the required flags are defined in a way that is safe for "set -u". +if [[ "${DUT_PLATFORM-}" == "netstack" ]]; then + declare -r RUNTIME="--runtime runsc-d" +elif [[ "${DUT_PLATFORM-}" == "linux" ]]; then + declare -r RUNTIME="" +else + echo "FAIL: Bad or missing --dut_platform argument: ${DUT_PLATFORM-}" + exit 2 +fi +if [[ ! -x "${INIT_SCRIPT-}" ]]; then + echo "FAIL: Bad or missing --init_script: ${INIT_SCRIPT-}" + exit 2 +fi + +# Variables specific to the control network and interface start with CTRL_. +# Variables specific to the test network and interface start with TEST_. +# Variables specific to the DUT start with DUT_. +# Variables specific to the test runner start with TEST_RUNNER_. +declare -r PACKETDRILL="/packetdrill/gtests/net/packetdrill/packetdrill" +# Use random numbers so that test networks don't collide. +declare -r CTRL_NET="ctrl_net-${RANDOM}${RANDOM}" +declare -r TEST_NET="test_net-${RANDOM}${RANDOM}" +declare -r tolerance_usecs=100000 +# On both DUT and test runner, testing packets are on the eth2 interface. +declare -r TEST_DEVICE="eth2" +# Number of bits in the *_NET_PREFIX variables. +declare -r NET_MASK="24" +function new_net_prefix() { + # Class C, 192.0.0.0 to 223.255.255.255, transitionally has mask 24. + echo "$(shuf -i 192-223 -n 1).$(shuf -i 0-255 -n 1).$(shuf -i 0-255 -n 1)" +} +# Last bits of the DUT's IP address. +declare -r DUT_NET_SUFFIX=".10" +# Control port. +declare -r CTRL_PORT="40000" +# Last bits of the test runner's IP address. +declare -r TEST_RUNNER_NET_SUFFIX=".20" +declare -r TIMEOUT="60" +declare -r IMAGE_TAG="gcr.io/gvisor-presubmit/packetdrill" + +# Make sure that docker is installed. +docker --version + +function finish { + local cleanup_success=1 + for net in "${CTRL_NET}" "${TEST_NET}"; do + # Kill all processes attached to ${net}. + for docker_command in "kill" "rm"; do + (docker network inspect "${net}" \ + --format '{{range $key, $value := .Containers}}{{$key}} {{end}}' \ + | xargs -r docker "${docker_command}") || \ + cleanup_success=0 + done + # Remove the network. + docker network rm "${net}" || \ + cleanup_success=0 + done + + if ((!$cleanup_success)); then + echo "FAIL: Cleanup command failed" + exit 4 + fi +} +trap finish EXIT + +# Subnet for control packets between test runner and DUT. +declare CTRL_NET_PREFIX=$(new_net_prefix) +while ! docker network create \ + "--subnet=${CTRL_NET_PREFIX}.0/${NET_MASK}" "${CTRL_NET}"; do + sleep 0.1 + declare CTRL_NET_PREFIX=$(new_net_prefix) +done + +# Subnet for the packets that are part of the test. +declare TEST_NET_PREFIX=$(new_net_prefix) +while ! docker network create \ + "--subnet=${TEST_NET_PREFIX}.0/${NET_MASK}" "${TEST_NET}"; do + sleep 0.1 + declare TEST_NET_PREFIX=$(new_net_prefix) +done + +docker pull "${IMAGE_TAG}" + +# Create the DUT container and connect to network. +DUT=$(docker create ${RUNTIME} --privileged --rm \ + --stop-timeout ${TIMEOUT} -it ${IMAGE_TAG}) +docker network connect "${CTRL_NET}" \ + --ip "${CTRL_NET_PREFIX}${DUT_NET_SUFFIX}" "${DUT}" \ + || (docker kill ${DUT}; docker rm ${DUT}; false) +docker network connect "${TEST_NET}" \ + --ip "${TEST_NET_PREFIX}${DUT_NET_SUFFIX}" "${DUT}" \ + || (docker kill ${DUT}; docker rm ${DUT}; false) +docker start "${DUT}" + +# Create the test runner container and connect to network. +TEST_RUNNER=$(docker create --privileged --rm \ + --stop-timeout ${TIMEOUT} -it ${IMAGE_TAG}) +docker network connect "${CTRL_NET}" \ + --ip "${CTRL_NET_PREFIX}${TEST_RUNNER_NET_SUFFIX}" "${TEST_RUNNER}" \ + || (docker kill ${TEST_RUNNER}; docker rm ${REST_RUNNER}; false) +docker network connect "${TEST_NET}" \ + --ip "${TEST_NET_PREFIX}${TEST_RUNNER_NET_SUFFIX}" "${TEST_RUNNER}" \ + || (docker kill ${TEST_RUNNER}; docker rm ${REST_RUNNER}; false) +docker start "${TEST_RUNNER}" + +# Run tcpdump in the test runner unbuffered, without dns resolution, just on the +# interface with the test packets. +docker exec -t ${TEST_RUNNER} tcpdump -U -n -i "${TEST_DEVICE}" & + +# Start a packetdrill server on the test_runner. The packetdrill server sends +# packets and asserts that they are received. +docker exec -d "${TEST_RUNNER}" \ + ${PACKETDRILL} --wire_server --wire_server_dev="${TEST_DEVICE}" \ + --wire_server_ip="${CTRL_NET_PREFIX}${TEST_RUNNER_NET_SUFFIX}" \ + --wire_server_port="${CTRL_PORT}" \ + --local_ip="${TEST_NET_PREFIX}${TEST_RUNNER_NET_SUFFIX}" \ + --remote_ip="${TEST_NET_PREFIX}${DUT_NET_SUFFIX}" + +# Because the Linux kernel receives the SYN-ACK but didn't send the SYN it will +# issue a RST. To prevent this IPtables can be used to filter those out. +docker exec "${TEST_RUNNER}" \ + iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP + +# Wait for the packetdrill server on the test runner to come. Attempt to +# connect to it from the DUT every 100 milliseconds until success. +while ! docker exec "${DUT}" \ + nc -zv "${CTRL_NET_PREFIX}${TEST_RUNNER_NET_SUFFIX}" "${CTRL_PORT}"; do + sleep 0.1 +done + +# Copy the packetdrill setup script to the DUT. +docker cp -L "${INIT_SCRIPT}" "${DUT}:packetdrill_setup.sh" + +# Copy the packetdrill scripts to the DUT. +declare -a dut_scripts +for script in $scripts; do + docker cp -L "${script}" "${DUT}:$(basename ${script})" + dut_scripts+=("/$(basename ${script})") +done + +# Start a packetdrill client on the DUT. The packetdrill client runs POSIX +# socket commands and also sends instructions to the server. +docker exec -t "${DUT}" \ + ${PACKETDRILL} --wire_client --wire_client_dev="${TEST_DEVICE}" \ + --wire_server_ip="${CTRL_NET_PREFIX}${TEST_RUNNER_NET_SUFFIX}" \ + --wire_server_port="${CTRL_PORT}" \ + --local_ip="${TEST_NET_PREFIX}${DUT_NET_SUFFIX}" \ + --remote_ip="${TEST_NET_PREFIX}${TEST_RUNNER_NET_SUFFIX}" \ + --init_scripts=/packetdrill_setup.sh \ + --tolerance_usecs="${tolerance_usecs}" "${dut_scripts[@]}" + +echo PASS: No errors. |