diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f159245..a7ab17d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -39,6 +39,9 @@ jobs:
- name: Pack terraform
run: dotnet pack terraform/terraform.csproj --configuration Release --no-build --output ./artifacts
+
+ - name: Pack Twinflow
+ run: dotnet pack Twinflow/Twinflow.csproj --configuration Release --no-build --output ./artifacts
- name: Publish to NuGet
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
diff --git a/KestrelShrike.Demo/KestrelShrike.Demo.csproj b/KestrelShrike.Demo/KestrelShrike.Demo.csproj
new file mode 100644
index 0000000..3be18a9
--- /dev/null
+++ b/KestrelShrike.Demo/KestrelShrike.Demo.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net10.0
+ enable
+ enable
+ true
+ true
+
+
+
+
+
+
+
diff --git a/KestrelShrike.Demo/Program.cs b/KestrelShrike.Demo/Program.cs
new file mode 100644
index 0000000..0a24309
--- /dev/null
+++ b/KestrelShrike.Demo/Program.cs
@@ -0,0 +1,23 @@
+using Microsoft.Extensions.Logging;
+using KestrelShrike;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Logging.SetMinimumLevel(LogLevel.Warning); // benchmark: silence per-request logs
+
+builder.WebHost.UseKestrel(kestrel =>
+{
+ kestrel.ListenAnyIP(8080);
+});
+
+// SHRIKE=0 → Kestrel's default Socket transport (baseline). Otherwise the epoll Shrike transport.
+if (Environment.GetEnvironmentVariable("SHRIKE") != "0")
+{
+ builder.WebHost.UseShrike(opts => opts.ReactorCount = Math.Max(1, 4));
+}
+
+var app = builder.Build();
+
+app.MapGet("/", () => "Hello from Shrike + Kestrel\n");
+
+app.Run();
diff --git a/KestrelShrike/EpollConnection.cs b/KestrelShrike/EpollConnection.cs
new file mode 100644
index 0000000..6ea040d
--- /dev/null
+++ b/KestrelShrike/EpollConnection.cs
@@ -0,0 +1,167 @@
+namespace KestrelShrike;
+
+///
+/// One TCP connection bridged to Kestrel through two BCL Pipes:
+/// - Input: the reactor drains recv into Input.Writer; Kestrel reads Input.Reader.
+/// - Output: Kestrel writes Output.Writer; the per-connection pump reads Output.Reader
+/// and sends — DIRECTLY from the thread-pool thread, because an epoll
+/// socket's send() is thread-safe. No reactor handoff (unlike io_uring's
+/// single-issuer ring). The reactor is only involved on EAGAIN (arm
+/// EPOLLOUT, signal the pump when writable).
+/// Lifetime: 2-ref count (reactor/recv side + pump side); the fd closes when both end.
+///
+internal sealed class EpollConnection
+{
+ public readonly int Fd;
+ public readonly int Ep;
+ private readonly EpollReactor _reactor;
+
+ public readonly Pipe Input;
+ public readonly Pipe Output;
+
+ private TaskCompletionSource? _writable; // set while the pump waits for EPOLLOUT
+ private int _refs = 2;
+ private int _closed;
+
+ private const int RecvChunk = 16 * 1024;
+
+ public EpollConnection(int fd, int ep, EpollReactor reactor)
+ {
+ Fd = fd;
+ Ep = ep;
+ _reactor = reactor;
+ var o = new PipeOptions(pauseWriterThreshold: 0, resumeWriterThreshold: 0, useSynchronizationContext: false);
+ Input = new Pipe(o);
+ Output = new Pipe(o);
+ }
+
+ public bool IsClosed => Volatile.Read(ref _closed) != 0;
+
+ // ---- recv (reactor thread): drain into Input.Writer. False => peer closed / error. ----
+ public unsafe bool OnReadable()
+ {
+ if (IsClosed) return false;
+
+ bool any = false;
+ bool ok = true;
+ while (true)
+ {
+ Span span = Input.Writer.GetSpan(RecvChunk);
+ long n;
+ fixed (byte* p = span) n = recv(Fd, p, (ulong)span.Length, 0);
+
+ if (n > 0) { Input.Writer.Advance((int)n); any = true; continue; }
+ if (n == 0) { ok = false; break; } // peer closed
+
+ int err = Marshal.GetLastPInvokeError();
+ if (err is EAGAIN or EWOULDBLOCK) break; // drained
+ if (err == EINTR) continue;
+ ok = false; break; // hard error
+ }
+
+ if (any) _ = Input.Writer.FlushAsync();
+ return ok;
+ }
+
+ // ---- output pump (thread pool) ----
+ public async Task RunOutputPump()
+ {
+ PipeReader reader = Output.Reader;
+ try
+ {
+ while (true)
+ {
+ ReadResult r = await reader.ReadAsync().ConfigureAwait(false);
+ if (r.IsCanceled) break;
+
+ ReadOnlySequence buf = r.Buffer;
+ bool fail = false;
+
+ foreach (ReadOnlyMemory seg in buf)
+ {
+ int off = 0;
+ while (off < seg.Length)
+ {
+ int sent = TrySend(seg.Span.Slice(off), out bool wouldBlock, out bool closed);
+ if (closed) { fail = true; break; }
+ if (sent > 0) { off += sent; continue; }
+ if (wouldBlock && !await WaitWritableAsync().ConfigureAwait(false)) { fail = true; break; }
+ // EINTR (sent == 0, not wouldBlock, not closed) just retries
+ }
+ if (fail) break;
+ }
+
+ reader.AdvanceTo(buf.End);
+ if (fail || r.IsCompleted) break;
+ }
+ }
+ catch { /* connection died mid-send */ }
+ finally { try { reader.Complete(); } catch { } DecRef(); }
+ }
+
+ private unsafe int TrySend(ReadOnlySpan data, out bool wouldBlock, out bool closed)
+ {
+ wouldBlock = false;
+ closed = false;
+ if (data.IsEmpty) return 0;
+
+ long n;
+ fixed (byte* p = data) n = send(Fd, p, data.Length, MSG_NOSIGNAL);
+ if (n > 0) return (int)n;
+
+ int err = (n == 0) ? EAGAIN : Marshal.GetLastPInvokeError();
+ if (err is EAGAIN or EWOULDBLOCK) { wouldBlock = true; return 0; }
+ if (err == EINTR) return 0;
+ closed = true;
+ return 0;
+ }
+
+ // ---- EAGAIN: arm EPOLLOUT and wait for the reactor's writable signal ----
+ private Task WaitWritableAsync()
+ {
+ if (IsClosed) return Task.FromResult(false);
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ Volatile.Write(ref _writable, tcs);
+ ArmEpollOut(); // epoll_ctl is thread-safe
+ if (IsClosed) tcs.TrySetResult(false); // raced with close
+ return tcs.Task;
+ }
+
+ public void SignalWritable() // reactor: EPOLLOUT fired
+ {
+ TaskCompletionSource? tcs = Interlocked.Exchange(ref _writable, null);
+ if (tcs is not null)
+ {
+ ArmEpollIn();
+ tcs.TrySetResult(true);
+ }
+ }
+
+ public void MarkClosed() // reactor thread: completes Input.Writer (sole writer)
+ {
+ if (Interlocked.Exchange(ref _closed, 1) == 1) return;
+ try { Input.Writer.Complete(); } catch { }
+ Interlocked.Exchange(ref _writable, null)?.TrySetResult(false); // unblock the pump
+ }
+
+ public void DecRef()
+ {
+ if (Interlocked.Decrement(ref _refs) != 0) return;
+ _reactor.Remove(this);
+ close(Fd);
+ }
+
+ private unsafe void ArmEpollOut()
+ {
+ byte* ev = stackalloc byte[EvSize];
+ WriteEpollEvent(ev, (uint)(EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLERR | EPOLLHUP) | EPOLLET, Fd);
+ epoll_ctl(Ep, EPOLL_CTL_MOD, Fd, (IntPtr)ev);
+ }
+
+ private unsafe void ArmEpollIn()
+ {
+ byte* ev = stackalloc byte[EvSize];
+ WriteEpollEvent(ev, (uint)(EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP) | EPOLLET, Fd);
+ epoll_ctl(Ep, EPOLL_CTL_MOD, Fd, (IntPtr)ev);
+ }
+}
diff --git a/KestrelShrike/EpollEngine.cs b/KestrelShrike/EpollEngine.cs
new file mode 100644
index 0000000..34efcab
--- /dev/null
+++ b/KestrelShrike/EpollEngine.cs
@@ -0,0 +1,36 @@
+using System.Threading.Channels;
+
+namespace KestrelShrike;
+
+/// Owns N epoll reactors (each with its own SO_REUSEPORT listener) and funnels accepted connections to Kestrel.
+internal sealed class EpollEngine
+{
+ private readonly EpollReactor[] _reactors;
+ private readonly Channel _accepted =
+ Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false });
+
+ public EpollEngine(ushort port, int reactorCount, int backlog, int maxEvents)
+ {
+ _reactors = new EpollReactor[reactorCount];
+ for (int i = 0; i < reactorCount; i++)
+ _reactors[i] = new EpollReactor(i, port, backlog, maxEvents) { OnAccept = c => _accepted.Writer.TryWrite(c) };
+ }
+
+ public void Start()
+ {
+ for (int i = 0; i < _reactors.Length; i++)
+ {
+ int idx = i;
+ var t = new Thread(() => _reactors[idx].Run()) { IsBackground = true, Name = $"shrike-k-r{idx}" };
+ t.Start();
+ }
+ }
+
+ public ValueTask AcceptAsync(CancellationToken ct) => _accepted.Reader.ReadAsync(ct);
+
+ public void Stop()
+ {
+ _accepted.Writer.TryComplete();
+ foreach (EpollReactor r in _reactors) r.Stop();
+ }
+}
diff --git a/KestrelShrike/EpollReactor.cs b/KestrelShrike/EpollReactor.cs
new file mode 100644
index 0000000..2d7a900
--- /dev/null
+++ b/KestrelShrike/EpollReactor.cs
@@ -0,0 +1,144 @@
+namespace KestrelShrike;
+
+///
+/// One reactor = one thread + one epoll instance + its own SO_REUSEPORT listener
+/// (Minima/Shrike topology — kernel balances accepts, no acceptor thread). It is
+/// purely a readiness driver: EPOLLIN → drain recv into the connection's input
+/// Pipe (Kestrel reads it); EPOLLOUT → wake a pump that hit EAGAIN; error/hup →
+/// close. Response sends happen on the pump's thread, not here.
+///
+internal sealed unsafe class EpollReactor
+{
+ public readonly int Id;
+ private readonly ushort _port;
+ private readonly int _backlog;
+ private readonly int _maxEvents;
+
+ private int _ep;
+ private int _listenFd;
+ private readonly ConcurrentDictionary _conns = new();
+ private volatile bool _running = true;
+
+ internal Action? OnAccept;
+
+ public EpollReactor(int id, ushort port, int backlog, int maxEvents)
+ {
+ Id = id;
+ _port = port;
+ _backlog = backlog;
+ _maxEvents = maxEvents;
+ }
+
+ public void Stop() => _running = false;
+
+ internal void Remove(EpollConnection conn) =>
+ _conns.TryRemove(new KeyValuePair(conn.Fd, conn));
+
+ public void Run()
+ {
+ _ep = epoll_create1(EPOLL_CLOEXEC);
+ if (_ep < 0) throw new Exception("epoll_create1 failed");
+ _listenFd = OpenReusePortListener(_port, _backlog);
+
+ byte* lev = stackalloc byte[EvSize];
+ WriteEpollEvent(lev, (uint)(EPOLLIN | EPOLLERR | EPOLLHUP), _listenFd);
+ if (epoll_ctl(_ep, EPOLL_CTL_ADD, _listenFd, (IntPtr)lev) != 0)
+ throw new Exception("epoll_ctl ADD listen failed");
+
+ IntPtr eventsBuf = Marshal.AllocHGlobal(EvSize * _maxEvents);
+ Console.WriteLine($"[shrike-k r{Id}] listening on 0.0.0.0:{_port}");
+
+ while (_running)
+ {
+ int n = epoll_wait(_ep, eventsBuf, _maxEvents, -1);
+ if (n < 0) { if (Marshal.GetLastPInvokeError() == EINTR) continue; break; }
+
+ for (int i = 0; i < n; i++)
+ {
+ ReadEpollEvent((byte*)eventsBuf + i * EvSize, out uint evs, out int fd);
+
+ if (fd == _listenFd) { AcceptLoop(); continue; }
+
+ if (!_conns.TryGetValue(fd, out var conn)) continue;
+
+ if ((evs & (uint)(EPOLLERR | EPOLLHUP | EPOLLRDHUP)) != 0)
+ {
+ Close(conn);
+ continue;
+ }
+
+ if ((evs & (uint)EPOLLIN) != 0)
+ {
+ if (!conn.OnReadable()) { Close(conn); continue; }
+ }
+
+ if ((evs & (uint)EPOLLOUT) != 0)
+ {
+ conn.SignalWritable();
+ }
+ }
+ }
+
+ Marshal.FreeHGlobal(eventsBuf);
+ close(_listenFd);
+ close(_ep);
+ }
+
+ private void AcceptLoop()
+ {
+ for (;;)
+ {
+ int cfd = accept4(_listenFd, IntPtr.Zero, IntPtr.Zero, SOCK_NONBLOCK | SOCK_CLOEXEC);
+ if (cfd >= 0)
+ {
+ int one = 1;
+ setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, ref one, sizeof(int));
+
+ byte* ev = stackalloc byte[EvSize];
+ WriteEpollEvent(ev, (uint)(EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP) | EPOLLET, cfd);
+ epoll_ctl(_ep, EPOLL_CTL_ADD, cfd, (IntPtr)ev);
+
+ var c = new EpollConnection(cfd, _ep, this);
+ _conns[cfd] = c;
+ OnAccept?.Invoke(c);
+ _ = c.RunOutputPump();
+ continue;
+ }
+ int err = Marshal.GetLastPInvokeError();
+ if (err == EINTR) continue;
+ break; // EAGAIN/EWOULDBLOCK (drained) or transient error
+ }
+ }
+
+ private static void Close(EpollConnection conn)
+ {
+ conn.MarkClosed(); // complete Input.Writer (reactor is its sole writer)
+ conn.DecRef(); // reactor/recv side done
+ }
+
+ private static int OpenReusePortListener(ushort port, int backlog)
+ {
+ int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP);
+ if (fd < 0) throw new Exception($"socket failed errno={Marshal.GetLastPInvokeError()}");
+
+ int one = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ref one, sizeof(int));
+ setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, ref one, sizeof(int));
+
+ int fl = fcntl(fd, F_GETFL, 0);
+ if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
+
+ var addr = new sockaddr_in
+ {
+ sin_family = (ushort)AF_INET,
+ sin_port = Htons(port),
+ sin_addr = new in_addr { s_addr = 0 },
+ sin_zero = new byte[8]
+ };
+ if (bind(fd, ref addr, (uint)Marshal.SizeOf()) != 0)
+ throw new Exception($"bind failed errno={Marshal.GetLastPInvokeError()}");
+ if (listen(fd, backlog) != 0)
+ throw new Exception($"listen failed errno={Marshal.GetLastPInvokeError()}");
+ return fd;
+ }
+}
diff --git a/KestrelShrike/KestrelShrike.csproj b/KestrelShrike/KestrelShrike.csproj
new file mode 100644
index 0000000..64b8a91
--- /dev/null
+++ b/KestrelShrike/KestrelShrike.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net10.0
+ enable
+ enable
+ true
+ KestrelShrike
+
+
+
+
+
+
+
diff --git a/KestrelShrike/Native.cs b/KestrelShrike/Native.cs
new file mode 100644
index 0000000..151e93c
--- /dev/null
+++ b/KestrelShrike/Native.cs
@@ -0,0 +1,297 @@
+// ReSharper disable always CheckNamespace
+// ReSharper disable always SuggestVarOrType_BuiltInTypes
+// (var is avoided intentionally in this project so that concrete types are visible at call sites.)
+// ReSharper disable always StackAllocInsideLoop
+// ReSharper disable always ClassCannotBeInstantiated
+#pragma warning disable CA2014
+
+namespace KestrelShrike;
+
+///
+/// Linux interop surface for a high-performance, epoll-driven TCP server.
+///
+/// Design goals:
+/// - **Minimal marshaling overhead**: prefer blittable types (e.g., pointers, ints).
+/// - **Explicit error handling**: all functions are marked .
+/// Use Marshal.GetLastPInvokeError() immediately after a failure to read errno.
+/// - **Unsafe-friendly**: exposes pointer overloads for zero-copy recv/send.
+///
+/// Platform notes:
+/// - Constants can differ across libc/architectures/kernels. The values here target
+/// mainstream Linux/glibc on x86_64. If you target other distros/architectures, verify
+/// these values against system headers (bits/socket.h, fcntl.h, sys/epoll.h, sys/eventfd.h).
+/// - Network byte order: ports must be big-endian (use htons); addresses must be set appropriately.
+/// - SIGPIPE: either ignore SIGPIPE process-wide or pass to send.
+///
+internal static unsafe class Native
+{
+ // =========================
+ // P/Invoke
+ // =========================
+
+ ///
+ /// Create a socket. Typically domain=AF_INET, type=SOCK_STREAM, protocol=IPPROTO_TCP.
+ /// Returns a file descriptor (>= 0) on success, or -1 on error (check errno).
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int socket(int domain, int type, int protocol);
+
+ ///
+ /// Bind a socket to an address/port. Use for IPv4.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int bind(int sockfd, ref sockaddr_in addr, uint addrlen);
+
+ ///
+ /// Mark a bound socket as passive (accept incoming connections).
+ /// is the kernel queue length hint.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int listen(int sockfd, int backlog);
+
+ ///
+ /// Accept a new connection. flags can include and
+ /// to atomically configure the accepted FD. Returns new client FD or -1 on error.
+ /// Use Marshal.GetLastPInvokeError() to check for / in edge-triggered loops.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int accept4(int sockfd, IntPtr addr, IntPtr addrlen, int flags);
+
+ ///
+ /// Set a socket option (int value). Common options: , TCP_NODELAY, etc.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int setsockopt(int sockfd, int level, int optname, ref int optval, uint optlen);
+
+ ///
+ /// Set SO_LINGER using struct.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int setsockopt(int sockfd, int level, int optname, ref Linger optval, uint optlen);
+
+ ///
+ /// File control. Typical usage: get/set O_NONBLOCK on a socket.
+ /// Returns result per command, or -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int fcntl(int fd, int cmd, int arg);
+
+ ///
+ /// Close a file descriptor (socket or epoll/eventfd). Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int close(int fd);
+
+ ///
+ /// Read from a file descriptor into unmanaged memory.
+ /// For sockets, prefer .
+ /// Returns bytes read (>=0) or -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long read(int fd, IntPtr buf, ulong count);
+
+ ///
+ /// Write to a file descriptor from unmanaged memory.
+ /// For sockets, prefer .
+ /// Returns bytes written (>=0) or -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long write(int fd, IntPtr buf, ulong count);
+
+ ///
+ /// Receive from a socket into unmanaged memory. Returns bytes received (>=0), 0 on orderly shutdown, or -1 on error.
+ /// Set flags to 0 for normal reads.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long recv(int sockfd, IntPtr buf, ulong len, int flags);
+
+ ///
+ /// Receive from a socket into a raw pointer. Equivalent to the IntPtr overload, but avoids extra pinning overhead when you already have a pointer.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long recv(int sockfd, byte* buf, ulong len, int flags);
+
+ ///
+ /// Send to a socket from unmanaged memory. Returns bytes sent (>=0) or -1 on error.
+ /// Consider passing in flags to avoid SIGPIPE on closed peers.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long send(int sockfd, IntPtr buf, ulong len, int flags);
+
+ ///
+ /// Send to a socket from a raw pointer (long length).
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long send(int sockfd, byte* buf, long len, int flags);
+
+ ///
+ /// Send to a socket from a raw void* and nuint length.
+ /// This signature maps closely to the native prototype and can reduce marshaling overhead in hot paths.
+ ///
+ [DllImport("libc", SetLastError = true)] public static extern nint send(int sockfd, void* buf, nuint len, int flags);
+
+ ///
+ /// Create an epoll instance. Returns an epoll file descriptor (>=0) or -1 on error.
+ /// Use to set close-on-exec at creation time.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int epoll_create1(int flags);
+
+ ///
+ /// Control the epoll interest list (add/mod/del). The ev points to an epoll_event struct in unmanaged memory.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int epoll_ctl(int epfd, int op, int fd, IntPtr ev);
+
+ ///
+ /// Wait for events. events points to a contiguous array of epoll_event (maxevents elements).
+ /// Returns number of events (>=0) or -1 on error. Use timeout < 0 to block indefinitely.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int epoll_wait(int epfd, IntPtr events, int maxevents, int timeout);
+
+ ///
+ /// Create an eventfd (userspace semaphore/notification). Great for waking worker threads from another thread.
+ /// Returns fd (>=0) or -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int eventfd(uint initval, int flags);
+
+ [DllImport("libc", SetLastError = true)] internal static extern int sched_setaffinity(int pid, IntPtr cpusetsize, ref ulong mask);
+
+ [DllImport("libc", SetLastError = true)] internal static extern int sched_setaffinity(int pid, IntPtr cpusetsize, ref cpu_set_t mask);
+
+ [DllImport("libc")] internal static extern int gettid(); // Linux thread id
+
+ // =========================
+ // Struct definitions
+ // =========================
+
+ ///
+ /// IPv4 address (network byte order).
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct in_addr
+ {
+ ///
+ /// Address in network byte order (big-endian). 0 == INADDR_ANY.
+ ///
+ public uint s_addr;
+ }
+
+ ///
+ /// IPv4 socket address. Must be passed with addrlen = (uint)sizeof(sockaddr_in).
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct sockaddr_in
+ {
+ /// Address family (AF_INET).
+ public ushort sin_family;
+
+ /// Port in network byte order (use htons).
+ public ushort sin_port;
+
+ /// IPv4 address (use INADDR_ANY or a specific address in network byte order).
+ public in_addr sin_addr;
+
+ ///
+ /// Padding to match native layout (8 bytes). Must be present for correct size.
+ /// It need not be initialized for normal usage; the kernel ignores it.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] sin_zero;
+ }
+
+ ///
+ /// linger option for SO_LINGER.
+ /// If l_onoff != 0, close() will block up to l_linger seconds to flush pending data.
+ /// Be careful: enabling linger can cause unexpected blocking on close.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Linger
+ {
+ public int l_onoff;
+ public int l_linger;
+ }
+
+
+ // =========================
+ // Constants
+ // =========================
+ // Socket families/types/protocols
+ internal const int AF_INET = 2;
+ internal const int SOCK_STREAM = 1;
+ internal const int IPPROTO_TCP = 6;
+
+ // setsockopt levels / names
+ internal const int SOL_SOCKET = 1;
+ internal const int SO_REUSEADDR = 2;
+ internal const int SO_REUSEPORT = 15;
+ internal const int SO_LINGER = 13;
+ ///
+ /// TCP_NODELAY (disable Nagle). Linux defines this at level IPPROTO_TCP with optname=1.
+ /// (Kept here as constant=1; use level=IPPROTO_TCP when calling setsockopt.)
+ ///
+ internal const int TCP_NODELAY = 1;
+
+ // fcntl / file status flags
+ internal const int O_NONBLOCK = 0x800; // Verify per-arch.
+ internal const int F_GETFL = 3;
+ internal const int F_SETFL = 4;
+
+ // epoll events
+ internal const int EPOLLIN = 0x001;
+ internal const int EPOLLOUT = 0x004;
+ internal const int EPOLLERR = 0x008;
+ internal const int EPOLLHUP = 0x010;
+ internal const int EPOLLRDHUP = 0x2000;
+ internal const uint EPOLLET = 0x80000000;
+ internal const uint EPOLLONESHOT = 0x40000000;
+
+ // epoll_ctl ops
+ internal const int EPOLL_CTL_ADD = 1;
+ internal const int EPOLL_CTL_DEL = 2;
+ internal const int EPOLL_CTL_MOD = 3;
+
+ // CLOEXEC / NONBLOCK flags (creation-time)
+ /// Close-on-exec for epoll_create1/eventfd. (Verify on your target kernel/arch.)
+ internal const int EPOLL_CLOEXEC = 0x80000;
+
+ ///
+ /// On many Linux systems, SOCK_CLOEXEC is 0x1000000 (not 0x80000).
+ /// Validate this constant on your target platform if you pass it to socket() or accept4().
+ ///
+ internal const int SOCK_CLOEXEC = 0x80000;
+
+ /// Creation-time nonblocking for socket/accept4.
+ internal const int SOCK_NONBLOCK = 0x800;
+
+ // eventfd flags
+ internal const int EFD_NONBLOCK = 0x800;
+ internal const int EFD_CLOEXEC = 0x80000;
+
+ // send/recv flags
+ ///
+ /// Suppress SIGPIPE on send. Alternatively, ignore SIGPIPE process-wide.
+ ///
+ internal const int MSG_NOSIGNAL = 0x4000;
+
+ // Common errno values we branch on in tight loops
+ internal const int EINTR = 4;
+ internal const int EAGAIN = 11;
+ internal const int EWOULDBLOCK = 11;
+ internal const int EPIPE = 32;
+ internal const int ECONNABORTED = 103;
+ internal const int ECONNRESET = 104;
+
+ public static void PinCurrentThreadToCpu(int cpuIndex)
+ {
+ if (cpuIndex < 0 || cpuIndex >= Environment.ProcessorCount)
+ throw new ArgumentOutOfRangeException(nameof(cpuIndex));
+
+ unsafe
+ {
+ var set = new cpu_set_t();
+ int word = cpuIndex / 64;
+ int bit = cpuIndex % 64;
+ set.Bits[word] = 1UL << bit;
+
+ int tid = gettid();
+ int ret = sched_setaffinity(tid, (IntPtr)sizeof(cpu_set_t), ref set);
+ if (ret != 0)
+ throw new InvalidOperationException($"sched_setaffinity failed with errno {Marshal.GetLastPInvokeError()}");
+ }
+ }
+}
+
+internal unsafe struct cpu_set_t
+{
+ public fixed ulong Bits[16]; // 1024 bits (enough for up to 1024 CPUs)
+}
\ No newline at end of file
diff --git a/KestrelShrike/ProcessorArchDependant.cs b/KestrelShrike/ProcessorArchDependant.cs
new file mode 100644
index 0000000..607e153
--- /dev/null
+++ b/KestrelShrike/ProcessorArchDependant.cs
@@ -0,0 +1,149 @@
+// ReSharper disable always CheckNamespace
+// ReSharper disable always SuggestVarOrType_BuiltInTypes
+// (var is avoided intentionally in this project so that concrete types are visible at call sites.)
+// ReSharper disable always StackAllocInsideLoop
+// ReSharper disable always ClassCannotBeInstantiated
+#pragma warning disable CA2014
+
+namespace KestrelShrike;
+
+///
+/// Provides architecture-dependent helpers for low-level socket and epoll interop.
+///
+///
+/// Linux’s struct epoll_event has different binary layouts depending on CPU architecture
+/// (notably 12 bytes on x86/x64 and 16 bytes on most other architectures like ARM/ARM64).
+/// This class exposes constants and helpers to correctly read and write those structures
+/// at runtime based on the process architecture.
+///
+///
+///
+/// These methods are used to serialize and deserialize epoll_event structures directly
+/// into unmanaged buffers when interfacing with epoll_wait, epoll_ctl, and related syscalls.
+///
+///
+internal static unsafe class ProcessorArchDependant
+{
+ // =============================================================================================
+ // Architecture-dependent configuration
+ // =============================================================================================
+
+ ///
+ /// Indicates whether the current platform uses a packed epoll_event layout (12 bytes).
+ ///
+ /// On x86 and x64 (little-endian), the epoll_event structure is packed to 12 bytes.
+ /// On ARM, ARM64, and others, it uses natural 8-byte alignment, resulting in 16 bytes.
+ ///
+ ///
+ internal static readonly bool Packed =
+ RuntimeInformation.ProcessArchitecture == Architecture.X64 ||
+ RuntimeInformation.ProcessArchitecture == Architecture.X86;
+
+ ///
+ /// The size (in bytes) of an epoll_event structure for the current runtime architecture.
+ ///
+ /// Typically 12 bytes for packed x86/x64 layouts and 16 for natural alignment layouts.
+ ///
+ ///
+ internal static readonly int EvSize = Packed ? 12 : 16;
+
+ // =============================================================================================
+ // Struct read/write helpers
+ // =============================================================================================
+
+ ///
+ /// Writes a Linux epoll_event structure into a preallocated unmanaged memory region.
+ ///
+ /// Destination pointer to write the structure into.
+ /// Bitmask of epoll events (e.g. EPOLLIN, EPOLLOUT, EPOLLRDHUP, etc.).
+ /// The file descriptor associated with the event.
+ ///
+ ///
+ /// Layouts by architecture:
+ ///
+ /// - Packed (x86/x64): events @ 0 (4 bytes), data @ 4 (8 bytes)
+ /// - Natural (ARM/others): events @ 0 (4 bytes), padding 4, data @ 8 (8 bytes)
+ ///
+ ///
+ /// Only the lower 32 bits of are stored in the data field.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void WriteEpollEvent(void* dest, uint events, int fd)
+ {
+ if (Packed)
+ {
+ // events @0 (4 bytes), data @4 (8 bytes)
+ *(uint*)dest = events;
+ *(ulong*)((byte*)dest + 4) = (uint)fd; // store fd in low 32 bits
+ }
+ else
+ {
+ // events @0 (4 bytes), pad 4, data @8 (8 bytes)
+ *(uint*)dest = events;
+ *(ulong*)((byte*)dest + 8) = (uint)fd;
+ }
+ }
+
+ ///
+ /// Reads a Linux epoll_event structure from unmanaged memory and extracts its fields.
+ ///
+ /// Pointer to the source buffer containing the epoll_event structure.
+ /// Outputs the event flags (EPOLLIN, EPOLLOUT, etc.).
+ /// Outputs the associated file descriptor.
+ ///
+ /// Reads using the correct layout depending on the flag.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void ReadEpollEvent(void* src, out uint events, out int fd)
+ {
+ if (Packed)
+ {
+ events = *(uint*)src;
+ fd = (int)*(uint*)((byte*)src + 4);
+ }
+ else
+ {
+ events = *(uint*)src;
+ fd = (int)*(uint*)((byte*)src + 8);
+ }
+ }
+
+ // Variations, TODO: Test performance required
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal static void WriteEpollEvent2(void* dest, uint events, int fd)
+ {
+ // Write events (always aligned 4B store)
+ *(uint*)dest = events;
+ // Compute data offset (packed: +4, natural: +8)
+ var data = (byte*)dest + (Packed ? 4 : 8);
+ // Store only low 32 bits of fd and zero the high 32 bits.
+ // Using two 4B stores avoids an unaligned 8B write in the packed layout.
+ *(uint*)data = (uint)fd; // low 32
+ *(uint*)(data + 4) = 0; // high 32
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal static void ReadEpollEvent2(void* src, out uint events, out int fd)
+ {
+ events = *(uint*)src;
+ var data = (byte*)src + (Packed ? 4 : 8);
+ // We only ever wrote the low 32 bits; read exactly those.
+ fd = (int)*(uint*)data;
+ }
+
+ // =============================================================================================
+ // Networking helpers
+ // =============================================================================================
+
+ ///
+ /// Converts a 16-bit unsigned integer from host byte order to network byte order (big-endian).
+ ///
+ /// The value to convert.
+ /// The converted value in network byte order.
+ ///
+ /// Equivalent to the native htons() function from the BSD sockets API.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static ushort Htons(ushort x) =>
+ BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(x) : x;
+}
\ No newline at end of file
diff --git a/KestrelShrike/ShrikeKestrel.cs b/KestrelShrike/ShrikeKestrel.cs
new file mode 100644
index 0000000..c5818cb
--- /dev/null
+++ b/KestrelShrike/ShrikeKestrel.cs
@@ -0,0 +1,151 @@
+using System.Net;
+using System.Threading.Channels;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace KestrelShrike;
+
+internal sealed class DuplexPipe : IDuplexPipe
+{
+ public DuplexPipe(PipeReader input, PipeWriter output) { Input = input; Output = output; }
+ public PipeReader Input { get; }
+ public PipeWriter Output { get; }
+}
+
+internal sealed class ShrikeConnectionContext : ConnectionContext,
+ IConnectionIdFeature, IConnectionTransportFeature, IConnectionItemsFeature,
+ IConnectionLifetimeFeature, IConnectionEndPointFeature
+{
+ private static long s_id;
+
+ private readonly EpollConnection _conn;
+ private readonly IDuplexPipe _transport;
+ private readonly CancellationTokenSource _closedCts = new();
+ private readonly FeatureCollection _features = new();
+ private bool _disposed;
+
+ public ShrikeConnectionContext(EpollConnection conn, EndPoint? localEndPoint)
+ {
+ _conn = conn;
+ _transport = new DuplexPipe(conn.Input.Reader, conn.Output.Writer);
+
+ ConnectionId = $"shrike-{Interlocked.Increment(ref s_id):x}";
+ LocalEndPoint = localEndPoint;
+ Items = new ConnectionItems();
+ ConnectionClosed = _closedCts.Token;
+
+ _features.Set(this);
+ _features.Set(this);
+ _features.Set(this);
+ _features.Set(this);
+ _features.Set(this);
+ }
+
+ public override string ConnectionId { get; set; }
+ public override IFeatureCollection Features => _features;
+ public override IDictionary