Commit 2cdee60cb6 for bind
commit 2cdee60cb6ac79f95c2ebef23ea59e11ff93d646
Author: Nicki Křížek <nicki@isc.org>
Date: Mon Sep 21 12:49:33 2026 +0000
Replace authsock.pl with isctest.tools.authsock
The mock update-policy "external" daemon used by nsupdate and tsiggss
becomes a Python script with the same options and the same per-request
log line, which nsupdate's tests.sh greps for the client address.
Assisted-by: Claude:claude-fable-5-1
diff --git a/bin/tests/system/authsock.pl b/bin/tests/system/authsock.pl
deleted file mode 100644
index 2829abb382..0000000000
--- a/bin/tests/system/authsock.pl
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/usr/bin/env 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.
-
-# test the update-policy external protocol
-
-require 5.6.0;
-
-use IO::File;
-use IO::Socket::UNIX;
-use Getopt::Long;
-
-my $path;
-my $typeallowed = "A";
-my $pidfile = "authsock.pid";
-my $timeout = 0;
-
-GetOptions("path=s" => \$path,
- "type=s" => \$typeallowed,
- "pidfile=s" => \$pidfile,
- "timeout=i" => \$timeout);
-
-STDOUT->autoflush(1);
-
-if (!defined($path)) {
- print("Usage: authsock.pl --path=<sockpath> --type=type --pidfile=pidfile\n");
- exit(1);
-}
-
-unlink($path);
-my $server = IO::Socket::UNIX->new(Local => $path, Type => SOCK_STREAM, Listen => 8) or
- die "unable to create socket $path";
-chmod 0777, $path;
-
-# setup our pidfile
-open(my $pid,">",$pidfile)
- or die "unable to open pidfile $pidfile";
-print $pid "$$\n";
-close($pid);
-
-if ($timeout != 0) {
- # die after the given timeout
- alarm($timeout);
-}
-
-while (my $client = $server->accept()) {
- $client->recv(my $buf, 8, 0);
- my ($version, $req_len) = unpack('N N', $buf);
-
- if ($version != 1 || $req_len < 17) {
- printf("Badly formatted request\n");
- $client->send(pack('N', 2));
- next;
- }
-
- $client->recv(my $buf, $req_len - 8, 0);
-
- my ($signer,
- $name,
- $addr,
- $type,
- $key,
- $key_data) = unpack('Z* Z* Z* Z* Z* N/a', $buf);
-
- if ($req_len != length($buf)+8) {
- printf("Length mismatch %u %u\n", $req_len, length($buf)+8);
- $client->send(pack('N', 2));
- next;
- }
-
- printf("version=%u signer=%s name=%s addr=%s type=%s key=%s key_data_len=%u\n",
- $version, $signer, $name, $addr, $type, $key, length($key_data));
-
- my $result;
- if ($typeallowed eq $type) {
- $result = 1;
- printf("allowed type %s == %s\n", $type, $typeallowed);
- } else {
- printf("disallowed type %s != %s\n", $type, $typeallowed);
- $result = 0;
- }
-
- $reply = pack('N', $result);
- $client->send($reply);
-}
diff --git a/bin/tests/system/isctest/tools/authsock.py b/bin/tests/system/isctest/tools/authsock.py
new file mode 100644
index 0000000000..987f4ecd1f
--- /dev/null
+++ b/bin/tests/system/isctest/tools/authsock.py
@@ -0,0 +1,158 @@
+# 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.
+
+"""
+Mock authorization daemon for update-policy "external" rules.
+
+Listen on a Unix stream socket and answer every update-policy check
+named sends: allow the update when it is for the one RR type given on
+the command line, deny it for any other. Log each request and the
+decision on stdout; tests start the daemon in the background and grep
+that log.
+
+The request and reply formats are documented in the ARM under the
+"external" update-policy rule (doc/arm/reference.rst) and produced by
+dns_ssu_external_match() in lib/dns/ssu_external.c. This tool speaks
+protocol version 1. The reply is a 4-byte integer in network byte
+order: 0 denies the update and 1 allows it; 2 is sent for a malformed
+request, which named also treats as a denial.
+"""
+
+import argparse
+import os
+import signal
+import socket
+import struct
+
+VERSION = 1
+HEADER = struct.Struct("!II") # protocol version, total request length
+WORD = struct.Struct("!I")
+# Shortest possible request: header, five empty NUL-terminated strings
+# and an empty TKEY token.
+MIN_REQUEST_LEN = HEADER.size + 5 + WORD.size
+REPLY_DENY = WORD.pack(0)
+REPLY_ALLOW = WORD.pack(1)
+REPLY_ERROR = WORD.pack(2)
+
+
+def parse_request(body: bytes) -> tuple[list[str], bytes]:
+ """
+ Split a request body, everything after the version and length
+ header, into its fields as the ARM lays them out: signer, name,
+ TCP source address, rdata type and key as NUL-terminated strings,
+ then the TKEY token length (4 bytes, network byte order) and the
+ token, which fills the rest of the body. Raise ValueError if the
+ body does not have that shape.
+ """
+ fields = []
+ rest = body
+ for _ in range(5):
+ value, sep, rest = rest.partition(b"\0")
+ if not sep:
+ raise ValueError("fewer than five NUL-terminated strings")
+ fields.append(value.decode())
+ if len(rest) < WORD.size:
+ raise ValueError("missing TKEY token length")
+ (token_len,) = WORD.unpack_from(rest)
+ token = rest[WORD.size :]
+ if len(token) != token_len:
+ raise ValueError(f"TKEY token length {token_len} != {len(token)}")
+ return fields, token
+
+
+def authorize(rdtype: str, allowed_type: str) -> bytes:
+ """
+ Allow the update if it is for the one permitted RR type, deny it
+ otherwise. Return the packed reply.
+ """
+ if rdtype == allowed_type:
+ print(f"allowed type {rdtype} == {allowed_type}", flush=True)
+ return REPLY_ALLOW
+ print(f"disallowed type {rdtype} != {allowed_type}", flush=True)
+ return REPLY_DENY
+
+
+def handle_request(conn: socket.socket, allowed_type: str) -> None:
+ """
+ Answer one request on an accepted connection.
+ """
+ header = conn.recv(HEADER.size, socket.MSG_WAITALL)
+ if len(header) < HEADER.size:
+ print(f"Short request header: {header.hex()}", flush=True)
+ conn.sendall(REPLY_ERROR)
+ return
+ version, req_len = HEADER.unpack(header)
+ if version != VERSION or req_len < MIN_REQUEST_LEN:
+ print(f"Badly formatted request: {header.hex()}", flush=True)
+ conn.sendall(REPLY_ERROR)
+ return
+
+ body = conn.recv(req_len - HEADER.size, socket.MSG_WAITALL)
+ if len(body) + HEADER.size != req_len:
+ print(f"Length mismatch {req_len} {len(body) + HEADER.size}", flush=True)
+ conn.sendall(REPLY_ERROR)
+ return
+
+ try:
+ (signer, name, addr, rdtype, key), token = parse_request(body)
+ except ValueError as exc:
+ print(f"Badly formatted request: {exc}: {body.hex()}", flush=True)
+ conn.sendall(REPLY_ERROR)
+ return
+ print(
+ f"version={version} signer={signer} name={name} addr={addr} "
+ f"type={rdtype} key={key} key_data_len={len(token)}",
+ flush=True,
+ )
+ conn.sendall(authorize(rdtype, allowed_type))
+
+
+def serve(path: str, allowed_type: str) -> None:
+ """
+ Listen on the Unix socket at path and answer requests forever.
+ """
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
+ server.bind(path)
+ server.listen()
+ os.chmod(path, 0o777)
+ while True:
+ conn, _ = server.accept()
+ with conn:
+ handle_request(conn, allowed_type)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(prog="authsock", description=__doc__)
+ parser.add_argument("--path", required=True, help="Unix socket path to listen on")
+ parser.add_argument(
+ "--type", default="A", help="the one RR type to allow (default: %(default)s)"
+ )
+ parser.add_argument(
+ "--pidfile", default="authsock.pid", help="where to write the process id"
+ )
+ parser.add_argument(
+ "--timeout",
+ type=int,
+ default=0,
+ help="exit after this many seconds (default: run until killed)",
+ )
+ args = parser.parse_args()
+
+ with open(args.pidfile, "w", encoding="utf-8") as pidfile:
+ print(os.getpid(), file=pidfile)
+ if args.timeout:
+ # The default SIGALRM disposition terminates the process.
+ signal.alarm(args.timeout)
+ serve(args.path, args.type)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bin/tests/system/nsupdate/tests.sh b/bin/tests/system/nsupdate/tests.sh
index e016fe81a0..f42ddcbc10 100755
--- a/bin/tests/system/nsupdate/tests.sh
+++ b/bin/tests/system/nsupdate/tests.sh
@@ -2436,7 +2436,7 @@ wait_for_log 10 "too many DNS UPDATEs queued" ns1/named.run || ret=1
n=$((n + 1))
ret=0
echo_i "check that grant external pass client address properly ($n)" {
-($PERL "${TOP_SRCDIR}/bin/tests/system/authsock.pl" --type=CNAME --path=ns1/auth.sock --pidfile=authsock.pid --timeout=120 >authsock.out.test$n 2>&1 &) &
+($PYTHON -m isctest.tools.authsock --type=CNAME --path=ns1/auth.sock --pidfile=authsock.pid --timeout=120 >authsock.out.test$n 2>&1 &) &
sleep 1
nextpart authsock.out.test$n >/dev/null
$NSUPDATE -k ns1/ddns.key -d <<EOF >nsupdate.udp.test$n 2>&1 || ret=1
diff --git a/bin/tests/system/tsiggss/tests.sh b/bin/tests/system/tsiggss/tests.sh
index 482b940e90..696351d656 100644
--- a/bin/tests/system/tsiggss/tests.sh
+++ b/bin/tests/system/tsiggss/tests.sh
@@ -120,7 +120,7 @@ status=$((status + ret))
echo_i "testing external update policy (CNAME) with auth sock ($n)"
ret=0
-($PERL "${TOP_SRCDIR}/bin/tests/system/authsock.pl" --type=CNAME --path=ns1/auth.sock --pidfile=authsock.pid --timeout=120 >/dev/null 2>&1 &) &
+($PYTHON -m isctest.tools.authsock --type=CNAME --path=ns1/auth.sock --pidfile=authsock.pid --timeout=120 >/dev/null 2>&1 &) &
sleep 1
test_update $n testcname.example.nil. CNAME "86400 CNAME testdenied.example.nil" "testdenied" || ret=1
n=$((n + 1))