From fceff40701dfe7882cfe261c3d2859c3d7632a03 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Wed, 19 Aug 2026 20:50:39 +0100 Subject: [PATCH 01/11] udp: make the socket buffer a knob, and say when the kernel does not honour it The receive/send buffer asked for on every UDP socket was a private const of 8 MiB, with a comment noting the kernel clamps it to net.core.rmem_max and calling the result "best-effort headroom". The comment was right and the consequence was invisible: on a stock Linux box that ceiling is 212,992 bytes, so the 8 MiB request is granted at about a fortieth of its size, and the only symptom is datagrams dropped under load - which reads as a bug in whatever is running on top. Measured here, unmodified: 4.2% of datagrams dropped to rcvbuf overflow on the h3 benchmark at saturation. setsockopt does not fail in this case, it clamps and reports success, so nothing said so. Two changes. SocketBufferBytes becomes a UdpOptions knob (default unchanged at 8 MiB), because an operator who raises the ceiling has no way to tell ioxide to use it. And the granted size is read back with getsockopt and reported once per process when it falls well short - once, not per reactor per port, since the clamp is a property of the machine. The message deliberately stops at the fact and does not tell anyone to raise the cap. Granting the full 8 MiB on this machine cost about 45% of h3 throughput at saturation on unmodified main: the drops stopped, a deep standing queue took their place, and peers timed out and retransmitted instead - 1.2 datagrams per request became 5.2. A shallow buffer drops early, and early drops are the signal congestion control is built to read. Which behaviour is wanted depends on the deployment, so this reports and leaves the judgement. A zero or negative request is refused rather than clamped, because it would leave the socket on the kernel minimum and look identical to the clamp it is trying to make visible. --- src/ioxide/Native/Native.Socket.cs | 1 + .../Reactor/Transport/Udp/Reactor.Udp.cs | 69 ++++++++++++++++--- .../Reactor/Transport/Udp/UdpOptions.cs | 18 +++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/ioxide/Native/Native.Socket.cs b/src/ioxide/Native/Native.Socket.cs index c05e3f27..b616764f 100644 --- a/src/ioxide/Native/Native.Socket.cs +++ b/src/ioxide/Native/Native.Socket.cs @@ -27,6 +27,7 @@ public static unsafe partial class Native { [DllImport("libc")] public static extern int getsockname(int fd, void* addr, uint* len); [DllImport("libc")] public static extern int listen(int fd, int backlog); [DllImport("libc")] public static extern int setsockopt(int fd, int level, int optname, void* optval, uint optlen); + [DllImport("libc")] public static extern int getsockopt(int fd, int level, int optname, void* optval, uint* optlen); public static ushort Htons(ushort x) => (ushort)((x << 8) | (x >> 8)); diff --git a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs index 884c7697..7cb95494 100644 --- a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs +++ b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs @@ -18,11 +18,9 @@ namespace ioxide; /// public sealed unsafe partial class Reactor { - // Receive/send buffer requested on every UDP socket. QUIC bursts (many connections per peer - // socket, GSO trains) overflow the ~212 KB default while the reactor drains a batch, so ask for - // more; the kernel clamps to net.core.rmem_max/wmem_max, making this best-effort headroom rather - // than a hard requirement. - private const int UdpSocketBufferBytes = 8 * 1024 * 1024; + // One warning per process, however many reactors and ports there are: the clamp below is a + // property of the machine, so N reactors reporting it N times is noise, not information. + private static int _udpBufferClampReported; /// /// Per-datagram handler, invoked inline on the reactor thread. Like the TCP , @@ -105,7 +103,7 @@ private void OpenUdpSockets() for (int i = 0; i < ports; i++) { ushort port = udpPorts[i]; - _udpFds[i] = OpenUdpSocket(port, _config.DualStack, _udp.Gro); + _udpFds[i] = OpenUdpSocket(port, _config.DualStack, _udp.Gro, _udp.SocketBufferBytes); _udpFdPorts[i] = port; ArmUdpRecv(i); // one multishot per socket, all sharing the ring } @@ -123,7 +121,7 @@ private int OpenClientUdpSocket() InitUdpBufRing(); // no UDP/QUIC in config, so startup skipped it } - int fd = OpenUdpSocket(0, _config.DualStack, _udp.Gro); // port 0: the kernel picks + int fd = OpenUdpSocket(0, _config.DualStack, _udp.Gro, _udp.SocketBufferBytes); // port 0: the kernel picks // Append. Indices stay stable (recv completions carry theirs in user_data), so growing the // tables cannot disturb the multishot recvs already armed on the existing sockets. @@ -201,6 +199,50 @@ private void InitUdpBufRing() _udpRecvTemplate->msg_controllen = UdpCtrlCap; } + /// + /// Say so, once, when the kernel granted materially less buffer than was asked for. + /// + /// SO_RCVBUF does not fail when it cannot honour a request - it clamps to net.core.rmem_max and + /// reports success. On a stock Linux box that ceiling is 212,992 bytes, so a server asking for + /// 8 MiB quietly runs with about a fortieth of it and the only symptom is datagrams dropped + /// under load, which looks like a bug anywhere but here. Reading the value back makes it visible. + /// + /// It deliberately does NOT tell the operator to raise the cap. Measured here, granting the full + /// 8 MiB cost about 45% of h3 throughput at saturation on unmodified main: the drops stopped and + /// a deep standing queue took their place, so the reactor worked through stale datagrams while + /// peers timed out and retransmitted. A small buffer drops early and keeps the queue short, + /// which congestion control is built to read. Which is better depends on the deployment, so this + /// reports the fact and leaves the judgement. + /// + /// getsockopt reports DOUBLE what was granted - the kernel's own bookkeeping overhead is + /// included - so the comparison halves it. + /// + private static void ReportBufferClamp(int fd, int requested) + { + int reported = 0; + uint size = sizeof(int); + if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &reported, &size) < 0) + { + return; + } + + int granted = reported / 2; + if (granted >= requested / 2) + { + return; // near enough; a small shortfall is not worth a startup warning + } + + if (Interlocked.Exchange(ref _udpBufferClampReported, 1) != 0) + { + return; + } + + Console.Error.WriteLine( + $"[ioxide] udp: asked for {requested / 1024} KiB of socket buffer, the kernel granted " + + $"{granted / 1024} KiB (capped by net.core.rmem_max). Expect datagrams to be dropped " + + "under load; raising the cap trades those drops for queueing delay, so measure it."); + } + private static int RoundUpPow2(int n) { int p = 1; @@ -221,8 +263,16 @@ private void ReturnUdpBuffer(ushort bid) Volatile.Write(ref *(ushort*)(_udpBufRing + 14), _udpBufRingTail); } - private static int OpenUdpSocket(ushort port, bool dualStack, bool gro) + private static int OpenUdpSocket(ushort port, bool dualStack, bool gro, int socketBufferBytes) { + // Refused rather than clamped: a zero or negative request would leave the socket on the + // kernel minimum, which looks identical to the clamp above and would be read as one. + if (socketBufferBytes <= 0) + { + throw new InvalidOperationException( + $"UdpOptions.SocketBufferBytes must be positive, got {socketBufferBytes}."); + } + int fd = socket(dualStack ? AF_INET6 : AF_INET, SOCK_DGRAM, 0); if (fd < 0) { @@ -237,9 +287,10 @@ private static int OpenUdpSocket(ushort port, bool dualStack, bool gro) setsockopt(fd, SOL_UDP, UDP_GRO, &one, sizeof(int)); } - int buf = UdpSocketBufferBytes; + int buf = socketBufferBytes; setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &buf, sizeof(int)); setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &buf, sizeof(int)); + ReportBufferClamp(fd, socketBufferBytes); if (dualStack) { diff --git a/src/ioxide/Reactor/Transport/Udp/UdpOptions.cs b/src/ioxide/Reactor/Transport/Udp/UdpOptions.cs index 9966d0ef..a09d2601 100644 --- a/src/ioxide/Reactor/Transport/Udp/UdpOptions.cs +++ b/src/ioxide/Reactor/Transport/Udp/UdpOptions.cs @@ -19,6 +19,24 @@ public sealed record UdpOptions /// public int RecvSlots { get; init; } = 16; + /// + /// Receive and send buffer requested on every UDP socket, in bytes. + /// + /// QUIC bursts overflow the stock ~208 KiB while the reactor drains a batch, so the default + /// asks for considerably more - roughly what other QUIC servers ask for. It is a REQUEST: the + /// kernel silently clamps it to net.core.rmem_max / wmem_max rather than failing, + /// and on a stock Linux box those are 212,992 bytes, so the default is clamped to about a + /// fortieth of itself and datagrams are dropped under load. ioxide reads the granted size back + /// and says so once at startup when that happens. + /// + /// Raising that ceiling is not automatically an improvement. Measured on the h3 benchmark here, + /// granting the full 8 MiB cost about 45% of throughput at saturation: the drops stopped and a + /// deep standing queue replaced them, so peers timed out and retransmitted instead. A shallow + /// buffer drops early, which is the signal congestion control is built to read. Treat both the + /// ceiling and this value as things to measure on the deployment rather than to maximise. + /// + public int SocketBufferBytes { get; init; } = 8 * 1024 * 1024; + /// /// Enable UDP_GRO on receive: the kernel coalesces a burst of equal-size datagrams from one /// peer into a single completion, and carries the From 616675c8d8780ed26635f657920a9764d1361410 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Wed, 19 Aug 2026 20:51:06 +0100 Subject: [PATCH 02/11] quic: route a migrated client's datagrams to the reactor that owns its connection (#205) Fixes #205. Every reactor binds the QUIC port with SO_REUSEPORT and the kernel chooses between them by hashing the sender's address. That is the right answer only while the address holds still, so a NAT rebind or a client changing network re-hashes to a reactor that has never heard of the connection, whose short-header packets it then drops. #209 taught the transport to migrate; this is what lets the packets reach the connection that could act on it. It bites the default configuration, since ReactorCount defaults to one per core. The connection cannot move to meet the packet. The ngtcp2 conn, the picotls session, the open streams and their ring-bound buffers are owned by one reactor thread and documented reactor-thread- only throughout; moving live state to whichever reactor a datagram landed on is precisely what shared-nothing forbids. So the datagram moves instead, which is ordinary message passing and rides ScheduleOnReactor - already public, already used for exactly this. Every connection id the server mints now carries its owning reactor in the first byte, chosen so cid[0] % ReactorCount is that reactor while the rest stays random (iq_stamp_shard). A reactor that receives a short header for an id it does not have reads that byte, copies the datagram and posts it to the owner. The copy is the point, not an inefficiency: the payload lives in the receiving reactor's io_uring provided-buffer ring, which is returned as soon as dispatch ends, so handing the owner a pointer into it would be a use-after-free under load. What crosses a thread is bytes, never reactor state. Only short headers are forwarded. A short header means the handshake finished, so the id is one this server minted and its first byte really does name the owner. A long header carries an id the CLIENT chose, and routing on a byte the peer controls would let anyone aim traffic at a reactor of their choosing. QuicOptions.Routing offers the alternative. KernelFilter additionally attaches a classic-BPF program to the reuseport group so the kernel routes by connection id directly. It is not the default, and the measurements say why - h3 benchmark, two reactors, one machine: Forward nothing at all until a client moves, then ~8.5us per datagram KernelFilter free with CPU headroom, ~-12% throughput at saturation, nothing for migrated The forward cost is a cross-thread wake, not work: CPU per request moved 2.44 -> 2.46us with every datagram forwarded, while throughput halved at fixed concurrency, and the reactors sat 36% idle. So Forward charges only the connections that actually migrate; KernelFilter charges every packet a little kernel work, invisible until there is no headroom left. Unless a large share of clients migrate, Forward is cheaper in aggregate. KernelFilter needs reactors to open their UDP sockets in ShardIndex order, since the program answers with a position in the reuseport group and that position is bind order. That rendezvous exists only under KernelFilter; the default leaves startup untouched. It also degrades rather than fails: if the kernel refuses the program, ioxide says so and forwarding stays underneath. Correctness never depends on the filter, only cost does. Both modes are tested and each discriminates in the opposite direction - Forward asserts datagrams were forwarded, KernelFilter asserts none were and that the program actually attached, so it cannot pass vacuously as Forward under another name. Confirmed both ways: suppressing the forward makes the Forward test fail. StartQuicSharded is new because every other QUIC entry point in the harness pins ReactorCount = 1, where a datagram has nowhere wrong to land. QuicForwardsSent/Received/Dropped and QuicStaleDatagrams are exposed for operators. The distinction matters and only the shard byte makes it possible: a short header for an unknown id that belongs elsewhere is a routing event, while one addressed here is ordinary - a migration retires ids and packets in flight still carry them. Also here, found on the way and not separable from the shim rebuild: iq_sync_path was calling ngtcp2_sockaddr_eq, which lives in lib/ngtcp2_addr.h and is NOT shipped under lib/includes. It compiled only by implicit declaration and stops building outright on GCC 14+, where that is an error. Replaced with a local mirror over the public types; a plain memcmp will not do, since sockaddr padding and flowinfo would read as an address change and fire migration callbacks on a connection that never moved. iq_abi is a new exported surface revision the managed side checks when it builds an engine, so a stale libioxide_ngtcp2.so fails at startup with a clear message instead of passing garbage across a boundary that just grew two parameters. Stale .so files have silently invalidated audit runs here before. --- src/ioxide/Reactor/Reactor.cs | 10 + src/ioxide/Reactor/Transport/Quic/QuicCid.cs | 6 + .../Reactor/Transport/Quic/QuicOptions.cs | 7 + .../Reactor/Transport/Quic/QuicRouting.cs | 48 ++++ .../Transport/Quic/Reactor.Quic.Forward.cs | 224 ++++++++++++++++ .../Reactor/Transport/Quic/Reactor.Quic.cs | 36 ++- .../Transport/Udp/Reactor.Udp.Steering.cs | 249 ++++++++++++++++++ .../Reactor/Transport/Udp/Reactor.Udp.cs | 31 ++- .../Connection/QuicEngineConnection.cs | 7 +- .../ioxide.ngtcp2/Engine/QuicClientEngine.cs | 2 + .../ioxide.ngtcp2/Engine/QuicEngine.cs | 2 + src/protocols/ioxide.ngtcp2/Interop/Ngtcp2.cs | 39 ++- .../ioxide.ngtcp2/native/ioxide_ngtcp2_shim.c | 86 +++++- .../linux-x64/native/libioxide_ngtcp2.so | Bin 1228456 -> 1228488 bytes .../Protocols/QuicMigrationTests.cs | 126 +++++++++ tests/Ioxide.Tests.Harness/TestServer.cs | 81 ++++++ 16 files changed, 943 insertions(+), 11 deletions(-) create mode 100644 src/ioxide/Reactor/Transport/Quic/QuicRouting.cs create mode 100644 src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Forward.cs create mode 100644 src/ioxide/Reactor/Transport/Udp/Reactor.Udp.Steering.cs diff --git a/src/ioxide/Reactor/Reactor.cs b/src/ioxide/Reactor/Reactor.cs index f795dcdd..96cf766f 100644 --- a/src/ioxide/Reactor/Reactor.cs +++ b/src/ioxide/Reactor/Reactor.cs @@ -12,6 +12,16 @@ namespace ioxide; public sealed unsafe partial class Reactor { private readonly int _id; + + /// + /// This reactor's position in the fleet - the id it was constructed with, in + /// 0 .. ShardCount-1. QUIC stamps it into the connection ids it mints so the kernel can + /// steer a connection's datagrams back here after the client changes address. + /// + public int ShardIndex => _id; + + /// How many reactors share this server's ports (). + public int ShardCount => _config.ReactorCount; private Ring _ring = null!; // created on the reactor thread (DEFER_TASKRUN requires same-thread setup+enter) // TcpConnection table indexed by fd (dense small ints - array beats Dictionary per CQE). diff --git a/src/ioxide/Reactor/Transport/Quic/QuicCid.cs b/src/ioxide/Reactor/Transport/Quic/QuicCid.cs index ca4c8a0b..72981422 100644 --- a/src/ioxide/Reactor/Transport/Quic/QuicCid.cs +++ b/src/ioxide/Reactor/Transport/Quic/QuicCid.cs @@ -18,6 +18,12 @@ namespace ioxide; public int Length => _len; + /// + /// The first byte, which is where a server-minted id carries its owning reactor. Zero for an + /// empty id. _a is the little-endian read of bytes 0-7, so its low byte is byte 0. + /// + public byte FirstByte => (byte)(_a & 0xFF); + public QuicCid(ReadOnlySpan bytes) { if (bytes.Length > MaxLength) diff --git a/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs b/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs index 40081a9d..5aac9a5a 100644 --- a/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs +++ b/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs @@ -10,6 +10,13 @@ public sealed record QuicOptions /// public int LocalCidLength { get; init; } = 8; + /// + /// How a datagram reaches the reactor that owns its connection when several reactors share the + /// port. Defaults to , which costs nothing until a client + /// changes address; see for the measured trade. + /// + public QuicRouting Routing { get; init; } = QuicRouting.Forward; + public QuicConnectionFactory? ConnectionFactory { get; init; } /// diff --git a/src/ioxide/Reactor/Transport/Quic/QuicRouting.cs b/src/ioxide/Reactor/Transport/Quic/QuicRouting.cs new file mode 100644 index 00000000..8dcbbb7f --- /dev/null +++ b/src/ioxide/Reactor/Transport/Quic/QuicRouting.cs @@ -0,0 +1,48 @@ +namespace ioxide; + +/// +/// How a QUIC datagram reaches the reactor that owns its connection, when the fleet has more than +/// one. +/// +/// The problem both settings solve: every reactor binds the QUIC port with SO_REUSEPORT and the +/// kernel chooses between them by hashing the sender's address, which stops being the right answer +/// the moment a client's address changes. See Reactor.Quic.Forward.cs. +/// +/// Measured on one machine with two reactors and the h3 benchmark, so treat the magnitudes as +/// indicative and the shape as real: +/// +/// +/// settingconnections that never migrate / that do +/// no cost at all / about 8.5 us per datagram +/// free with CPU headroom, about -12% throughput at saturation / no cost +/// +/// +/// So the choice is who pays. charges only the connections that actually +/// migrate, and charges them a cross-thread wake per datagram. charges +/// every connection a little kernel work per packet - invisible while there is CPU to spare, and +/// real once there is not - and charges migrating ones nothing. Unless a large share of clients +/// migrate, is cheaper in aggregate, which is why it is the default. +/// +public enum QuicRouting +{ + /// + /// Let the kernel hash as it does today, and hand a misdirected datagram to its owner over the + /// reactor post queue. Costs nothing until a client actually moves, needs no privileges, and + /// leaves reactor startup exactly as it is. + /// + Forward = 0, + + /// + /// Additionally attach a classic-BPF program to the port's SO_REUSEPORT group so the kernel + /// routes by connection id rather than by address, and a migrated client's datagrams arrive at + /// their owner directly. + /// + /// Two consequences worth knowing. Reactors must then open their UDP sockets in ShardIndex + /// order, because the program answers with a position in the reuseport group and that position + /// is bind order - a startup-only rendezvous that does not exist otherwise. And the filter is + /// best-effort: if the kernel refuses it (an old kernel, a seccomp policy, a restricted + /// container) ioxide says so and carries on, with still underneath as the + /// backstop. Correctness never depends on the filter; only the cost does. + /// + KernelFilter = 1, +} diff --git a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Forward.cs b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Forward.cs new file mode 100644 index 00000000..699714ed --- /dev/null +++ b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Forward.cs @@ -0,0 +1,224 @@ +using System.Buffers; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +namespace ioxide; + +/// +/// Cross-reactor delivery for QUIC: when a datagram lands on a reactor that does not own the +/// connection it names, hand it to the one that does. +/// +/// Why it is needed. Every reactor binds the QUIC port with SO_REUSEPORT, and the kernel picks +/// between them by hashing the sender's address. That is stable only while the address is - so a +/// NAT rebind, or a client moving network, re-hashes to a different reactor, one that has never +/// heard of the connection. Its short-header packets are then dropped and the connection dies +/// unreachable on the reactor that could still serve it. Issue #205. +/// +/// Why this direction rather than moving the connection. The ngtcp2 conn, the picotls session and +/// the open streams are native state owned by one reactor thread, and QuicConnection is +/// reactor-thread-only throughout. Moving live state to the reactor the packet happened to land on +/// is what shared-nothing forbids; moving the PACKET to the state it belongs to is ordinary message +/// passing, which is how the model is meant to work - and it rides +/// , which reactors already expose for exactly this. +/// +/// What crosses a thread boundary is therefore a COPY of the bytes, never a reference to reactor +/// state. That copy is not incidental: the datagram lives in the receiving reactor's io_uring +/// provided-buffer ring, which is returned the moment the dispatch call returns, so handing the +/// owner a pointer into it would be a use-after-free under load. +/// +/// Only short-header packets are forwarded. A short header means the handshake is done, so its +/// destination id is one this server minted and its first byte really does name the owner (see +/// iq_stamp_shard in the ngtcp2 shim). A long header carries a connection id the CLIENT chose, and +/// routing on a byte the peer controls would let anyone aim traffic at a reactor of their choosing. +/// +public sealed unsafe partial class Reactor +{ + /// + /// Datagrams that may be awaiting delivery to any one reactor before further ones are dropped. + /// Dropping is safe here in a way it rarely is: QUIC treats a lost packet as loss and resends, + /// so the ceiling costs a retransmit rather than a connection. Without it, a reactor that + /// stalls would let its siblings queue for it without limit. + /// + private const int QuicForwardMaxOutstanding = 1024; + + /// + /// The reactors sharing one ServerConfig, so a datagram can be handed to the one that owns it. + /// + /// Keyed on the config INSTANCE rather than held in a static, because a process routinely runs + /// several independent servers at once - every test suite here does - and they must not be able + /// to see, or post into, each other's reactors. ConditionalWeakTable keys by reference identity + /// (not the record's value equality) and holds the fleet no longer than the config itself. + /// + private static readonly ConditionalWeakTable QuicFleets = new(); + + private sealed class QuicFleet + { + public readonly Reactor?[] Members; // by ShardIndex; null until that reactor starts + public readonly int[] Outstanding; // datagrams in flight toward each member + public readonly ConcurrentQueue Spare = new(); + + public QuicFleet(int count) + { + Members = new Reactor?[count]; + Outstanding = new int[count]; + } + } + + /// + /// One datagram in transit between reactors. Pooled, because a migrated connection forwards + /// every packet until the peer settles, and an envelope per datagram would be a steady stream + /// of garbage on the hot path. + /// + private sealed class QuicForward + { + public byte[] Payload = []; + public int Length; + public byte[] PeerAddr = new byte[UdpNameCap]; + public int PeerAddrLen; + public int SocketFd; + public ushort LocalPort; + public byte Tos; + public int Owner; // the ShardIndex this was addressed to + public QuicFleet Fleet = null!; + public Reactor Target = null!; + } + + private QuicFleet? _quicFleet; + + private long _quicForwardsSent; + private long _quicForwardsReceived; + private long _quicForwardsDropped; + + /// Datagrams this reactor handed to a sibling because it did not own the connection. + public long QuicForwardsSent => Volatile.Read(ref _quicForwardsSent); + + /// Datagrams a sibling handed to this reactor. + public long QuicForwardsReceived => Volatile.Read(ref _quicForwardsReceived); + + /// + /// Datagrams that could not be forwarded and were dropped - the owner's queue was at + /// , or it had stopped. Nonzero means a reactor is not + /// keeping up; the peers will retransmit. + /// + public long QuicForwardsDropped => Volatile.Read(ref _quicForwardsDropped); + + // Join the fleet. Called from InitQuic, on the reactor's own thread, before the loop starts. + private void QuicJoinFleet() + { + if (ShardCount <= 1 || (uint)_id >= (uint)ShardCount) + { + return; // nothing to forward to, or an id outside the configured fleet + } + + QuicFleet fleet = QuicFleets.GetValue(_config, static c => new QuicFleet(c.ReactorCount)); + _quicFleet = fleet; + Volatile.Write(ref fleet.Members[_id], this); + } + + /// + /// Hand a datagram to the reactor that owns its connection. Returns false when this reactor is + /// the owner (so the id is simply unknown - stale, retired, or hostile) or when there is + /// nowhere to send it, in which case the caller drops as before. + /// + private bool QuicTryForward(in UdpDatagram datagram, in QuicCid dcid) + { + QuicFleet? fleet = _quicFleet; + if (fleet is null || dcid.Length == 0) + { + return false; + } + + int owner = dcid.FirstByte % fleet.Members.Length; + if (owner == _id) + { + return false; // addressed here, and we do not have it: not a routing problem + } + + Reactor? target = Volatile.Read(ref fleet.Members[owner]); + if (target is null || target._stopRequested || target._wakeFd <= 0) + { + _quicForwardsDropped++; + return true; // handled: there is nothing better to do with it than drop it + } + + // Reserve a slot before copying, so a stalled owner cannot make its siblings do the work. + if (Interlocked.Increment(ref fleet.Outstanding[owner]) > QuicForwardMaxOutstanding) + { + Interlocked.Decrement(ref fleet.Outstanding[owner]); + _quicForwardsDropped++; + return true; + } + + if (!fleet.Spare.TryDequeue(out QuicForward? forward)) + { + forward = new QuicForward(); + } + + int length = datagram.Payload.Length; + if (forward.Payload.Length < length) + { + if (forward.Payload.Length > 0) + { + ArrayPool.Shared.Return(forward.Payload); + } + forward.Payload = ArrayPool.Shared.Rent(length); + } + + // The copy that makes this safe. Payload points into the recv slot, which goes back to the + // provided-buffer ring as soon as this dispatch returns. + datagram.Payload.CopyTo(forward.Payload); + forward.Length = length; + + int addrLen = Math.Min(datagram.PeerAddrLen, UdpNameCap); + new ReadOnlySpan((void*)datagram.PeerAddr, addrLen).CopyTo(forward.PeerAddr); + forward.PeerAddrLen = addrLen; + + forward.SocketFd = datagram.SocketFd; + forward.LocalPort = datagram.LocalPort; + forward.Tos = datagram.Tos; + forward.Owner = owner; + forward.Fleet = fleet; + forward.Target = target; + + _quicForwardsSent++; + + // Static lambda over the envelope alone: no closure, no per-datagram allocation. + target.ScheduleOnReactor( + static state => + { + var envelope = (QuicForward)state!; + envelope.Target.QuicReceiveForward(envelope); + }, + forward); + + return true; + } + + // Runs on the OWNING reactor's thread, out of its post queue. + private void QuicReceiveForward(QuicForward forward) + { + QuicFleet fleet = forward.Fleet; + + try + { + _quicForwardsReceived++; + + fixed (byte* payload = forward.Payload) + fixed (byte* addr = forward.PeerAddr) + { + // GRO segment size 0: trains are split before routing, so a forwarded datagram is + // always a single one. + QuicDispatchDatagram(new UdpDatagram(forward.SocketFd, forward.LocalPort, + (nint)addr, forward.PeerAddrLen, + new ReadOnlySpan(payload, forward.Length), 0, forward.Tos)); + } + } + finally + { + Interlocked.Decrement(ref fleet.Outstanding[forward.Owner]); + forward.Fleet = null!; + forward.Target = null!; + fleet.Spare.Enqueue(forward); + } + } +} diff --git a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs index 21c6a11e..e46544b0 100644 --- a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs +++ b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs @@ -24,6 +24,19 @@ public sealed unsafe partial class Reactor private readonly HashSet _quicConnSet = []; private readonly List _quicSweepScratch = []; + private long _quicStaleDatagrams; + + /// + /// Short-header datagrams naming a connection id that is addressed to THIS reactor and which it + /// does not have: retired after a migration (ngtcp2 rotates ids when the path changes, so + /// packets already in flight still carry the old one), long dead, or hostile. + /// + /// Ordinary, and not a routing problem - a migration produces a handful every time. Datagrams + /// that belong to a DIFFERENT reactor are not counted here; they are forwarded to it, and + /// counted by . + /// + public long QuicStaleDatagrams => Volatile.Read(ref _quicStaleDatagrams); + // No-op unless ServerConfig.Quic is set (the port itself is bound by OpenUdpSockets). private void InitQuic() { @@ -32,6 +45,7 @@ private void InitQuic() return; } _quicOptions = options; + QuicJoinFleet(); // so a datagram for another reactor's connection can reach it AddTicker(QuicSweep); } @@ -101,7 +115,27 @@ private void QuicDispatchDatagram(in UdpDatagram datagram) if (!longHeader) { - return; // short header for an unknown CID: stale/garbage (stateless reset later) + // A short header names a connection that must already exist, so reaching here means + // this reactor cannot serve this datagram. There are two very different reasons for + // that, and conflating them makes the count useless: + // + // the id is not ours - the datagram reached the WRONG reactor, which is the + // routing failing and the thing worth alarming on + // the id IS ours - routing worked and the id is simply gone: retired after a + // migration (ngtcp2 rotates ids when the path changes, so + // packets in flight still carry the old one), or stale, or + // hostile. Ordinary, and not a routing problem + // + // Telling them apart is only possible because a server-minted id carries its owner, + // and the first case is recoverable: hand the datagram to the reactor it names rather + // than dropping a live connection's traffic. See Reactor.Quic.Forward.cs. + if (QuicTryForward(in datagram, in dcid)) + { + return; + } + + _quicStaleDatagrams++; + return; } // No factory (or no QuicOptions at all, on a client-only reactor): nothing is accepted here. diff --git a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.Steering.cs b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.Steering.cs new file mode 100644 index 00000000..0b670274 --- /dev/null +++ b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.Steering.cs @@ -0,0 +1,249 @@ +using System.Runtime.CompilerServices; +using static ioxide.Native; + +namespace ioxide; + +/// +/// Connection-id steering for QUIC: a classic-BPF program attached to the QUIC port's +/// SO_REUSEPORT group so the kernel picks the reactor by reading the connection id out of +/// the datagram, instead of hashing the sender's address. +/// +/// The default without it is the 4-tuple hash, which is correct only while a client's address +/// never changes. When it does - a NAT rebind, a phone moving from wifi to cellular, a deliberate +/// migration - the hash lands the datagram on a reactor that has never heard of that connection, +/// and a short-header packet for an unknown id is dropped. The connection stays alive and +/// unreachable on its own reactor until the idle sweep evicts it. Since the state that could serve +/// it (the ngtcp2 conn, the picotls session, the open streams) is native memory owned by one +/// reactor thread and documented reactor-thread-only, the fix has to move the DATAGRAM to the +/// state, never the state to the datagram. +/// +/// Which is what the connection id is for. Every id this server mints carries its owning reactor +/// in the first byte (see iq_stamp_shard in the shim), chosen so cid[0] % ReactorCount is +/// exactly that reactor. The filter below recomputes it and returns it as the index into the +/// reuseport group. The id travels with the connection, so the routing survives whatever the +/// address does. +/// +/// Two things this depends on, both handled here: +/// +/// +/// The filter answers with a position in the reuseport group, and that position is +/// bind order. So the reactors have to open the QUIC socket in ShardIndex order, +/// which is what arranges. It is a startup-only cost. +/// The program must not be attached until every reactor has joined the group, or an index +/// can point past the end of it. The last reactor out attaches it. +/// +/// +/// If anything about that does not hold - the kernel refuses the program, a reactor fails to bind, +/// the fleet never assembles - steering is abandoned and the port keeps the 4-tuple hash. That is +/// today's behaviour, so the fallback is never worse than not having tried. +/// +public sealed unsafe partial class Reactor +{ + /// Attach a reuseport-steering program (Linux 4.5+). Not in SocketOptionName. + private const int SO_ATTACH_REUSEPORT_CBPF = 51; + + /// + /// How long a reactor waits for its turn to open the QUIC socket before giving up on ordering. + /// Generous, because it only has to cover other reactor threads reaching the same point in + /// startup; if it is ever hit, something is wrong with the fleet rather than merely slow. + /// + private const int QuicSteeringTurnTimeoutMs = 10_000; + + /// + /// The shard byte is one byte, so beyond 256 reactors it cannot encode the owner and steering + /// is not attempted. Nothing else changes; the port keeps the 4-tuple hash. + /// + private const int QuicSteeringMaxShards = 256; + + // Keyed by the ServerConfig instance the fleet shares, so two servers in one process (which is + // the normal shape in the test suites) get independent gates and cannot wait on each other. + // ConditionalWeakTable keys on reference identity, not the record's value equality, which is + // what we want here - and it holds the gate no longer than the config itself. + private static readonly ConditionalWeakTable QuicSteeringGates = new(); + + private sealed class QuicSteeringGate + { + public readonly object Lock = new(); + public int Turn; // the ShardIndex allowed to open right now + public int QuicFd = -1; // any socket in the group; attaching to one sets it for all + public bool Abandoned; // ordering broke, so the indices cannot be trusted + public bool Settled; // attach (or the decision not to) already happened + } + + /// + /// Whether this reactor should take part in ordered opening. QUIC only - a plain UDP server + /// keeps today's startup exactly - and only when there is a fleet to steer across. + /// + private bool _quicSteeringAttached; + + /// + /// True on the reactor that attached the steering program (the last one to bind, since the + /// group has to be complete first). The filter is group-wide, so one reactor reporting this is + /// the fleet reporting it. False everywhere under , and false + /// when the kernel refused the program. + /// + public bool QuicKernelSteeringAttached => _quicSteeringAttached; + + private bool QuicSteeringActive => + _config.Quic is { Routing: QuicRouting.KernelFilter } && + ShardCount > 1 && + ShardCount <= QuicSteeringMaxShards; + + private QuicSteeringGate? QuicSteeringBegin() + { + if (!QuicSteeringActive) + { + return null; + } + + QuicSteeringGate gate = QuicSteeringGates.GetValue(_config, static _ => new QuicSteeringGate()); + QuicSteeringAwaitTurn(gate); + return gate; + } + + /// + /// Block until it is this reactor's turn to open its sockets, so that group position equals + /// . + /// + /// The timeout is what keeps a misconfigured fleet from becoming a hang: a caller that starts + /// fewer reactors than would otherwise leave everyone + /// after the missing one waiting forever. On expiry the gate is abandoned ONCE and every + /// waiter is released together, so the delay is paid a single time rather than per reactor. + /// + private void QuicSteeringAwaitTurn(QuicSteeringGate gate) + { + lock (gate.Lock) + { + long deadline = Environment.TickCount64 + QuicSteeringTurnTimeoutMs; + + while (gate.Turn != ShardIndex && !gate.Abandoned) + { + int remaining = (int)(deadline - Environment.TickCount64); + if (remaining <= 0 || !Monitor.Wait(gate.Lock, remaining)) + { + Console.Error.WriteLine( + $"[r{_id}] quic: waited {QuicSteeringTurnTimeoutMs} ms for reactor {gate.Turn} " + + "to bind; giving up on connection-id steering and falling back to " + + "cross-reactor forwarding"); + gate.Abandoned = true; + Monitor.PulseAll(gate.Lock); + break; + } + } + } + } + + /// + /// Hand the turn to the next reactor, and - if this was the last one and every reactor bound + /// cleanly - attach the steering program now that the group is complete. + /// + /// The fleet's gate, or null when steering is not active. + /// This reactor's QUIC socket, or -1 if opening it failed. + private void QuicSteeringRelease(QuicSteeringGate? gate, int quicFd) + { + if (gate is null) + { + return; + } + + lock (gate.Lock) + { + if (quicFd < 0) + { + // This reactor never joined the group, so every later index is off by one. + gate.Abandoned = true; + } + else if (gate.QuicFd < 0) + { + gate.QuicFd = quicFd; + } + + gate.Turn++; + Monitor.PulseAll(gate.Lock); + + if (gate.Turn < ShardCount || gate.Settled) + { + return; + } + + gate.Settled = true; + if (gate.Abandoned || gate.QuicFd < 0) + { + return; // already reported; the port keeps the 4-tuple hash + } + + QuicAttachSteering(gate.QuicFd, ShardCount); + } + } + + /// + /// Attach the steering program to the QUIC reuseport group. Failure is reported and otherwise + /// ignored: an older kernel, a seccomp policy or a restricted container can all refuse it, and + /// the only consequence is that address changes go back to breaking connections. + /// + private void QuicAttachSteering(int fd, int shards) + { + // classic-BPF instruction: { u16 code; u8 jt; u8 jf; u32 k; } + // + // 0 ld len A = datagram length + // 1 jge #9 ? next : ->8 too short to hold a connection id: fall through on the + // length itself, which is in bounds and deterministic + // 2 ldb [0] the QUIC first byte + // 3 and #0x80 its header-form bit + // 4 jeq #0 ? ->7 : next clear = short header + // 5 ldb [6] long header: first byte / 4 version / 1 dcid len, so DCID + // starts at 6. A client's Initial id is its own random value, + // which makes this a hash - and a stable one, so every packet + // of a handshake still reaches one reactor. + // 6 ja ->8 + // 7 ldb [1] short header: DCID starts straight after the first byte. + // This is a connection id WE minted, so the byte is the shard. + // 8 mod #shards the reuseport index + // 9 ret a + // + // Byte loads, not word loads, because iq_stamp_shard controls exactly one byte. Reading + // more would mix in bytes it does not constrain and the two sides would disagree. + (ushort code, byte jt, byte jf, uint k)[] program = + [ + (0x80, 0, 0, 0), // ld len + (0x35, 0, 6, 9), // jge #9 + (0x30, 0, 0, 0), // ldb [0] + (0x54, 0, 0, 0x80), // and #0x80 + (0x15, 2, 0, 0), // jeq #0 + (0x30, 0, 0, 6), // ldb [6] + (0x05, 0, 0, 1), // ja + (0x30, 0, 0, 1), // ldb [1] + (0x94, 0, 0, (uint)shards), // mod #shards + (0x16, 0, 0, 0), // ret a + ]; + + byte* instructions = stackalloc byte[program.Length * 8]; + for (int i = 0; i < program.Length; i++) + { + byte* at = instructions + i * 8; + *(ushort*)at = program[i].code; + at[2] = program[i].jt; + at[3] = program[i].jf; + *(uint*)(at + 4) = program[i].k; + } + + // struct sock_fprog { unsigned short len; struct sock_filter *filter; } - the pointer is + // 8-aligned, so the length sits in the first two bytes of a 16-byte struct. + byte* fprog = stackalloc byte[16]; + new Span(fprog, 16).Clear(); + *(ushort*)fprog = (ushort)program.Length; + *(byte**)(fprog + 8) = instructions; + + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, fprog, 16) < 0) + { + Console.Error.WriteLine( + $"[r{_id}] quic: could not attach connection-id steering; the port keeps the " + + "4-tuple hash and migrated clients fall back to cross-reactor forwarding, which " + + "is correct but costs a hop per datagram"); + return; + } + + _quicSteeringAttached = true; + Console.WriteLine($"[r{_id}] quic: connection-id steering attached across {shards} reactors"); + } +} diff --git a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs index 7cb95494..0c40ed96 100644 --- a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs +++ b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs @@ -100,12 +100,33 @@ private void OpenUdpSockets() InitUdpBufRing(); - for (int i = 0; i < ports; i++) + // Ordered across the fleet ONLY when QuicRouting.KernelFilter is asked for: that program + // answers with a position in the reuseport group and the position is bind order, so + // reactor N's socket has to be the Nth one in. Under the default routing this returns null + // immediately and startup is exactly as it was. See Reactor.Udp.Steering.cs. + QuicSteeringGate? gate = QuicSteeringBegin(); + int quicFd = -1; + + try + { + for (int i = 0; i < ports; i++) + { + ushort port = udpPorts[i]; + _udpFds[i] = OpenUdpSocket(port, _config.DualStack, _udp.Gro, _udp.SocketBufferBytes); + _udpFdPorts[i] = port; + ArmUdpRecv(i); // one multishot per socket, all sharing the ring + + if (_config.Quic is { } configured && port == configured.Port) + { + quicFd = _udpFds[i]; + } + } + } + finally { - ushort port = udpPorts[i]; - _udpFds[i] = OpenUdpSocket(port, _config.DualStack, _udp.Gro, _udp.SocketBufferBytes); - _udpFdPorts[i] = port; - ArmUdpRecv(i); // one multishot per socket, all sharing the ring + // Hands the turn on whatever happened. A reactor that threw reports -1, which abandons + // steering for the fleet rather than attaching a filter whose indices are now wrong. + QuicSteeringRelease(gate, quicFd); } } diff --git a/src/protocols/ioxide.ngtcp2/Connection/QuicEngineConnection.cs b/src/protocols/ioxide.ngtcp2/Connection/QuicEngineConnection.cs index dea5a25c..2a640b5d 100644 --- a/src/protocols/ioxide.ngtcp2/Connection/QuicEngineConnection.cs +++ b/src/protocols/ioxide.ngtcp2/Connection/QuicEngineConnection.cs @@ -201,6 +201,10 @@ private static void FillSockaddrInLoopback(Span sa, ushort port) // Adopt the connection: validate the client's first datagram and create the ngtcp2 conn. Runs // inside the factory, before the transport records the route; returns false to reject. reactor // and socketFd are captured here because engine callbacks can fire during iq_accept. + // + // The reactor's shard identity goes across too: it is stamped into the connection id the + // server mints, which is what lets the kernel steer this connection's later datagrams back to + // THIS reactor even after the client changes address. See Reactor.Udp.Steering.cs. internal bool TryAccept(nint enginePtr, Reactor reactor, in UdpDatagram datagram, Span scidOut, out int scidLen) { _reactor = reactor; @@ -222,7 +226,8 @@ internal bool TryAccept(nint enginePtr, Reactor reactor, in UdpDatagram datagram loc, (nuint)local.Length, (void*)datagram.PeerAddr, (nuint)datagram.PeerAddrLen, pkt, (nuint)datagram.Payload.Length, - NowNs(), (void*)GCHandle.ToIntPtr(_self), scid); + NowNs(), (void*)GCHandle.ToIntPtr(_self), + (uint)reactor.ShardIndex, (uint)reactor.ShardCount, scid); } if (_conn == 0) diff --git a/src/protocols/ioxide.ngtcp2/Engine/QuicClientEngine.cs b/src/protocols/ioxide.ngtcp2/Engine/QuicClientEngine.cs index 66fe052b..c8fa7d2e 100644 --- a/src/protocols/ioxide.ngtcp2/Engine/QuicClientEngine.cs +++ b/src/protocols/ioxide.ngtcp2/Engine/QuicClientEngine.cs @@ -23,6 +23,8 @@ public sealed unsafe class QuicClientEngine : IDisposable public QuicClientEngine(string alpn = "h3") { + Ngtcp2.RequireAbi(); + Alpn = alpn; // The same callback table the server engine registers: the shim now routes both directions diff --git a/src/protocols/ioxide.ngtcp2/Engine/QuicEngine.cs b/src/protocols/ioxide.ngtcp2/Engine/QuicEngine.cs index b3c8b3d7..9202a4e3 100644 --- a/src/protocols/ioxide.ngtcp2/Engine/QuicEngine.cs +++ b/src/protocols/ioxide.ngtcp2/Engine/QuicEngine.cs @@ -130,6 +130,8 @@ public QuicEngine(string certPemPath, string keyPemPath, uint cidLength = 8, str "QUIC connection ID length must be 1..20 bytes."); } + Ngtcp2.RequireAbi(); + CidLength = cidLength; // Clamp to a floor: the pump overshoots the high-water by at most one egress chunk (16 KiB), // so a cap below that would wedge a response mid-flight. 256 KiB gives comfortable headroom. diff --git a/src/protocols/ioxide.ngtcp2/Interop/Ngtcp2.cs b/src/protocols/ioxide.ngtcp2/Interop/Ngtcp2.cs index 36b671c1..7bfeb1bd 100644 --- a/src/protocols/ioxide.ngtcp2/Interop/Ngtcp2.cs +++ b/src/protocols/ioxide.ngtcp2/Interop/Ngtcp2.cs @@ -90,12 +90,49 @@ internal struct Callbacks [DllImport(Lib)] internal static extern void iq_engine_free(nint engine); + /// The shim's exported-surface revision; checked against at engine + /// construction so a stale native library fails loudly instead of mis-routing in silence. + [DllImport(Lib)] internal static extern uint iq_abi(); + + /// What this managed binding was written against. Bump both together. + internal const uint Abi = 2; + + /// + /// Refuse to run against a native library this binding was not built for. + /// + /// Worth an explicit check rather than trusting the package: the shim and this assembly ship + /// together, so a mismatch means a stale libioxide_ngtcp2.so has been picked up out of a build + /// output - which has silently invalidated whole test runs here before. The failure it prevents + /// is not a crash but arguments landing in the wrong parameters, which looks like a logic bug + /// somewhere else entirely. + /// + internal static void RequireAbi() + { + uint native; + try + { + native = iq_abi(); + } + catch (EntryPointNotFoundException) + { + native = 0; // predates the counter, so certainly too old + } + + if (native != Abi) + { + throw new InvalidOperationException( + $"ioxide.ngtcp2: the native library reports ABI {native}, this binding needs {Abi}. " + + "A stale libioxide_ngtcp2.so is being loaded - rebuild it with scripts/build-ngtcp2-native.sh."); + } + } + [DllImport(Lib)] internal static extern nint iq_accept( nint engine, void* localSa, nuint localSaLen, void* remoteSa, nuint remoteSaLen, byte* pkt, nuint pktLen, - ulong ts, void* user, byte* scidOut); + ulong ts, void* user, + uint shard, uint shardCount, byte* scidOut); [DllImport(Lib)] internal static extern void iq_conn_free(nint conn); diff --git a/src/protocols/ioxide.ngtcp2/native/ioxide_ngtcp2_shim.c b/src/protocols/ioxide.ngtcp2/native/ioxide_ngtcp2_shim.c index 92d007d8..fe65855f 100644 --- a/src/protocols/ioxide.ngtcp2/native/ioxide_ngtcp2_shim.c +++ b/src/protocols/ioxide.ngtcp2/native/ioxide_ngtcp2_shim.c @@ -37,6 +37,12 @@ #include #include +/* Exported-surface revision. Bump on any change to an exported signature or a struct crossing the + * boundary; iq_abi() hands it to the managed side, which refuses to start on a mismatch. + * 1 - iq_callbacks gained struct_size and on_path_change + * 2 - iq_accept gained shard / shard_count for connection-id steering */ +#define IQ_ABI 2 + /* ---- callback table into C# ------------------------------------------------------------- */ typedef struct iq_callbacks { @@ -154,6 +160,11 @@ typedef struct iq_conn { ngtcp2_path path; ngtcp2_ccerr last_error; void *user; + /* Which reactor owns this connection, and how many there are. Stamped into every CID it + * mints so the SO_REUSEPORT filter can route by connection id instead of by 4-tuple - the + * difference between surviving a client's address change and dropping it. */ + uint32_t shard; + uint32_t shard_count; ptls_raw_extension_t exts[2]; /* [0] QUIC transport params (filled by ngtcp2), [1] terminator */ char alpn[64]; /* client: the single protocol we offer */ ptls_iovec_t alpn_vec; /* points into alpn[], handed to picotls for the CH */ @@ -775,12 +786,32 @@ static int iq_cb_acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, return 0; } +/* Stamp the owning reactor into the first CID byte. The kernel filter routes on cid[0] % + * shard_count, so the byte is picked to leave exactly `shard` as the remainder while staying + * otherwise random - the CID keeps its unpredictability, it just stops being uniform. + * + * shard_count <= 1 is a single-reactor server: nothing to steer, so the CID is left fully random. + * A count above 256 cannot be encoded in one byte, and the caller disables steering for it. */ +static void iq_stamp_shard(uint8_t *cid, uint32_t shard, uint32_t shard_count) +{ + if (shard_count <= 1 || shard_count > 256 || shard >= shard_count) { + return; + } + + unsigned v = (unsigned)cid[0] - ((unsigned)cid[0] % shard_count) + shard; + if (v > 255) { + v -= shard_count; /* still congruent to shard mod shard_count, and back inside a byte */ + } + cid[0] = (uint8_t)v; +} + static int iq_cb_get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, size_t cidlen, void *user_data) { (void)conn; iq_conn *c = user_data; ptls_openssl_random_bytes(cid->data, cidlen); + iq_stamp_shard(cid->data, c->shard, c->shard_count); cid->datalen = cidlen; ptls_openssl_random_bytes(token, NGTCP2_STATELESS_RESET_TOKENLEN); if (c->cbs.on_new_cid) c->cbs.on_new_cid(c->user, cid->data, cid->datalen); @@ -1280,16 +1311,29 @@ const char *iq_version(void) return ngtcp2_version(0)->version_str; } +/* Bumped whenever an exported signature or struct layout changes. The managed binding checks this + * when it builds an engine, so a stale libioxide_ngtcp2.so left in a build output fails at startup + * with a clear message rather than silently passing garbage across the boundary. */ +uint32_t iq_abi(void) +{ + return IQ_ABI; +} + /* ---- connection ------------------------------------------------------------------------- */ /* Validate the first datagram of a new connection and build the server conn for it. * scid_out receives the connection ID this server minted (engine->cidlen bytes) so the caller - * can register the route. Returns NULL if the packet is not an acceptable Initial. */ + * can register the route. shard / shard_count identify the reactor calling this, and are stamped + * into that CID and every later one this connection issues, so the kernel's SO_REUSEPORT filter + * routes the connection's datagrams back here even after the client changes address. Pass + * shard 0, count 1 for a single-reactor server: the CID is then left fully random. + * Returns NULL if the packet is not an acceptable Initial. */ iq_conn *iq_accept(iq_engine *e, const void *local_sa, size_t local_salen, const void *remote_sa, size_t remote_salen, const uint8_t *pkt, size_t pktlen, - uint64_t ts, void *user, uint8_t *scid_out) + uint64_t ts, void *user, + uint32_t shard, uint32_t shard_count, uint8_t *scid_out) { ngtcp2_pkt_hd hd; if (ngtcp2_accept(&hd, pkt, pktlen) != 0) { @@ -1302,6 +1346,9 @@ iq_conn *iq_accept(iq_engine *e, } c->cbs = e->cbs; c->user = user; + /* Set before ngtcp2_conn_server_new, because the CID generator callback can fire during it. */ + c->shard = shard; + c->shard_count = shard_count; /* Clamped at the entry point, like every other caller-supplied length here. The destinations * are ngtcp2_sockaddr_union - 28 bytes, sa/in/in6 only, NOT sockaddr_storage - and the field @@ -1376,6 +1423,7 @@ iq_conn *iq_accept(iq_engine *e, ngtcp2_cid scid; scid.datalen = e->cidlen; /* bounded to 1..NGTCP2_MAX_CIDLEN when the engine was created */ ptls_openssl_random_bytes(scid.data, scid.datalen); + iq_stamp_shard(scid.data, shard, shard_count); if (ngtcp2_conn_server_new(&c->conn, &hd.scid, &scid, &c->path, hd.version, &callbacks, &settings, ¶ms, NULL, c) != 0) { @@ -1442,6 +1490,38 @@ void iq_conn_free(iq_conn *c) * ngtcp2 fills the path we hand to writev_stream with the destination it chose for THAT datagram, * which for a PATH_RESPONSE or a PATH_CHALLENGE probe is not the current path at all. Calling this * before the datagram is handed back is what lets the caller send it where ngtcp2 meant it to go. */ +/* ngtcp2's own sockaddr comparison lives in lib/ngtcp2_addr.h, which is INTERNAL - it is not + * shipped under lib/includes, so calling it compiled only by implicit declaration and stops + * building outright on GCC 14+, where that is an error rather than a warning. This mirrors it + * using the public types. + * + * A plain memcmp is not a substitute: sockaddr_in carries sin_zero padding and sockaddr_in6 + * carries flowinfo and scope_id, so two spellings of the same peer can differ byte-wise and would + * read as an address change - firing a migration callback on a connection that never moved. */ +static int iq_sockaddr_eq(const ngtcp2_sockaddr *a, const ngtcp2_sockaddr *b) +{ + if (a->sa_family != b->sa_family) { + return 0; + } + + switch (a->sa_family) { + case NGTCP2_AF_INET: { + const ngtcp2_sockaddr_in *ai = (const ngtcp2_sockaddr_in *)(const void *)a; + const ngtcp2_sockaddr_in *bi = (const ngtcp2_sockaddr_in *)(const void *)b; + return ai->sin_port == bi->sin_port && + memcmp(&ai->sin_addr, &bi->sin_addr, sizeof(ai->sin_addr)) == 0; + } + case NGTCP2_AF_INET6: { + const ngtcp2_sockaddr_in6 *ai = (const ngtcp2_sockaddr_in6 *)(const void *)a; + const ngtcp2_sockaddr_in6 *bi = (const ngtcp2_sockaddr_in6 *)(const void *)b; + return ai->sin6_port == bi->sin6_port && + memcmp(&ai->sin6_addr, &bi->sin6_addr, sizeof(ai->sin6_addr)) == 0; + } + default: + return 0; /* cannot arrive from a UDP recvmsg; re-reporting is harmless if it ever does */ + } +} + static void iq_sync_path(iq_conn *c) { if (c->path.remote.addrlen == 0) { @@ -1449,7 +1529,7 @@ static void iq_sync_path(iq_conn *c) } if (c->path.remote.addrlen == c->reported_addrlen && - ngtcp2_sockaddr_eq(&c->reported_addr.sa, c->path.remote.addr)) { + iq_sockaddr_eq(&c->reported_addr.sa, c->path.remote.addr)) { return; } diff --git a/src/protocols/ioxide.ngtcp2/runtimes/linux-x64/native/libioxide_ngtcp2.so b/src/protocols/ioxide.ngtcp2/runtimes/linux-x64/native/libioxide_ngtcp2.so index d506d1a011dbbd3f02a635768d0a36e5124be97a..a5a44ac806e16cee1b62346d5c74f893d97a2ad8 100755 GIT binary patch delta 232435 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%0D36|`f_!$ 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 Q4d3bVM?|N+;XCJl0j{P_7XSbN delta 232471 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≤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 + { + // The multi-reactor case, which every other QUIC test here is blind to: they pin + // ReactorCount = 1, where a datagram has nowhere wrong to land. Issue #205. + // + // Note what does NOT prove anything here. "The request still succeeded" is satisfied by + // QUIC retransmitting until something gets through, so it passes even against a server + // that drops every migrated packet - measured, not assumed. And the handler's reactor + // cannot differ before and after, because one connection is served by one handler on + // one reactor, so asserting that asserts nothing. + // + // What is real is whether the datagrams actually arrived somewhere else and were + // handed on. So the test drives the address until they do, then asserts the exchange + // continues from there. + (string certPath, string keyPath) = TestCert.Ensure(); + using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]); + + const int reactors = 4; + + (int serverPort, Reactor[] fleet) = TestServer.StartQuicSharded(reactors, + engine.CreateFactory(), + quicHandle: static (_, conn) => new Nghttp3Connection(conn).RunBufferedAsync( + static _ => Nghttp3Response.Text("migrated-ok")), + routing: QuicRouting.Forward); + + long Forwarded() + { + long n = 0; + foreach (Reactor reactor in fleet) { n += reactor.QuicForwardsSent; } + return n; + } + + long Dropped() + { + long n = 0; + foreach (Reactor reactor in fleet) { n += reactor.QuicForwardsDropped; } + return n; + } + + using var forwarder = new UdpForwarder(serverPort); + using var client = new H3TestClient("127.0.0.1", forwarder.Port); + + client.Connect(); + Assert.True(client.CompleteHandshake(10_000), "the handshake through the forwarder did not complete"); + + (int status, string body) = client.Request("GET", "/before", null, 10_000); + Assert.Equal(200, status); + Assert.Equal("migrated-ok", body); + + // Change address until the kernel actually hands this connection to a DIFFERENT + // reactor, which is the situation under test. Each change has a 1-in-4 chance of + // landing back on the owner, so this settles at once in practice; the loop is here so + // the test never rests on that coin. + int swaps = 0; + while (Forwarded() == 0 && swaps < 8) + { + swaps++; + forwarder.SwapUpstream(); + + (int afterStatus, string afterBody) = client.Request("GET", $"/after-{swaps}", null, 15_000); + + Assert.Equal(200, afterStatus); + Assert.Equal("migrated-ok", afterBody); + } + + Assert.True(Forwarded() > 0, + $"after {swaps} address changes no datagram ever reached a reactor that did not own " + + "the connection, so the routing was never exercised"); + + // And it keeps working now that the packets are arriving at the wrong reactor every time. + Assert.Equal(200, client.Request("GET", "/steady", null, 15_000).Status); + + Assert.Equal(0L, Dropped()); + Assert.True(forwarder.FromServerAfterSwap > 0, + "the server never sent anything to the client's new address, so nothing migrated"); + }); + + runner.Test("quic/migration: kernel steering delivers a migrated client without any forwarding", () => + { + // The other half of QuicRouting. Under Forward the datagrams arrive at the wrong + // reactor and are handed on; under KernelFilter they should never arrive wrong in the + // first place, so the forward path stays untouched. Asserting zero is only meaningful + // alongside the sibling test above, which shows the same scenario produces forwards + // when the kernel is not doing the routing. + (string certPath, string keyPath) = TestCert.Ensure(); + using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]); + + const int reactors = 4; + + (int serverPort, Reactor[] fleet) = TestServer.StartQuicSharded(reactors, + engine.CreateFactory(), + quicHandle: static (_, conn) => new Nghttp3Connection(conn).RunBufferedAsync( + static _ => Nghttp3Response.Text("migrated-ok")), + routing: QuicRouting.KernelFilter); + + // Without this the rest passes vacuously on any machine where the program will not + // load - it would simply be measuring Forward again under another name. + bool attached = false; + foreach (Reactor reactor in fleet) { attached |= reactor.QuicKernelSteeringAttached; } + Assert.True(attached, "the steering program never attached, so this ran as Forward"); + + using var forwarder = new UdpForwarder(serverPort); + using var client = new H3TestClient("127.0.0.1", forwarder.Port); + + client.Connect(); + Assert.True(client.CompleteHandshake(10_000), "the handshake through the forwarder did not complete"); + + Assert.Equal(200, client.Request("GET", "/before", null, 10_000).Status); + + for (int swap = 1; swap <= 3; swap++) + { + forwarder.SwapUpstream(); + + (int status, string body) = client.Request("GET", $"/after-{swap}", null, 15_000); + + Assert.Equal(200, status); + Assert.Equal("migrated-ok", body); + } + + long forwarded = 0; + foreach (Reactor reactor in fleet) { forwarded += reactor.QuicForwardsSent; } + + Assert.Equal(0L, forwarded); + }); + runner.Test("control: the same exchange through a forwarder that never swaps", () => { // Without this, the test above is satisfied by a forwarder that works and a migration diff --git a/tests/Ioxide.Tests.Harness/TestServer.cs b/tests/Ioxide.Tests.Harness/TestServer.cs index 7d854ab1..e2ddb4cd 100644 --- a/tests/Ioxide.Tests.Harness/TestServer.cs +++ b/tests/Ioxide.Tests.Harness/TestServer.cs @@ -374,6 +374,87 @@ public static int StartSharded(int reactorCount, Func + /// A QUIC server spread across reactors, all sharing one + /// SO_REUSEPORT'd UDP port - the shape ioxide actually ships, and the one every other QUIC + /// entry point here avoids by pinning a single reactor. + /// + /// It exists for connection-id steering. With one reactor a datagram cannot land in the wrong + /// place, so a single-reactor test cannot tell correct routing from no routing at all; only a + /// fleet can. The reactors share ONE ServerConfig instance, which is also what the steering + /// gate keys on to order their binds. + /// + /// How many reactors share the port. Two is enough to discriminate. + /// + /// The QUIC UDP port and the reactors themselves - the latter because the property under test + /// is WHICH reactor a datagram reached, which is only visible from inside them + /// (). + /// + public static (int Port, Reactor[] Reactors) StartQuicSharded(int reactorCount, + QuicConnectionFactory quicFactory, + Func? quicHandle = null, int quicIdleMs = 60_000, + QuicRouting routing = QuicRouting.Forward) + { + int tcpPort = ReserveFreePort(); + int udpPort = ReserveFreePort(); + + var config = new ServerConfig + { + ReactorCount = reactorCount, + RecvBufferSize = 4096, + RecvSlots = 256, + Tcp = new TcpOptions + { + Port = (ushort)tcpPort, + WriteSlabSize = 16 * 1024, + PoolMax = 64, + RecvQueueEntries = 64, + }, + Udp = new UdpOptions { RecvSlots = 16, Ports = [] }, + Quic = new QuicOptions + { + Port = (ushort)udpPort, + LocalCidLength = 8, + ConnectionFactory = quicFactory, + IdleTimeoutMs = quicIdleMs, + Routing = routing, + }, + }; + + using var ready = new CountdownEvent(reactorCount); + var reactors = new Reactor[reactorCount]; + + for (int i = 0; i < reactorCount; i++) + { + int shard = i; + var reactor = new Reactor(shard, config) + { + TcpHandle = static (_, _) => Task.CompletedTask, + QuicHandle = quicHandle, + OnStart = _ => ready.Signal(), + }; + reactors[shard] = reactor; + + var thread = new Thread(RunGuarded(reactor, tcpPort)) + { + IsBackground = true, + Name = $"test-quic-shard-{udpPort}-{shard}", + }; + thread.Start(); + Track(reactor, thread); + } + + // Every reactor has to be past OnStart before the port is usable: the binds are ORDERED + // when steering is on, so an early reactor is listening while a later one has not bound + // yet - and the filter is not attached until the last one is in. + if (!ready.Wait(20_000)) + { + WaitForListen(tcpPort); + } + + return (udpPort, reactors); + } + /// Incremental mode (IOU_PBUF_RING_INC) needs 6.12+; tests skip below that. public static bool KernelAtLeast(int major, int minor) { From 8832ea987eb47bfafbb969872b6e1217a1d228ed Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Wed, 19 Aug 2026 20:58:51 +0100 Subject: [PATCH 03/11] docs: a page for how ioxide does HTTP/3, end to end The learn section had QUIC & HTTP/3, which walks a datagram through the transport and is written for someone reading the code. What it does not answer is the set of questions a deployment actually has: which certificate is served for which name, how one gets renewed without dropping traffic, how client certificates are checked, what happens when a client's address changes, and what a fleet of reactors does about it. Those were spread across doc comments and samples or nowhere at all. /how-ioxide-does-h3 collects them. It covers the three layers and what each owns, why TLS on QUIC is a separate stack from TLS on TCP with its own configuration (the thing most likely to catch someone running both), SNI and why the host table closes at CreateFactory, ReplaceCertificates and its three surprising properties, mutual TLS and why PeerCommonName exists rather than substring-matching the subject, connection migration, and the routing modes added in #205 with the measurements behind the default. It links to QUIC & HTTP/3 rather than repeating it: this page is what the server does, that one is how the code does it. quic-h3.html gains the same routing material in short form, because its ingress walkthrough said unknown short-header packets are dropped as stale traffic - true when it was written, and no longer true for a fleet, where such a packet may belong to a sibling reactor. The full comparison lives on the new page so the two cannot drift. --- docs/how-ioxide-does-h3.html | 251 +++++++++++++++++++++++++++++++++++ docs/index.html | 2 +- docs/learn/architecture.html | 1 + docs/learn/clients.html | 1 + docs/learn/dev-core.html | 1 + docs/learn/dev-file.html | 1 + docs/learn/dev-pg.html | 1 + docs/learn/dev-redis.html | 1 + docs/learn/dev-tls.html | 1 + docs/learn/files.html | 1 + docs/learn/multiport.html | 1 + docs/learn/overview.html | 1 + docs/learn/postgres.html | 1 + docs/learn/quic-h3.html | 37 +++++- docs/learn/redis.html | 1 + docs/learn/tls.html | 1 + 16 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 docs/how-ioxide-does-h3.html diff --git a/docs/how-ioxide-does-h3.html b/docs/how-ioxide-does-h3.html new file mode 100644 index 00000000..c1e1d842 --- /dev/null +++ b/docs/how-ioxide-does-h3.html @@ -0,0 +1,251 @@ + + + + + + How ioxide does HTTP/3 - ioxide + + + + + + + + + +
+ + +
+

How ioxide does HTTP/3

+

Everything an HTTP/3 deployment actually has to decide, in one place: which + pieces do what, how TLS is terminated on QUIC, how a server picks a certificate by name and + replaces one without dropping traffic, how client certificates are checked, what happens when a + client changes address mid-connection, and how a fleet of reactors keeps serving it. For the + packet-level walk through the transport, see + QUIC & HTTP/3.

+ +

The pieces

+

Three layers, each with one job.

+ + + + + +
layerwhat it does
ioxide coreBinds the UDP port, receives datagrams on the ring, + and routes each one to a logical connection by its destination connection id. Knows nothing + about crypto or HTTP.
ioxide.ngtcp2The QUIC protocol engine - ngtcp2 for the + transport, picotls for TLS 1.3, both bundled as one native library. Owns the handshake, + packet protection, loss recovery and flow control.
ioxide.nghttp3Turns decrypted stream bytes into requests and + responses, including QPACK. There is also ioxide.http3, a pure-C# implementation + of the same layer.
+ +

TLS here is not the TLS you configured for TCP

+

ioxide terminates TLS twice, through two entirely separate stacks, and this catches people + out. Over TCP it is OpenSSL, configured through TlsService. Over QUIC it is + picotls, driven by ngtcp2 and configured on QuicEngine. They share no code and no + configuration object, deliberately - QUIC does not use TLS records at all, it carries the + handshake in its own CRYPTO frames, so the two have almost nothing in common below the + certificate.

+ +
What this means in practice. A server offering both HTTP/2 over TCP and + HTTP/3 over QUIC configures its certificates twice, once per stack. Rotating one does + not rotate the other. The Playground/Http2/Rotate and + Playground/Http3/Rotate samples exist as a pair for exactly this reason.
+ +

Serving several names: SNI

+

Register each name with its certificate before the engine starts serving. The default + certificate given to the constructor answers anything unmatched - including clients that send no + SNI at all.

+ +
using var engine = new QuicEngine(defaultCert, defaultKey, cidLength: 8, alpn: ["h3"]);
+
+engine.AddHost("alpha.test", alphaCert, alphaKey);
+engine.AddHost("beta.test",  betaCert,  betaKey);
+
+var quic = new QuicOptions { Port = 8443, ConnectionFactory = engine.CreateFactory() };
+ +

Certificates are given as PEM paths, because that is how ngtcp2 loads them. The host + table is closed by CreateFactory: adding a name afterwards throws, because the + handshake reads that table without a lock and a concurrent write would be a data race rather + than a late registration. To change what a running server offers, use the next section.

+ +

Renewing certificates without dropping traffic

+

ReplaceCertificates swaps the whole set atomically on a live engine - the + default and every named host together, so the server is never briefly serving a mix of + generations.

+ +
engine.ReplaceCertificates(
+    new QuicCertificate(renewedCert, renewedKey),
+    new Dictionary<string, QuicCertificate>
+    {
+        ["alpha.test"] = new(alphaRenewedCert, alphaRenewedKey),
+        ["beta.test"]  = new(betaRenewedCert,  betaRenewedKey),
+    });
+ +

Three properties worth knowing before you wire this to a renewal hook:

+
    +
  • The set replaces, it does not merge. A host left out of the dictionary stops being + served by name and falls back to the default certificate. Pass the full set every time.
  • +
  • Connections in flight keep the generation they started with. picotls keeps reading + the context for the life of the connection, so a renewal a moment later installs a new + generation without disturbing anything already handshaking or established.
  • +
  • It does not change who may connect. Client trust anchors and whether a client + certificate is required belong to the engine, not to this call - so renewing a server + certificate can never quietly widen or narrow access.
  • +
+ +

The old contexts are kept rather than freed, because a handshake may be between reading one + and using what it found, and picotls does not refcount contexts. They are released when the + engine is disposed. Renewing a handful of names a few times a year costs kilobytes.

+ +

Mutual TLS

+

Give the engine the trust anchors, and say whether a certificate is mandatory.

+ +
using var engine = new QuicEngine(
+    certPath, keyPath, cidLength: 8, alpn: ["h3"],
+    clientCaPemPath: "ca.pem",           // or clientCaPem: "-----BEGIN CERTIFICATE-----..."
+    requireClientCertificate: true);
+ +

With requireClientCertificate: false a certificate is requested and verified if + offered, but its absence is not fatal - useful when authorisation is decided per route rather + than per connection. Anchors are fixed when the engine is built; unlike server certificates they + are not replaceable on a running engine.

+ +

Once the handshake completes, the connection carries the peer's identity:

+ + + +
PeerSubjectThe full subject, rendered for humans to read.
PeerCommonNameThe CN taken structurally from the + distinguished name.
+ +
Compare PeerCommonName, never a substring of + PeerSubject. The rendered form escapes a literal / as + \/, which still contains a / - so an organisation named + Acme\/CN=admin.internal renders as something a + Contains("/CN=admin.internal") check happily accepts, while being an entirely + different principal. The structural field exists to make that class of bug impossible.
+ +

When the client changes address

+

This is what QUIC's connection ids are for, and it is far more ordinary than "the user + switched from wifi to cellular". Home and mobile NATs recycle UDP mappings after fairly short + idle periods, so a connection that goes quiet and then speaks again can reappear from a + different source port without the client having moved at all.

+ +

ioxide feeds ngtcp2 the address each datagram actually arrived on, and migration is then + ngtcp2's decision rather than ours: it probes the new path with PATH_CHALLENGE and + waits for the matching PATH_RESPONSE. Until that completes the new path is under an + anti-amplification limit, so a forged address cannot be used to make the server flood a + third party. Adoption of the new path happens before validation finishes - the limit, not the + ordering, is what makes that safe. The connection is reported to the application through + UpdatePeerAddress, and the streams on it never notice.

+ +

Keeping a moved client on its own reactor

+

A single-reactor server handles the above and is done. A real one runs a reactor per core, + all bound to the same UDP port through SO_REUSEPORT, and there the kernel decides + which reactor gets each datagram by hashing the sender's address. Change the address and + the hash picks a different reactor - one that has never heard of this connection.

+ +
The connection cannot move to meet the packet. The + ngtcp2_conn, the picotls session, the open streams and their ring-bound buffers are + owned by one reactor thread. Handing live state to whichever reactor a datagram happened to land + on is the one thing a shared-nothing runtime forbids. So the datagram moves instead.
+ +

ioxide mints its own connection ids, so it writes the owning reactor into them: the first + byte is chosen so that cid[0] % ReactorCount is the owner, with the rest left + random. The id travels with the connection, so it keeps naming the right reactor whatever the + address does. QuicOptions.Routing then decides who acts on that.

+ + + + + + + + + +
connections that never migrateconnections that do
Forward (default)nothing - the path is never enteredabout 8.5 µs per datagram
KernelFilterfree while there is CPU headroom; about -12% throughput at saturationnothing
+ +

Forward leaves the kernel hashing as it always did. A reactor that + receives a short-header packet for an id it does not hold reads the owner from that first byte, + copies the datagram, and posts it across on the queue reactors already use to hand each other + work. The copy is the point rather than an inefficiency: the payload lives in the receiving + reactor's provided-buffer ring, which is handed back the instant dispatch returns, so passing a + pointer into it would be a use-after-free under load. What crosses a thread is bytes, never + reactor state.

+ +

KernelFilter additionally attaches a classic-BPF program to the reuseport + group so the kernel reads that byte itself and delivers straight to the owner. It needs reactors + to open their UDP sockets in shard order, since the program answers with a position in the + reuseport group and that position is bind order - a startup-only rendezvous that does not exist + under the default. It also degrades rather than fails: if the kernel refuses the program (an + older kernel, a seccomp policy, a restricted container) ioxide says so and forwarding stays + underneath. Correctness never depends on the filter; only cost does.

+ +

Which to choose is a question of who pays. Forward charges only the connections + that actually migrate, and charges them a cross-thread wake per datagram - measurably a + latency cost rather than a CPU one, since CPU per request barely moved even with every + datagram forwarded. KernelFilter charges every packet a little kernel work, which is + invisible while there is CPU to spare and real once there is not. Unless a large share of your + clients migrate, Forward is cheaper in aggregate, which is why it is the default.

+ +
Only short headers are forwarded. A short header means the handshake is + finished, so the id is one this server minted and its first byte genuinely names the owner. A + long header carries an id the client chose, and routing on a byte the peer controls + would let anyone aim traffic at a reactor of their choosing.
+ +

Watching it work

+ + + + +
QuicForwardsSentDatagrams that arrived at the wrong reactor and + were handed to their owner. Zero on a server whose clients never move; rising is normal where + they do.
QuicForwardsDroppedShould stay at zero. Rises only when a + reactor is not keeping up with what its siblings hand it; the peers retransmit.
QuicStaleDatagramsShort headers addressed to this + reactor for an id it no longer holds. Ordinary - a migration retires connection ids and + packets already in flight still carry the old ones.
+ +

One more thing worth checking on any box serving QUIC seriously: ioxide asks each UDP socket + for UdpOptions.SocketBufferBytes (8 MiB by default), and Linux silently clamps + that to net.core.rmem_max - 212,992 bytes on a stock install - rather than failing. + ioxide reads the granted size back and says so once at startup. Raising the cap is not + automatically an improvement: it trades early drops, which congestion control is built to read, + for a deep standing queue. Measure it on the deployment.

+ +

Continue with QUIC & HTTP/3 for the + packet-level path through the transport, or TLS for the TCP-side + stack this one deliberately shares nothing with.

+
+
+ + + + + + diff --git a/docs/index.html b/docs/index.html index e3eac6d8..bc887866 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7321,7 +7321,7 @@

Client · static files

diff --git a/docs/learn/architecture.html b/docs/learn/architecture.html index 790576c7..e12bd5dc 100644 --- a/docs/learn/architecture.html +++ b/docs/learn/architecture.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/clients.html b/docs/learn/clients.html index 2021abf1..9105c9c3 100644 --- a/docs/learn/clients.html +++ b/docs/learn/clients.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/dev-core.html b/docs/learn/dev-core.html index d9e81d5b..797e39a1 100644 --- a/docs/learn/dev-core.html +++ b/docs/learn/dev-core.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/dev-file.html b/docs/learn/dev-file.html index e7787f1a..52927d1d 100644 --- a/docs/learn/dev-file.html +++ b/docs/learn/dev-file.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/dev-pg.html b/docs/learn/dev-pg.html index b9541ca6..ca200c0b 100644 --- a/docs/learn/dev-pg.html +++ b/docs/learn/dev-pg.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/dev-redis.html b/docs/learn/dev-redis.html index b7e83ed4..2fd97853 100644 --- a/docs/learn/dev-redis.html +++ b/docs/learn/dev-redis.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/dev-tls.html b/docs/learn/dev-tls.html index a3fe4485..898c9856 100644 --- a/docs/learn/dev-tls.html +++ b/docs/learn/dev-tls.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/files.html b/docs/learn/files.html index fee98c48..75d699c9 100644 --- a/docs/learn/files.html +++ b/docs/learn/files.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/multiport.html b/docs/learn/multiport.html index 2e4861a4..3db338d6 100644 --- a/docs/learn/multiport.html +++ b/docs/learn/multiport.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/overview.html b/docs/learn/overview.html index 115dad10..6d4281b0 100644 --- a/docs/learn/overview.html +++ b/docs/learn/overview.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/postgres.html b/docs/learn/postgres.html index ac0eb5d6..49ab665e 100644 --- a/docs/learn/postgres.html +++ b/docs/learn/postgres.html @@ -31,6 +31,7 @@
Internals
Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
Packages
ioxide core ioxide.pg diff --git a/docs/learn/quic-h3.html b/docs/learn/quic-h3.html index 1369ab87..b9bbe8ce 100644 --- a/docs/learn/quic-h3.html +++ b/docs/learn/quic-h3.html @@ -183,8 +183,9 @@

Ingress: the life of a datagram

long-header packet is a new handshake: the factory runs iq_accept (validates the Initial, creates the ngtcp2 conn, mints our SCID), the reactor snapshots the peer address, registers the CIDs, inits the two-owner refcount, and launches your - QuicHandle fault-observed. Unknown short-header packets are dropped - stale - traffic from dead connections. + QuicHandle fault-observed. An unknown short-header packet is either + stale traffic from a dead connection or a live one whose client changed address - see + Multi-reactor routing, which is how the two are told apart.
  • Engine read. OnDatagram feeds the payload to iq_conn_read. ngtcp2 decrypts, handles ACKs and flow control, and fires callbacks mid-call - the important one being stream data.
  • @@ -205,6 +206,38 @@

    Ingress: the life of a datagram

    still executing iq_conn_read above it on the same stack. Deferring the wake to after the engine call makes reentrancy impossible by construction. +

    Multi-reactor routing, and the client that moves

    +

    Everything above describes one reactor. A real server runs one per core, all bound to the + same UDP port through SO_REUSEPORT, and the kernel decides which of them gets each + datagram by hashing the sender's address. That is a perfectly good answer right up until the + address changes.

    + +

    It changes more often than "the user switched networks" suggests: home and mobile NATs + recycle UDP mappings after fairly short idle periods, so a connection that goes quiet and speaks + again can come back from a different port without the client having moved at all. The hash then + picks a different reactor - one that has never heard of this connection. Its packets + carry a short header, so there is no handshake to accept and nothing to look up. Historically + they were dropped, and the connection died on the reactor that could still have served it.

    + +
    The connection cannot come to the packet. The ngtcp2_conn, + the picotls session, the open streams and their ring-bound buffers are owned by one reactor + thread, and QuicConnection is reactor-thread-only throughout. Moving live state to + whichever reactor a datagram happened to land on is the one thing shared-nothing forbids. So the + datagram moves instead - which is not a violation but the model working as intended, + the same message passing ScheduleOnReactor already exists for.
    + +

    Both routing modes rest on one trick: ioxide mints its own connection ids, so it can write + the owning reactor into them. The first byte is chosen so that cid[0] % ReactorCount + is the owner, while the remaining randomness is untouched. The id travels with the connection, so + it keeps naming the right reactor no matter what the address does.

    + +

    Two modes decide who does the routing. QuicRouting.Forward, the default, lets + the kernel hash as before and has the receiving reactor hand a stranger to its owner over the + post queue - so nothing is paid until a client actually moves. QuicRouting.KernelFilter + attaches a classic-BPF program to the reuseport group so the kernel reads the shard byte itself. + The full comparison, with numbers, is on + How ioxide does HTTP/3.

    +

    The read surface

    The handler-facing API deliberately mirrors TcpConnection - the arm flag, the sticky pending bit that closes the lost-wakeup race, the generation token - but it is a diff --git a/docs/learn/redis.html b/docs/learn/redis.html index 032dc754..affb40bd 100644 --- a/docs/learn/redis.html +++ b/docs/learn/redis.html @@ -31,6 +31,7 @@

    Internals
    Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
    Packages
    ioxide core ioxide.pg diff --git a/docs/learn/tls.html b/docs/learn/tls.html index af5b0646..df453d84 100644 --- a/docs/learn/tls.html +++ b/docs/learn/tls.html @@ -31,6 +31,7 @@
    Internals
    Architecture QUIC & HTTP/3 + How ioxide does HTTP/3
    Packages
    ioxide core ioxide.pg From f44c8023f8241f94e2dcddb961652a9a940f1984 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Wed, 19 Aug 2026 21:04:16 +0100 Subject: [PATCH 04/11] playground: show the QUIC routing knob at its default, and regenerate the panes Every sample carries its full knob set at the shipping default, so a reader can see what is configurable without going looking. QuicOptions.Routing was missing from all twelve QUIC and HTTP/3 samples. Set explicitly to QuicRouting.Forward - the default - with what the choice actually means: Forward costs nothing until a client changes address, KernelFilter has the kernel route by connection id instead and costs a little on every packet. The comment points at /how-ioxide-does-h3 rather than restating the measurements, so there is one place for them to be wrong. Site panes regenerated from the samples, since they are generated rather than written. --- Playground/Http3/ManagedBuffered/Program.cs | 4 ++ .../Http3/ManagedStreamedBoth/Program.cs | 4 ++ Playground/Http3/MutualTls/Program.cs | 4 ++ Playground/Http3/Nghttp3Buffered/Program.cs | 4 ++ Playground/Http3/Nghttp3Request/Program.cs | 4 ++ Playground/Http3/Nghttp3Response/Program.cs | 4 ++ Playground/Http3/Rotate/Program.cs | 4 ++ Playground/Http3/Sni/Program.cs | 4 ++ Playground/Proxy/H3ToH1/Program.cs | 4 ++ Playground/Quic/Alpn/Program.cs | 4 ++ Playground/Quic/Pipe/Program.cs | 4 ++ Playground/Quic/Raw/Program.cs | 4 ++ docs/index.html | 48 +++++++++++++++++++ 13 files changed, 96 insertions(+) diff --git a/Playground/Http3/ManagedBuffered/Program.cs b/Playground/Http3/ManagedBuffered/Program.cs index 413a2cc8..d1e52fab 100644 --- a/Playground/Http3/ManagedBuffered/Program.cs +++ b/Playground/Http3/ManagedBuffered/Program.cs @@ -56,6 +56,10 @@ Port = quicPort, LocalCidLength = 8, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Http3/ManagedStreamedBoth/Program.cs b/Playground/Http3/ManagedStreamedBoth/Program.cs index 0dba0a09..d52822eb 100644 --- a/Playground/Http3/ManagedStreamedBoth/Program.cs +++ b/Playground/Http3/ManagedStreamedBoth/Program.cs @@ -65,6 +65,10 @@ Port = quicPort, LocalCidLength = 8, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Http3/MutualTls/Program.cs b/Playground/Http3/MutualTls/Program.cs index c25daf6a..d77b0636 100644 --- a/Playground/Http3/MutualTls/Program.cs +++ b/Playground/Http3/MutualTls/Program.cs @@ -80,6 +80,10 @@ Port = quicPort, LocalCidLength = 8, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Http3/Nghttp3Buffered/Program.cs b/Playground/Http3/Nghttp3Buffered/Program.cs index 446428c8..aeaf95a7 100644 --- a/Playground/Http3/Nghttp3Buffered/Program.cs +++ b/Playground/Http3/Nghttp3Buffered/Program.cs @@ -99,6 +99,10 @@ LocalCidLength = (int)cidLength, // must match the engine's cidLength IdleTimeoutMs = idleTimeoutMs, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Http3/Nghttp3Request/Program.cs b/Playground/Http3/Nghttp3Request/Program.cs index 007b908a..32e7146b 100644 --- a/Playground/Http3/Nghttp3Request/Program.cs +++ b/Playground/Http3/Nghttp3Request/Program.cs @@ -93,6 +93,10 @@ LocalCidLength = 8, // must match the engine's cidLength IdleTimeoutMs = 60_000, // close a connection idle this long (no packets) ConnectionFactory = engine.CreateFactory(), // the engine adopts each new connection + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Http3/Nghttp3Response/Program.cs b/Playground/Http3/Nghttp3Response/Program.cs index 869e918e..a5b1d15c 100644 --- a/Playground/Http3/Nghttp3Response/Program.cs +++ b/Playground/Http3/Nghttp3Response/Program.cs @@ -68,6 +68,10 @@ Port = quicPort, LocalCidLength = 8, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Http3/Rotate/Program.cs b/Playground/Http3/Rotate/Program.cs index a3eaa4d5..72f1b9f6 100644 --- a/Playground/Http3/Rotate/Program.cs +++ b/Playground/Http3/Rotate/Program.cs @@ -114,6 +114,10 @@ LocalCidLength = 8, // From here the host table is live, and only ReplaceCertificates may change it. ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Http3/Sni/Program.cs b/Playground/Http3/Sni/Program.cs index bc06cf5e..c35f9162 100644 --- a/Playground/Http3/Sni/Program.cs +++ b/Playground/Http3/Sni/Program.cs @@ -85,6 +85,10 @@ LocalCidLength = 8, // From here the host table is live and closed to further additions. ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Proxy/H3ToH1/Program.cs b/Playground/Proxy/H3ToH1/Program.cs index 53ae2330..6ece64ec 100644 --- a/Playground/Proxy/H3ToH1/Program.cs +++ b/Playground/Proxy/H3ToH1/Program.cs @@ -75,6 +75,10 @@ LocalCidLength = 8, // CID bytes this endpoint mints (must match the engine) IdleTimeoutMs = 60_000, // transport idle backstop; 0 disables sweep eviction ConnectionFactory = engine.CreateFactory(), // adopts new connections into the engine + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Quic/Alpn/Program.cs b/Playground/Quic/Alpn/Program.cs index 33133e10..dc3a90ee 100644 --- a/Playground/Quic/Alpn/Program.cs +++ b/Playground/Quic/Alpn/Program.cs @@ -70,6 +70,10 @@ LocalCidLength = 8, // must match the engine's cidLength IdleTimeoutMs = 60_000, // close a connection idle this long (no packets) ConnectionFactory = engine.CreateFactory(), // the engine adopts each new connection + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; diff --git a/Playground/Quic/Pipe/Program.cs b/Playground/Quic/Pipe/Program.cs index 47d397dc..a40a1195 100644 --- a/Playground/Quic/Pipe/Program.cs +++ b/Playground/Quic/Pipe/Program.cs @@ -72,6 +72,10 @@ Port = quicPort, // every reactor binds it via SO_REUSEPORT LocalCidLength = 8, // short headers carry no CID length on the wire ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, IdleTimeoutMs = 60_000, // transport backstop; 0 disables the sweep }, }; diff --git a/Playground/Quic/Raw/Program.cs b/Playground/Quic/Raw/Program.cs index 7cbbeb96..db71ccfb 100644 --- a/Playground/Quic/Raw/Program.cs +++ b/Playground/Quic/Raw/Program.cs @@ -71,6 +71,10 @@ Port = quicPort, // every reactor binds it via SO_REUSEPORT LocalCidLength = 8, // short headers carry no CID length on the wire ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, IdleTimeoutMs = 60_000, // transport backstop; 0 disables the sweep }, }; diff --git a/docs/index.html b/docs/index.html index bc887866..f9ba4ed9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1137,6 +1137,10 @@

    h3 · a certificate per host

    LocalCidLength = 8, // From here the host table is live and closed to further additions. ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -1245,6 +1249,10 @@

    h3 · certificate renewal

    LocalCidLength = 8, // From here the host table is live, and only ReplaceCertificates may change it. ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -2386,6 +2394,10 @@

    HTTP/3 · buffered

    Port = quicPort, LocalCidLength = 8, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -2467,6 +2479,10 @@

    HTTP/3 · response streamed (nghttp3)

    Port = quicPort, LocalCidLength = 8, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -2568,6 +2584,10 @@

    HTTP/3 · request + response streamed

    Port = quicPort, LocalCidLength = 8, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -2737,6 +2757,10 @@

    HTTP/3 · mutual TLS

    Port = quicPort, LocalCidLength = 8, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -2861,6 +2885,10 @@

    HTTP/3 · buffered (nghttp3)

    LocalCidLength = (int)cidLength, // must match the engine's cidLength IdleTimeoutMs = idleTimeoutMs, ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -3025,6 +3053,10 @@

    QUIC · two protocols by ALPN

    LocalCidLength = 8, // must match the engine's cidLength IdleTimeoutMs = 60_000, // close a connection idle this long (no packets) ConnectionFactory = engine.CreateFactory(), // the engine adopts each new connection + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -5409,6 +5441,10 @@

    QUIC · pipes

    Port = quicPort, // every reactor binds it via SO_REUSEPORT LocalCidLength = 8, // short headers carry no CID length on the wire ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, IdleTimeoutMs = 60_000, // transport backstop; 0 disables the sweep }, }; @@ -5509,6 +5545,10 @@

    QUIC · raw streams

    Port = quicPort, // every reactor binds it via SO_REUSEPORT LocalCidLength = 8, // short headers carry no CID length on the wire ConnectionFactory = engine.CreateFactory(), + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, IdleTimeoutMs = 60_000, // transport backstop; 0 disables the sweep }, }; @@ -5625,6 +5665,10 @@

    HTTP/3 · request streamed (nghttp3)

    LocalCidLength = 8, // must match the engine's cidLength IdleTimeoutMs = 60_000, // close a connection idle this long (no packets) ConnectionFactory = engine.CreateFactory(), // the engine adopts each new connection + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; @@ -6270,6 +6314,10 @@

    HTTP/3 in · HTTP/1.1 out

    LocalCidLength = 8, // CID bytes this endpoint mints (must match the engine) IdleTimeoutMs = 60_000, // transport idle backstop; 0 disables sweep eviction ConnectionFactory = engine.CreateFactory(), // adopts new connections into the engine + // Where a moved client's packets go when several reactors share the port. Forward costs + // nothing until a client actually changes address; KernelFilter has the kernel route by + // connection id instead, which costs a little on every packet. See /how-ioxide-does-h3. + Routing = QuicRouting.Forward, }, }; From f172bd35fef9efab9528ae009d9b50c266bb8f30 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Wed, 19 Aug 2026 21:08:15 +0100 Subject: [PATCH 05/11] chore: 0.7.210 across the packages --- src/clients/ioxide.file/ioxide.file.csproj | 2 +- src/clients/ioxide.httpclient/ioxide.httpclient.csproj | 2 +- src/clients/ioxide.pg/ioxide.pg.csproj | 2 +- src/clients/ioxide.redis/ioxide.redis.csproj | 2 +- src/ioxide/ioxide.csproj | 2 +- src/protocols/ioxide.http2/ioxide.http2.csproj | 2 +- src/protocols/ioxide.http3/ioxide.http3.csproj | 2 +- src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj | 2 +- src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj | 2 +- src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj | 2 +- src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/clients/ioxide.file/ioxide.file.csproj b/src/clients/ioxide.file/ioxide.file.csproj index 92e9ac96..46746df4 100644 --- a/src/clients/ioxide.file/ioxide.file.csproj +++ b/src/clients/ioxide.file/ioxide.file.csproj @@ -8,7 +8,7 @@ ioxide.file ioxide.file - 0.7.209 + 0.7.210 MDA2AV File serving for the ioxide io_uring runtime: immutable asset snapshots with baked responses, pooled positional ring reads, atomic reloads. MIT diff --git a/src/clients/ioxide.httpclient/ioxide.httpclient.csproj b/src/clients/ioxide.httpclient/ioxide.httpclient.csproj index ac0cef5c..95e3a5d9 100644 --- a/src/clients/ioxide.httpclient/ioxide.httpclient.csproj +++ b/src/clients/ioxide.httpclient/ioxide.httpclient.csproj @@ -8,7 +8,7 @@ ioxide.httpclient ioxide.httpclient - 0.7.209 + 0.7.210 MDA2AV The ring-native HTTP/1.1 client for the ioxide io_uring runtime - the upstream leg between a proxy and an origin. Connections are opened on the reactor thread that will use them, so a request never crosses a thread on its way out or back, and every response resumes the awaiting handler inline on its own reactor. Includes client-side TLS (SNI, ALPN, certificate verification and client certificates for mutual TLS) for https:// origins. Depends on ioxide core alone: no protocol package, no native asset. MIT diff --git a/src/clients/ioxide.pg/ioxide.pg.csproj b/src/clients/ioxide.pg/ioxide.pg.csproj index 724fc9a0..49d944b8 100644 --- a/src/clients/ioxide.pg/ioxide.pg.csproj +++ b/src/clients/ioxide.pg/ioxide.pg.csproj @@ -8,7 +8,7 @@ ioxide.pg ioxide.pg - 0.7.209 + 0.7.210 MDA2AV Postgres driver for the ioxide io_uring runtime: pooled ring-native connections per reactor, ring-native connect and handshake, inline completion resume. MIT diff --git a/src/clients/ioxide.redis/ioxide.redis.csproj b/src/clients/ioxide.redis/ioxide.redis.csproj index dd95c79b..5347c15e 100644 --- a/src/clients/ioxide.redis/ioxide.redis.csproj +++ b/src/clients/ioxide.redis/ioxide.redis.csproj @@ -8,7 +8,7 @@ ioxide.redis ioxide.redis - 0.7.209 + 0.7.210 MDA2AV Redis client for the ioxide io_uring runtime: pooled ring-native connections per reactor, full RESP2 protocol, a generic command API plus typed helpers (strings, keys, hashes, lists, sets, sorted sets, pub/sub, transactions, scripting), and pipelining. Inline completion resume. MIT diff --git a/src/ioxide/ioxide.csproj b/src/ioxide/ioxide.csproj index d2970e4c..ddcd4bf0 100644 --- a/src/ioxide/ioxide.csproj +++ b/src/ioxide/ioxide.csproj @@ -8,7 +8,7 @@ ioxide ioxide - 0.7.209 + 0.7.210 MDA2AV A shared-nothing io_uring runtime for .NET: one ring per reactor thread, inline completions, zero native dependencies. The engine - reactor, connection, and the IRingHost client seam. Includes TLS termination: the OpenSSL handshake driven over the ring, then kernel TLS (kTLS) transmit offload, so handlers keep writing plaintext. TLS needs OpenSSL 3 and the Linux tls module; nothing else does, and neither is loaded unless you use it. MIT diff --git a/src/protocols/ioxide.http2/ioxide.http2.csproj b/src/protocols/ioxide.http2/ioxide.http2.csproj index c7aada48..ea5b738e 100644 --- a/src/protocols/ioxide.http2/ioxide.http2.csproj +++ b/src/protocols/ioxide.http2/ioxide.http2.csproj @@ -8,7 +8,7 @@ ioxide.http2 ioxide.http2 - 0.7.209 + 0.7.210 MDA2AV Pure-C# HTTP/2 for the ioxide io_uring runtime: framing, HPACK (static and dynamic tables, Huffman) and flow control, with zero native code. Serves h2c with prior knowledge and h2 over TLS by ALPN, buffered or streamed in either direction. MIT diff --git a/src/protocols/ioxide.http3/ioxide.http3.csproj b/src/protocols/ioxide.http3/ioxide.http3.csproj index e92cb86b..f81501e7 100644 --- a/src/protocols/ioxide.http3/ioxide.http3.csproj +++ b/src/protocols/ioxide.http3/ioxide.http3.csproj @@ -8,7 +8,7 @@ ioxide.http3 ioxide.http3 - 0.7.209 + 0.7.210 MDA2AV Pure C# HTTP/3 for the ioxide io_uring runtime: frame parsing, QPACK (static table + Huffman) and request dispatch with zero native dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, drop-in alternative to ioxide.nghttp3. MIT diff --git a/src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj b/src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj index 5c59c5d3..97163a22 100644 --- a/src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj +++ b/src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj @@ -8,7 +8,7 @@ ioxide.nghttp2 ioxide.nghttp2 - 0.7.209 + 0.7.210 MDA2AV HTTP/2 for the ioxide io_uring runtime: framing, HPACK and flow control from nghttp2, statically linked behind a small shim with no external dependencies beyond libc. Serves HTTP/2 over any TcpConnection - h2c with prior knowledge, or h2 over TLS via ALPN. nghttp2 is sans-I/O, so ioxide keeps the ring and the loop. MIT diff --git a/src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj b/src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj index e5820f53..48c2adb9 100644 --- a/src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj +++ b/src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj @@ -8,7 +8,7 @@ ioxide.nghttp3 ioxide.nghttp3 - 0.7.209 + 0.7.210 MDA2AV HTTP/3 layer for the ioxide io_uring runtime: nghttp3 (H3 + QPACK) bundled as a single self-contained native library with no external dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, no ioxide.ngtcp2 dependency. MIT diff --git a/src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj b/src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj index 6f76761d..9f631891 100644 --- a/src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj +++ b/src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj @@ -8,7 +8,7 @@ ioxide.ngtcp2 ioxide.ngtcp2 - 0.7.209 + 0.7.210 MDA2AV QUIC engine for the ioxide io_uring runtime: ngtcp2 + picotls bundled as a single self-contained native library (only system dependency: libcrypto.so.3 / OpenSSL 3.x). Plugs into the reactor's QUIC transport via QuicConnection. Server side; engine bindings in progress. MIT diff --git a/src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj b/src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj index 43cb9c0e..136fc25e 100644 --- a/src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj +++ b/src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj @@ -8,7 +8,7 @@ ioxide.Kestrel ioxide.Kestrel - 0.7.209 + 0.7.210 MDA2AV ASP.NET Core Kestrel transport backed by the ioxide io_uring runtime: one reactor (ring) per core, SO_REUSEPORT load-balanced, with Kestrel's HTTP request loop pinned to the reactor thread. Drop-in via UseIoxide(). MIT From b745e0885899575f8020146889ef8343ab792038 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Thu, 20 Aug 2026 00:40:16 +0100 Subject: [PATCH 06/11] tests: stop the h3 client opening a control stream per request H3TestClient built a fresh HTTP/3 session inside Request() - ih3_client_new plus a new control, QPACK encoder and QPACK decoder stream - on every call. HTTP/3 permits exactly one control stream per peer, and RFC 9114 6.2.1 requires a second one to be treated as a connection error of type H3_STREAM_CREATION_ERROR. So from the second request onward this client was speaking invalid HTTP/3. The server was right and said so. nghttp3 returned H3_STREAM_CREATION_ERROR from read_stream on the duplicate stream (type 0x00 carrying SETTINGS, and later a duplicate QPACK decoder), the run loop set _protocolFailed, exited, and closed the connection - about a millisecond after the first response. Every nghttp3 run loop did it: buffered sync, buffered async, streaming, and streamed response. The pure-C# stack did not, because it never saw a second control stream: it only ever ran one request per connection in these tests. None of that was visible, for the worst possible reason. The requests already in flight kept being answered off state the transport had already dismantled - the CIDs unregistered, the connection out of _quicConnSet, PeerAddr freed and zeroed, the transport's reference dropped - so three more requests returned 200 apiece and every test stayed green. A test client that provokes a connection error and then passes anyway is worse than one that fails. Found while investigating why a migrated connection's pinned socket died instantly: the pin is released in QuicRemoveConnection, which this was triggering after the first response. Fixing this is a prerequisite for that work, and for any test that means to exercise more than one request over one connection. The session now stands up once per connection; only the bidi request stream and the response state are per request. 431 pass across E2E, Unit, Chaos, Http, Tls and File. Connections now live to the end of their test and exit clean (isClosed=True, protocolFailed=False) rather than on a protocol failure. --- tests/Ioxide.Tests.Harness/H3TestClient.cs | 59 ++++++++++++++++------ 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/tests/Ioxide.Tests.Harness/H3TestClient.cs b/tests/Ioxide.Tests.Harness/H3TestClient.cs index d0bd1a42..002914da 100644 --- a/tests/Ioxide.Tests.Harness/H3TestClient.cs +++ b/tests/Ioxide.Tests.Harness/H3TestClient.cs @@ -155,23 +155,13 @@ public bool CompleteHandshake(int timeoutMs) public (int Status, string Body) Request(string method, string path, byte[]? body, (string Name, string Value)[]? extraHeaders, int timeoutMs) { - // Client H3 conn + its control/QPACK uni streams. - var h3Cbs = new Ih3Callbacks - { - OnBeginHeaders = &OnH3BeginHeaders, - OnHeader = &OnH3Header, - OnEndHeaders = &OnH3EndHeaders, - OnData = &OnH3Data, - OnEndStream = &OnH3EndStream, - }; - _h3 = ih3_client_new(h3Cbs, (void*)GCHandle.ToIntPtr(_self)); - Assert.True(_h3 != 0, "h3 client conn init failed"); + EnsureH3Session(); - long ctrl = iq_conn_open_uni(_conn); - long qenc = iq_conn_open_uni(_conn); - long qdec = iq_conn_open_uni(_conn); - Assert.True(ctrl >= 0 && qenc >= 0 && qdec >= 0, "failed to open client uni streams"); - Assert.True(ih3_bind_streams(_h3, ctrl, qenc, qdec) == 0, "bind streams failed"); + // Per request: a fresh bidi stream and fresh response state. The H3 SESSION is not per + // request - see EnsureH3Session. + _status = -1; + _body.Clear(); + _done = false; _requestSid = iq_client_open_bidi(_conn); Assert.True(_requestSid >= 0, "failed to open request stream"); @@ -217,6 +207,43 @@ public bool CompleteHandshake(int timeoutMs) return (_status, Encoding.UTF8.GetString(_body.ToArray())); } + /// + /// Stand up the HTTP/3 session once per connection: the client conn and its control, QPACK + /// encoder and QPACK decoder streams. + /// + /// Once, not per request. HTTP/3 permits exactly one control stream per peer, and RFC 9114 + /// 6.2.1 requires a second one to be treated as a connection error of type + /// H3_STREAM_CREATION_ERROR. Doing this per request opened a fresh control and QPACK pair every + /// time, so from the second request onward this client was speaking invalid HTTP/3 - and a + /// correct server answered by killing the connection. It still looked green, because the + /// requests were already in flight and kept being served off state the server had torn down, + /// which is precisely the sort of thing a test client must not do. + /// + private void EnsureH3Session() + { + if (_h3 != 0) + { + return; + } + + var h3Cbs = new Ih3Callbacks + { + OnBeginHeaders = &OnH3BeginHeaders, + OnHeader = &OnH3Header, + OnEndHeaders = &OnH3EndHeaders, + OnData = &OnH3Data, + OnEndStream = &OnH3EndStream, + }; + _h3 = ih3_client_new(h3Cbs, (void*)GCHandle.ToIntPtr(_self)); + Assert.True(_h3 != 0, "h3 client conn init failed"); + + long ctrl = iq_conn_open_uni(_conn); + long qenc = iq_conn_open_uni(_conn); + long qdec = iq_conn_open_uni(_conn); + Assert.True(ctrl >= 0 && qenc >= 0 && qdec >= 0, "failed to open client uni streams"); + Assert.True(ih3_bind_streams(_h3, ctrl, qenc, qdec) == 0, "bind streams failed"); + } + // Pump the client H3 engine's egress (prefaces, the request) into the QUIC engine per stream. private readonly byte[] _h3Buf = new byte[16 * 1024]; From 7cacc9d5a4d31d2959acf1ce88098d022dd7ca33 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Thu, 20 Aug 2026 08:27:27 +0100 Subject: [PATCH 07/11] tests: give the h3 client loss recovery, so a lost datagram is not fatal H3TestClient never called iq_conn_handle_expiry. The shim has exported it, and iq_conn_expiry beside it, since the engine binding landed - the client simply never fired either. So it had no loss recovery whatsoever: a dropped datagram left its packet unacked forever, nothing retransmitted, the congestion window filled, and writev_stream began answering 0 with nothing consumed. Both ends then sat silent until the 10 s write deadline called it a stall. The diagnosis took a while because the symptom accuses the server. What settled it was that the client's own sent and received datagram counts were FROZEN across every spin - it was neither sending nor receiving, so it was not blocked on anything the peer had done. Flow control looks different and recovers: the streaming-upload test shows n=-208 (STREAM_DATA_BLOCKED) and comes back. Here it was n=0 with consumed=-1, which is ngtcp2 saying it has nothing to send at all. Nothing here ever lost a packet before, which is why an omission this size survived: every connection served one request over loopback and was gone. Once connections started living across several requests, and an address change started discarding datagrams in flight to the forwarder's old socket, the gap became a hang - and it presented as a server that stopped answering after a migration, which is exactly the bug it is not. Timers fire from PumpIn, which every wait loop already drives. Three consecutive full E2E runs green; the migration tests had been failing roughly two runs in three before this. --- tests/Ioxide.Tests.Harness/H3TestClient.cs | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/Ioxide.Tests.Harness/H3TestClient.cs b/tests/Ioxide.Tests.Harness/H3TestClient.cs index 002914da..0b4d587a 100644 --- a/tests/Ioxide.Tests.Harness/H3TestClient.cs +++ b/tests/Ioxide.Tests.Harness/H3TestClient.cs @@ -270,6 +270,7 @@ private void DrainH3Out() // datagram, falling back to the generic flush when the stream can't take more. private readonly byte[] _sendScratch = new byte[1452]; + private void WriteStream(long sid, ReadOnlySpan data, bool fin) { // Never drop: the shim's ih3_writev already told nghttp3 these bytes are written @@ -355,8 +356,40 @@ private void FlushOut() /// Whether the peer ended the connection. A refusal test asserts on this. public bool PeerClosed => _peerClosed; + /// + /// Give ngtcp2 its loss timers. Without this the client has NO loss recovery: a lost datagram + /// leaves its packet unacked forever, the congestion window fills, writev_stream starts + /// answering 0 with nothing consumed, and the connection deadlocks with both sides silent. + /// + /// It went unnoticed because nothing here ever lost a packet - each connection served one + /// request over loopback and was gone. A connection that lives for several requests across an + /// address change does lose them, and then a real server looks like it hung. + /// + private void FireExpiredTimers() + { + if (_conn == 0) + { + return; + } + + ulong now = NowNs(); + if (iq_conn_expiry(_conn) > now) + { + return; + } + + // Nonzero is terminal here exactly as it is for a read: draining, closing, or a protocol + // error, none of which a later datagram undoes. + if (iq_conn_handle_expiry(_conn, now) != 0) + { + _peerClosed = true; + } + } + private void PumpIn() { + FireExpiredTimers(); + try { IPEndPoint? from = null; @@ -506,6 +539,8 @@ private struct Ih3Callbacks [MarshalAs(UnmanagedType.LPUTF8Str)] string? keyPath, IqCallbacks cbs); [DllImport(QuicLib)] private static extern long iq_client_open_bidi(nint conn); [DllImport(QuicLib)] private static extern long iq_conn_open_uni(nint conn); + [DllImport(QuicLib)] private static extern ulong iq_conn_expiry(nint conn); + [DllImport(QuicLib)] private static extern int iq_conn_handle_expiry(nint conn, ulong ts); [DllImport(QuicLib)] private static extern nint iq_conn_write(nint conn, byte* dest, nuint destLen, long streamId, byte* data, nuint dataLen, int fin, long* pConsumed, ulong ts); [DllImport(QuicLib)] private static extern int iq_conn_read(nint conn, void* remoteSa, nuint remoteLen, byte* pkt, nuint pktLen, byte ecn, ulong ts); [DllImport(QuicLib)] private static extern int iq_conn_is_established(nint conn); From ca779a6857bb2d92baf65f4cd909401284383a8f Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Thu, 20 Aug 2026 08:48:04 +0100 Subject: [PATCH 08/11] tests: re-mint a spec fixture when it stops meaning what it was minted to mean The mtls test "a client certificate that is not valid yet is refused" failed today, reporting that the server had served a not-yet-valid certificate. It had not. The fixture had gone stale. Spec fixtures are cached by their spec rather than by freshness, deliberately, because most of them are supposed to be invalid and re-minting an expired one every run would defeat the test. That is safe in one direction only. An expired certificate stays expired forever; one minted to be NOT YET VALID becomes valid the moment its notBefore arrives. This fixture is minted with notBefore = now + 1 day, so the copy left in /tmp yesterday became a perfectly good certificate overnight, the server accepted it exactly as it should, and the test called that a product bug. A test that fails once a day has passed is a bug in the test, and one that accuses the product of a security defect is worse than most. The cached file is now reused only while its actual validity state still matches the intent - not yet started if the spec asked for that, expired if it asked for that. Confirmed both ways: planting an already-valid certificate in the not-yet-valid fixture's place makes the cache re-mint it (CN=alice back to CN=future-alice, notBefore tomorrow), where before it was served as-is and the test failed. Unrelated to the QUIC work in flight; found because the date rolled over mid-session. --- tests/Ioxide.Tests.Harness/TestCert.cs | 36 +++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/Ioxide.Tests.Harness/TestCert.cs b/tests/Ioxide.Tests.Harness/TestCert.cs index 344d294d..605e91b8 100644 --- a/tests/Ioxide.Tests.Harness/TestCert.cs +++ b/tests/Ioxide.Tests.Harness/TestCert.cs @@ -72,6 +72,38 @@ private static bool Fresh(string certPath, params string[] alsoRequired) } } + /// + /// Whether a cached spec fixture still has the validity state it was created to have. + /// + /// These fixtures are cached by content rather than by freshness, because most of them are + /// deliberately invalid and re-minting would defeat them. That is safe in one direction only: + /// an EXPIRED certificate stays expired forever, but one minted to be NOT YET VALID becomes + /// valid the moment its notBefore arrives. A "not valid yet is refused" fixture minted with + /// notBefore = now + 1 day therefore starts being served the next day, and the test fails + /// reporting a product bug that does not exist - which is exactly what happened on the day + /// this was written. + /// + /// So the cached file is reused only while its actual state still matches the intent. + /// + private static bool StillMatchesSpec(string certPath, ClientCertSpec spec) + { + try + { + using X509Certificate2 cert = X509CertificateLoader.LoadCertificateFromFile(certPath); + DateTime now = DateTime.Now; + + bool shouldBeStarted = spec.NotBefore <= TimeSpan.Zero; + bool shouldBeUnexpired = spec.NotAfter > TimeSpan.Zero; + + return cert.NotBefore <= now == shouldBeStarted + && now < cert.NotAfter == shouldBeUnexpired; + } + catch + { + return false; // truncated or not a certificate at all: mint it again + } + } + /// /// A CA, a server certificate and two client certificates - one the CA signed, one a DIFFERENT /// CA signed. mTLS cannot be tested without all four: proving a good certificate is let in says @@ -402,7 +434,9 @@ public static (string CaPath, string CertPath, string KeyPath) EnsureClientCert( // Deliberately NOT the Fresh() check: half of these are supposed to be outside their // validity window, and re-minting an expired fixture on every run would defeat the test. - if (File.Exists(certPath) && File.Exists(keyPath)) + // What IS checked is that the fixture still means what it was minted to mean - see + // StillMatchesSpec. + if (File.Exists(certPath) && File.Exists(keyPath) && StillMatchesSpec(certPath, spec)) { return (ca, certPath, keyPath); } From 7a3c707a7326de41729e6da12bd545baddab7985 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Thu, 20 Aug 2026 09:45:12 +0100 Subject: [PATCH 09/11] quic: claim a migrated client's address, so the forwarding stops after one hop Cross-reactor forwarding (#205) is correct but permanent. The kernel picks a socket by hashing the sender's address, and after a migration that address does not change back - so every one of that client's datagrams keeps landing on the wrong reactor and keeps paying a cross-thread hop for the life of the connection. A datagram socket bound to the same address as the others but connect()ed to one peer is a MORE SPECIFIC match than a wildcard bind, and the kernel's lookup takes the narrowest match before it ever reaches the reuseport hash. So the owning reactor opens one toward the peer's new address and the datagrams arrive there directly. Three things about that, each measured rather than assumed. connect() on a datagram socket puts nothing on the wire - it is a local declaration, and the peer is never told. Adding one does not re-scatter anybody else: eight established peers, none moved. And it cannot bootstrap itself, since the owning reactor only learns the new address from a datagram and the datagrams are going elsewhere - forwarding has to deliver the first one. The two are complements: forwarding makes the claim possible, the claim stops forwarding being forever. The claim is made from the reactor's sweep, NOT from the engine's path-change report, and that distinction is the whole difference between working and thrashing. ngtcp2 reports a path many times while it validates one, alternating between the old address for data and the new one for PATH_CHALLENGE probes. Claiming on each report tore down a working socket and built another, with datagrams already queued on the one it closed - losing packets in order to avoid a hop, which is the wrong way round. Measured: five claims for a single address change, flip-flopping between two ports. From the sweep it is one claim per address, because by then the path has settled. Only connections that actually moved are claimed. The sweep visits every connection every 250 ms, so a guard that asked merely "is this address claimed yet" would spend a descriptor and an armed receive on every connection on the server to change nothing - they are already being delivered here by the hash. PeerAddressMoved is the signal, set where the address is adopted. Bounded and best-effort throughout. A ceiling of 512 concurrent claims, because ngtcp2 adopts a new path BEFORE it finishes validating it, so a forged datagram can reach here; past the ceiling the connection simply keeps forwarding. A bind that fails is swallowed for the same reason - this runs under a callback whose exceptions fault the connection, and faulting a working connection to skip an optimisation is a bad trade. Ignored entirely under KernelFilter, where the kernel already routes by connection id. QuicPinsCreated and QuicPinsOpen expose it. Tests cover both directions: that the forwarding stops after a claim, that one address change produces one claim rather than one per path report, and that a fleet whose clients never move claims nothing and forwards nothing. Also here, because the migration tests could not be trusted until it was proven: both h3 stacks now assert that the SAME connection object serves before and after the address changes, and that the factory ran exactly once. Status codes cannot tell migration from a client quietly re-handshaking, and a reconnect would take the h3 session, the QPACK tables and any application state with it while the tests stayed green. Confirmed both ways - asserting a DIFFERENT connection makes both fail. 436 tests pass; E2E stable across three consecutive runs. Benchmarks against main: -0.4% Tls/OpenSsl, -0.3% Nghttp3Response, -1.1% ManagedBuffered, -0.1% ManagedStreamedBoth, and Nghttp3Buffered repeats at +2.5%/+1.9% against a baseline that measures +2.1% against itself - so no regression on either h3 stack, buffered or streamed. --- src/ioxide/Connection/Quic/QuicConnection.cs | 28 +++ src/ioxide/Native/Native.Socket.cs | 1 + .../Reactor/Transport/Quic/QuicOptions.cs | 7 + .../Transport/Quic/Reactor.Quic.Pin.cs | 179 ++++++++++++++++ .../Reactor/Transport/Quic/Reactor.Quic.cs | 8 + .../Reactor/Transport/Udp/Reactor.Udp.cs | 50 ++++- .../Protocols/QuicMigrationTests.cs | 202 ++++++++++++++++++ 7 files changed, 472 insertions(+), 3 deletions(-) create mode 100644 src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Pin.cs diff --git a/src/ioxide/Connection/Quic/QuicConnection.cs b/src/ioxide/Connection/Quic/QuicConnection.cs index 33460673..a4e0a978 100644 --- a/src/ioxide/Connection/Quic/QuicConnection.cs +++ b/src/ioxide/Connection/Quic/QuicConnection.cs @@ -143,6 +143,27 @@ protected void Send(ReadOnlySpan payload, int gsoSegmentSize = 0) Reactor.UdpSendTo(SocketFd, PeerAddr, PeerAddrLen, payload, gsoSegmentSize); } + /// + /// Index in the reactor's fd table of the socket claiming this connection's peer address, or -1 + /// when it holds none. Owned by the reactor; see Reactor.Quic.Pin.cs. + /// + internal int PinSlot = -1; + + /// + /// The address the current claim names, so a repeated report of the SAME path does not tear a + /// working claim down and build it again. Owned by the reactor. + /// + internal readonly byte[] PinnedAddr = new byte[Reactor.UdpNameCap]; + internal int PinnedAddrLen; + + /// + /// Set once this connection's peer address has actually moved. Only a connection that migrated + /// is worth claiming an address for: one still at the address it was accepted on is already + /// being delivered here by the kernel's hash, so a claim would spend a descriptor to change + /// nothing. Owned by the reactor. + /// + internal bool PeerAddressMoved; + /// Adopt a validated peer migration (copies the sockaddr out of the datagram). public unsafe void UpdatePeerAddress(nint addr, int addrLen) { @@ -157,6 +178,13 @@ public unsafe void UpdatePeerAddress(nint addr, int addrLen) Buffer.MemoryCopy((void*)addr, (void*)PeerAddr, Reactor.UdpNameCap, addrLen); PeerAddrLen = addrLen; + PeerAddressMoved = true; + + // Deliberately NOT claiming the address here. ngtcp2 reports a path repeatedly while it + // validates one - alternating between the old path for data and the new one for + // PATH_CHALLENGE probes - so a claim made on each report churns sockets between two + // addresses and drops datagrams already queued on the one it closes. The reactor's sweep + // picks this up once the address has stopped moving. See Reactor.Quic.Pin.cs. } // --- read surface: engine enqueues on the reactor thread, the handler awaits from anywhere. diff --git a/src/ioxide/Native/Native.Socket.cs b/src/ioxide/Native/Native.Socket.cs index b616764f..16788cde 100644 --- a/src/ioxide/Native/Native.Socket.cs +++ b/src/ioxide/Native/Native.Socket.cs @@ -22,6 +22,7 @@ public static unsafe partial class Native { [DllImport("libc")] public static extern int socket(int domain, int type, int proto); [DllImport("libc")] public static extern int bind(int fd, void* addr, uint len); + [DllImport("libc")] public static extern int connect(int fd, void* addr, uint len); /// Read a socket's local address - the only way to learn the port the kernel picked for a /// bind to port 0 (QUIC client sockets take an ephemeral port). [DllImport("libc")] public static extern int getsockname(int fd, void* addr, uint* len); diff --git a/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs b/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs index 5aac9a5a..be204458 100644 --- a/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs +++ b/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs @@ -17,6 +17,13 @@ public sealed record QuicOptions /// public QuicRouting Routing { get; init; } = QuicRouting.Forward; + /// + /// Under , claim a migrated client's new address with a socket + /// of the owning reactor's own, so the kernel delivers there directly and the forwarding stops + /// after the first datagram or two. Costs one file descriptor per migrated connection. + /// + public bool PinMigratedPeers { get; init; } = true; + public QuicConnectionFactory? ConnectionFactory { get; init; } /// diff --git a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Pin.cs b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Pin.cs new file mode 100644 index 00000000..08c494c4 --- /dev/null +++ b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Pin.cs @@ -0,0 +1,179 @@ +using static ioxide.Native; + +namespace ioxide; + +/// +/// The optimisation on top of cross-reactor forwarding: once a migrated client's new address is +/// known, claim it with a socket of this reactor's own so the kernel delivers there directly and +/// the forwarding stops. +/// +/// Forwarding alone is correct but permanent. The kernel picks a socket by hashing the sender's +/// address, and after a migration that address does not change back - so every one of that +/// client's datagrams keeps landing on the wrong reactor and keeps paying a hop, for the life of +/// the connection. See Reactor.Quic.Forward.cs. +/// +/// A datagram socket bound to the same address as the others but connect()ed to one peer is +/// a MORE SPECIFIC match than a wildcard bind, and the kernel's lookup takes the narrowest match +/// before it ever reaches the reuseport hash. So the owning reactor opens one of those toward the +/// peer's new address, and from the next datagram on it arrives here directly. +/// +/// Three things about that, each verified rather than assumed: +/// +/// +/// connect() on a datagram socket puts nothing on the wire. It is a local declaration +/// of intent - no handshake, and the peer is never told. +/// Adding it does NOT re-scatter anybody else. A connected socket does not take part in +/// reuseport selection, so the group stays the size it was and every other peer keeps landing +/// exactly where it did. Measured: eight established peers, none moved. +/// It cannot bootstrap itself. This reactor only learns the new address from a datagram, and +/// the datagrams are going elsewhere - so forwarding has to deliver the first one. The two are +/// complements: forwarding makes the pin possible, the pin stops forwarding being forever. +/// +/// +public sealed unsafe partial class Reactor +{ + /// + /// Ceiling on pinned sockets held at once, because each costs a file descriptor and ngtcp2 + /// adopts a new path BEFORE it finishes validating it - so a forged datagram can reach here. + /// Past the ceiling a migrated connection simply keeps forwarding, which is slower and still + /// correct. That is the right way round: the cheap failure is the safe one. + /// + private const int QuicMaxPinnedPeers = 512; + + private int _quicServingFd = -1; // the wildcard QUIC socket; pins are only made against it + private int _quicPinsOpen; + private long _quicPinsCreated; + + /// Peers currently claimed by a socket of this reactor's own. + public int QuicPinsOpen => Volatile.Read(ref _quicPinsOpen); + + /// Pins this reactor has opened since it started - one per migration it kept up with. + public long QuicPinsCreated => Volatile.Read(ref _quicPinsCreated); + + /// + /// Claim 's current peer address, replacing any earlier claim. Called + /// from the reactor's sweep, NOT from the engine's path-change report: a path is reported many + /// times while ngtcp2 validates it, alternating between the old address and the one being + /// probed, and claiming each report churns sockets and drops queued datagrams. By the next + /// sweep the address has settled, and a repeat of one already claimed costs nothing. Reactor thread only, and quietly does + /// nothing whenever the claim would be pointless or impossible - a connection that never moved, + /// a single-reactor server, a client-side connection on its own socket, or the ceiling reached. + /// + internal void QuicPinPeer(QuicConnection conn) + { + // Not under KernelFilter: the kernel is already routing by connection id there, so a claim + // would be a descriptor spent on nothing. + if (_quicOptions is not { PinMigratedPeers: true, Routing: QuicRouting.Forward } options || + !conn.PeerAddressMoved || + ShardCount <= 1 || + _quicServingFd < 0 || + conn.SocketFd != _quicServingFd || + conn.PeerAddr == 0 || conn.PeerAddrLen <= 0) + { + return; + } + + // ngtcp2 reports a path more than once while it probes, and each report used to tear the + // claim down and build a new one - closing a socket with datagrams already queued on it, + // which the peer then had to retransmit. Losing packets to "optimise" delivery is the wrong + // way round, so a repeat of the SAME address is left alone. + ReadOnlySpan current = new((void*)conn.PeerAddr, conn.PeerAddrLen); + if (conn.PinSlot >= 0 && + conn.PinnedAddrLen == conn.PeerAddrLen && + current.SequenceEqual(conn.PinnedAddr.AsSpan(0, conn.PinnedAddrLen))) + { + return; + } + + // A genuinely new address: the old claim names somewhere the peer no longer is, so it goes. + QuicUnpinPeer(conn); + + if (_quicPinsOpen >= QuicMaxPinnedPeers) + { + return; // keep forwarding: slower, correct, and bounded + } + + // Never let a failed claim escape. This runs inside ngtcp2's path-change callback, where an + // exception is caught and recorded as a connection fault - so a transient bind failure would + // kill a connection that is working perfectly well, to skip an optimisation. Not claiming + // costs a hop per datagram; faulting costs the connection. + int fd; + try + { + fd = OpenUdpSocket(options.Port, _config.DualStack, _udp.Gro, _udp.SocketBufferBytes, + conn.PeerAddr, conn.PeerAddrLen); + } + catch (InvalidOperationException) + { + return; // the port could not be bound again: forwarding continues, which is correct + } + + if (fd < 0) + { + return; // the address is already claimed, or the kernel refused: forwarding continues + } + + conn.PinSlot = UdpAdoptSocket(fd, options.Port); + + // Remember WHAT was claimed, so the repeat reports above can be recognised. Without this + // the comparison never matches and every report rebuilds a working claim. + current.CopyTo(conn.PinnedAddr); + conn.PinnedAddrLen = conn.PeerAddrLen; + + _quicPinsOpen++; + _quicPinsCreated++; + } + + /// + /// Drop this connection's claim, if it holds one. Called on teardown and before re-claiming. + /// The slot is not reusable yet - it becomes so once the kernel's last completion for it + /// arrives, which is what stops a stale completion being read as a new socket's traffic. + /// + internal void QuicUnpinPeer(QuicConnection conn) + { + int slot = conn.PinSlot; + if (slot < 0) + { + return; + } + + conn.PinSlot = -1; + conn.PinnedAddrLen = 0; + + if ((uint)slot >= (uint)_udpFds.Length || _udpFds[slot] < 0) + { + return; + } + + int fd = _udpFds[slot]; + _udpFds[slot] = -1; // marks it released BEFORE the close, so no re-arm can race the fd away + close(fd); + _quicPinsOpen--; + } + + /// + /// Put an already-open socket into the fd table and arm it on this ring, reusing a slot left by + /// a released pin when one has finished draining. Returns its index. + /// + private int UdpAdoptSocket(int fd, ushort port) + { + int index; + if (_udpFreeSlots.Count > 0) + { + index = _udpFreeSlots.Pop(); + _udpFds[index] = fd; + _udpFdPorts[index] = port; + } + else + { + // Append. Indices stay stable - recv completions carry theirs in user_data - so growing + // the tables cannot disturb the multishots already armed on the existing sockets. + index = _udpFds.Length; + _udpFds = [.. _udpFds, fd]; + _udpFdPorts = [.. _udpFdPorts, port]; + } + + ArmUdpRecv(index); + return index; + } +} diff --git a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs index e46544b0..8afe7012 100644 --- a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs +++ b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs @@ -264,6 +264,8 @@ public void QuicRemoveConnection(QuicConnection conn) // (engine close racing the idle sweep) cannot double-release. if (_quicConnSet.Remove(conn)) { + QuicUnpinPeer(conn); // give the descriptor back before the address it names is freed + // Wake the handler with closed=1 first - it resumes inline, sees IsClosed, and releases // its own ref - then invalidate any awaiter that could outlive this life. conn.MarkClosed(); @@ -295,7 +297,13 @@ private void QuicSweep() { QuicRemoveConnection(conn); conn.OnEvicted(QuicEvictReason.IdleTimeout); + continue; } + + // A connection whose address has moved gets its new one claimed, so its datagrams stop + // being forwarded. Done here rather than when the move is reported because a path is + // reported many times while ngtcp2 validates it; by the next sweep it has settled. + QuicPinPeer(conn); } } diff --git a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs index 0c40ed96..dd2bcf6b 100644 --- a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs +++ b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs @@ -57,6 +57,12 @@ public sealed unsafe partial class Reactor private const int ENOBUFS_UDP = 105; private int[] _udpFds = []; + + // Slots whose socket was closed (a released pin) and whose multishot has finished draining, so + // the index can be handed out again. A slot is only recycled after the kernel's last completion + // for it: reusing one earlier would let a stale completion be read as the new socket's traffic. + // See Reactor.Quic.Pin.cs. + private readonly Stack _udpFreeSlots = new(); private ushort[] _udpFdPorts = []; // Shared provided-buffer ring for all UDP sockets (one registration, one bgid). @@ -118,7 +124,9 @@ private void OpenUdpSockets() if (_config.Quic is { } configured && port == configured.Port) { - quicFd = _udpFds[i]; + // Also remembered for the life of the reactor: a pinned socket is only ever + // made against the wildcard QUIC socket, never a client's own ephemeral one. + quicFd = _quicServingFd = _udpFds[i]; } } } @@ -284,7 +292,8 @@ private void ReturnUdpBuffer(ushort bid) Volatile.Write(ref *(ushort*)(_udpBufRing + 14), _udpBufRingTail); } - private static int OpenUdpSocket(ushort port, bool dualStack, bool gro, int socketBufferBytes) + private static int OpenUdpSocket(ushort port, bool dualStack, bool gro, int socketBufferBytes, + nint connectTo = 0, int connectLen = 0) { // Refused rather than clamped: a zero or negative request would leave the socket on the // kernel minimum, which looks identical to the clamp above and would be read as one. @@ -345,6 +354,17 @@ private static int OpenUdpSocket(ushort port, bool dualStack, bool gro, int sock } } + // A pinned socket: bound to the same address as its siblings, but naming ONE peer. That + // makes it a more specific match than the wildcard binds, so the kernel delivers that + // peer's datagrams here without consulting the reuseport hash at all - and, measured, + // without disturbing which socket any other peer lands on. Purely local: connect() on a + // datagram socket sends nothing. + if (connectTo != 0 && connect(fd, (void*)connectTo, (uint)connectLen) < 0) + { + close(fd); + return -1; + } + return fd; } @@ -352,6 +372,11 @@ private static int OpenUdpSocket(ushort port, bool dualStack, bool gro, int sock // then delivers a CQE per datagram, each selecting a ring buffer, until the multishot terminates. private void ArmUdpRecv(int socketIndex) { + if (_udpFds[socketIndex] < 0) + { + return; // slot released; nothing to arm and nothing to re-arm onto + } + IoUringSqe* sqe = GetSqeOrFlush(); Unsafe.InitBlockUnaligned(sqe, 0, 64); sqe->opcode = IORING_OP_RECVMSG; @@ -373,6 +398,22 @@ private void OnUdpRecvCompletion(int socketIndex, int res, uint flags) bool more = (flags & IORING_CQE_F_MORE) != 0; + if (_udpFds[socketIndex] < 0) + { + // The socket was closed while this was in flight (a pin released). Hand back any buffer + // the kernel already selected, and take the terminating completion as the signal that + // nothing more will reference this slot. + if ((flags & IORING_CQE_F_BUFFER) != 0) + { + ReturnUdpBuffer((ushort)(flags >> IORING_CQE_BUFFER_SHIFT)); + } + if (!more) + { + _udpFreeSlots.Push(socketIndex); + } + return; + } + if (res < 0) { // -ENOBUFS: the ring momentarily drained (a burst outran the depth). Buffers return @@ -583,7 +624,10 @@ private void CloseUdpFds() { foreach (int fd in _udpFds) { - close(fd); + if (fd >= 0) + { + close(fd); + } } } diff --git a/tests/Ioxide.Tests.E2E/Protocols/QuicMigrationTests.cs b/tests/Ioxide.Tests.E2E/Protocols/QuicMigrationTests.cs index d710b218..3e4aad7d 100644 --- a/tests/Ioxide.Tests.E2E/Protocols/QuicMigrationTests.cs +++ b/tests/Ioxide.Tests.E2E/Protocols/QuicMigrationTests.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Sockets; using ioxide; +using ioxide.http3; using ioxide.nghttp3; using ioxide.ngtcp2; @@ -146,6 +147,83 @@ long Dropped() "the server never sent anything to the client's new address, so nothing migrated"); }); + runner.Test("quic/migration: claiming the new address stops the forwarding", () => + { + // Forwarding alone is correct but permanent: the address does not change back, so every + // later datagram keeps landing on the wrong reactor and keeps paying a hop. The owning + // reactor claims the new address with a connected socket, which the kernel prefers over + // the wildcard binds, and delivery comes straight to it. + // + // What this asserts is that the forwarding STOPS - not that a claim was made, which + // would be satisfied by a claim that never receives anything. + (string certPath, string keyPath) = TestCert.Ensure(); + using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]); + + (int serverPort, Reactor[] fleet) = TestServer.StartQuicSharded(4, + engine.CreateFactory(), + quicHandle: static (_, conn) => new Nghttp3Connection(conn).RunBufferedAsync( + static _ => Nghttp3Response.Text("migrated-ok")), + routing: QuicRouting.Forward); + + long Forwarded() + { + long n = 0; + foreach (Reactor reactor in fleet) { n += reactor.QuicForwardsSent; } + return n; + } + + long Pins() + { + long n = 0; + foreach (Reactor reactor in fleet) { n += reactor.QuicPinsCreated; } + return n; + } + + using var forwarder = new UdpForwarder(serverPort); + using var client = new H3TestClient("127.0.0.1", forwarder.Port); + + client.Connect(); + Assert.True(client.CompleteHandshake(10_000), "the handshake through the forwarder did not complete"); + Assert.Equal(200, client.Request("GET", "/before", null, 10_000).Status); + + // Move until the datagrams genuinely land on another reactor, which is what makes a + // claim necessary at all. + int swaps = 0; + while (Forwarded() == 0 && swaps < 8) + { + swaps++; + forwarder.SwapUpstream(); + Assert.Equal(200, client.Request("GET", $"/after-{swaps}", null, 15_000).Status); + } + + Assert.True(Forwarded() > 0, + $"after {swaps} address changes nothing was forwarded, so no claim was needed and " + + "this test proved nothing"); + + // ngtcp2 only reports the new path once it has probed it, so give the claim a request + // to be made in, then measure from there. + Assert.Equal(200, client.Request("GET", "/settle", null, 15_000).Status); + Assert.True(Pins() > 0, "the new address was never claimed"); + + long settled = Forwarded(); + for (int i = 0; i < 5; i++) + { + Assert.Equal(200, client.Request("GET", $"/steady-{i}", null, 15_000).Status); + } + + Assert.Equal(settled, Forwarded()); + + // One claim per address, not one per report. ngtcp2 announces a path several times + // while it probes, and rebuilding the claim on each announcement closes a socket with + // datagrams already queued on it - losing packets in order to avoid a hop, which is + // the wrong trade. There were at most `swaps` distinct addresses, plus one for the + // address the connection started on. + Console.Error.WriteLine($"pins created: {Pins()} across {swaps} address change(s)"); + Assert.True(Pins() <= swaps + 1, + $"{Pins()} claims for {swaps} address change(s): the same address is being " + + "re-claimed on every path report"); + }); + runner.Test("quic/migration: kernel steering delivers a migrated client without any forwarding", () => { // The other half of QuicRouting. Under Forward the datagrams arrive at the wrong @@ -194,6 +272,130 @@ long Dropped() Assert.Equal(0L, forwarded); }); + // The claim every other test here leans on and none of them checks: that the connection + // SURVIVED. A client that quietly re-handshakes after its address changes also answers 200 + // to everything, so status codes cannot tell migration from reconnection - and if it were + // reconnecting, the h3 session, the QPACK tables and any application state would be gone + // while the tests stayed green. + // + // So the handler names the connection object serving each request, and the test asserts the + // name did not change. One connection, one accept, across an address change. + foreach ((string stack, Func handler) in + new (string, Func)[] + { + ("nghttp3", static (_, conn) => new Nghttp3Connection(conn).RunBufferedAsync( + _ => Nghttp3Response.Text($"conn-{conn.GetHashCode():x8}"))), + ("pure-c#", static (_, conn) => new Http3Connection(conn).RunAsync( + _ => Http3Response.Text($"conn-{conn.GetHashCode():x8}"))), + }) + { + string name = stack; + Func h3 = handler; + + runner.Test($"quic/migration: {name} - the SAME connection serves after the address changes", () => + { + (string certPath, string keyPath) = TestCert.Ensure(); + using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]); + + int accepts = 0; + (int serverPort, Reactor[] fleet) = TestServer.StartQuicSharded(4, + engine.CreateFactory(), + quicHandle: (r, conn) => + { + Interlocked.Increment(ref accepts); + return h3(r, conn); + }, + routing: QuicRouting.Forward); + + long Forwarded() + { + long n = 0; + foreach (Reactor reactor in fleet) { n += reactor.QuicForwardsSent; } + return n; + } + + using var forwarder = new UdpForwarder(serverPort); + using var client = new H3TestClient("127.0.0.1", forwarder.Port); + + client.Connect(); + Assert.True(client.CompleteHandshake(10_000), "the handshake through the forwarder did not complete"); + + (int beforeStatus, string served) = client.Request("GET", "/before", null, 10_000); + Assert.Equal(200, beforeStatus); + Assert.True(served.StartsWith("conn-"), + $"expected the serving connection to name itself, got '{served}'"); + + // Keep moving until the datagrams genuinely reach a reactor that does not own the + // connection - otherwise the kernel may have re-hashed back to the owner and + // nothing about routing was exercised. + int swaps = 0; + while (Forwarded() == 0 && swaps < 8) + { + swaps++; + forwarder.SwapUpstream(); + + (int afterStatus, string afterServed) = client.Request("GET", $"/after-{swaps}", null, 15_000); + + Assert.Equal(200, afterStatus); + Assert.Equal(served, afterServed); + } + + Assert.True(Forwarded() > 0, + $"after {swaps} address changes nothing was ever forwarded, so the connection " + + "never actually moved between reactors and this proves nothing"); + + // And it is still the same connection several requests later, on the new address. + for (int i = 0; i < 3; i++) + { + (int status, string owner) = client.Request("GET", $"/settled-{i}", null, 15_000); + Assert.Equal(200, status); + Assert.Equal(served, owner); + } + + // The discriminator against a client that re-handshaked: a reconnect would run the + // factory again. Migration must not. + Assert.Equal(1, Volatile.Read(ref accepts)); + }); + } + + runner.Test("control: a fleet whose clients never move claims no addresses", () => + { + // The cost side of the claim. It is made from the sweep, which visits every connection + // every 250 ms, so a guard that only looks at "is this address claimed yet" would claim + // one for every connection on the server whether or not it had ever moved - a + // descriptor and an armed receive apiece, to change nothing. Only a connection whose + // address actually moved is worth claiming. + (string certPath, string keyPath) = TestCert.Ensure(); + using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]); + + (int serverPort, Reactor[] fleet) = TestServer.StartQuicSharded(4, + engine.CreateFactory(), + quicHandle: static (_, conn) => new Nghttp3Connection(conn).RunBufferedAsync( + static _ => Nghttp3Response.Text("ok")), + routing: QuicRouting.Forward); + + using var client = new H3TestClient("127.0.0.1", serverPort); + client.Connect(); + Assert.True(client.CompleteHandshake(10_000), "handshake did not complete"); + + // Long enough to cross several sweeps, which is when a claim would be made. + for (int i = 0; i < 6; i++) + { + Assert.Equal(200, client.Request("GET", $"/r{i}", null, 15_000).Status); + } + + long pins = 0; + long forwards = 0; + foreach (Reactor reactor in fleet) + { + pins += reactor.QuicPinsCreated; + forwards += reactor.QuicForwardsSent; + } + + Assert.Equal(0L, pins); + Assert.Equal(0L, forwards); + }); + runner.Test("control: the same exchange through a forwarder that never swaps", () => { // Without this, the test above is satisfied by a forwarder that works and a migration From 2e5101bbe961f38fa80a06e786acd205474fe19c Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Thu, 20 Aug 2026 18:59:23 +0100 Subject: [PATCH 10/11] chore: trim the comments on the QUIC routing work Roughly half the prose added across #205 and the claim work said the same thing twice, or explained the investigation rather than the code. Kept: why the packet moves instead of the connection, why the copy is not optional, why the claim runs from the sweep, and the classic-BPF listing, which is unreadable without it. One comment was also wrong - it said the claim runs inside ngtcp2's path-change callback, which stopped being true when it moved to the sweep. No behaviour change; 436 tests pass. --- src/ioxide/Connection/Quic/QuicConnection.cs | 23 +--- .../Reactor/Transport/Quic/QuicOptions.cs | 10 +- .../Reactor/Transport/Quic/QuicRouting.cs | 46 +++---- .../Transport/Quic/Reactor.Quic.Forward.cs | 81 +++++------- .../Transport/Quic/Reactor.Quic.Pin.cs | 79 ++++-------- .../Reactor/Transport/Quic/Reactor.Quic.cs | 5 +- .../Transport/Udp/Reactor.Udp.Steering.cs | 118 ++++++------------ .../Reactor/Transport/Udp/Reactor.Udp.cs | 23 ++-- .../Protocols/QuicMigrationTests.cs | 60 ++++----- tests/Ioxide.Tests.Harness/H3TestClient.cs | 30 ++--- tests/Ioxide.Tests.Harness/TestCert.cs | 21 ++-- 11 files changed, 175 insertions(+), 321 deletions(-) diff --git a/src/ioxide/Connection/Quic/QuicConnection.cs b/src/ioxide/Connection/Quic/QuicConnection.cs index a4e0a978..af699d8f 100644 --- a/src/ioxide/Connection/Quic/QuicConnection.cs +++ b/src/ioxide/Connection/Quic/QuicConnection.cs @@ -143,24 +143,16 @@ protected void Send(ReadOnlySpan payload, int gsoSegmentSize = 0) Reactor.UdpSendTo(SocketFd, PeerAddr, PeerAddrLen, payload, gsoSegmentSize); } - /// - /// Index in the reactor's fd table of the socket claiming this connection's peer address, or -1 - /// when it holds none. Owned by the reactor; see Reactor.Quic.Pin.cs. - /// + /// fd-table index of the socket claiming this peer, or -1. See Reactor.Quic.Pin.cs. internal int PinSlot = -1; - /// - /// The address the current claim names, so a repeated report of the SAME path does not tear a - /// working claim down and build it again. Owned by the reactor. - /// + /// What the claim names, so a repeat of the same path does not rebuild it. internal readonly byte[] PinnedAddr = new byte[Reactor.UdpNameCap]; internal int PinnedAddrLen; /// - /// Set once this connection's peer address has actually moved. Only a connection that migrated - /// is worth claiming an address for: one still at the address it was accepted on is already - /// being delivered here by the kernel's hash, so a claim would spend a descriptor to change - /// nothing. Owned by the reactor. + /// Set once the peer address has moved. Only a migrated connection is worth claiming: one still + /// where it was accepted is already delivered here by the hash. /// internal bool PeerAddressMoved; @@ -180,11 +172,8 @@ public unsafe void UpdatePeerAddress(nint addr, int addrLen) PeerAddrLen = addrLen; PeerAddressMoved = true; - // Deliberately NOT claiming the address here. ngtcp2 reports a path repeatedly while it - // validates one - alternating between the old path for data and the new one for - // PATH_CHALLENGE probes - so a claim made on each report churns sockets between two - // addresses and drops datagrams already queued on the one it closes. The reactor's sweep - // picks this up once the address has stopped moving. See Reactor.Quic.Pin.cs. + // The address is NOT claimed here: ngtcp2 reports a path repeatedly while validating it, + // so claiming per report churns sockets. The sweep does it once the address settles. } // --- read surface: engine enqueues on the reactor thread, the handler awaits from anywhere. diff --git a/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs b/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs index be204458..d49bfacc 100644 --- a/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs +++ b/src/ioxide/Reactor/Transport/Quic/QuicOptions.cs @@ -11,16 +11,14 @@ public sealed record QuicOptions public int LocalCidLength { get; init; } = 8; /// - /// How a datagram reaches the reactor that owns its connection when several reactors share the - /// port. Defaults to , which costs nothing until a client - /// changes address; see for the measured trade. + /// How a datagram reaches the reactor owning its connection when several share the port. + /// See for the measured trade. /// public QuicRouting Routing { get; init; } = QuicRouting.Forward; /// - /// Under , claim a migrated client's new address with a socket - /// of the owning reactor's own, so the kernel delivers there directly and the forwarding stops - /// after the first datagram or two. Costs one file descriptor per migrated connection. + /// Under , claim a migrated client's new address so the kernel + /// delivers to its owner directly and forwarding stops. One descriptor per migrated connection. /// public bool PinMigratedPeers { get; init; } = true; diff --git a/src/ioxide/Reactor/Transport/Quic/QuicRouting.cs b/src/ioxide/Reactor/Transport/Quic/QuicRouting.cs index 8dcbbb7f..7c87fc40 100644 --- a/src/ioxide/Reactor/Transport/Quic/QuicRouting.cs +++ b/src/ioxide/Reactor/Transport/Quic/QuicRouting.cs @@ -1,48 +1,38 @@ namespace ioxide; /// -/// How a QUIC datagram reaches the reactor that owns its connection, when the fleet has more than -/// one. +/// How a QUIC datagram reaches the reactor that owns its connection when there is more than one. +/// Both settings solve the same problem: the kernel picks a reactor by hashing the sender's +/// address, which stops being right the moment that address changes. See Reactor.Quic.Forward.cs. /// -/// The problem both settings solve: every reactor binds the QUIC port with SO_REUSEPORT and the -/// kernel chooses between them by hashing the sender's address, which stops being the right answer -/// the moment a client's address changes. See Reactor.Quic.Forward.cs. -/// -/// Measured on one machine with two reactors and the h3 benchmark, so treat the magnitudes as -/// indicative and the shape as real: +/// The choice is who pays. Measured on one machine, two reactors, h3 benchmark - treat the +/// magnitudes as indicative: /// /// -/// settingconnections that never migrate / that do -/// no cost at all / about 8.5 us per datagram -/// free with CPU headroom, about -12% throughput at saturation / no cost +/// settingnever migrates / migrates +/// nothing / ~8.5 us per datagram +/// free with headroom, ~-12% at saturation / nothing /// /// -/// So the choice is who pays. charges only the connections that actually -/// migrate, and charges them a cross-thread wake per datagram. charges -/// every connection a little kernel work per packet - invisible while there is CPU to spare, and -/// real once there is not - and charges migrating ones nothing. Unless a large share of clients -/// migrate, is cheaper in aggregate, which is why it is the default. +/// Unless a large share of clients migrate, is cheaper in aggregate, which is +/// why it is the default. /// public enum QuicRouting { /// - /// Let the kernel hash as it does today, and hand a misdirected datagram to its owner over the - /// reactor post queue. Costs nothing until a client actually moves, needs no privileges, and - /// leaves reactor startup exactly as it is. + /// Hash as before, and hand a misdirected datagram to its owner over the reactor post queue. + /// No privileges, and reactor startup is unchanged. /// Forward = 0, /// - /// Additionally attach a classic-BPF program to the port's SO_REUSEPORT group so the kernel - /// routes by connection id rather than by address, and a migrated client's datagrams arrive at - /// their owner directly. + /// Additionally attach a classic-BPF program to the SO_REUSEPORT group, so the kernel routes by + /// connection id and a migrated client's datagrams arrive at their owner directly. /// - /// Two consequences worth knowing. Reactors must then open their UDP sockets in ShardIndex - /// order, because the program answers with a position in the reuseport group and that position - /// is bind order - a startup-only rendezvous that does not exist otherwise. And the filter is - /// best-effort: if the kernel refuses it (an old kernel, a seccomp policy, a restricted - /// container) ioxide says so and carries on, with still underneath as the - /// backstop. Correctness never depends on the filter; only the cost does. + /// Reactors must then open their UDP sockets in ShardIndex order, since the program answers + /// with a position in the group and that is bind order - a startup-only rendezvous. Best-effort: + /// if the kernel refuses the program, remains underneath. Correctness + /// never depends on the filter, only cost does. /// KernelFilter = 1, } diff --git a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Forward.cs b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Forward.cs index 699714ed..a1177e3b 100644 --- a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Forward.cs +++ b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Forward.cs @@ -5,49 +5,38 @@ namespace ioxide; /// -/// Cross-reactor delivery for QUIC: when a datagram lands on a reactor that does not own the -/// connection it names, hand it to the one that does. +/// Cross-reactor delivery for QUIC: a datagram that lands on a reactor which does not own the +/// connection it names is handed to the one that does. Issue #205. /// -/// Why it is needed. Every reactor binds the QUIC port with SO_REUSEPORT, and the kernel picks -/// between them by hashing the sender's address. That is stable only while the address is - so a -/// NAT rebind, or a client moving network, re-hashes to a different reactor, one that has never -/// heard of the connection. Its short-header packets are then dropped and the connection dies -/// unreachable on the reactor that could still serve it. Issue #205. +/// Every reactor binds the QUIC port with SO_REUSEPORT and the kernel chooses between them by +/// hashing the sender's address, which stops being the right answer the moment that address changes +/// - a NAT rebind re-hashes to a reactor that has never heard of the connection, and its +/// short-header packets used to be dropped. /// -/// Why this direction rather than moving the connection. The ngtcp2 conn, the picotls session and -/// the open streams are native state owned by one reactor thread, and QuicConnection is -/// reactor-thread-only throughout. Moving live state to the reactor the packet happened to land on -/// is what shared-nothing forbids; moving the PACKET to the state it belongs to is ordinary message -/// passing, which is how the model is meant to work - and it rides -/// , which reactors already expose for exactly this. +/// The connection cannot move to meet the packet: the ngtcp2 conn, the picotls session and the +/// streams are owned by one reactor thread. So the packet moves instead, over +/// - message passing, not shared state. /// -/// What crosses a thread boundary is therefore a COPY of the bytes, never a reference to reactor -/// state. That copy is not incidental: the datagram lives in the receiving reactor's io_uring -/// provided-buffer ring, which is returned the moment the dispatch call returns, so handing the -/// owner a pointer into it would be a use-after-free under load. +/// What crosses is a COPY of the bytes. The datagram lives in the receiving reactor's +/// provided-buffer ring, handed back the moment dispatch returns, so passing a pointer into it +/// would be a use-after-free under load. /// -/// Only short-header packets are forwarded. A short header means the handshake is done, so its -/// destination id is one this server minted and its first byte really does name the owner (see -/// iq_stamp_shard in the ngtcp2 shim). A long header carries a connection id the CLIENT chose, and -/// routing on a byte the peer controls would let anyone aim traffic at a reactor of their choosing. +/// Only short headers are forwarded: their id is one this server minted, so its first byte really +/// does name the owner (iq_stamp_shard in the shim). A long header's id is chosen by the CLIENT, +/// and routing on a byte the peer controls would let anyone aim traffic at a reactor. /// public sealed unsafe partial class Reactor { /// - /// Datagrams that may be awaiting delivery to any one reactor before further ones are dropped. - /// Dropping is safe here in a way it rarely is: QUIC treats a lost packet as loss and resends, - /// so the ceiling costs a retransmit rather than a connection. Without it, a reactor that - /// stalls would let its siblings queue for it without limit. + /// Datagrams that may be in flight toward one reactor before further ones are dropped. Dropping + /// is safe here: QUIC resends, so the ceiling costs a retransmit rather than a connection. /// private const int QuicForwardMaxOutstanding = 1024; /// - /// The reactors sharing one ServerConfig, so a datagram can be handed to the one that owns it. - /// - /// Keyed on the config INSTANCE rather than held in a static, because a process routinely runs - /// several independent servers at once - every test suite here does - and they must not be able - /// to see, or post into, each other's reactors. ConditionalWeakTable keys by reference identity - /// (not the record's value equality) and holds the fleet no longer than the config itself. + /// The reactors sharing one ServerConfig. Keyed on the config INSTANCE, not a static, because a + /// process can run several independent servers and they must not post into each other. + /// ConditionalWeakTable keys by reference identity, not the record's value equality. /// private static readonly ConditionalWeakTable QuicFleets = new(); @@ -65,9 +54,8 @@ public QuicFleet(int count) } /// - /// One datagram in transit between reactors. Pooled, because a migrated connection forwards - /// every packet until the peer settles, and an envelope per datagram would be a steady stream - /// of garbage on the hot path. + /// One datagram in transit between reactors. Pooled: an envelope per datagram would be a steady + /// stream of garbage on the hot path. /// private sealed class QuicForward { @@ -96,9 +84,8 @@ private sealed class QuicForward public long QuicForwardsReceived => Volatile.Read(ref _quicForwardsReceived); /// - /// Datagrams that could not be forwarded and were dropped - the owner's queue was at - /// , or it had stopped. Nonzero means a reactor is not - /// keeping up; the peers will retransmit. + /// Datagrams dropped rather than forwarded - the owner was at + /// or had stopped. Nonzero means a reactor is behind. /// public long QuicForwardsDropped => Volatile.Read(ref _quicForwardsDropped); @@ -116,9 +103,8 @@ private void QuicJoinFleet() } /// - /// Hand a datagram to the reactor that owns its connection. Returns false when this reactor is - /// the owner (so the id is simply unknown - stale, retired, or hostile) or when there is - /// nowhere to send it, in which case the caller drops as before. + /// Hand a datagram to the reactor that owns its connection. False when this reactor IS the + /// owner (so the id is merely unknown - stale, retired or hostile) and the caller should drop. /// private bool QuicTryForward(in UdpDatagram datagram, in QuicCid dcid) { @@ -131,17 +117,17 @@ private bool QuicTryForward(in UdpDatagram datagram, in QuicCid dcid) int owner = dcid.FirstByte % fleet.Members.Length; if (owner == _id) { - return false; // addressed here, and we do not have it: not a routing problem + return false; // addressed here and we do not have it: not a routing problem } Reactor? target = Volatile.Read(ref fleet.Members[owner]); if (target is null || target._stopRequested || target._wakeFd <= 0) { _quicForwardsDropped++; - return true; // handled: there is nothing better to do with it than drop it + return true; // nothing better to do with it than drop it } - // Reserve a slot before copying, so a stalled owner cannot make its siblings do the work. + // Reserved before the copy, so a stalled owner cannot make its siblings do the work. if (Interlocked.Increment(ref fleet.Outstanding[owner]) > QuicForwardMaxOutstanding) { Interlocked.Decrement(ref fleet.Outstanding[owner]); @@ -164,8 +150,8 @@ private bool QuicTryForward(in UdpDatagram datagram, in QuicCid dcid) forward.Payload = ArrayPool.Shared.Rent(length); } - // The copy that makes this safe. Payload points into the recv slot, which goes back to the - // provided-buffer ring as soon as this dispatch returns. + // The copy that makes this safe: Payload points into the recv slot, which returns to the + // provided-buffer ring as soon as dispatch does. datagram.Payload.CopyTo(forward.Payload); forward.Length = length; @@ -182,7 +168,7 @@ private bool QuicTryForward(in UdpDatagram datagram, in QuicCid dcid) _quicForwardsSent++; - // Static lambda over the envelope alone: no closure, no per-datagram allocation. + // Static lambda over the envelope alone - no closure, no per-datagram allocation. target.ScheduleOnReactor( static state => { @@ -206,8 +192,7 @@ private void QuicReceiveForward(QuicForward forward) fixed (byte* payload = forward.Payload) fixed (byte* addr = forward.PeerAddr) { - // GRO segment size 0: trains are split before routing, so a forwarded datagram is - // always a single one. + // GRO size 0: trains are split before routing, so this is always one datagram. QuicDispatchDatagram(new UdpDatagram(forward.SocketFd, forward.LocalPort, (nint)addr, forward.PeerAddrLen, new ReadOnlySpan(payload, forward.Length), 0, forward.Tos)); diff --git a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Pin.cs b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Pin.cs index 08c494c4..d73f40da 100644 --- a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Pin.cs +++ b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.Pin.cs @@ -3,40 +3,21 @@ namespace ioxide; /// -/// The optimisation on top of cross-reactor forwarding: once a migrated client's new address is -/// known, claim it with a socket of this reactor's own so the kernel delivers there directly and -/// the forwarding stops. +/// Ends the cross-reactor forwarding a migrated client would otherwise pay forever: the owning +/// reactor claims the peer's new address with a socket connect()ed to it, which outranks the +/// wildcard binds in the kernel's lookup, so the datagrams arrive here directly. /// -/// Forwarding alone is correct but permanent. The kernel picks a socket by hashing the sender's -/// address, and after a migration that address does not change back - so every one of that -/// client's datagrams keeps landing on the wrong reactor and keeps paying a hop, for the life of -/// the connection. See Reactor.Quic.Forward.cs. +/// connect() on a datagram socket sends nothing - it is a local declaration - and a connected +/// socket takes no part in reuseport selection, so no other peer moves (measured: eight, none did). /// -/// A datagram socket bound to the same address as the others but connect()ed to one peer is -/// a MORE SPECIFIC match than a wildcard bind, and the kernel's lookup takes the narrowest match -/// before it ever reaches the reuseport hash. So the owning reactor opens one of those toward the -/// peer's new address, and from the next datagram on it arrives here directly. -/// -/// Three things about that, each verified rather than assumed: -/// -/// -/// connect() on a datagram socket puts nothing on the wire. It is a local declaration -/// of intent - no handshake, and the peer is never told. -/// Adding it does NOT re-scatter anybody else. A connected socket does not take part in -/// reuseport selection, so the group stays the size it was and every other peer keeps landing -/// exactly where it did. Measured: eight established peers, none moved. -/// It cannot bootstrap itself. This reactor only learns the new address from a datagram, and -/// the datagrams are going elsewhere - so forwarding has to deliver the first one. The two are -/// complements: forwarding makes the pin possible, the pin stops forwarding being forever. -/// +/// It cannot bootstrap itself: this reactor only learns the new address from a datagram, and the +/// datagrams are going elsewhere. Forwarding delivers the first one. See Reactor.Quic.Forward.cs. /// public sealed unsafe partial class Reactor { /// - /// Ceiling on pinned sockets held at once, because each costs a file descriptor and ngtcp2 - /// adopts a new path BEFORE it finishes validating it - so a forged datagram can reach here. - /// Past the ceiling a migrated connection simply keeps forwarding, which is slower and still - /// correct. That is the right way round: the cheap failure is the safe one. + /// Ceiling on concurrent claims: each costs a descriptor, and ngtcp2 adopts a path before it + /// finishes validating it, so a forged datagram can reach here. Past it, forwarding continues. /// private const int QuicMaxPinnedPeers = 512; @@ -47,7 +28,7 @@ public sealed unsafe partial class Reactor /// Peers currently claimed by a socket of this reactor's own. public int QuicPinsOpen => Volatile.Read(ref _quicPinsOpen); - /// Pins this reactor has opened since it started - one per migration it kept up with. + /// Claims opened since startup - one per address a connection moved to. public long QuicPinsCreated => Volatile.Read(ref _quicPinsCreated); /// @@ -61,8 +42,7 @@ public sealed unsafe partial class Reactor /// internal void QuicPinPeer(QuicConnection conn) { - // Not under KernelFilter: the kernel is already routing by connection id there, so a claim - // would be a descriptor spent on nothing. + // Not under KernelFilter - the kernel already routes by connection id there. if (_quicOptions is not { PinMigratedPeers: true, Routing: QuicRouting.Forward } options || !conn.PeerAddressMoved || ShardCount <= 1 || @@ -73,10 +53,7 @@ internal void QuicPinPeer(QuicConnection conn) return; } - // ngtcp2 reports a path more than once while it probes, and each report used to tear the - // claim down and build a new one - closing a socket with datagrams already queued on it, - // which the peer then had to retransmit. Losing packets to "optimise" delivery is the wrong - // way round, so a repeat of the SAME address is left alone. + // Already claimed: leave it. Rebuilding would close a socket with datagrams queued on it. ReadOnlySpan current = new((void*)conn.PeerAddr, conn.PeerAddrLen); if (conn.PinSlot >= 0 && conn.PinnedAddrLen == conn.PeerAddrLen && @@ -85,18 +62,15 @@ internal void QuicPinPeer(QuicConnection conn) return; } - // A genuinely new address: the old claim names somewhere the peer no longer is, so it goes. - QuicUnpinPeer(conn); + QuicUnpinPeer(conn); // the old claim names somewhere the peer no longer is if (_quicPinsOpen >= QuicMaxPinnedPeers) { - return; // keep forwarding: slower, correct, and bounded + return; // keep forwarding: slower, correct, bounded } - // Never let a failed claim escape. This runs inside ngtcp2's path-change callback, where an - // exception is caught and recorded as a connection fault - so a transient bind failure would - // kill a connection that is working perfectly well, to skip an optimisation. Not claiming - // costs a hop per datagram; faulting costs the connection. + // A failed claim must never propagate: this runs from the sweep, where a throw would take + // the ticker down. Not claiming only costs a hop. int fd; try { @@ -105,18 +79,17 @@ internal void QuicPinPeer(QuicConnection conn) } catch (InvalidOperationException) { - return; // the port could not be bound again: forwarding continues, which is correct + return; // could not bind again: forwarding continues } if (fd < 0) { - return; // the address is already claimed, or the kernel refused: forwarding continues + return; // already claimed, or the kernel refused: forwarding continues } conn.PinSlot = UdpAdoptSocket(fd, options.Port); - // Remember WHAT was claimed, so the repeat reports above can be recognised. Without this - // the comparison never matches and every report rebuilds a working claim. + // What was claimed, so the check above can recognise a repeat. current.CopyTo(conn.PinnedAddr); conn.PinnedAddrLen = conn.PeerAddrLen; @@ -125,9 +98,8 @@ internal void QuicPinPeer(QuicConnection conn) } /// - /// Drop this connection's claim, if it holds one. Called on teardown and before re-claiming. - /// The slot is not reusable yet - it becomes so once the kernel's last completion for it - /// arrives, which is what stops a stale completion being read as a new socket's traffic. + /// Drop this connection's claim. The slot becomes reusable only once the kernel's last + /// completion for it arrives, so a stale completion is never read as a new socket's traffic. /// internal void QuicUnpinPeer(QuicConnection conn) { @@ -146,14 +118,13 @@ internal void QuicUnpinPeer(QuicConnection conn) } int fd = _udpFds[slot]; - _udpFds[slot] = -1; // marks it released BEFORE the close, so no re-arm can race the fd away + _udpFds[slot] = -1; // released before the close, so no re-arm can race the fd away close(fd); _quicPinsOpen--; } /// - /// Put an already-open socket into the fd table and arm it on this ring, reusing a slot left by - /// a released pin when one has finished draining. Returns its index. + /// Add an open socket to the fd table and arm it, reusing a drained slot when one is free. /// private int UdpAdoptSocket(int fd, ushort port) { @@ -166,8 +137,8 @@ private int UdpAdoptSocket(int fd, ushort port) } else { - // Append. Indices stay stable - recv completions carry theirs in user_data - so growing - // the tables cannot disturb the multishots already armed on the existing sockets. + // Indices stay stable (completions carry theirs in user_data), so growing the tables + // cannot disturb multishots already armed. index = _udpFds.Length; _udpFds = [.. _udpFds, fd]; _udpFdPorts = [.. _udpFdPorts, port]; diff --git a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs index 8afe7012..c095caa4 100644 --- a/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs +++ b/src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs @@ -300,9 +300,8 @@ private void QuicSweep() continue; } - // A connection whose address has moved gets its new one claimed, so its datagrams stop - // being forwarded. Done here rather than when the move is reported because a path is - // reported many times while ngtcp2 validates it; by the next sweep it has settled. + // Claim a moved connection's new address, so its datagrams stop being forwarded. Here + // rather than at the path report, which fires repeatedly while ngtcp2 validates. QuicPinPeer(conn); } } diff --git a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.Steering.cs b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.Steering.cs index 0b670274..2db1a8bb 100644 --- a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.Steering.cs +++ b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.Steering.cs @@ -4,38 +4,19 @@ namespace ioxide; /// -/// Connection-id steering for QUIC: a classic-BPF program attached to the QUIC port's -/// SO_REUSEPORT group so the kernel picks the reactor by reading the connection id out of -/// the datagram, instead of hashing the sender's address. +/// : a classic-BPF program on the QUIC port's +/// SO_REUSEPORT group, so the kernel picks the reactor by reading the connection id out of +/// the datagram rather than hashing the sender's address - which is what breaks when that address +/// changes. Every id this server mints carries its owner in the first byte (iq_stamp_shard in the +/// shim), chosen so cid[0] % ReactorCount is that reactor. /// -/// The default without it is the 4-tuple hash, which is correct only while a client's address -/// never changes. When it does - a NAT rebind, a phone moving from wifi to cellular, a deliberate -/// migration - the hash lands the datagram on a reactor that has never heard of that connection, -/// and a short-header packet for an unknown id is dropped. The connection stays alive and -/// unreachable on its own reactor until the idle sweep evicts it. Since the state that could serve -/// it (the ngtcp2 conn, the picotls session, the open streams) is native memory owned by one -/// reactor thread and documented reactor-thread-only, the fix has to move the DATAGRAM to the -/// state, never the state to the datagram. +/// Two consequences. The filter answers with a position in the reuseport group and that position +/// is BIND ORDER, so reactors must open the QUIC socket in order - what +/// arranges, at startup only. And it cannot be attached until +/// every reactor has joined, or an index can point past the end; the last one out attaches it. /// -/// Which is what the connection id is for. Every id this server mints carries its owning reactor -/// in the first byte (see iq_stamp_shard in the shim), chosen so cid[0] % ReactorCount is -/// exactly that reactor. The filter below recomputes it and returns it as the index into the -/// reuseport group. The id travels with the connection, so the routing survives whatever the -/// address does. -/// -/// Two things this depends on, both handled here: -/// -/// -/// The filter answers with a position in the reuseport group, and that position is -/// bind order. So the reactors have to open the QUIC socket in ShardIndex order, -/// which is what arranges. It is a startup-only cost. -/// The program must not be attached until every reactor has joined the group, or an index -/// can point past the end of it. The last reactor out attaches it. -/// -/// -/// If anything about that does not hold - the kernel refuses the program, a reactor fails to bind, -/// the fleet never assembles - steering is abandoned and the port keeps the 4-tuple hash. That is -/// today's behaviour, so the fallback is never worse than not having tried. +/// Any failure - the kernel refuses the program, a reactor fails to bind, the fleet never +/// assembles - abandons steering and leaves the 4-tuple hash, with forwarding still underneath. /// public sealed unsafe partial class Reactor { @@ -43,22 +24,16 @@ public sealed unsafe partial class Reactor private const int SO_ATTACH_REUSEPORT_CBPF = 51; /// - /// How long a reactor waits for its turn to open the QUIC socket before giving up on ordering. - /// Generous, because it only has to cover other reactor threads reaching the same point in - /// startup; if it is ever hit, something is wrong with the fleet rather than merely slow. + /// How long a reactor waits its turn before giving up on ordering. Generous: it only covers + /// other reactors reaching the same point in startup. /// private const int QuicSteeringTurnTimeoutMs = 10_000; - /// - /// The shard byte is one byte, so beyond 256 reactors it cannot encode the owner and steering - /// is not attempted. Nothing else changes; the port keeps the 4-tuple hash. - /// + /// Beyond 256 reactors one byte cannot encode the owner, so steering is skipped. private const int QuicSteeringMaxShards = 256; - // Keyed by the ServerConfig instance the fleet shares, so two servers in one process (which is - // the normal shape in the test suites) get independent gates and cannot wait on each other. - // ConditionalWeakTable keys on reference identity, not the record's value equality, which is - // what we want here - and it holds the gate no longer than the config itself. + // Keyed by the shared ServerConfig instance, so two servers in one process get independent + // gates. ConditionalWeakTable keys on reference identity, not the record's value equality. private static readonly ConditionalWeakTable QuicSteeringGates = new(); private sealed class QuicSteeringGate @@ -70,10 +45,7 @@ private sealed class QuicSteeringGate public bool Settled; // attach (or the decision not to) already happened } - /// - /// Whether this reactor should take part in ordered opening. QUIC only - a plain UDP server - /// keeps today's startup exactly - and only when there is a fleet to steer across. - /// + /// QUIC only, and only when there is a fleet to steer across. private bool _quicSteeringAttached; /// @@ -102,13 +74,11 @@ private sealed class QuicSteeringGate } /// - /// Block until it is this reactor's turn to open its sockets, so that group position equals - /// . + /// Block until it is this reactor's turn, so group position equals . /// - /// The timeout is what keeps a misconfigured fleet from becoming a hang: a caller that starts - /// fewer reactors than would otherwise leave everyone - /// after the missing one waiting forever. On expiry the gate is abandoned ONCE and every - /// waiter is released together, so the delay is paid a single time rather than per reactor. + /// The timeout stops a misconfigured fleet becoming a hang - starting fewer reactors than + /// would strand everyone after the missing one. On + /// expiry the gate is abandoned ONCE and all waiters released, so the delay is paid once. /// private void QuicSteeringAwaitTurn(QuicSteeringGate gate) { @@ -134,8 +104,7 @@ private void QuicSteeringAwaitTurn(QuicSteeringGate gate) } /// - /// Hand the turn to the next reactor, and - if this was the last one and every reactor bound - /// cleanly - attach the steering program now that the group is complete. + /// Hand the turn on, and if this was the last reactor and all bound cleanly, attach the program. /// /// The fleet's gate, or null when steering is not active. /// This reactor's QUIC socket, or -1 if opening it failed. @@ -150,8 +119,7 @@ private void QuicSteeringRelease(QuicSteeringGate? gate, int quicFd) { if (quicFd < 0) { - // This reactor never joined the group, so every later index is off by one. - gate.Abandoned = true; + gate.Abandoned = true; // never joined, so every later index is off by one } else if (gate.QuicFd < 0) { @@ -177,32 +145,29 @@ private void QuicSteeringRelease(QuicSteeringGate? gate, int quicFd) } /// - /// Attach the steering program to the QUIC reuseport group. Failure is reported and otherwise - /// ignored: an older kernel, a seccomp policy or a restricted container can all refuse it, and - /// the only consequence is that address changes go back to breaking connections. + /// Attach the program to the QUIC reuseport group. Failure is reported and otherwise ignored - + /// an old kernel, seccomp or a restricted container can refuse it, and forwarding still works. /// private void QuicAttachSteering(int fd, int shards) { // classic-BPF instruction: { u16 code; u8 jt; u8 jf; u32 k; } // - // 0 ld len A = datagram length - // 1 jge #9 ? next : ->8 too short to hold a connection id: fall through on the - // length itself, which is in bounds and deterministic - // 2 ldb [0] the QUIC first byte - // 3 and #0x80 its header-form bit - // 4 jeq #0 ? ->7 : next clear = short header - // 5 ldb [6] long header: first byte / 4 version / 1 dcid len, so DCID - // starts at 6. A client's Initial id is its own random value, - // which makes this a hash - and a stable one, so every packet - // of a handshake still reaches one reactor. + // 0 ld len A = datagram length + // 1 jge #9 ? : ->8 too short for a connection id: fall through on the length, + // which is in bounds and deterministic + // 2 ldb [0] QUIC first byte + // 3 and #0x80 header-form bit + // 4 jeq #0 ? ->7 : clear = short header + // 5 ldb [6] long header: 1 first byte + 4 version + 1 dcid len, so DCID is + // at 6. The client chose that id, so this acts as a stable hash - + // every packet of a handshake still reaches one reactor. // 6 ja ->8 - // 7 ldb [1] short header: DCID starts straight after the first byte. - // This is a connection id WE minted, so the byte is the shard. - // 8 mod #shards the reuseport index + // 7 ldb [1] short header: DCID at 1, an id WE minted, so the byte is ours + // 8 mod #shards the reuseport index // 9 ret a // - // Byte loads, not word loads, because iq_stamp_shard controls exactly one byte. Reading - // more would mix in bytes it does not constrain and the two sides would disagree. + // Byte loads, not word loads: iq_stamp_shard controls exactly one byte, and reading more + // would mix in bytes it does not constrain. (ushort code, byte jt, byte jf, uint k)[] program = [ (0x80, 0, 0, 0), // ld len @@ -228,7 +193,7 @@ private void QuicAttachSteering(int fd, int shards) } // struct sock_fprog { unsigned short len; struct sock_filter *filter; } - the pointer is - // 8-aligned, so the length sits in the first two bytes of a 16-byte struct. + // 8-aligned, so len sits in the first two bytes of 16. byte* fprog = stackalloc byte[16]; new Span(fprog, 16).Clear(); *(ushort*)fprog = (ushort)program.Length; @@ -237,9 +202,8 @@ private void QuicAttachSteering(int fd, int shards) if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, fprog, 16) < 0) { Console.Error.WriteLine( - $"[r{_id}] quic: could not attach connection-id steering; the port keeps the " - + "4-tuple hash and migrated clients fall back to cross-reactor forwarding, which " - + "is correct but costs a hop per datagram"); + $"[r{_id}] quic: could not attach connection-id steering; falling back to " + + "cross-reactor forwarding, which is correct but costs a hop per datagram"); return; } diff --git a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs index dd2bcf6b..1a7ef672 100644 --- a/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs +++ b/src/ioxide/Reactor/Transport/Udp/Reactor.Udp.cs @@ -58,10 +58,8 @@ public sealed unsafe partial class Reactor private int[] _udpFds = []; - // Slots whose socket was closed (a released pin) and whose multishot has finished draining, so - // the index can be handed out again. A slot is only recycled after the kernel's last completion - // for it: reusing one earlier would let a stale completion be read as the new socket's traffic. - // See Reactor.Quic.Pin.cs. + // Slots from released pins whose multishot has finished draining. Only recycled after the + // kernel's last completion, or a stale one would read as the new socket's traffic. private readonly Stack _udpFreeSlots = new(); private ushort[] _udpFdPorts = []; @@ -124,8 +122,8 @@ private void OpenUdpSockets() if (_config.Quic is { } configured && port == configured.Port) { - // Also remembered for the life of the reactor: a pinned socket is only ever - // made against the wildcard QUIC socket, never a client's own ephemeral one. + // Kept for the reactor's life: pins are only made against this socket, never + // a client's own ephemeral one. quicFd = _quicServingFd = _udpFds[i]; } } @@ -354,11 +352,9 @@ private static int OpenUdpSocket(ushort port, bool dualStack, bool gro, int sock } } - // A pinned socket: bound to the same address as its siblings, but naming ONE peer. That - // makes it a more specific match than the wildcard binds, so the kernel delivers that - // peer's datagrams here without consulting the reuseport hash at all - and, measured, - // without disturbing which socket any other peer lands on. Purely local: connect() on a - // datagram socket sends nothing. + // A pinned socket: same address as its siblings but naming ONE peer, so it outranks the + // wildcard binds and the reuseport hash is never consulted for that peer. Measured: no + // other peer moves. connect() on a datagram socket sends nothing. if (connectTo != 0 && connect(fd, (void*)connectTo, (uint)connectLen) < 0) { close(fd); @@ -400,9 +396,8 @@ private void OnUdpRecvCompletion(int socketIndex, int res, uint flags) if (_udpFds[socketIndex] < 0) { - // The socket was closed while this was in flight (a pin released). Hand back any buffer - // the kernel already selected, and take the terminating completion as the signal that - // nothing more will reference this slot. + // Closed while this was in flight (a released pin). Hand back any buffer the kernel + // selected; the terminating completion means nothing more references this slot. if ((flags & IORING_CQE_F_BUFFER) != 0) { ReturnUdpBuffer((ushort)(flags >> IORING_CQE_BUFFER_SHIFT)); diff --git a/tests/Ioxide.Tests.E2E/Protocols/QuicMigrationTests.cs b/tests/Ioxide.Tests.E2E/Protocols/QuicMigrationTests.cs index 3e4aad7d..76333508 100644 --- a/tests/Ioxide.Tests.E2E/Protocols/QuicMigrationTests.cs +++ b/tests/Ioxide.Tests.E2E/Protocols/QuicMigrationTests.cs @@ -72,18 +72,13 @@ public static void Register(Runner runner) runner.Test("quic/migration: a fleet serves a client whose packets moved to another reactor", () => { - // The multi-reactor case, which every other QUIC test here is blind to: they pin - // ReactorCount = 1, where a datagram has nowhere wrong to land. Issue #205. + // The multi-reactor case every other QUIC test is blind to - they pin ReactorCount = 1, + // where a datagram has nowhere wrong to land. Issue #205. // - // Note what does NOT prove anything here. "The request still succeeded" is satisfied by - // QUIC retransmitting until something gets through, so it passes even against a server - // that drops every migrated packet - measured, not assumed. And the handler's reactor - // cannot differ before and after, because one connection is served by one handler on - // one reactor, so asserting that asserts nothing. - // - // What is real is whether the datagrams actually arrived somewhere else and were - // handed on. So the test drives the address until they do, then asserts the exchange - // continues from there. + // "The request succeeded" proves nothing: QUIC retransmits until something gets + // through, so it passes even against a server that drops every migrated packet + // (measured). What is real is whether datagrams arrived elsewhere and were handed on, + // so the test drives the address until they do. (string certPath, string keyPath) = TestCert.Ensure(); using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]); @@ -149,13 +144,9 @@ long Dropped() runner.Test("quic/migration: claiming the new address stops the forwarding", () => { - // Forwarding alone is correct but permanent: the address does not change back, so every - // later datagram keeps landing on the wrong reactor and keeps paying a hop. The owning - // reactor claims the new address with a connected socket, which the kernel prefers over - // the wildcard binds, and delivery comes straight to it. - // - // What this asserts is that the forwarding STOPS - not that a claim was made, which - // would be satisfied by a claim that never receives anything. + // Forwarding alone is permanent: the address does not change back, so every later + // datagram pays a hop. The claim ends that. This asserts the forwarding STOPS, not that + // a claim was made - a claim that never receives anything would satisfy that. (string certPath, string keyPath) = TestCert.Ensure(); using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]); @@ -213,11 +204,9 @@ long Pins() Assert.Equal(settled, Forwarded()); - // One claim per address, not one per report. ngtcp2 announces a path several times - // while it probes, and rebuilding the claim on each announcement closes a socket with - // datagrams already queued on it - losing packets in order to avoid a hop, which is - // the wrong trade. There were at most `swaps` distinct addresses, plus one for the - // address the connection started on. + // One claim per address, not per report: ngtcp2 announces a path several times while + // probing, and rebuilding on each closes a socket with datagrams queued on it. At most + // `swaps` distinct addresses, plus the one it started on. Console.Error.WriteLine($"pins created: {Pins()} across {swaps} address change(s)"); Assert.True(Pins() <= swaps + 1, $"{Pins()} claims for {swaps} address change(s): the same address is being " @@ -272,14 +261,10 @@ long Pins() Assert.Equal(0L, forwarded); }); - // The claim every other test here leans on and none of them checks: that the connection - // SURVIVED. A client that quietly re-handshakes after its address changes also answers 200 - // to everything, so status codes cannot tell migration from reconnection - and if it were - // reconnecting, the h3 session, the QPACK tables and any application state would be gone - // while the tests stayed green. - // - // So the handler names the connection object serving each request, and the test asserts the - // name did not change. One connection, one accept, across an address change. + // What every other test here assumes and none checks: that the connection SURVIVED. A + // client that quietly re-handshakes also answers 200 to everything, so status codes cannot + // tell migration from reconnection. The handler names the connection object serving each + // request; the name must not change, and the factory must run once. foreach ((string stack, Func handler) in new (string, Func)[] { @@ -325,9 +310,8 @@ long Forwarded() Assert.True(served.StartsWith("conn-"), $"expected the serving connection to name itself, got '{served}'"); - // Keep moving until the datagrams genuinely reach a reactor that does not own the - // connection - otherwise the kernel may have re-hashed back to the owner and - // nothing about routing was exercised. + // Keep moving until the datagrams reach a reactor that does not own the connection - + // the kernel may re-hash back to the owner, exercising nothing. int swaps = 0; while (Forwarded() == 0 && swaps < 8) { @@ -360,11 +344,9 @@ long Forwarded() runner.Test("control: a fleet whose clients never move claims no addresses", () => { - // The cost side of the claim. It is made from the sweep, which visits every connection - // every 250 ms, so a guard that only looks at "is this address claimed yet" would claim - // one for every connection on the server whether or not it had ever moved - a - // descriptor and an armed receive apiece, to change nothing. Only a connection whose - // address actually moved is worth claiming. + // The cost side. The claim runs from the sweep, which visits every connection every + // 250 ms, so a guard asking only "is this claimed yet" would spend a descriptor and an + // armed receive on every connection on the server to change nothing. (string certPath, string keyPath) = TestCert.Ensure(); using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]); diff --git a/tests/Ioxide.Tests.Harness/H3TestClient.cs b/tests/Ioxide.Tests.Harness/H3TestClient.cs index 0b4d587a..11739c61 100644 --- a/tests/Ioxide.Tests.Harness/H3TestClient.cs +++ b/tests/Ioxide.Tests.Harness/H3TestClient.cs @@ -157,8 +157,7 @@ public bool CompleteHandshake(int timeoutMs) { EnsureH3Session(); - // Per request: a fresh bidi stream and fresh response state. The H3 SESSION is not per - // request - see EnsureH3Session. + // Per request: a fresh bidi stream and response state. The session is not - see above. _status = -1; _body.Clear(); _done = false; @@ -208,16 +207,10 @@ public bool CompleteHandshake(int timeoutMs) } /// - /// Stand up the HTTP/3 session once per connection: the client conn and its control, QPACK - /// encoder and QPACK decoder streams. - /// - /// Once, not per request. HTTP/3 permits exactly one control stream per peer, and RFC 9114 - /// 6.2.1 requires a second one to be treated as a connection error of type - /// H3_STREAM_CREATION_ERROR. Doing this per request opened a fresh control and QPACK pair every - /// time, so from the second request onward this client was speaking invalid HTTP/3 - and a - /// correct server answered by killing the connection. It still looked green, because the - /// requests were already in flight and kept being served off state the server had torn down, - /// which is precisely the sort of thing a test client must not do. + /// The H3 session - client conn plus its control and QPACK streams - stood up once per + /// CONNECTION, not per request. HTTP/3 allows one control stream per peer and RFC 9114 6.2.1 + /// makes a second one a connection error, so doing this per request spoke invalid HTTP/3 from + /// the second request on and a correct server killed the connection. /// private void EnsureH3Session() { @@ -357,13 +350,9 @@ private void FlushOut() public bool PeerClosed => _peerClosed; /// - /// Give ngtcp2 its loss timers. Without this the client has NO loss recovery: a lost datagram - /// leaves its packet unacked forever, the congestion window fills, writev_stream starts - /// answering 0 with nothing consumed, and the connection deadlocks with both sides silent. - /// - /// It went unnoticed because nothing here ever lost a packet - each connection served one - /// request over loopback and was gone. A connection that lives for several requests across an - /// address change does lose them, and then a real server looks like it hung. + /// ngtcp2's loss timers. Without them there is NO loss recovery: a lost datagram stays unacked, + /// the congestion window fills, writev_stream answers 0, and both ends deadlock silently - + /// which looks exactly like a server that hung after a migration. /// private void FireExpiredTimers() { @@ -378,8 +367,7 @@ private void FireExpiredTimers() return; } - // Nonzero is terminal here exactly as it is for a read: draining, closing, or a protocol - // error, none of which a later datagram undoes. + // Nonzero is terminal, as for a read: draining, closing, or a protocol error. if (iq_conn_handle_expiry(_conn, now) != 0) { _peerClosed = true; diff --git a/tests/Ioxide.Tests.Harness/TestCert.cs b/tests/Ioxide.Tests.Harness/TestCert.cs index 605e91b8..badc925d 100644 --- a/tests/Ioxide.Tests.Harness/TestCert.cs +++ b/tests/Ioxide.Tests.Harness/TestCert.cs @@ -73,17 +73,12 @@ private static bool Fresh(string certPath, params string[] alsoRequired) } /// - /// Whether a cached spec fixture still has the validity state it was created to have. + /// Whether a cached spec fixture still has the validity state it was minted for. /// - /// These fixtures are cached by content rather than by freshness, because most of them are - /// deliberately invalid and re-minting would defeat them. That is safe in one direction only: - /// an EXPIRED certificate stays expired forever, but one minted to be NOT YET VALID becomes - /// valid the moment its notBefore arrives. A "not valid yet is refused" fixture minted with - /// notBefore = now + 1 day therefore starts being served the next day, and the test fails - /// reporting a product bug that does not exist - which is exactly what happened on the day - /// this was written. - /// - /// So the cached file is reused only while its actual state still matches the intent. + /// These are cached by spec rather than freshness, since most are deliberately invalid. That is + /// safe one way only: an EXPIRED certificate stays expired, but a NOT YET VALID one becomes + /// valid when its notBefore arrives - so a "not valid yet" fixture starts being served the next + /// day and the test reports a product bug that does not exist. /// private static bool StillMatchesSpec(string certPath, ClientCertSpec spec) { @@ -432,10 +427,8 @@ public static (string CaPath, string CertPath, string KeyPath) EnsureClientCert( using FileStream guard = Lock(dir, "spec"); - // Deliberately NOT the Fresh() check: half of these are supposed to be outside their - // validity window, and re-minting an expired fixture on every run would defeat the test. - // What IS checked is that the fixture still means what it was minted to mean - see - // StillMatchesSpec. + // Not the Fresh() check - half of these are supposed to be outside their validity window. + // StillMatchesSpec checks the weaker thing: that it still means what it was minted to mean. if (File.Exists(certPath) && File.Exists(keyPath) && StillMatchesSpec(certPath, spec)) { return (ca, certPath, keyPath); From 134692d02d21e5c9289e7f1f684b57e29f7c4271 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Thu, 20 Aug 2026 19:01:37 +0100 Subject: [PATCH 11/11] docs: fold learn/quic-h3 into how-ioxide-does-h3 Two pages covering one subject, with the routing material duplicated across both. Now one: deployment first (layers, TLS on QUIC, SNI, rotation, mTLS, migration, routing, the counters), then the packet-level walk - ingress, read surface, egress, the engines, timers, and the invariants. learn/quic-h3.html is deleted and every sidebar and inbound link repointed. No broken links. --- docs/how-ioxide-does-h3.html | 344 +++++++++++++++++++++++++++- docs/learn/architecture.html | 3 +- docs/learn/clients.html | 1 - docs/learn/dev-core.html | 1 - docs/learn/dev-file.html | 1 - docs/learn/dev-pg.html | 1 - docs/learn/dev-redis.html | 1 - docs/learn/dev-tls.html | 1 - docs/learn/files.html | 1 - docs/learn/multiport.html | 1 - docs/learn/overview.html | 3 +- docs/learn/postgres.html | 1 - docs/learn/quic-h3.html | 421 ----------------------------------- docs/learn/redis.html | 1 - docs/learn/tls.html | 1 - 15 files changed, 338 insertions(+), 444 deletions(-) delete mode 100644 docs/learn/quic-h3.html diff --git a/docs/how-ioxide-does-h3.html b/docs/how-ioxide-does-h3.html index c1e1d842..9ff70581 100644 --- a/docs/how-ioxide-does-h3.html +++ b/docs/how-ioxide-does-h3.html @@ -8,6 +8,29 @@ + @@ -31,7 +54,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core @@ -46,9 +68,9 @@

    How ioxide does HTTP/3

    Everything an HTTP/3 deployment actually has to decide, in one place: which pieces do what, how TLS is terminated on QUIC, how a server picks a certificate by name and replaces one without dropping traffic, how client certificates are checked, what happens when a - client changes address mid-connection, and how a fleet of reactors keeps serving it. For the - packet-level walk through the transport, see - QUIC & HTTP/3.

    + client changes address mid-connection, how a fleet of reactors keeps serving it - and then the + packet-level walk through the transport itself, from a datagram arriving to a response going + out.

    The pieces

    Three layers, each with one job.

    @@ -165,7 +187,7 @@

    When the client changes address

    ordering, is what makes that safe. The connection is reported to the application through UpdatePeerAddress, and the streams on it never notice.

    -

    Keeping a moved client on its own reactor

    +

    Keeping a moved client on its own reactor

    A single-reactor server handles the above and is done. A real one runs a reactor per core, all bound to the same UDP port through SO_REUSEPORT, and there the kernel decides which reactor gets each datagram by hashing the sender's address. Change the address and @@ -238,9 +260,315 @@

    Watching it work

    automatically an improvement: it trades early drops, which congestion control is built to read, for a deep standing queue. Measure it on the deployment.

    -

    Continue with QUIC & HTTP/3 for the - packet-level path through the transport, or TLS for the TCP-side - stack this one deliberately shares nothing with.

    +

    Why QUIC can't reuse the TCP plumbing

    +

    The TCP side is built around one fact: one connection = one fd. The reactor's + connection table is keyed by fd, recv completions arrive per-fd, and the kernel does the + demultiplexing. QUIC inverts all of it:

    +
      +
    • One UDP socket carries every connection. There is nothing per-connection at the + kernel level - the reactor must demultiplex datagrams itself, by the Destination Connection + ID (DCID) in each packet's cleartext prefix.
    • +
    • Encryption is part of the transport. TLS 1.3 runs inside QUIC (handshake + messages ride CRYPTO frames), and every packet is sealed with keys the handshake derives. + Bytes coming off the wire are useless until an engine decrypts them.
    • +
    • Streams, not a byte pipe. One connection multiplexes many independent streams; + what pops out of the engine is (streamId, bytes, fin) events, not a single + ordered sequence.
    • +
    +

    So QUIC gets its own demux in the reactor, its own connection type, and an engine that owns + all the cryptography - while reusing the UDP layer (multishot recvmsg + GRO) and + the io_uring send path underneath.

    + +

    The layer map

    +
    +
    kernel
    +
    io_uring UDP - multishot recvmsg, GRO coalescing in, + GSO segmentation out. One socket per reactor via SO_REUSEPORT. +
    Reactor/Transport/Udp/*
    +
    +
    core · demux
    +
    QuicDispatch - splits GRO trains per segment, + parses the DCID (RFC 8999), routes to a live connection or adopts a new one via the + factory.
    Reactor/Transport/Quic/Reactor.Quic.cs
    +
    +
    ioxide.ngtcp2
    +
    QuicEngineConnection - feeds datagrams to ngtcp2 + (decrypt, ACK, flow control), copies decrypted stream events into the recv queue, pumps + egress back out with GSO batching. picotls does the TLS 1.3 handshake. +
    src/ioxide.ngtcp2/* + libioxide_ngtcp2.so
    +
    +
    core · read surface
    +
    QuicConnection - the handler-facing API: + ReadAsync over an SPSC ring of stream items, SendStream, + the two-owner refcount. Mirrors TcpConnection's pattern, shares no code with it. +
    Connection/Quic/*
    +
    +
    handler
    +
    Reactor.QuicHandle - your delegate, launched once per + adopted connection; the QUIC twin of TcpHandle.
    +
    +
    ioxide.nghttp3
    +
    Nghttp3Connection - nghttp3 rides the read surface: assembles + requests (QPACK, framing), runs your req => resp callback, drains response + frames back through SendStream. +
    src/ioxide.nghttp3/* + libioxide_nghttp3.so
    +
    + +

    Wiring it up, shown with every QUIC/h3 knob at its default + (Playground/Http3/Nghttp3Buffered is the same as an editable reference):

    +
    var engine = new QuicEngine(
    +    certPath, keyPath,
    +    cidLength: 8,                      // connection-id length this endpoint mints
    +    alpn: ["h3"],                      // pin the protocol; null accepts whatever the client offers
    +    maxSendRetentionBytes: 16L << 20); // send-retention high-water: bounds memory so a response
    +                                       // larger than the window streams instead of buffering whole
    +
    +var config = new ServerConfig
    +{
    +    ReactorCount   = Environment.ProcessorCount,  // io_uring rings/threads - one per core
    +    RingEntries    = 8192,                         // SQ/CQ depth per ring
    +    DualStack      = false,                         // true = one IPv6 socket also accepts IPv4-mapped
    +    RecvBufferSize = 32 * 1024,                    // bytes per shared recv buffer
    +    RecvSlots      = 4096,                          // shared recv buffer-ring depth
    +    Incremental    = null,                          // per-connection recv rings (6.12+)
    +    Tcp = null,                        // QUIC-only: no TCP listener at all
    +    Udp = new UdpOptions
    +    {
    +        RecvSlots = 16,                // multishot recv slots per reactor
    +        Gro = true,                    // UDP_GRO: coalesce received datagrams into one recv
    +    },
    +    Quic = new QuicOptions
    +    {
    +        Port = 8443,                   // UDP port, bound automatically on every reactor
    +        LocalCidLength = 8,            // must match the engine's cidLength
    +        IdleTimeoutMs = 60_000,        // close a connection idle this long
    +        ConnectionFactory = engine.CreateFactory(),
    +    },
    +};
    +
    +var h3Options = new Nghttp3Options    // the HTTP/3 layer's own knobs, passed to Nghttp3Connection
    +{
    +    QpackDynamicTableCapacity = 0,     // 0 = headers stay literal (never blocks on a table update)
    +    QpackBlockedStreams = 0,           // raise both together to trade bytes for the dynamic table
    +};
    +
    +reactor.TcpHandle  = Handlers.Raw;
    +reactor.QuicHandle = (r, conn) => new Nghttp3Connection(conn, h3Options).RunBufferedAsync(
    +    static req => Nghttp3Response.Text($"hello {Encoding.ASCII.GetString(req.Path.Span)}"));
    + +

    Ingress: the life of a datagram

    +

    A client datagram lands as a multishot recvmsg completion on the reactor + thread, and everything below happens inline in that dispatch - same as the TCP side, there is + no handoff to another thread anywhere in the path.

    +
      +
    1. GRO split. The kernel may hand us a train - several datagrams from the + same peer socket coalesced into one buffer. Connections share client sockets, so one train + can interleave packets of different connections. QuicDispatch splits + the train and routes each segment independently.
    2. +
    3. DCID demux. The first bytes of a QUIC packet are cleartext and version-independent + (RFC 8999). Long headers carry an explicit DCID length; short headers carry exactly the + LocalCidLength bytes this endpoint mints. The DCID is looked up in the reactor's + _quicConns dictionary.
    4. +
    5. Adopt or route. A known DCID goes straight to its connection. An unknown + long-header packet is a new handshake: the factory runs iq_accept + (validates the Initial, creates the ngtcp2 conn, mints our SCID), the reactor snapshots the + peer address, registers the CIDs, inits the two-owner refcount, and launches your + QuicHandle fault-observed. An unknown short-header packet is either + stale traffic from a dead connection or a live one whose client changed address - see + Keeping a moved client on its own reactor above.
    6. +
    7. Engine read. OnDatagram feeds the payload to + iq_conn_read. ngtcp2 decrypts, handles ACKs and flow control, and fires + callbacks mid-call - the important one being stream data.
    8. +
    9. Copy and enqueue. ngtcp2's decrypted spans die when iq_conn_read + returns, so OnStreamData copies each event into a pooled buffer and enqueues a + (StreamId, bytes, Fin) item on the connection's SPSC recv ring. Stream lifecycle + (closed / reset / stop-sending) rides the same ring as Kind-tagged items, so the + handler sees everything in order.
    10. +
    11. Fire once. Only after iq_conn_read has fully unwound does the engine + fire the read signal - the same inline-resume IVTS as TCP. The handler resumes synchronously + on the reactor thread, drains the ring, sends responses. By the time the loop re-enters the + kernel, those responses are already staged.
    12. +
    + +
    Why "fire once, after the read unwinds"? The handler resumes + inline when the signal fires. If OnStreamData fired it directly, the + handler would run - and call SendStream, re-entering ngtcp2 - while ngtcp2 is + still executing iq_conn_read above it on the same stack. Deferring the wake to + after the engine call makes reentrancy impossible by construction.
    + +

    The read surface

    +

    The handler-facing API deliberately mirrors TcpConnection - the arm flag, the + sticky pending bit that closes the lost-wakeup race, the generation token - but it is a + separate implementation. The two transports share a pattern, not a base class: TCP's + items are buffer-ring slots with bids; QUIC's are pooled copies tagged with stream ids. The + write sides have nothing in common at all.

    +
    // A raw QUIC handler: one per connection, streams demuxed by the item's StreamId.
    +reactor.QuicHandle = async (r, conn) =>
    +{
    +    try
    +    {
    +        while (true)
    +        {
    +            QuicRecvSnapshot snap = await conn.ReadAsync();
    +
    +            while (conn.TryGetDelivery(in snap, out QuicRecvRing.Delivery item))
    +            {
    +                if (item.Kind == QuicStreamEvent.Data)
    +                    conn.SendStream(item.StreamId, item.AsSpan(), item.Fin);  // echo
    +                conn.ReturnBuffer(in item);   // pooled buffer back to the pool
    +            }
    +
    +            if (snap.IsClosed) break;
    +            conn.ResetRead();
    +        }
    +    }
    +    finally { conn.DecRef(); }   // release the handler's ref
    +};
    +

    Lifecycle is the same two-owner refcount as TCP: the transport holds one reference, the + handler holds the other, and teardown (freeing the peer-address block, unrouting CIDs) only + runs when both are gone - so an evicted connection can never be freed under a live handler. + All teardown funnels through QuicRemoveConnection, whether the engine closed + (error, idle) or the sweep evicted.

    + +

    Egress: SendStream and the retention contract

    +

    There is no IBufferWriter, no write slab, and no FlushAsync on the + QUIC side - deliberately. TCP hands you a raw byte pipe and you await the send for + backpressure. In QUIC the engine owns framing, pacing, congestion control and retransmission; + SendStream(streamId, bytes, fin) hands bytes over and returns. Awaiting a flush + would await nothing meaningful.

    +

    Two hard rules shape the implementation:

    +
      +
    • ngtcp2 does not copy. Stream data passed to the engine is retained by + pointer for retransmission until the peer acknowledges it. So SendStream + copies your span into native chunks owned by the connection (OutStream chains), + the engine is fed pointers into those, and the acked_stream_data_offset + callback frees chunks as the ack watermark advances. Stream close purges the rest.
    • +
    • Never drop what the engine deferred. When the congestion window is full, + iq_conn_write takes nothing - and the layer above (nghttp3) has already + accounted those bytes as written and will never re-emit them. The unsent tail stays in the + chunk chain and is replayed on every flush (each inbound ACK, each timer) until it fits.
    • +
    • Retention is a backpressure high-water, not a hard cap. Retained bytes + (sent-but-unacked plus unsent) are bounded by QuicEngine's + maxSendRetentionBytes (default 16 MiB). A producer feeding a response checks + CanQueueSend and pauses there; as acks drain retention below the mark, the + ack/timer egress path fires OnSendCapacityAvailable to resume it - the read loop + never sees a download's acks, so that callback is the resume. This is what lets a response of + any size stream out in bounded memory rather than buffering whole. The cap only closes + the connection as a backstop, at twice the high-water, when a producer ignores backpressure and + keeps pushing - or when a peer simply stops acking.
    • +
    +
    War story. Both rules exist because their violations shipped first. + Passing fixed spans of reused buffers to ngtcp2 was harmless for a while - only + because a timer bug meant retransmission never ran. The day the timer was fixed, every loss + retransmitted STREAM frames out of recycled memory: segfaults in + ngtcp2_pkt_encode_stream_frame on a good day, silently corrupted frames on the + wire on a bad one.
    +

    On the wire side, datagrams produced during one engine cycle are batched into a single + UDP_SEGMENT (GSO) send - one syscall for up to a 63 KB run of equal-size + datagrams instead of one per packet.

    + +

    The native engines and their shims

    +

    Neither ngtcp2 nor nghttp3 is P/Invoked directly. Both APIs are built on large versioned + structs and callback tables whose layout shifts between releases - marshaling those from C# + would break silently on every upstream bump. Instead each package bundles a small C shim that + owns every struct layout (compiled against the exact vendored headers) and exposes a flat, + stable ABI:

    +
      +
    • libioxide_ngtcp2.so (~1.2 MB) - ngtcp2 + its picotls crypto backend + + picotls, statically linked; the only system dependency is libcrypto.so.3. + Exports iq_*: engine/accept/read/write, uni-stream open, ALPN, expiry. + Built by scripts/build-ngtcp2-native.sh.
    • +
    • libioxide_nghttp3.so (~244 KB) - nghttp3, statically linked, zero external + dependencies (it does no I/O and no crypto). Exports ih3_*: conn create/bind, + read_stream, submit_response, writev, shutdown/close. Built by + scripts/build-nghttp3-native.sh.
    • +
    +

    Both .so files are committed and packed into the NuGet + runtimes/linux-x64/native/, so consumers install nothing.

    + +

    TLS 1.3 and ALPN

    +

    QUIC folds TLS into the transport (RFC 9001): handshake messages travel in CRYPTO frames, + and TLS's job shrinks to the handshake plus key derivation - packet protection itself is + QUIC's own AEAD, applied by ngtcp2. In this stack picotls runs that handshake; the managed + side never touches TLS at all. That's why QuicConnection has no handshake or + crypto surface, and why the cert/key go into QuicEngine's constructor.

    +

    ALPN is enforced in the shim's client-hello hook: new QuicEngine(cert, key, + alpn: ["h3"]) installs an allowlist - a client offering none of the listed protocols + fails the handshake with no_application_protocol, per the RFC. With no allowlist + the server accepts whatever the client offers first. The chosen token is surfaced as + QuicConnection.NegotiatedProtocol after the handshake.

    + +

    The HTTP/3 layer

    +

    ioxide.nghttp3 references only the core - not ioxide.ngtcp2. It needs + nothing but the abstract read/write surface, so it would ride any future engine (quicly, etc.) + unchanged. One Nghttp3Connection wraps one QuicConnection:

    +
    reactor.QuicHandle = (r, conn) => new Nghttp3Connection(conn).RunBufferedAsync(
    +    static req => Nghttp3Response.Text($"hello {Encoding.ASCII.GetString(req.Path.Span)} via {Encoding.ASCII.GetString(req.Method.Span)}"));
    +

    Inside RunAsync:

    +
      +
    1. Lazy setup. On the first wake (which is by definition post-handshake), it opens + the three server unidirectional streams H3 requires - control + QPACK encoder/decoder - + binds them into nghttp3, and the SETTINGS preface rides out on that same drain.
    2. +
    3. Assembly. Every recv item is fed to nghttp3_conn_read_stream; + nghttp3 demuxes uni-stream types itself and fires header/data/end callbacks, which + accumulate into an Nghttp3Request (method, path, headers, body - all post-QPACK + bytes as ReadOnlyMemory<byte>; the library never decodes to strings, and the + memories are valid until the handler returns).
    4. +
    5. Dispatch. Completed requests run your callback on the reactor thread; the + Nghttp3Response (status, headers, body) is submitted back to nghttp3, whose output + frames are drained through SendStream - response bodies are copied into shim + memory that lives until the stream closes, because nghttp3 holds references too.
    6. +
    7. Lifecycle mirroring. The Kind-tagged items keep nghttp3's view of + every stream in sync with QUIC's: reset → shutdown_stream_read, + stop-sending → shutdown_stream_write, closed → + close_stream. A cancelled request is torn down on both sides instead of + half-ignored.
    8. +
    +

    QPACK runs with a zero-size dynamic table (the nghttp3 default we keep): static-table-only + compression, no encoder/decoder state to corrupt, still ~87% header savings in practice.

    + +

    Timers

    +

    QUIC is timer-hungry - loss detection and PTO probes need millisecond deadlines, and the + reactor's 250 ms ticker is far too coarse: a retransmit that waits 250 ms per loss + turns load spikes into self-sustaining storms. So deadlines are split:

    +
      +
    • Loss/PTO timers - QuicFireDueTimers runs at the top of every loop + pass: one cheap comparison against the earliest deadline across live connections, and a full + sweep only when it's due. Under load the loop spins on completions, so timers fire at + completion-batch granularity (~RTT). Dispatch re-arms the minimum after every datagram.
    • +
    • Idle eviction - stays on the 250 ms ticker, which doubles as the wake floor + when the reactor is otherwise asleep. Abandoned connections (a killed benchmark client) are + reaped by ngtcp2's idle timeout and unrouted through the same teardown funnel.
    • +
    + +

    Rules learned the hard way

    +

    Each of these is load-bearing; every one of them shipped broken first and was found by a + benchmark, a packet capture, or a core dump.

    +
    + + + + + + + + +
    RuleOr else
    Demux GRO trains per segmentTrains interleave datagrams of different + connections (they share the client's 4-tuple); routing a whole train by its first DCID feeds + other connections' packets to the wrong engine, which silently drops them.
    Retain stream bytes until ackedngtcp2 keeps pointers into your buffers for + retransmission. Reused buffers become corrupted retransmitted frames - or a segfault inside + the packet encoder.
    Fire the read signal after iq_conn_read unwindsThe handler + resumes inline and re-enters ngtcp2 mid-callback, on the same stack.
    Never discard engine-deferred bytesnghttp3 already accounted them as + written; the stream starves forever and the connection live-locks around it.
    initial_max_streams is a window, not a capExtend it as streams + close (ngtcp2_conn_extend_max_streams_*) or every connection stalls for good + after its first 100 requests.
    An engine expiry can already be in the pastUnsigned expiry - now + underflows and schedules the retransmit timer ~584 years out, permanently killing that + connection's loss recovery.
    Free H3 response bodies only at stream closenghttp3 holds references into + the body for output it already accepted; freeing on stop-sending is a use-after-free.
    + +

    Continue with Architecture for the reactor + model these layers ride on, or TLS for the TCP-side stack this one + deliberately shares nothing with.

    diff --git a/docs/learn/architecture.html b/docs/learn/architecture.html index e12bd5dc..c6de0a00 100644 --- a/docs/learn/architecture.html +++ b/docs/learn/architecture.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core @@ -183,7 +182,7 @@

    Configuration

    Udp.Ports[]raw UDP sockets; datagrams reach Reactor.OnDatagram Udp.RecvSlots16shared UDP provided-buffer ring depth (in-flight datagrams) Udp.GrotrueUDP_GRO: coalesce equal-size datagram bursts into one completion - Quicnullenable the QUIC transport - see QUIC & HTTP/3 + Quicnullenable the QUIC transport - see QUIC & HTTP/3 diff --git a/docs/learn/clients.html b/docs/learn/clients.html index 9105c9c3..4c4cdf1c 100644 --- a/docs/learn/clients.html +++ b/docs/learn/clients.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/dev-core.html b/docs/learn/dev-core.html index 797e39a1..6b871a29 100644 --- a/docs/learn/dev-core.html +++ b/docs/learn/dev-core.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/dev-file.html b/docs/learn/dev-file.html index 52927d1d..95ab52fe 100644 --- a/docs/learn/dev-file.html +++ b/docs/learn/dev-file.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/dev-pg.html b/docs/learn/dev-pg.html index ca200c0b..1b1e0f7e 100644 --- a/docs/learn/dev-pg.html +++ b/docs/learn/dev-pg.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/dev-redis.html b/docs/learn/dev-redis.html index 2fd97853..552e36d5 100644 --- a/docs/learn/dev-redis.html +++ b/docs/learn/dev-redis.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/dev-tls.html b/docs/learn/dev-tls.html index 898c9856..92d1c4fd 100644 --- a/docs/learn/dev-tls.html +++ b/docs/learn/dev-tls.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/files.html b/docs/learn/files.html index 75d699c9..695571e6 100644 --- a/docs/learn/files.html +++ b/docs/learn/files.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/multiport.html b/docs/learn/multiport.html index 3db338d6..bd63059f 100644 --- a/docs/learn/multiport.html +++ b/docs/learn/multiport.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/overview.html b/docs/learn/overview.html index 6d4281b0..776b9f20 100644 --- a/docs/learn/overview.html +++ b/docs/learn/overview.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core @@ -125,7 +124,7 @@

    Speaking a protocol

  • ioxide.http2 - HTTP/2, pure C#. h2c with prior knowledge, or h2 over TLS chosen by ALPN.
  • ioxide.nghttp3 / ioxide.http3 - HTTP/3, over - QUIC from ioxide.ngtcp2.
  • + QUIC from ioxide.ngtcp2.
  • TLS - OpenSSL on the ring, kernel TLS as an opt-in, plus a portable SslStream path. In the core package; no reference to add.
  • diff --git a/docs/learn/postgres.html b/docs/learn/postgres.html index 49ab665e..8e4ad12b 100644 --- a/docs/learn/postgres.html +++ b/docs/learn/postgres.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/quic-h3.html b/docs/learn/quic-h3.html deleted file mode 100644 index b9bbe8ce..00000000 --- a/docs/learn/quic-h3.html +++ /dev/null @@ -1,421 +0,0 @@ - - - - - - QUIC & HTTP/3 - ioxide - - - - - - - - - -
    - - -
    -

    QUIC & HTTP/3

    -

    ioxide serves HTTP/3 through three layers, each with one job: the core's QUIC - transport routes UDP datagrams to logical connections, ioxide.ngtcp2 runs the - protocol engine (ngtcp2 + picotls, bundled native), and ioxide.nghttp3 turns decrypted - stream bytes into requests and responses (nghttp3, bundled native). This page walks the whole - path a request takes, and ends with the invariants that were learned the hard way.

    - -

    Why QUIC can't reuse the TCP plumbing

    -

    The TCP side is built around one fact: one connection = one fd. The reactor's - connection table is keyed by fd, recv completions arrive per-fd, and the kernel does the - demultiplexing. QUIC inverts all of it:

    -
      -
    • One UDP socket carries every connection. There is nothing per-connection at the - kernel level - the reactor must demultiplex datagrams itself, by the Destination Connection - ID (DCID) in each packet's cleartext prefix.
    • -
    • Encryption is part of the transport. TLS 1.3 runs inside QUIC (handshake - messages ride CRYPTO frames), and every packet is sealed with keys the handshake derives. - Bytes coming off the wire are useless until an engine decrypts them.
    • -
    • Streams, not a byte pipe. One connection multiplexes many independent streams; - what pops out of the engine is (streamId, bytes, fin) events, not a single - ordered sequence.
    • -
    -

    So QUIC gets its own demux in the reactor, its own connection type, and an engine that owns - all the cryptography - while reusing the UDP layer (multishot recvmsg + GRO) and - the io_uring send path underneath.

    - -

    The layer map

    -
    -
    kernel
    -
    io_uring UDP - multishot recvmsg, GRO coalescing in, - GSO segmentation out. One socket per reactor via SO_REUSEPORT. -
    Reactor/Transport/Udp/*
    -
    -
    core · demux
    -
    QuicDispatch - splits GRO trains per segment, - parses the DCID (RFC 8999), routes to a live connection or adopts a new one via the - factory.
    Reactor/Transport/Quic/Reactor.Quic.cs
    -
    -
    ioxide.ngtcp2
    -
    QuicEngineConnection - feeds datagrams to ngtcp2 - (decrypt, ACK, flow control), copies decrypted stream events into the recv queue, pumps - egress back out with GSO batching. picotls does the TLS 1.3 handshake. -
    src/ioxide.ngtcp2/* + libioxide_ngtcp2.so
    -
    -
    core · read surface
    -
    QuicConnection - the handler-facing API: - ReadAsync over an SPSC ring of stream items, SendStream, - the two-owner refcount. Mirrors TcpConnection's pattern, shares no code with it. -
    Connection/Quic/*
    -
    -
    handler
    -
    Reactor.QuicHandle - your delegate, launched once per - adopted connection; the QUIC twin of TcpHandle.
    -
    -
    ioxide.nghttp3
    -
    Nghttp3Connection - nghttp3 rides the read surface: assembles - requests (QPACK, framing), runs your req => resp callback, drains response - frames back through SendStream. -
    src/ioxide.nghttp3/* + libioxide_nghttp3.so
    -
    - -

    Wiring it up, shown with every QUIC/h3 knob at its default - (Playground/Http3/Nghttp3Buffered is the same as an editable reference):

    -
    var engine = new QuicEngine(
    -    certPath, keyPath,
    -    cidLength: 8,                      // connection-id length this endpoint mints
    -    alpn: ["h3"],                      // pin the protocol; null accepts whatever the client offers
    -    maxSendRetentionBytes: 16L << 20); // send-retention high-water: bounds memory so a response
    -                                       // larger than the window streams instead of buffering whole
    -
    -var config = new ServerConfig
    -{
    -    ReactorCount   = Environment.ProcessorCount,  // io_uring rings/threads - one per core
    -    RingEntries    = 8192,                         // SQ/CQ depth per ring
    -    DualStack      = false,                         // true = one IPv6 socket also accepts IPv4-mapped
    -    RecvBufferSize = 32 * 1024,                    // bytes per shared recv buffer
    -    RecvSlots      = 4096,                          // shared recv buffer-ring depth
    -    Incremental    = null,                          // per-connection recv rings (6.12+)
    -    Tcp = null,                        // QUIC-only: no TCP listener at all
    -    Udp = new UdpOptions
    -    {
    -        RecvSlots = 16,                // multishot recv slots per reactor
    -        Gro = true,                    // UDP_GRO: coalesce received datagrams into one recv
    -    },
    -    Quic = new QuicOptions
    -    {
    -        Port = 8443,                   // UDP port, bound automatically on every reactor
    -        LocalCidLength = 8,            // must match the engine's cidLength
    -        IdleTimeoutMs = 60_000,        // close a connection idle this long
    -        ConnectionFactory = engine.CreateFactory(),
    -    },
    -};
    -
    -var h3Options = new Nghttp3Options    // the HTTP/3 layer's own knobs, passed to Nghttp3Connection
    -{
    -    QpackDynamicTableCapacity = 0,     // 0 = headers stay literal (never blocks on a table update)
    -    QpackBlockedStreams = 0,           // raise both together to trade bytes for the dynamic table
    -};
    -
    -reactor.TcpHandle  = Handlers.Raw;
    -reactor.QuicHandle = (r, conn) => new Nghttp3Connection(conn, h3Options).RunBufferedAsync(
    -    static req => Nghttp3Response.Text($"hello {Encoding.ASCII.GetString(req.Path.Span)}"));
    - -

    Ingress: the life of a datagram

    -

    A client datagram lands as a multishot recvmsg completion on the reactor - thread, and everything below happens inline in that dispatch - same as the TCP side, there is - no handoff to another thread anywhere in the path.

    -
      -
    1. GRO split. The kernel may hand us a train - several datagrams from the - same peer socket coalesced into one buffer. Connections share client sockets, so one train - can interleave packets of different connections. QuicDispatch splits - the train and routes each segment independently.
    2. -
    3. DCID demux. The first bytes of a QUIC packet are cleartext and version-independent - (RFC 8999). Long headers carry an explicit DCID length; short headers carry exactly the - LocalCidLength bytes this endpoint mints. The DCID is looked up in the reactor's - _quicConns dictionary.
    4. -
    5. Adopt or route. A known DCID goes straight to its connection. An unknown - long-header packet is a new handshake: the factory runs iq_accept - (validates the Initial, creates the ngtcp2 conn, mints our SCID), the reactor snapshots the - peer address, registers the CIDs, inits the two-owner refcount, and launches your - QuicHandle fault-observed. An unknown short-header packet is either - stale traffic from a dead connection or a live one whose client changed address - see - Multi-reactor routing, which is how the two are told apart.
    6. -
    7. Engine read. OnDatagram feeds the payload to - iq_conn_read. ngtcp2 decrypts, handles ACKs and flow control, and fires - callbacks mid-call - the important one being stream data.
    8. -
    9. Copy and enqueue. ngtcp2's decrypted spans die when iq_conn_read - returns, so OnStreamData copies each event into a pooled buffer and enqueues a - (StreamId, bytes, Fin) item on the connection's SPSC recv ring. Stream lifecycle - (closed / reset / stop-sending) rides the same ring as Kind-tagged items, so the - handler sees everything in order.
    10. -
    11. Fire once. Only after iq_conn_read has fully unwound does the engine - fire the read signal - the same inline-resume IVTS as TCP. The handler resumes synchronously - on the reactor thread, drains the ring, sends responses. By the time the loop re-enters the - kernel, those responses are already staged.
    12. -
    - -
    Why "fire once, after the read unwinds"? The handler resumes - inline when the signal fires. If OnStreamData fired it directly, the - handler would run - and call SendStream, re-entering ngtcp2 - while ngtcp2 is - still executing iq_conn_read above it on the same stack. Deferring the wake to - after the engine call makes reentrancy impossible by construction.
    - -

    Multi-reactor routing, and the client that moves

    -

    Everything above describes one reactor. A real server runs one per core, all bound to the - same UDP port through SO_REUSEPORT, and the kernel decides which of them gets each - datagram by hashing the sender's address. That is a perfectly good answer right up until the - address changes.

    - -

    It changes more often than "the user switched networks" suggests: home and mobile NATs - recycle UDP mappings after fairly short idle periods, so a connection that goes quiet and speaks - again can come back from a different port without the client having moved at all. The hash then - picks a different reactor - one that has never heard of this connection. Its packets - carry a short header, so there is no handshake to accept and nothing to look up. Historically - they were dropped, and the connection died on the reactor that could still have served it.

    - -
    The connection cannot come to the packet. The ngtcp2_conn, - the picotls session, the open streams and their ring-bound buffers are owned by one reactor - thread, and QuicConnection is reactor-thread-only throughout. Moving live state to - whichever reactor a datagram happened to land on is the one thing shared-nothing forbids. So the - datagram moves instead - which is not a violation but the model working as intended, - the same message passing ScheduleOnReactor already exists for.
    - -

    Both routing modes rest on one trick: ioxide mints its own connection ids, so it can write - the owning reactor into them. The first byte is chosen so that cid[0] % ReactorCount - is the owner, while the remaining randomness is untouched. The id travels with the connection, so - it keeps naming the right reactor no matter what the address does.

    - -

    Two modes decide who does the routing. QuicRouting.Forward, the default, lets - the kernel hash as before and has the receiving reactor hand a stranger to its owner over the - post queue - so nothing is paid until a client actually moves. QuicRouting.KernelFilter - attaches a classic-BPF program to the reuseport group so the kernel reads the shard byte itself. - The full comparison, with numbers, is on - How ioxide does HTTP/3.

    - -

    The read surface

    -

    The handler-facing API deliberately mirrors TcpConnection - the arm flag, the - sticky pending bit that closes the lost-wakeup race, the generation token - but it is a - separate implementation. The two transports share a pattern, not a base class: TCP's - items are buffer-ring slots with bids; QUIC's are pooled copies tagged with stream ids. The - write sides have nothing in common at all.

    -
    // A raw QUIC handler: one per connection, streams demuxed by the item's StreamId.
    -reactor.QuicHandle = async (r, conn) =>
    -{
    -    try
    -    {
    -        while (true)
    -        {
    -            QuicRecvSnapshot snap = await conn.ReadAsync();
    -
    -            while (conn.TryGetDelivery(in snap, out QuicRecvRing.Delivery item))
    -            {
    -                if (item.Kind == QuicStreamEvent.Data)
    -                    conn.SendStream(item.StreamId, item.AsSpan(), item.Fin);  // echo
    -                conn.ReturnBuffer(in item);   // pooled buffer back to the pool
    -            }
    -
    -            if (snap.IsClosed) break;
    -            conn.ResetRead();
    -        }
    -    }
    -    finally { conn.DecRef(); }   // release the handler's ref
    -};
    -

    Lifecycle is the same two-owner refcount as TCP: the transport holds one reference, the - handler holds the other, and teardown (freeing the peer-address block, unrouting CIDs) only - runs when both are gone - so an evicted connection can never be freed under a live handler. - All teardown funnels through QuicRemoveConnection, whether the engine closed - (error, idle) or the sweep evicted.

    - -

    Egress: SendStream and the retention contract

    -

    There is no IBufferWriter, no write slab, and no FlushAsync on the - QUIC side - deliberately. TCP hands you a raw byte pipe and you await the send for - backpressure. In QUIC the engine owns framing, pacing, congestion control and retransmission; - SendStream(streamId, bytes, fin) hands bytes over and returns. Awaiting a flush - would await nothing meaningful.

    -

    Two hard rules shape the implementation:

    -
      -
    • ngtcp2 does not copy. Stream data passed to the engine is retained by - pointer for retransmission until the peer acknowledges it. So SendStream - copies your span into native chunks owned by the connection (OutStream chains), - the engine is fed pointers into those, and the acked_stream_data_offset - callback frees chunks as the ack watermark advances. Stream close purges the rest.
    • -
    • Never drop what the engine deferred. When the congestion window is full, - iq_conn_write takes nothing - and the layer above (nghttp3) has already - accounted those bytes as written and will never re-emit them. The unsent tail stays in the - chunk chain and is replayed on every flush (each inbound ACK, each timer) until it fits.
    • -
    • Retention is a backpressure high-water, not a hard cap. Retained bytes - (sent-but-unacked plus unsent) are bounded by QuicEngine's - maxSendRetentionBytes (default 16 MiB). A producer feeding a response checks - CanQueueSend and pauses there; as acks drain retention below the mark, the - ack/timer egress path fires OnSendCapacityAvailable to resume it - the read loop - never sees a download's acks, so that callback is the resume. This is what lets a response of - any size stream out in bounded memory rather than buffering whole. The cap only closes - the connection as a backstop, at twice the high-water, when a producer ignores backpressure and - keeps pushing - or when a peer simply stops acking.
    • -
    -
    War story. Both rules exist because their violations shipped first. - Passing fixed spans of reused buffers to ngtcp2 was harmless for a while - only - because a timer bug meant retransmission never ran. The day the timer was fixed, every loss - retransmitted STREAM frames out of recycled memory: segfaults in - ngtcp2_pkt_encode_stream_frame on a good day, silently corrupted frames on the - wire on a bad one.
    -

    On the wire side, datagrams produced during one engine cycle are batched into a single - UDP_SEGMENT (GSO) send - one syscall for up to a 63 KB run of equal-size - datagrams instead of one per packet.

    - -

    The native engines and their shims

    -

    Neither ngtcp2 nor nghttp3 is P/Invoked directly. Both APIs are built on large versioned - structs and callback tables whose layout shifts between releases - marshaling those from C# - would break silently on every upstream bump. Instead each package bundles a small C shim that - owns every struct layout (compiled against the exact vendored headers) and exposes a flat, - stable ABI:

    -
      -
    • libioxide_ngtcp2.so (~1.2 MB) - ngtcp2 + its picotls crypto backend + - picotls, statically linked; the only system dependency is libcrypto.so.3. - Exports iq_*: engine/accept/read/write, uni-stream open, ALPN, expiry. - Built by scripts/build-ngtcp2-native.sh.
    • -
    • libioxide_nghttp3.so (~244 KB) - nghttp3, statically linked, zero external - dependencies (it does no I/O and no crypto). Exports ih3_*: conn create/bind, - read_stream, submit_response, writev, shutdown/close. Built by - scripts/build-nghttp3-native.sh.
    • -
    -

    Both .so files are committed and packed into the NuGet - runtimes/linux-x64/native/, so consumers install nothing.

    - -

    TLS 1.3 and ALPN

    -

    QUIC folds TLS into the transport (RFC 9001): handshake messages travel in CRYPTO frames, - and TLS's job shrinks to the handshake plus key derivation - packet protection itself is - QUIC's own AEAD, applied by ngtcp2. In this stack picotls runs that handshake; the managed - side never touches TLS at all. That's why QuicConnection has no handshake or - crypto surface, and why the cert/key go into QuicEngine's constructor.

    -

    ALPN is enforced in the shim's client-hello hook: new QuicEngine(cert, key, - alpn: ["h3"]) installs an allowlist - a client offering none of the listed protocols - fails the handshake with no_application_protocol, per the RFC. With no allowlist - the server accepts whatever the client offers first. The chosen token is surfaced as - QuicConnection.NegotiatedProtocol after the handshake.

    - -

    The HTTP/3 layer

    -

    ioxide.nghttp3 references only the core - not ioxide.ngtcp2. It needs - nothing but the abstract read/write surface, so it would ride any future engine (quicly, etc.) - unchanged. One Nghttp3Connection wraps one QuicConnection:

    -
    reactor.QuicHandle = (r, conn) => new Nghttp3Connection(conn).RunBufferedAsync(
    -    static req => Nghttp3Response.Text($"hello {Encoding.ASCII.GetString(req.Path.Span)} via {Encoding.ASCII.GetString(req.Method.Span)}"));
    -

    Inside RunAsync:

    -
      -
    1. Lazy setup. On the first wake (which is by definition post-handshake), it opens - the three server unidirectional streams H3 requires - control + QPACK encoder/decoder - - binds them into nghttp3, and the SETTINGS preface rides out on that same drain.
    2. -
    3. Assembly. Every recv item is fed to nghttp3_conn_read_stream; - nghttp3 demuxes uni-stream types itself and fires header/data/end callbacks, which - accumulate into an Nghttp3Request (method, path, headers, body - all post-QPACK - bytes as ReadOnlyMemory<byte>; the library never decodes to strings, and the - memories are valid until the handler returns).
    4. -
    5. Dispatch. Completed requests run your callback on the reactor thread; the - Nghttp3Response (status, headers, body) is submitted back to nghttp3, whose output - frames are drained through SendStream - response bodies are copied into shim - memory that lives until the stream closes, because nghttp3 holds references too.
    6. -
    7. Lifecycle mirroring. The Kind-tagged items keep nghttp3's view of - every stream in sync with QUIC's: reset → shutdown_stream_read, - stop-sending → shutdown_stream_write, closed → - close_stream. A cancelled request is torn down on both sides instead of - half-ignored.
    8. -
    -

    QPACK runs with a zero-size dynamic table (the nghttp3 default we keep): static-table-only - compression, no encoder/decoder state to corrupt, still ~87% header savings in practice.

    - -

    Timers

    -

    QUIC is timer-hungry - loss detection and PTO probes need millisecond deadlines, and the - reactor's 250 ms ticker is far too coarse: a retransmit that waits 250 ms per loss - turns load spikes into self-sustaining storms. So deadlines are split:

    -
      -
    • Loss/PTO timers - QuicFireDueTimers runs at the top of every loop - pass: one cheap comparison against the earliest deadline across live connections, and a full - sweep only when it's due. Under load the loop spins on completions, so timers fire at - completion-batch granularity (~RTT). Dispatch re-arms the minimum after every datagram.
    • -
    • Idle eviction - stays on the 250 ms ticker, which doubles as the wake floor - when the reactor is otherwise asleep. Abandoned connections (a killed benchmark client) are - reaped by ngtcp2's idle timeout and unrouted through the same teardown funnel.
    • -
    - -

    Rules learned the hard way

    -

    Each of these is load-bearing; every one of them shipped broken first and was found by a - benchmark, a packet capture, or a core dump.

    -
    - - - - - - - - -
    RuleOr else
    Demux GRO trains per segmentTrains interleave datagrams of different - connections (they share the client's 4-tuple); routing a whole train by its first DCID feeds - other connections' packets to the wrong engine, which silently drops them.
    Retain stream bytes until ackedngtcp2 keeps pointers into your buffers for - retransmission. Reused buffers become corrupted retransmitted frames - or a segfault inside - the packet encoder.
    Fire the read signal after iq_conn_read unwindsThe handler - resumes inline and re-enters ngtcp2 mid-callback, on the same stack.
    Never discard engine-deferred bytesnghttp3 already accounted them as - written; the stream starves forever and the connection live-locks around it.
    initial_max_streams is a window, not a capExtend it as streams - close (ngtcp2_conn_extend_max_streams_*) or every connection stalls for good - after its first 100 requests.
    An engine expiry can already be in the pastUnsigned expiry - now - underflows and schedules the retransmit timer ~584 years out, permanently killing that - connection's loss recovery.
    Free H3 response bodies only at stream closenghttp3 holds references into - the body for output it already accepted; freeing on stop-sending is a use-after-free.
    - -

    Continue with Architecture for the reactor - model these layers ride on, or core internals for the TCP-side - counterparts.

    -
    -
    - - - - - - diff --git a/docs/learn/redis.html b/docs/learn/redis.html index affb40bd..78a2dd6b 100644 --- a/docs/learn/redis.html +++ b/docs/learn/redis.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core diff --git a/docs/learn/tls.html b/docs/learn/tls.html index df453d84..1d845e9d 100644 --- a/docs/learn/tls.html +++ b/docs/learn/tls.html @@ -30,7 +30,6 @@ Custom clients
    Internals
    Architecture - QUIC & HTTP/3 How ioxide does HTTP/3
    Packages
    ioxide core