Proxeno.Keryx.Sctp
0.2.0
dotnet add package Proxeno.Keryx.Sctp --version 0.2.0
NuGet\Install-Package Proxeno.Keryx.Sctp -Version 0.2.0
<PackageReference Include="Proxeno.Keryx.Sctp" Version="0.2.0" />
<PackageVersion Include="Proxeno.Keryx.Sctp" Version="0.2.0" />
<PackageReference Include="Proxeno.Keryx.Sctp" />
paket add Proxeno.Keryx.Sctp --version 0.2.0
#r "nuget: Proxeno.Keryx.Sctp, 0.2.0"
#:package Proxeno.Keryx.Sctp@0.2.0
#addin nuget:?package=Proxeno.Keryx.Sctp&version=0.2.0
#tool nuget:?package=Proxeno.Keryx.Sctp&version=0.2.0

Keryx
A from-scratch WebRTC media stack for .NET. Pure managed C#, zero native dependencies, Apache-2.0. Stream real-time audio and video from a .NET server to any WebRTC client — the whole protocol suite (ICE, DTLS-SRTP, RTP/RTCP, SCTP) is implemented here, in this repository, against the RFCs. Keryx (κῆρυξ) is the Greek herald — the one who carries the message.
Status: pre-release (0.x). APIs will change. Every capability listed here is backed by a test in this repository — including a real-Chrome media interop test — and the Scope section says plainly what is and isn't here yet.
What you can build
Keryx is the sending side of WebRTC: one .NET process encodes once and streams to browsers and native clients over standard WebRTC — no plugin, no gateway, no native runtime on your server. It's built for low-latency, server-to-client media, such as:
- Game & cloud-gaming streaming — render and encode on a server, play in a browser.
- Camera, drone & robotics feeds — live video and telemetry to an operator's screen.
- Screen share & remote desktop — a headless host streaming its own output.
- Interactive media servers — anything that renders a frame and needs it on a remote display in well under a second.
The far end is ordinary WebRTC, so your client is a plain browser RTCPeerConnection and its
<video> element — nothing Keryx-specific to install.
Goals
- Pure managed. 100% C# on .NET 10 — no native library to cross-compile, ship, or keep patched.
Cryptography comes only from
System.Security.Cryptography; ciphers are never hand-rolled. - Genuinely open. Apache-2.0 from the first commit — embed it in commercial or proprietary products with no copyleft or source-available strings attached.
- Faithful to the specs. Each protocol layer is implemented against its RFC, cites the clause in the source, and has a test behind every claim.
- Strictly layered. Every protocol is its own package with no upward dependencies, testable in isolation over in-memory transports — including lossy and reordering ones.
- Honest about maturity. A 0.x that tells you exactly what is proven, what is experimental, and what is not here.
Quickstart: offering H.264 to a browser
using Keryx;
var pc = new PeerConnection(new PeerConnectionConfig
{
StunServers = { new IPEndPoint(IPAddress.Parse("74.125.250.129"), 19302) }, // optional
});
// Channels created before negotiation are DCEP-opened once SCTP associates.
var controller = pc.CreateDataChannel("controller", ordered: false, maxRetransmits: 0);
pc.OnPictureLossIndication += (_, e) => encoder.RequestKeyframe();
var offerSdp = await pc.CreateOfferAsync(ct); // -> to the browser
await pc.SetRemoteDescriptionAsync(answerSdp, SdpType.Answer, ct); // <- browser's answer
pc.AddIceCandidate(candidate, sdpMid); // <- trickled candidates
await pc.WaitForConnectedAsync(TimeSpan.FromSeconds(15), ct);
pc.SendVideoFrame(annexBAccessUnit, rtpTimestamp90k); // one H.264 access unit per call
pc.SendAudioFrame(opusPacket, rtpTimestamp48k);
(await controller).OnMessage += (isBinary, payload) => HandleInput(payload);
// Inbound NACKs are served from the send history as RTX automatically; read the link back:
var video = pc.GetStats().Video;
var lost = video?.Quality?.FractionLost; // 0..1, from the peer's reception reports
var rtt = video?.Quality?.RoundTripTime; // RFC 3550 LSR/DLSR arithmetic
var resent = video?.Retransmission?.PacketsRetransmitted;
That single surface runs ICE gathering, DTLS with fingerprint pinning, SRTP keying, SCTP data channels and the RTCP loops behind those calls.
What you get
The parts a real media server needs, exposed as first-class API instead of SDP string-splicing and raw RTCP parsing in your application:
- Typed RTCP feedback. PLI, FIR, NACK and transport-cc arrive as dedicated events
(
OnPictureLossIndication, …), anda=rtcp-fblines are emitted natively per configured codec. H.264 offersnack,nack pliandccm firby default. - NACK-driven retransmission that actually retransmits. Bare
a=rtcp-fb nackis backed by a real RFC 4588 repair stream: anrtxcodec, a dedicated SSRC published asa=ssrc-group:FID, and a ring of recently sent packets that inbound NACKs are served from — under a per-packet resend rate limit and a bandwidth budget, with counters onGetStats(). If the answer drops thertxcodec, retransmission is switched off rather than promised and not delivered. - Sender-side link quality. The reception-report blocks the browser sends are folded into
GetStats()per track: fraction lost, cumulative loss, interarrival jitter and LSR/DLSR round-trip time — the signal a rate controller needs, without parsing RTCP yourself. - A codec-agnostic packetizer seam. H.264 (packetization-mode=1, STAP-A/FU-A) and Opus ship
in-box; other codecs implement one interface (
IRtpPayloadizer) plus oneSdpCodecentry.
Architecture

