Commit 2a5013028e for bind
commit 2a5013028e757598d916162449fb7731406b4c89
Author: Martin Basti <mbasti@isc.org>
Date: Fri Oct 30 19:35:21 2026 +0000
Migrate dnstap unix socket checks to pytest
The fstrm_capture checks move to tests_dnstap_unix.py, skipped when
the build did not find fstrm_capture. A fixture runs fstrm_capture
for the duration of a test and reconnects named to it. Instead of
polling the capture file for a minimum size, the test sends SIGHUP to
fstrm_capture and retries until dnstap-read shows the expected
frames.
The counts have always relied on no resolver frames reaching the
socket. Instead of stopping every other server around the checks,
ns4 now marks its only root server bogus, so every resolution fails
without a single resolver query being sent and the other servers
keep running.
With nothing stopping ns2 and ns3 any more, the remaining roll
checks in tests.sh no longer need to restart them.
Assisted-by: Claude:claude-fable-5-1
diff --git a/bin/tests/system/dnstap/ns4/named.conf.j2 b/bin/tests/system/dnstap/ns4/named.conf.j2
index 72e78160dc..a77a7ccb5b 100644
--- a/bin/tests/system/dnstap/ns4/named.conf.j2
+++ b/bin/tests/system/dnstap/ns4/named.conf.j2
@@ -11,7 +11,12 @@ options {
};
-server 10.53.0.1 { tcp-only yes; };
+/*
+ * The unix socket checks expect only client-facing frames; marking the
+ * sole root server bogus makes every resolution fail without sending a
+ * single resolver query.
+ */
+server 10.53.0.1 { bogus yes; };
{% include "_common/controls.conf.j2" %}
diff --git a/bin/tests/system/dnstap/tests.sh b/bin/tests/system/dnstap/tests.sh
index 5b881690a7..ca650cc1c6 100644
--- a/bin/tests/system/dnstap/tests.sh
+++ b/bin/tests/system/dnstap/tests.sh
@@ -15,283 +15,17 @@ set -e
. ../conf.sh
-DIGOPTS="-p ${PORT}"
RNDCCMD="$RNDC -p ${CONTROLPORT} -c ../_common/rndc.conf"
status=0
-# dnstap_data_ready <fstrm_capture_PID> <capture_file> <min_file_size>
-# Flushes capture_file and checks wheter its size is >= min_file_size.
-dnstap_data_ready() {
- # Process id of running fstrm_capture.
- fstrm_capture_pid=$1
- # Output file provided to fstrm_capture via -w switch.
- capture_file=$2
- # Minimum expected file size.
- min_size_expected=$3
-
- kill -HUP $fstrm_capture_pid
- file_size=$(wc -c <"$capture_file" | tr -d ' ')
- if [ $file_size -lt $min_size_expected ]; then
- return 1
- fi
-}
-
-check_count() {
- [ $2 -eq $3 ] || {
- echo_i "$1 $2 expected $3"
- ret=1
- }
-}
-
echo_i "wait for servers to finish loading"
ret=0
-wait_for_log 20 "all zones loaded" ns1/named.run || ret=1
wait_for_log 20 "all zones loaded" ns2/named.run || ret=1
wait_for_log 20 "all zones loaded" ns3/named.run || ret=1
-wait_for_log 20 "all zones loaded" ns4/named.run || ret=1
-wait_for_log 20 "all zones loaded" ns5/named.run || ret=1
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))
-if [ -n "$FSTRM_CAPTURE" ]; then
- ret=0
- echo_i "starting fstrm_capture"
- $FSTRM_CAPTURE -t protobuf:dnstap.Dnstap -u ns4/dnstap.out \
- -w dnstap.out >fstrm_capture.out.1 2>&1 &
- fstrm_capture_pid=$!
- wait_for_log 10 "socket path ns4/dnstap.out" fstrm_capture.out.1 || ret=1
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-fi
-
-echo_i "reopen capture stream"
-$RNDCCMD -s 10.53.0.4 dnstap -reopen | sed 's/^/ns4 /' | cat_i
-
-# ns4 must not be able to reach any other server, so that only its
-# client-facing frames are written to the unix socket.
-$RNDCCMD -s 10.53.0.1 stop | sed 's/^/ns1 /' | cat_i
-$RNDCCMD -s 10.53.0.2 stop | sed 's/^/ns2 /' | cat_i
-$RNDCCMD -s 10.53.0.3 stop | sed 's/^/ns3 /' | cat_i
-$RNDCCMD -s 10.53.0.5 stop | sed 's/^/ns5 /' | cat_i
-
-if [ -n "$FSTRM_CAPTURE" ]; then
- ret=0
- $DIG $DIGOPTS @10.53.0.4 a.example >dig.out || ret=1
-
- # send an UPDATE to ns4
- $NSUPDATE <<-EOF >nsupdate.out 2>&1 && ret=1
- server 10.53.0.4 ${PORT}
- zone example
- update add b.example 3600 in a 10.10.10.10
- send
-EOF
- grep "update failed: NOTAUTH" nsupdate.out >/dev/null || ret=1
-
- echo_i "checking unix socket message counts"
- sleep 2
- retry_quiet 5 dnstap_data_ready $fstrm_capture_pid dnstap.out 450 || {
- echo_i "dnstap output file smaller than expected"
- ret=1
- }
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
- kill $fstrm_capture_pid
- wait
- udp4=$($DNSTAPREAD dnstap.out | grep "UDP " | wc -l)
- tcp4=$($DNSTAPREAD dnstap.out | grep "TCP " | wc -l)
- aq4=$($DNSTAPREAD dnstap.out | grep "AQ " | wc -l)
- ar4=$($DNSTAPREAD dnstap.out | grep "AR " | wc -l)
- cq4=$($DNSTAPREAD dnstap.out | grep "CQ " | wc -l)
- cr4=$($DNSTAPREAD dnstap.out | grep "CR " | wc -l)
- fq4=$($DNSTAPREAD dnstap.out | grep "FQ " | wc -l)
- fr4=$($DNSTAPREAD dnstap.out | grep "FR " | wc -l)
- rq4=$($DNSTAPREAD dnstap.out | grep "RQ " | wc -l)
- rr4=$($DNSTAPREAD dnstap.out | grep "RR " | wc -l)
- uq4=$($DNSTAPREAD dnstap.out | grep "UQ " | wc -l)
- ur4=$($DNSTAPREAD dnstap.out | grep "UR " | wc -l)
-
- echo_i "checking UDP message counts"
- ret=0
- check_count ns4 $udp4 4
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking TCP message counts"
- ret=0
- check_count ns4 $tcp4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking AUTH_QUERY message counts"
- ret=0
- check_count ns4 $aq4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking AUTH_RESPONSE message counts"
- ret=0
- check_count ns4 $ar4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking CLIENT_QUERY message counts"
- ret=0
- check_count ns4 $cq4 1
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking CLIENT_RESPONSE message counts"
- ret=0
- check_count ns4 $cr4 1
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking RESOLVER_QUERY message counts"
- ret=0
- check_count ns4 $rq4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking RESOLVER_RESPONSE message counts"
- ret=0
- check_count ns4 $rr4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking FORWARDER_QUERY message counts"
- ret=0
- check_count ns4 $fq4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking FORWARDER_RESPONSE message counts"
- ret=0
- check_count ns4 $fr4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking UPDATE_QUERY message counts"
- ret=0
- check_count ns4 $uq4 1
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking UPDATE_RESPONSE message counts"
- ret=0
- check_count ns4 $ur4 1
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- mv dnstap.out dnstap.out.save
-
- echo_i "restarting fstrm_capture"
- $FSTRM_CAPTURE -t protobuf:dnstap.Dnstap -u ns4/dnstap.out \
- -w dnstap.out >fstrm_capture.out.2 2>&1 &
- fstrm_capture_pid=$!
- wait_for_log 10 "socket path ns4/dnstap.out" fstrm_capture.out.2 || {
- echo_i "failed"
- ret=1
- }
- $RNDCCMD -s 10.53.0.4 dnstap -reopen | sed 's/^/ns4 /' | cat_i
- $DIG $DIGOPTS @10.53.0.4 a.example >dig.out || ret=1
-
- echo_i "checking reopened unix socket message counts"
- sleep 2
- retry_quiet 5 dnstap_data_ready $fstrm_capture_pid dnstap.out 270 || {
- echo_i "dnstap output file smaller than expected"
- ret=1
- }
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
- kill $fstrm_capture_pid
- wait
- udp4=$($DNSTAPREAD dnstap.out | grep "UDP " | wc -l)
- tcp4=$($DNSTAPREAD dnstap.out | grep "TCP " | wc -l)
- aq4=$($DNSTAPREAD dnstap.out | grep "AQ " | wc -l)
- ar4=$($DNSTAPREAD dnstap.out | grep "AR " | wc -l)
- cq4=$($DNSTAPREAD dnstap.out | grep "CQ " | wc -l)
- cr4=$($DNSTAPREAD dnstap.out | grep "CR " | wc -l)
- fq4=$($DNSTAPREAD dnstap.out | grep "FQ " | wc -l)
- fr4=$($DNSTAPREAD dnstap.out | grep "FR " | wc -l)
- rq4=$($DNSTAPREAD dnstap.out | grep "RQ " | wc -l)
- rr4=$($DNSTAPREAD dnstap.out | grep "RR " | wc -l)
- uq4=$($DNSTAPREAD dnstap.out | grep "UQ " | wc -l)
- ur4=$($DNSTAPREAD dnstap.out | grep "UR " | wc -l)
-
- echo_i "checking UDP message counts"
- ret=0
- check_count ns4 $udp4 2
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking TCP message counts"
- ret=0
- check_count ns4 $tcp4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking AUTH_QUERY message counts"
- ret=0
- check_count ns4 $aq4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking AUTH_RESPONSE message counts"
- ret=0
- check_count ns4 $ar4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking CLIENT_QUERY message counts"
- ret=0
- check_count ns4 $cq4 1
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking CLIENT_RESPONSE message counts"
- ret=0
- check_count ns4 $cr4 1
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking RESOLVER_QUERY message counts"
- ret=0
- check_count ns4 $rq4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking RESOLVER_RESPONSE message counts"
- ret=0
- check_count ns4 $rr4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking FORWARDER_QUERY message counts"
- ret=0
- check_count ns4 $fq4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking FORWARDER_RESPONSE message counts"
- ret=0
- check_count ns4 $fr4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking UPDATE_QUERY message counts"
- ret=0
- check_count ns4 $uq4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-
- echo_i "checking UPDATE_RESPONSE message counts"
- ret=0
- check_count ns4 $ur4 0
- if [ $ret != 0 ]; then echo_i "failed"; fi
- status=$((status + ret))
-fi
-
_test_dnstap_roll() (
ip="$1"
ns="$2"
@@ -317,12 +51,10 @@ test_dnstap_roll() {
status=$((status + ret))
}
-start_server --noclean --restart --port "${PORT}" ns3
test_dnstap_roll "no versions" 10.53.0.3 ns3 6
test_dnstap_roll "no versions" 10.53.0.3 ns3 3
test_dnstap_roll "no versions" 10.53.0.3 ns3 1
-start_server --noclean --restart --port "${PORT}" ns2
test_dnstap_roll "versions" 10.53.0.2 ns2 6
test_dnstap_roll "versions" 10.53.0.2 ns2 3
test_dnstap_roll "versions" 10.53.0.2 ns2 1
diff --git a/bin/tests/system/dnstap/tests_dnstap_unix.py b/bin/tests/system/dnstap/tests_dnstap_unix.py
new file mode 100644
index 0000000000..9246236c41
--- /dev/null
+++ b/bin/tests/system/dnstap/tests_dnstap_unix.py
@@ -0,0 +1,112 @@
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+"""
+Check the "rndc dnstap" commands: -reopen against a unix socket output read
+by fstrm_capture.
+"""
+
+from dataclasses import dataclass
+
+import os
+import signal
+import subprocess
+
+import dns.rcode
+import dns.update
+import pytest
+
+from dnstap.common import ARTIFACTS, check_counts
+
+import isctest
+import isctest.mark
+
+pytestmark = [
+ isctest.mark.with_dnstap,
+ pytest.mark.requires_zones_loaded("ns1", "ns2", "ns3", "ns4", "ns5"),
+ pytest.mark.extra_artifacts(ARTIFACTS),
+]
+
+
+@dataclass
+class Capture:
+ proc: subprocess.Popen
+ output: str
+
+ def wait_for_counts(self, expected):
+ """
+ Make fstrm_capture flush its output and wait until the expected
+ frames have been written.
+ """
+
+ def check():
+ self.proc.send_signal(signal.SIGHUP)
+ try:
+ check_counts(self.output, expected)
+ except subprocess.CalledProcessError:
+ # dnstap-read failed on a partially written frame; try again.
+ return False
+ return True
+
+ isctest.run.retry_with_timeout(check, 10)
+
+
+@pytest.fixture(name="capture")
+def capture_fixture(request, ns4):
+ """
+ Run fstrm_capture on ns4's dnstap socket for the duration of the test
+ and make named connect to it.
+ """
+ output = f"dnstap.out.{request.node.originalname}"
+ logfile = f"fstrm_capture.out.{request.node.originalname}"
+ args = [
+ os.environ["FSTRM_CAPTURE"],
+ "-t",
+ "protobuf:dnstap.Dnstap",
+ "-u",
+ "ns4/dnstap.out",
+ "-w",
+ output,
+ ]
+ with open(logfile, "wb") as log, subprocess.Popen(
+ args, stdout=log, stderr=subprocess.STDOUT
+ ) as proc:
+ try:
+ with isctest.log.WatchLogFromStart(logfile, timeout=10) as watcher:
+ watcher.wait_for_line("socket path ns4/dnstap.out")
+ ns4.rndc("dnstap -reopen")
+ yield Capture(proc, output)
+ finally:
+ proc.terminate()
+
+
+def query(ns4):
+ msg = isctest.query.create("a.example.", "A")
+ isctest.query.udp(msg, ns4.ip, expected_rcode=dns.rcode.SERVFAIL)
+
+
+@isctest.mark.with_fstrm_capture
+def test_unix_socket(ns4, capture):
+ query(ns4)
+ update = dns.update.Update("example")
+ update.add("b.example.", 3600, "A", "10.10.10.10")
+ ns4.nsupdate(update, expected_rcode=dns.rcode.NOTAUTH)
+ capture.wait_for_counts({"UDP": 4, "CQ": 1, "CR": 1, "UQ": 1, "UR": 1})
+
+
+@isctest.mark.with_fstrm_capture
+def test_unix_socket_reopen(ns4, capture):
+ """
+ Check that named reconnects to a new listener on the socket after the
+ previous one (from the test above) has gone away.
+ """
+ query(ns4)
+ capture.wait_for_counts({"UDP": 2, "CQ": 1, "CR": 1})
diff --git a/bin/tests/system/dnstap/tests_sh_dnstap.py b/bin/tests/system/dnstap/tests_sh_dnstap.py
index df2936633c..6e60f2589c 100644
--- a/bin/tests/system/dnstap/tests_sh_dnstap.py
+++ b/bin/tests/system/dnstap/tests_sh_dnstap.py
@@ -17,13 +17,8 @@ pytestmark = [
isctest.mark.with_dnstap,
pytest.mark.extra_artifacts(
[
- "dig.out*",
- "dnstap.out*",
- "fstrm_capture.out.*",
- "nsupdate.out*",
"ns*/dnstap.out*",
"ns2/example.db",
- "ns2/example.db.jnl",
]
),
]
diff --git a/bin/tests/system/isctest/mark.py b/bin/tests/system/isctest/mark.py
index ddb4f85cc3..d96966875a 100644
--- a/bin/tests/system/isctest/mark.py
+++ b/bin/tests/system/isctest/mark.py
@@ -135,6 +135,11 @@ with_curl = pytest.mark.skipif(
not shutil.which(os.getenv("CURL", "")), reason="curl is required"
)
+with_fstrm_capture = pytest.mark.skipif(
+ not shutil.which(os.getenv("FSTRM_CAPTURE", "")),
+ reason="fstrm_capture is required",
+)
+
def _openssl_provider_available(provider: str) -> bool:
dylib = f".{os.environ.get('DYLIB', 'so')}"