Wait for the UDP socket to bind before starting the client, detect server exit, and clean up on skip. Preserve both handshake checks. Add regression checks and document the rationale for upstream submission. Validated on x86-64-v3: 796 passes, 131 existing skips, zero failures. All seven focused checks and the delayed-start reproduction passed.
8.6 KiB
GnuTLS UDP server readiness
Under load, the test client can start before gnutls-serv binds its UDP
socket, and the first handshake fails with Connection refused.
serv-udp.sh currently waits a fixed four seconds; elapsed time does not
establish server readiness. udp-server-readiness.patch replaces that wait
with polling for the local IPv4 UDP endpoint.
Scope and waiting behavior
The patch changes the existing wait_udp_server() and adds a new
check_if_udp_port_bound() beside it in tests/scripts/common.sh.
serv-udp.sh is its only caller in 3.8.13. The TCP helpers wait_server()
and wait_for_port(), including their existing sleeps, are unchanged.
Both original DTLS handshake checks remain unchanged.
Each iteration checks process liveness and the socket before sleeping.
A ready socket returns immediately. An unsuccessful check sleeps two
seconds only if another attempt remains: at most 90 attempts, consistent
with the existing wait_server() budget implemented by wait_for_port(),
with no sleep after the final check. Server exit fails early; exhausting the
budget fails and terminates the server. No handshake is retried, and no
protocol timeout is changed. Once bound, the kernel can queue datagrams
while the server is scheduled; the probe itself sends no packets.
The existing have_port_finder() prefers ss, then netstat. If neither
exists, it prints neither ss nor netstat found and exits 77 (skip).
In the normal test flow, port selection calls it before launching a server.
The probe runs in a subshell so that, even if this skip occurs after launch,
the waiting helper can terminate and reap the server before exiting 77.
Why an IPv4 socket is expected
This is specific to the server used by this test, not a general rule that
IPv6 sockets cannot serve IPv4 clients. The client explicitly uses
127.0.0.1. The server's --udp path calls udp_server(), which calls
listen_socket(..., SOCK_DGRAM). That function iterates the wildcard
addresses returned by getaddrinfo(NULL, ..., AI_PASSIVE):
| Server build / Linux setting | Binding behavior |
|---|---|
IPv6 enabled, net.ipv6.bindv6only=0 |
Requests IPV6_V6ONLY=1 on the IPv6 socket, binds [::]:PORT, and separately binds 0.0.0.0:PORT. It overrides the system's dual-stack default. |
IPv6 enabled, net.ipv6.bindv6only=1 |
The same explicit socket option and separate IPv4/IPv6 binds. |
HAVE_IPV6 undefined |
Skips every address family except AF_INET; only the IPv4 wildcard is attempted. |
udp_server() uses wait_for_connection(), which puts every listener
from that list into select() and returns a readable socket for recvfrom();
it does not permanently choose one socket based on getaddrinfo() order.
The first two cases were traced with the actual GnuTLS 3.8.13 binary in
separate Linux network namespaces: setsockopt(IPV6_V6ONLY, [1]) and both
UDP binds returned success under each setting. The no-IPv6 case was checked
in source, not by building a second binary. The same bind implementation
was checked directly on GitLab master.
Thus, successful normal startup for this invocation provides an explicit
IPv4 socket; a lone IPv6 wildcard is not the expected success path.
There is one portability caveat: upstream discards the return value of
setsockopt(IPV6_V6ONLY). On a platform where that call fails and the server
ends up with only a dual-stack socket, this helper would time out despite
IPv4 reachability. Such a platform needs additional handling before this
patch can claim support. Blindly accepting every IPv6 wildcard would also
accept IPv6-only sockets before the separate IPv4 bind finishes.
Source: src/serv.c, listen_socket(),
src/udp-serv.c,
and tests/serv-udp.sh.
Port matching and ownership limit
Only -an is passed to the socket-listing tool: BSD netstat -u selects
Unix-domain sockets, whereas Linux netstat -u selects UDP. The parser
handles the extra state column in ss, Linux colon-separated endpoints,
and BSD dot-separated endpoints, including *.PORT. It matches the full
local port and rejects TCP, IPv6 entries, peer ports, and longer numbers.
A live PID plus a bound port does not prove that PID owns the socket.
Existing GETPORT selection checks for an unused port and uses a test
port-lock directory; launch_bare_server() also calls
wait_for_free_port() before starting the process. These are advisory:
the launcher does not enforce the latter's result, and another process
can bind between the check and launch. The patch does not close that race
or add nonportable PID parsing. An unrelated process can satisfy the
socket check; the real handshakes remain the functional check and may
fail (or reach the wrong server). This is a startup-order fix, not a
socket-ownership guarantee.
Reproduction and focused checks
Apply the patch to an unpacked source tree, then run the companion checks with Python's standard library and a shell:
patch --fuzz=0 -d /path/to/gnutls -p1 < udp-server-readiness.patch
SHELL=/bin/sh python3 verify-readiness.py /path/to/gnutls/tests/scripts/common.sh -v
Set NETSTAT=/path/to/netstat to exercise one outside PATH. The checks
cover Linux/BSD output samples, false matches, immediate readiness,
missing tools, process exit, timeout cleanup, and real IPv4 UDP sockets
whose bind is delayed six seconds. The missing-tools fixture is skipped
if an absolute fallback ss path cannot be hidden with PATH. Native
BSD execution remains untested.
To reproduce with GnuTLS itself, run tests/serv-udp.sh with SERV pointing
to a wrapper that sleeps six seconds, then execs gnutls-serv with all
arguments. Set CLI to the matching gnutls-cli, srcdir to the source
tests directory, and abs_top_builddir to a writable build directory.
With GnuTLS 3.8.13, the original helper failed the first handshake with
Connection refused; the patched helper passed both with the same binaries.
GnuTLS submission
Development and merge requests are on GitLab.
CONTRIBUTING.md on master
was read directly for this review. It requires the contributor's DCO
Signed-off-by, successful and failure test coverage, consistent coding
style, and adequate documentation; GitLab CI runs for merge requests.
Its commenting guidance asks for comments explaining non-obvious behavior
or protocol expectations. It does not prescribe an additional special
test-suite comment. The patch now explains its IPv4 binding assumption
next to the probe.
The submission will contain the shell patch, without the Python verifier
or a new Python test dependency. The existing serv-udp.sh supplies the
functional success check. Running it through the six-second startup
wrapper supplies a reproducible regression case: it fails before the fix
and passes after it. The local verifier was used to validate socket-output
parsing and the helper's success, process-exit, skip-cleanup, and timeout
branches. Those branch checks are local evidence, not new automated
coverage in the upstream suite; the MR must state that distinction.
No dedicated unit-test harness for these shell helpers was found in the 3.8.13 tests inspected. That does not establish that Python cannot be used upstream; keeping this submission dependency-free is a scope choice. Use the existing test and before/after reproduction as the submission's coverage argument, retaining the platform limitations above. Apply the patch in an upstream checkout and include those results with the contributor's own sign-off. No MR or sign-off has been created.
Local NixOS integration and build results
overlays/default.nix imports the overlays/patches overlay, which loads
gnutls/default.nix to apply the patch and keep serv-udp.sh enabled.
The patch itself has no Nix dependencies and applies to 3.8.13 and GitLab
master without fuzz.
The final patch was rebuilt with:
nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.gnutls
That x86-64-v3 build passed: 927 tests, 796 passes, 131 existing skips,
zero failures/errors, and PASS: serv-udp.sh. The patch bytes in the built
derivation were compared with the repository artifact; both have SHA-256
59013d47fd446f2dd065012a2259ccc1898fedc8a053a630e13efa0076368760.
All seven local checks passed, including skip cleanup and exactly 90
probes with 89 sleeps on timeout. The six-second before/after reproduction
was also repeated successfully with the final helper.