chg: test: Use isctest.asyncserver in the "nsupdate" test

Reimplement the custom server written in Perl in Python using the AsyncDnsServer class.

Merge branch 'stepan/nsupdate-asyncserver' into 'main'

See merge request isc-projects/bind9!10915
This commit is contained in:
Štěpán Balážik 2025-10-23 12:25:56 +00:00
commit 09ad0ded75
3 changed files with 90 additions and 105 deletions

View file

@ -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(

View file

@ -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";
}
}

View file

@ -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()