treefmt / nix fmt (pull_request) Successful in 4s
build_systems / prebuild-common-x86-64-v3 (pull_request) Successful in 24s
build_systems / build-portal-1 (pull_request) Successful in 20s
test ebook search / test-ebook-search (pull_request) Successful in 1m13s
build_systems / build-bob (pull_request) Successful in 54s
build_systems / build-brain (pull_request) Successful in 57s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m4s
build_systems / build-jeeves (pull_request) Successful in 1m30s
161 lines
8.6 KiB
Markdown
161 lines
8.6 KiB
Markdown
# 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.
|