From: Štěpán Balážik Date: Wed, 29 Apr 2026 07:52:23 +0000 (+0200) Subject: Reimplement the FORMERR system test in Python X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=151b2d60456f2b12cfdc9e16c9b4d63b8eafba32;p=thirdparty%2Fbind9.git 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. --- diff --git a/bin/tests/system/formerr/badnsec3owner b/bin/tests/system/formerr/badnsec3owner deleted file mode 100644 index de6e692b17b..00000000000 --- a/bin/tests/system/formerr/badnsec3owner +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/badrecordname b/bin/tests/system/formerr/badrecordname deleted file mode 100644 index f79df49181b..00000000000 --- a/bin/tests/system/formerr/badrecordname +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/dupans b/bin/tests/system/formerr/dupans deleted file mode 100644 index 142022a7e93..00000000000 --- a/bin/tests/system/formerr/dupans +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/dupquestion b/bin/tests/system/formerr/dupquestion deleted file mode 100644 index 327b8ca2588..00000000000 --- a/bin/tests/system/formerr/dupquestion +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/formerr.pl b/bin/tests/system/formerr/formerr.pl deleted file mode 100644 index fd7d298a9dd..00000000000 --- a/bin/tests/system/formerr/formerr.pl +++ /dev/null @@ -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
] [-p ] [-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; diff --git a/bin/tests/system/formerr/keyclass b/bin/tests/system/formerr/keyclass deleted file mode 100644 index c84568afee1..00000000000 --- a/bin/tests/system/formerr/keyclass +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/malformeddeltype b/bin/tests/system/formerr/malformeddeltype deleted file mode 100644 index 2d14ae41de6..00000000000 --- a/bin/tests/system/formerr/malformeddeltype +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/malformedrrsig b/bin/tests/system/formerr/malformedrrsig deleted file mode 100644 index 07d2af021ad..00000000000 --- a/bin/tests/system/formerr/malformedrrsig +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/nametoolong b/bin/tests/system/formerr/nametoolong deleted file mode 100644 index b81545fadc5..00000000000 --- a/bin/tests/system/formerr/nametoolong +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/noquestions b/bin/tests/system/formerr/noquestions deleted file mode 100644 index f087bcd4b37..00000000000 --- a/bin/tests/system/formerr/noquestions +++ /dev/null @@ -1 +0,0 @@ -00 00 00 00 00 00 00 00 00 00 00 00 diff --git a/bin/tests/system/formerr/optwrongname b/bin/tests/system/formerr/optwrongname deleted file mode 100644 index 7b486d932b6..00000000000 --- a/bin/tests/system/formerr/optwrongname +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/qtypeasanswer b/bin/tests/system/formerr/qtypeasanswer deleted file mode 100644 index d254d1d42f9..00000000000 --- a/bin/tests/system/formerr/qtypeasanswer +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/questionclass b/bin/tests/system/formerr/questionclass deleted file mode 100644 index b34d1e30f52..00000000000 --- a/bin/tests/system/formerr/questionclass +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/shortquestion b/bin/tests/system/formerr/shortquestion deleted file mode 100644 index 168ed902edd..00000000000 --- a/bin/tests/system/formerr/shortquestion +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/shortrecord b/bin/tests/system/formerr/shortrecord deleted file mode 100644 index d9a2ab7c43f..00000000000 --- a/bin/tests/system/formerr/shortrecord +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/tests.sh b/bin/tests/system/formerr/tests.sh deleted file mode 100644 index a30198d7bd7..00000000000 --- a/bin/tests/system/formerr/tests.sh +++ /dev/null @@ -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: 000080010000000000000000" ]; 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 diff --git a/bin/tests/system/formerr/tests_formerr.py b/bin/tests/system/formerr/tests_formerr.py new file mode 100644 index 00000000000..7d6dfa0b267 --- /dev/null +++ b/bin/tests/system/formerr/tests_formerr.py @@ -0,0 +1,517 @@ +# 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, + 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), + ), + 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, + 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 diff --git a/bin/tests/system/formerr/tests_sh_formerr.py b/bin/tests/system/formerr/tests_sh_formerr.py deleted file mode 100644 index a091546d51c..00000000000 --- a/bin/tests/system/formerr/tests_sh_formerr.py +++ /dev/null @@ -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() diff --git a/bin/tests/system/formerr/tsignotlast b/bin/tests/system/formerr/tsignotlast deleted file mode 100644 index 108936c51f6..00000000000 --- a/bin/tests/system/formerr/tsignotlast +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/tsigwrongclass b/bin/tests/system/formerr/tsigwrongclass deleted file mode 100644 index 29aea084aa0..00000000000 --- a/bin/tests/system/formerr/tsigwrongclass +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/twoquestionnames b/bin/tests/system/formerr/twoquestionnames deleted file mode 100644 index f363a23c357..00000000000 --- a/bin/tests/system/formerr/twoquestionnames +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/twoquestiontypes b/bin/tests/system/formerr/twoquestiontypes deleted file mode 100644 index 451f8f6ee4e..00000000000 --- a/bin/tests/system/formerr/twoquestiontypes +++ /dev/null @@ -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 diff --git a/bin/tests/system/formerr/wrongclass b/bin/tests/system/formerr/wrongclass deleted file mode 100644 index 96211e07a8f..00000000000 --- a/bin/tests/system/formerr/wrongclass +++ /dev/null @@ -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