Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 34 additions & 21 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ AES-CTR runs **8-wide** on the block-aligned bulk (`src/crypto/crypto.zig`).

## FakeTLS fronting & domain selection

The FakeTLS ServerHello is a fixed three-record shape — `ServerHello` (one x25519
key_share, a client-echoed cipher) + `ChangeCipherSpec` + one `ApplicationData`
The FakeTLS ServerHello is a fixed three-record shape — `ServerHello` (one key_share:
an X25519MLKEM768 `0x11ec` share when the client offered one, else a classical x25519
share, plus a client-echoed cipher) + `ChangeCipherSpec` + one `ApplicationData`
"certificate" record — validated by the Telegram client only for framing + the HMAC
in `server-random`. Two hard constraints follow:

Expand All @@ -92,33 +93,45 @@ in `server-random`. Two hard constraints follow:
Treat it as frozen the moment a link is shared. (See `config.zig` /
[COMPATIBILITY.md](COMPATIBILITY.md).)

2. **We can only mimic a domain whose genuine TLS 1.3 is single-round x25519.** Our
FakeTLS emits exactly one ServerHello with an **x25519** key_share and cannot
replicate a `HelloRetryRequest` or a non-x25519 group. So a *good* fronting
domain negotiates **x25519 in one round** for a modern (Chrome-shaped, MLKEM-
offering) ClientHello — the client offers key_shares for both MLKEM and x25519,
and such a server simply picks x25519, which is exactly what we emit. Big RU
sites like `rutube.ru`, `ozon.ru`, `vk.com`, `yandex.ru`, `dzen.ru` do this.

Some domains do **not**: e.g. `wb.ru` and `mail.ru` prefer `secp521r1` and send a
`HelloRetryRequest` (they even fail an x25519-only hello). For such a domain a
genuine handshake is `ClientHello → HRR → ClientHello#2 → ServerHello(secp521r1)`
while ours is a single `ServerHello(x25519)` — a **passive ServerHello mismatch we
cannot fix without changing `tls_domain`**, which constraint (1) forbids. The
installer therefore probes the chosen domain and **warns** if it is a poor mimicry
target, so the choice is made well *before* the link is locked. There is no
runtime fix for an already-distributed link pointed at a poor domain — it is an
accepted residual.
2. **We can only mimic a domain whose genuine TLS 1.3 is single-round, and since June
2026 it must support X25519MLKEM768.** Our FakeTLS emits exactly one ServerHello,
with either an X25519MLKEM768 (`0x11ec`) key_share (when the client offered it) or a
classical x25519 key_share — never an HRR, never a group the client didn't offer.
Two things make a domain a *good* target:

- **Single round, no HRR.** For a modern (Chrome-shaped, MLKEM-offering) ClientHello
the domain must answer in one ServerHello. `wb.ru` / `mail.ru` prefer `secp521r1`
and send a `HelloRetryRequest` (genuine handshake:
`ClientHello → HRR → ClientHello#2 → ServerHello(secp521r1)`) while ours is a
single record — an unfixable **passive ServerHello mismatch**.
- **Post-quantum capable.** Since the night of 4→5 June 2026 the TSPU flags a domain
that negotiates *only* classical x25519 (declining X25519MLKEM768): iOS clients —
and everyone on their NAT egress IP — fronting such a domain get blocked. The
signal is a property of the **domain** (the censor probes the SNI out-of-band), so
our 0x11ec echo can't buy it back; the domain itself must negotiate
X25519MLKEM768. A PQ-capable domain that does so in one round is exactly what our
FakeTLS mimics via the 0x11ec echo, so it satisfies both requirements at once. See
[THREAT_MODEL.md](THREAT_MODEL.md) "Post-quantum key_share".

The installer probes the chosen domain (`src/ctl/fronting_domain.zig`, offering
`X25519MLKEM768:X25519`) and **warns** if it does only classical x25519 or an HRR,
so the choice is made well *before* the link is locked. There is no runtime fix for
an already-distributed link pointed at a poor domain — it is an accepted residual,
and the `tls_domain` immutability in (1) means an existing deploy on a now-marked
domain cannot migrate without invalidating every link.

**Masking / active probes** are independent of the link: non-validating (no-secret)
traffic is transparently relayed to the mask target. Field-verified behavior
(against a live tunnel-mode deployment):

