Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/clients/ioxide.file/ioxide.file.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.file</RootNamespace>

<PackageId>ioxide.file</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>File serving for the ioxide io_uring runtime: immutable asset snapshots with baked responses, pooled positional ring reads, atomic reloads.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.httpclient/ioxide.httpclient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.httpclient</RootNamespace>

<PackageId>ioxide.httpclient</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.pg/ioxide.pg.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.pg</RootNamespace>

<PackageId>ioxide.pg</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>Postgres driver for the ioxide io_uring runtime: pooled ring-native connections per reactor, ring-native connect and handshake, inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.redis/ioxide.redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.redis</RootNamespace>

<PackageId>ioxide.redis</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
29 changes: 29 additions & 0 deletions src/ioxide/Connection/Quic/QuicConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ protected QuicConnection(int recvQueueEntries = 256)
/// </summary>
public abstract void OnDatagram(ReadOnlySpan<byte> payload, byte tos);

/// <summary>
/// The same delivery, plus the address it actually came FROM. An engine that supports
/// connection migration needs this: a peer that changed network sends on the same connection
/// id from a new address, and the transport is the only layer that can see the difference.
/// </summary>
/// <remarks>
/// Virtual rather than abstract, forwarding to the two-argument form, so a binding that does
/// not care about paths - and every existing subclass - keeps working untouched. The address
/// points into the recv slot and is valid only for the duration of the call; anything keeping
/// it must copy.
///
/// Being told an address is NOT permission to answer it freely. An engine that adopts a peer
/// address on the strength of a datagram claiming it becomes an amplification reflector for
/// whoever spoofed it, so an engine must bound what it sends on a path until that path has
/// been validated - which is what QUIC's PATH_CHALLENGE and the 3x anti-amplification limit
/// are for. Adoption and validation are not the same event and do not happen in that order.
/// </remarks>
public virtual void OnDatagram(ReadOnlySpan<byte> payload, byte tos, nint peerAddr, int peerAddrLen)
=> OnDatagram(payload, tos);

/// <summary>Next engine deadline in <see cref="Environment.TickCount64"/> ms; long.MaxValue = none.</summary>
public abstract long GetNextTimeout(long nowMs);

Expand Down Expand Up @@ -126,6 +146,15 @@ protected void Send(ReadOnlySpan<byte> payload, int gsoSegmentSize = 0)
/// <summary>Adopt a validated peer migration (copies the sockaddr out of the datagram).</summary>
public unsafe void UpdatePeerAddress(nint addr, int addrLen)
{
// Same guard Send() carries, and for the same reason: QuicRemoveConnection frees this
// block and zeroes the field, so a late call must not write through it. A null destination
// here would be an access violation rather than an exception, which no catch upstream
// could contain.
if (PeerAddr == 0 || addr == 0 || addrLen <= 0 || addrLen > Reactor.UdpNameCap)
{
return;
}

Buffer.MemoryCopy((void*)addr, (void*)PeerAddr, Reactor.UdpNameCap, addrLen);
PeerAddrLen = addrLen;
}
Expand Down
4 changes: 2 additions & 2 deletions src/ioxide/Reactor/Transport/Quic/Reactor.Quic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private void QuicDispatchDatagram(in UdpDatagram datagram)
if (_quicConns.TryGetValue(dcid, out QuicConnection? conn))
{
conn.LastSeenMs = Environment.TickCount64;
conn.OnDatagram(datagram.Payload, datagram.Tos);
conn.OnDatagram(datagram.Payload, datagram.Tos, datagram.PeerAddr, datagram.PeerAddrLen);
QuicArmTimer(conn); // reads/handler sends (inline above) moved the engine deadline
return;
}
Expand Down Expand Up @@ -141,7 +141,7 @@ private void QuicDispatchDatagram(in UdpDatagram datagram)
freshQuicConnection.DecRef(); // no handler configured: the transport stays the only owner
}

freshQuicConnection.OnDatagram(datagram.Payload, datagram.Tos);
freshQuicConnection.OnDatagram(datagram.Payload, datagram.Tos, datagram.PeerAddr, datagram.PeerAddrLen);
QuicArmTimer(freshQuicConnection);
}

Expand Down
2 changes: 1 addition & 1 deletion src/ioxide/ioxide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide</RootNamespace>

