№ 1  Samizdat
A censorship-circumvention protocol from Lantern

Samizdatсамиздат: copied by hand, passed on in secret

Proxy traffic inside an ordinary TLS 1.3 and HTTP/2 connection to a real website, with the start of each tunnel connection shaped like real TLS.

01 — what a censor sees

Two kinds of observer

A censor can watch a connection, or it can poke at the server. Samizdat is built so that both see a browser and a website. The client connects to the server's IP address and names a cover site in the TLS handshake. The server is not that site. It holds the key to tell its own clients from everyone else, and hands everyone else to the real cover site.

Watching the wire

  1. A TLS 1.3 ClientHello from uTLS with Chrome's fingerprint, naming the cover site.
  2. The authentication tag sits in the 32-byte SessionID, which browsers fill with random bytes in every TLS 1.3 ClientHello anyway. It's sealed to the server's key and to that exact ClientHello, and no part of it repeats between connections.
  3. The first ten TLS records after the handshake follow sizes and directions drawn from a model of real TLS flows, not the fixed opening of an HTTP/2 tunnel.
  4. After that, a long-lived HTTP/2 connection carries the tunnelled streams, up to 100 at a time, like a browser's.

Probing the server

  1. Any ClientHello without a valid tag, or one that has already been used, is forwarded byte for byte to the real cover site the server is configured with. (Probes that don't start with a TLS handshake are closed; see Limits.)
  2. The prober completes a real handshake with the cover site's real certificate and gets its real HTTP responses. The server doesn't rewrite or decrypt any of it.
  3. Replaying a captured ClientHello doesn't work either: the server remembers the SessionIDs it has accepted for 24 to 48 hours (less under very heavy load), and a version 2 tag expires 12 hours either side of the server's clock.
  4. Clients can pin the server's certificate, so a client that's been sent to the cover site fails instead of talking to it.
02 — the first packets give it away

What CenKL found

In late 2025, researchers at the University of Michigan and elsewhere recorded 326,531,763 real TLS flows at a regional ISP and built a model of how real TLS connections begin: the size and direction of each packet after the handshake. They then measured how far nine circumvention protocols stray from it, using KL divergence (in nats; 0 means identical).

Their conclusion: all of them are distinguishable from real-world TLS, as early as the first post-handshake packet.

1208 bytes

Trojan, VMess, VLESS and XTLS Vision send unusually many 1,208-byte packets in their first three packets after the handshake. Real TLS does that at the first packet only 0.38% of the time. The cause is Go's dynamic TLS record sizing, which Xray leaves on.

CenKL §4, Fig. 3 a–d
71.7%

of XTLS Vision connections send a 1,208-byte packet first. Vision pads the inner TLS handshake to 900–1,400 bytes, and 38.4% of that range is above 1,208, so the padding makes the record-sizing tell more common, not less.

CenKL §4
28.88 nats

from NaïveProxy's first packets, and 27.81 from VLESS-over-gRPC. HTTP/2 tunnels open with a connection preface, SETTINGS and a CONNECT in a fixed pattern, which real TLS rarely produces. Samizdat's tunnel is HTTP/2 too.

CenKL §4, Fig. 3 f–g
0.977 → 0.830

The authors' countermeasure, ProxyShaper, reshapes a connection's first 10 TLS records with sizes and directions drawn from their model. Against VLESS, it cut a gradient-boosted classifier's precision from 0.977 to 0.830. It helps; it doesn't make a proxy invisible.

CenKL §5, Table 3

Samizdat, measured against the same model

Samizdat now does what ProxyShaper does, by default at both ends. Both sides derive a shared seed from the TLS session's exported keying material, draw the same ten-record schedule from CenKL's released order-1 Markov model, and send exactly those record sizes in those directions, carrying HTTP/2 data inside and padding the rest. It also turns off Go's dynamic record sizing and drops TLS session tickets, which added a tell of their own.

Nats of KL divergence from CenKL's size-and-direction model, first 10 packets after the handshake, 800 flows each on loopback, segmented at a 1,448-byte MSS. The floor isn't zero because 800 flows is a finite sample: real TLS drawn from the model itself scores 11.3. These are our measurements with the authors' released model, not the paper's; see samizdat#29.

The 1,448-byte cap matters. CenKL's model counts 1,460-byte packets, from senders without TCP timestamps. Linux servers send timestamps and fit 1,448 bytes, so a scheduled 1,460-byte record leaves as two packets, 1,448 and 12. The paper notes the same mismatch in its own testbed. Samizdat leaves sizes above 1,448 out of the schedule.

03 — side by side

Compared

How samizdat's defaults compare with the protocols it's most often weighed against. Every claim about another protocol comes from its source code, its authors, or a peer-reviewed measurement, linked in the cell. Where no one has measured something, the table says so. Nobody outside Lantern has evaluated samizdat yet.

Samizdat VLESS + TLS VLESS + Vision REALITY Trojan NaïveProxy
Client auth X25519 secret with the client's key share; AES-GCM seals time and short ID into the SessionID, bound to the whole ClientHello UUID, sent inside TLSXray docs Same as VLESS X25519 secret with the client's key share; AES-GCM seals version, time and short ID into the SessionID, bound to the whole ClientHelloREALITY tls.go SHA-224 of the password, sent inside TLSTrojan protocol HTTP proxy authorization, inside TLSNaïveProxy README
What a prober gets Real cover site
Raw bytes forwarded to it: its certificate, its TLS stack, its pages
Operator's fallback
A configured fallback server, behind the operator's own certificateinbound.go
Operator's fallback
Same as VLESS
Real target site
Raw bytes piped to the targettls.go
Operator's fallback
A preset endpoint, 127.0.0.1:80 by default, behind the operator's certificateTrojan protocol
Operator's web server
Caddy serving a site, behind the operator's certificateREADME
Replaying a captured handshake Refused
Accepted SessionIDs are remembered for 24–48 h (less past a million per window) and sent to the cover site if seen again; version 2 tags also expire after 12 h. Memory only: a restart forgets.
Not applicable
Auth is inside TLS, so a copied ClientHello carries none
Not applicable No replay cache
The time check is off by default (maxTimeDiff 0). A 2026 proof of concept tells REALITY apart by replaying handshakes.handshake_server_tls13.go · net4people #576
Not applicable Not applicable
If the cover site is down Clients still connect; only probes are affected Not applicable Not applicable Clients fail too
The server dials the target on every connection and closes the client's if that failstls.go
Not applicable Not applicable
Multiplexing Always
HTTP/2 CONNECT: one TLS connection carries up to 100 streams
Off by default
Mux is optionaloutbound.go
Off
Vision breaks mux connections that carry TCPoutbound.go
Not recommended
Its README pairs it with VisionREADME
No
One tunnel per request
Yes
HTTP/2 CONNECT
How connections start Shaped
First 10 records drawn from CenKL's model of real TLS; dynamic record sizing off. 12.4 nats against an 11.3 floor in our measurement.
Unshaped
Go's 1,208-byte records stand out in the first three packetsCenKL §4
Padded
Inner handshake padded to 900–1,399 bytes; 71.7% of connections send a 1,208-byte first packetCenKL §4 · proxy.go
Not measured
CenKL didn't test REALITY; with Vision it carries Vision's padding
Unshaped
The same 1,208-byte recordsCenKL §4
Distinctive
HTTP/2 opening adds 28.88 nats despite padding its first 8 reads and writesCenKL §4
TLS-in-TLS detection (TPR) Not measured
Tunnelled TLS is still inside; HTTP/2 multiplexing is meant to blur it, most when several streams share the connection
0.748Xue et al. 2024, Table 3 0.513 at 0.20% FPR, with a classifier built for VisionXue et al. 2024, §7.2 Not measured 0.737; 0.179 with 8 concurrent streams (trojan-go)Xue et al. 2024, Table 3 0.328Xue et al. 2024, Table 3
Timing Not shaped
Optional 1–30 ms delay before each client write, which research suggests doesn't help (see Limits)
Not shaped Not shaped Not shaped Not shaped Not shaped

Xray-core read at c456141 and REALITY at 4a26e87 (November 2026). TPR = true positive rate of the attack in Xue et al., USENIX Security 2024; their deployment measured false positives over 110 million real flows. Neither paper evaluated REALITY.

REALITY is the closest relative

Samizdat's version 2 authentication is REALITY's idea, implemented independently: an X25519 secret between the client's ordinary key share and the server's static key, an AES-GCM seal over a time and a short ID, and the whole ClientHello as associated data, so a copied tag fails in any other handshake. The differences are in what surrounds it: samizdat remembers what it has accepted, checks the time by default, doesn't need the cover site to be reachable for its own clients, multiplexes over HTTP/2, and shapes the start of the connection. REALITY hands authenticated clients a throwaway certificate signed with the shared key; samizdat serves its own and lets clients pin it.

04 — under the hood

How it works

AAuthentication in the SessionID

The client takes an X25519 secret between its own key share and the server's public key, derives an AES-256-GCM key with HKDF over the ClientHello's random, and seals version ‖ time ‖ short ID with the rest of the ClientHello as associated data. The 32 bytes that come out replace the SessionID. Older clients send version 1, an HMAC tag; servers accept both until told otherwise.

BMasquerade

A connection that doesn't authenticate is spliced, raw, to the cover site's real address. The server reads only the ClientHello it needs to check the tag, sends it on to the cover site, and copies bytes both ways, without rewriting them or terminating TLS, until both sides close or ten minutes pass.

CConnection-start shaping

Both ends export keying material from the finished TLS session. 32 bytes seed a ChaCha8 sampler that draws ten records' sizes and directions from the model; 8 bytes key an HMAC over the schedule, and its first 8 bytes are a marker that tells the server this client shapes. During the schedule each side sends exactly its scheduled records, carrying queued HTTP/2 bytes and padding. After the tenth, data passes straight through.

DOne connection, many streams

Every proxied connection is an HTTP/2 CONNECT stream on a long-lived TLS connection that carries up to 100 of them, so a censor counting TLS connections to an address sees few. Servers run samizdat's own lean HTTP/2 server, which batches frames into fewer TLS records and system calls: 66–90% less server CPU per gigabyte downloaded than Go's standard one in benchmarks, with the same frames on the wire.

EFragmented ClientHello

Optionally, the ClientHello is written in pieces split around the SNI, with a short random delay, in the style of Geneva, so a middlebox that reads the SNI from a single packet often doesn't find it there. It's best effort: the split can fall just outside the hostname.

FLegacy clients keep working

A server reads the first 8 bytes after the handshake. The shaping marker means a shaping client; an HTTP/2 preface means an older one, which is served exactly as before. New servers serve every client released so far; new clients need new servers.

05 — what it doesn't do

Limits

No proxy that carries real traffic is invisible. These are the gaps we know about.

  • Timing isn't shaped. CenKL's timing-aware classifier still reaches about 0.9 recall against shaped traffic. Samizdat's optional 1–30 ms random delay isn't a fix: Xue et al. (NDSS 2025) found that random delays like obfs4's can make cross-layer RTT fingerprinting easier, not harder.
  • Tunnelled TLS is still inside. Applications using samizdat bring their own TLS, so there is TLS inside samizdat's TLS. Multiplexing helps most when several streams share the connection; Xue et al. (USENIX Security 2024) found it far weaker with only one.
  • Shaping covers ten records. After that, traffic is whatever the tunnelled applications send. The schedule fixes TLS record sizes; TCP can still split or merge them into packets.
  • The model is one network's view. It was built from one regional ISP in late 2025. Real TLS elsewhere, or later, may look different, and a censor with its own data can build its own model.
  • The replay record lives in memory. A server restart forgets the SessionIDs it has accepted, and more than a million in one window shortens how long they're kept. Version 2's 12-hour time limit still applies.
  • Probes that aren't TLS are closed. A connection that doesn't open with a TLS handshake record is closed rather than forwarded to the cover site, so it doesn't see how the cover site would react.
  • Not independently reviewed. The version 2 authentication hasn't had an outside cryptographic review, and the measurements on this page are ours, on loopback.
06 — use it

Use it

Samizdat is a Go library with no sing-box dependency, and an inbound and outbound in lantern-box. Lantern's apps use it today.

go get github.com/getlantern/samizdat

client, err := samizdat.NewClient(samizdat.ClientConfig{
    ServerAddr:  "203.0.113.7:443",
    ServerName:  "cover.example",   // the cover site named in the handshake
    PublicKey:   serverPublicKey,   // 32-byte X25519 public key
    ShortID:     shortID,           // 8-byte client identifier
    Fingerprint: "chrome",
})
conn, err := client.DialContext(ctx, "tcp", "example.com:443")
// lantern-box (sing-box) outbound
{
  "type": "samizdat",
  "tag": "samizdat-out",
  "server": "203.0.113.7",
  "server_port": 443,
  "public_key": "<64 hex chars>",
  "short_id": "<16 hex chars>",
  "server_name": "cover.example",
  "cert_spki_sha256": "<optional pin, 64 hex chars>"
}

Generate keys with go run ./cmd/samizdat-server --genkeys. Pick a cover site that's reachable from the server, serves TLS 1.3 with HTTP/2, and is unremarkable on the network you're getting around.

07 — sources

References

  1. Aaron Ortwein, Armin Huremagic, Shabbir Hussain, Holly Bergen, Joshua J. Daymude, Diwen Xue, Jedidiah R. Crandall, Roya Ensafi. CenKL: Information-Theoretic Visibility into Censorship Circumvention Traffic Patterns. IEEE Symposium on Security and Privacy, 2027. Models and ProxyShaper: censoredplanet/obfuscation, censoredplanet/Xray-ProxyShaper.
  2. Diwen Xue, Michalis Kallitsis, Amir Houmansadr, Roya Ensafi. Fingerprinting Obfuscated Proxy Traffic with Encapsulated TLS Handshakes. USENIX Security 2024.
  3. Diwen Xue, Robert Stanley, Piyush Kumar, Roya Ensafi. The Discriminative Power of Cross-layer RTTs in Fingerprinting Proxy Traffic. NDSS 2025.
  4. Alice, Bob, Carol, Jan Beznazwy, Amir Houmansadr. How China Detects and Blocks Shadowsocks. IMC 2020.
  5. Kevin Bock, George Hughey, Xiao Qiang, Dave Levin. Geneva: Evolving Censorship Evasion Strategies. ACM CCS 2019.
  6. XTLS. REALITY and Xray-core, including the Vision design discussion #1295.
  7. net4people/bbs: #490 (Russia freezing TLS connections after about 16 KB), #546 (Russia policing TLS connection counts on port 443), #576 (REALITY detected by handshake replay).