1 1.1 christos # Copyright (C) Internet Systems Consortium, Inc. ("ISC") 2 1.1 christos # 3 1.1 christos # SPDX-License-Identifier: MPL-2.0 4 1.1 christos # 5 1.1 christos # This Source Code Form is subject to the terms of the Mozilla Public 6 1.1 christos # License, v. 2.0. If a copy of the MPL was not distributed with this 7 1.1 christos # file, you can obtain one at https://mozilla.org/MPL/2.0/. 8 1.1 christos # 9 1.1 christos # See the COPYRIGHT file distributed with this work for additional 10 1.1 christos # information regarding copyright ownership. 11 1.1 christos 12 1.1 christos """ 13 1.1 christos End-to-end check for the immediate UDP-to-TCP fallback on a query-id 14 1.1 christos mismatch. 15 1.1 christos 16 1.1 christos The fake authoritative server at 10.53.0.2 answers every UDP query for 17 1.1 christos trigger.example./A with a response whose DNS message id has been flipped. 18 1.1 christos The resolver at 10.53.0.1 must escalate to TCP on the first such response 19 1.1 christos and return the correct A record that the fake server serves over TCP. 20 1.1 christos """ 21 1.1 christos 22 1.1 christos from pathlib import Path 23 1.1 christos 24 1.1 christos import dns.message 25 1.1 christos import dns.rdatatype 26 1.1 christos import pytest 27 1.1 christos 28 1.1 christos import isctest 29 1.1 christos 30 1.1 christos pytestmark = pytest.mark.extra_artifacts( 31 1.1 christos [ 32 1.1 christos "ans*/ans.run", 33 1.1 christos "ns*/named.stats*", 34 1.1 christos ] 35 1.1 christos ) 36 1.1 christos 37 1.1 christos 38 1.1 christos MISMATCH_LABEL = "mismatch responses received" 39 1.1 christos MISMATCHTCP_LABEL = "queries retried over TCP after a response with mismatched query id" 40 1.1 christos 41 1.1 christos 42 1.1 christos def _named_stats(ns1) -> str: 43 1.1 christos stats_path = Path(ns1.directory) / "named.stats" 44 1.1 christos if stats_path.exists(): 45 1.1 christos stats_path.unlink() 46 1.1 christos ns1.rndc("stats") 47 1.1 christos return stats_path.read_text(encoding="utf-8") 48 1.1 christos 49 1.1 christos 50 1.1 christos def _counter(stats: str, label: str) -> int: 51 1.1 christos for line in stats.splitlines(): 52 1.1 christos line = line.strip() 53 1.1 christos if line.endswith(label): 54 1.1 christos return int(line.split()[0]) 55 1.1 christos return 0 56 1.1 christos 57 1.1 christos 58 1.1 christos def test_mismatch_tcp_fallback(ns1): 59 1.1 christos """ 60 1.1 christos Issue a single recursive query for a name whose UDP responses are 61 1.1 christos being spoofed. The resolver must escalate to TCP on the first 62 1.1 christos near-miss and return the correct A record. 63 1.1 christos """ 64 1.1 christos msg = dns.message.make_query("trigger.example.", dns.rdatatype.A, want_dnssec=False) 65 1.1 christos res = isctest.query.udp(msg, ns1.ip, timeout=10) 66 1.1 christos isctest.check.noerror(res) 67 1.1 christos 68 1.1 christos answers = [rrset for rrset in res.answer if rrset.rdtype == dns.rdatatype.A] 69 1.1 christos assert answers, f"no A RRset in response: {res}" 70 1.1 christos addresses = {item.address for rrset in answers for item in rrset} 71 1.1 christos assert "192.0.2.42" in addresses, f"unexpected answer: {addresses}" 72 1.1 christos 73 1.1 christos 74 1.1 christos def test_mismatch_counter(ns1): 75 1.1 christos """ 76 1.1 christos After the spoofed exchange completes the resolver's existing 77 1.1 christos "mismatch responses received" counter must be non-zero, confirming 78 1.1 christos the dispatcher actually saw the wrong-id response, and the new 79 1.1 christos "queries retried over TCP after a response with mismatched query 80 1.1 christos id" counter must also be non-zero, confirming that the TCP 81 1.1 christos fallback path actually fired in response to that mismatch. 82 1.1 christos """ 83 1.1 christos msg = dns.message.make_query("trigger.example.", dns.rdatatype.A, want_dnssec=False) 84 1.1 christos isctest.query.udp(msg, ns1.ip, timeout=10) 85 1.1 christos 86 1.1 christos stats = _named_stats(ns1) 87 1.1 christos assert _counter(stats, MISMATCH_LABEL) > 0, stats 88 1.1 christos assert _counter(stats, MISMATCHTCP_LABEL) > 0, stats 89