1
0
mirror of https://gitlab.isc.org/isc-projects/bind9.git synced 2025-04-18 09:44:09 +03:00

Use isctest.asyncserver in the "forward" test

Replace the custom DNS servers used in the "forward" system test with
new code based on the isctest.asyncserver module.

For ans6, instead of configuring the responses to send at runtime, set
them up when the server is started.  Make sure the server supports
toggling response sending at runtime to enable simulating forwarder
timeouts as required by one of the checks.

For ans11, put most of the responses to be provided by that server into
a zone file, only retaining code modifying zone-based answers in the
form of a response handler, to improve code readability.  Use explicit
domain names instead of variables as that server only handles a single
domain and fixed strings improve readability in this case.  Make sure
the server supports toggling response sending at runtime to enable
simulating forwarder timeouts as required by one of the checks.

Migrate sendcmd() and its uses to the new way of sending control queries
to custom servers used in system tests.
This commit is contained in:
Michał Kępień 2025-04-11 09:18:50 -05:00
parent b47be755db
commit 23bc8430d9
No known key found for this signature in database
6 changed files with 151 additions and 840 deletions

View File

@ -1,272 +1,58 @@
# 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.
"""
Copyright (C) Internet Systems Consortium, Inc. ("ISC")
from __future__ import print_function
import os
import sys
import signal
import socket
import select
import struct
from datetime import datetime, timedelta
import time
import functools
SPDX-License-Identifier: MPL-2.0
import dns, dns.message, dns.query, dns.flags
from dns.rdatatype import *
from dns.rdataclass import *
from dns.rcode import *
from dns.name import *
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 typing import AsyncGenerator
import dns.rdatatype
from isctest.asyncserver import (
ControllableAsyncDnsServer,
DnsResponseSend,
DomainHandler,
QueryContext,
ResponseAction,
ToggleResponsesCommand,
)
# Log query to file
def logquery(type, qname):
with open("qlog", "a") as f:
f.write("%s %s\n", type, qname)
class ExtraAnswersHandler(DomainHandler):
"""
Answer from zone data, inserting extra RRsets into responses to A queries.
"""
domains = ["attackSecureDomain.net3."]
# Create a UDP listener
def udp_listen(ip, port, is_ipv6=False):
try:
udp = socket.socket(
socket.AF_INET6 if is_ipv6 else socket.AF_INET, socket.SOCK_DGRAM
)
try:
udp.bind((ip, port))
except:
udp.close()
udp = None
except:
udp = None
if udp is None and not is_ipv6:
raise socket.error("Can not create an IPv4 UDP listener")
return udp
# Create a TCP listener
def tcp_listen(ip, port, is_ipv6=False):
try:
tcp = socket.socket(
socket.AF_INET6 if is_ipv6 else socket.AF_INET, socket.SOCK_STREAM
)
try:
tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp.bind((ip, port))
tcp.listen(100)
except:
tcp.close()
tcp = None
except:
tcp = None
if tcp is None and not is_ipv6:
raise socket.error("Can not create an IPv4 TCP listener")
return tcp
############################################################################
# Control channel - send "1" or "0" to enable or disable the "silent" mode.
############################################################################
silent = False
def ctrl_channel(msg):
global silent
msg = msg.splitlines().pop(0)
print("Received control message: %s" % msg)
if len(msg) != 1:
return
if silent:
if msg == b"0":
silent = False
print("Silent mode was disabled")
else:
if msg == b"1":
silent = True
print("Silent mode was enabled")
############################################################################
# Respond to a DNS query.
############################################################################
def create_response(msg):
m = dns.message.from_wire(msg)
qname = m.question[0].name.to_text()
rrtype = m.question[0].rdtype
typename = dns.rdatatype.to_text(rrtype)
with open("query.log", "a") as f:
f.write("%s %s\n" % (typename, qname))
print("%s %s" % (typename, qname), end=" ")
r = dns.message.make_response(m)
r.set_rcode(NOERROR)
if rrtype == A:
tld = qname.split(".")[-2] + "."
ns = "local." + tld
r.answer.append(dns.rrset.from_text(qname, 300, IN, A, "10.53.0.11"))
r.answer.append(dns.rrset.from_text(tld, 300, IN, NS, "local." + tld))
r.additional.append(dns.rrset.from_text(ns, 300, IN, A, "10.53.0.11"))
elif rrtype == NS:
r.answer.append(dns.rrset.from_text(qname, 300, IN, NS, "."))
elif rrtype == SOA:
r.answer.append(dns.rrset.from_text(qname, 300, IN, SOA, ". . 0 0 0 0 0"))
else:
r.authority.append(dns.rrset.from_text(qname, 300, IN, SOA, ". . 0 0 0 0 0"))
r.flags |= dns.flags.AA
return r
def sigterm(signum, frame):
print("Shutting down now...")
os.remove("ans.pid")
running = False
sys.exit(0)
############################################################################
# Main
#
# Set up responder and control channel, open the pid file, and start
# the main loop, listening for queries on the query channel or commands
# on the control channel and acting on them.
############################################################################
ip4 = "10.53.0.11"
ip6 = "fd92:7065:b8e:ffff::11"
try:
port = int(os.environ["PORT"])
except:
port = 5300
try:
ctrlport = int(os.environ["EXTRAPORT1"])
except:
ctrlport = 5300
ctrl4_tcp = tcp_listen(ip4, ctrlport)
query4_udp = udp_listen(ip4, port)
query6_udp = udp_listen(ip6, port, is_ipv6=True)
query4_tcp = tcp_listen(ip4, port)
query6_tcp = tcp_listen(ip6, port, is_ipv6=True)
havev6 = query6_udp is not None and query6_tcp is not None
signal.signal(signal.SIGTERM, sigterm)
f = open("ans.pid", "w")
pid = os.getpid()
print(pid, file=f)
f.close()
running = True
print("Listening on %s port %d" % (ip4, ctrlport))
print("Listening on %s port %d" % (ip4, port))
if havev6:
print("Listening on %s port %d" % (ip6, port))
print("Ctrl-c to quit")
if havev6:
input = [ctrl4_tcp, query4_udp, query6_udp, query4_tcp, query6_tcp]
else:
input = [ctrl4_tcp, query4_udp, query4_tcp]
hung_conns = []
while running:
try:
inputready, outputready, exceptready = select.select(input, [], [])
except select.error as e:
break
except socket.error as e:
break
except KeyboardInterrupt:
break
for s in inputready:
if s == ctrl4_tcp:
print("Control channel connected")
conn = None
try:
# Handle control channel input
conn, addr = s.accept()
msg = conn.recv(1)
if msg:
ctrl_channel(msg)
conn.close()
except s.timeout:
pass
if conn:
conn.close()
elif s == query4_tcp or s == query6_tcp:
print(
"TCP query received on %s" % (ip4 if s == query4_tcp else ip6), end=" "
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[ResponseAction, None]:
if qctx.qtype == dns.rdatatype.A:
ns_rrset = dns.rrset.from_text(
"net3.", 300, qctx.qclass, dns.rdatatype.NS, "local.net3."
)
conn = None
try:
# Handle incoming queries
conn, addr = s.accept()
if not silent:
# get TCP message length
msg = conn.recv(2)
if len(msg) != 2:
print("NO RESPONSE (can not read the message length)")
conn.close()
continue
length = struct.unpack(">H", msg[:2])[0]
msg = conn.recv(length)
if len(msg) != length:
print("NO RESPONSE (can not read the message)")
conn.close()
continue
rsp = create_response(msg)
if rsp:
print(dns.rcode.to_text(rsp.rcode()))
wire = rsp.to_wire()
conn.send(struct.pack(">H", len(wire)))
conn.send(wire)
else:
print("NO RESPONSE (can not create a response)")
else:
# Do not respond and hang the connection.
print("NO RESPONSE (silent mode)")
hung_conns.append(conn)
continue
except socket.error as e:
print("NO RESPONSE (error: %s)" % str(e))
if conn:
conn.close()
elif s == query4_udp or s == query6_udp:
print(
"UDP query received on %s" % (ip4 if s == query4_udp else ip6), end=" "
qctx.response.answer.append(ns_rrset)
a_rrset = dns.rrset.from_text(
"local.net3.", 300, qctx.qclass, dns.rdatatype.A, "10.53.0.11"
)
# Handle incoming queries
msg = s.recvfrom(65535)
if not silent:
rsp = create_response(msg[0])
if rsp:
print(dns.rcode.to_text(rsp.rcode()))
s.sendto(rsp.to_wire(), msg[1])
else:
print("NO RESPONSE (can not create a response)")
else:
# Do not respond.
print("NO RESPONSE (silent mode)")
if not running:
break
qctx.response.additional.append(a_rrset)
yield DnsResponseSend(qctx.response)
def main() -> None:
server = ControllableAsyncDnsServer(commands=[ToggleResponsesCommand])
server.install_response_handler(ExtraAnswersHandler())
server.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,15 @@
; 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.
$TTL 86400
@ SOA . . 0 0 0 0 0
@ NS .
@ A 10.53.0.11

View File

@ -1,562 +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.
#
# This is the name server from hell. It provides canned
# responses based on pattern matching the queries, and
# can be reprogrammed on-the-fly over a TCP connection.
#
# The server listens for queries on port 5300 (or PORT).
#
# The server listens for control connections on port 5301 (or EXTRAPORT1).
#
# A control connection is a TCP stream of lines like
#
# /pattern/
# name ttl type rdata
# name ttl type rdata
# ...
# /pattern/
# name ttl type rdata
# name ttl type rdata
# ...
#
# There can be any number of patterns, each associated
# with any number of response RRs. Each pattern is a
# Perl regular expression. If an empty pattern ("//") is
# received, the server will ignore all incoming queries (TCP
# connections will still be accepted, but both UDP queries
# and TCP queries will not be responded to). If a non-empty
# pattern is then received over the same control connection,
# default behavior is restored.
#
# Each incoming query is converted into a string of the form
# "qname qtype" (the printable query domain name, space,
# printable query type) and matched against each pattern.
#
# The first pattern matching the query is selected, and
# the RR following the pattern line are sent in the
# answer section of the response.
#
# Each new control connection causes the current set of
# patterns and responses to be cleared before adding new
# ones.
#
# The server handles UDP and TCP queries. Zone transfer
# responses work, but must fit in a single 64 k message.
#
# Now you can add TSIG, just specify key/key data with:
#
# /pattern <key> <key_data>/
# name ttl type rdata
# name ttl type rdata
#
# Note that this data will still be sent with any request for
# pattern, only this data will be signed. Currently, this is only
# done for TCP.
#
# /pattern bad-id <key> <key_data>/
# /pattern bad-id/
#
# will add 50 to the message id of the response.
use IO::File;
use IO::Socket;
use Data::Dumper;
use Net::DNS;
use Net::DNS::Packet;
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;
# We default to listening on 10.53.0.2 for historical reasons
# XXX: we should also be able to specify IPv6
my $server_addr = "10.53.0.6";
if (@ARGV > 0) {
$server_addr = @ARGV[0];
}
my $mainport = int($ENV{'PORT'});
if (!$mainport) { $mainport = 5300; }
my $ctrlport = int($ENV{'EXTRAPORT1'});
if (!$ctrlport) { $ctrlport = 5301; }
print "listening on $server_addr:$mainport,$ctrlport.\n";
print "Using Net::DNS $Net::DNS::VERSION\n";
# XXX: we should also be able to set the port numbers to listen on.
my $ctlsock = IO::Socket::INET->new(LocalAddr => "$server_addr",
LocalPort => $ctrlport, Proto => "tcp", Listen => 5, Reuse => 1) or die "$!";
my $udpsock = IO::Socket::INET->new(LocalAddr => "$server_addr",
LocalPort => $mainport, Proto => "udp", Reuse => 1) or die "$!";
my $tcpsock = IO::Socket::INET->new(LocalAddr => "$server_addr",
LocalPort => $mainport, Proto => "tcp", Listen => 5, 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 @answers = ();
my @rules;
my $udphandler;
my $tcphandler;
sub handleUDP {
my ($buf) = @_;
my $request;
if ($Net::DNS::VERSION > 0.68) {
$request = new Net::DNS::Packet(\$buf, 0);
$@ and die $@;
} else {
my $err;
($request, $err) = new Net::DNS::Packet(\$buf, 0);
$err and die $err;
}
my @questions = $request->question;
my $qname = $questions[0]->qname;
my $qtype = $questions[0]->qtype;
my $qclass = $questions[0]->qclass;
my $id = $request->header->id;
my $packet = new Net::DNS::Packet($qname, $qtype, $qclass);
$packet->header->qr(1);
$packet->header->aa(1);
$packet->header->id($id);
# get the existing signature if any, and clear the additional section
my $prev_tsig;
while (my $rr = $request->pop("additional")) {
$prev_tsig = $rr if ($rr->type eq "TSIG");
}
my $r;
my $answers = 0;
my $match;
my $key_name;
my $key_data;
foreach $r (@rules) {
my $pattern = $r->{pattern};
($match, $key_name, $key_data) = split(/ /,$pattern);
print "[handleUDP] $match, $key_name, $key_data\n";
$match =~ tr/\// /;
if ("$qname $qtype" =~ /$match/) {
my $a;
foreach $a (@{$r->{answer}}) {
my $resp;
$resp = new Net::DNS::RR("$qname $a");
$packet->push("answer", $resp);
++$answers;
}
last;
}
}
if ($answers eq 0) {
my $soa;
$soa = new Net::DNS::RR("$qname 300 IN SOA . . 0 0 0 0 0");
$packet->push("authority", $soa)
}
if (defined($key_name) && defined($key_data)) {
my $tsig;
# Sign the packet
print " Signing the response with " .
"$key_name/$key_data\n";
if ($Net::DNS::VERSION < 0.69) {
$tsig = Net::DNS::RR->new(
"$key_name TSIG $key_data");
} else {
$tsig = Net::DNS::RR->new(
name => $key_name,
type => 'TSIG',
key => $key_data);
}
# These kluges are necessary because Net::DNS
# doesn't know how to sign responses. We
# clear compnames so that the TSIG key and
# algorithm name won't be compressed, and
# add one to arcount because the signing
# function will attempt to decrement it,
# which is incorrect in a response. Finally
# we set request_mac to the previous digest.
$packet->{"compnames"} = {}
if ($Net::DNS::VERSION < 0.70);
$packet->{"header"}{"arcount"} += 1
if ($Net::DNS::VERSION < 0.70);
if (defined($prev_tsig)) {
if ($Net::DNS::VERSION < 0.73) {
my $rmac = pack('n H*',
length($prev_tsig->mac)/2,
$prev_tsig->mac);
$tsig->{"request_mac"} =
unpack("H*", $rmac);
} else {
$tsig->request_mac(
$prev_tsig->mac);
}
}
$packet->sign_tsig($tsig);
}
#$packet->print;
return $packet->data;
}
# namelen:
# given a stream of data, reads a DNS-formatted name and returns its
# total length, thus making it possible to skip past it.
sub namelen {
my ($data) = @_;
my $len = 0;
my $label_len = 0;
do {
$label_len = unpack("c", $data);
$data = substr($data, $label_len + 1);
$len += $label_len + 1;
} while ($label_len != 0);
return ($len);
}
# packetlen:
# given a stream of data, reads a DNS wire-format packet and returns
# its total length, making it possible to skip past it.
sub packetlen {
my ($data) = @_;
my $q;
my $rr;
my $header;
my $offset;
#
# decode/encode were introduced in Net::DNS 0.68
# parse is no longer a method and calling it here makes perl croak.
#
my $decode = 0;
$decode = 1 if ($Net::DNS::VERSION >= 0.68);
if ($decode) {
($header, $offset) = Net::DNS::Header->decode(\$data);
} else {
($header, $offset) = Net::DNS::Header->parse(\$data);
}
for (1 .. $header->qdcount) {
if ($decode) {
($q, $offset) =
Net::DNS::Question->decode(\$data, $offset);
} else {
($q, $offset) =
Net::DNS::Question->parse(\$data, $offset);
}
}
for (1 .. $header->ancount) {
if ($decode) {
($q, $offset) = Net::DNS::RR->decode(\$data, $offset);
} else {
($q, $offset) = Net::DNS::RR->parse(\$data, $offset);
}
}
for (1 .. $header->nscount) {
if ($decode) {
($q, $offset) = Net::DNS::RR->decode(\$data, $offset);
} else {
($q, $offset) = Net::DNS::RR->parse(\$data, $offset);
}
}
for (1 .. $header->arcount) {
if ($decode) {
($q, $offset) = Net::DNS::RR->decode(\$data, $offset);
} else {
($q, $offset) = Net::DNS::RR->parse(\$data, $offset);
}
}
return $offset;
}
# sign_tcp_continuation:
# This is a hack to correct the problem that Net::DNS has no idea how
# to sign multiple-message TCP responses. Several data that are included
# in the digest when signing a query or the first message of a response are
# omitted when signing subsequent messages in a TCP stream.
#
# Net::DNS::Packet->sign_tsig() has the ability to use a custom signing
# function (specified by calling Packet->sign_func()). We use this
# function as the signing function for TCP continuations, and it removes
# the unwanted data from the digest before calling the default sign_hmac
# function.
sub sign_tcp_continuation {
my ($key, $data) = @_;
# copy out first two bytes: size of the previous MAC
my $rmacsize = unpack("n", $data);
$data = substr($data, 2);
# copy out previous MAC
my $rmac = substr($data, 0, $rmacsize);
$data = substr($data, $rmacsize);
# try parsing out the packet information
my $plen = packetlen($data);
my $pdata = substr($data, 0, $plen);
$data = substr($data, $plen);
# remove the keyname, ttl, class, and algorithm name
$data = substr($data, namelen($data));
$data = substr($data, 6);
$data = substr($data, namelen($data));
# preserve the TSIG data
my $tdata = substr($data, 0, 8);
# prepare a new digest and sign with it
$data = pack("n", $rmacsize) . $rmac . $pdata . $tdata;
return Net::DNS::RR::TSIG::sign_hmac($key, $data);
}
sub handleTCP {
my ($buf) = @_;
my $request;
if ($Net::DNS::VERSION > 0.68) {
$request = new Net::DNS::Packet(\$buf, 0);
$@ and die $@;
} else {
my $err;
($request, $err) = new Net::DNS::Packet(\$buf, 0);
$err and die $err;
}
my @questions = $request->question;
my $qname = $questions[0]->qname;
my $qtype = $questions[0]->qtype;
my $qclass = $questions[0]->qclass;
my $id = $request->header->id;
my $opaque;
my $packet = new Net::DNS::Packet($qname, $qtype, $qclass);
$packet->header->qr(1);
$packet->header->aa(1);
$packet->header->id($id);
# get the existing signature if any, and clear the additional section
my $prev_tsig;
my $signer;
my $continuation = 0;
if ($Net::DNS::VERSION < 0.81) {
while (my $rr = $request->pop("additional")) {
if ($rr->type eq "TSIG") {
$prev_tsig = $rr;
}
}
}
my @results = ();
my $count_these = 0;
my $r;
my $answers = 0;
my $match;
my $key_name;
my $key_data;
my $tname;
foreach $r (@rules) {
my $pattern = $r->{pattern};
my($match, $key_name, $key_data, $tname) = split(/ /,$pattern);
print "[handleTCP] $match, $key_name, $key_data, $tname \n";
$match =~ tr/\// /;
if ("$qname $qtype" =~ /$match/) {
$count_these++;
my $a;
foreach $a (@{$r->{answer}}) {
my $resp;
$resp = new Net::DNS::RR("$qname $a");
$packet->push("answer", $resp);
++$answers;
}
last;
}
}
if ($answers eq 0) {
my $soa;
$soa = new Net::DNS::RR("$qname 300 SOA . . 0 0 0 0 0");
$packet->push("authority", $soa)
}
if (defined($key_name) && $key_name eq "bad-id") {
$packet->header->id(($id+50)%0xffff);
$key_name = $key_data;
($key_data, $tname) = split(/ /,$tname)
}
if (defined($key_name) && defined($key_data)) {
my $tsig;
# sign the packet
print " Signing the data with " .
"$key_name/$key_data\n";
if ($Net::DNS::VERSION < 0.69) {
$tsig = Net::DNS::RR->new(
"$key_name TSIG $key_data");
} elsif ($Net::DNS::VERSION >= 0.81 &&
$continuation) {
} elsif ($Net::DNS::VERSION >= 0.75 &&
$continuation) {
$tsig = $prev_tsig;
} else {
$tsig = Net::DNS::RR->new(
name => $key_name,
type => 'TSIG',
key => $key_data);
}
# These kluges are necessary because Net::DNS
# doesn't know how to sign responses. We
# clear compnames so that the TSIG key and
# algorithm name won't be compressed, and
# add one to arcount because the signing
# function will attempt to decrement it,
# which is incorrect in a response. Finally
# we set request_mac to the previous digest.
$packet->{"compnames"} = {}
if ($Net::DNS::VERSION < 0.70);
$packet->{"header"}{"arcount"} += 1
if ($Net::DNS::VERSION < 0.70);
if (defined($prev_tsig)) {
if ($Net::DNS::VERSION < 0.73) {
my $rmac = pack('n H*',
length($prev_tsig->mac)/2,
$prev_tsig->mac);
$tsig->{"request_mac"} =
unpack("H*", $rmac);
} elsif ($Net::DNS::VERSION < 0.81) {
$tsig->request_mac(
$prev_tsig->mac);
}
}
$tsig->sign_func($signer) if defined($signer);
$tsig->continuation($continuation) if
($Net::DNS::VERSION >= 0.71 &&
$Net::DNS::VERSION <= 0.74 );
if ($Net::DNS::VERSION < 0.81) {
$packet->sign_tsig($tsig);
} elsif ($continuation) {
$opaque = $packet->sign_tsig($opaque);
} else {
$opaque = $packet->sign_tsig($request);
}
$signer = \&sign_tcp_continuation
if ($Net::DNS::VERSION < 0.70);
$continuation = 1;
my $copy =
Net::DNS::Packet->new(\($packet->data));
$prev_tsig = $copy->pop("additional");
}
#$packet->print;
push(@results,$packet->data);
if ($tname eq "") {
$tname = $qname;
}
$packet = new Net::DNS::Packet($tname, $qtype, $qclass);
$packet->header->qr(1);
$packet->header->aa(1);
$packet->header->id($id);
print " A total of $count_these patterns matched\n";
return \@results;
}
# Main
my $rin;
my $rout;
for (;;) {
$rin = '';
vec($rin, fileno($ctlsock), 1) = 1;
vec($rin, fileno($tcpsock), 1) = 1;
vec($rin, fileno($udpsock), 1) = 1;
select($rout = $rin, undef, undef, undef);
if (vec($rout, fileno($ctlsock), 1)) {
warn "ctl conn";
my $conn = $ctlsock->accept;
my $rule = ();
@rules = ();
while (my $line = $conn->getline) {
chomp $line;
if ($line =~ m!^/(.*)/$!) {
if (length($1) == 0) {
$udphandler = sub { return; };
$tcphandler = sub { return; };
} else {
$udphandler = \&handleUDP;
$tcphandler = \&handleTCP;
$rule = { pattern => $1, answer => [] };
push(@rules, $rule);
}
} else {
push(@{$rule->{answer}}, $line);
}
}
$conn->close;
#print Dumper(@rules);
#print "+=+=+ $rules[0]->{'pattern'}\n";
#print "+=+=+ $rules[0]->{'answer'}->[0]->{'rname'}\n";
#print "+=+=+ $rules[0]->{'answer'}->[0]\n";
} elsif (vec($rout, fileno($udpsock), 1)) {
printf "UDP request\n";
my $buf;
$udpsock->recv($buf, 512);
my $result = &$udphandler($buf);
if (defined($result)) {
my $num_chars = $udpsock->send($result);
print " Sent $num_chars bytes via UDP\n";
}
} elsif (vec($rout, fileno($tcpsock), 1)) {
my $conn = $tcpsock->accept;
my $buf;
for (;;) {
my $lenbuf;
my $n = $conn->sysread($lenbuf, 2);
last unless $n == 2;
my $len = unpack("n", $lenbuf);
$n = $conn->sysread($buf, $len);
last unless $n == $len;
print "TCP request\n";
my $result = &$tcphandler($buf);
if (defined($result)) {
foreach my $response (@$result) {
$len = length($response);
$n = $conn->syswrite(pack("n", $len), 2);
$n = $conn->syswrite($response, $len);
print " Sent: $n chars via TCP\n";
}
}
}
$conn->close;
}
}

View File

@ -0,0 +1,81 @@
"""
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 typing import AsyncGenerator
import dns
from isctest.asyncserver import (
ControllableAsyncDnsServer,
DnsResponseSend,
QueryContext,
ResponseAction,
ResponseHandler,
ToggleResponsesCommand,
)
class ChaseDsHandler(ResponseHandler):
"""
Yield responses triggering DS chasing logic in `named`. These responses
cannot be served from a static zone file because most of them need to be
generated dynamically so that the owner name of the returned RRset is
copied from the QNAME sent by the client:
- A/AAAA queries for `ns1.sld.tld.` elicit responses with IP addresses,
- all NS queries below `sld.tld.` elicit a delegation to `ns1.sld.tld.`,
- all other queries elicit a negative response with a common SOA record.
"""
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[ResponseAction, None]:
ns1_sld_tld = dns.name.from_text("ns1.sld.tld.")
sld_tld = dns.name.from_text("sld.tld.")
if qctx.qname == ns1_sld_tld and qctx.qtype == dns.rdatatype.A:
response_type = dns.rdatatype.A
response_rdata = "10.53.0.2"
response_section = qctx.response.answer
elif qctx.qname == ns1_sld_tld and qctx.qtype == dns.rdatatype.AAAA:
response_type = dns.rdatatype.AAAA
response_rdata = "fd92:7065:b8e:ffff::2"
response_section = qctx.response.answer
elif qctx.qname.is_subdomain(sld_tld) and qctx.qtype == dns.rdatatype.NS:
response_type = dns.rdatatype.NS
response_rdata = "ns1.sld.tld."
response_section = qctx.response.answer
else:
response_type = dns.rdatatype.SOA
response_rdata = ". . 0 0 0 0 0"
response_section = qctx.response.authority
qctx.response.set_rcode(dns.rcode.NOERROR)
qctx.response.use_edns(None)
response_rrset = dns.rrset.from_text(
qctx.qname, 300, qctx.qclass, response_type, response_rdata
)
response_section.append(response_rrset)
yield DnsResponseSend(qctx.response, authoritative=True)
def main() -> None:
server = ControllableAsyncDnsServer([ToggleResponsesCommand])
server.install_response_handler(ChaseDsHandler())
server.run()
if __name__ == "__main__":
main()

