--- /dev/null
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+from isctest.asyncserver import AsyncDnsServer
+
+
+def main() -> None:
+ AsyncDnsServer().run()
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+$TTL 300
+@ SOA ns.hack. . 0 0 0 0 300
+@ NS ns
+ns A 10.53.0.1
+
+sibling NS ns.sibling
+ns.sibling A 10.53.0.1
--- /dev/null
+$ORIGIN .
+$TTL 300
+@ SOA a.root-servers.nil. . 0 0 0 0 300
+@ NS a.root-servers.nil.
+a.root-servers.nil. A 10.53.0.1
+
+hack. NS ns.hack.
+ns.hack. A 10.53.0.1
--- /dev/null
+$TTL 300
+@ SOA ns.sibling.hack. . 0 0 0 0 300
+@ NS ns
+ns A 10.53.0.1
+victim A 203.0.113.42
--- /dev/null
+"""
+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.
+"""
+
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import AsyncDnsServer, DomainHandler, StaticResponseHandler
+
+
+class PoisonReferralForwarder(DomainHandler, StaticResponseHandler):
+ domains = ["fwd.hack."]
+ authority = [
+ dns.rrset.from_text(
+ "hack.", 300, dns.rdataclass.IN, dns.rdatatype.NS, "ns.fwd.hack."
+ )
+ ]
+ additional = [
+ dns.rrset.from_text(
+ "ns.fwd.hack.", 300, dns.rdataclass.IN, dns.rdatatype.A, "10.53.0.3"
+ )
+ ]
+ rcode = dns.rcode.NOERROR
+ authoritative = False
+
+
+def main() -> None:
+ server = AsyncDnsServer()
+ server.install_response_handler(PoisonReferralForwarder())
+ server.run()
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+from collections.abc import AsyncGenerator
+
+import dns.rcode
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import (
+ AsyncDnsServer,
+ DnsResponseSend,
+ QueryContext,
+ ResponseAction,
+ ResponseHandler,
+)
+
+
+class AttackerAuthority(ResponseHandler):
+ async def get_responses(
+ self, qctx: QueryContext
+ ) -> AsyncGenerator[ResponseAction, None]:
+ if qctx.qtype == dns.rdatatype.A:
+ qctx.response.answer.append(
+ dns.rrset.from_text(
+ qctx.qname, 300, qctx.qclass, dns.rdatatype.A, "6.6.6.6"
+ )
+ )
+
+ yield DnsResponseSend(qctx.response)
+
+
+def main() -> None:
+ server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
+ server.install_response_handler(AttackerAuthority())
+ server.run()
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+options {
+ query-source address 10.53.0.4;
+ notify-source 10.53.0.4;
+ transfer-source 10.53.0.4;
+ port @PORT@;
+ pid-file "named.pid";
+ listen-on { 10.53.0.4; };
+ listen-on-v6 { none; };
+ recursion yes;
+ allow-query { any; };
+ allow-recursion { any; };
+ dnssec-validation no;
+ qname-minimization off;
+};
+
+controls {
+ inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+include "../../_common/rndc.key";
+
+zone "." {
+ type hint;
+ file "../../_common/root.hint";
+};
+
+zone "fwd.hack" {
+ type forward;
+ forward first;
+ forwarders { 10.53.0.2 port @PORT@; };
+};
--- /dev/null
+options {
+ query-source address 10.53.0.5;
+ notify-source 10.53.0.5;
+ transfer-source 10.53.0.5;
+ port @PORT@;
+ pid-file "named.pid";
+ listen-on { 10.53.0.5; };
+ listen-on-v6 { none; };
+ recursion yes;
+ allow-query { any; };
+ allow-recursion { any; };
+ dnssec-validation no;
+ qname-minimization off;
+ forward first;
+ forwarders { 10.53.0.2 port @PORT@; };
+};
+
+controls {
+ inet 10.53.0.5 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+include "../../_common/rndc.key";
+
+zone "." {
+ type hint;
+ file "../../_common/root.hint";
+};
--- /dev/null
+# 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.
+
+import dns.name
+import dns.rdataclass
+import dns.rdatatype
+import pytest
+
+from isctest.instance import NamedInstance
+
+import isctest
+
+
+def _a_records(response, qname: str) -> set[str]:
+ rrset = response.get_rrset(
+ response.answer,
+ dns.name.from_text(qname),
+ dns.rdataclass.IN,
+ dns.rdatatype.A,
+ )
+ if rrset is None:
+ return set()
+ return {rdata.address for rdata in rrset}
+
+
+@pytest.mark.parametrize("resolver_name", ["ns4", "ns5"])
+def test_forward_first_referral_ns_above_forward_zone_not_cached(
+ servers: dict[str, NamedInstance],
+ resolver_name: str,
+) -> None:
+ resolver = servers[resolver_name]
+ resolver.rndc("flush")
+
+ trigger = isctest.query.create("trigger.fwd.hack.", "A", dnssec=False)
+ isctest.query.tcp(trigger, resolver.ip)
+
+ sibling = isctest.query.create("victim.sibling.hack.", "A", dnssec=False)
+ response = isctest.query.tcp(sibling, resolver.ip)
+ isctest.check.noerror(response)
+
+ assert _a_records(response, "victim.sibling.hack.") == {"203.0.113.42"}