Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a8e15b6
feat: wire-time accounting, pacing, and SACK coalescing
ericsssan Mar 18, 2026
975e97e
fix: prevent acked_frames overflow from stalling streams
ericsssan Mar 18, 2026
95d4db3
fix: route retransmitted Initials to existing connection
ericsssan Mar 18, 2026
0050dc5
feat: coalesce Initial+Handshake into single UDP datagram
ericsssan Mar 18, 2026
370653d
feat: extend coalescing to include first 1-RTT packet
ericsssan Mar 18, 2026
d4a5968
fix: revert 1-RTT coalescing — breaks connection migration
ericsssan Mar 18, 2026
8909a3a
feat: BBR v3 congestion control and build infrastructure
ericsssan Mar 18, 2026
7eeea9c
fix: flush pending Handshake CRYPTO in tick(), not just receive()
ericsssan Mar 19, 2026
157c44e
docs: update README — BBR v3, pacing, coalescing, 22/22 interop
ericsssan Mar 19, 2026
ec9111b
fix: separate queue-time from wire-time in delivery rate computation
ericsssan Mar 19, 2026
62746a8
fix: bootstrap BBR initial pacing rate to avoid Startup throttle
ericsssan Mar 19, 2026
4c278cf
fix: BBR Drain cwnd, delivery rate, pacing refill, ACK priority
ericsssan Mar 19, 2026
b81dc00
fix: enforce pacing during BBR Startup, increase stream buffer to 128KB
ericsssan Mar 19, 2026
bdc81b1
fix: BBR v3 correctness gaps — Drain inflight_hi, loss bounding, Prob…
ericsssan Mar 19, 2026
46d2ecf
fix: BBR recovery — early loss exit, 1.0× pacing, PTO for queued data
ericsssan Mar 19, 2026
3cbcb0e
fix: BBR interop 22/22 — blackhole recovery, path migration, coalesce…
ericsssan Mar 21, 2026
35b6c39
fix: BBR min_rtt poisoned by bootstrap RTT, causing Drain deadlock
ericsssan Mar 21, 2026
08f0daf
style: fix zig fmt trailing blank line in bbr.zig
ericsssan Mar 22, 2026
ee9396d
fix: BBR goodput 9.4 Mbps — interleaved send, path migration, blackho…
ericsssan Mar 26, 2026
6644eb2
fix: bypass pacing during BBR Startup to prevent 1-per-RTT lock-in
ericsssan Mar 26, 2026
987e9b3
fix: BBR ProbeBW stuck in DOWN, collapsing throughput mid-transfer
ericsssan Mar 26, 2026
831f1aa
fix: CM socket use_cm_sock was a permanent one-way flag causing data …
ericsssan Mar 27, 2026
fa83024
docs: update interop goodput to 9429 kbps
ericsssan Mar 27, 2026
2e1363e
docs: update interop results with 11-client test matrix
ericsssan Mar 27, 2026
9d38fb6
style: simplify review cleanup — remove redundant comments and code
ericsssan Mar 27, 2026
b15be4d
fix: scale BDP headroom for 100 Gbps, conditional CM write, add unit …
ericsssan Mar 27, 2026
2934bbe
fix: use post-ACK inflight for Drain/DOWN exit, add unit tests
ericsssan Mar 27, 2026
099a1c0
fix: 4 bugs from comprehensive audit
ericsssan Mar 27, 2026
5709ac0
fix: PTO sent PING instead of Handshake retransmit when client Finish…
ericsssan Mar 27, 2026
b174181
docs: add performance TODO for high-bandwidth scaling
ericsssan Mar 27, 2026
6f19d88
perf: zero-copy encrypt into send queue, eliminating 1452-byte memcpy…
ericsssan Mar 27, 2026
b142d70
fix: pad server Initial datagrams to 1200 bytes per RFC 9000 §14.1
ericsssan Mar 28, 2026
7baf905
fix: windowed max filter simultaneous expiry causing max_bw collapse
ericsssan Mar 28, 2026
9ea55b2
style: fix zig fmt
ericsssan Mar 28, 2026
9294400
fix: keylog overwrite — append all connections' keys to /logs/keys.log
ericsssan Mar 28, 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
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.zig-cache
zig-cache
zig-out
.git
.github
.claude
.serena
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,46 @@ A QUIC protocol library for Zig. Sans-I/O — you own the socket; the library ow
- TLS 1.3 server handshake with AES-128-GCM and ChaCha20-Poly1305 (RFC 9001)
- Session resumption and 0-RTT
- Loss recovery, RTT estimation, PTO (RFC 9002)
- CUBIC congestion control (RFC 9438)
- CUBIC and BBR v3 congestion control (RFC 9438)
- Stream multiplexing and flow control
- Path migration and NAT rebinding
- Pacing with wire-time accounting
- Packet coalescing (RFC 9000 §12.2)
- PMTUD, retry tokens, key rotation, ECN
- Ed25519 and P-256 certificates
- Zero external dependencies

