Reimplement the FORMERR system test in Python

Replace the shell and Perl based FORMERR system test with a Python
test that constructs the malformed DNS packets directly and checks
the responses.

Remove the legacy shell and Perl test script and the intermediate
packet files in hex, leaving the packet construction inline in
tests_formerr.py.

Preserve the same wire for all packets sent to the server, but
construct them in a more explicit and readable way.

(cherry picked from commit 151b2d6045)
This commit is contained in:
Štěpán Balážik 2026-04-29 09:52:23 +02:00
parent 95eb81bf1c
commit 28e0c79b12
23 changed files with 525 additions and 465 deletions

View file

@ -1,7 +0,0 @@
# bad NSEC3 owner (:) not in base32hex valid characters
# header: id=0008 opcode=query questions=1 authority=1
00 08 00 00 00 01 00 00 00 01 00 00
# question: ./A/IN
00 00 01 00 01
# :./NSEC3/IN/1 length=7 hashtype=240 flags=0 interations=0 salt=- hashlen=1 hash=ff
01 58 00 00 32 00 01 00 00 00 01 00 07 f0 00 00 00 00 01 ff

View file

@ -1,21 +0,0 @@
# oversized owner name
# header: additional=1
00 00 00 00 00 00 00 00 00 00 00 01
# owner name too big (256 octets) A/IN/1 0.0.0.0
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00
00 01 00 01 00 00 00 01 00 04 00 00 00 00

View file

@ -1,8 +0,0 @@
# multiple singletons (SOA)
# header questions=1 answers=2
00 00 00 00 00 01 00 02 00 00 00 00
# question SOA/IN
00 00 06 00 01
# 2 SOA records that differ in expire
00 00 06 00 01 00 00 00 01 00 16 00 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05
00 00 06 00 01 00 00 00 01 00 16 00 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 06

View file

@ -1,10 +0,0 @@
# header: 2 questions
00 00 00 00 00 02 00 00 00 00 00 00
# question: AAAAAAAAAAAAAA./A/IN
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00
00 01
00 01
# question: AAAAAAAAAAAAAA./A/IN
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00
00 01
00 01

View file

@ -1,97 +0,0 @@
#!/usr/bin/perl
# 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.
# This is a tool for sending an arbitrary packet via UDP or TCP to an
# arbitrary address and port. The packet is specified in a file or on
# the standard input, in the form of a series of bytes in hexadecimal.
# Whitespace is ignored, as is anything following a '#' symbol.
#
# For example, the following input would generate normal query for
# isc.org/NS/IN":
#
# # QID:
# 0c d8
# # header:
# 01 00 00 01 00 00 00 00 00 00
# # qname isc.org:
# 03 69 73 63 03 6f 72 67 00
# # qtype NS:
# 00 02
# # qclass IN:
# 00 01
#
# Note that we do not wait for a response for the server. This is simply
# a way of injecting arbitrary packets to test server resposnes.
#
# Usage: packet.pl [-a <address>] [-p <port>] [-t (udp|tcp)] [filename]
#
# If not specified, address defaults to 127.0.0.1, port to 53, protocol
# to udp, and file to stdin.
#
# XXX: Doesn't support IPv6 yet
require 5.006_001;
use strict;
use Getopt::Std;
use IO::File;
use IO::Socket;
sub usage {
print ("Usage: packet.pl [-a address] [-p port] [file]\n");
exit 1;
}
my %options={};
getopts("a:p:", \%options);
my $addr = "127.0.0.1";
$addr = $options{a} if defined $options{a};
my $port = 53;
$port = $options{p} if defined $options{p};
my $file = "STDIN";
if (@ARGV >= 1) {
my $filename = shift @ARGV;
open FH, "<$filename" or die "$filename: $!";
$file = "FH";
}
my $input = "";
while (defined(my $line = <$file>) ) {
chomp $line;
$line =~ s/#.*$//;
$input .= $line;
}
$input =~ s/\s+//g;
my $data = pack("H*", $input);
my $len = length $data;
my $output = unpack("H*", $data);
print ("sending: $output\n");
my $sock = IO::Socket::INET->new(PeerAddr => $addr, PeerPort => $port,
Proto => "tcp") or die "$!";
my $bytes;
$bytes = $sock->syswrite(pack("n", $len), 2);
$bytes = $sock->syswrite($data, $len);
$bytes = $sock->sysread($data, 2);
$len = unpack("n", $data);
$bytes = $sock->sysread($data, $len);
print "got: ", unpack("H*", $data). "\n";
$sock->close;
close $file;

View file