<PackageId>ioxide</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http2/ioxide.http2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http2</RootNamespace>

<PackageId>ioxide.http2</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http3/ioxide.http3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http3</RootNamespace>

<PackageId>ioxide.http3</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp2</RootNamespace>

<PackageId>ioxide.nghttp2</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp3</RootNamespace>

<PackageId>ioxide.nghttp3</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,38 @@ internal static void CbHandshakeCompleted(void* user)
catch (Exception e) { c.OnCallbackFault(e, nameof(CbHandshakeCompleted)); }
}

/// <summary>
/// ngtcp2 moved this connection to a new peer address, and the transport must follow - it owns
/// the socket, which is the half ngtcp2 cannot do for itself.
///
/// It is worth being exact about what this does NOT say. The address is not validated when
/// this fires: ngtcp2 adopts the current path on the first non-probing 1-RTT packet from a new
/// address and validates afterwards. Safety comes from the packet having decrypted under 1-RTT
/// keys - which an off-path attacker cannot forge - and from ngtcp2's own anti-amplification
/// limit, which caps what it will send on a path that has not validated yet.
/// </summary>
[UnmanagedCallersOnly]
internal static void CbPathChange(void* user, void* remoteAddr, nuint len)
{
QuicEngineConnection? c = From(user);
if (c is null)
{
return;
}

try
{
// Flush FIRST. Anything already coalesced in the GSO batch was built for the address
// in force when it was queued, and this call is what changes that address - sending
// the batch afterwards would deliver those datagrams to a different peer address than
// ngtcp2 addressed them to. Egress.cs states the batch is single-destination "by
// construction"; that holds only because this flush keeps it true.
c.FlushBatchBeforePathChange();
c.UpdatePeerAddress((nint)remoteAddr, (int)len);
}
catch (Exception e) { c.OnCallbackFault(e, nameof(CbPathChange)); }
}

[UnmanagedCallersOnly]
internal static void CbNewCid(void* user, byte* cid, nuint len)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ public unsafe partial class QuicEngineConnection
// One sendmsg per engine cycle instead of one per datagram: ngtcp2 emits runs of equal-size
// (MTU-full) datagrams under load, which is exactly the UDP_SEGMENT shape. A shorter datagram
// may only END a batch (GSO semantics: equal segments, the last may be short). All datagrams
// in a batch are this connection's, so the destination is single by construction.
// in a batch are this connection's, and the batch is flushed whenever the peer address moves
// (FlushBatchBeforePathChange), so the destination is single for the life of a batch. That
// qualifier matters since migration landed: a connection can have more than one live path
// while one is being validated, and ngtcp2 addresses a PATH_RESPONSE to the path its
// challenge arrived on rather than to the current one.

private readonly byte[] _gsoBuf = new byte[63 * 1024]; // < 65507, the UDP payload ceiling
private int _gsoLen;
Expand Down Expand Up @@ -89,6 +93,12 @@ private void QueueSend(ReadOnlySpan<byte> datagram)
}
}

/// <summary>
/// Send what is queued before the peer address moves. The batch is addressed at send time, not
/// at queue time, so a path change with datagrams still coalesced would readdress them.
/// </summary>
internal void FlushBatchBeforePathChange() => FlushGso();

