This is an automated email from the ASF dual-hosted git repository.
bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 4f76e2f356 Fedora 44 test fixes (#13198)
4f76e2f356 is described below
commit 4f76e2f356e9551d388a5f5a4bd38fc351d63998
Author: Brian Neradt <[email protected]>
AuthorDate: Wed May 27 12:10:19 2026 -0500
Fedora 44 test fixes (#13198)
* Probe legacy TLS handshakes in tests
Fedora 44 crypto policy can accept legacy TLS command-line flags
while rejecting real TLS 1.0 and 1.1 handshakes. Closed-port probes
therefore let legacy TLS AuTests run in environments where the protocol
cannot actually negotiate.
This uses the AuTest port allocator to start a local OpenSSL server and
probe a real handshake before enabling those tests. The same helper also
gates curl TLS-version checks so both probes reflect runtime policy
rather than option parsing.
* Update cert select curl expectation
Newer curl and OpenSSL versions still return 60 for the bad-CA case,
but their verbose output includes the presented certificate and uses
different verification text. The old expectation made the cert-selection
AuTest depend on wording that varies by toolchain.
This keeps the assertion on the stable certificate-verification failure
and checks that the selected certificate is for bar.com, while still
rejecting the foo.com certificate in that failure path.
* Use temp socket path in JSONRPC test
Docker bind mounts can reject Unix-domain socket connects even after the
socket inode is created. The JSONRPC Catch test hit that behavior in the
Fedora 44 container when its socket was under the bind-mounted tree.
This moves the socket and lock into a per-run temporary directory and
defers setup to Catch startup so failures are reported by the test
framework. This also falls back to a short /tmp path when the temp path
would exceed Unix socket limits.
---
src/mgmt/rpc/server/unit_tests/test_rpcserver.cc | 90 ++++++++-
tests/gold_tests/autest-site/conditions.test.ext | 225 +++++++++++++++------
tests/gold_tests/autest-site/ports.py | 65 ++++--
.../tls/tls_check_cert_select_plugin.test.py | 5 +-
4 files changed, 297 insertions(+), 88 deletions(-)
diff --git a/src/mgmt/rpc/server/unit_tests/test_rpcserver.cc
b/src/mgmt/rpc/server/unit_tests/test_rpcserver.cc
index 0385ac4308..4c67d681e2 100644
--- a/src/mgmt/rpc/server/unit_tests/test_rpcserver.cc
+++ b/src/mgmt/rpc/server/unit_tests/test_rpcserver.cc
@@ -28,11 +28,16 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
+#include <unistd.h>
#include <thread>
#include <future>
#include <chrono>
#include <fstream>
+#include <cerrno>
+#include <cstring>
+#include <cstdlib>
+#include <vector>
#include "swoc/swoc_file.h"
@@ -71,12 +76,75 @@ add_method_handler(const std::string &name, Func &&call)
namespace
{
-const std::string sockPath{"tests/var/jsonrpc20_test.sock"};
-const std::string lockPath{"tests/var/jsonrpc20_test.lock"};
-constexpr int default_backlog{5};
-constexpr int default_maxRetriesOnTransientErrors{64};
-constexpr size_t default_incoming_req_max_size{32000 * 3};
-DbgCtl dbg_ctl{"rpc.test.client"};
+constexpr std::string_view rpc_test_dir_template{"ats_rpc_XXXXXX"};
+constexpr std::string_view rpc_test_socket_name{"s"};
+constexpr std::string_view rpc_test_lock_name{"l"};
+constexpr size_t
max_rpc_socket_path_size{sizeof(sockaddr_un::sun_path) - 1};
+
+fs::path rpcTestDir;
+std::string sockPath;
+std::string lockPath;
+constexpr int default_backlog{5};
+constexpr int default_maxRetriesOnTransientErrors{64};
+constexpr size_t default_incoming_req_max_size{32000 * 3};
+DbgCtl dbg_ctl{"rpc.test.client"};
+
+/** Prepare JSONRPC socket paths beneath @a base.
+ *
+ * This owns creation of the per-run test directory and publishes the socket
+ * and lock paths shared by the JSONRPC test server and clients.
+ *
+ * @param[in] base Candidate parent directory for the test directory.
+ * @param[out] error Reason setup failed, if a usable directory was not
created.
+ * @return @c true if the socket and lock paths are ready for this test run.
+ */
+bool
+try_setup_rpc_test_paths(fs::path const &base, std::string &error)
+{
+ auto const dir_template = (base / rpc_test_dir_template).string();
+ auto const socket_path = (fs::path{dir_template} /
rpc_test_socket_name).string();
+
+ if (socket_path.size() > max_rpc_socket_path_size) {
+ error = "JSONRPC test socket path is too long under " + base.string() + ":
" + socket_path;
+ return false;
+ }
+
+ std::vector<char> mutable_template{dir_template.begin(), dir_template.end()};
+ mutable_template.push_back('\0');
+
+ char *created_dir = mkdtemp(mutable_template.data());
+ if (created_dir == nullptr) {
+ error = "Failed to create JSONRPC test directory under " + base.string() +
": " + std::strerror(errno);
+ return false;
+ }
+
+ rpcTestDir = fs::path{created_dir};
+ sockPath = (rpcTestDir / rpc_test_socket_name).string();
+ lockPath = (rpcTestDir / rpc_test_lock_name).string();
+ return true;
+}
+
+/** Prepare JSONRPC socket paths for the test run.
+ *
+ * This prefers the environment temporary directory, then falls back to @c /tmp
+ * when the generated Unix-domain socket path would be too long or setup fails.
+ *
+ * @param[out] error Reason setup failed, if no candidate directory works.
+ * @return @c true if the socket and lock paths are ready for this test run.
+ */
+bool
+setup_rpc_test_paths(std::string &error)
+{
+ if (try_setup_rpc_test_paths(fs::temp_directory_path(), error)) {
+ error.clear();
+ return true;
+ }
+ if (try_setup_rpc_test_paths(fs::path{"/tmp"}, error)) {
+ error.clear();
+ return true;
+ }
+ return false;
+}
} // end anonymous namespace
@@ -88,6 +156,11 @@ struct RPCServerTestListener : Catch::EventListenerBase {
void
testRunStarting(Catch::TestRunInfo const & /* testRunInfo ATS_UNUSED */)
override
{
+ std::string setup_error;
+ bool const setup_ok = setup_rpc_test_paths(setup_error);
+ INFO(setup_error);
+ REQUIRE(setup_ok);
+
Layout::create();
init_diags("rpc", nullptr);
RecProcessInit();
@@ -123,6 +196,11 @@ struct RPCServerTestListener : Catch::EventListenerBase {
if (jsonrpcServer) {
delete jsonrpcServer; // will stop the thread
}
+
+ std::error_code ec;
+ if (!rpcTestDir.empty()) {
+ fs::remove_all(rpcTestDir, ec);
+ }
}
private:
diff --git a/tests/gold_tests/autest-site/conditions.test.ext
b/tests/gold_tests/autest-site/conditions.test.ext
index e228b19fae..41c5a6cacd 100644
--- a/tests/gold_tests/autest-site/conditions.test.ext
+++ b/tests/gold_tests/autest-site/conditions.test.ext
@@ -20,6 +20,110 @@ import os
import subprocess
import json
import re
+import tempfile
+import time
+
+from ports import get_port_number
+
+OPENSSL_TLS_FLAGS = {
+ "1.0": "-tls1",
+ "1.1": "-tls1_1",
+ "1.2": "-tls1_2",
+ "1.3": "-tls1_3",
+}
+
+
+def _terminate_process(process):
+ if process.poll() is not None:
+ return
+ try:
+ process.terminate()
+ process.wait(timeout=2)
+ except (OSError, subprocess.TimeoutExpired):
+ try:
+ process.kill()
+ process.wait(timeout=2)
+ except (OSError, subprocess.TimeoutExpired):
+ pass
+
+
+def _probe_openssl_server(tls_version, client_probe):
+ """Run a local OpenSSL server for TLS capability probes.
+
+ This owns the temporary certificate, port allocation, and server process
+ lifecycle so AuTest conditions can perform a real handshake with the client
+ being checked. Local setup failures return ``False`` so callers can skip
+ dependent tests instead of failing the harness.
+
+ :param tls_version: TLS version string to look up in
+ ``OPENSSL_TLS_FLAGS``.
+ :param client_probe: Callable that receives the server port and TLS flag
+ and returns whether the client completed the expected handshake.
+ :returns: ``True`` if the client probe succeeds against the local server,
+ otherwise ``False``.
+ """
+ tls_flag = OPENSSL_TLS_FLAGS.get(tls_version)
+ if tls_flag is None:
+ return False
+
+ try:
+ with tempfile.TemporaryDirectory() as tmpdir:
+ cert_path = os.path.join(tmpdir, "cert.pem")
+ key_path = os.path.join(tmpdir, "key.pem")
+ result = subprocess.run(
+ [
+ "openssl",
+ "req",
+ "-x509",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-sha256",
+ "-keyout",
+ key_path,
+ "-out",
+ cert_path,
+ "-subj",
+ "/CN=localhost",
+ "-days",
+ "1",
+ ],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ timeout=10,
+ )
+ if result.returncode != 0:
+ return False
+
+ with get_port_number() as port:
+ server = subprocess.Popen(
+ [
+ "openssl",
+ "s_server",
+ "-quiet",
+ "-accept",
+ f"127.0.0.1:{port}",
+ "-cert",
+ cert_path,
+ "-key",
+ key_path,
+ tls_flag,
+ "-cipher",
+ "DEFAULT@SECLEVEL=0",
+ "-www",
+ ],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ try:
+ time.sleep(0.5)
+ if server.poll() is not None:
+ return False
+ return client_probe(port, tls_flag)
+ finally:
+ _terminate_process(server)
+ except (OSError, subprocess.SubprocessError):
+ return False
def HasOpenSSLVersion(self, version):
@@ -67,38 +171,39 @@ def HasLegacyTLSSupport(self):
always disable both legacy versions together. If TLSv1.0 is unavailable,
TLSv1.1 will be too.
- The check connects to localhost on a closed port to avoid any external
- network dependency. A "connection refused" error means the TLS protocol
- was available but nothing was listening; "no protocols available" means
- the crypto-policy blocked TLSv1.0 entirely.
+ The check starts a local OpenSSL server and uses an OpenSSL client to
+ complete a real TLSv1.0 handshake. This avoids external network dependency
+ while catching environments that accept legacy TLS command-line flags but
+ reject the protocol during an actual handshake.
"""
def check_tls1_support():
- try:
- # Connect to localhost on a port nothing is listening on.
- # This avoids external network dependency while still detecting
- # whether the crypto-policy allows TLSv1.0.
- result = subprocess.run(
- ['openssl', 's_client', '-tls1', '-connect', '127.0.0.1:1'],
- capture_output=True,
- text=True,
- timeout=5,
- input='' # Don't wait for interactive input
- )
- output = result.stdout + result.stderr
- # "no protocols available" means TLSv1 is disabled by crypto-policy
- if 'no protocols available' in output:
+
+ def client_probe(port, tls_flag):
+ try:
+ result = subprocess.run(
+ [
+ 'openssl',
+ 's_client',
+ tls_flag,
+ '-connect',
+ f'127.0.0.1:{port}',
+ '-cipher',
+ 'DEFAULT@SECLEVEL=0',
+ '-brief',
+ ],
+ capture_output=True,
+ text=True,
+ timeout=5,
+ input='Q\n')
+ output = result.stdout + result.stderr
+ return result.returncode == 0 and 'Protocol version: TLSv1' in
output
+ except subprocess.TimeoutExpired:
+ return False
+ except Exception:
return False
- # Connection refused or other errors mean TLSv1 was attempted
- # (the protocol is available, just no server listening)
- return True
- except subprocess.TimeoutExpired:
- # Timeout on localhost shouldn't happen, but if it does,
- # assume TLSv1 is not available (safer than false positive)
- return False
- except Exception:
- # If we can't determine, assume TLSv1 is not available (safer)
- return False
+
+ return _probe_openssl_server("1.0", client_probe)
return self.Condition(check_tls1_support, "System does not support legacy
TLS protocols (TLSv1.0/TLSv1.1)")
@@ -154,7 +259,6 @@ def HasCurlTLSVersionSupport(self, tls_version):
"""
def check_curl_tls_support():
- # Map semantic versions used by tests to curl flags.
version_map = {
"1.0": ("--tlsv1", "1.0"),
"1.1": ("--tlsv1.1", "1.1"),
@@ -165,43 +269,36 @@ def HasCurlTLSVersionSupport(self, tls_version):
return False
tls_flag, tls_max = version_map[tls_version]
- try:
- # Connect to localhost closed port to avoid network dependencies.
- # "connection refused" means curl accepted the TLS flags and tried.
- result = subprocess.run(
- [
- "curl",
- "-svk",
- "--connect-timeout",
- "2",
- "--max-time",
- "3",
- tls_flag,
- "--tls-max",
- tls_max,
- "https://127.0.0.1:1",
- ],
- capture_output=True,
- text=True,
- timeout=5,
- )
- output = (result.stdout + result.stderr).lower()
- unsupported_markers = [
- "unsupported protocol",
- "no protocols available",
- "option --tlsv",
- "unknown option",
- "is unknown",
- ]
- if any(marker in output for marker in unsupported_markers):
+
+ def client_probe(port, _tls_flag):
+ try:
+ result = subprocess.run(
+ [
+ "curl",
+ "-svk",
+ "--connect-timeout",
+ "2",
+ "--max-time",
+ "5",
+ "--ciphers",
+ "DEFAULT@SECLEVEL=0",
+ tls_flag,
+ "--tls-max",
+ tls_max,
+ f"https://127.0.0.1:{port}/",
+ ],
+ capture_output=True,
+ text=True,
+ timeout=7,
+ )
+ output = (result.stdout + result.stderr).lower()
+ return result.returncode == 0 and f"ssl connection using
tlsv{tls_version}" in output
+ except subprocess.TimeoutExpired:
+ return False
+ except Exception:
return False
- # Any attempt to connect implies curl accepted the TLS setting.
- return True
- except subprocess.TimeoutExpired:
- return False
- except Exception:
- return False
+ return _probe_openssl_server(tls_version, client_probe)
return self.Condition(
check_curl_tls_support, "Curl does not support TLSv{version} in this
environment".format(version=tls_version))
diff --git a/tests/gold_tests/autest-site/ports.py
b/tests/gold_tests/autest-site/ports.py
index fb36b4088d..cfc56f4a30 100644
--- a/tests/gold_tests/autest-site/ports.py
+++ b/tests/gold_tests/autest-site/ports.py
@@ -16,6 +16,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from contextlib import contextmanager
from typing import Set
import socket
import subprocess
@@ -236,6 +237,50 @@ def _get_port_by_bind():
return port
+def _reserve_port():
+ """
+ Get a port from the global port queue.
+
+ Returns:
+ A tuple containing the port value and whether it should be recycled
+ into the queue when the caller is done with it.
+ """
+ _setup_port_queue()
+ if g_ports.qsize() > 0:
+ try:
+ port = _get_available_port(g_ports)
+ host.WriteVerbose("_reserve_port", f"Using port from port queue:
{port}")
+ return port, True
+ except PortQueueSelectionError:
+ port = _get_port_by_bind()
+ host.WriteVerbose("_reserve_port", f"Queue was drained. Using port
from a bound socket: {port}")
+ return port, False
+
+ # Since the queue could not be populated, use a port via bind.
+ port = _get_port_by_bind()
+ host.WriteVerbose("_reserve_port", f"Queue is empty. Using port from a
bound socket: {port}")
+ return port, False
+
+
+@contextmanager
+def get_port_number():
+ """
+ Reserve a port number from the same allocator used by get_port().
+
+ This is useful for helper code that needs a temporary listening port but
+ does not have an AuTest object with Setup hooks for recycling it. Queue
+ ports are recycled when the context exits.
+
+ :returns: A context manager yielding the reserved port value.
+ """
+ port, recycle_port = _reserve_port()
+ try:
+ yield port
+ finally:
+ if recycle_port:
+ g_ports.put(port)
+
+
def get_port(obj, name):
'''
Get a port and set it to the specified variable on the object.
@@ -247,22 +292,10 @@ def get_port(obj, name):
Returns:
The port value.
'''
- _setup_port_queue()
- port = 0
- if g_ports.qsize() > 0:
- try:
- port = _get_available_port(g_ports)
- host.WriteVerbose("get_port", f"Using port from port queue:
{port}")
- # setup clean up step to recycle the port
- obj.Setup.Lambda(
- func_cleanup=lambda: g_ports.put(port),
description=f"recycling port: {port}, queue size: {g_ports.qsize()}")
- except PortQueueSelectionError:
- port = _get_port_by_bind()
- host.WriteVerbose("get_port", f"Queue was drained. Using port from
a bound socket: {port}")
- else:
- # Since the queue could not be populated, use a port via bind.
- port = _get_port_by_bind()
- host.WriteVerbose("get_port", f"Queue is empty. Using port from a
bound socket: {port}")
+ port, recycle_port = _reserve_port()
+ if recycle_port:
+ obj.Setup.Lambda(
+ func_cleanup=lambda: g_ports.put(port), description=f"recycling
port: {port}, queue size: {g_ports.qsize()}")
# Assign to the named variable.
obj.Variables[name] = port
diff --git a/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py
b/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py
index 6cc069ea03..2ba8ae6026 100644
--- a/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py
+++ b/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py
@@ -170,7 +170,8 @@ tr.MakeCurlCommand(
tr.ReturnCode = 60
tr.StillRunningAfter = server
tr.StillRunningAfter = ts
-tr.Processes.Default.Streams.All = Testers.ContainsExpression("unknown CA",
"Failed handshake")
-tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com",
"Cert should contain bar.com")
+tr.Processes.Default.Streams.All = Testers.ContainsExpression(r"curl: \(60\)
SSL certificate", "Failed certificate verification")
+# Older versions of curl do not print certificate subject details when
+# certificate verification fails.
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com",
"Cert should not contain foo.com")
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("404", "Should
make an exchange")