Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
356d8dc
Ioxide engine: ioxide 0.4.161, all endpoints served, native TLS termi…
MDA2AV Aug 8, 2026
383800a
Ioxide engine: resolve certificates lazily, refuse handshakes without…
MDA2AV Aug 8, 2026
57e0e0d
Ioxide engine: ioxide 0.4.165, and kernelTx / kernelRx kTLS options
MDA2AV Aug 9, 2026
dfadfde
Ioxide engine: address Sonar findings (discard shutdown() result, gua…
MDA2AV Aug 9, 2026
c903ca8
IoxideFiles: ioxide.file 0.4.169, and keep serving edited files
MDA2AV Aug 9, 2026
68ff18b
Playground: static served two ways, so the difference can be measured
MDA2AV Aug 9, 2026
f55db2b
Drop nuget.config: ioxide 0.4.169 is on nuget.org
MDA2AV Aug 9, 2026
b8f4399
feat(ioxide): HTTP/2 and HTTP/3, streamed both ways, with mutual TLS
MDA2AV Aug 15, 2026
a0e409c
feat(ioxide): protocols are configured per port
MDA2AV Aug 16, 2026
c0dde9a
docs(ioxide): say that a port can serve HTTP/1.1 and HTTP/2 together
MDA2AV Aug 16, 2026
282c1b7
feat(ioxide): every protocol combination, including HTTP/3 without TCP
MDA2AV Aug 16, 2026
54b049b
docs(ioxide): a port per protocol combination in the sample
MDA2AV Aug 16, 2026
fde5f0c
docs(ioxide): a live port for every protocol combination, no switches
MDA2AV Aug 16, 2026
8c8eca8
docs(ioxide): one host, one combination per port
MDA2AV Aug 16, 2026
9a4aff1
refactor(ioxide): group the options, and show HTTP/1.1 behind mutual TLS
MDA2AV Aug 16, 2026
8f0a17d
docs(ioxide): say which certificate HTTP/3 serves
MDA2AV Aug 16, 2026
c72045e
fix(ioxide): warn when the HTTP/3 certificate is not the endpoint's
MDA2AV Aug 16, 2026
aa4bc12
fix(playground): owner-only private keys, and no key left in /tmp
MDA2AV Aug 16, 2026
e427a54
fix(ioxide): the engine no longer writes a certificate out for HTTP/3
MDA2AV Aug 16, 2026
3c90727
refactor(ioxide): name the shared HTTP/2 and HTTP/3 code for what it is
MDA2AV Aug 16, 2026
a44b5df
refactor(ioxide): split the HTTP/1.1 loop out of ConnectionDriver, dr…
MDA2AV Aug 16, 2026
7f7ad75
docs(ioxide): compact the engine comments
MDA2AV Aug 16, 2026
8db9c0f
refactor(ioxide): group the Protocol folder by protocol family
MDA2AV Aug 16, 2026
941a380
refactor(ioxide): move kernel TLS into IoxideOptions.Tcp
MDA2AV Aug 16, 2026
7630fe4
docs(playground): show the new IoxideOptions.Tcp group
MDA2AV Aug 16, 2026
6967e4c
docs(playground): show both kernel TLS knobs, with what enabling them…
MDA2AV Aug 16, 2026
a4b1a8e
feat(ioxide): configure the reactors through IoxideOptions
MDA2AV Aug 16, 2026
e0cf7d5
refactor(ioxide): drop the configure hook, options carry their own de…
MDA2AV Aug 16, 2026
e162d4a
refactor(ioxide): remove the connectionFactory seam
MDA2AV Aug 16, 2026
d430395
refactor(ioxide): keep the h3 options with the rest of the QUIC state
MDA2AV Aug 16, 2026
aa3fdf7
refactor(ioxide): give the TCP listener its own partial, matching QUIC
MDA2AV Aug 16, 2026
0b3cb42
fix(ioxide): start from no listeners, so an HTTP/3-only server cannot…
MDA2AV Aug 16, 2026
51505b6
refactor(ioxide): resolve the TCP listener in the constructor, like QUIC
MDA2AV Aug 16, 2026
5900d87
refactor(ioxide): let each transport resolve itself out of the constr…
MDA2AV Aug 16, 2026
0b61125
refactor(ioxide): Infrastructure/Endpoints, matching the Internal engine
MDA2AV Aug 16, 2026
636d9ce
refactor(ioxide): Server.cs holds class Host, so name it Host.cs
MDA2AV Aug 16, 2026
859fab4
refactor(ioxide): drop the Ioxide prefix from the infrastructure types
MDA2AV Aug 16, 2026
414561d
refactor(ioxide): EngineOptions, and drop the prefix from the option …
MDA2AV Aug 16, 2026
c1c9afb
refactor(ioxide): put the TLS handshake with its caller, drop the Tls…
MDA2AV Aug 16, 2026
3f83dc8
refactor(ioxide): name the TCP field for what it holds, and stop stor…
MDA2AV Aug 17, 2026
20ac31c
docs(ioxide): say what the dual-stack check is for, and name what dis…
MDA2AV Aug 17, 2026
30d2ad5
refactor(ioxide): let an endpoint carry its own security
MDA2AV Aug 17, 2026
d483324
fix(ioxide): finish the prefix rename where it was left half done
MDA2AV Aug 17, 2026
aa9ee12
refactor(ioxide): drop _primary, the endpoints already hold the first…
MDA2AV Aug 17, 2026
430e98c
refactor(ioxide): name the two injected records, and follow the house…
MDA2AV Aug 17, 2026
5b7a6fe
refactor(ioxide): drop the port table, the endpoints array already an…
MDA2AV Aug 17, 2026
ae3050e
refactor(ioxide): let an endpoint carry its own protocols
MDA2AV Aug 17, 2026
dfc0e1a
refactor(ioxide): keep _tcpPorts with the transport that resolves it
MDA2AV Aug 17, 2026
0816304
refactor(ioxide): split the endpoint in two, and let the secure one c…
MDA2AV Aug 17, 2026
4c13d48
feat(ioxide): client certificates per port, so two secure ports can t…
MDA2AV Aug 17, 2026
4fea67c
refactor(ioxide): mutual TLS comes off the binding, not the engine op…
MDA2AV Aug 17, 2026
64f8022
fix(ioxide): send the certificate's chain, not only its leaf
MDA2AV Aug 17, 2026
3461cf6
fix(ioxide): refuse an endpoint that requires a client certificate bu…
MDA2AV Aug 17, 2026
9d6ab1d
docs(ioxide): say what each transport takes a certificate as, and whe…
MDA2AV Aug 17, 2026
eaec0a5
fix(ioxide): 0.5.192, and client anchors as PEM text reach HTTP/3 too
MDA2AV Aug 17, 2026
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
152 changes: 152 additions & 0 deletions Engine/Ioxide/EngineOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using ioxide;

namespace GenHTTP.Engine.Ioxide;

/// <summary>
/// Protocol and TLS options for the ioxide engine. The port, its certificate and whether it asks
/// for a client certificate stay on <c>Bind</c>; which protocols it then serves lives here.
/// </summary>
public sealed record EngineOptions
{
internal static readonly EngineOptions Default = new();

/// <summary>
/// The protocols every endpoint serves, unless <see cref="ProtocolsByPort"/> says otherwise.
/// An endpoint bound with <c>enableQuic</c> serves HTTP/3 whatever is set here.
/// </summary>
public Protocols Protocols { get; init; } = Protocols.Http1;

/// <summary>
/// Protocols for one port, overriding <see cref="Protocols"/> - bind the ports, then name the
/// ones that differ: <c>{ [8081] = Protocols.Http2, [8443] = Protocols.All }</c>.
/// </summary>
public Dictionary<ushort, Protocols> ProtocolsByPort { get; init; } = [];

/// <summary>The reactors: how many, and the io_uring machinery each one owns.</summary>
public ReactorOptions Reactor { get; init; } = new();

/// <summary>The TCP endpoints: how TLS is terminated for HTTP/1.1 and HTTP/2.</summary>
public TcpTransportOptions Tcp { get; init; } = new();

/// <summary>The HTTP/3 endpoint: the certificate QUIC serves, and QPACK.</summary>
public Http3Options Http3 { get; init; } = new();
}

/// <summary>
/// The reactors. Each runs on its own thread, owns an io_uring ring and the connections accepted
/// on it, and shares nothing with the others - so these are per reactor, not per server, and the
/// memory they describe is multiplied by <see cref="ReactorCount"/>.
/// </summary>
public sealed record ReactorOptions
{
/// <summary>
/// How many reactors to run. One per core suits a server with the machine to itself; anything
/// sharing the box - a colocated load generator, a database, sibling containers - wants fewer,
/// or the reactors and everything else fight for the same cores.
/// </summary>
public int ReactorCount { get; init; } = Environment.ProcessorCount;

/// <summary>io_uring submission and completion queue depth, per reactor.</summary>
public uint RingEntries { get; init; } = 8192;

/// <summary>
/// Bytes per buffer in the shared recv ring. Larger reads more per completion and wastes more
/// per idle connection. Unused when <see cref="Incremental"/> is set.
/// </summary>
public int RecvBufferSize { get; init; } = 32 * 1024;

/// <summary>
/// Buffers in the shared recv ring. Running out costs a retry, not a lost byte. Unused when
/// <see cref="Incremental"/> is set.
/// </summary>
public int RecvSlots { get; init; } = 4096;

/// <summary>
/// Give each connection its own small buffer ring (IOU_PBUF_RING_INC, kernel 6.12+) instead of
/// drawing from the shared one. Setting this IS enabling the mode, and the two shared-ring
/// knobs above then go unused. Reserves MaxConnections x RecvSlots x RecvBufferSize per
/// reactor up front, so it trades memory for not sharing a ring between connections.
/// </summary>
public IncrementalOptions? Incremental { get; init; }
}

/// <summary>
/// The TCP endpoints, where OpenSSL terminates TLS for HTTP/1.1 and HTTP/2. HTTP/3 is not
/// configured here: QUIC carries its own TLS 1.3 inside ngtcp2, so none of this reaches it.
/// </summary>
public sealed record TcpTransportOptions
{
/// <summary>
/// Produce TLS records in the kernel (kTLS) on the send path instead of in OpenSSL, which
/// still drives the handshake. Requires the Linux <c>tls</c> module and TLS 1.3.
/// </summary>
public bool TxKernelTls { get; init; }

/// <summary>
/// Decrypt TLS records in the kernel on the receive path. Experimental, and requires
/// <see cref="TxKernelTls"/> - RX shares the ULP handoff TX installs, so ioxide refuses RX
/// alone. The peer must send no post-handshake control records.
/// </summary>
public bool RxKernelTls { get; init; }

/// <summary>
/// listen() backlog per reactor - the accept queue that absorbs a burst of connections. Every
/// reactor binds its own SO_REUSEPORT listener, so the server absorbs this many per reactor.
/// </summary>
public int ListenBacklog { get; init; } = 1024;

/// <summary>
/// Bytes of write buffer per connection. A response that fits leaves in one send; a larger one
/// is handled by <see cref="WriteOverflow"/>.
/// </summary>
public int WriteSlabSize { get; init; } = 16 * 1024;

/// <summary>
/// What a response larger than <see cref="WriteSlabSize"/> does: grow the slab and keep one
/// send, or chain pooled slabs and flush them with one vectored sendmsg instead of reallocating.
/// </summary>
public WriteOverflowStrategy WriteOverflow { get; init; } = WriteOverflowStrategy.Grow;

/// <summary>Connections kept pooled per reactor for reuse rather than freed.</summary>
public int PoolMax { get; init; } = 1024;

/// <summary>
/// Send responses with zero-copy (IORING_OP_SEND_ZC) instead of a normal send. Trades the
/// in-kernel payload copy for page pinning and a second completion per send, so it only pays
/// for large responses. kTLS connections always fall back to a plain send.
/// </summary>
public bool ZeroCopySend { get; init; }

/// <summary>
/// Depth of the per-connection recv queue, a power of two. Overflow closes the connection.
/// </summary>
public int RecvQueueEntries { get; init; } = 64;
}

/// <summary>
/// The HTTP/3 endpoint. Only consulted when a port serves <see cref="Protocols.Http3"/>.
/// </summary>
public sealed record Http3Options
{
/// <summary>
/// PEM certificate chain for the HTTP/3 listener, as a path. Required to serve HTTP/3, and
/// should be the certificate bound to the endpoint rather than a second one - it is named
/// separately only because ngtcp2 loads PEM by path, and the engine writes none for you.
/// </summary>
public string? CertificatePath { get; init; }

/// <summary>PEM private key for the HTTP/3 listener. Pairs with <see cref="CertificatePath"/>.</summary>
public string? KeyPath { get; init; }

/// <summary>
/// Bytes of QPACK dynamic table advertised to HTTP/3 clients. 0 keeps every header literal
/// against the static table, which costs bytes but can never stall a stream on a table update.
/// </summary>
public long QpackDynamicTableCapacity { get; init; }

/// <summary>
/// How many HTTP/3 streams may wait on a QPACK table insertion. Only meaningful alongside a
/// nonzero <see cref="QpackDynamicTableCapacity"/>, and the price paid for one.
/// </summary>
public long QpackBlockedStreams { get; init; }
}
12 changes: 8 additions & 4 deletions Engine/Ioxide/GenHTTP.Engine.Ioxide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

<PropertyGroup>

<TargetFrameworks>net11.0</TargetFrameworks>
<TargetFrameworks>net11.0;net10.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\API\GenHTTP.Api.csproj" />
<ProjectReference Include="..\Shared\GenHTTP.Engine.Shared.csproj" />
<PackageReference Include="ioxide" Version="0.1.1" />
<PackageReference Include="ioxide.tls" Version="0.1.1" />
<PackageReference Include="ioxide" Version="0.5.192" />
<!-- HTTP/2 in pure C# (framing, HPACK, flow control); HTTP/3 as ngtcp2 (QUIC) plus nghttp3
(H3 + QPACK, including a static-table response encoder). -->
<PackageReference Include="ioxide.http2" Version="0.5.192" />
<PackageReference Include="ioxide.ngtcp2" Version="0.5.192" />
<PackageReference Include="ioxide.nghttp3" Version="0.5.192" />
<PackageReference Include="Glyph11" Version="0.3.6" />
<!-- Parallel parse path for benchmarking — GENHTTP_IOXIDE_PARSER=pico (see ConnectionDriver). -->
<!-- Parallel parse path for benchmarking — GENHTTP_IOXIDE_PARSER=pico (see Http1Driver). -->
<PackageReference Include="Glyph11.Pico" Version="0.0.1" />
</ItemGroup>

Expand Down
25 changes: 25 additions & 0 deletions Engine/Ioxide/Host.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using GenHTTP.Api.Infrastructure;

using GenHTTP.Engine.Ioxide.Infrastructure;
using ioxide;

namespace GenHTTP.Engine.Ioxide;

/// <summary>
/// Entry point to host an application using the ioxide io_uring engine.
/// </summary>
public static class Host
{

/// <param name="onReactorStart">
/// Registers ring-native services on each reactor's own thread before it serves, e.g.
/// <c>r => PgPool.Start(r, pgOptions)</c>. Handlers resolve them via <c>IoxideReactor.Current</c>.
/// </param>
/// <param name="options">
/// Everything the engine is tuned by: the reactors, the TCP transport, protocols per port,
/// the HTTP/3 certificate, mutual TLS and QPACK. Ports and certificates stay on <c>Bind</c>.
/// </param>
public static IServerHost Create(Action<Reactor>? onReactorStart = null, EngineOptions? options = null)
=> new ServerHost(onReactorStart, options);

}
30 changes: 0 additions & 30 deletions Engine/Ioxide/Hosting/IoxideEndPoint.cs

This file was deleted.

Loading
Loading