At runtime everything multiplexes over one UDP socket (BUNDLE + rtcp-mux): ICE consumes STUN, DTLS records (first byte 20–63) carry SCTP, and SRTP/SRTCP (128–191) carry media — see docs/architecture.md for the wire-path diagram and design rules, and docs/layers/ for per-layer design notes.
Packages
| Package | Contents |
|---|---|
Keryx |
Composition root: the PeerConnection API. Reference this to get the whole stack. |
Keryx.Core |
Binary readers/writers, the IDatagramTransport seam, logging abstraction. |
Keryx.Stun |
STUN (RFC 5389) messages and client. |
Keryx.Sdp |
Lossless SDP model, JSEP offer builder, answer negotiator. |
Keryx.Rtp |
RTP, RTCP with typed feedback, H.264/Opus packetizers, IRtpPayloadizer. |
Keryx.Dtls |
DTLS 1.2 handshake + record layer, DTLS-SRTP keying export. |
Keryx.Srtp |
SRTP/SRTCP: AES-CM + HMAC-SHA1-80 and AEAD AES-GCM. |
Keryx.Ice |
ICE agent (RFC 8445 subset, dual-stack IPv4/IPv6, aggressive nomination). |
Keryx.Turn |
TURN client: relay allocations, permissions, channel binding (RFC 8656). |
Keryx.Sctp |
SCTP over DTLS, DCEP data channels, partial reliability. |
Packages are published as Proxeno.Keryx* on nuget.org; the assemblies and namespaces are
Keryx.* (so using Keryx; is what you write). Reference Proxeno.Keryx to get everything.
Verification status
Every claim is backed by a test in this repository.
- RFC test vectors — STUN: all four RFC 5769 vectors, byte-exact both directions · SRTP: RFC 3711 B.2 keystream + B.3 key derivation, RFC 7714 §16/§17 GCM · SCTP: CRC32c check vectors · RTP: header edge-case matrix, RFC 6184 golden packets.
- Loopback integration — two
PeerConnections over real UDP sockets: ICE connects, DTLS completes with mutual fingerprint pinning, 30 real H.264 access units arrive byte-identical, data channels round-trip (including 64 KB binary), PLI/FIR arrive as typed events, and a Generic NACK is answered with RFC 4588 RTX packets on the repair SSRC. - Loss sweeps — a seeded fault injector splices in under the sender's SRTP through
PeerConnectionConfig.TransportInterceptorand drops, duplicates, reorders and delays media datagrams while STUN, DTLS and RTCP pass through untouched. At 1%, 5% and 15% uniform loss, and under ten-packet bursts, every packet the receiver could detect as missing comes back; with retransmission switched off at the same seeds, exactly those packets stay lost (2098-packet stream: 19 / 113 / 314 permanent holes). - Chrome interop — headless Chrome (tested with 151) answers a Keryx offer over HTTP
signaling: ICE connects, DTLS completes (Keryx as server,
SRTP_AES128_CM_HMAC_SHA1_80), Chrome decodes and renders the H.264 Keryx sends (60+ frames, 640×360, keyframes counted, video element playing) and both data channels echo. A second session runs the same fifteen seconds twice across a 5% lossy link: with RFC 4588 retransmission Chrome NACKs, accepts 79 of 80 destroyed packets back as repairs and decodes 433 frames at 29 fps with no freezes; without it, the same seed leaves 51 frames at 15 fps and five freezes. Run them locally withdotnet test tests/Keryx.IntegrationTests --filter "Category=ChromeInterop". - Soak — five minutes of 30 fps video across a 5% lossy, reordering link: 31,815 packets,
1,564 repaired, zero holes, zero history misses, and a managed heap that ends 0.9% below
where it started (
--filter "Category=Soak").
885 tests across ten projects (dotnet test Keryx.slnx), plus the trait-gated Chrome interop and
soak suites.
Benchmarks
BenchmarkDotNet 0.15.8 (default job), Apple M5 Max / macOS 26.6 / .NET 10.0.11 arm64, run 2026-08-21. Measured, absolute numbers.
| Benchmark | Keryx | Allocated |
|---|---|---|
| H.264 packetization, 25 KB access unit @ MTU 1200 | 0.85 μs | 0 B |
| RTP header write + parse (12-byte header) | 2.6 ns | 0 B |
| SDP: generate full offer | 1.9 μs | 19.4 KB |
| SDP: parse Chrome answer | 2.6 μs | 25.7 KB |
| SRTP protect 1200-byte packet (AES-CM/SHA1-80) | 1.1 μs | 0 B |
Zero allocation on the hot RTP / SRTP / packetization paths is a design goal; the SDP paths allocate the document they build or parse and are not on the per-packet path.
RFC coverage
| RFC | What | Where tested |
|---|---|---|
| 5389 (+5769 vectors) | STUN, MESSAGE-INTEGRITY/FINGERPRINT dummy-length rule | Keryx.Stun.Tests |
| 8445 | ICE: pair priority, checks, role conflict, prflx, nomination | Keryx.Ice.Tests |
| 4566 / 8829 (subset) | SDP model, JSEP offer/answer | Keryx.Sdp.Tests |
| 3550 | RTP header, SR/RR/SDES/BYE, NTP mapping | Keryx.Rtp.Tests |
| 4585 / 5104 | PLI, FIR, GenericNack (PID/BLP), REMB parse | Keryx.Rtp.Tests |
| draft-holmer-rmcat-transport-wide-cc-01 | transport-cc feedback parse/build | Keryx.Rtp.Tests |
| 6184 / 7587 / 8285 | H.264 STAP-A/FU-A, Opus, header extensions | Keryx.Rtp.Tests |
| 4588 / 5576 | RTX packet format (OSN, own seq space), apt, ssrc-group:FID |
Keryx.Rtp.Tests, Keryx.Sdp.Tests, Keryx.IntegrationTests |
| 3711 (B.2, B.3) / 7714 (§16, §17) | SRTP/SRTCP AES-CM + GCM, ROC, replay | Keryx.Srtp.Tests |
| 5764 §4.2 | DTLS-SRTP key split, use_srtp | Keryx.Srtp.Tests, Keryx.Dtls.Tests |
| 6347 / 5246 / 5288 / 7627 / 5705 | DTLS 1.2 records, PRF vectors, AES-GCM, EMS, exporter | Keryx.Dtls.Tests |
| 9260 / 3758 / 8831 / 8832 | SCTP, FORWARD-TSN, data channels, DCEP | Keryx.Sctp.Tests |
| 7983 / 5761 | Demux on the bundled transport | Keryx.Rtp.Tests, Keryx.IntegrationTests |
| 8656 | TURN allocations, permissions, channel binding | Keryx.Turn.Tests |
Scope
Implemented: the offerer-side media server path end to end — sendonly H.264 + Opus with
BUNDLE/rtcp-mux, trickle ICE (in and out), DTLS 1.2 both roles with fingerprint pinning, SRTP
AES-CM and AES-GCM, bidirectional data channels with partial reliability, typed RTCP feedback
in both directions, RFC 4588 RTX retransmission driven by inbound NACKs, sender-side loss/jitter/RTT
statistics from reception reports, a minimal recvonly answerer, and a raw RTP receive surface.
Recent additions: a=extmap with outbound transport-wide-cc (TWCC) sequence numbers; TURN
relay-candidate gathering (allocations, permissions, channel binding); and dual-stack IPv6 host /
server-reflexive gathering with address-family-correct pairing.
Experimental (compiles and is tested, but not yet a production path): a send-side GCC
bandwidth estimator and pacer (in Keryx.Rtp, not yet wired into the send loop), and simulcast
transport primitives (RID / a=simulcast negotiation and ingest demux are complete; per-subscriber
forwarding is scaffolded).
Not implemented (yet, honestly): ULPFEC and RED, REMB generation, regular (non-aggressive) nomination, renegotiation and ICE restart, IPv6 relay allocation, SCTP stream reset (RE-CONFIG), jitter buffering on the receive surface, audio receive processing. Per-layer simplifications are documented in docs/layers/.
Security
The DTLS and SRTP implementations verify everything the RFCs require — peer CertificateVerify
over the transcript, Finished verify_data, fingerprint pinning against the SDP a=fingerprint,
auth tags, anti-replay windows — and those checks are covered by tamper/replay tests. They have
not yet received an independent security review; do not protect sensitive production traffic
with them until they have. Primitives are the platform's, never hand-rolled. Details and
reporting: SECURITY.md.
Contributing
Contributions are welcome — see CONTRIBUTING.md. The short version: layering is
law, zero NuGet dependencies in src/, warnings are errors, wire parsers never throw on hostile
input, and protocol claims cite their RFC section. Sign off commits with git commit -s (DCO).
License
Apache-2.0. See LICENSE. © Keryx contributors.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Proxeno.Keryx.Core (>= 0.2.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Proxeno.Keryx.Sctp:
| Package | Downloads |
|---|---|
|
Proxeno.Keryx
Keryx: a from-scratch WebRTC stack for .NET. This package is the composition root - an RTCPeerConnection-style API over the Keryx STUN/ICE/SDP/DTLS/SRTP/RTP/SCTP layers. |
GitHub repositories
This package is not used by any popular GitHub repositories.