## Build

```sh
zig build test # run tests
zig build # build server binary
zig build test # run tests (default: BBR)
zig build test -Dcongestion=cubic # run tests with CUBIC
zig build # build server binary
zig build -Dcongestion=cubic # build with CUBIC
```

Requires Zig 0.16.0-dev or later.

## Interop Results

<!-- INTEROP_START -->
Tested against ngtcp2 client — 22/22 passing, goodput 9394 kbps on 10 Mbps link:

| Result | Test cases |
| :---: | --- |
| ✅ Pass (22/22) | handshake, transfer, longrtt, chacha20, multiplexing, retry, resumption, zerortt, http3, blackhole, keyupdate, ecn, amplificationlimit, handshakeloss, transferloss, handshakecorruption, transfercorruption, v2, ipv6, rebind-port, rebind-addr, connectionmigration |
Tested against 11 QUIC clients via [quic-interop-runner](https://github.com/quic-interop/quic-interop-runner) on a 10 Mbps / 30 ms RTT link:

| Client | Tests | Goodput |
| --- | --- | --- |
| ngtcp2 | 22/22 | 9432 kbps |
| quic-go | 20/20 | 9507 kbps |
| quiche | 18/18 | — |
| neqo | 19/22 | — |
| kwik | 19/21 | 7849 kbps |
| picoquic | 16/22 | — |
| mvfst | 12/16 | 9496 kbps |
| aioquic | 13/21 | 9190 kbps |
| lsquic | — | 9454 kbps |
| msquic | — | 7937 kbps |
| quinn | — | 9462 kbps |

Test cases: handshake, transfer, longrtt, chacha20, multiplexing, retry, resumption, zerortt, http3, blackhole, keyupdate, ecn, amplificationlimit, handshakeloss, transferloss, handshakecorruption, transfercorruption, v2, ipv6, rebind-port, rebind-addr, connectionmigration
<!-- INTEROP_END -->

## Limitations
Expand Down
20 changes: 20 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Performance TODO

## High-bandwidth scaling (target: 100 Gbps)

### Ring buffer sizing
- [ ] Make `SEND_QUEUE_DEPTH` runtime-configurable (currently 256, overflows at ~3 Gbps/30ms)
- [ ] Make `MAX_SENT` runtime-configurable (currently 256, same limit — evictions break loss detection)
- [ ] Scale `SEND_BUF_SIZE` per-stream based on negotiated BDP (currently 64 KB, tight at 10 Mbps)

### Syscall reduction
- [ ] GSO (`UDP_SEGMENT`) for Linux — batch N QUIC packets into 1 sendmsg (60× fewer send syscalls at 1 Gbps)
- [ ] recvmmsg for Linux — batch receive multiple datagrams per syscall
- [ ] Increase `SEND_BATCH` and `BATCH_SIZE` for higher packet rates (currently 32/16)

### Zero-copy send path
- [ ] Encrypt directly into send queue slot (currently: pkt_scratch → enc_scratch → sq[].buf = 2 copies per packet)

### Pacing at high rates
- [ ] Sub-millisecond pacing for >1 Gbps (current 1ms timer tick limits pacing granularity)
- [ ] Consider io_uring or busy-poll for microsecond-level pacing
31 changes: 26 additions & 5 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,35 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

// Congestion control algorithm selection: bbr (default) or cubic.
const Algorithm = enum { bbr, cubic };
const congestion = b.option(Algorithm, "congestion", "Congestion control algorithm: bbr (default) or cubic") orelse .bbr;
const congestion_cubic = congestion == .cubic;

const build_options = b.addOptions();
build_options.addOption(bool, "congestion_cubic", congestion_cubic);
const build_options_mod = build_options.createModule();

// Public module: consumers import this as @import("zquic")
const zquic_mod = b.addModule("zquic", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "build_options", .module = build_options_mod },
},
});

