From 4042b805ff77f115b44ed659f8eed1793bc41720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?= Date: Thu, 28 Aug 2025 18:10:19 +0200 Subject: [PATCH 1/2] Enable ignoring TCP connections Add a TCP connection handler, IgnoreAllConnections that allows establishing TCP connection but not reading anything from it. This re-uses the horrible hack from ConnectionReset handler and might break at any point in the future. See the comments and e4078885073a6c5b59729f4313108e3e7637efdb for more details. --- bin/tests/system/isctest/asyncserver.py | 103 +++++++++++++++--------- 1 file changed, 63 insertions(+), 40 deletions(-) diff --git a/bin/tests/system/isctest/asyncserver.py b/bin/tests/system/isctest/asyncserver.py index 01d79e2351..f25ca4192b 100644 --- a/bin/tests/system/isctest/asyncserver.py +++ b/bin/tests/system/isctest/asyncserver.py @@ -414,6 +414,68 @@ class ConnectionHandler(abc.ABC): raise NotImplementedError +def block_reading(peer: Peer, writer_not_the_reader: asyncio.StreamWriter) -> None: + """ + Block reads for the reader associated with the provided writer. + + Yes, pass the writer, not the reader. See the comments below for details. + """ + + try: + # Python >= 3.7 + loop = asyncio.get_running_loop() + except AttributeError: + # Python < 3.7 + loop = asyncio.get_event_loop() + + logging.info("Blocking reads from %s", peer) + + # This is Michał's submission for the Ugliest Hack of the Year contest. + # (The alternative was implementing an asyncio transport from scratch.) + # + # In order to prevent the client socket from being read from, simply + # not calling `reader.read()` is not enough, because asyncio buffers + # incoming data itself on the transport level. However, `StreamReader` + # does not expose the underlying transport as a property. Therefore, + # cheat by extracting it from `StreamWriter` as it is the same + # bidirectional transport as for the read side (a `Transport`, which is + # a subclass of both `ReadTransport` and `WriteTransport`) and call + # `ReadTransport.pause_reading()` to remove the underlying socket from + # the set of descriptors monitored by the selector, thereby preventing + # any reads from happening on the client socket. However... + loop.call_soon(writer_not_the_reader.transport.pause_reading) # type: ignore + + # ...due to `AsyncDnsServer._handle_tcp()` being a coroutine, by the + # time it gets executed, asyncio transport code will already have added + # the client socket to the set of descriptors monitored by the + # selector. Therefore, if the client starts sending data immediately, + # a read from the socket will have already been scheduled by the time + # this handler gets executed. There is no way to prevent that from + # happening, so work around it by abusing the fact that the transport + # at hand is specifically an instance of `_SelectorSocketTransport` + # (from asyncio.selector_events) and set the size of its read buffer to + # just a single byte. This does give asyncio enough time to read that + # single byte from the client socket's buffer before that socket is + # removed from the set of monitored descriptors, but prevents the + # one-off read from emptying the client socket buffer _entirely_, which + # is enough to trigger sending an RST segment when the connection is + # closed shortly afterwards. + writer_not_the_reader.transport.max_size = 1 # type: ignore + + +@dataclass +class IgnoreAllConnections(ConnectionHandler): + """ + A connection handler that makes the server not read anything from the + client socket, effectively ignoring all incoming connections. + """ + + async def handle( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer + ) -> None: + block_reading(peer, writer) + + @dataclass class ConnectionReset(ConnectionHandler): """ @@ -435,46 +497,7 @@ class ConnectionReset(ConnectionHandler): async def handle( self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer ) -> None: - try: - # Python >= 3.7 - loop = asyncio.get_running_loop() - except AttributeError: - # Python < 3.7 - loop = asyncio.get_event_loop() - - logging.info("Blocking reads from %s", peer) - - # This is Michał's submission for the Ugliest Hack of the Year contest. - # (The alternative was implementing an asyncio transport from scratch.) - # - # In order to prevent the client socket from being read from, simply - # not calling `reader.read()` is not enough, because asyncio buffers - # incoming data itself on the transport level. However, `StreamReader` - # does not expose the underlying transport as a property. Therefore, - # cheat by extracting it from `StreamWriter` as it is the same - # bidirectional transport as for the read side (a `Transport`, which is - # a subclass of both `ReadTransport` and `WriteTransport`) and call - # `ReadTransport.pause_reading()` to remove the underlying socket from - # the set of descriptors monitored by the selector, thereby preventing - # any reads from happening on the client socket. However... - loop.call_soon(writer.transport.pause_reading) # type: ignore - - # ...due to `AsyncDnsServer._handle_tcp()` being a coroutine, by the - # time it gets executed, asyncio transport code will already have added - # the client socket to the set of descriptors monitored by the - # selector. Therefore, if the client starts sending data immediately, - # a read from the socket will have already been scheduled by the time - # this handler gets executed. There is no way to prevent that from - # happening, so work around it by abusing the fact that the transport - # at hand is specifically an instance of `_SelectorSocketTransport` - # (from asyncio.selector_events) and set the size of its read buffer to - # just a single byte. This does give asyncio enough time to read that - # single byte from the client socket's buffer before that socket is - # removed from the set of monitored descriptors, but prevents the - # one-off read from emptying the client socket buffer _entirely_, which - # is enough to trigger sending an RST segment when the connection is - # closed shortly afterwards. - writer.transport.max_size = 1 # type: ignore + block_reading(peer, writer) if self.delay > 0: logging.info( From 99b0cf2dd7eea9f4052b721404c60469ecd2a144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?= Date: Thu, 28 Aug 2025 18:13:36 +0200 Subject: [PATCH 2/2] Use isctest.asyncserver in the "nsupdate" test Reimplement the custom server written in Perl in Python using the AsyncDnsServer class. --- bin/tests/system/nsupdate/ans4/ans.pl | 65 --------------------------- bin/tests/system/nsupdate/ans4/ans.py | 27 +++++++++++ 2 files changed, 27 insertions(+), 65 deletions(-) delete mode 100644 bin/tests/system/nsupdate/ans4/ans.pl create mode 100644 bin/tests/system/nsupdate/ans4/ans.py diff --git a/bin/tests/system/nsupdate/ans4/ans.pl b/bin/tests/system/nsupdate/ans4/ans.pl deleted file mode 100644 index 30c792f1cf..0000000000 --- a/bin/tests/system/nsupdate/ans4/ans.pl +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/perl - -# 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. - -use IO::Socket; -use IO::File; -use strict; - -# Ignore SIGPIPE so we won't fail if peer closes a TCP socket early -local $SIG{PIPE} = 'IGNORE'; - -# Flush logged output after every line -local $| = 1; - -my $server_addr = "10.53.0.4"; -if (@ARGV > 0) { - $server_addr = @ARGV[0]; -} - -my $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $udpsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $localport, Proto => "udp", Reuse => 1) or die "$!"; -my $tcpsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $localport, Proto => "tcp", Listen => 5, Reuse => 1) or die "$!"; - -print "listening on $server_addr:$localport.\n"; - -my $pidf = new IO::File "ans.pid", "w" or die "cannot open pid file: $!"; -print $pidf "$$\n" or die "cannot write pid file: $!"; -$pidf->close or die "cannot close pid file: $!";; -sub rmpid { unlink "ans.pid"; exit 1; }; - -$SIG{INT} = \&rmpid; -$SIG{TERM} = \&rmpid; - -# Main -for (;;) { - my $rin; - my $rout; - - $rin = ''; - vec($rin, fileno($udpsock), 1) = 1; - vec($rin, fileno($tcpsock), 1) = 1; - - select($rout = $rin, undef, undef, undef); - - if (vec($rout, fileno($udpsock), 1)) { - printf "UDP request\n"; - my $buf; - $udpsock->recv($buf, 512); - } elsif (vec($rout, fileno($tcpsock), 1)) { - printf "TCP request\n"; - } -} diff --git a/bin/tests/system/nsupdate/ans4/ans.py b/bin/tests/system/nsupdate/ans4/ans.py new file mode 100644 index 0000000000..ab066c4bee --- /dev/null +++ b/bin/tests/system/nsupdate/ans4/ans.py @@ -0,0 +1,27 @@ +# 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. + +from isctest.asyncserver import ( + AsyncDnsServer, + IgnoreAllConnections, + IgnoreAllQueries, +) + + +def main() -> None: + server = AsyncDnsServer() + server.install_connection_handler(IgnoreAllConnections()) + server.install_response_handler(IgnoreAllQueries()) + server.run() + + +if __name__ == "__main__": + main()