From: Alessio Podda Date: Thu, 7 May 2026 08:03:49 +0000 (+0200) Subject: Add forward-first referral poisoning reproducer X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ee34bbd208de9559b9955fa04a40805f3f32cc04;p=thirdparty%2Fbind9.git Add forward-first referral poisoning reproducer Add a system test covering authority-section NS referrals returned by configured forwarders under forward first. The test verifies that a forwarder for fwd.hack cannot install the parent hack zone cut and redirect resolution for the sibling zone sibling.hack. --- diff --git a/bin/tests/system/fwdfirst/ans1/ans.py b/bin/tests/system/fwdfirst/ans1/ans.py new file mode 100644 index 00000000000..e8123047cda --- /dev/null +++ b/bin/tests/system/fwdfirst/ans1/ans.py @@ -0,0 +1,22 @@ +""" +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() diff --git a/bin/tests/system/fwdfirst/ans1/hack.db b/bin/tests/system/fwdfirst/ans1/hack.db new file mode 100644 index 00000000000..644501682f9 --- /dev/null +++ b/bin/tests/system/fwdfirst/ans1/hack.db @@ -0,0 +1,7 @@ +$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 diff --git a/bin/tests/system/fwdfirst/ans1/root.db b/bin/tests/system/fwdfirst/ans1/root.db new file mode 100644 index 00000000000..70e64272666 --- /dev/null +++ b/bin/tests/system/fwdfirst/ans1/root.db @@ -0,0 +1,8 @@ +$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 diff --git a/bin/tests/system/fwdfirst/ans1/sibling.hack.db b/bin/tests/system/fwdfirst/ans1/sibling.hack.db new file mode 100644 index 00000000000..d05d9d573ff --- /dev/null +++ b/bin/tests/system/fwdfirst/ans1/sibling.hack.db @@ -0,0 +1,5 @@ +$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 diff --git a/bin/tests/system/fwdfirst/ans2/ans.py b/bin/tests/system/fwdfirst/ans2/ans.py new file mode 100644 index 00000000000..cf40fbeb376 --- /dev/null +++ b/bin/tests/system/fwdfirst/ans2/ans.py @@ -0,0 +1,45 @@ +""" +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() diff --git a/bin/tests/system/fwdfirst/ans3/ans.py b/bin/tests/system/fwdfirst/ans3/ans.py new file mode 100644 index 00000000000..8a7ecf8a4de --- /dev/null +++ b/bin/tests/system/fwdfirst/ans3/ans.py @@ -0,0 +1,50 @@ +""" +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() diff --git a/bin/tests/system/fwdfirst/ns4/named.conf.j2 b/bin/tests/system/fwdfirst/ns4/named.conf.j2 new file mode 100644 index 00000000000..46d5dd8b997 --- /dev/null +++ b/bin/tests/system/fwdfirst/ns4/named.conf.j2 @@ -0,0 +1,31 @@ +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@; }; +}; diff --git a/bin/tests/system/fwdfirst/ns5/named.conf.j2 b/bin/tests/system/fwdfirst/ns5/named.conf.j2 new file mode 100644 index 00000000000..82068f1a94c --- /dev/null +++ b/bin/tests/system/fwdfirst/ns5/named.conf.j2 @@ -0,0 +1,27 @@ +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"; +}; diff --git a/bin/tests/system/fwdfirst/tests_fwdfirst.py b/bin/tests/system/fwdfirst/tests_fwdfirst.py new file mode 100644 index 00000000000..4ad3fa9c500 --- /dev/null +++ b/bin/tests/system/fwdfirst/tests_fwdfirst.py @@ -0,0 +1,49 @@ +# 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"}