From: Štěpán Balážik Date: Sat, 25 Apr 2026 14:13:59 +0000 (+0200) Subject: Port the long TCP stream test to Python X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8d3a08c7d5249875066cf0548797ceb7a88d3ca5;p=thirdparty%2Fbind9.git Port the long TCP stream test to Python Previously, the packet.pl script was used to send the a series of frames to named; this worked by accident as most of these were refused by the kernel with EAGAIN. packet.pl prints a dot every 1000 packets, so this slowed the script down and allowed some frames to get through. Reimplement the test in Python: build the packet with dnspython, send ~6 MiB of data over TCP discarding all replies and then check if the server is still alive. The server may close the connection before we have written the whole stream. This shows up notably on Alma Linux 8: under concurrent load its small default TCP buffers let the flow control between us and named wedge, named then hits its idle timeout and resets the connection mid-write, and drain() raises ConnectionError. That is fine -- the connection being torn down is acceptable and the only thing this test asserts is that named survives the flood, which the final query checks -- so tolerate the server closing the connection early. --- diff --git a/bin/tests/system/tcp/1996-alloc_dnsbuf-crash-test.pkt b/bin/tests/system/tcp/1996-alloc_dnsbuf-crash-test.pkt deleted file mode 100644 index 7520c3a35f9..00000000000 --- a/bin/tests/system/tcp/1996-alloc_dnsbuf-crash-test.pkt +++ /dev/null @@ -1,12 +0,0 @@ -# Transaction ID -0001 -# Standard query -0000 -# Questions: 1, Additional: 1 -0001 0000 0000 0000 -# QNAME: www.isc.org -03 697363 03 6F7267 00 -# Type: AXFR -00fc -# Class: IN -0001 diff --git a/bin/tests/system/tcp/tests.sh b/bin/tests/system/tcp/tests.sh deleted file mode 100644 index fc829f77f48..00000000000 --- a/bin/tests/system/tcp/tests.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh - -# 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. - -set -e - -# shellcheck source=../conf.sh -. ../conf.sh - -dig_with_opts() { - "${DIG}" -p "${PORT}" "$@" -} - -rndccmd() { - "${RNDC}" -p "${CONTROLPORT}" -c ../_common/rndc.conf -s "$@" -} - -status=0 -n=0 - -#################################################### -# NOTE: The next test resets the debug level to 1. # -#################################################### - -n=$((n + 1)) -echo_i "checking that BIND 9 doesn't crash on long TCP messages ($n)" -ret=0 -# Avoid logging useless information. -rndccmd 10.53.0.1 trace 1 || ret=1 -{ $PERL ../packet.pl -a "10.53.0.1" -p "${PORT}" -t tcp -r 300000 1996-alloc_dnsbuf-crash-test.pkt || ret=1; } | cat_i -dig_with_opts +tcp @10.53.0.1 txt.example >dig.out.test$n || ret=1 -if [ $ret != 0 ]; then echo_i "failed"; fi -status=$((status + ret)) - -echo_i "exit status: $status" -[ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/tcp/tests_sh_tcp.py b/bin/tests/system/tcp/tests_sh_tcp.py deleted file mode 100644 index 8aba3c456c0..00000000000 --- a/bin/tests/system/tcp/tests_sh_tcp.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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. - -import pytest - -pytestmark = pytest.mark.extra_artifacts( - [ - "dig.out.*", - ] -) - - -def test_tcp(run_tests_sh): - run_tests_sh() diff --git a/bin/tests/system/tcp/tests_tcp.py b/bin/tests/system/tcp/tests_tcp.py index ef16ddcc09d..d69e857089a 100644 --- a/bin/tests/system/tcp/tests_tcp.py +++ b/bin/tests/system/tcp/tests_tcp.py @@ -11,11 +11,12 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -from collections.abc import Iterable +from collections.abc import Iterable, Iterator from types import TracebackType from typing import NamedTuple import asyncio +import contextlib import socket import struct import time @@ -241,6 +242,45 @@ def close_tcp_connections(connections: Iterable[socket.socket]) -> None: sock.close() +async def send_long_tcp_stream( + host: str, port: int, message: dns.message.Message, min_bytes: int +) -> None: + frame = message.to_wire(prepend_length=True) + chunk_frames = max(1, 65536 // len(frame)) + frames_remaining = (min_bytes + len(frame) - 1) // len(frame) + + async def discard_stream(reader: asyncio.StreamReader) -> None: + with contextlib.suppress(OSError): + while await reader.read(65535): + pass + + async def run() -> None: + reader, writer = await asyncio.open_connection(host, port) + discard_task = asyncio.create_task(discard_stream(reader)) + try: + with contextlib.suppress(ConnectionError): + remaining = frames_remaining + while remaining > 0: + frames = min(chunk_frames, remaining) + writer.write(frame * frames) + await writer.drain() + remaining -= frames + writer.write_eof() + await writer.drain() + writer.close() + with contextlib.suppress(ConnectionError, OSError): + await writer.wait_closed() + await discard_task + finally: + writer.close() + if not discard_task.done(): + discard_task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await discard_task + + await asyncio.wait_for(run(), timeout=10 * TIMEOUT) + + def test_tcp_garbage(ns7: NamedInstance, named_port: int) -> None: with create_socket(ns7.ip, named_port) as sock: msg = isctest.query.create( @@ -430,3 +470,43 @@ def test_tcp_high_water(ns5: NamedInstance, named_port: int) -> None: check_tcp_response(ns5.ip) asyncio.run(run()) + + +def debug_level(ns: NamedInstance) -> int: + status = ns.rndc("status").out + matches = status.grep("debug level:") + assert matches, f"'debug level' not found in rndc status:\n{status}" + return int(matches[0].string.partition(":")[2]) + + +@contextlib.contextmanager +def temporary_trace_level(ns: NamedInstance, level: int) -> Iterator[None]: + """Lower the debug level for a noisy section, then restore the default.""" + prev_level = debug_level(ns) + ns.rndc(f"trace {level}") + try: + yield + finally: + # Don't mask an in-flight test failure if named has died. + ns.rndc(f"trace {prev_level}", raise_on_exception=False) + + +def test_long_tcp_messages(ns1: NamedInstance, named_port: int) -> None: + isctest.log.info("checking that BIND 9 doesn't crash on long TCP messages") + stream_bytes = 6 * 1024 * 1024 + msg = isctest.query.create( + "isc.org.", + "AXFR", + dnssec=False, + use_edns=False, + rd=False, + ad=False, + message_id=1, + ) + + # Avoid logging the huge query stream at the default debug level. + with temporary_trace_level(ns1, 1): + asyncio.run(send_long_tcp_stream(ns1.ip, named_port, msg, stream_bytes)) + + msg = isctest.query.create("txt.example.", "A") + isctest.query.tcp(msg, ns1.ip)