- Fronting the **real** domain (`mask_port=443` → `tls_domain:443`) makes a prober
see the genuine site + a CA-chained cert — **verified working** for a single-round
domain (probing the proxy fronting `rutube.ru` returned its real GlobalSign cert).
domain (probing the proxy fronting `rutube.ru` returned its real GlobalSign cert;
note that domain-selection guidance has since tightened to require X25519MLKEM768 —
see constraint 2 — so re-probe any candidate for PQ support before committing).
This requires the proxy to resolve the domain — see the DNS note below.
- It only works for domains whose TLS 1.3 is **single-round x25519**. Our relay
- It only works for domains whose TLS 1.3 is **single-round** (x25519 or the PQ
hybrid). Our relay
carries a single ClientHello↔ServerHello exchange, **not** a `HelloRetryRequest`
multi-round. Fronting an HRR domain (e.g. `wb.ru`) yields an *incomplete* handshake
(no certificate) — worse than a complete one. This is the same reason such domains
Expand Down
34 changes: 27 additions & 7 deletions THREAT_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,23 @@ encapsulation: FakeTLS clients validate only record framing + the HMAC in
server-random, passive fingerprinting keys on the *group and size*, and we are not a
TLS terminator — so a cryptographically valid ciphertext is unnecessary.)

**The `tls_domain` must itself support X25519MLKEM768 (June-2026 TSPU marker).** The
echo above fixes the *connection-level* downgrade tell, but there is a second,
domain-level check the TSPU rolled out the night of 4→5 June 2026 that the echo does
**not** address. The censor appears to probe the *SNI domain* out-of-band for
post-quantum support and blocks the flow when the domain lacks it: a `tls_domain`
whose real TLS 1.3 negotiates only classical x25519 (no X25519MLKEM768) is a passive
marker, and iOS clients — plus **everyone sharing their NAT egress IP** — fronting
such a domain get dropped. Because the signal is a property of the domain (evidence:
the community's fix is "change the domain", `@Sni_checker_bot` checks a *domain*, a
self-signed backend must run OpenSSL 3.5+ to offer the group, and one domain's two IPs
can score differently), our own ServerHello can't buy it back. The only lever is
choosing a `tls_domain` that genuinely negotiates X25519MLKEM768 in a single round —
which our FakeTLS then mimics correctly via the 0x11ec echo. `mtbuddy install` /
`setup masking` now probe for this and warn. This collides with `tls_domain`
immutability: a live deploy already pinned to a classical-x25519 domain cannot migrate
without invalidating every distributed link. (See `src/ctl/fronting_domain.zig`.)

It does **not** defend against a third strategy that DPI vendors are now leaning on:
**SNI ↔ destination-IP consistency.** When `tls_domain` is a well-known third-party
domain that does not resolve to your proxy's IP, a DPI box can flag the flow *passively*,
Expand All @@ -130,18 +147,21 @@ strongest to most practical:
2. **Prefer a plausible, less-prominent borrowed domain** over a globally famous one. A
massively popular CDN-hosted domain is the easiest IP↔SNI mismatch to catch; a
regional / less-watched HTTPS site is a weaker signal. It must still pass the
single-round-x25519 ServerHello check below.
single-round X25519MLKEM768 ServerHello check below.
3. **IP and egress hygiene.** Borrowed-SNI FakeTLS is worth most when the *IP itself*
isn't already burned: prefer a fresh address, and use IPv6 rotation
(`mtbuddy ipv6-hop`) and/or tunnel egress so the visible endpoint isn't a long-lived,
reputation-flagged IP.

**Separate but related — ServerHello suitability.** The synthetic ServerHello does a
single-round x25519 key exchange with no HelloRetryRequest. A `tls_domain` whose real
TLS 1.3 server prefers a non-x25519 group or issues an HRR (e.g. `wb.ru`, `mail.ru`
select secp521r1) produces a *passive ServerHello mismatch* independent of the IP↔SNI
issue. `mtbuddy install` / `setup masking` warn about this; prefer a domain that
negotiates x25519 in a single round.
**Separate but related — ServerHello suitability.** The synthetic ServerHello is a
single-round exchange with no HelloRetryRequest, emitting either an X25519MLKEM768
(0x11ec) or a classical x25519 key_share depending on what the client offered. A
`tls_domain` whose real TLS 1.3 server issues an HRR or prefers a group we can't emit
(e.g. `wb.ru`, `mail.ru` select secp521r1) produces a *passive ServerHello mismatch*
independent of the IP↔SNI issue. Combined with the June-2026 domain marker above, the
target to prefer is a domain that negotiates **X25519MLKEM768 in a single round**;
`mtbuddy install` / `setup masking` warn when a domain does only classical x25519 or
does an HRR.

