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