// Static library artifact
const lib_mod = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
lib_mod.addImport("build_options", build_options_mod);
const lib = b.addLibrary(.{
.name = "zquic",
.root_module = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
}),
.root_module = lib_mod,
});
b.installArtifact(lib);

Expand Down Expand Up @@ -83,6 +97,8 @@ pub fn build(b: *std.Build) void {
"src/quic/stream.zig",
"src/quic/flow_control.zig",
"src/quic/congestion/cubic.zig",
"src/quic/congestion/bbr.zig",
"src/quic/congestion/common.zig",
"src/quic/transport_params.zig",
"src/quic/loss_recovery.zig",
"src/quic/tls.zig",
Expand All @@ -104,7 +120,11 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
mod.addImport("build_options", build_options_mod);
const t = b.addTest(.{ .root_module = mod });
// Connection(16) is ~2.2 MB; Debug mode disables copy elision, creating
// ~16 MB of stack frames in accept() + test. 64 MB gives enough headroom.
t.stack_size = 64 * 1024 * 1024;
const run = b.addRunArtifact(t);
test_step.dependOn(&run.step);
}
Expand All @@ -119,5 +139,6 @@ pub fn build(b: *std.Build) void {
server_test_mod.addImport("http3", http3_mod);
server_test_mod.addImport("qpack", qpack_mod);
const server_test = b.addTest(.{ .root_module = server_test_mod });
server_test.stack_size = 64 * 1024 * 1024;
test_step.dependOn(&b.addRunArtifact(server_test).step);
}
15 changes: 8 additions & 7 deletions interop-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,14 @@ phase_verify_setup() {
echo -e "${GREEN}✓${NC} zquic Docker image ready"

# Verify implementations.json includes zquic
if grep -q '"zquic"' "$INTEROP_DIR/implementations.json"; then
echo -e "${GREEN}✓${NC} zquic registered in implementations.json"
local impl_file="$INTEROP_DIR/implementations_quic.json"
if grep -q '"zquic"' "$impl_file" 2>/dev/null; then
echo -e "${GREEN}✓${NC} zquic registered in implementations_quic.json"
else
echo -e "${YELLOW}⚠${NC} zquic not in implementations.json, adding it..."
python3 << 'PYTHON_SCRIPT'
import json
config_file = '$INTEROP_DIR/implementations.json'
echo -e "${YELLOW}⚠${NC} zquic not in implementations_quic.json, adding it..."
python3 - "$impl_file" << 'PYTHON_SCRIPT'
import json, sys
config_file = sys.argv[1]
with open(config_file, 'r') as f:
config = json.load(f)
if 'zquic' not in config:
Expand All @@ -380,7 +381,7 @@ if 'zquic' not in config:
with open(config_file, 'w') as f:
json.dump(config, f, indent=2)
PYTHON_SCRIPT
echo -e "${GREEN}✓${NC} zquic added to implementations.json"
echo -e "${GREEN}✓${NC} zquic added to implementations_quic.json"
fi

echo ""
Expand Down
2 changes: 1 addition & 1 deletion interop/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ COPY . .

RUN set -e; \
. /build_env.sh; \
zig build -Doptimize=ReleaseSafe -Dtarget="${TARGET}"
zig build -Doptimize=ReleaseSafe -Dtarget="${TARGET}" -Dcongestion=bbr

# Stage 2: Runtime image with network simulator support.
FROM martenseemann/quic-network-simulator-endpoint:latest
Expand Down
Loading
Loading