Imported from alta/swift-nio-quic (
AGENTS.md). Install upstream withnpx skills add alta/swift-nio-quic. Copyright stays with the author.
swift-nio-quic
A pure-Swift, RFC 9000–compliant implementation of the QUIC transport protocol, built on SwiftNIO. The goal is a modern, ergonomic, fully tested QUIC stack with minimal dependencies that runs everywhere SwiftNIO runs—macOS, Linux, and beyond.
This file sets the tone for the project. Read it before making changes.
Scope
This package is QUIC transport only. Application protocols layer on top of it in their own packages:
- HTTP/3 and QPACK (RFC 9114 / RFC 9204) will live in a separate
swift-nio-http3package and repository that depends on this one. Keep this package free of HTTP/3 concerns.
A library, not an application. The deliverable is a clean Swift API for opening and accepting QUIC connections and streams, layered on SwiftNIO’s event loops and channels, with correctness verified against the RFCs and against other implementations via the QUIC Interop Runner.
Target RFCs, in rough order of implementation:
- RFC 8999: Version-Independent Properties of QUIC
- RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport (the core)
- RFC 9001: Using TLS to Secure QUIC
- RFC 9002: QUIC Loss Detection and Congestion Control
- RFC 9221: An Unreliable Datagram Extension to QUIC
- RFC 9368: Compatible Version Negotiation for QUIC
- RFC 9369: QUIC Version 2
Local copies of these specs live in docs/specs/ so we can cite
them precisely. See docs/PLAN.md for the roadmap and
docs/ARCHITECTURE.md for the design.
Principles
- Spec fidelity over cleverness. QUIC is a security protocol. When in
doubt, do what the RFC says, and cite the section in a comment
(e.g.
// RFC 9000 § 17.2.2). Deviations must be deliberate, documented, and justified. - Correctness is tested, not asserted. Every protocol element ships with
tests. We prefer many small, deterministic unit tests (using
NIOEmbedded’sEmbeddedChannel/EmbeddedEventLoopfor time and I/O control) plus higher-level integration and interop tests. A change without a test is incomplete. - Minimal dependencies. SwiftNIO, our swift-nio-ssl fork (for the BoringSSL QUIC-TLS interface), swift-crypto, swift-collections, and swift-log. Adding a dependency is a decision to be argued for, not a default.
- Modern, ergonomic Swift. Swift 6 language mode with full strict
concurrency. Public API should feel native to the SwiftNIO ecosystem and
read well at the call site.
async/awaitat the edges; the hot path stays on the event loop. - Sans-I/O core. The protocol state machines (connection, streams, packet
framing, recovery) are pure and synchronous: bytes and events in, bytes and
events out, no I/O. SwiftNIO
ChannelHandlers and the UDP datagram plumbing are a thin shell around that core. This keeps the protocol logic deterministically testable and the I/O layer swappable. - No silent truncation or partial parsing. Reject malformed input explicitly with the correct QUIC transport error code. Frauds, overflows, and length mismatches are errors, not surprises.
TLS strategy
QUIC does not run TLS over records; it drives the TLS 1.3 handshake state
machine directly and consumes per-encryption-level secrets (Initial, 0-RTT,
Handshake, 1-RTT). BoringSSL exposes exactly this via its QUIC API
(SSL_set_quic_method, SSL_provide_quic_data, SSL_process_quic_post_handshake,
and the SSL_QUIC_METHOD callbacks set_read_secret / set_write_secret /
add_handshake_data / flush_flight / send_alert).
apple/swift-nio-ssl already vendors BoringSSL with the full QUIC API
compiled and exported via the public CNIOBoringSSL product, but its Swift
NIOSSL layer exposes none of it. So we maintain a fork at
alta/swift-nio-ssl that surfaces the
QUIC-TLS hooks through a clean Swift API, and depend on the fork from here.
The fork is a means, not an end: the goal is to land this QUIC-TLS API
upstream in apple/swift-nio-ssl and then retire the fork. The full strategy
(API constraints, upstreaming process, packet protection split) lives in
docs/TLS.md; the bar for every change to the fork:
- No BoringSSL in the public API—ever. Upstream policy is that the public
surface contains no part of BoringSSL (it has no API/ABI stability; see
apple/swift-nio-ssl#484). The QUIC-TLS API must expose only Swift / NIOSSL
types: a NIOSSL-defined encryption-level enum,
ByteBuffer/[UInt8], and the IANA cipher-suite id asUInt16(translated fromSSL_CIPHER *inside the module). Nossl_encryption_level_t,SSL_CIPHER,OpaquePointer, orCNIOBoringSSLimport in any public declaration. - Minimal, additive, secure, correct. Smallest surface that fully drives a
QUIC handshake. New code under
Sources/NIOSSL/QUIC/; existing-file edits surgical. Copy key material out of callbacks immediately; never log secrets. - Match upstream conventions exactly, including XCTest for tests (not
swift-testing),
NIOSSLError,Sendable, availability, and the platform matrix. ReuseNIOSSLContext/TLSConfigurationfor certs/keys/trust/ALPN/SNI. - Engage upstream first with a pitch issue/forum thread on the API shape before a large PR.
Mechanics while the fork exists:
- The fork is checked out as a sibling directory at
../swift-nio-ssl, with anupstreamremote pointing atapple/swift-nio-ssl. mainon the fork mirrorsupstream/mainand carries none of this work. The QUIC-TLS API lives on thequicbranch, so the whole diff is one branch and one pull request when it goes upstream. Changes land via pull requests intoquic; periodically rebasequicontoupstream/mainto keep the diff tiny, and fast-forwardmainfromupstream/main.- The workflow edits on
quic(.github/workflows/) are fork CI plumbing, not part of the upstream proposal. Drop them from the branch that becomes the upstream pull request. Package.swiftdepends on the fork by git URL;Package.resolvedpins the exact commit for reproducible CI builds.- For local co-development of both repos at once, use SwiftPM’s edit mode:
swift package edit swift-nio-ssl --path ../swift-nio-ssl. Undo withswift package unedit swift-nio-ssl. The redirect lives underPackages/, which is git-ignored—never commit it.
We use swift-crypto for QUIC packet protection (HKDF key derivation, AES-GCM / ChaCha20-Poly1305 AEAD, header protection) rather than reaching into BoringSSL’s EVP layer, because its CryptoKit-shaped API is cleaner and already a sanctioned cross-platform dependency. BoringSSL’s job is strictly the TLS 1.3 handshake and the secrets it yields.
Layout
Sources/
NIOQUIC/ Core transport: varints, packets, frames, connection &
stream state machines, packet protection, loss recovery,
congestion control, and the NIO datagram channel handlers.
quic-interop/ Executable client/server for the QUIC Interop Runner. Uses
HTTP/0.9 over QUIC streams for the transport-level test cases
(handshake, transfer, retry, resumption, …); the http3 test
case belongs to the swift-nio-http3 package.
Tests/
NIOQUICTests/
docs/
PLAN.md Roadmap and milestones.
ARCHITECTURE.md Design, module boundaries, threading/concurrency model.
TLS.md QUIC-TLS strategy: the swift-nio-ssl fork and the upstream plan.
specs/ Local RFC text + a spec-to-code index.
Conventions
- The best code is no code, and the second best is as short as necessary. Prefer deleting and reusing over adding. Do not invent a type or method when this repo or SwiftNIO already has one—search first.
- Comments are plain and brief. No baroque comment blocks; a private or internal member gets a line at most. Keep the RFC citation, cut the restatement of what the code shows.
- Short declarative doc comments, one concept per sentence. A doc comment says what a member is and the invariant or RFC rule it upholds. Cut secondary and tertiary phrases that only restate it. Don’t run several clauses into one colon- or semicolon-joined sentence, and don’t glue clauses with an em-dash either: split the run-on into separate short sentences. A full stop almost always reads clearer. Never use an em dash in a doc comment. Writing voice rations it for prose in issues, PRs, and commits.
- No design rationale in doc comments. Why one design was chosen over
another—a knob on the configuration instead of queried from the handshake, one
data structure instead of another—belongs in
docs/*.md(ARCHITECTURE.md,TLS.md) or the PR that landed it, not on the declaration. Keep the “what it is” and the RFC citation; drop the “why this shape and not that one.” - Do not use a map to iterate over a struct. Comparing or transforming a
struct’s typed fields is a handful of plain
ifstatements, not a key-path table. - Comments cite the spec. Prefer
// RFC 9000 § 19.6: CRYPTO frameover a vague restatement of the code. - Comment punctuation: separate a label from its explanation with a colon,
not an em dash. Write
// length: encoded as a varint, not// length — encoded as a varint. - Smart quotes in comment prose. Comment prose uses Unicode typography:
’for apostrophes and“ ”for quotation (// the peer’s “most likely” path), not ASCII'/". Straight quotes are reserved for code: string literals, and anything inside a backticked code span, stay straight.swift formathas no rule for this, so it is review-enforced, like the punctuation rule above. - Backtick every symbol in Markdown. In
docs/*.md(and issues, PRs, and comments), wrap anything that names code or the wire in backticks: type, function, file, and module names; frame names (MAX_DATA,RESET_STREAM,CRYPTO); transport-parameter ids (initial_max_data); error codes (FLOW_CONTROL_ERROR); env vars (QLOGDIR). Prose acronyms (RFC, TLS, ABI), spec/protocol proper nouns (BoringSSL, MASQUE, WebTransport), and tool names (Docker, libFuzzer) stay bare. - Apache license header on every source file (see existing files for the exact block). CI enforces it.
- Naming follows SwiftNIO.
NIO-prefixed products,ByteBufferfor wire data,EventLoop/Channelidioms,Sendable-correct types. The full naming strategy—the publicQUICprefix, RFC nouns with NIO verbs, and the noun/verb tables—lives indocs/NAMING.md. - American spelling, except where SwiftNIO sets a term. Prose and new
identifiers use American spelling:
behavior,honor,initialize,serialize. Where SwiftNIO has settled a term, follow SwiftNIO instead and use it in prose too, so a sentence and the symbol it names do not disagree.cancelledis the case in point: SwiftNIO spells it with twols about fourteen times to one, and this package’sQUICError.cancelledfollows it. Two things are never respelled. Verbatim spec text underdocs/specs/*.txtstays as published, and so does any value defined by a spec rather than by this package, such as the qlogevent_typestring"cancelled"fromdraft-ietf-quic-qlog-quic-events. Check SwiftNIO before assuming a spelling is drift. - Format with
swift format(configuration in.swift-format). Run before committing. - Errors are typed. Map protocol failures to QUIC transport error codes (RFC 9000 § 20); don’t throw stringly-typed or generic errors on the wire path.
Writing voice
These rules govern every piece of prose this project publishes, whoever writes it. That covers issue titles and bodies, pull request titles and descriptions, issue and PR comments, code review comments and replies, commit subjects and bodies, GitHub Discussions, release notes, and every Markdown file in the repository. They shape the first draft. They are not a pass over finished writing, and they apply whether the prose is the deliverable or a by-product of shipping something else.
Code, commands, identifiers, product names, legal text, and quoted material keep their exact form, as the end of this section repeats. Doc comments in source follow the Conventions rules above, which are stricter on some points, such as banning the em dash outright.
dev/prose-lint.sh enforces the mechanical part on repo Markdown: Unicode
quotation and apostrophes, and no spaced em dash. dev/soundness.sh runs it, and
so does the prose job on every PR. The rest of this section is judgment and
stays under review, so run the text past it before posting rather than after.
For prose in issues, PRs, comments, commit bodies, and design docs, match the project owner’s voice: plain, concrete, a little dry. Lead with the reasoning and state the conclusion at the end. Aim for the qualities the owner admires in RFC prose. It is concise, unambiguous, and well-structured for the reader, without RFC formality.
Two filters govern this prose. The first is Orwell’s rules from “Politics and the English Language.” The second is ASD-STE100 Simplified Technical English. Both appear in full in the global preferences; the rules below are what they mean here.
- Ground first, conclude later. Open on the current state, prior context, or a concrete fact (“Currently, …,” “As proposed in #2227, …”), then reason forward. Set up the premise, then the consequence: “Given that …,” “Since …,” “Now that ….” The why precedes the what.
- Active voice. Give each sentence a clear subject and an active verb. Name the actor where the actor matters. Keep a passive construction only where it serves emphasis or technical accuracy, or where the actor is unknown.
- Concrete over abstract. Name the components, files, people, and numbers. Identifiers, types, files, and commands go in backticks, always.
- No idioms. Avoid idioms, slang, figurative language, and vague verbs. Use the specific technical term instead, and define it or link to its definition.
- One term per thing. Use the same term for the same thing. Do not vary a term only to avoid repetition.
- No possessive on a thing that cannot possess. A file, a type, a function,
a module, and a tool own nothing. Write “the overhead math in
applicationStreamFrames”, not “applicationStreamFrames’s overhead math”, and “the event loop of itsChannel”, not “itsChannel’s event loop”. Protocol actors do act, so a possessive on one is fine: “the peer’s advertised limit” and “the client’s address” stay. - Understated. State limitations flat (“a palliative fix, but works”). No hype adjectives: powerful, seamless, robust, elegant, game-changing.
- Impersonal by default. Prefer a concrete subject for what the code does: “This package does not reimplement…,” not “We don’t reimplement….” Reserve “we”/“I” for genuine shared decisions and personal credit, not as a filler subject. Use the proposal mood for proposals (“would,” “could,” “this proposes”). Credit people by name.
- Short sentences, no fragments. Put one main action or statement in each sentence, and write complete sentences. Vary the length within complete grammar; do not write paragraphs of same-shape medium sentences.
- Say it in as few words as it takes. The samples make their point in 200–600 words and stop. Cut the preamble, the recap, and the second sentence that restates the first.
- Ration the em dash. The closed em dash (
word—word) remains available, but fewer than one sentence in ten should carry one. Prefer a full stop, a comma, or a colon. Never use the spaced em dash (—). - Smart quotes, American punctuation. Prose uses Unicode typography:
’for apostrophes and“ ”for quotation. A period or a comma falls inside the closing quotation mark. A colon or a semicolon falls outside. A question mark or an exclamation point goes inside only where it belongs to the quoted material. Straight quotes stay straight inside a backticked code span, and quoted code keeps its own punctuation. - Italics for emphasis on a key term, a block quote for a central line.
- Lists and
- [ ]checkboxes only for real enumeration or open decisions, never decorative parallelism. Surface open questions as paired alternatives (“Should we X? Or Y?”). - Warmth belongs in the asides. A parenthetical can carry it: “(thanks LetsEncrypt!)”. An ending can turn warm or punchy rather than summing up.
Cut the AI tells: “it’s worth noting,” “importantly,” “notably,” “as we can see,” “let’s dive in”; rule-of-three everything; bolding the obvious; “In summary” / “Overall” / “Net:” wrap-ups; and hedge stacks (“it might be worth possibly considering”). Cut the overused pet words too. Seam and seam-first stand in for any old step or boundary, ground gets used as a verb, and load-bearing gets used for “important.” Keep each one for where it carries its real meaning, and use a plain word otherwise.
Preserve code, commands, identifiers, product names, legal text, and quoted material exactly. Do not simplify them.
Register shifts with the medium. Design docs, issues, and PR bodies stay closer to formal: proposal mood, open questions as checkboxes, little to no joking. PR descriptions stay impersonal: “This PR closes…,” not “I closed….” Inline comments and short notes can carry more warmth and dry wit.
On GitHub’s GFM surfaces (issues, PR descriptions, comments) write each paragraph
and list item as a single long line. Do not hard-wrap at ~80 columns. GFM
turns a soft newline into a <br>, so source-level wrapping shows up as ugly
mid-sentence breaks in the rendered view. Repo Markdown (README, docs/*.md)
renders with normal Markdown semantics, so wrapping there is fine. This rule is
specific to the issue/PR/comment surface.
Working in this repo
For current status before starting, read the roadmap tracker (issue #1,
gh issue view 1): the live per-task checklist, every item linked to the commit
or PR that landed it. docs/PLAN.md is the roadmap narrative—
objectives, dependencies, architecture at a glance, and what each milestone is
for with its at-a-glance status—not a per-task mirror. docs/specs/TRACEABILITY.md
maps RFC sections to code and tests. These are kept in sync with the code as work
lands (see Landing changes).
- Build:
swift build - Test:
swift test(Swift Testing; some integration suites are tagged and opt-in) - Resolve/refresh deps:
swift package resolve/swift package update - Benchmark: the performance benchmarks live in their own standalone package under
Benchmarks/(ordo-onepackage-benchmark), deliberately kept out of the main build and CI; run them withcd Benchmarks && swift package benchmark(jemalloc required, seeBenchmarks/README.md).
Landing changes
Changes land via pull requests into main, not direct commits. For a unit of
work, branch, push, and open a PR with gh pr create. Branch names are short and
descriptive of the change (loss-detection-timer, interop-endpoint)—not
tool-generated session names. Don’t merge until the owner says so (e.g. “merge
when green”). Squash-and-merge is the policy.
Once a PR is open, fixes are additive commits, never an amend + force-push: a reviewer needs to see what changed between looks, and the squash collapses the commits at the end anyway. Reserve history rewriting for before a PR exists.
Keep the three trackers in sync with the code as it evolves—they drift apart fast otherwise, and reconciling after the fact is its own chore. Each owns one job, so they don’t duplicate:
- Issue #1 on
alta/swift-nio-quic, the live task tracker: check off items and link the commit or PR that landed them. This is the only place per-task status lives. Edit it withgh issue edit 1. docs/PLAN.md, the roadmap narrative: milestone definitions and their at-a-glance status (the✅/🚧/⬜markers), plus objectives, dependencies, and architecture. Update a milestone’s marker and its prose when the milestone itself moves; leave per-task churn to #1.docs/specs/TRACEABILITY.md: the RFC-section → implementation → tests matrix.
A change that lands or moves a feature updates whichever of the three it touches, in the same PR—not in a later cleanup pass.
The same discipline covers the docs that mirror the code, not just the status
trackers above. A change to the public surface updates
docs/API.md; a change that moves a spec section’s status updates
both docs/specs/TRACEABILITY.md (the
section-by-section matrix) and the coarse spec-to-code index in
docs/specs/README.md that summarizes it—in the same PR.
The README.md index drifts silently otherwise: it once read ⬜ pending for
whole areas that had long since landed.
Validate before pushing
swift build / swift test alone do not match CI, and green-locally-red-on-CI
has bitten more than once. Before pushing, run CI’s actual gates:
- Build with warnings as errors:
swift build --build-tests -Xswiftc -warnings-as-errors. The package enables theExistentialAnyupcoming feature, so a bareError(vsany Error) is a warning CI turns into an error. Nevergrep -v ExistentialAnythe output—that hides the exact failures CI enforces. - Lint with the doc rules, strict:
swift format lint --strict --recursive Sources Tests Package.swift.ValidateDocumentationCommentsis on, so a throwing function needs a- Throws:section, parameters need- Parameters:, and so on. - Run the rest of the Soundness suite:
dev/soundness.sh(unacceptable language, license headers, broken symlinks, shellcheck, yamllint, flake8, and the prose typography lint of Writing voice) from the same pinned upstream scripts, no GitHub token or Actions runner needed; it wantsshellcheckandyamllinton PATH. Neitherswift buildnorswift formatcovers these, and a red language or license check blocks the PR like any build failure—the inclusive-language word list is an easy trip in comment prose.
Push only when all three exit 0.
CI runs the unit tests across released Swift toolchains and the nightly ones. The released toolchains are the gate—judge a change by those, plus the macOS, Static SDK, and Release builds. The nightly jobs are informational: they can go red from an upstream compiler bug, and that’s accepted. Don’t disable nightly; don’t block on it.
Commits and PR descriptions
Follow SwiftNIO’s conventions (the alta/swift-nio-ssl fork uses the same, for
upstreaming):
- Subject: one short imperative line, no scope/type prefix (not
feat:, notloss recovery:); e.g. “Detect lost packets by the packet threshold”. - Body:
Motivation:/Modifications:/Result:. The commit template (dev/git.commit.template) uses plain headers; the PR template (.github/PULL_REQUEST_TEMPLATE.md) uses### Motivation:and so on. - Feature PRs are framed by the spec, not the project. The squash commit
outlives the project’s week-to-week context, so ground the reader in what
survives—and write for human code reviewers. The subject names the
protocol capability, not the vehicle: “Initiate 1-RTT key updates”, not
“Enable the keyupdate interop case”; the interop case is where the
capability gets proven, not what the change is. The Motivation opens on
the RFC mechanism and what it is for, in two or three short paragraphs
with the section citations as links, ending with this stack’s gap in a
sentence. Sentences carry one concept each: a clause that introduces a
second mechanism gets its own sentence, not a colon. Keep it succinct, and keep internal details (the interop runner,
test plumbing) out of it—interop and tests are proof, not motive, and
leading with “the runner’s case expects…” reads as implementing to make a
test pass. Instead the Modifications end with a validation bullet: the
tests and the interop case, as “how this is verified”. The
Result:states the protocol claim (“the client-initiated half of RFC 9001 § 6 works end to end”). Diagnosed-from-failure fixes invert this: open on the investigation and the observed failure (e.g. #54, #55). Meta changes (renames, ergonomics, build) need no rubric. - Squash-and-merge collapses a PR into one commit, so the PR description is what
matters most—it becomes the squash commit (swift-nio appends
(#N)). Intermediate commits can be looser. - Write PR bodies impersonally (“This PR closes…”, not “I closed…”) and in long lines (see the GFM note under Writing voice).
- Show the pending public API in Swift. When a PR adds, removes, or changes
public API, put the pending declarations in a Swift code block in the
description—the new or changed signatures as they will read at the call site,
not the whole diff—so a reviewer sees the surface the change commits to without
reconstructing it. It is the same shape
docs/API.mdwill carry once the change lands. A PR that touches no public API omits the block.
Dependencies
Bump SwiftPM dependencies manually—no Dependabot (tried, removed). Run
swift package update, then swift build + swift test, and commit the
Package.resolved change. The alta/swift-nio-ssl fork is tracked on its quic
branch, so an update also refreshes that commit pin.
Definition of done for a protocol feature
- Implemented against the cited RFC sections, sans-I/O where possible.
- Unit tested deterministically (happy path, boundary values, and at least the adversarial/malformed cases the RFC calls out).
- Wired into the NIO channel layer with an integration test.
- Where applicable, covered by the relevant Interop Runner test case.
- Public API documented; spec citations in code.
- Recorded in the RFC traceability matrix
(
docs/specs/TRACEABILITY.md): the relevant section’s row points at the implementation and its tests, and its status is updated. No normative section is silently skipped—unimplemented sections stay listed as pending against a milestone.
