Real signal delivery on Linux (observed via x86_64-musl workspace
testing) can interrupt blocking and nonblocking recv/recvmsg calls
with EINTR. Several production and test receive paths treated that as
a fatal error rather than a transient interruption:
- Client::open's blocking receive loop and Client::recv_once/
recv_available's blocking receive returned ClientError::Socket on
Interrupted instead of retrying within their existing timeout
budget.
- AsyncClient::poll_open and poll_recv's nonblocking recvmsg path
(Linux ancillary) failed the same way instead of retrying through
the existing readiness/work-budget machinery.
- The server's ancillary receive loop could terminate the listener on
an interrupted nonblocking recvmsg.
- The client's MSG_ERRQUEUE TX-timestamp drain turned an interrupted
read of optional metadata into a fatal ClientError::Socket.
- Fake-server test helpers panicked or misclassified EINTR as a
timeout instead of retrying it.
Interrupted is now retried transparently everywhere a receive can
observe it, while preserving each path's existing timing contract:
- Blocking Open reuses its already-absolute per-attempt deadline, so
interruptions resume within the same attempt without retransmitting
or extending the deadline.
- recv_once/recv_available compute one absolute deadline per logical
receive (when a timeout is configured) so repeated interruptions
cannot extend it past the configured timeout, and restore the
socket's configured read timeout afterward so a later call in the
same recv_available budget is unaffected.
- AsyncClient's poll_open already bounds its retry loop with
OPEN_POLL_WORK_BUDGET; poll_recv gains an equivalent
RECV_POLL_WORK_BUDGET, since unlike WouldBlock, Interrupted does not
clear Tokio's readiness state, so an unbounded retry loop was
otherwise possible under sustained signal delivery.
- The server's receive loop retries Interrupted the same way it
already retries WouldBlock, yielding to the runtime after a bounded
number of consecutive interruptions for the same reason.
- The error-queue drain treats an interrupted read as no record
obtained this drain (deferring to the next natural drain point)
rather than a socket error, matching its existing EWOULDBLOCK
handling.
Deterministic regression tests use a small test-only injection hook
(mirroring existing send-injection test hooks) rather than real signal
timing, covering: Open completing despite interruptions with no
retransmit, Open's deadline not extending under repeated
interruptions, recv_once classifying a reply normally despite
interruptions, recv_once's timeout not extending under repeated
interruptions, and the AsyncClient equivalents for open and recv.
No protocol, session, or timeout-classification behavior changes; no
new dependencies; send-side EINTR handling is unchanged and out of
scope.
Replace the three per-kind request encoders and decoders with one
directional pair, ahead of introducing irtt-server.
Sender side: encode_request(RequestToEncode, hmac_key) constructs OPEN,
CLOSE and ECHO requests from borrowed values. Params and the ECHO payload
are borrowed rather than owned, removing a Params clone and a payload Vec
from the client's hot paths. Wire output is byte-identical; all captured
vectors still pass unchanged.
Receiver side: decode_request(packet) structurally classifies an inbound
request with no HMAC key, no negotiated Params, no session state and no
allocation. A server must extract a token before it knows which key
applies, so authentication is a separate step. Classification follows the
verified server spec: reserved bits and FLAG_REPLY are rejected, OPEN
takes precedence over CLOSE and over an echo-shaped body, and only the
structural minimums (4/12/16, plus 16 with FLAG_HMAC) are enforced.
Trailing bytes are tolerated, no negotiated length ceiling is applied, and
a zero token is structurally valid. OPEN parameters stay encoded bytes so
an authenticating receiver can verify before decoding untrusted data, and
everything after an ECHO sequence number is exposed as an opaque tail.
Add verify_packet_hmac(key, packet), which locates the standard field
itself and rejects a packet lacking FLAG_HMAC. hmac_present reports packet
layout, never authentication. The offset-based verify_hmac stays public as
the low-level primitive for hand-built and captured wire vectors, matching
the existing compute_hmac pair.
Refactor the private envelope into structural header decoding, codec flag
rules, HMAC presence policy and verification as separate steps. The reply
codecs compose the same steps in the same order, so decode_open_reply and
decode_echo_reply keep their exact error ordering and the client's
fatal-versus-ignore classification is unchanged. Reply decoding stays
state-specific: the client already knows which reply to expect, and the
two need different semantic context.
Migrate the production client and every fake peer to the new API; the
fake peers now admit requests the way a server does, instead of parsing
packets by hand.
Use internal monotonic time by default and introduce poll_timeouts_at for callers requiring a specific sampled Instant. This simplifies the API for standard usage while maintaining flexibility for external event loops.
Verify that the client correctly handles custom packet lengths and server fill negotiation to ensure protocol compliance and support for larger MTUs. Add validation to reject empty server fill strings during configuration.
Introduce a BackendPeer abstraction to transparently switch between fake and real IRTT backends. This simplifies test logic by removing manual skip checks and providing a consistent interface for server lifecycle management.
Introduce a mechanism to run tests against a live irtt binary to verify real-world protocol interoperability, including HMAC authentication and various timestamp modes.