diff --git a/bin/tests/system/chain/ans3/ans.pl b/bin/tests/system/chain/ans3/ans.pl deleted file mode 100644 index 434eaa9809..0000000000 --- a/bin/tests/system/chain/ans3/ans.pl +++ /dev/null @@ -1,167 +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 Getopt::Long; -use Net::DNS::Nameserver; - -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; }; -sub term { }; - -$SIG{INT} = \&rmpid; -if ($Net::DNS::VERSION > 1.41) { - $SIG{TERM} = \&term; -} else { - $SIG{TERM} = \&rmpid; -} - -my $localaddr = "10.53.0.3"; - -my $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $verbose = 0; -my $ttl = 60; -my $zone = "example.broken"; -my $nsname = "ns3.$zone"; -my $synth = "synth-then-dname.$zone"; -my $synth2 = "synth2-then-dname.$zone"; - -sub reply_handler { - my ($qname, $qclass, $qtype, $peerhost, $query, $conn) = @_; - my ($rcode, @ans, @auth, @add); - - print ("request: $qname/$qtype\n"); - STDOUT->flush(); - - if ($qname eq "example.broken") { - if ($qtype eq "SOA") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass SOA . . 0 0 0 0 0"); - push @ans, $rr; - } elsif ($qtype eq "NS") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass NS $nsname"); - push @ans, $rr; - $rr = new Net::DNS::RR("$nsname $ttl $qclass A $localaddr"); - push @add, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname eq "cname-to-$synth2") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name.$synth2"); - push @ans, $rr; - $rr = new Net::DNS::RR("name.$synth2 $ttl $qclass CNAME name"); - push @ans, $rr; - $rr = new Net::DNS::RR("$synth2 $ttl $qclass DNAME ."); - push @ans, $rr; - $rcode = "NOERROR"; - } elsif ($qname eq "$synth" || $qname eq "$synth2") { - if ($qtype eq "DNAME") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME ."); - push @ans, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname eq "name.$synth") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name."); - push @ans, $rr; - $rr = new Net::DNS::RR("$synth $ttl $qclass DNAME ."); - push @ans, $rr; - $rcode = "NOERROR"; - } elsif ($qname eq "name.$synth2") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name."); - push @ans, $rr; - $rr = new Net::DNS::RR("$synth2 $ttl $qclass DNAME ."); - push @ans, $rr; - $rcode = "NOERROR"; - # The following three code branches referring to the "example.dname" - # zone are necessary for the resolver variant of the CVE-2021-25215 - # regression test to work. A named instance cannot be used for - # serving the DNAME records below as a version of BIND vulnerable to - # CVE-2021-25215 would crash while answering the queries asked by - # the tested resolver. - } elsif ($qname eq "ns3.example.dname") { - if ($qtype eq "A") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass A 10.53.0.3"); - push @ans, $rr; - } - if ($qtype eq "AAAA") { - my $rr = new Net::DNS::RR("example.dname. $ttl $qclass SOA . . 0 0 0 0 $ttl"); - push @auth, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname eq "self.example.self.example.dname") { - my $rr = new Net::DNS::RR("self.example.dname. $ttl $qclass DNAME dname."); - push @ans, $rr; - $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME self.example.dname."); - push @ans, $rr; - $rcode = "NOERROR"; - } elsif ($qname eq "self.example.dname") { - if ($qtype eq "DNAME") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME dname."); - push @ans, $rr; - } - $rcode = "NOERROR"; - # The next few branches produce a zone with an illegal NS below a DNAME. - } elsif ($qname eq "jeff.dname") { - if ($qtype eq "SOA") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass SOA . . 0 0 0 0 0"); - push @ans, $rr; - } elsif ($qtype eq "NS") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass NS ns.jeff.dname."); - push @ans, $rr; - $rr = new Net::DNS::RR("$nsname $ttl $qclass A $localaddr"); - push @add, $rr; - } elsif ($qtype eq "DNAME") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME mutt.example."); - push @ans, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname eq "ns.jeff.dname") { - if ($qtype eq "A") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass A 10.53.0.3"); - push @ans, $rr; - } elsif ($qtype eq "AAAA") { - my $rr = new Net::DNS::RR("jeff.dname. $ttl $qclass SOA . . 0 0 0 0 $ttl"); - push @auth, $rr; - } - $rcode = "NOERROR"; - } else { - $rcode = "REFUSED"; - } - return ($rcode, \@ans, \@auth, \@add, { aa => 1 }); -} - -GetOptions( - 'port=i' => \$localport, - 'verbose!' => \$verbose, -); - -my $ns = Net::DNS::Nameserver->new( - LocalAddr => $localaddr, - LocalPort => $localport, - ReplyHandler => \&reply_handler, - Verbose => $verbose, -); - -if ($Net::DNS::VERSION >= 1.42) { - $ns->start_server(); - select(undef, undef, undef, undef); - $ns->stop_server(); - unlink "ans.pid"; -} else { - $ns->main_loop; -} diff --git a/bin/tests/system/chain/ans3/ans.py b/bin/tests/system/chain/ans3/ans.py new file mode 100755 index 0000000000..4a87dfc89c --- /dev/null +++ b/bin/tests/system/chain/ans3/ans.py @@ -0,0 +1,122 @@ +""" +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.name +import dns.rdataclass +import dns.rdatatype +import dns.rrset +import dns.zone + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + DomainHandler, + QueryContext, + ResponseAction, +) + +try: + dns_namerelation_equal = dns.name.NameRelation.EQUAL + dns_namerelation_subdomain = dns.name.NameRelation.SUBDOMAIN +except AttributeError: # dnspython < 2.0.0 compat + dns_namerelation_equal = dns.name.NAMERELN_EQUAL # type: ignore + dns_namerelation_subdomain = dns.name.NAMERELN_SUBDOMAIN # type: ignore + + +def get_dname_rrset_at_name( + zone: dns.zone.Zone, name: dns.name.Name +) -> dns.rrset.RRset: + node = zone.get_node(name) + assert node + dname = node.get_rdataset(dns.rdataclass.IN, dns.rdatatype.DNAME) + assert dname + rrset = dns.rrset.RRset(name, dname.rdclass, dname.rdtype) + rrset.update(dname) + return rrset + + +class CnameThenDnameHandler(DomainHandler): + """ + For certain trigger QNAMEs, insert a DNAME RRset after the CNAME chain + prepared from zone data. + """ + + domains = ["example.broken."] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + assert qctx.zone + assert qctx.zone.origin + + relative_qname = qctx.qname.relativize(qctx.zone.origin) + if relative_qname.labels[-1].endswith(b"-then-dname"): + last_cname = qctx.response.answer[-1] + assert last_cname.rdtype == dns.rdatatype.CNAME + dname_owner = last_cname.name.parent() + dname_rrset = get_dname_rrset_at_name(qctx.zone, dname_owner) + qctx.response.answer.append(dname_rrset) + + yield DnsResponseSend(qctx.response, authoritative=True) + + +class Cve202125215(DomainHandler): + """ + Attempt to trigger the resolver variant of CVE-2021-25215. A `named` + instance cannot be used for serving the DNAME records returned by this + response handler as a version of BIND 9 vulnerable to CVE-2021-25215 would + crash while answering the queries sent by the tested resolver. + """ + + domains = ["example.dname."] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + assert qctx.zone + assert qctx.zone.origin + + self_example_dname = dns.name.Name(["self"]).concatenate(qctx.zone.origin) + dname_rrset = get_dname_rrset_at_name(qctx.zone, self_example_dname) + + relation, _, _ = qctx.qname.fullcompare(self_example_dname) + + if relation in (dns_namerelation_equal, dns_namerelation_subdomain): + del qctx.response.authority[:] + qctx.response.set_rcode(dns.rcode.NOERROR) + + if relation == dns_namerelation_subdomain: + qctx.response.answer.append(dname_rrset) + cname_rrset = dns.rrset.from_text( + qctx.qname, + 60, + qctx.qclass, + dns.rdatatype.CNAME, + self_example_dname.to_text(), + ) + qctx.response.answer.append(cname_rrset) + + yield DnsResponseSend(qctx.response, authoritative=True) + + +def main() -> None: + server = AsyncDnsServer(acknowledge_manual_dname_handling=True) + server.install_response_handler(CnameThenDnameHandler()) + server.install_response_handler(Cve202125215()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/chain/ans3/example.broken.db b/bin/tests/system/chain/ans3/example.broken.db new file mode 100644 index 0000000000..4c5b720de4 --- /dev/null +++ b/bin/tests/system/chain/ans3/example.broken.db @@ -0,0 +1,29 @@ +; 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. + +@ 60 SOA . . 0 0 0 0 0 + +@ 60 NS ns3 +ns3 60 A 10.53.0.3 + +cname-to-synth2-then-dname 60 CNAME name.synth2-then-dname + +name.synth-then-dname 60 CNAME name. +name.synth2-then-dname 60 CNAME name. + +; isctest.asyncserver does no special handling of DNAME records (e.g. by +; occluding names below them or synthesizing CNAMEs), so the following +; DNAME records are only returned for certain trigger QNAMEs (see +; CnameThenDnameHandler.get_responses() in ans3/ans.py) or if they are +; queried for explicitly. + +synth-then-dname 60 DNAME . +synth2-then-dname 60 DNAME . diff --git a/bin/tests/system/chain/ans3/example.dname.db b/bin/tests/system/chain/ans3/example.dname.db new file mode 100644 index 0000000000..02eecea47f --- /dev/null +++ b/bin/tests/system/chain/ans3/example.dname.db @@ -0,0 +1,24 @@ +; 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. + +@ 60 SOA . . 0 0 0 0 0 + +@ 60 NS ns3 +ns3 60 A 10.53.0.3 + +; isctest.asyncserver does no special handling of DNAME records (e.g. by +; occluding names below them or synthesizing CNAMEs), so the following +; DNAME record is only appended to responses by a custom response +; handler that attempts to trigger the resolver variant of +; CVE-2021-25215 in `named` (see Cve202125215Handler.get_responses() +; in ans3/ans.py) or when it is queried for explicitly. + +self 60 DNAME dname. diff --git a/bin/tests/system/chain/ans3/jeff.dname.db b/bin/tests/system/chain/ans3/jeff.dname.db new file mode 100644 index 0000000000..7f528f170b --- /dev/null +++ b/bin/tests/system/chain/ans3/jeff.dname.db @@ -0,0 +1,21 @@ +; 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. + +@ 60 SOA . . 0 0 0 0 0 + +@ 60 NS ns +ns 60 A 10.53.0.3 + +; isctest.asyncserver does no special handling of DNAME records (e.g. by +; occluding names below them or synthesizing CNAMEs), so the following +; DNAME record is only returned when it is queried for explicitly. + +@ 60 DNAME mutt.example. diff --git a/bin/tests/system/chain/ans4/.gitignore b/bin/tests/system/chain/ans4/.gitignore deleted file mode 100644 index a74b07aee4..0000000000 --- a/bin/tests/system/chain/ans4/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*.pyc diff --git a/bin/tests/system/chain/ans4/README.anspy b/bin/tests/system/chain/ans4/README.anspy deleted file mode 100644 index 7cb0bf09e2..0000000000 --- a/bin/tests/system/chain/ans4/README.anspy +++ /dev/null @@ -1,24 +0,0 @@ -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. - -REQUIREMENTS -ans.py requires at least dnspython 1.12.0. - -"ans.py" is a fairly simple Python script that will respond as an -authoritative server to DNS queries. It opens a UDP socket on 10.53.0.4 -and fd92:7065:b8e:ffff::8, port 5300 (or PORT) (these are for DNS queries) -and a TCP socket addresses on 10.53.0.4 at port 5301 (or EXTRAPORT1) -(this is the control channel). - -Please note that all functionality and formatting are subject to change as -we determine what features the tool will need. - -"ans.py" will respond to queries as follows: TBD diff --git a/bin/tests/system/chain/ans4/ans.py b/bin/tests/system/chain/ans4/ans.py index 839067faa5..3e042ea58c 100755 --- a/bin/tests/system/chain/ans4/ans.py +++ b/bin/tests/system/chain/ans4/ans.py @@ -1,386 +1,481 @@ -# 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") -############################################################################ -# ans.py: See README.anspy for details. -############################################################################ +SPDX-License-Identifier: MPL-2.0 -from __future__ import print_function -import os -import sys -import signal -import socket -import select -from datetime import datetime, timedelta -import functools +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/. -import dns, dns.message, dns.query -from dns.rdatatype import * -from dns.rdataclass import * -from dns.rcode import * -from dns.name import * +See the COPYRIGHT file distributed with this work for additional +information regarding copyright ownership. +""" -############################################################################ -# set up the RRs to be returned in the next answer -# -# the message contains up to two pipe-separated ('|') fields. -# -# the first field of the message is a comma-separated list -# of actions indicating what to put into the answer set -# (e.g., a dname, a cname, another cname, etc) -# -# supported actions: -# - cname (cname from the current name to a new one in the same domain) -# - dname (dname to a new domain, plus a synthesized cname) -# - xname ("external" cname, to a new name in a new domain) -# -# example: xname, dname, cname represents a CNAME to an external -# domain which is then answered by a DNAME and synthesized -# CNAME pointing to yet another domain, which is then answered -# by a CNAME within the same domain, and finally an answer -# to the query. each RR in the answer set has a corresponding -# RRSIG. these signatures are not valid, but will exercise the -# response parser. -# -# the second field is a comma-separated list of which RRs in the -# answer set to include in the answer, in which order. if prepended -# with 's', the number indicates which signature to include. -# -# examples: for the answer set "cname, cname, cname", an rr set -# '1, s1, 2, s2, 3, s3, 4, s4' indicates that all four RRs should -# be included in the answer, with siagntures, in the original -# order, while 4, s4, 3, s3, 2, s2, 1, s1' indicates the order -# should be reversed, 's3, s3, s3, s3' indicates that the third -# RRSIG should be repeated four times and everything else should -# be omitted, and so on. -# -# if there is no second field (i.e., no pipe symbol appears in -# the line) , the default is to send all answers and signatures. -# if a pipe symbol exists but the second field is empty, then -# nothing is sent at all. -############################################################################ -actions = [] -rrs = [] +from dataclasses import dataclass +from enum import Enum +from typing import AsyncGenerator, List, Optional, Tuple + +import abc +import logging +import re + +import dns.rcode +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + ControlCommand, + ControllableAsyncDnsServer, + DnsResponseSend, + DomainHandler, + QueryContext, + ResponseAction, +) + +try: + RdataType = dns.rdatatype.RdataType +except AttributeError: # dnspython < 2.0.0 compat + RdataType = int # type: ignore -def ctl_channel(msg): - global actions, rrs +class ChainNameGenerator: + """ + Convenience class generating sequential owner/target names used in chained + responses. - msg = msg.splitlines().pop(0) - print("received control message: %s" % msg) + >>> name_generator = ChainNameGenerator() + >>> name_generator.current_name + + >>> name_generator.generate_next_name() + + >>> name_generator.generate_next_name() + + >>> name_generator.generate_next_sld() + + >>> name_generator.generate_next_sld() + + >>> name_generator.current_name + + >>> name_generator.generate_next_name() + + >>> name_generator.generate_next_name_in_next_sld() + + >>> name_generator.generate_next_name_in_next_sld() + + """ - msg = msg.split(b"|") - if len(msg) == 0: - return + def __init__(self) -> None: + self._i = 0 + self._current_label = dns.name.Name(["test"]) + self._current_sld = dns.name.Name(["domain"]) + self._tld = dns.name.Name(["nil", ""]) - actions = [x.strip() for x in msg[0].split(b",")] - n = functools.reduce( - lambda n, act: (n + (2 if act == b"dname" else 1)), [0] + actions - ) + @property + def current_name(self) -> dns.name.Name: + return self._current_label.concatenate(self.current_domain) - if len(msg) == 1: - rrs = [] - for i in range(n): - for b in [False, True]: - rrs.append((i, b)) - return + @property + def current_domain(self) -> dns.name.Name: + return self._current_sld.concatenate(self._tld) - rlist = [x.strip() for x in msg[1].split(b",")] - rrs = [] - for item in rlist: - if item[0] == b"s"[0]: - i = int(item[1:].strip()) - 1 - if i > n: - print("invalid index %d" + (i + 1)) - continue - rrs.append((int(item[1:]) - 1, True)) - else: - i = int(item) - 1 - if i > n: - print("invalid index %d" % (i + 1)) - continue - rrs.append((i, False)) + def generate_next_name(self) -> dns.name.Name: + self._current_label = dns.name.Name([f"cname{self._i}"]) + self._i += 1 + return self.current_name + + def generate_next_sld(self) -> dns.name.Name: + self._current_sld = dns.name.Name([f"domain{self._i}"]) + self._i += 1 + return self.current_domain + + def generate_next_name_in_next_sld(self) -> dns.name.Name: + self.generate_next_name() + self.generate_next_sld() + return self.current_name -############################################################################ -# Respond to a DNS query. -############################################################################ -def create_response(msg): - m = dns.message.from_wire(msg) - qname = m.question[0].name.to_text() - labels = qname.lower().split(".") - wantsigs = True if m.ednsflags & dns.flags.DO else False +class RecordGenerator(abc.ABC): + """ + An abstract class used as a base class for RRset generators (see the + description of "actions" in `ChainSetupCommand`) and as a convenience class + for creating RRsets in `ChainResponseHandler`. + """ - # get qtype - rrtype = m.question[0].rdtype - typename = dns.rdatatype.to_text(rrtype) + @classmethod + def create_rrset( + cls, owner: dns.name.Name, rrtype: RdataType, rdata: str + ) -> dns.rrset.RRset: + return dns.rrset.from_text(owner, 86400, dns.rdataclass.IN, rrtype, rdata) - # for 'www.example.com.'... - # - name is 'www' - # - domain is 'example.com.' - # - sld is 'example' - # - tld is 'com.' - name = labels.pop(0) - domain = ".".join(labels) - sld = labels.pop(0) - tld = ".".join(labels) + @classmethod + def create_rrset_signature( + cls, owner: dns.name.Name, rrtype: RdataType + ) -> dns.rrset.RRset: + covers = dns.rdatatype.to_text(rrtype) + ttl = "86400" + expiry = "20900101000000" + inception = "20250101000000" + domain = "domain.nil." + sigdata = "OCXH2De0yE4NMTl9UykvOsJ4IBGs/ZIpff2rpaVJrVG7jQfmj50otBAp " + sigdata += "A0Zo7dpBU4ofv0N/F2Ar6LznCncIojkWptEJIAKA5tHegf/jY39arEpO " + sigdata += "cevbGp6DKxFhlkLXNcw7k9o7DSw14OaRmgAjXdTFbrl4AiAa0zAttFko " + sigdata += "Tso=" + rdata = f"{covers} 5 3 {ttl} {expiry} {inception} 12345 {domain} {sigdata}" + return cls.create_rrset(owner, dns.rdatatype.RRSIG, rdata) - print("query: " + qname + "/" + typename) - print("domain: " + domain) + def __init__(self, name_generator: ChainNameGenerator) -> None: + self._name_generator = name_generator - # default answers, depending on QTYPE. - # currently only A, AAAA, TXT and NS are supported. - ttl = 86400 - additionalA = "10.53.0.4" - additionalAAAA = "fd92:7065:b8e:ffff::4" - if typename == "A": - final = "10.53.0.4" - elif typename == "AAAA": - final = "fd92:7065:b8e:ffff::4" - elif typename == "TXT": - final = "Some\ text\ here" - elif typename == "NS": - domain = qname - final = "ns1.%s" % domain - else: - final = None + def get_rrsets(self) -> Tuple[List[dns.rrset.RRset], List[dns.rrset.RRset]]: + """ + Return the lists of records and their signatures that should be + generated in response to a given "action". - # RRSIG rdata - won't validate but will exercise response parsing - t = datetime.now() - delta = timedelta(30) - t1 = t - delta - t2 = t + delta - inception = t1.strftime("%Y%m%d000000") - expiry = t2.strftime("%Y%m%d000000") - sigdata = "OCXH2De0yE4NMTl9UykvOsJ4IBGs/ZIpff2rpaVJrVG7jQfmj50otBAp A0Zo7dpBU4ofv0N/F2Ar6LznCncIojkWptEJIAKA5tHegf/jY39arEpO cevbGp6DKxFhlkLXNcw7k9o7DSw14OaRmgAjXdTFbrl4AiAa0zAttFko Tso=" + This method is a wrapper around `generate_rrsets()` that ensures all + derived classes obey their promises about the number of records they + generate. + """ + responses, signatures = self.generate_rrsets() + assert len(responses) == self.response_count + assert len(signatures) == self.response_count + return responses, signatures - # construct answer set. - answers = [] - sigs = [] - curdom = domain - curname = name - i = 0 + @property + @abc.abstractmethod + def response_count(self) -> int: + """ + How many records this generator creates each time the "action" + associated with it is used. Every generated record needs to be + accompanied by its corresponding signature, so e.g. setting this to 1 + causes `get_rrsets()` callers to expect it to return two RRset lists, + each containing one RRset. - for action in actions: - if name != "test": - continue - if action == b"xname": - owner = curname + "." + curdom - newname = "cname%d" % i - i += 1 - newdom = "domain%d.%s" % (i, tld) - i += 1 - target = newname + "." + newdom - print("add external CNAME %s to %s" % (owner, target)) - answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target)) - rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % ( - ttl, - expiry, - inception, - domain, - sigdata, - ) - print("add external RRISG(CNAME) %s to %s" % (owner, target)) - sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) - curname = newname - curdom = newdom - continue + This property could be derived from the size of the lists returned by + `generate_rrsets()`, but it is left as a separate value to enable early + detection of invalid "selector" indexes when the control commands are + first parsed. + """ + raise NotImplementedError - if action == b"cname": - owner = curname + "." + curdom - newname = "cname%d" % i - target = newname + "." + curdom - i += 1 - print("add CNAME %s to %s" % (owner, target)) - answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target)) - rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % ( - ttl, - expiry, - inception, - domain, - sigdata, - ) - print("add RRSIG(CNAME) %s to %s" % (owner, target)) - sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) - curname = newname - continue + @abc.abstractmethod + def generate_rrsets(self) -> Tuple[List[dns.rrset.RRset], List[dns.rrset.RRset]]: + """ + Return the lists of records and their signatures that should be + generated in response to a given "action". - if action == b"dname": - owner = curdom - newdom = "domain%d.%s" % (i, tld) - i += 1 - print("add DNAME %s to %s" % (owner, newdom)) - answers.append(dns.rrset.from_text(owner, ttl, IN, DNAME, newdom)) - rrsig = "DNAME 5 3 %d %s %s 12345 %s %s" % ( - ttl, - expiry, - inception, - domain, - sigdata, - ) - print("add RRSIG(DNAME) %s to %s" % (owner, newdom)) - sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) - owner = curname + "." + curdom - target = curname + "." + newdom - print("add synthesized CNAME %s to %s" % (owner, target)) - answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target)) - rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % ( - ttl, - expiry, - inception, - domain, - sigdata, - ) - print("add synthesized RRSIG(CNAME) %s to %s" % (owner, target)) - sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) - curdom = newdom - continue + This method must be defined by every derived class, but RecordGenerator + users should call `get_rrsets()` instead. + """ + raise NotImplementedError - # now add the final answer - owner = curname + "." + curdom - answers.append(dns.rrset.from_text(owner, ttl, IN, rrtype, final)) - rrsig = "%s 5 3 %d %s %s 12345 %s %s" % ( - typename, - ttl, - expiry, - inception, - domain, - sigdata, - ) - sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) - # prepare the response and convert to wire format - r = dns.message.make_response(m) +class CnameRecordGenerator(RecordGenerator): - if name != "test": - r.answer.append(answers[-1]) - if wantsigs: - r.answer.append(sigs[-1]) - else: - for i, sig in rrs: - if sig and not wantsigs: - continue - elif sig: - r.answer.append(sigs[i]) - else: - r.answer.append(answers[i]) + response_count = 1 - if typename != "NS": - r.authority.append( - dns.rrset.from_text(domain, ttl, IN, "NS", ("ns1.%s" % domain)) + def generate_rrsets(self) -> Tuple[List[dns.rrset.RRset], List[dns.rrset.RRset]]: + owner = self._name_generator.current_name + target = self._name_generator.generate_next_name().to_text() + response = self.create_rrset(owner, dns.rdatatype.CNAME, target) + signature = self.create_rrset_signature(owner, response.rdtype) + return [response], [signature] + + +class DnameRecordGenerator(RecordGenerator): + + response_count = 2 + + def generate_rrsets(self) -> Tuple[List[dns.rrset.RRset], List[dns.rrset.RRset]]: + dname_owner = self._name_generator.current_domain + cname_owner = self._name_generator.current_name + dname_target = self._name_generator.generate_next_sld().to_text() + cname_target = self._name_generator.current_name.to_text() + dname_response = self.create_rrset( + dname_owner, dns.rdatatype.DNAME, dname_target ) - r.additional.append( - dns.rrset.from_text(("ns1.%s" % domain), 86400, IN, A, additionalA) - ) - r.additional.append( - dns.rrset.from_text(("ns1.%s" % domain), 86400, IN, AAAA, additionalAAAA) - ) - - r.flags |= dns.flags.AA - r.use_edns() - return r.to_wire() + cname_response = self.create_rrset( + cname_owner, dns.rdatatype.CNAME, cname_target + ) + dname_signature = self.create_rrset_signature( + dname_owner, dname_response.rdtype + ) + cname_signature = self.create_rrset_signature( + cname_owner, cname_response.rdtype + ) + return [dname_response, cname_response], [dname_signature, cname_signature] -def sigterm(signum, frame): - print("Shutting down now...") - os.remove("ans.pid") - running = False - sys.exit(0) +class XnameRecordGenerator(RecordGenerator): + + response_count = 1 + + def generate_rrsets(self) -> Tuple[List[dns.rrset.RRset], List[dns.rrset.RRset]]: + owner = self._name_generator.current_name + target = self._name_generator.generate_next_name_in_next_sld().to_text() + response = self.create_rrset(owner, dns.rdatatype.CNAME, target) + signature = self.create_rrset_signature(owner, response.rdtype) + return [response], [signature] -############################################################################ -# 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.4" -ip6 = "fd92:7065:b8e:ffff::4" +class FinalRecordGenerator(RecordGenerator): -try: - port = int(os.environ["PORT"]) -except: - port = 5300 + response_count = 1 -try: - ctrlport = int(os.environ["EXTRAPORT1"]) -except: - ctrlport = 5300 + def generate_rrsets(self) -> Tuple[List[dns.rrset.RRset], List[dns.rrset.RRset]]: + owner = self._name_generator.current_name + response = self.create_rrset(owner, dns.rdatatype.A, "10.53.0.4") + signature = self.create_rrset_signature(owner, response.rdtype) + return [response], [signature] -query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -query4_socket.bind((ip4, port)) -havev6 = True -try: - query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) - try: - query6_socket.bind((ip6, port)) - except: - query6_socket.close() - havev6 = False -except: - havev6 = False +class ChainAction(Enum): + """ + Chained answer types that this server can send. `ChainSetupCommand` sets + up a collection of these for generating responses. + """ -ctrl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -ctrl_socket.bind((ip4, ctrlport)) -ctrl_socket.listen(5) + CNAME = CnameRecordGenerator + DNAME = DnameRecordGenerator + XNAME = XnameRecordGenerator + FINAL = FinalRecordGenerator -signal.signal(signal.SIGTERM, sigterm) -f = open("ans.pid", "w") -pid = os.getpid() -print(pid, file=f) -f.close() +@dataclass(frozen=True) +class ChainSelector: + """ + A "selector" for a specific RRset - one of all possible RRsets generated by + `ChainAction`s - to include in responses to queries. + """ -running = True + response_index: int + response_signature: bool -print("Listening on %s port %d" % (ip4, port)) -if havev6: - print("Listening on %s port %d" % (ip6, port)) -print("Control channel on %s port %d" % (ip4, ctrlport)) -print("Ctrl-c to quit") -if havev6: - input = [query4_socket, query6_socket, ctrl_socket] -else: - input = [query4_socket, ctrl_socket] +class ChainSetupCommand(ControlCommand): + """ + Set up a chained response to return for subsequent queries. -while running: - try: - inputready, outputready, exceptready = select.select(input, [], []) - except select.error as e: - break - except socket.error as e: - break - except KeyboardInterrupt: - break + The control query consists of two label sequences separated by a `_` label. - for s in inputready: - if s == ctrl_socket: - # Handle control channel input - conn, addr = s.accept() - print("Control channel connected") - while True: - msg = conn.recv(65535) - if not msg: - break - ctl_channel(msg) - conn.close() - if s == query4_socket or s == query6_socket: - print("Query received on %s" % (ip4 if s == query4_socket else ip6)) - # Handle incoming queries - msg = s.recvfrom(65535) - rsp = create_response(msg[0]) - if rsp: - s.sendto(rsp, msg[1]) - if not running: - break + The first label sequence is a set of "actions"; these cause a set of + response RRsets to be generated. Valid labels in that sequence are: + + - `cname`: CNAME from the current name to a new one in the same domain, + - `dname`: DNAME to a new domain, plus a synthesized CNAME, + - `xname`: "external" CNAME, to a new name in a new domain. + + The final response to the client query (an A RRset) is automatically + appended to the ANSWER section of every response. + + Example: `xname.dname.cname` represents a CNAME to an external domain which + is then answered by a DNAME and a synthesized CNAME pointing to yet another + domain, which is then answered by a CNAME within the same domain, and + finally an answer to the query. + + Each of the generated RRsets is associated with a corresponding RRSIG. + These signatures are not valid, but are intended to exercise the response + parser. + + The second label sequence is a set of "selectors"; these specify which + RRsets out of all the possible RRsets generated by "actions" to actually + include in the answer and in what order. The RRsets are indexed starting + from 1. If prepended with `s`, the number indicates which signature to + include. + + Examples: + + - `cname.cname.cname._.1.s1.2.s2.3.s3.4.s4` indicates that all four + RRsets (three CNAME RRsets + one A RRset with the final answer) should + be included in the answer, with their corresponding signatures, in the + original order, + + - `cname.cname.cname._.4.s4.3.s3.2.s2.1.s1` causes the same RRsets to be + returned, but in reverse order, + + - `cname.cname.cname._.s3.s3.s3.s3` causes the RRSIG RRset for the third + CNAME to be repeated four times in the response and everything else to + be omitted. + """ + + control_subdomain = "setup-chain" + + def __init__(self) -> None: + self._current_handler: Optional[ChainResponseHandler] = None + + def handle( + self, args: List[str], server: ControllableAsyncDnsServer, qctx: QueryContext + ) -> Optional[str]: + try: + actions, selectors = self._parse_args(args) + except ValueError as exc: + qctx.response.set_rcode(dns.rcode.SERVFAIL) + logging.error("%s", exc) + return str(exc) + + if self._current_handler: + server.uninstall_response_handler(self._current_handler) + + answer_rrsets = self._prepare_answer(actions, selectors) + + self._current_handler = ChainResponseHandler(answer_rrsets) + server.install_response_handler(self._current_handler) + + return "chain response setup successful" + + def _parse_args( + self, args: List[str] + ) -> Tuple[List[ChainAction], List[ChainSelector]]: + try: + delimiter = args.index("_") + except ValueError as exc: + raise ValueError("chain setup delimiter not found in QNAME") from exc + + args_actions = args[:delimiter] + actions = self._parse_args_actions(args_actions) + + args_selectors = args[delimiter + 1 :] + selectors = self._parse_args_selectors(args_selectors, actions) + + return actions, selectors + + def _parse_args_actions(self, args_actions: List[str]) -> List[ChainAction]: + actions = [] + + for action in args_actions + ["FINAL"]: + try: + actions.append(ChainAction[action.upper()]) + except KeyError as exc: + raise ValueError(f"unsupported action '{action}'") from exc + + return actions + + def _parse_args_selectors( + self, args_selectors: List[str], actions: List[ChainAction] + ) -> List[ChainSelector]: + max_response_index = self._get_max_response_index(actions) + selectors = [] + + for selector in args_selectors: + match = re.match(r"^(?Ps?)(?P[0-9]+)$", selector) + if not match: + raise ValueError(f"invalid selector '{selector}'") + response_index = int(match.group("index")) + if response_index > max_response_index: + raise ValueError( + f"invalid response index {response_index} in '{selector}'" + ) + response_signature = bool(match.group("signature")) + selectors.append(ChainSelector(response_index, response_signature)) + + return selectors + + def _get_max_response_index(self, actions: List[ChainAction]) -> int: + rrset_generator_classes = [a.value for a in actions] + return sum(g.response_count for g in rrset_generator_classes) + + def _prepare_answer( + self, actions: List[ChainAction], selectors: List[ChainSelector] + ) -> List[dns.rrset.RRset]: + all_responses, all_signatures = self._generate_rrsets(actions) + return self._select_rrsets(all_responses, all_signatures, selectors) + + def _generate_rrsets( + self, actions: List[ChainAction] + ) -> Tuple[List[dns.rrset.RRset], List[dns.rrset.RRset]]: + all_responses = [] + all_signatures = [] + name_generator = ChainNameGenerator() + + for action in actions: + rrset_generator_class = action.value + rrset_generator = rrset_generator_class(name_generator) + responses, signatures = rrset_generator.get_rrsets() + all_responses.extend(responses) + all_signatures.extend(signatures) + + return all_responses, all_signatures + + def _select_rrsets( + self, + all_responses: List[dns.rrset.RRset], + all_signatures: List[dns.rrset.RRset], + selectors: List[ChainSelector], + ) -> List[dns.rrset.RRset]: + rrsets = [] + + for selector in selectors: + index = selector.response_index - 1 + source = all_signatures if selector.response_signature else all_responses + rrsets.append(source[index]) + + return rrsets + + +class ChainResponseHandler(DomainHandler): + """ + For trigger queries (`test.domain.nil`), return a chained response + previously prepared by `ChainSetupCommand`. + + For any other query, return a non-chained response (a single A RRset). + """ + + domains = ["domain.nil."] + + def __init__(self, answer_rrsets: List[dns.rrset.RRset]): + super().__init__() + self._answer_rrsets = answer_rrsets + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + trigger_qname = dns.name.from_text("test.domain.nil.") + if qctx.qname == trigger_qname: + answer_rrsets = self._answer_rrsets + else: + answer_rrsets = self._non_chain_answer(qctx) + + for rrset in answer_rrsets: + qctx.response.answer.append(rrset) + for rrset in self._authority_rrsets: + qctx.response.authority.append(rrset) + for rrset in self._additional_rrsets: + qctx.response.additional.append(rrset) + + qctx.response.set_rcode(dns.rcode.NOERROR) + qctx.response.use_edns() + yield DnsResponseSend(qctx.response, authoritative=True) + + def _non_chain_answer(self, qctx: QueryContext) -> List[dns.rrset.RRset]: + owner = qctx.qname + return [ + RecordGenerator.create_rrset(owner, dns.rdatatype.A, "10.53.0.4"), + RecordGenerator.create_rrset_signature(owner, dns.rdatatype.A), + ] + + @property + def _authority_rrsets(self) -> List[dns.rrset.RRset]: + owner = dns.name.from_text("domain.nil.") + return [ + RecordGenerator.create_rrset(owner, dns.rdatatype.NS, "ns1.domain.nil."), + ] + + @property + def _additional_rrsets(self) -> List[dns.rrset.RRset]: + owner = dns.name.from_text("ns1.domain.nil.") + return [ + RecordGenerator.create_rrset(owner, dns.rdatatype.A, "10.53.0.4"), + RecordGenerator.create_rrset( + owner, dns.rdatatype.AAAA, "fd92:7065:b8e:ffff::4" + ), + ] + + +def main() -> None: + server = ControllableAsyncDnsServer(commands=[ChainSetupCommand]) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/chain/tests.sh b/bin/tests/system/chain/tests.sh index 7128b6cd8e..9346f2a4d2 100644 --- a/bin/tests/system/chain/tests.sh +++ b/bin/tests/system/chain/tests.sh @@ -19,7 +19,10 @@ DIGOPTS="-p ${PORT}" RNDCCMD="$RNDC -c ../_common/rndc.conf -p ${CONTROLPORT} -s" sendcmd() { - send 10.53.0.4 "${EXTRAPORT1}" + SERVER="${1}" + COMMAND="${2}" + COMMAND_ARGS="${3}" + $DIG $DIGOPTS "@${SERVER}" "${COMMAND_ARGS}.${COMMAND}._control." TXT +time=5 +tries=1 +tcp >/dev/null 2>&1 } status=0 @@ -502,29 +505,29 @@ n=$((n + 1)) echo_i "checking CNAME chains in various orders ($n)" ret=0 $RNDCCMD 10.53.0.7 null --- start test$n - step 1 --- 2>&1 | sed 's/^/ns7 /' | cat_i -echo "cname,cname,cname|1,2,3,4,s1,s2,s3,s4" | sendcmd +sendcmd 10.53.0.4 setup-chain "cname.cname.cname._.1.2.3.4.s1.s2.s3.s4" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.1.$n 2>&1 grep 'status: NOERROR' dig.out.1.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 2' dig.out.1.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 null --- start test$n - step 2 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "cname,cname,cname|1,1,2,2,3,4,s4,s3,s1" | sendcmd +sendcmd 10.53.0.4 setup-chain "cname.cname.cname._.1.1.2.2.3.4.s4.s3.s1" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.2.$n 2>&1 grep 'status: NOERROR' dig.out.2.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 2' dig.out.2.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 null --- start test$n - step 3 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "cname,cname,cname|2,1,3,4,s3,s1,s2,s4" | sendcmd +sendcmd 10.53.0.4 setup-chain "cname.cname.cname._.2.1.3.4.s3.s1.s2.s4" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.3.$n 2>&1 grep 'status: NOERROR' dig.out.3.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 2' dig.out.3.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 null --- start test$n - step 4 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "cname,cname,cname|4,3,2,1,s4,s3,s2,s1" | sendcmd +sendcmd 10.53.0.4 setup-chain "cname.cname.cname._.4.3.2.1.s4.s3.s2.s1" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.4.$n 2>&1 grep 'status: NOERROR' dig.out.4.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 2' dig.out.4.$n >/dev/null 2>&1 || ret=1 -echo "cname,cname,cname|4,3,2,1,s4,s3,s2,s1" | sendcmd +sendcmd 10.53.0.4 setup-chain "cname.cname.cname._.4.3.2.1.s4.s3.s2.s1" $RNDCCMD 10.53.0.7 null --- start test$n - step 5 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.5.$n 2>&1 @@ -532,7 +535,7 @@ grep 'status: NOERROR' dig.out.5.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 2' dig.out.5.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 null --- start test$n - step 6 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "cname,cname,cname|4,3,3,3,s1,s1,1,3,4" | sendcmd +sendcmd 10.53.0.4 setup-chain "cname.cname.cname._.4.3.3.3.s1.s1.1.3.4" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.6.$n 2>&1 grep 'status: NOERROR' dig.out.6.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 2' dig.out.6.$n >/dev/null 2>&1 || ret=1 @@ -543,7 +546,7 @@ n=$((n + 1)) echo_i "checking that only the initial CNAME is cached ($n)" ret=0 $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "cname,cname,cname|1,2,3,4,s1,s2,s3,s4" | sendcmd +sendcmd 10.53.0.4 setup-chain "cname.cname.cname._.1.2.3.4.s1.s2.s3.s4" $RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.1.$n 2>&1 sleep 1 @@ -558,19 +561,19 @@ echo_i "checking DNAME chains in various orders ($n)" ret=0 $RNDCCMD 10.53.0.7 null --- start test$n - step 1 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "dname,dname|5,4,3,2,1,s5,s4,s3,s2,s1" | sendcmd +sendcmd 10.53.0.4 setup-chain "dname.dname._.5.4.3.2.1.s5.s4.s3.s2.s1" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.1.$n 2>&1 grep 'status: NOERROR' dig.out.1.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 3' dig.out.1.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 null --- start test$n - step 2 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "dname,dname|5,4,3,2,1,s5,s4,s3,s2,s1" | sendcmd +sendcmd 10.53.0.4 setup-chain "dname.dname._.5.4.3.2.1.s5.s4.s3.s2.s1" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.2.$n 2>&1 grep 'status: NOERROR' dig.out.2.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 3' dig.out.2.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 null --- start test$n - step 3 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "dname,dname|2,3,s1,s2,s3,s4,1" | sendcmd +sendcmd 10.53.0.4 setup-chain "dname.dname._.2.3.s1.s2.s3.s4.1" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.3.$n 2>&1 grep 'status: NOERROR' dig.out.3.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 3' dig.out.3.$n >/dev/null 2>&1 || ret=1 @@ -582,19 +585,19 @@ n=$((n + 1)) echo_i "checking external CNAME/DNAME chains in various orders ($n)" ret=0 $RNDCCMD 10.53.0.7 null --- start test$n - step 1 --- 2>&1 | sed 's/^/ns7 /' | cat_i -echo "xname,dname|1,2,3,4,s1,s2,s3,s4" | sendcmd +sendcmd 10.53.0.4 setup-chain "xname.dname._.1.2.3.4.s1.s2.s3.s4" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.1.$n 2>&1 grep 'status: NOERROR' dig.out.1.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 2' dig.out.1.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 null --- start test$n - step 2 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "xname,dname|s2,2,s1,1,4,s4,3" | sendcmd +sendcmd 10.53.0.4 setup-chain "xname.dname._.s2.2.s1.1.4.s4.3" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.2.$n 2>&1 grep 'status: NOERROR' dig.out.2.$n >/dev/null 2>&1 || ret=1 grep 'ANSWER: 2' dig.out.2.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 null --- start test$n - step 3 --- 2>&1 | sed 's/^/ns7 /' | cat_i $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i -echo "xname,dname|s2,2,2,2" | sendcmd +sendcmd 10.53.0.4 setup-chain "xname.dname._.s2.2.2.2" $DIG $DIGOPTS @10.53.0.7 test.domain.nil >dig.out.3.$n 2>&1 grep 'status: SERVFAIL' dig.out.3.$n >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i diff --git a/bin/tests/system/forward/tests.sh b/bin/tests/system/forward/tests.sh index 896268fe61..0e4fd5ac62 100644 --- a/bin/tests/system/forward/tests.sh +++ b/bin/tests/system/forward/tests.sh @@ -21,7 +21,10 @@ dig_with_opts() ( ) sendcmd() ( - dig_with_opts "@${1}" "${2}._control." TXT +time=5 +tries=1 +tcp >/dev/null 2>&1 + SERVER="${1}" + COMMAND="${2}" + COMMAND_ARGS="${3}" + dig_with_opts "@${SERVER}" "${COMMAND_ARGS}.${COMMAND}._control." TXT +time=5 +tries=1 +tcp >/dev/null 2>&1 ) rndccmd() { @@ -260,7 +263,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. -sendcmd 10.53.0.6 "disable.send-responses" +sendcmd 10.53.0.6 send-responses "disable" # 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,7 +274,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" +sendcmd 10.53.0.6 send-responses "enable" if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -325,7 +328,7 @@ status=$((status + ret)) # See [GL #3129]. # Enable silent mode for ans11. -sendcmd 10.53.0.11 "disable.send-responses" +sendcmd 10.53.0.11 send-responses "disable" n=$((n + 1)) echo_i "checking the handling of hung DS fetch while chasing DS ($n)" ret=0 @@ -339,7 +342,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. -sendcmd 10.53.0.11 "enable.send-responses" +sendcmd 10.53.0.11 send-responses "enable" if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret))