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:
parent
b47be755db
commit
23bc8430d9
@ -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()
|
||||
|
15
bin/tests/system/forward/ans11/attackSecureDomain.net3.db
Normal file
15
bin/tests/system/forward/ans11/attackSecureDomain.net3.db
Normal 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
|
@ -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;
|
||||
}
|
||||
}
|
81
bin/tests/system/forward/ans6/ans.py
Normal file
81
bin/tests/system/forward/ans6/ans.py
Normal 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()
|
@ -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))
|
||||
|
||||
|
@ -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-*",
|
||||
|
Loading…
x
Reference in New Issue
Block a user