# 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()`](https://gitlab.com/gnutls/gnutls/-/blob/master/src/serv.c#L937), [`src/udp-serv.c`](https://gitlab.com/gnutls/gnutls/-/blob/master/src/udp-serv.c), and [`tests/serv-udp.sh`](https://gitlab.com/gnutls/gnutls/-/blob/master/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: ```sh 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 `exec`s `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](https://gitlab.com/gnutls/gnutls). [`CONTRIBUTING.md` on master](https://gitlab.com/gnutls/gnutls/-/blob/master/CONTRIBUTING.md) 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: ```sh 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.