From 3005a39af3094f03c159d1dc881894781b37e528 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Thu, 4 Sep 2025 14:47:41 +0200 Subject: [PATCH 1/2] test: avoid non-loopback network traffic from node_init_tests/init_test The test calls: `AppInitMain()` -> `StartMapPort()` -> `StartThreadMapPort()` -> `ThreadMapPort()` -> `ProcessPCP()` -> `PCPRequestPortMap()` -> `CreateSock()` and on the returned value from `CreateSock()` it calls the `Connect()` method. Thus, change `BasicTestingSetup::BasicTestingSetup()` to set `-natpmp` to 0. This way `node_init_tests/init_test` or other tests will not do network activity due to `ThreadMapPort()`. Also add a comment about `natpmp=0` in `test/functional/test_framework/util.py`. --- src/test/util/setup_common.cpp | 2 ++ test/functional/test_framework/util.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 39c691c33646..3ddce1324b31 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -199,6 +199,8 @@ BasicTestingSetup::BasicTestingSetup(const ChainType chainType, TestOpts opts) m_args.ForceSetArg("-datadir", fs::PathToString(m_path_root)); gArgs.ForceSetArg("-datadir", fs::PathToString(m_path_root)); + gArgs.ForceSetArg("-natpmp", "0"); // Avoid non-loopback network traffic during tests. + SelectParams(chainType); InitLogging(*m_node.args); AppInitParameterInteraction(*m_node.args); diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 3e7e6dd97a99..4918a7f4d5e3 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -576,7 +576,7 @@ def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect= # in tests. f.write("peertimeout=999999999\n") f.write("printtoconsole=0\n") - f.write("natpmp=0\n") + f.write("natpmp=0\n") # Avoid non-loopback network traffic during tests. f.write("shrinkdebugfile=0\n") # To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync f.write("unsafesqlitesync=1\n") From d88d4fb889b43e809289fdd14062d5ba125e63a8 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Thu, 21 Nov 2024 07:04:52 +0100 Subject: [PATCH 2/2] ci: detect outbound internet traffic generated while running tests Resolves https://github.com/bitcoin/bitcoin/issues/31339 --- ci/test/00_setup_env.sh | 2 +- ci/test/00_setup_env_mac_native.sh | 2 + ci/test/00_setup_env_mac_native_fuzz.sh | 2 + ci/test/00_setup_env_native_alpine_musl.sh | 2 +- ci/test/02_run_container.py | 1 + ci/test/03_test_script.sh | 67 ++++++++++++++++++++++ 6 files changed, 74 insertions(+), 2 deletions(-) diff --git a/ci/test/00_setup_env.sh b/ci/test/00_setup_env.sh index 8d9a16ab7841..f1b8da7e96f7 100755 --- a/ci/test/00_setup_env.sh +++ b/ci/test/00_setup_env.sh @@ -59,7 +59,7 @@ export BASE_OUTDIR=${BASE_OUTDIR:-$BASE_SCRATCH_DIR/out} # The folder for previous release binaries. # This folder exists only on the ci guest, and on the ci host as a volume. export PREVIOUS_RELEASES_DIR=${PREVIOUS_RELEASES_DIR:-$BASE_ROOT_DIR/prev_releases} -export CI_BASE_PACKAGES=${CI_BASE_PACKAGES:-build-essential pkgconf curl ca-certificates ccache python3-dev rsync git procps bison e2fsprogs cmake ninja-build} +export CI_BASE_PACKAGES=${CI_BASE_PACKAGES:-build-essential pkgconf curl ca-certificates ccache python3-dev rsync git procps bison e2fsprogs cmake ninja-build net-tools tcpdump} export GOAL=${GOAL:-install} export DIR_QA_ASSETS=${DIR_QA_ASSETS:-${BASE_SCRATCH_DIR}/qa-assets} export CI_RETRY_EXE=${CI_RETRY_EXE:-"retry"} diff --git a/ci/test/00_setup_env_mac_native.sh b/ci/test/00_setup_env_mac_native.sh index a9b4d5c8c2e5..7d2edb238f45 100755 --- a/ci/test/00_setup_env_mac_native.sh +++ b/ci/test/00_setup_env_mac_native.sh @@ -20,3 +20,5 @@ export BITCOIN_CONFIG="\ -DCMAKE_EXE_LINKER_FLAGS='-Wl,-stack_size -Wl,0x80000' \ " export BITCOIN_CMD="bitcoin -m" # Used in functional tests +# Can't run tcpdump: tcpdump: en0: You don't have permission to capture on that device +export CI_TCPDUMP_OK_TO_FAIL=1 diff --git a/ci/test/00_setup_env_mac_native_fuzz.sh b/ci/test/00_setup_env_mac_native_fuzz.sh index ea074e47732f..de254b0ec5de 100755 --- a/ci/test/00_setup_env_mac_native_fuzz.sh +++ b/ci/test/00_setup_env_mac_native_fuzz.sh @@ -16,3 +16,5 @@ export RUN_UNIT_TESTS=false export RUN_FUNCTIONAL_TESTS=false export RUN_FUZZ_TESTS=true export GOAL="all" +# Can't run tcpdump: tcpdump: en0: You don't have permission to capture on that device +export CI_TCPDUMP_OK_TO_FAIL=1 diff --git a/ci/test/00_setup_env_native_alpine_musl.sh b/ci/test/00_setup_env_native_alpine_musl.sh index e42cb4a3f1e8..8fd87fd65930 100755 --- a/ci/test/00_setup_env_native_alpine_musl.sh +++ b/ci/test/00_setup_env_native_alpine_musl.sh @@ -8,7 +8,7 @@ export LC_ALL=C.UTF-8 export CONTAINER_NAME=ci_native_alpine_musl export CI_IMAGE_NAME_TAG="mirror.gcr.io/alpine:3.23" -export CI_BASE_PACKAGES="build-base musl-dev pkgconf curl ccache make ninja git python3-dev py3-pip which patch xz procps rsync util-linux bison e2fsprogs cmake dash linux-headers" +export CI_BASE_PACKAGES="build-base musl-dev pkgconf curl ccache make ninja git python3-dev py3-pip which patch xz procps rsync util-linux bison e2fsprogs cmake dash linux-headers net-tools tcpdump" export PIP_PACKAGES="--break-system-packages pyzmq pycapnp" export DEP_OPTS="DEBUG=1" export GOAL="install" diff --git a/ci/test/02_run_container.py b/ci/test/02_run_container.py index abaa535553db..39f577bd951d 100755 --- a/ci/test/02_run_container.py +++ b/ci/test/02_run_container.py @@ -134,6 +134,7 @@ def main(): cmd_run = ["docker", "run", "--rm", "--interactive", "--detach", "--tty"] cmd_run += [ "--cap-add=LINUX_IMMUTABLE", + "--cap-add=NET_RAW", *shlex.split(os.getenv("CI_CONTAINER_CAP", "")), f"--mount=type=bind,src={os.environ['BASE_READ_ONLY_DIR']},dst={os.environ['BASE_READ_ONLY_DIR']},readonly", f"--mount={CI_CCACHE_MOUNT}", diff --git a/ci/test/03_test_script.sh b/ci/test/03_test_script.sh index 4e4d975778dd..8cc117bbb8cd 100755 --- a/ci/test/03_test_script.sh +++ b/ci/test/03_test_script.sh @@ -163,7 +163,65 @@ if [[ "$CI_OS_NAME" == "macos" && "${GOAL}" = "install deploy" ]]; then fi fi +# Return a list of the network interfaces on the machine, for example: docker0 lo enp3s0 wlp2s0 +function get_interfaces() +{ + set -o pipefail + ifconfig | awk -F ':| ' '/^[^[:space:]]/ { print $1 }' + set +o pipefail +} + +# Generate a file name for storing raw packets captured by tcpdump. +function tcpdump_file() +{ + local test_name="$1" + local interface_name="$2" + echo "/tmp/tcpdump_${test_name}_$interface_name" +} + +# Start tcpdump on each interface in the background. +function traffic_monitor_begin() +{ + test_name="$1" + for ifname in $(get_interfaces) ; do + if [[ "$ifname" == lo* ]] ; then + # Only report as unwanted traffic to local interfaces to port 53 (DNS) + # because that is usually proxied to upstream resolvers on the internet. + filter="port 53" + fi + tcpdump -nU -i "$ifname" -w "$(tcpdump_file "$test_name" "$ifname")" "$filter" & + done +} + +# Read tcpdump raw packet files that are generated by traffic_monitor_begin() and if any of them contain +# data, then print the packets and exit with an error. +function traffic_monitor_end() +{ + test_name="$1" + + for ifname in $(get_interfaces) ; do + f=$(tcpdump_file "$test_name" "$ifname") + if [ ! -e "$f" ] && [ "$CI_TCPDUMP_OK_TO_FAIL" = "1" ] ; then + # In some CI environments this script is not running as root and so the + # tcpdump errors and does not create $f. Those would set + # CI_TCPDUMP_OK_TO_FAIL=1 and we skip them silently here. + continue + fi + # We are running as root and those files are created with owner:group = + # tcpdump:tcpdump and then `tcpdump -r` refuses to read them with an error + # "permission denied" if they are not owned by root:root. + chown root:root "$f" + out="$(tcpdump -n -r "$f" --direction=out tcp or udp)" + if [ -n "$out" ] ; then + echo "Error: detected outbound TCP or UDP packets on a non loopback interface or DNS queries during $test_name tests:" >&2 + tcpdump -n -r "$f" tcp or udp + exit 1 + fi + done +} + if [ "$RUN_UNIT_TESTS" = "true" ]; then + traffic_monitor_begin "unit" DIR_UNIT_TEST_DATA="${DIR_UNIT_TEST_DATA}" \ LD_LIBRARY_PATH="${DEPENDS_DIR}/${HOST}/lib" \ CTEST_OUTPUT_ON_FAILURE=ON \ @@ -171,9 +229,11 @@ if [ "$RUN_UNIT_TESTS" = "true" ]; then --stop-on-failure \ "${MAKEJOBS}" \ --timeout $(( TEST_RUNNER_TIMEOUT_FACTOR * 60 )) + traffic_monitor_end "unit" fi if [ "$RUN_FUNCTIONAL_TESTS" = "true" ]; then + traffic_monitor_begin "functional" # parses TEST_RUNNER_EXTRA as an array which allows for multiple arguments such as TEST_RUNNER_EXTRA='--exclude "rpc_bind.py --ipv6"' eval "TEST_RUNNER_EXTRA=($TEST_RUNNER_EXTRA)" LD_LIBRARY_PATH="${DEPENDS_DIR}/${HOST}/lib" \ @@ -186,9 +246,11 @@ if [ "$RUN_FUNCTIONAL_TESTS" = "true" ]; then "${TEST_RUNNER_EXTRA[@]}" \ --quiet \ --failfast + traffic_monitor_end "functional" fi if [ "${RUN_TIDY}" = "true" ]; then + traffic_monitor_begin "tidy" cmake -B /tidy-build -DLLVM_DIR=/usr/lib/llvm-"${TIDY_LLVM_V}"/cmake -DCMAKE_BUILD_TYPE=Release -S "${BASE_ROOT_DIR}"/contrib/devtools/bitcoin-tidy cmake --build /tidy-build "$MAKEJOBS" cmake --build /tidy-build --target bitcoin-tidy-tests "$MAKEJOBS" @@ -205,9 +267,11 @@ if [ "${RUN_TIDY}" = "true" ]; then echo "^^^ ⚠️ Failure generated from clang-tidy" false fi + traffic_monitor_end "tidy" fi if [[ "${RUN_IWYU}" == true ]]; then + traffic_monitor_begin "iwyu" # TODO: Consider enforcing IWYU across the entire codebase. FILES_WITH_ENFORCED_IWYU="/src/(((crypto|index|kernel|primitives|univalue/(lib|test)|util|zmq)/.*|common/license_info|node/blockstorage|node/utxo_snapshot|clientversion|core_io|signet)\\.cpp)" jq --arg patterns "$FILES_WITH_ENFORCED_IWYU" 'map(select(.file | test($patterns)))' "${BASE_BUILD_DIR}/compile_commands.json" > "${BASE_BUILD_DIR}/compile_commands_iwyu_errors.json" @@ -235,9 +299,11 @@ if [[ "${RUN_IWYU}" == true ]]; then run_iwyu "compile_commands_iwyu_warnings.json" git --no-pager diff + traffic_monitor_end "iwyu" fi if [ "$RUN_FUZZ_TESTS" = "true" ]; then + traffic_monitor_begin "fuzz" # shellcheck disable=SC2086 LD_LIBRARY_PATH="${DEPENDS_DIR}/${HOST}/lib" \ "${BASE_BUILD_DIR}/test/fuzz/test_runner.py" \ @@ -246,4 +312,5 @@ if [ "$RUN_FUZZ_TESTS" = "true" ]; then -l DEBUG \ "${DIR_FUZZ_IN}" \ --empty_min_time=60 + traffic_monitor_end "fuzz" fi