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))