Post-quantum-secure L4/L6 universal transport framework in Rust.
Phantom Protocol is an SDK — a foundation for building secure networked
products (VPN, messaging, …), not an end-user application. It gives applications
an authenticated, confidential, post-quantum-secure byte pipe, pairing a hybrid
classical-plus-PQ handshake (X25519 + ML-KEM-768 KEM, Ed25519 + ML-DSA-65
signatures — FIPS 203 / FIPS 204, pure Rust) with a transport layer: TCP /
WebSocket sessions and a native reliable-UDP transport (PhantomUDP), plus
WASI / embedded byte-stream framing. Cross-language bindings via UniFFI
(Python, Swift, Kotlin, C); native WASM target; bare-metal EmbeddedLeg for
no_std. (An optional TLS-over-TCP DPI-mimicry transport — mimicry feature —
makes a flow look like HTTPS to passive DPI; anti-DPI obfuscation only, detectable
by active probing — see Status & limitations.)
Pre-1.0 (
0.4.0). Wire format may break between minors; SemVer kicks in at 1.0. 0 workspace warnings, 0unsafeoutside two audited opt-ins, MSRV Rust 1.93, every row of the cross-target matrix green. Those rows arecargo check --lib; which targets ship a prebuilt artifact and which ones the test suite has ever run on is Platform support. No release of this project has been reviewed by a second person, and there has been no external security audit — both are set out in Status & limitations, with what would change them. Known behaviours that have surprised consumers are collected indocs/known-deviations.md.
Published on crates.io as phantom-protocol
(the import path is phantom_protocol):
[dependencies]
phantom-protocol = "0.4"or cargo add phantom-protocol. API docs: https://docs.rs/phantom-protocol.
Loopback demo (30 seconds):
cargo run --manifest-path core/Cargo.toml --example loopback_demoServer + CLI ping (2 minutes):
# Generate a persistent server identity
cargo run --manifest-path cli/Cargo.toml -- keygen --out ./server.key
# Start the reference server
cargo run --manifest-path server/Cargo.toml -- --bind 0.0.0.0:4242 --signing-key-file ./server.key
# In another terminal: get the public key and ping
cargo run --manifest-path cli/Cargo.toml -- pubkey --in ./server.key
cargo run --manifest-path cli/Cargo.toml -- ping --host 127.0.0.1 --port 4242 \
--pinned-key-hex <hex-from-pubkey> --msg helloLanguage bindings (15 minutes): see
tests/bindings/PACKAGING.md for Swift, Kotlin,
Python, and C packaging workflows.
| Transport | Entry points | migrate()? |
Firewall-friendly? | Use it for |
|---|---|---|---|---|
| PhantomUDP | connect_pinned_udp / PhantomUdpListener::bind_udp |
Yes | Mostly | the default — the production transport |
| TCP | connect_pinned / PhantomListener::bind |
No — returns Err(Unsupported); reconnect with 0-RTT |
Yes | reach, where UDP is blocked — not speed |
| WebSocket | WebSocketLeg (wasm32 only) |
No | Yes (port 443) | browsers |
| Embedded | EmbeddedLeg |
No | N/A | UART / USB links |
If you can use PhantomUDP, use it. The byte-pipe legs exist for reach — a
network that blocks or throttles UDP, a proxy, a browser sandbox — and Phantom
over TCP in particular pays for that reach with latency. Its reliability layer
(ARQ, SACK loss detection, congestion control) is the same one PhantomUDP uses
and runs unchanged, which over a socket that already retransmits and already has
a congestion window means two control loops stacked on each other, communicating
only through the queue between them. Measured consequence, from the WAN harness
in testbed/:
min-RTT on the TCP leg has been observed as high as 4112 ms — that is queueing
under our own sender, not a property of the route — and its throughput has moved
with the route from run to run, so the figure to quote is one with a control
from its own run: in run 20260822-062705 the server received 4.83 Mbit/s over
this leg while raw UDP echo on the same path in the same run measured
13.26 Mbit/s round-trip. How the harness counts, and which control each figure
is read against, is in
testbed/README.md.
This is what the leg is, not a defect being worked on: the inner wire, the pinning, the AEAD and the replay window are identical on every transport, and TCP is fully conformant. It simply has no way to be quick under load, and no way to migrate.
session.send() / session.recv() operate on a single implicit stream
(simplest). session.open_stream() / session.accept_stream() give you
independent multiplexed streams with per-stream flow control.
- Hybrid post-quantum handshake — X25519 + ML-KEM-768 KEM, Ed25519 + ML-DSA-65
signatures. Both halves must verify. The post-quantum halves are pure-Rust
RustCrypto (
ml-kem,ml-dsa) — no C anywhere in them, which is why the full handshake compiles on native, mobile andwasm32alike. The rest of the crypto substrate is not C-free: see What needs a C compiler. (Bare-metalthumbv7emisstd-gated to the framing transport only — see Status & limitations.) - 0-RTT resumption — AEAD-sealed early-data (≤ 16 KiB) folded into the
single
ClientHello, one-shot anti-replay via a consumedSessionCacheticket, best-effort fallback to a 1-RTT handshake when the ticket is unknown / expired or the blob fails to open. - Mid-session rekey — HKDF ratchet,
REKEYflag + per-packetepoch. - Transports —
PhantomSessionruns an authenticated session over TCP, WebSocket, and a native reliable-UDP transport (PhantomUDP): connection-ID demux, SACK loss recovery + RFC-9002 fast-retransmit, a BBR-style congestion controller, and seamless connection migration (one live path at a time — Wi-Fi↔cellular without a re-handshake), all with no extra crypto layer. WASI and Embedded ride as framing legs. An optionalmimicryfeature (a TLS-over-TCPMimicTlsLeg—connect_pinned_mimic/bind_mimic) makes a flow look like HTTPS to passive DPI + JA3/JA4 fingerprinting; it is anti-DPI obfuscation only and is detectable by active probing (see Status & limitations). Bandwidth aggregation across transports is deliberately not pursued. The UDP data plane has been measured on a real WAN route against a QUIC reference and raw no-protocol controls (see Performance) — over one route, and with no external audit. - Multi-stream — strict-priority scheduler,
WINDOW_UPDATEper-stream flow control, BBRv2-inspired pacing (Startup / Drain / ProbeBW / ProbeRTT). Loss is not a state: it is answered by aninflight_hivolume bound, from which Startup is exempt. - DoS-resistant handshake — stateless HMAC-SHA-256 cookie (per-process master → hourly-rotated derived secret, 5-minute validity buckets) + adaptive blake3 proof-of-work (load-tiered difficulty 0–16).
- Per-direction replay protection — RFC 4303 §3.4.3 sliding-window bitmap, default 1024 bits, checked after AEAD verify.
- Observability — OpenTelemetry metrics + traces (opt-in
telemetry-otelfeature). Lock-free hot-path atomics (≤ 2.5 ns / call), OTLP/gRPC push to any backend (Datadog, Honeycomb, Grafana Cloud, self-hosted via OTel Collector). Pre-built Grafana dashboard + Prometheus alert rules indocs/observability/. - Signed build provenance — every release artifact carries a sigstore-backed
in-toto attestation naming the workflow, commit and runner that produced it
(SLSA v1.0 Build L2; verify with
gh attestation verify). - Cross-platform, and precisely so — thirteen matrix rows over twelve targets
are hard-gated in CI with no
allow_failurerows, but a row iscargo check --lib: it proves the crate compiles there and nothing else. Four targets ship a prebuilt release artifact (linux-gnu and apple-darwin, x86_64 and aarch64). The Rust test suite runs on x86_64 Linux; aarch64 macOS additionally runs one pinned loopback handshake through the Swift binding. Windows, iOS, musl, browser wasm and bare metal are compiled and never executed, and Android is in no workflow at all. Full breakdown in Platform support — read it before choosing a target.
cargo build --manifest-path core/Cargo.toml
cargo test --manifest-path core/Cargo.toml --lib
cargo clippy --manifest-path core/Cargo.toml --lib -- -D warnings
cargo fmt --manifest-path core/Cargo.toml --checkLoopback integration tests are #[ignore]-gated:
cargo test --manifest-path core/Cargo.toml --test tcp_integration -- --ignoredMore commands (benches, fuzz, miri, cross-targets, embedded) live in the CI
workflow files under .github/workflows/; the PR
checklist is in CONTRIBUTING.md.
Server identity must be pinned — connect_with_transport requires a
HybridVerifyingKey; there is no skip path (Security Invariant 1).
PhantomUDP is the recommended transport: it supports seamless migrate().
use std::sync::Arc;
use phantom_protocol::api::{PhantomUdpListener, PhantomSession};
#[tokio::main]
async fn main() -> Result<(), phantom_protocol::CoreError> {
// ── Server ────────────────────────────────────────────────────────────────
let listener = PhantomUdpListener::builder("127.0.0.1:0").bind().await?;
let server_addr = listener.local_addr(); // e.g. "127.0.0.1:54321"
let pinned_key = listener.verifying_key_bytes(); // share out-of-band
let listener = Arc::clone(&listener);
tokio::spawn(async move {
let outcome = listener.accept().await?;
let session = outcome.session();
let _req = session.recv().await?;
session.send(b"hello, post-quantum world".to_vec()).await?;
Ok::<_, phantom_protocol::CoreError>(())
});
// ── Client ────────────────────────────────────────────────────────────────
let port: u16 = server_addr.parse::<std::net::SocketAddr>().unwrap().port();
let session = phantom_protocol::connect_pinned_udp(
"127.0.0.1".into(), port, pinned_key,
).await?;
session.await_ready().await?;
session.send(b"ping".to_vec()).await?;
let _reply = session.recv().await?;
Ok(())
}use std::sync::Arc;
use phantom_protocol::api::{PhantomListener, PhantomSession, TcpSessionTransport};
use phantom_protocol::crypto::hybrid_sign::HybridVerifyingKey;
#[tokio::main]
async fn main() -> Result<(), phantom_protocol::CoreError> {
let listener = PhantomListener::builder("127.0.0.1:0").bind().await?;
let server_addr = listener.local_addr();
let pinned_key = listener.verifying_key_bytes();
let listener = Arc::clone(&listener);
tokio::spawn(async move {
let outcome = listener.accept().await?;
let session = outcome.session();
let _req = session.recv().await?;
session.send(b"hello, post-quantum world".to_vec()).await?;
Ok::<_, phantom_protocol::CoreError>(())
});
let session = phantom_protocol::connect_pinned(
"127.0.0.1".into(),
server_addr.parse::<std::net::SocketAddr>().unwrap().port(),
pinned_key,
).await?;
// Same rule as the UDP form above: `connect_pinned` returns before the
// handshake has run, so this is where the pinned-key check surfaces. Without
// it a wrong key looks like a successful connect.
session.await_ready().await?;
session.send(b"ping".to_vec()).await?;
let _reply = session.recv().await?;
Ok(())
}Runnable forms: core/examples/loopback_demo.rs,
core/examples/embedded_demo.rs,
core/examples/crypto_bench.rs.
| Role | Primitive | Standard / source |
|---|---|---|
| KEM | X25519 + ML-KEM-768 | RFC 7748 + FIPS 203 (RustCrypto ml-kem) |
| Signatures | Ed25519 + ML-DSA-65 | FIPS 186-5 + FIPS 204 (RustCrypto ml-dsa) |
| AEAD (primary) | AES-256-GCM | ring, HW-accelerated (AES-NI / ARMv8 PMULL) |
| AEAD (fallback) | ChaCha20-Poly1305 | RFC 8439, auto-selected without AES intrinsics |
| KDF | HKDF-SHA-256 + keyed BLAKE3 | RFC 5869 for the KEM combine / rekey / 0-RTT keying; crypto::kdf::derive_key_32 label derivations use blake3::derive_key, swapping to HKDF-SHA-256 under --features fips |
| Hash / MAC | SHA-256, HMAC-SHA-256, blake3 (keyed) | FIPS 180-4 / FIPS 198-1 + non-FIPS |
The PQ primitives moved off the C-bound pqcrypto-* crates to the RustCrypto
FIPS-203 / FIPS-204 implementations, and those two are now pure Rust on every
target.
The crate as a whole is not C-free, and this section used to say it was. On a
default build three dependencies run a build script that invokes cc:
| Dependency | Why | Reached from |
|---|---|---|
ring |
the AEAD substrate (crypto::adaptive_crypto) — 11 architecture-independent .c files plus per-arch assembly |
classical-crypto (on by default) |
blake3 |
the default KDF (crypto::kdf::derive_key_32) — SIMD assembly on x86-64, blake3_neon.c on aarch64 |
std |
zstd-sys |
the zstd C library | compression-zstd (on by default) |
wasm32-unknown-unknown is no exception. Building the crate for that target with
the feature set CI's cross.yml uses compiles 12 C objects out of ring and
36 out of zstd-sys; re-derive it rather than trusting this paragraph:
cargo tree --manifest-path core/Cargo.toml -i cc -e normal,build \
--no-default-features --features std,compression-zstd,classical-crypto \
--target wasm32-unknown-unknown
# and, after building that row:
find target/wasm32-unknown-unknown/debug/build -name '*.o' | wc -l--features fips does not help: it swaps ring for aws-lc-rs, which builds
AWS-LC through cmake and needs more of a C toolchain, not less.
The one genuinely C-free build is the bare-metal row —
--no-default-features --features embedded,no-std — where cc is not in the
dependency graph at all and the build emits no object files. That row is also the
one without the handshake: no ring, no zstd, and the PQ crates and
PhantomSession are std-gated out of it (see
Status & limitations). So "C-free" and "post-quantum
session" are, today, two different builds — which is the honest form of the claim
this section previously made.
┌─────────────────────────────────────────────────────────────────┐
│ Public API (core/src/api/) │
│ PhantomSession · PhantomListener · TcpSessionTransport │
├─────────────────────────────────────────────────────────────────┤
│ Transport (core/src/transport/) │
│ Handshake{Client,Server} · Session · scheduler · pacer · paths │
│ api/{tcp,udp}_transport · legs/{websocket, wasi, embedded} │
├─────────────────────────────────────────────────────────────────┤
│ Crypto (core/src/crypto/) │
│ hybrid_kem · hybrid_sign · adaptive_crypto · kdf · pow · rng │
├─────────────────────────────────────────────────────────────────┤
│ Security (core/src/security/) │
│ ReplayWindow │
├─────────────────────────────────────────────────────────────────┤
│ Runtime (core/src/runtime/) │
│ TokioRuntime (native) · WasmRuntime · EmbeddedRuntime (scaffold) · WasiRuntime │
└─────────────────────────────────────────────────────────────────┘
The formal architecture spec is
docs/architecture/ARCHITECTURE.md;
the unified wire protocol (incl. 0-RTT) is
docs/protocol/PROTOCOL.md. The eleven
numbered security invariants cited throughout the code are listed in
docs/security/invariants.md,
and the threat model is
docs/security/threat-model.md.
- Single unified wire protocol. One
PacketHeader(15 bytes on the wire, fully header-protected — the variable header fields and the leadingversionbyte are HP-masked; the AEAD AAD image is 47 bytes) wrapped in a barePhantomPacket(header+payload+ TLV-headroomextensions) — noVersionedPacketenum, no per-session wire-version negotiation.epoch,REKEY,PATH_VALIDATION,COALESCED,WINDOW_UPDATEflags live in the one header; the recv path deserializesPhantomPacketdirectly and drops any frame whoseheader.versiondiffers. Handshake messages are borsh structs: oneClientHello(with the optional 0-RTTearly_datablob folded in) and three server replies —ServerHello,HelloRetryRequest,ServerReject— carried under a one-byte-discriminantServerReplywrapper; one signedHandshakeTranscriptleads withprotocol_variant. The pinned bytes areWIRE_VERSION= 8 andPROTOCOL_VERSION= 5; both are tamper-check anchors and a hook for a future deliberate bump. - Path validation (wired into the live UDP data plane).
PathRegistry+ constant-time challenge/response; path 0 pre-validated, secondary paths transitionUnvalidated → Validating → Validated. It backs the PhantomUDP data plane end-to-end — the PATH-001 send-gate + recv-relax, seamless connection migration (one live path at a time), and passive-NAT-rebind recovery. Only bandwidth aggregation / simultaneous multipath is unbuilt (rejected for this workload — see Status & limitations).
Two kinds of number live here and neither substitutes for the other. The first set is loopback and in-process: it measures the cryptography and the packet codec on one machine, and says nothing about how the transport behaves on a path. The second set is from a real route, and every figure there is printed beside the raw no-protocol control measured in the same run: on that route the one-way downward ceiling moved by a factor of three between two campaigns five days apart, and the upward one by about a third within a single day, so a throughput number without its own control is not a result.
Reference numbers on Apple M1 Pro (8P + 2E, 16 GiB), macOS 26.0, rustc 1.93.0,
ring with ARMv8 AES-PMULL (snapshot 2026-05-17, criterion --quick, default
target-cpu). The snapshot predates the current wire — it was captured under
WIRE_VERSION = 2, whereas the shipped format is
WIRE_VERSION = 8 — so the crypto / throughput shape
is representative but re-capture before quoting these as live figures (see
BENCHMARKS.md):
| Path | Number | Notes |
|---|---|---|
| Hybrid PQ handshake (pinned) | 1.06 ms / ~945 conn/s/core | full production path; ~7,500 cold handshakes/s aggregate on 8P cores |
| AEAD encrypt, 64 KiB | 4.67 GiB/s/core (13.1 µs) | encrypt_packet with header-AAD + replay window |
| AEAD decrypt, 16 KiB | 4.68 GiB/s/core | same path |
| 1 MiB round-trip | 391.5 µs → 5.0 GiB/s | encrypt + decrypt |
Raw AES-256-GCM (ring) |
5,514 MiB/s at 64 KiB | bare cipher, no framing |
| ChaCha20-Poly1305 (software) | 1,555 MiB/s at 1 MiB | ~3.5× slower than AES on this part |
| ClientHello parse + cookie + reputation | 4.60 µs / ~217K/s/core | DoS gate hot path |
kem_encapsulate |
80.7 µs | hybrid X25519 + ML-KEM-768 |
hybrid_sign / hybrid_verify |
310.0 µs / 131.6 µs | Ed25519 + ML-DSA-65 |
RUSTFLAGS="-C target-cpu=native" typically adds +5–10%; PGO via cargo-pgo
adds another +5–10% on stable workloads. The release profile (opt-level=3,
lto="fat", codegen-units=1, panic="abort") is set at the workspace root.
Linux x86_64 with AES-NI lands in similar ballparks. Full methodology and
production tuning (bbr, fq, LimitNOFILE, allocator swap, CPU pinning) in
BENCHMARKS.md and docs/operations/perf-tuning.md.
Measured by testbed/: a probe on a
workstation drives a scenario matrix against a daemon on a remote host. The
daemon binds the three Phantom legs, a quinn QUIC reference leg — a mature
implementation of the same class of protocol, on the same path, in the same run
— and raw UDP controls that carry no protocol at all: an echo, and a one-way
capacity ladder in each direction. The raw control is the denominator; the
reference is a second opinion, not a ranking. (A raw TCP echo runs as well, but a
TCP socket brings its own congestion control, so it is a second reference rather
than a control.)
Every rate is counted by the end that received it: the server on an upload, the
client on a download. A sending side's own count would be measuring how fast
send() filled a buffer. Every run in the tables below had both ends built from
one commit, verified from the run manifest.
2026-09-05 — two campaigns on the same day, six hours apart, three runs each: the last full campaigns before this release. Uploads ran for 60 s and downloads for 180 s, and every PhantomUDP transfer converged rather than ending mid-ramp.
| Upload | Phantom UDP | quinn (reference) | One-way upward control, same run |
|---|---|---|---|
20260905-032309 |
28.11 Mbit/s | not run | 84.97 |
20260905-033508 |
27.02 | not run | 84.26 |
20260905-034708 |
28.33 | not run | 84.49 |
20260905-103139 |
26.03 | 28.03 | 58.64 |
20260905-105207 |
27.10 | 65.85 | 59.29 |
20260905-111225 |
23.71 | 31.54 | 55.56 |
| Download | Phantom UDP | quinn (reference) | One-way downward control, same run |
|---|---|---|---|
20260905-032309 |
16.30 Mbit/s | not run | 76.51 |
20260905-033508 |
16.38 | not run | 76.64 |
20260905-034708 |
16.45 | not run | 76.77 |
20260905-103139 |
11.46 | 0.33 | 76.53 |
20260905-105207 |
12.07 | 0.49 | 76.47 |
20260905-111225 |
11.13 | 0.36 | 76.51 |
Read these before reusing any of them:
- The two campaigns ran the same library. Between their two builds
core/srcdiffers by one compile-time assertion and one test, so what differs between the morning rows and the afternoon rows is the route. - The upward control fell by about a third within the day — 84.26–84.97
Mbit/s in the morning, 55.56–59.29 in the afternoon — and in
105207quinn delivered 111% of that run's control, which says the ladder under-read the path rather than that quinn beat it. A share of the upward link is therefore not yet a reliable figure. The morning's 32–34% is quotable against its own control; the afternoon's is not. - Upload against quinn: 1.1–2.4× in quinn's favour, and the reference is the noisier of the two. Across three consecutive runs it moved 2.3×, while this leg moved 1.14×.
- Download against quinn goes the other way, and the route explains it. The downward control held its 76.5 Mbit/s ceiling in both campaigns, but in the afternoon it lost 1.0–7.3% of datagrams at 1 Mbit/s and 2.0–9.9% at 5 Mbit/s, far below that ceiling, where in the morning it lost at most 0.4% on the same rungs. quinn ships a loss-based controller (Cubic), which reads each of those losses as congestion, and its 0.33–0.49 Mbit/s is within what the Mathis et al. formula predicts for such a controller at that loss and a round trip of about 200 ms. This transport paces to a measured delivery rate instead, and held 11.13–12.07 Mbit/s. That is a statement about the route, not a ranking.
- Duplex — the download half of a transfer running both ways at once, against the one-way download of the same run: 81–89% in the morning, 49–63% in the afternoon.
- No released build is the build measured here — not 0.3.0 and not 0.4.0. One
congestion-control change landed after these campaigns — the loss-driven volume
bound no longer applies during Startup — and it has so far been measured only in
the in-tree bottleneck model (see
CHANGELOG.md), not on this route. Nothing in 0.4.0 touches the data plane's rate behaviour, so the gap is the same one change wide as it was at 0.3.0.
2026-08-17 and 2026-08-22. Upload only, and against a round-trip echo, because no one-way upward control was taken in these runs:
| Run | Phantom UDP | quinn (reference) | Raw UDP echo, same run |
|---|---|---|---|
2026-08-17 124710 |
16.51 Mbit/s | 5.81 | 33.52 round-trip |
2026-08-17 130955 |
18.96 | 40.52 | 31.61 round-trip |
2026-08-22 061422 |
1.96 | 9.13 | 12.17 round-trip |
2026-08-22 062705 |
did not establish — Timeout on connect |
9.38 | 13.26 round-trip |
- Rows from different campaigns are not comparable. The route on 2026-08-22 was materially worse than on 2026-08-17: the same raw UDP echo control read 12.17 / 13.26 Mbit/s against 33.52 / 31.61, and the one-way downward ceiling read 21.09 / 20.90 against 60.45 / 62.97. Within one campaign the control holds steady and the rows can be read against each other.
- The reference moved sevenfold between two adjacent runs on the same route (5.81, then 40.52). A single comparison against quinn is not a ranking, in either direction.
- August downloads are not quoted. In the two 2026-08-17 runs above, PhantomUDP's download was still accelerating when its window closed, so its 3.57 / 4.84 Mbit/s measured a ramp rather than a capacity, against a one-way downward control of 60.45 / 62.97; the byte-pipe legs and the reference read 0.55–0.95.
- The
Timeoutin run062705was a lostServerHello, and 0.3.0 fixes it. The server received the hello, completed the handshake and sent its reply — a six-datagram flight with no retransmission of its own — and the reply was lost on the way down. The client repeated its hello three times, and each repeat was routed into the session the server had already committed, which does not parse handshake messages, so one lost datagram out of six cost the whole connect at the end of the client's 8-second budget. A PhantomUDP listener now retains the reply flight and repeats it byte for byte when the same hello arrives again (docs/protocol/PROTOCOL.md§ 6.1). Isolated PhantomUDP connect timeouts appear in earlier runs too — one handshake in ten in each 2026-08-17 run — but those runs did not record what would confirm their cause. - Handshake: a median of 416.0–589.1 ms per run against 254.5–304.4 ms for quinn, in the four runs above. The difference is about one round trip, and it is a design choice rather than a defect: a PhantomUDP listener always answers a first hello with a stateless cookie before it commits any state, so the handshake takes two round trips where QUIC's takes one. The hybrid post-quantum cryptography itself costs about a millisecond (see the loopback table above).
An earlier campaign (2026-08-03, five runs) put upload at 2.40–3.59 Mbit/s,
counted by the server, in the four runs whose upload connected, against a
round-trip echo of 27.75–42.85 Mbit/s from the same runs. Cumulative
WINDOW_UPDATE (the WIRE_VERSION 6 → 7 bump), a segment-idempotent
flow-control charge, and symmetric reliable-byte accounting on both ends of the
ledger landed between that campaign and 2026-08-17. The campaigns are not
comparable, so no ratio between them is claimed.
Two results from the 2026-08-22 campaign are not about speed at all, and are the firmer part of it:
- A departed UDP client no longer holds a server session open. Median server-side UDP session lifetime fell from 135.55 s to 2.19 s, and the share of sessions living past 100 s from 32.5% to 2.3% (986 sessions before the change, 256 after, across all legs).
- The bandwidth estimator's overshoot is mostly filter memory, not bad samples. On a download session of that campaign the filtered ten-second maximum ran at a median 1.52× of what was actually delivered over the same interval, while the raw single sample ran at 1.02× — one session per run, because both quantities are only recorded where the server was the sender.
The harness, its scenarios, and how each figure above is computed — which end
counts, which control is the denominator, when a transfer counts as converged —
are documented in
testbed/README.md.
The rule this section follows is that a throughput figure never appears without
the raw control from its own run.
Production embedder. Auto-loads-or-creates a persistent HybridSigningKey,
pushes OTLP telemetry to an OTel Collector / SaaS backend, handles SIGTERM
/ SIGINT with a 10s drain.
It binds the TCP leg only. phantom-server listens with
PhantomListener::bind_with_signing_key on --bind and opens no UDP socket, so
it does not serve PhantomUDP — the transport recommended above — and the
Dockerfile, compose file and Helm chart below expose and probe TCP 4242
accordingly. A PhantomUDP deployment embeds PhantomUdpListener in its own
server binary; PhantomUdpListener::bind_udp_with_signing_key_bytes accepts the
same 64-byte seed that phantom-cli keygen and phantom-server write, so one
pinned identity can serve both transports.
| Flag | Env | Default |
|---|---|---|
--bind |
PHANTOM_BIND |
0.0.0.0:4242 |
--otlp-endpoint |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
--otel-service-name |
OTEL_SERVICE_NAME |
phantom-server |
--otel-trace-sample-ratio |
OTEL_TRACES_SAMPLER_ARG |
1.0 (root-span head sampling; 0 = no traces) |
--signing-key-file |
PHANTOM_SIGNING_KEY_FILE |
/etc/phantom-server/signing.key (0600, auto-created) |
--log-json |
PHANTOM_LOG_JSON |
false |
--log-filter |
RUST_LOG |
info,phantom_protocol=debug |
--max-sessions |
PHANTOM_MAX_SESSIONS |
1024 (0 = unbounded) |
--max-sessions-per-ip |
PHANTOM_MAX_SESSIONS_PER_IP |
64 (0 = off) |
cargo run --manifest-path server/Cargo.toml -- \
--bind 0.0.0.0:4242 \
--otlp-endpoint http://otel-collector:4317Multi-stage Dockerfile (rust:1-slim-bookworm → debian:bookworm-slim),
non-root phantom UID 65532, EXPOSE 4242 (no inbound metrics port — telemetry
is OTLP push), signing-key volume at
/etc/phantom-server. docker-compose.yml is ready to run with a named volume
and TCP healthcheck.
docker build -t phantom-server:0.4.0 .
docker compose up -dProduction-shape chart at
docs/operations/helm/phantom-protocol/.
appVersion: 0.4.0, ClusterIP service on 4242, 3 replicas,
tcpSocket liveness / readiness. Raw manifests + walkthrough in
docs/operations/kubernetes.md.
Hardened unit text in docs/operations/systemd.md
(NoNewPrivileges, ProtectSystem=strict, MemoryDenyWriteExecute,
SystemCallFilter, 30s TimeoutStopSec) plus a multi-instance template using
SO_REUSEPORT.
OpenTelemetry metrics + traces over OTLP/gRPC (replacing an earlier
hand-rolled Prometheus endpoint). The reference server pushes to
OTEL_EXPORTER_OTLP_ENDPOINT; backends supported include OTel Collector
(→ Prometheus / Tempo / Loki), Datadog, Honeycomb, Grafana Cloud, AWS
CloudWatch — anything OTLP-compatible. Pre-built Grafana dashboard at
docs/observability/grafana/phantom-otel-dashboard.json
and Prometheus alert rules at
docs/observability/prometheus/alerts.yml.
End-to-end docker-compose demo in
examples/observability-demo/. Full setup
recipes in docs/observability/otlp-setup.md.
phantom-cli (sibling crate, edition 2024):
cargo run --manifest-path cli/Cargo.toml -- keygen --out ./server.key
cargo run --manifest-path cli/Cargo.toml -- pubkey --in ./server.key
cargo run --manifest-path cli/Cargo.toml -- ping --host 127.0.0.1 --port 4242 \
--pinned-key-hex <hex-from-keygen> --msg hello
cargo run --manifest-path cli/Cargo.toml -- versionThree different things get called "supported", and the difference decides how much
work an adopter has to do, so they are separated here. Compiled means
.github/workflows/cross.yml runs cargo check --lib for that target on every
push — a hard gate, no allow_failure row, and no more than a compile. There are
thirteen such rows over twelve targets: x86_64-unknown-linux-gnu appears twice,
once with the default features and once with the ring-free fips set.
Prebuilt artifact means a tagged release publishes a library tarball for it
(release.yml, job build-artifacts), with a sigstore-backed provenance
attestation. Tests executed means test code has actually run on that target.
| Target | Compiled in CI | Prebuilt artifact | Tests executed there |
|---|---|---|---|
x86_64-unknown-linux-gnu |
yes | yes | the whole suite — every job in ci.yml (unit, security_invariants, property, wire vectors, KATs, TCP + PhantomUDP loopback, and the embedded / mimicry / telemetry-otel / fips feature jobs) |
aarch64-apple-darwin |
yes | yes | one test: the Swift binding's pinned loopback round-trip, on macos-latest (bindings.yml, job swift) |
x86_64-apple-darwin |
yes | yes | none |
aarch64-unknown-linux-gnu |
yes (via cross) |
yes | none |
aarch64-unknown-linux-musl |
yes (via cross) |
no | none |
x86_64-pc-windows-msvc |
yes, on a real windows-latest runner |
no | none |
aarch64-pc-windows-msvc |
yes, on a real windows-latest runner |
no | none |
aarch64-apple-ios (device) / aarch64-apple-ios-sim |
yes | no | none |
wasm32-unknown-unknown |
yes | no | none — examples/wasm-demo/ is driven by hand |
wasm32-wasip2 |
yes | no | two: the guest fixture's round-trips, executed under wasmtime on a Linux host (cross.yml, job wasi-integration). WASI is client-side framing-only — see docs/operations/wasi.md |
thumbv7em-none-eabihf (--no-default-features --features embedded,no-std) |
yes | no | none — the embedded feature's tests run on the x86_64 Linux host, not on the device |
aarch64-linux-android / armv7-linux-androideabi / x86_64-linux-android |
no | no | none |
If you are not on x86_64 Linux, read this. The risk is not that a target is unpackaged; it is that nothing has ever been run there. On Windows, in particular: the crate compiles for both MSVC targets on a real Windows runner, and not one unit test or loopback integration test has ever executed on Windows. There is no artifact either, so a Windows adopter builds the crate themselves, stands up their own cross-build if they need a cdylib for a binding, runs the suite on the target themselves, and owns any platform-specific failure it turns up — sockets, path handling and timer behaviour included. The same applies to iOS, musl, browser wasm and bare metal.
Android is the widest gap, because the tree looks equipped and CI is not: a grep
for "android" across all eight workflows returns nothing, while
tests/bindings/kotlin/build-jnilibs.sh cross-builds three ABIs
(aarch64-linux-android, armv7-linux-androideabi, x86_64-linux-android) and
examples/mobile/android/
is a complete Jetpack Compose application. Both are run by hand, against an NDK the
repository does not pin. Treat the Android path as a recipe that worked when it was
written, not as a gated one.
This is a deliberate deferral rather than an oversight, and what closing it would
take is written down in
docs/DEFERRED_WORK.md § 5.
| Binding | Maturity | Notes |
|---|---|---|
| Swift | Generated; one loopback test runs in CI | Auto-gen via UniFFI 0.32. bindings.yml's swift job builds the cdylib on macos-latest and runs a pinned loopback round-trip through the binding. The iOS XCFramework is built and shape-checked by build-xcframework.sh + check_xcframework.sh, by hand — no workflow runs either, and no test has run on an iOS target; recipe in docs/operations/mobile.md |
| Kotlin | Generated; compile-checked only | Auto-gen. run_kotlin_test.sh type-checks the generated Kotlin against JNA + coroutines on Linux and does not execute it. No workflow builds or tests an Android target: the NDK + Gradle jniLibs recipe in mobile.md and build-jnilibs.sh (three ABIs) are manual — see Platform support |
| Python | UniFFI surface auto-gen | Demo harness tests/run_test.py |
| C | Experimental | Hand-curated header — UniFFI 0.32 has no C generator. Covers connect_pinned / connect_pinned_udp (incl. _with_config / _with_resumption), the bind*_with_signing_key_bytes / bind*_with_config_bytes constructors, generate_signing_key, and PhantomConfig; the typed HybridSigningKey / HybridVerifyingKey objects and runtime injection stay Rust-only. README recommends Swift / Kotlin / Python instead |
| WASM (browser) | Demo shipped | examples/wasm-demo/ pairs with docs/operations/wasm.md; uses WebSocketLeg + WasmRuntime |
Regen: tests/bindings/{generate_python,generate_swift,generate_kotlin,generate_c}.sh.
On bare-metal thumbv7em-none-eabihf Phantom Protocol ships the framing transport
only — EmbeddedLeg and its length-prefix codec. The PQ handshake,
PhantomSession, the crypto primitives, and TokioRuntime are std-gated and
not built there; a bare-metal embedder brings its own crypto/handshake driver
and runs it over the leg. PQ-on-bare-metal is descoped for 1.0 (see
Status & limitations).
EmbeddedLeg<R, W, const N: usize> wraps any embedded-io-async = 0.7 byte
stream (UART, USB-CDC, …) with 4-byte BE length-prefix framing — the same wire
shape as TcpSessionTransport. Pure-Rust, no_std + alloc, target-arch-agnostic
(builds on host x86_64 for unit tests and on bare-metal thumbv7em-none-eabihf).
Per-(R, W) SessionTransport impl via the impl_embedded_session_transport!
macro. RngProvider trait injects a hardware RNG when getrandom isn't
available. core/examples/embedded_demo.rs runs
the full session over a mock byte stream on a host (std), demonstrating the
leg — not a bare-metal handshake.
Full threat model, mitigations, and disclosure policy are in
SECURITY.md and
docs/security/threat-model.md. Headline points:
- Mandatory server identity pinning —
connect_with_transportrequires aHybridVerifyingKey; no skip path. - Forward secrecy — ephemeral hybrid KEM per handshake + HKDF-based
mid-session rekey (epoch saturates at
u8::MAX, never wraps). - Replay rejection happens after AEAD verify — RFC 4303 §3.4.3 sliding-window bitmap, per-direction.
- Downgrade resistance — the pinned protocol version and
protocol_variantare signed under the handshake transcript; stripped-ENCRYPTEDpost-handshake packets are dropped. - 0-RTT anti-replay — the server
peek()s the ticket, verifies theClientHello.resumption_binderin constant time (proof-of-possession), then eagerlyremove()s it, so a ticket is strictly one-shot (and is re-inserted unchanged if the handshake later fails); oversized / expired / AEAD-failing early-data is best-effort and never fatal to the handshake. - AEAD nonce-exhaustion guard —
CryptoError::NonceExhaustedatAEAD_MAX_INVOCATIONS = 2^48. ZeroizeOnDropon all key-bearing structs;#![deny(unsafe_code)]crate-wide with two audited opt-ins (transport/legs/websocket.rswasm-bindgen glue,transport/legs/wasi.rsWIT-bindgenSend/Sync) — both cross-language-boundary glue, so a native build compiles nounsafeat all.- Cancel-safety audit: zero bugs found across all
tokio::select!sites. - Documented production panic sites with
PANIC-SAFETY:invariants — seedocs/security/panic-sites.md.
No FIPS validation and no Common Criteria evaluation exist, and none is in progress. The files under
docs/compliance/are self-authored readiness/gap analyses, not certifications — do not rely on them for any compliance claim.
- FIPS 140-3: the crypto uses several FIPS-approved primitives (ML-KEM-768,
ML-DSA-65, Ed25519, SHA-256, HMAC/HKDF-SHA-256), and an optional
fipsCargo feature swaps the remaining non-approved primitives toward anaws-lc-rssubstrate. This is not a validated cryptographic module (no CMVP). Gap analysis:docs/compliance/fips-readiness.md; CAVP-style known-answer vectors incore/tests/cavp.rs. - Common Criteria: an internal SFR gap-mapping exercise against NIAP
PP-Module VPN Client exists for design reference only
(
docs/compliance/cc-pp-mapping.md). No lab evaluation is planned.
Report privately, not via public issues. Embargo SLA 90 days; ack within
5 business days, triage within 14. Contact in SECURITY.md.
cargo deny (permissive-license allowlist, yanked = "deny",
unknown-registry = "deny") and cargo audit run in CI. Release artifacts
carry sigstore-backed in-toto build-provenance attestations via
actions/attest-build-provenance@v4 (SHA-pinned). Every artifact is covered, and
the attestation names the workflow, the commit and the runner that produced it, so
a tarball claiming to be a release of this crate can be checked against a
signature only a run of this repository's workflow can produce. Verify with
gh attestation verify --owner <org> <artifact> or
cosign verify-blob-attestation.
That is SLSA v1.0 Build Level 2, not Level 3. L3 asks that the build run
somewhere the provenance signing identity is not reachable from the build steps
themselves; here the attest step sits inline in the same build-artifacts job
that compiles, and that job restores a Swatinem/rust-cache shared with the rest
of CI. Reaching L3 is a workflow change, not a code change — see
docs/DEFERRED_WORK.md §1.
Maturity: early. Not production-ready. This is a single implementation with no external security audit. The cryptographic handshake + identity layer and the UDP data plane (SACK loss recovery, congestion control, connection migration) are implemented and tested — against a deterministic fault-injection transport (loss / reorder), the
udp_integrationloopback suite, and, since August 2026, a real WAN route measured against a QUIC reference and raw no-protocol controls (see Performance). What that measurement does not cover is route diversity: one server, one client, one provider pair, no mobile carrier, no satellite, no lossy radio. The gating limitations are the absence of an independent security audit, the pre-1.0 wire churn, and that single route — not an unfinished data plane. Do not protect anything high-risk with this until it has been independently audited.
- No release has been reviewed by a second person, and there has been no
external security audit. Of 229 pull requests, none carries a review.
main's branch protection hasrequired_pull_request_reviewsunset andenforce_adminsfalse, so its 35 required status checks are the entire gate: every design argument indocs/, every security invariant and every line of crypto here was written and merged by one person.CODEOWNERSauto-requests review on the six security-sensitive paths, which is a request and not a requirement — the file says so itself. Nothing about this is hidden in the history; it is stated here because a reader weighing the library cannot see it from the outside. What would change it: a second maintainer, plus "Require approvals" and "Require review from Code Owners" enabled onmain. Until then, read the code rather than the rationale, and do not protect anything high-risk with it. - Platform coverage is narrower than the matrix suggests. Thirteen hard
compile gates over twelve targets, four prebuilt artifacts, and a test suite that
runs on x86_64 Linux plus one loopback handshake on aarch64 macOS. Windows, iOS, musl, browser wasm and
bare metal compile and never execute; Android is in no workflow. See
Platform support and
docs/DEFERRED_WORK.md§ 5. - Pre-1.0 (
0.4.0). Wire format may break between minors; SemVer applies once 1.0 ships. The current wire protocol is a single pinned version — the former V1/V2/V3 axes were collapsed pre-1.0, with no negotiation and no fallback, so there are no cross-version migration guides. 0.4.x and 0.3.x peers do interoperate; 0.2.x peers do not: this release speaksWIRE_VERSION8 andPROTOCOL_VERSION5, which is what 0.3.0 speaks, so either end of a 0.4/0.3 pair may be upgraded on its own and a CI job proves it in both directions with each version as the server. One behaviour in such a pair is still not symmetric, and it is not a wire mismatch: a 0.4.0 client may open a 256th concurrent stream that a 0.3.0 server refuses in silence, because 0.3.0 counted the reserved raw-application stream against the same cap. Keep to 255 concurrent streams against a peer whose build you do not know —docs/known-deviations.md§ 5 has what the stuck stream looks like. 0.2.x spoke 6 and 3, and the handshake refuses that mismatch with a typedServerRejectrather than negotiating down. A 0.3 → 0.4 upgrade asks two things of you, both in your own files rather than in your code. Name thetokioandtimefeatures your own code uses, in your own manifest —signal,process,fs,io-stdandtime/std: this release stops asking for them, and Cargo's feature unification is what had been handing them to you, so code that never mentions this crate can stop compiling. Doing it first is safe and is correct against every version. And regenerate the language bindings rather than relinking them, which holds for every upgrade in this series: every UniFFI checksum moved in 0.3.0 and eleven move again here, becauseuniffifolds an exported item's doc comment into its checksum and this release corrects eleven of those comments.UNIFFI_CONTRACT_VERSIONis unchanged, so the coarse gate passes and a stale binding fails at import time in your process instead. The features, the crates that leave and the eleven checksums are each listed inCHANGELOG.md. - Native UDP transport (PhantomUDP): handshake + demux + reliability shipped.
PhantomSessionruns an authenticated session over TCP, WebSocket, and raw UDP (connection-ID demux, server accept, fragmented handshake). The UDP data plane has SACK-based loss recovery (RFC-9002-style fast-retransmit) + an RFC-6298 RTO- a BBR-style congestion controller + mid-session rekey, exercised over a
deterministic fault-injection transport (loss / reorder —
test_harness/fault_transport.rs) and theudp_integrationsuite. Seamless connection migration (one live path at a time, Wi-Fi↔cellular without re-handshake) shipped too. The earlier experimental KCP / FakeTLS legs and the unusedTransportLegmultipath trait were removed (never wired into the data plane). Bandwidth aggregation across transports is not planned — analysis showed it regresses for this workload and harms unobservability. What remains: an external audit, and route diversity — every WAN figure in this README comes from one route between one pair of hosts.
- a BBR-style congestion controller + mid-session rekey, exercised over a
deterministic fault-injection transport (loss / reorder —
- TLS-mimicry transport (
mimicryfeature, off by default). AMimicTlsLegmakes a Phantom flow look like ordinary HTTPS (a synthetic TLS 1.3 handshake, then the session inside ApplicationData records) to defeat DPI that blocks unknown/high-entropy traffic. The outer TLS is anti-DPI obfuscation only — the handshake is theater (no real ECDHE/cert) and holds no keys; the inner Phantom PQ session is the sole auth/conf. It defeats parsers, not provers: a determined active-probing censor that completes a real TLS handshake detects it in one round trip, and against such an adversary it is net-negative. Use only where the threat is passive/commercial DPI, not active probing. Honest residuals- SAFE/UNSAFE guidance in
docs/security/threat-model.md§6.1.
- SAFE/UNSAFE guidance in
- Mobile connection migration (Wi-Fi ↔ LTE): use the UDP transport for real
migration, or reconnect with 0-RTT on TCP.
PhantomSession.migrate()performs real single-path seamless migration when the session is backed byUdpClientTransport(viaconnect_pinned_udp). On TCP-backed sessions it returnsErr(CoreError::Unsupported). On a network change with TCP, reconnect — folding the first request in viaconnect_pinned_with_resumptionto minimise cost. Theexamples/mobile/sample apps demonstrate the reconnect-with-0-RTT model; seedocs/operations/mobile.mdfor the UDP migration path. - Work deferred past 0.2.0 — hermetic/reproducible builds, the
no-stdPQ handshake, WASI server-side sessions, and ECN congestion feedback — is consolidated with rationale indocs/DEFERRED_WORK.md. - Loss recovery and congestion control: measured on a real route — on one
route. The UDP data plane has RFC-9002-style SACK + dup-ACK fast-retransmit,
an RFC-6298 RTO, a BBR-style congestion window, and mid-session rekey. It is
exercised by
udp_integrationovertest_harness/fault_transport.rs(injected loss + reorder) and by the WAN campaigns above, where it runs beside a QUIC reference and raw controls on the same path in the same run. That instrument is what found the congestion-control defects fixed in 0.3.0 (recorded inCHANGELOG.md), none of which the test suite could see: at a loopback round trip of 0.4 ms a 5600-byte congestion window still yields 112 Mbit/s, and the same window on a 210 ms path yields 0.213. What is still missing is a second route and an external review — treat the data plane as measured-on-one-path, not battle-tested. - What the transport does not reach, stated as measurements rather than as
work items. These are properties of the builds measured on the one route
there is — those of the 2026-09-05 campaigns, which differ from this release
by the one Startup change noted under Performance — and none
of them is a defect with a fix pending. Every throughput figure taken on the
route is against a raw no-protocol control from the same run.
- Upload reaches about a third of the measured ceiling. The morning campaign of 2026-09-05 put it at 27.02–28.33 Mbit/s against an 84.26–84.97 Mbit/s one-way upward control from the same runs — 32–34% of the link, up from 22–24% against the same 84 Mbit/s control on 2026-08-26, before the loss-response change. The transfer reached its final rate about six seconds into its sixty, where the 2026-08-26 runs took 13–18 s. A third of the link is a real gap, and the reason it is quotable at all is that both the numerator and the denominator come from the same run. It is quotable only against that run: six hours later the same control read 55.56–59.29 Mbit/s on the same route with the same library, so a share of the upward link is not yet a stable figure.
- Duplex runs short of the slower one-way direction, by an amount that moves with the route. The download half of a duplex transfer, against a one-way download from the same run, ran at 81–89% in the morning campaign of 2026-09-05 and at 49–63% six hours later, when the downward control had started losing 1–10% of datagrams at low rates. The criterion the project set for itself was "reproducibly no worse than the slower one-way", and neither campaign meets it. Within each campaign the three runs agree to within 14 percentage points, where the three 60-second runs of 2026-08-26 spread across 35 (52–86%).
- The ARQ send buffer is bounded in segments, not bytes. At 1024 segments
it is 1024 × the segment size, so on a small application frame it binds
about four times sooner than the peer's window does: a
send_ceilingsweep reads 266,240 B of inflight on a 256-byte frame against 1,048,492 B on a 2308-byte one, in three runs each. An application that writes small messages pays for that; one that writes large ones does not notice. - The TCP leg carries two congestion controllers stacked. Phantom's own
BBR-style controller runs inside a TCP connection that has one of its own.
The leg exists to cross networks that pass TCP and nothing else, not to go
fast; the UDP leg is the production path and the only one where
migrate()works. - Against a mature implementation of the same class, on the same path, in the same run. On upload, in the three afternoon runs of 2026-09-05, quinn delivered 1.1–2.4× what this transport did (28.03 / 65.85 / 31.54 against 26.03 / 27.10 / 23.71 Mbit/s, counted by the server) against a one-way upward control of 58.64 / 59.29 / 55.56 from the same runs. The reference itself moved 2.3× across those three runs while this leg moved 1.14×, and in one of them quinn delivered 111% of the control, so the control under-read the path. That control moved between 55 and 85 Mbit/s within one day, so shares of the upward link are not yet reliable. On download, in the same runs, the route lost 1–10% of datagrams far below its 76.5 Mbit/s ceiling, and this transport held 11.13–12.07 Mbit/s where quinn's loss-based controller held 0.33–0.49 — more than an order of magnitude. In a loopback run of the download scenario, with no route and no raw control beside it, quinn moved roughly three times what this transport did. The size of the gap in either direction is a property of the path rather than a ranking.
- Negative-security suite: 73 always-on tests in
core/tests/security_invariants.rs, covering most — not all — of the eleven numbered security invariants (listed indocs/security/invariants.md): identity pinning, the unencrypted-packet receive gate, replay rejection, rekey and epoch handling, path validation, transcript binding of the 0-RTT verdict, and 0-RTT ticket handling. Three are pinned elsewhere and deliberately not here: the two FIPS invariants (build-mode transcript binding, power-on self-tests) belong to a build this suite does not compile and are gated by thefips-featureCI job, and the TLS-mimicry parser bounds ride the off-by-defaultmimicryfeature and its own job. Two more are pinned in part — the AEAD nonce ceiling (2^48) is not reachable from a test, so what is pinned is the counter feeding it, and the path-validation tests cover the state machine rather than its constant-timeness, which is an audit (docs/compliance/constant-time-audit.md) and not a measurement. Where only part of an invariant is pinned, the tests say so. Plus the proptest, fuzz, wire-vector, runtime-integration, and CAVP suites, the library's own unit tests —cargo test --manifest-path core/Cargo.toml --libprints how many, which is the form this claim takes now because the count it used to name had been wrong by 65 for a release — and#[ignore]-gated loopback integration suites (TCP, UDP — including injected loss/reorder via the fault transport — WASI, TLS-mimicry). 0 workspace warnings, 0 clippy warnings. Note: broad test coverage, a fault-injection rig, and a WAN measurement campaign are not a substitute for an external security audit. - Broad feature coverage across the planned phases, but not production-ready. The handshake / identity / data-plane / observability / cross-target work is in place and tested; what remains open is an external security audit, CMVP/CC validation, formal verification (ProVerif / Tamarin), and a soak across more than one route — none done.
PhantomListener::bind()generates a fresh signing key per process — identities don't survive restart. Pin-stable production deployments must usebind_with_signing_key()with a key loaded from disk (phantom-cli keygenwrites 0600 seed files).- The library ships no HTTP server. The library exposes OTel
instruments; embedders configure the exporter.
server/src/telemetry.rsis the reference OTLP/gRPC wiring. - MSRV: Rust 1.93 (raised from 1.75 — the PQ crates pull
pkcs8 0.11→ edition2024 → Rust ≥1.85; the CI gate is 1.93).cli/uses edition 2024. - 7 fuzz harnesses, run in CI (
.github/workflows/fuzz.yml: 60 s per target per PR, 600 s nightly). Fuzzing needs nightly; onlyfuzz_embedded_framing's body also compiles on stable. - Embedded is framing-only on bare-metal. The
thumbv7em-none-eabihfbuild (--no-default-features --features embedded,no-std) shipsEmbeddedLeg+ its length-prefix framing; the PQ crypto, handshake, andPhantomSessionarestd-gated out. PQ-on-bare-metal is descoped for 1.0 — the RustCrypto primitives needallocplus entropy/heap the embedder supplies, and a real bare-metal handshake (no-std crypto, an Embassy/RTIC runtime, a QEMU-hosted handshake test) is a separate sub-project. Bare-metal embedders run their own crypto over the leg. The hardthumbv7emCI gate iscargo check --lib— it proves the framing compiles, not that a session runs.
- Architecture:
docs/architecture/ARCHITECTURE.md; contributor workflow in CONTRIBUTING.md - Wire protocol:
docs/protocol/PROTOCOL.md(the single unified protocol, incl. 0-RTT) - Security:
SECURITY.md,docs/security/threat-model.md,docs/security/incident-response.md,docs/security/cancel-safety-audit.md,docs/security/panic-sites.md - Compliance:
docs/compliance/—fips-readiness.md,cc-pp-mapping.md,constant-time-audit.md,rng-audit.md,key-management.md,self-tests.md,fips-security-policy.md - Operations:
docs/operations/—perf-tuning.md,deployment.md,docker.md,systemd.md,kubernetes.md(+helm/),mobile.md,wasm.md,wasi.md,zero-rtt.md - Known deviations:
docs/known-deviations.md— behaviour that is deliberate, documented and has still surprised a consumer: read it before filing a bug - Deferred work:
docs/DEFERRED_WORK.md— capabilities consciously deferred, including the platform-coverage gap - Policy:
docs/policy/versioning.md - Performance:
BENCHMARKS.md(loopback benches),testbed/README.md(the WAN measurement harness, and how each real-route figure is computed) - Change log:
CHANGELOG.md
See CONTRIBUTING.md. PRs must pass cargo fmt --check,
cargo clippy --lib -- -D warnings, cargo test --lib, and cargo deny check.
The cli-check CI job requires that core API edits keep
cli/ building.
Developed with AI assistance from Anthropic's Fable 5 via Claude Code. All architectural decisions, security invariants, threat model, and FIPS / CC compliance artifacts are authored, reviewed, tested, and maintained by the human author.
Apache License 2.0. See LICENSE.