private void FlushGso()
{
if (_gsoLen == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ namespace ioxide.ngtcp2;
/// <summary>Ingress: datagrams routed by the transport are fed to ngtcp2 here.</summary>
public unsafe partial class QuicEngineConnection
{
/// <summary>Kept for callers that have no address to give - feeds ngtcp2 the path it already has.</summary>
public override void OnDatagram(ReadOnlySpan<byte> payload, byte tos)
=> OnDatagram(payload, tos, 0, 0);

public override void OnDatagram(ReadOnlySpan<byte> payload, byte tos, nint peerAddr, int peerAddrLen)
{
if (_closed)
{
Expand All @@ -12,24 +16,29 @@ public override void OnDatagram(ReadOnlySpan<byte> payload, byte tos)
_inEngineCycle = true;
try
{
OnDatagramCore(payload, tos);
OnDatagramCore(payload, tos, peerAddr, peerAddrLen);
}
finally
{
EndEngineCycle();
}
}

private void OnDatagramCore(ReadOnlySpan<byte> payload, byte tos)
private void OnDatagramCore(ReadOnlySpan<byte> payload, byte tos, nint peerAddr, int peerAddrLen)
{
// One call = one wire datagram: the transport pre-splits GRO trains before demux.
int rv;

// TODO: can we get the byte* without a fixed
fixed (byte* p = payload)
{
// milestone: no migration - the path is fixed at accept, so remote_sa is unused.
rv = Ngtcp2.iq_conn_read(_conn, null, 0, p, (nuint)payload.Length, tos, NowNs());
// The address this datagram really came from. ngtcp2 compares it against the path in
// force and, when they differ, validates the new one with PATH_CHALLENGE before
// adopting it - so passing it is not "trust the sender", it is giving the library the
// input its own migration logic needs. Zero means the caller had none, and the path
// it already holds is used.
rv = Ngtcp2.iq_conn_read(_conn, (void*)peerAddr, (nuint)peerAddrLen,
p, (nuint)payload.Length, tos, NowNs());
}
if (rv != 0)
{
Expand Down
1 change: 1 addition & 0 deletions src/protocols/ioxide.ngtcp2/Engine/QuicClientEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public QuicClientEngine(string alpn = "h3")
// too (without them, send-retention would never be freed).
var callbacks = new Ngtcp2.Callbacks
{
StructSize = (nuint)sizeof(Ngtcp2.Callbacks),
OnStreamData = &QuicEngineConnection.CbStreamData,
OnStreamClose = &QuicEngineConnection.CbStreamClose,
OnHandshakeCompleted = &QuicEngineConnection.CbHandshakeCompleted,
Expand Down
2 changes: 2 additions & 0 deletions src/protocols/ioxide.ngtcp2/Engine/QuicEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public QuicEngine(string certPemPath, string keyPemPath, uint cidLength = 8, str

var callbacks = new Ngtcp2.Callbacks
{
StructSize = (nuint)sizeof(Ngtcp2.Callbacks),
OnStreamData = &QuicEngineConnection.CbStreamData,
OnStreamClose = &QuicEngineConnection.CbStreamClose,
OnHandshakeCompleted = &QuicEngineConnection.CbHandshakeCompleted,
Expand All @@ -145,6 +146,7 @@ public QuicEngine(string certPemPath, string keyPemPath, uint cidLength = 8, str
OnStreamReset = &QuicEngineConnection.CbStreamReset,
OnStreamStopSending = &QuicEngineConnection.CbStreamStopSending,
OnAckedStreamData = &QuicEngineConnection.CbAckedStreamData,
OnPathChange = &QuicEngineConnection.CbPathChange,
};

byte[] alpnWire = AlpnWire(alpn);
Expand Down
11 changes: 11 additions & 0 deletions src/protocols/ioxide.ngtcp2/Interop/Ngtcp2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ internal static unsafe class Ngtcp2
[StructLayout(LayoutKind.Sequential)]
internal struct Callbacks
{
/// <summary>sizeof(iq_callbacks) as THIS side compiled it. The shim refuses a table it
/// does not recognise, because the struct is passed by value and mirrored by hand - a
/// member added on one side only is otherwise a silent read past the caller's buffer.</summary>
public nuint StructSize;

public delegate* unmanaged<void*, long, byte*, nuint, int, void> OnStreamData;
public delegate* unmanaged<void*, long, ulong, void> OnStreamClose;
public delegate* unmanaged<void*, void> OnHandshakeCompleted;
Expand All @@ -27,6 +32,12 @@ internal struct Callbacks
public delegate* unmanaged<void*, long, ulong, void> OnStreamReset;
public delegate* unmanaged<void*, long, ulong, void> OnStreamStopSending;
public delegate* unmanaged<void*, long, ulong, ulong, void> OnAckedStreamData;

/// <summary>ngtcp2 moved this connection to a new peer address. NOT a statement that the
/// address was validated first - adoption happens on the first non-probing 1-RTT packet
/// from it, and validation follows. The protection is ngtcp2's 3x anti-amplification limit
/// on an unvalidated path, plus the packet having decrypted under 1-RTT keys.</summary>
public delegate* unmanaged<void*, void*, nuint, void> OnPathChange;
}


Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.ngtcp2</RootNamespace>

<PackageId>ioxide.ngtcp2</PackageId>
<Version>0.6.208</Version>
<Version>0.7.209</Version>
<Authors>MDA2AV</Authors>
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Loading
Loading