From: Štěpán Balážik Date: Wed, 8 Apr 2026 10:05:16 +0000 (+0200) Subject: Reimplement 'reclimit/ans2' server using ControllableAsyncServer X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=383e04910e3d2eea587c4fba7a27b32e52092288;p=thirdparty%2Fbind9.git Reimplement 'reclimit/ans2' server using ControllableAsyncServer Ensure packet-for-packet compatibility with the old server including bugs. --- diff --git a/bin/tests/system/reclimit/ans2/ans.pl b/bin/tests/system/reclimit/ans2/ans.pl deleted file mode 100644 index 4576951d7cc..00000000000 --- a/bin/tests/system/reclimit/ans2/ans.pl +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env 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 strict; -use warnings; - -use IO::File; -use IO::Socket; -use Net::DNS; - -my $localaddr = "10.53.0.2"; -my $limit = getlimit(); -my $no_more_waiting = 0; -my @delayed_response; -my $timeout; - -my $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $udpsock = IO::Socket::INET->new(LocalAddr => "$localaddr", - LocalPort => $localport, Proto => "udp", Reuse => 1) or die "$!"; - -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; - -my $count = 0; -my $send_response = 0; - -sub getlimit { - if ( -e "ans.limit") { - open(FH, "<", "ans.limit"); - my $line = ; - chomp $line; - close FH; - if ($line =~ /^\d+$/) { - return $line; - } - } - - return 0; -} - -# If $wait == 0 is returned, returned reply will be sent immediately. -# If $wait == 1 is returned, sending the returned reply might be delayed; see -# comments inside handle_UDP() for details. -sub reply_handler { - my ($qname, $qclass, $qtype) = @_; - my ($rcode, @ans, @auth, @add, $wait); - - print ("request: $qname/$qtype\n"); - STDOUT->flush(); - - $wait = 0; - $count += 1; - - if ($qname eq "count" ) { - if ($qtype eq "TXT") { - my ($ttl, $rdata) = (0, "$count"); - my $rr = new Net::DNS::RR("$qname $ttl $qclass $qtype $rdata"); - push @ans, $rr; - print ("\tcount: $count\n"); - } - $rcode = "NOERROR"; - } elsif ($qname eq "reset" ) { - $count = 0; - $send_response = 0; - $limit = getlimit(); - $rcode = "NOERROR"; - print ("\tlimit: $limit\n"); - } elsif ($qname eq "direct.example.org" ) { - if ($qtype eq "A") { - my ($ttl, $rdata) = (3600, $localaddr); - my $rr = new Net::DNS::RR("$qname $ttl $qclass $qtype $rdata"); - push @ans, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname eq "indirect1.example.org" || - $qname eq "indirect2.example.org" || - $qname eq "indirect3.example.org" || - $qname eq "indirect4.example.org" || - $qname eq "indirect5.example.org" || - $qname eq "indirect6.example.org" || - $qname eq "indirect7.example.org" || - $qname eq "indirect8.example.org") { - if (! $send_response) { - my $rr = new Net::DNS::RR("$qname 86400 $qclass NS ns1.1.example.org"); - push @auth, $rr; - } elsif ($qtype eq "A") { - my ($ttl, $rdata) = (3600, $localaddr); - my $rr = new Net::DNS::RR("$qname $ttl $qclass $qtype $rdata"); - push @ans, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname =~ /^ns1\.(\d+)\.example\.org$/) { - my $next = $1 + 1; - $wait = 1; - if ($limit == 0 || (! $send_response && $next <= $limit)) { - my $rr = new Net::DNS::RR("$1.example.org 86400 $qclass NS ns1.$next.example.org"); - push @auth, $rr; - } else { - $send_response = 1; - if ($qtype eq "A") { - my ($ttl, $rdata) = (3600, "10.53.0.4"); - my $rr = new Net::DNS::RR("$qname $ttl $qclass $qtype $rdata"); - print("\tresponse: $qname $ttl $qclass $qtype $rdata\n"); - push @ans, $rr; - } - } - $rcode = "NOERROR"; - } elsif ($qname eq "direct.example.net" ) { - if ($qtype eq "A") { - my ($ttl, $rdata) = (3600, $localaddr); - my $rr = new Net::DNS::RR("$qname $ttl $qclass $qtype $rdata"); - push @ans, $rr; - } - $rcode = "NOERROR"; - } elsif( $qname =~ /^ns1\.(\d+)\.example\.net$/ ) { - my $next = ($1 + 1) * 16; - for (my $i = 1; $i < 16; $i++) { - my $s = $next + $i; - my $rr = new Net::DNS::RR("$1.example.net 86400 $qclass NS ns1.$s.example.net"); - push @auth, $rr; - $rr = new Net::DNS::RR("ns1.$s.example.net 86400 $qclass A 10.53.0.7"); - push @add, $rr; - } - $rcode = "NOERROR"; - } else { - $rcode = "NXDOMAIN"; - } - - return ($rcode, \@ans, \@auth, \@add, $wait); -} - -sub handleUDP { - my ($buf, $peer) = @_; - my ($request, $rcode, $ans, $auth, $add, $wait); - - $request = new Net::DNS::Packet(\$buf, 0); - $@ and die $@; - - my ($question) = $request->question; - my $qname = $question->qname; - my $qclass = $question->qclass; - my $qtype = $question->qtype; - - ($rcode, $ans, $auth, $add, $wait) = reply_handler($qname, $qclass, $qtype); - - my $reply = $request->reply(); - - $reply->header->rcode($rcode); - $reply->header->aa(@$ans ? 1 : 0); - $reply->header->id($request->header->id); - $reply->{answer} = $ans if $ans; - $reply->{authority} = $auth if $auth; - $reply->{additional} = $add if $add; - - if ($wait) { - # reply_handler() asked us to delay sending this reply until - # another reply with $wait == 1 is generated or a timeout - # occurs. - if (@delayed_response) { - # A delayed reply is already queued, so we can now send - # both the delayed reply and the current reply. - send_delayed_response(); - return $reply; - } elsif ($no_more_waiting) { - # It was determined before that there is no point in - # waiting for "accompanying" queries. Thus, send the - # current reply immediately. - return $reply; - } else { - # No delayed reply is queued and the client is expected - # to send an "accompanying" query shortly. Do not send - # the current reply right now, just save it for later - # and wait for an "accompanying" query to be received. - @delayed_response = ($reply, $peer); - $timeout = 0.5; - return; - } - } else { - # Send reply immediately. - return $reply; - } -} - -sub send_delayed_response { - my ($reply, $peer) = @delayed_response; - # Truncation to 512 bytes is required for triggering "NS explosion" on - # builds without IPv6 support - $udpsock->send($reply->data(512), 0, $peer); - undef @delayed_response; - undef $timeout; -} - -# Main -my $rin; -my $rout; -for (;;) { - $rin = ''; - vec($rin, fileno($udpsock), 1) = 1; - - select($rout = $rin, undef, undef, $timeout); - - if (vec($rout, fileno($udpsock), 1)) { - my ($buf, $peer, $reply); - $udpsock->recv($buf, 512); - $peer = $udpsock->peername(); - $reply = handleUDP($buf, $peer); - # Truncation to 512 bytes is required for triggering "NS - # explosion" on builds without IPv6 support - $udpsock->send($reply->data(512), 0, $peer) if $reply; - } else { - # An "accompanying" query was expected to come in, but did not. - # Assume the client never sends "accompanying" queries to - # prevent pointlessly waiting for them ever again. - $no_more_waiting = 1; - # Send the delayed reply to the query which caused us to wait. - send_delayed_response(); - } -} diff --git a/bin/tests/system/reclimit/ans2/ans.py b/bin/tests/system/reclimit/ans2/ans.py new file mode 100644 index 00000000000..25847c21b76 --- /dev/null +++ b/bin/tests/system/reclimit/ans2/ans.py @@ -0,0 +1,91 @@ +""" +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 collections.abc import AsyncGenerator + +import dns.rcode + +from isctest.asyncserver import ( + ControllableAsyncDnsServer, + DnsResponseSend, + QueryContext, +) + +from ..reclimit_ans import ( + DirectExampleHandler, + FallbackNxdomainHandler, + IndirectExampleOrgHandler, + LimitControlCommand, + Ns1ExampleOrgHandler, + ReclimitHandler, + ReclimitStateHandler, + a, + is_ns1_example, + ns, +) + + +class Ns1ExampleNetHandler(ReclimitHandler): + def match(self, qctx: QueryContext) -> bool: + is_ns1_example(qctx.qname, "net") + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + current_ns_number = int(qctx.qname.labels[1]) + next_ns_block_start = (current_ns_number + 1) * 16 + for offset in range(1, 16): + target_ns_number = next_ns_block_start + offset + qctx.response.authority.append( + ns( + f"{current_ns_number}.example.net.", + f"ns1.{target_ns_number}.example.net.", + ) + ) + + # XXX: Perl version truncates the responses at 512 bytes to force NS explosion on non-IPv6 builds. + # Mimic this to have packet-for-packet compatibility, but remove before merge as non-IPv6 builds + # are no longer a thing, I think. + for offset in range(1, 16): + target_ns_number = next_ns_block_start + offset + qctx.response.additional.append( + a(f"ns1.{target_ns_number}.example.net.", 7) + ) + + # You would think that this was incorrect and end up with one A record over the 512 byte limit, + # but thats's not the case, because Perl's DNS library has more agressive name compression than dnspython, + # so this ends up working out. + if len(qctx.response.to_wire()) > 512: + break + + yield DnsResponseSend(qctx.response, authoritative=False) + + +def main() -> None: + server = ControllableAsyncDnsServer( + default_aa=True, default_rcode=dns.rcode.NOERROR + ) + server.install_response_handlers( + state_handler := ReclimitStateHandler(indirect_send_response_default=False), + DirectExampleHandler(state_handler), + IndirectExampleOrgHandler(state_handler), + Ns1ExampleOrgHandler(state_handler), + Ns1ExampleNetHandler(state_handler), + FallbackNxdomainHandler(state_handler), + ) + server.install_control_command(LimitControlCommand(state_handler)) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/reclimit/tests.sh b/bin/tests/system/reclimit/tests.sh index 2dae19bd08a..8c7fdb472bd 100644 --- a/bin/tests/system/reclimit/tests.sh +++ b/bin/tests/system/reclimit/tests.sh @@ -76,7 +76,6 @@ echo_i "set max-recursion-depth=12" n=$((n + 1)) echo_i "attempt excessive-depth lookup ($n)" ret=0 -echo "1000" >ans2/ans.limit set_limit 10.53.0.2 1000 $n set_limit 10.53.0.4 1000 $n dig_with_opts @10.53.0.2 reset >/dev/null || ret=1 @@ -92,7 +91,6 @@ status=$((status + ret)) n=$((n + 1)) echo_i "attempt permissible lookup ($n)" ret=0 -echo "12" >ans2/ans.limit set_limit 10.53.0.2 12 $n set_limit 10.53.0.4 12 $n ns3_reset @@ -111,7 +109,6 @@ echo_i "set max-recursion-depth=5" n=$((n + 1)) echo_i "attempt excessive-depth lookup ($n)" ret=0 -echo "12" >ans2/ans.limit set_limit 10.53.0.2 12 $n cp ns3/named2.conf ns3/named.conf ns3_reset @@ -128,7 +125,6 @@ status=$((status + ret)) n=$((n + 1)) echo_i "attempt permissible lookup ($n)" ret=0 -echo "5" >ans2/ans.limit set_limit 10.53.0.2 5 $n set_limit 10.53.0.4 5 $n ns3_reset @@ -147,7 +143,6 @@ echo_i "set max-recursion-depth=100, max-recursion-queries=50" n=$((n + 1)) echo_i "attempt excessive-queries lookup ($n)" ret=0 -echo "13" >ans2/ans.limit set_limit 10.53.0.2 13 $n set_limit 10.53.0.4 13 $n cp ns3/named3.conf ns3/named.conf @@ -171,7 +166,6 @@ status=$((status + ret)) n=$((n + 1)) echo_i "attempt permissible lookup ($n)" ret=0 -echo "12" >ans2/ans.limit set_limit 10.53.0.2 12 $n ns3_reset dig_with_opts @10.53.0.2 reset >/dev/null || ret=1 @@ -191,7 +185,6 @@ echo_i "set max-recursion-depth=100, max-recursion-queries=40" n=$((n + 1)) echo_i "attempt excessive-queries lookup ($n)" ret=0 -echo "11" >ans2/ans.limit set_limit 10.53.0.2 11 $n cp ns3/named4.conf ns3/named.conf ns3_reset @@ -212,7 +205,6 @@ status=$((status + ret)) n=$((n + 1)) echo_i "attempt permissible lookup ($n)" ret=0 -echo "9" >ans2/ans.limit set_limit 10.53.0.2 9 $n ns3_reset dig_with_opts @10.53.0.2 reset >/dev/null || ret=1