@ -1,7 +0,0 @@
# mismatched key class
# header: questions=1 additional=1
00 00 00 00 00 01 00 00 00 00 00 01
# question: ./A/IN
00 00 01 00 01
# additional: ./KEY/CLASS2 flags=0 protocol=0 algorithm=248 keydata=00
00 00 19 00 02 00 00 00 01 00 05 00 00 00 f8 00

View file

@ -1,5 +0,0 @@
# UPDATE malformed 'delete type' update change (non empty data)
# header: UPDATE authority=1
00 00 28 00 00 00 00 00 00 01 00 00
# ./A/ANY TTL=0 length=1 data=00
00 00 01 00 ff 00 00 00 00 00 01 00

View file

@ -1,5 +0,0 @@
# malformed RRRSIG
# header: QUERY, additional=1
00 00 00 00 00 00 00 00 00 00 00 01
# ./RRSIG/IN TTL=1 covers=0 algorithm=240 labels=0 ttl=1 expire=2 signed=3 id=0
00 00 2e 00 01 00 00 00 01 00 14 00 00 f0 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 00

View file

@ -1,19 +0,0 @@
00 00 00 00 00 01 00 00 00 00 00 00
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00
00 01
00 01

View file

@ -1 +0,0 @@
00 00 00 00 00 00 00 00 00 00 00 00

View file

@ -1,5 +0,0 @@
# OPT record with wrong name (not .)
# header: QUERY, additional=1
00 00 00 00 00 00 00 00 00 00 00 01
# OPT record (owner A.)
01 41 00 00 29 00 01 00 00 00 00 00 00

View file

@ -1,5 +0,0 @@
# QTYPE-only type as answer
# header, answers=1
00 00 00 00 00 00 00 01 00 00 00 00
# ./MAILB/IN
00 00 fd 00 01 00 00 00 01 00 00

View file

@ -1,7 +0,0 @@
# two questions of different classes
# header: QUERY, questions=2
00 00 00 00 00 02 00 00 00 00 00 00
# ./A/IN
00 00 01 00 01
# ./A/CLASS2
00 00 01 00 02

View file

@ -1,5 +0,0 @@
# truncated question section
# header: QUERY, questions=1
00 00 00 00 00 01 00 00 00 00 00 00
# truncated question (no class)
00 00 01

View file

@ -1,5 +0,0 @@
# truncated record
# header: QUERY, additional=1
00 09 00 00 00 00 00 00 00 00 00 01
# truncated A record (no ttl, length or data)
00 00 01 00 01

View file

@ -1,177 +0,0 @@
#!/bin/sh
# 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.
set -e
. ../conf.sh
status=0
echo_i "test name too long"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} nametoolong >nametoolong.out
ans=$(grep got: nametoolong.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$((status + 1))
fi
echo_i "two question names"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} twoquestionnames >twoquestionnames.out
ans=$(grep got: twoquestionnames.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$((status + 1))
fi
echo_i "two question types"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} twoquestiontypes >twoquestiontypes.out
ans=$(grep got: twoquestiontypes.out)
if [ "${ans}" != "got: 0000800100020000000000000e41414141414141414141414141410000010001c00c00020001" ]; then
echo_i "failed"
status=$((status + 1))
fi
echo_i "duplicate questions"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} dupquestion >dupquestion.out
ans=$(grep got: dupquestion.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "duplicate answer"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} dupans >dupans.out
ans=$(grep got: dupans.out)
if [ "${ans}" != "got: 0000800100010000000000000000060001" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "question only type in answer"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} qtypeasanswer >qtypeasanswer.out
ans=$(grep got: qtypeasanswer.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
# this would be NOERROR if it included a COOKIE option,
# but is a FORMERR without one.
echo_i "empty question section (and no COOKIE option)"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} noquestions >noquestions.out
ans=$(grep got: noquestions.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$((status + 1))
fi
echo_i "bad nsec3 owner"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} badnsec3owner >badnsec3owner.out
ans=$(grep got: badnsec3owner.out)
# SERVFAIL (2) rather than FORMERR (1)
if [ "${ans}" != "got: 0008800200010000000000000000010001" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "short record before rdata "
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} shortrecord >shortrecord.out
ans=$(grep got: shortrecord.out)
if [ "${ans}" != "got: 000980010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "short question"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} shortquestion >shortquestion.out
ans=$(grep got: shortquestion.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "mismatch classes in question section"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} questionclass >questionclass.out
ans=$(grep got: questionclass.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "bad record owner name"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} badrecordname >badrecordname.out
ans=$(grep got: badrecordname.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "mismatched class in record"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} wrongclass >wrongclass.out
ans=$(grep got: wrongclass.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "mismatched KEY class"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} keyclass >keyclass.out
ans=$(grep got: keyclass.out)
if [ "${ans}" != "got: 0000800100010000000000000000010001" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "OPT wrong owner name"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} optwrongname >optwrongname.out
ans=$(grep got: optwrongname.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "RRSIG invalid covers"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} malformedrrsig >malformedrrsig.out
ans=$(grep got: malformedrrsig.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "UPDATE malformed delete type"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} malformeddeltype >malformeddeltype.out
ans=$(grep got: malformeddeltype.out)
if [ "${ans}" != "got: 0000a8010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "TSIG wrong class"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} tsigwrongclass >tsigwrongclass.out
ans=$(grep got: tsigwrongclass.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "TSIG not last"
$PERL formerr.pl -a 10.53.0.1 -p ${PORT} tsignotlast >tsignotlast.out
ans=$(grep got: tsignotlast.out)
if [ "${ans}" != "got: 000080010000000000000000" ]; then
echo_i "failed"
status=$(expr $status + 1)
fi
echo_i "exit status: $status"
[ $status -eq 0 ] || exit 1

View file

@ -0,0 +1,525 @@
# 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 name_pointer(offset: int) -> bytes:
return u16(0xC000 | offset)
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,
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,
ttl=1,
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,
ttl=1,
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,
ttl=1,
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,
ttl=1,
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,
ttl=1,
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,
ttl=1,
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),
),
wire(
formerr_response_header(qdcount=2),
question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
question(name_pointer(12), dns.rdatatype.RdataType.NS),
),
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,
ttl=1,
),
),
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(message_id=8, 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(
message_id=8, rcode=dns.rcode.SERVFAIL, qdcount=1
),
question(root(), dns.rdatatype.RdataType.A),
),
id="badnsec3owner",
),
pytest.param(
wire(
header(message_id=9, arcount=1),
# Truncated A record (no ttl, length or data)
question(root(), dns.rdatatype.RdataType.A),
),
formerr_response_header(message_id=9),
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="questionclass",
),
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),
ttl=33554433,
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(
header(arcount=2),
tsig_rr(),
# TSIG should be the last record
a_rr(),
),
formerr_response_header(),
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