Bottom line: FakeTLS with a borrowed SNI is strong camouflage against handshake
fingerprinting and active probing, but it is **not** a guarantee against IP↔SNI
Expand Down
9 changes: 6 additions & 3 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,12 @@ port = 443

[censorship]
# Domain to impersonate in TLS ServerHello and forward unauthenticated clients to.
# Choose a popular, high-traffic domain that negotiates single-round x25519 (most big
# sites: rutube.ru, ozon.ru, vk.com, yandex.ru). Avoid HRR/secp521r1 domains like
# wb.ru — our single-round FakeTLS can't match them. ⚠️ Immutable once links ship.
# Since June 2026 pick a popular domain that negotiates single-round X25519MLKEM768
# (post-quantum) — a classical-x25519-only domain now marks iOS clients (and everyone
# on their NAT IP) for blocking. Verify a candidate + the exact IP you'll use with:
# openssl s_client -groups X25519MLKEM768 -connect <domain>:443 (needs OpenSSL 3.5+)
# or @Sni_checker_bot. Avoid HRR/secp521r1 domains (e.g. wb.ru) — our single-round
# FakeTLS can't match them either way. ⚠️ Immutable once links ship — choose carefully.
tls_domain = "rutube.ru"

# Forward unauthenticated / invalid connections to tls_domain (active probing defense).
Expand Down
26 changes: 19 additions & 7 deletions src/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,25 @@ pub const Config = struct {
/// as hex, so the tg:// link is a function of (secret, tls_domain). Changing
/// tls_domain changes EVERY user's link — never do it on a live deployment.
///
/// Mimicry note: our 3-record FakeTLS emits ONE ServerHello with an x25519
/// key_share and cannot replicate a HelloRetryRequest. Pick a domain whose
/// genuine TLS 1.3 negotiates **x25519 in a single round** (most big sites:
/// rutube.ru, ozon.ru, vk.com, yandex.ru, dzen.ru). Domains that prefer
/// secp521r1 / reject x25519 and HRR (e.g. wb.ru, mail.ru) produce a passive
/// ServerHello mismatch that cannot be fixed without changing tls_domain —
/// which the immutability rule above forbids. So choose well at install time.
/// Mimicry note: our 3-record FakeTLS emits ONE ServerHello, single-round, no
/// HelloRetryRequest. It echoes an X25519MLKEM768 (0x11ec) key_share when the
/// client offered one and a classical x25519 (0x001d) key_share otherwise — so it
/// can mimic a domain that negotiates **either group in a single round**.
///
/// Since the June-2026 TSPU rollout, prefer a domain that negotiates
/// **X25519MLKEM768** (post-quantum). A domain that only does classical x25519 is
/// now a passive marker: iOS clients — and everyone sharing their NAT egress IP —
/// fronting such a domain get blocked (the TSPU appears to probe the SNI domain
/// out-of-band for PQ support, so this is a property of the *domain*, not of the
/// ServerHello we emit). Verify a candidate with:
/// openssl s_client -groups X25519MLKEM768 -connect <domain>:443 (OpenSSL 3.5+)
/// or the @Sni_checker_bot. Note one domain can have multiple IPs with mixed PQ
/// support (e.g. rutube.ru), giving inconsistent results — check the IP you'll use.
///
/// Domains that prefer secp521r1 / reject x25519 and HRR (e.g. wb.ru, mail.ru) stay
/// unmimicable regardless (our single-round ServerHello can't match an HRR). None
/// of this is fixable without changing tls_domain — which the immutability rule
/// above forbids — so choose well at install time.
/// See ARCHITECTURE.md "FakeTLS fronting & domain selection".
tls_domain: []const u8 = default_tls_domain,
users: std.StringHashMap([16]u8),
Expand Down
Loading
Loading