Home | History | Annotate | Line # | Download | only in formerr
      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 from typing import SupportsInt
     13 
     14 import socket
     15 
     16 import dns.flags
     17 import dns.name
     18 import dns.opcode
     19 import dns.rcode
     20 import dns.rdataclass
     21 import dns.rdatatype
     22 import dns.rdtypes.ANY.NSEC3
     23 import dns.rdtypes.ANY.OPT
     24 import dns.rdtypes.ANY.RRSIG
     25 import dns.rdtypes.ANY.SOA
     26 import dns.rdtypes.ANY.TSIG
     27 import dns.rdtypes.IN.A
     28 import pytest
     29 
     30 
     31 def wire(*parts: bytes) -> bytes:
     32     return b"".join(parts)
     33 
     34 
     35 def u16(value: SupportsInt) -> bytes:
     36     return int(value).to_bytes(2, byteorder="big")
     37 
     38 
     39 def u32(value: SupportsInt) -> bytes:
     40     return int(value).to_bytes(4, byteorder="big")
     41 
     42 
     43 def name(text: str) -> bytes:
     44     return dns.name.from_text(text).to_wire()
     45 
     46 
     47 def name_pointer(offset: int) -> bytes:
     48     return u16(0xC000 | offset)
     49 
     50 
     51 def root() -> bytes:
     52     return dns.name.root.to_wire()
     53 
     54 
     55 def header(
     56     message_id: int = 0,
     57     flags: int = 0,
     58     opcode: int = dns.opcode.QUERY,
     59     rcode: dns.rcode.Rcode = dns.rcode.NOERROR,
     60     qdcount: int = 0,
     61     ancount: int = 0,
     62     nscount: int = 0,
     63     arcount: int = 0,
     64 ) -> bytes:
     65     return wire(
     66         u16(message_id),
     67         u16(flags | dns.opcode.to_flags(opcode) | int(rcode)),
     68         u16(qdcount),
     69         u16(ancount),
     70         u16(nscount),
     71         u16(arcount),
     72     )
     73 
     74 
     75 def formerr_response_header(
     76     message_id: int = 0,
     77     opcode: int = dns.opcode.QUERY,
     78     rcode: dns.rcode.Rcode = dns.rcode.FORMERR,
     79     qdcount: int = 0,
     80     ancount: int = 0,
     81     nscount: int = 0,
     82     arcount: int = 0,
     83 ) -> bytes:
     84     return header(
     85         message_id=message_id,
     86         flags=dns.flags.QR,
     87         opcode=opcode,
     88         rcode=rcode,
     89         qdcount=qdcount,
     90         ancount=ancount,
     91         nscount=nscount,
     92         arcount=arcount,
     93     )
     94 
     95 
     96 def question(
     97     qname: bytes,
     98     qtype: dns.rdatatype.RdataType = dns.rdatatype.RdataType.A,
     99     qclass: dns.rdataclass.RdataClass = dns.rdataclass.RdataClass.IN,
    100 ) -> bytes:
    101     return wire(
    102         qname,
    103         u16(qtype),
    104         u16(qclass),
    105     )
    106 
    107 
    108 def rr(
    109     owner: bytes,
    110     rrtype: SupportsInt,
    111     rrclass: SupportsInt,
    112     *,
    113     ttl: int = 1,
    114     rdata: bytes = b"",
    115 ) -> bytes:
    116     return wire(
    117         question(
    118             owner, dns.rdatatype.RdataType(rrtype), dns.rdataclass.RdataClass(rrclass)
    119         ),
    120         u32(ttl),
    121         u16(len(rdata)),
    122         rdata,
    123     )
    124 
    125 
    126 def oversized_name() -> bytes:
    127     labels = [bytes([15]) + b"A" * 15 for _ in range(15)]
    128     labels.append(bytes([14]) + b"A" * 14 + root())
    129     return wire(*labels)
    130 
    131 
    132 def soa_rr(
    133     *,
    134     minimum: int,
    135 ) -> bytes:
    136     return rr(
    137         root(),
    138         dns.rdatatype.RdataType.SOA,
    139         dns.rdataclass.RdataClass.IN,
    140         rdata=dns.rdtypes.ANY.SOA.SOA(
    141             dns.rdataclass.RdataClass.IN,
    142             dns.rdatatype.RdataType.SOA,
    143             dns.name.root,
    144             dns.name.root,
    145             1,
    146             2,
    147             3,
    148             4,
    149             minimum,
    150         ).to_wire(),
    151     )
    152 
    153 
    154 def nsec3_rr(
    155     *,
    156     owner: bytes,
    157 ) -> bytes:
    158     return rr(
    159         owner,
    160         dns.rdatatype.RdataType.NSEC3,
    161         dns.rdataclass.RdataClass.IN,
    162         rdata=dns.rdtypes.ANY.NSEC3.NSEC3(
    163             dns.rdataclass.RdataClass.IN,
    164             dns.rdatatype.RdataType.NSEC3,
    165             algorithm=240,
    166             flags=0,
    167             iterations=0,
    168             salt=b"",
    169             next=b"\xff",
    170             windows=[],
    171         ).to_wire(),
    172     )
    173 
    174 
    175 def key_rdata(
    176     *,
    177     flags: int,
    178     protocol: int,
    179     algorithm: int,
    180     keydata: bytes,
    181 ) -> bytes:
    182     # No dns.rdtypes.ANY.KEY.KEY class, so construct the rdata manually
    183     return wire(u16(flags), bytes([protocol, algorithm]), keydata)
    184 
    185 
    186 def key_rr(*, rdclass: dns.rdataclass.RdataClass) -> bytes:
    187     return rr(
    188         root(),
    189         dns.rdatatype.RdataType.KEY,
    190         rdclass,
    191         rdata=key_rdata(
    192             flags=0,
    193             protocol=0,
    194             algorithm=248,
    195             keydata=b"\x00",
    196         ),
    197     )
    198 
    199 
    200 def malformed_rrsig_rr() -> bytes:
    201     return rr(
    202         root(),
    203         dns.rdatatype.RdataType.RRSIG,
    204         dns.rdataclass.RdataClass.IN,
    205         rdata=dns.rdtypes.ANY.RRSIG.RRSIG(
    206             dns.rdataclass.RdataClass.IN,
    207             dns.rdatatype.RdataType.RRSIG,
    208             0,
    209             240,
    210             0,
    211             1,
    212             2,
    213             3,
    214             0,
    215             dns.name.root,
    216             b"\x00",
    217         ).to_wire(),
    218     )
    219 
    220 
    221 def tsig_rr(
    222     *,
    223     owner: bytes = root(),
    224     rdclass: dns.rdataclass.RdataClass = dns.rdataclass.RdataClass.ANY,
    225     algorithm: dns.name.Name = dns.name.root,
    226     time_signed: int = 0x010203040506,
    227     fudge: int = 0x0102,
    228     mac: bytes = b"\x00",
    229     original_id: int = 0,
    230     error: int = 0,
    231     other: bytes = b"",
    232 ) -> bytes:
    233     return rr(
    234         owner,
    235         dns.rdatatype.RdataType.TSIG,
    236         rdclass,
    237         rdata=dns.rdtypes.ANY.TSIG.TSIG(
    238             rdclass,
    239             dns.rdatatype.RdataType.TSIG,
    240             algorithm,
    241             time_signed,
    242             fudge,
    243             mac,
    244             original_id,
    245             error,
    246             other,
    247         ).to_wire(),
    248     )
    249 
    250 
    251 def opt_rr(*, owner: bytes) -> bytes:
    252     return rr(
    253         owner,
    254         dns.rdatatype.RdataType.OPT,
    255         dns.rdataclass.RdataClass.IN,
    256         ttl=0,
    257         rdata=dns.rdtypes.ANY.OPT.OPT(
    258             dns.rdataclass.RdataClass.IN,
    259             dns.rdatatype.RdataType.OPT,
    260             [],
    261         ).to_wire(),
    262     )
    263 
    264 
    265 def a_rdata(ipv4_bytes: bytes = b"\x00\x00\x00\x00") -> bytes:
    266     return dns.rdtypes.IN.A.A(
    267         dns.rdataclass.RdataClass.IN,
    268         dns.rdatatype.RdataType.A,
    269         ipv4_bytes,
    270     ).to_wire()
    271 
    272 
    273 def a_rr(owner: bytes = root()) -> bytes:
    274     return rr(
    275         owner,
    276         dns.rdatatype.RdataType.A,
    277         dns.rdataclass.RdataClass.IN,
    278         rdata=a_rdata(),
    279     )
    280 
    281 
    282 def query_raw_tcp(host: str, port: int, packet_wire: bytes) -> bytes:
    283     with (
    284         socket.create_connection((host, port), timeout=10) as sock,
    285         sock.makefile("rwb") as f,
    286     ):
    287         f.write(u16(len(packet_wire)))
    288         f.write(packet_wire)
    289         f.flush()
    290         length = int.from_bytes(f.read(2), byteorder="big")
    291         return f.read(length)
    292 
    293 
    294 @pytest.mark.parametrize(
    295     "query_wire,expected_wire",
    296     [
    297         pytest.param(
    298             wire(
    299                 header(qdcount=1),
    300                 question(oversized_name()),
    301             ),
    302             formerr_response_header(),
    303             id="nametoolong",
    304         ),
    305         pytest.param(
    306             wire(
    307                 header(qdcount=2),
    308                 question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
    309                 # Two names concatenated in the QNAME field
    310                 question(
    311                     wire(name("AAAAAAAAAAAAAA."), name("AAAAAAAAAAAAAB.")),
    312                     dns.rdatatype.RdataType.A,
    313                 ),
    314             ),
    315             formerr_response_header(),
    316             id="twoquestionnames",
    317         ),
    318         pytest.param(
    319             wire(
    320                 header(qdcount=2),
    321                 question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
    322                 question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.NS),
    323             ),
    324             wire(
    325                 formerr_response_header(qdcount=2),
    326                 question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
    327                 question(name_pointer(12), dns.rdatatype.RdataType.NS),
    328             ),
    329             id="twoquestiontypes",
    330         ),
    331         pytest.param(
    332             wire(
    333                 header(qdcount=2),
    334                 question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
    335                 question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
    336             ),
    337             formerr_response_header(),
    338             id="dupquestion",
    339         ),
    340         pytest.param(
    341             wire(
    342                 header(qdcount=1, ancount=2),
    343                 question(root(), dns.rdatatype.RdataType.SOA),
    344                 soa_rr(minimum=5),
    345                 soa_rr(minimum=6),
    346             ),
    347             wire(
    348                 formerr_response_header(qdcount=1),
    349                 question(root(), dns.rdatatype.RdataType.SOA),
    350             ),
    351             id="dupans",
    352         ),
    353         pytest.param(
    354             wire(
    355                 header(ancount=1),
    356                 rr(
    357                     root(),
    358                     dns.rdatatype.RdataType.MAILB,
    359                     dns.rdataclass.RdataClass.IN,
    360                 ),
    361             ),
    362             formerr_response_header(),
    363             id="qtypeasanswer",
    364         ),
    365         pytest.param(
    366             header(),
    367             # This would be NOERROR if it included a COOKIE option,
    368             # but is a FORMERR without one.
    369             formerr_response_header(),
    370             id="noquestions",
    371         ),
    372         pytest.param(
    373             wire(
    374                 header(qdcount=1, nscount=1),
    375                 question(root(), dns.rdatatype.RdataType.A),
    376                 # Bad NSEC3 owner: X. is not in the base32hex alphabet.
    377                 nsec3_rr(owner=name("X.")),
    378             ),
    379             wire(
    380                 formerr_response_header(rcode=dns.rcode.SERVFAIL, qdcount=1),
    381                 question(root(), dns.rdatatype.RdataType.A),
    382             ),
    383             id="badnsec3owner",
    384         ),
    385         pytest.param(
    386             wire(
    387                 header(arcount=1),
    388                 # Truncated A record (no ttl, length or data)
    389                 question(root(), dns.rdatatype.RdataType.A),
    390             ),
    391             formerr_response_header(),
    392             id="shortrecord",
    393         ),
    394         pytest.param(
    395             wire(
    396                 header(qdcount=1),
    397                 # Truncated question (no class)
    398                 root(),
    399                 u16(dns.rdatatype.RdataType.A),
    400             ),
    401             formerr_response_header(),
    402             id="shortquestion",
    403         ),
    404         pytest.param(
    405             wire(
    406                 header(qdcount=2),
    407                 question(
    408                     root(),
    409                     dns.rdatatype.RdataType.A,
    410                     dns.rdataclass.RdataClass.IN,
    411                 ),
    412                 question(
    413                     root(),
    414                     dns.rdatatype.RdataType.A,
    415                     dns.rdataclass.RdataClass(2),
    416                 ),
    417             ),
    418             formerr_response_header(),
    419             id="twoquestionclasses",
    420         ),
    421         pytest.param(
    422             wire(
    423                 header(arcount=1),
    424                 a_rr(owner=oversized_name()),
    425             ),
    426             formerr_response_header(),
    427             id="badrecordname",
    428         ),
    429         pytest.param(
    430             wire(
    431                 header(arcount=2),
    432                 a_rr(),
    433                 rr(
    434                     root(),
    435                     dns.rdatatype.RdataType(65280),
    436                     dns.rdataclass.RdataClass(256),
    437                     rdata=a_rdata(),
    438                 ),
    439             ),
    440             formerr_response_header(),
    441             id="wrongclass",
    442         ),
    443         pytest.param(
    444             wire(
    445                 header(qdcount=1, arcount=1),
    446                 question(root(), dns.rdatatype.RdataType.A),
    447                 key_rr(rdclass=dns.rdataclass.RdataClass(2)),
    448             ),
    449             wire(
    450                 formerr_response_header(qdcount=1),
    451                 question(root(), dns.rdatatype.RdataType.A),
    452             ),
    453             id="keyclass",
    454         ),
    455         pytest.param(
    456             wire(
    457                 header(arcount=1),
    458                 # OPT owner should be root
    459                 opt_rr(owner=name("A.")),
    460             ),
    461             formerr_response_header(),
    462             id="optwrongname",
    463         ),
    464         pytest.param(
    465             wire(
    466                 header(arcount=1),
    467                 malformed_rrsig_rr(),
    468             ),
    469             formerr_response_header(),
    470             id="malformedrrsig",
    471         ),
    472         pytest.param(
    473             wire(
    474                 header(opcode=dns.opcode.UPDATE, nscount=1),
    475                 rr(
    476                     root(),
    477                     dns.rdatatype.RdataType.A,
    478                     dns.rdataclass.RdataClass.ANY,
    479                     ttl=0,
    480                     # Non-empty rdata for DELETE type
    481                     rdata=b"\x00",
    482                 ),
    483             ),
    484             formerr_response_header(opcode=dns.opcode.UPDATE),
    485             id="malformeddeltype",
    486         ),
    487         pytest.param(
    488             wire(
    489                 header(arcount=1),
    490                 # Class should be ANY not IN
    491                 tsig_rr(rdclass=dns.rdataclass.RdataClass.IN),
    492             ),
    493             formerr_response_header(),
    494             id="tsigwrongclass",
    495         ),
    496         pytest.param(
    497             wire(
    498                 # Test non-zero message ID is preserved in the response
    499                 header(message_id=67, arcount=2),
    500                 tsig_rr(),
    501                 # TSIG should be the last record
    502                 a_rr(),
    503             ),
    504             formerr_response_header(message_id=67),
    505             id="tsignotlast",
    506         ),
    507     ],
    508 )
    509 def test_formerr(
    510     query_wire: bytes,
    511     expected_wire: bytes,
    512     named_port: int,
    513     ns1,
    514 ) -> None:
    515     response_wire = query_raw_tcp(ns1.ip, named_port, query_wire)
    516     assert response_wire == expected_wire
    517