View File

@ -21,7 +21,7 @@ dig_with_opts() (
)
sendcmd() (
send "$1" "$EXTRAPORT1"
dig_with_opts "@${1}" "${2}._control." TXT +time=5 +tries=1 +tcp >/dev/null 2>&1
)
rndccmd() {
@ -260,7 +260,7 @@ n=$((n + 1))
echo_i "checking that a forwarder timeout prevents it from being reused in the same fetch context ($n)"
ret=0
# Make ans6 receive queries without responding to them.
echo "//" | sendcmd 10.53.0.6
sendcmd 10.53.0.6 "disable.send-responses"
# Query for a record in a zone which is forwarded to a non-responding forwarder
# and is delegated from the root to check whether the forwarder will be retried
# when a delegation is encountered after falling back to full recursive
@ -271,6 +271,7 @@ dig_with_opts txt.example7. txt @$f1 >dig.out.$n.f1 || ret=1
start_pattern="sending packet from [^ ]* to 10\.53\.0\.6"
retry_quiet 5 wait_for_log ns3/named.run "$start_pattern"
check_sent 1 ns3/named.run "$start_pattern" ";txt\.example7\.[[:space:]]*IN[[:space:]]*TXT$" || ret=1
sendcmd 10.53.0.6 "enable.send-responses"
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))
@ -310,15 +311,6 @@ grep "status: SERVFAIL" dig.out.$n >/dev/null || ret=1
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))
# Prepare ans6 for the chasing DS tests.
sendcmd 10.53.0.6 <<EOF
/ns1.sld.tld/A/
300 A 10.53.0.2
/sld.tld/NS/
300 NS ns1.sld.tld.
/sld.tld/
EOF
n=$((n + 1))
echo_i "checking switch from forwarding to normal resolution while chasing DS ($n)"
ret=0
@ -333,7 +325,7 @@ status=$((status + ret))
# See [GL #3129].
# Enable silent mode for ans11.
echo "1" | sendcmd 10.53.0.11
sendcmd 10.53.0.11 "disable.send-responses"
n=$((n + 1))
echo_i "checking the handling of hung DS fetch while chasing DS ($n)"
ret=0
@ -347,7 +339,7 @@ nextpart ns3/named.run >/dev/null
dig_with_opts @$f1 xxx.yyy.sld.tld ds >dig.out.$n.f1 || ret=1
grep "status: SERVFAIL" dig.out.$n.f1 >/dev/null || ret=1
# Disable silent mode for ans11.
echo "0" | sendcmd 10.53.0.11
sendcmd 10.53.0.11 "enable.send-responses"
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))

View File

@ -16,7 +16,6 @@ pytestmark = pytest.mark.extra_artifacts(
"dig.out.*",
"statschannel.out.*",
"ans*/ans.run",
"ans*/query.log",
"ns*/trusted.conf",
"ns1/K*",
"ns1/dsset-*",