Commit d3665612f0 for bind

commit d3665612f0bb6f3d825f57c0d767b8a20c3a2450
Author: Nicki Křížek <nicki@isc.org>
Date:   Thu Sep 10 15:28:20 2026 +0000

    Replace testsock.pl and testsock6.pl with a Python tool

    Both interface-presence probes become one isctest.tools.testsock
    module: without arguments it sweeps the 10.53.0.* addresses read from
    ifconfig.sh.in (formerly testsock.pl), with explicit addresses it
    checks just those (formerly testsock6.pl).  The pytest runner performs
    the check in-process; start.pl and the legacy tests.sh call sites run
    the tool with $PYTHON -m.

    The IO::Socket::IP availability gate in testsock6() is gone with the
    Perl: the probe always runs now, so the IPv6 test sections it guards
    can no longer be silently skipped for lack of a Perl module.

    Assisted-by: Claude:claude-fable-5

diff --git a/bin/tests/system/conf.sh b/bin/tests/system/conf.sh
index 1b180dda1a..2f78df6372 100644
--- a/bin/tests/system/conf.sh
+++ b/bin/tests/system/conf.sh
@@ -25,11 +25,7 @@ fi
 export PYTHONPATH="$TOP_SRCDIR/bin/tests/system${PYTHONPATH:+:$PYTHONPATH}"

 testsock6() {
-  if test -n "$PERL" && $PERL -e "use IO::Socket::IP;" 2>/dev/null; then
-    $PERL "$TOP_SRCDIR/bin/tests/system/testsock6.pl" "$@"
-  else
-    false
-  fi
+  $PYTHON -m isctest.tools.testsock "$@"
 }

 echofail() {
diff --git a/bin/tests/system/conftest.py b/bin/tests/system/conftest.py
index d3a25cb3aa..1713c42e44 100644
--- a/bin/tests/system/conftest.py
+++ b/bin/tests/system/conftest.py
@@ -12,6 +12,7 @@
 from pathlib import Path
 from re import compile as Re

+import errno
 import filecmp
 import os
 import shutil
@@ -28,6 +29,7 @@ pytest.register_assert_rewrite("isctest")
 from isctest.vars.build import SYSTEM_TEST_DIR_GIT_PATH

 import isctest
+import isctest.tools.testsock

 # pylint: enable=wrong-import-position

@@ -585,12 +587,14 @@ def system_test(
     """

     def check_net_interfaces():
+        port = int(os.environ["PORT"])
         try:
-            isctest.run.perl(
-                f"{os.environ['srcdir']}/testsock.pl", ["-p", os.environ["PORT"]]
-            )
-        except subprocess.CalledProcessError as exc:
-            isctest.log.error("testsock.pl: exited with code %d", exc.returncode)
+            for check_port in range(port, port + isctest.vars.ports.PORTS_PER_TEST):
+                isctest.tools.testsock.check_ipv4_interfaces(check_port)
+        except OSError as exc:
+            isctest.log.error("testsock: %s", exc)
+            if exc.errno == errno.EADDRINUSE:
+                raise RuntimeError(f"test port range is in use: {exc}") from exc
             pytest.skip("Network interface aliases not set up.")

     def setup_test():
diff --git a/bin/tests/system/dns64/tests.sh b/bin/tests/system/dns64/tests.sh
index 310ccfc11a..d8ecc3ba74 100644
--- a/bin/tests/system/dns64/tests.sh
+++ b/bin/tests/system/dns64/tests.sh
@@ -1479,7 +1479,7 @@ n=$((n + 1))
 if [ $ret != 0 ]; then echo_i "failed"; fi
 status=$((status + ret))

-if $PERL ../testsock6.pl fd92:7065:b8e:fffe::10.53.0.4 2>/dev/null; then
+if testsock6 fd92:7065:b8e:fffe::10.53.0.4 2>/dev/null; then
   echo_i "checking resolver-use-dns64 ($n)"
   ret=0
   $DIG $DIGOPTS @10.53.0.3 no-aaaa aaaa >dig.out.ns3.test$n || ret=1
diff --git a/bin/tests/system/ifconfig.sh.in b/bin/tests/system/ifconfig.sh.in
index 126777c658..aea9a3f6ca 100755
--- a/bin/tests/system/ifconfig.sh.in
+++ b/bin/tests/system/ifconfig.sh.in
@@ -28,7 +28,7 @@
 # interface MTU.
 #
 # See also org.isc.bind.system (a version of this script for use on macOS)
-# and testsock.pl (which checks the interfaces are configured)
+# and isctest/tools/testsock.py (which checks the interfaces are configured)
 #

 sys=@SYSTEM@
@@ -172,7 +172,7 @@ sequence() (
 #
 # See also `org.isc.bind.system`.
 #
-# This `max` setting is grepped out for use by testsock.pl
+# This `max` setting is read by isctest/tools/testsock.py
 #
 max=11
 case $1 in
diff --git a/bin/tests/system/isctest/tools/testsock.py b/bin/tests/system/isctest/tools/testsock.py
new file mode 100644
index 0000000000..862be20671
--- /dev/null
+++ b/bin/tests/system/isctest/tools/testsock.py
@@ -0,0 +1,106 @@
+# 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.
+
+"""
+Check that the test network interfaces are up.
+
+Try to bind() a UDP socket on each of the given addresses (IPv4 or IPv6).
+
+When no address is specified, check the 10.53.0.* test addresses.
+"""
+
+from collections.abc import Iterable
+from pathlib import Path
+
+import argparse
+import re
+import socket
+import sys
+
+import dns.inet
+
+# ifconfig.sh.in sets the test interfaces up; its max= setting is the
+# authoritative count of the 10.53.0.* addresses.
+IFCONFIG_SCRIPT = Path(__file__).resolve().parents[2] / "ifconfig.sh.in"
+
+
+def check_addr(address: str, port: int = 0) -> None:
+    """
+    Try to bind a UDP socket to the given address and port; raise
+    OSError on failure and ValueError for a malformed address.
+    """
+    try:
+        family = dns.inet.af_for_address(address)
+    except ValueError:
+        raise ValueError(f"{address}: not an IPv4 or IPv6 address") from None
+    with socket.socket(family, socket.SOCK_DGRAM) as sock:
+        try:
+            sock.bind((address, port))
+        except OSError as exc:
+            raise OSError(
+                exc.errno, f"bind({address}, {port}): {exc.strerror}"
+            ) from exc
+
+
+def interface_ids() -> range:
+    """
+    Return the range of configured test interface ids, read from the
+    max= setting in ifconfig.sh.in.
+    """
+    matches = re.findall(
+        r"^max=(\d+)\s*$",
+        IFCONFIG_SCRIPT.read_text(encoding="utf-8"),
+        flags=re.MULTILINE,
+    )
+    if not matches:
+        raise RuntimeError(f"could not find max IP address in {IFCONFIG_SCRIPT}")
+    return range(1, int(matches[-1]) + 1)
+
+
+def check_ipv4_interfaces(port: int = 0, server_id: int | None = None) -> None:
+    """
+    Check that the 10.53.0.* test interfaces (or just 10.53.0.<server_id>)
+    are up and the given port can be bound on them; raise OSError on
+    failure.
+    """
+    ids: Iterable[int]
+    if server_id is not None:
+        ids = [server_id]
+    else:
+        ids = interface_ids()
+    for interface_id in ids:
+        check_addr(f"10.53.0.{interface_id}", port)
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(prog="testsock", description=__doc__)
+    parser.add_argument(
+        "-p", "--port", type=int, default=0, help="UDP port to bind (default: any)"
+    )
+    parser.add_argument(
+        "-i", "--id", type=int, help="check only the 10.53.0.<id> interface"
+    )
+    parser.add_argument(
+        "address", nargs="*", help="addresses to check instead of 10.53.0.*"
+    )
+    args = parser.parse_args()
+    try:
+        if args.address:
+            for address in args.address:
+                check_addr(address, args.port)
+        else:
+            check_ipv4_interfaces(args.port, args.id)
+    except (OSError, ValueError) as exc:
+        sys.exit(f"testsock: {exc}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/org.isc.bind.system b/bin/tests/system/org.isc.bind.system
index 48a5756eaa..bf68419ca8 100644
--- a/bin/tests/system/org.isc.bind.system
+++ b/bin/tests/system/org.isc.bind.system
@@ -11,7 +11,7 @@
 # See the COPYRIGHT file distributed with this work for additional
 # information regarding copyright ownership.

-# see also ifconfig.sh.in and testsock.pl
+# see also ifconfig.sh.in and isctest/tools/testsock.py

 ifup() {
 	/sbin/ifconfig lo0 10.53.$1.$3 alias
diff --git a/bin/tests/system/start.pl b/bin/tests/system/start.pl
index 57d99270ef..551b6da369 100755
--- a/bin/tests/system/start.pl
+++ b/bin/tests/system/start.pl
@@ -100,6 +100,14 @@ my $DIG = $ENV{'DIG'};
 my $PERL = $ENV{'PERL'};
 my $PYTHON = $ENV{'PYTHON'};

+# Make the isctest package importable, for the standalone helper tools
+# ($PYTHON -m isctest.tools.<name>).
+if (defined $ENV{'PYTHONPATH'}) {
+	$ENV{'PYTHONPATH'} = "$srcdir:$ENV{'PYTHONPATH'}";
+} else {
+	$ENV{'PYTHONPATH'} = $srcdir;
+}
+
 # Start the server(s)

 my @ns;
@@ -173,7 +181,7 @@ sub check_ns_port {
 	my $tries = 0;

 	while (1) {
-		my $return = system("$PERL $srcdir/testsock.pl -p $port $options");
+		my $return = system("$PYTHON -m isctest.tools.testsock -p $port $options");

 		if ($return == 0) {
 			last;
diff --git a/bin/tests/system/testsock.pl b/bin/tests/system/testsock.pl
deleted file mode 100755
index e793874d93..0000000000
--- a/bin/tests/system/testsock.pl
+++ /dev/null
@@ -1,55 +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.
-
-# Test whether the interfaces on 10.53.0.* are up.
-
-require 5.001;
-
-use Cwd 'abs_path';
-use File::Basename;
-use Socket;
-use Getopt::Long;
-
-my $port = 0;
-my $id = 0;
-GetOptions("p=i" => \$port,
-           "i=i" => \$id);
-
-my @ids;
-if ($id != 0) {
-	@ids = ($id);
-} else {
-	my $dir = dirname(abs_path($0));
-	my $fn = "$dir/ifconfig.sh.in";
-	open FH, "< $fn" or die "open < $fn: $!\n";
-	while (<FH>) {
-		@ids = (1..$1)
-		    if /^max=(\d+)\s*$/;
-	}
-	close FH;
-	die "could not find max IP address in $fn\n"
-	    unless @ids > 1;
-}
-
-foreach $id (@ids) {
-        my $addr = pack("C4", 10, 53, 0, $id);
-	my $sa = pack_sockaddr_in($port, $addr);
-	socket(SOCK, PF_INET, SOCK_STREAM, getprotobyname("tcp"))
-      		or die "$0: socket: $!\n";
-	setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
-
-	bind(SOCK, $sa)
-	    	or die sprintf("$0: bind(%s, %d): $!\n",
-			       inet_ntoa($addr), $port);
-	close(SOCK);
-}
diff --git a/bin/tests/system/testsock6.pl b/bin/tests/system/testsock6.pl
deleted file mode 100644
index 9d4e5b7a8f..0000000000
--- a/bin/tests/system/testsock6.pl
+++ /dev/null
@@ -1,26 +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.
-
-require 5.001;
-
-use IO::Socket::IP;
-
-foreach $addr (@ARGV) {
-	my $sock;
-	$sock = IO::Socket::IP->new(LocalAddr => $addr,
-				    Domain => PF_INET6,
-                                    LocalPort => 0,
-                                    Proto     => tcp)
-                             or die "Can't bind : $@\n";
-	close($sock);
-}