View file

@ -1,40 +0,0 @@
# 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 pytest
pytestmark = pytest.mark.extra_artifacts(
[
"badnsec3owner.out",
"badrecordname.out",
"dupans.out",
"dupquestion.out",
"keyclass.out",
"malformeddeltype.out",
"malformedrrsig.out",
"nametoolong.out",
"noquestions.out",
"optwrongname.out",
"qtypeasanswer.out",
"questionclass.out",
"shortquestion.out",
"shortrecord.out",
"tsignotlast.out",
"tsigwrongclass.out",
"twoquestionnames.out",
"twoquestiontypes.out",
"wrongclass.out",
]
)
def test_formerr(run_tests_sh):
run_tests_sh()

View file

@ -1,8 +0,0 @@
# SIG not last in additional
# header: QUERY additional=2
00 00 00 00 00 00 00 00 00 00 00 02
# Additional records:
# SIG record (class ANY)
00 00 fa 00 ff 00 00 00 01 00 12 00 01 02 03 04 05 06 01 02 00 01 00 00 00 00 00 00 00
# A record
00 00 01 00 01 00 00 00 01 00 04 00 00 00 00

View file

@ -1,5 +0,0 @@
# TSIG wrong class
# header: QUERY, additional=1
00 00 00 00 00 00 00 00 00 00 00 01
# class should be ANY (00 ff) not IN (00 01)
00 00 fa 00 01 00 00 00 01 00 12 00 01 02 03 04 05 06 01 02 00 01 00 00 00 00 00 00 00

View file

@ -1,11 +0,0 @@
# two questions with different names
00 00 00 00 00 02 00 00 00 00 00 00
# AAAAAAAAAAAAAA./A/IN
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00
00 01
00 01
# AAAAAAAAAAAAAB./A/IN
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 42 00
00 01
00 01

View file

@ -1,10 +0,0 @@
# two questions that differ by type
00 00 00 00 00 02 00 00 00 00 00 00
# AAAAAAAAAAAAAAA./A/IN
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00
00 01
00 01
# AAAAAAAAAAAAAAA./NS/IN
0e 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00
00 02
00 01

View file

@ -1,7 +0,0 @@
# class mismatch
# header: QUERY, additional=2
00 00 00 00 00 00 00 00 00 00 00 02
# ./A/IN
00 00 01 00 01 00 00 00 01 00 04 00 00 00 00
# ./TYPE65280/CLASS256
00 ff 00 01 00 02 00 00 01 00 04 00 00 00 00