bind9/bin/tests/system/formerr/tests_formerr.py
Štěpán Balážik 62a3804a4c Simplify the packets in the formerr system test
Normalize the message ID to 0 and the TTL of records to 1 unless
required (OPT and UPDATE records require TTL=0).

Rename the questionclass test case to twoquestionclasses for
consistency.
2026-06-17 20:08:48 +02:00

508 lines
13 KiB
Python

# 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 typing import SupportsInt
import socket
import dns.flags
import dns.name
import dns.opcode
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.rdtypes.ANY.NSEC3
import dns.rdtypes.ANY.OPT
import dns.rdtypes.ANY.RRSIG
import dns.rdtypes.ANY.SOA
import dns.rdtypes.ANY.TSIG
import dns.rdtypes.IN.A
import pytest
def wire(*parts: bytes) -> bytes:
return b"".join(parts)
def u16(value: SupportsInt) -> bytes:
return int(value).to_bytes(2, byteorder="big")
def u32(value: SupportsInt) -> bytes:
return int(value).to_bytes(4, byteorder="big")
def name(text: str) -> bytes:
return dns.name.from_text(text).to_wire()
def root() -> bytes:
return dns.name.root.to_wire()
def header(
message_id: int = 0,
flags: int = 0,
opcode: int = dns.opcode.QUERY,
rcode: dns.rcode.Rcode = dns.rcode.NOERROR,
qdcount: int = 0,
ancount: int = 0,
nscount: int = 0,
arcount: int = 0,
) -> bytes:
return wire(
u16(message_id),
u16(flags | dns.opcode.to_flags(opcode) | int(rcode)),
u16(qdcount),
u16(ancount),
u16(nscount),
u16(arcount),
)
def formerr_response_header(
message_id: int = 0,
opcode: int = dns.opcode.QUERY,
rcode: dns.rcode.Rcode = dns.rcode.FORMERR,
qdcount: int = 0,
ancount: int = 0,
nscount: int = 0,
arcount: int = 0,
) -> bytes:
return header(
message_id=message_id,
flags=dns.flags.QR,
opcode=opcode,
rcode=rcode,
qdcount=qdcount,
ancount=ancount,
nscount=nscount,
arcount=arcount,
)
def question(
qname: bytes,
qtype: dns.rdatatype.RdataType = dns.rdatatype.RdataType.A,
qclass: dns.rdataclass.RdataClass = dns.rdataclass.RdataClass.IN,
) -> bytes:
return wire(
qname,
u16(qtype),
u16(qclass),
)
def rr(
owner: bytes,
rrtype: SupportsInt,
rrclass: SupportsInt,
*,
ttl: int = 1,
rdata: bytes = b"",
) -> bytes:
return wire(
question(
owner, dns.rdatatype.RdataType(rrtype), dns.rdataclass.RdataClass(rrclass)
),
u32(ttl),
u16(len(rdata)),
rdata,
)
def oversized_name() -> bytes:
labels = [bytes([15]) + b"A" * 15 for _ in range(15)]
labels.append(bytes([14]) + b"A" * 14 + root())
return wire(*labels)
def soa_rr(
*,
minimum: int,
) -> bytes:
return rr(
root(),
dns.rdatatype.RdataType.SOA,
dns.rdataclass.RdataClass.IN,
rdata=dns.rdtypes.ANY.SOA.SOA(
dns.rdataclass.RdataClass.IN,
dns.rdatatype.RdataType.SOA,
dns.name.root,
dns.name.root,
1,
2,
3,
4,
minimum,
).to_wire(),
)
def nsec3_rr(
*,
owner: bytes,
) -> bytes:
return rr(
owner,
dns.rdatatype.RdataType.NSEC3,
dns.rdataclass.RdataClass.IN,
rdata=dns.rdtypes.ANY.NSEC3.NSEC3(
dns.rdataclass.RdataClass.IN,
dns.rdatatype.RdataType.NSEC3,
algorithm=240,
flags=0,
iterations=0,
salt=b"",
next=b"\xff",
windows=[],
).to_wire(),
)
def key_rdata(
*,
flags: int,
protocol: int,
algorithm: int,
keydata: bytes,
) -> bytes:
# No dns.rdtypes.ANY.KEY.KEY class, so construct the rdata manually
return wire(u16(flags), bytes([protocol, algorithm]), keydata)
def key_rr(*, rdclass: dns.rdataclass.RdataClass) -> bytes:
return rr(
root(),
dns.rdatatype.RdataType.KEY,
rdclass,
rdata=key_rdata(
flags=0,
protocol=0,
algorithm=248,
keydata=b"\x00",
),
)
def malformed_rrsig_rr() -> bytes:
return rr(
root(),
dns.rdatatype.RdataType.RRSIG,
dns.rdataclass.RdataClass.IN,
rdata=dns.rdtypes.ANY.RRSIG.RRSIG(
dns.rdataclass.RdataClass.IN,
dns.rdatatype.RdataType.RRSIG,
0,
240,
0,
1,
2,
3,
0,
dns.name.root,
b"\x00",
).to_wire(),
)
def tsig_rr(
*,
owner: bytes = root(),
rdclass: dns.rdataclass.RdataClass = dns.rdataclass.RdataClass.ANY,
algorithm: dns.name.Name = dns.name.root,
time_signed: int = 0x010203040506,
fudge: int = 0x0102,
mac: bytes = b"\x00",
original_id: int = 0,
error: int = 0,
other: bytes = b"",
) -> bytes:
return rr(
owner,
dns.rdatatype.RdataType.TSIG,
rdclass,
rdata=dns.rdtypes.ANY.TSIG.TSIG(
rdclass,
dns.rdatatype.RdataType.TSIG,
algorithm,
time_signed,
fudge,
mac,
original_id,
error,
other,
).to_wire(),
)
def opt_rr(*, owner: bytes) -> bytes:
return rr(
owner,
dns.rdatatype.RdataType.OPT,
dns.rdataclass.RdataClass.IN,
ttl=0,
rdata=dns.rdtypes.ANY.OPT.OPT(
dns.rdataclass.RdataClass.IN,
dns.rdatatype.RdataType.OPT,
[],
).to_wire(),
)
def a_rdata(ipv4_bytes: bytes = b"\x00\x00\x00\x00") -> bytes:
return dns.rdtypes.IN.A.A(
dns.rdataclass.RdataClass.IN,
dns.rdatatype.RdataType.A,
ipv4_bytes,
).to_wire()
def a_rr(owner: bytes = root()) -> bytes:
return rr(
owner,
dns.rdatatype.RdataType.A,
dns.rdataclass.RdataClass.IN,
rdata=a_rdata(),
)
def query_raw_tcp(host: str, port: int, packet_wire: bytes) -> bytes:
with (
socket.create_connection((host, port), timeout=10) as sock,
sock.makefile("rwb") as f,
):
f.write(u16(len(packet_wire)))
f.write(packet_wire)
f.flush()
length = int.from_bytes(f.read(2), byteorder="big")
return f.read(length)
@pytest.mark.parametrize(
"query_wire,expected_wire",
[
pytest.param(
wire(
header(qdcount=1),
question(oversized_name()),
),
formerr_response_header(),
id="nametoolong",
),
pytest.param(
wire(
header(qdcount=2),
question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
# Two names concatenated in the QNAME field
question(
wire(name("AAAAAAAAAAAAAA."), name("AAAAAAAAAAAAAB.")),
dns.rdatatype.RdataType.A,
),
),
formerr_response_header(),
id="twoquestionnames",
),
pytest.param(
wire(
header(qdcount=2),
question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.NS),
),
formerr_response_header(),
id="twoquestiontypes",
),
pytest.param(
wire(
header(qdcount=2),
question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
),
formerr_response_header(),
id="dupquestion",
),
pytest.param(
wire(
header(qdcount=1, ancount=2),
question(root(), dns.rdatatype.RdataType.SOA),
soa_rr(minimum=5),
soa_rr(minimum=6),
),
wire(
formerr_response_header(qdcount=1),
question(root(), dns.rdatatype.RdataType.SOA),
),
id="dupans",
),
pytest.param(
wire(
header(ancount=1),
rr(
root(),
dns.rdatatype.RdataType.MAILB,
dns.rdataclass.RdataClass.IN,
),
),
formerr_response_header(),
id="qtypeasanswer",
),
pytest.param(
header(),
# This would be NOERROR if it included a COOKIE option,
# but is a FORMERR without one.
formerr_response_header(),
id="noquestions",
),
pytest.param(
wire(
header(qdcount=1, nscount=1),
question(root(), dns.rdatatype.RdataType.A),
# Bad NSEC3 owner: X. is not in the base32hex alphabet.
nsec3_rr(owner=name("X.")),
),
wire(
formerr_response_header(rcode=dns.rcode.SERVFAIL, qdcount=1),
question(root(), dns.rdatatype.RdataType.A),
),
id="badnsec3owner",
),
pytest.param(
wire(
header(arcount=1),
# Truncated A record (no ttl, length or data)
question(root(), dns.rdatatype.RdataType.A),
),
formerr_response_header(),
id="shortrecord",
),
pytest.param(
wire(
header(qdcount=1),
# Truncated question (no class)
root(),
u16(dns.rdatatype.RdataType.A),
),
formerr_response_header(),
id="shortquestion",
),
pytest.param(
wire(
header(qdcount=2),
question(
root(),
dns.rdatatype.RdataType.A,
dns.rdataclass.RdataClass.IN,
),
question(
root(),
dns.rdatatype.RdataType.A,
dns.rdataclass.RdataClass(2),
),
),
formerr_response_header(),
id="twoquestionclasses",
),
pytest.param(
wire(
header(arcount=1),
a_rr(owner=oversized_name()),
),
formerr_response_header(),
id="badrecordname",
),
pytest.param(
wire(
header(arcount=2),
a_rr(),
rr(
root(),
dns.rdatatype.RdataType(65280),
dns.rdataclass.RdataClass(256),
rdata=a_rdata(),
),
),
formerr_response_header(),
id="wrongclass",
),
pytest.param(
wire(
header(qdcount=1, arcount=1),
question(root(), dns.rdatatype.RdataType.A),
key_rr(rdclass=dns.rdataclass.RdataClass(2)),
),
wire(
formerr_response_header(qdcount=1),
question(root(), dns.rdatatype.RdataType.A),
),
id="keyclass",
),
pytest.param(
wire(
header(arcount=1),
# OPT owner should be root
opt_rr(owner=name("A.")),
),
formerr_response_header(),
id="optwrongname",
),
pytest.param(
wire(
header(arcount=1),
malformed_rrsig_rr(),
),
formerr_response_header(),
id="malformedrrsig",
),
pytest.param(
wire(
header(opcode=dns.opcode.UPDATE, nscount=1),
rr(
root(),
dns.rdatatype.RdataType.A,
dns.rdataclass.RdataClass.ANY,
ttl=0,
# Non-empty rdata for DELETE type
rdata=b"\x00",
),
),
formerr_response_header(opcode=dns.opcode.UPDATE),
id="malformeddeltype",
),
pytest.param(
wire(
header(arcount=1),
# Class should be ANY not IN
tsig_rr(rdclass=dns.rdataclass.RdataClass.IN),
),
formerr_response_header(),
id="tsigwrongclass",
),
pytest.param(
wire(
# Test non-zero message ID is preserved in the response
header(message_id=67, arcount=2),
tsig_rr(),
# TSIG should be the last record
a_rr(),
),
formerr_response_header(message_id=67),
id="tsignotlast",
),
],
)
def test_formerr(
query_wire: bytes,
expected_wire: bytes,
named_port: int,
ns1,
) -> None:
response_wire = query_raw_tcp(ns1.ip, named_port, query_wire)
assert response_wire == expected_wire