Not now — after there are users. Filed so the thinking is not lost, and deliberately not started: this is a censorship-resistance feature, and nobody is being censored yet. Revisit when there is a deployment that reports being blocked.
What we would actually be buying
Not message confidentiality. The ratchet and sealed sender already mean the relay holds ciphertext it cannot open. TLS is the outer wrapper, and it earns three things: it hides who is talking to which relay from the network path, it gets through middleboxes, and it authenticates the relay — the last of which we already do separately by pinning relay identity.
So the only prize here is traffic shape. Today an observer sees SNI: relay.flamenet.io, a DNS lookup for it, and a recognisable long-poll rhythm. They cannot read a word, and they can block all of it.
The part of obfs4 worth taking
Active probe resistance, not "looks like random noise". A censor who spots a suspicious flow and then connects to that IP themselves learns nothing, because the handshake needs a node ID and public key they do not have. Without that, obfuscation only moves detection from passive to active: block what you cannot identify, then scan to confirm.
⚠️The "indistinguishable from random" half has aged. Fully random traffic is itself a fingerprint — real traffic has structure — and obfs4 has been blocked at scale by entropy tests plus active probing. The field moved toward mimicry: uTLS to forge a browser's TLS fingerprint, REALITY to borrow a real site's handshake.
Which matters here, because we already have the best mimicry option on the shelf: WebRTC data channels over the coturn already deployed for calls. That is not "looks like" traffic we generate — it is traffic we generate, from infrastructure already running, and nobody blocks video calls. Worth prototyping alongside rather than after.
Where this project beats Tor
Bridge distribution is the hard problem, not the crypto. A bridge works only while the censor lacks the line; Tor needs BridgeDB, moat, email distribution and domain-fronted rendezvous, and censors still enumerate bridges by running clients and harvesting.
🔴We have a contact graph and an encrypted channel between contacts. A bridge line is a couple of hundred bytes and could ride exactly the rail ContactCard already uses — the thing people paste and scan today. Whoever invites you hands you a way in, sealed.
That makes enumeration social infiltration one contact at a time rather than scraping a list. It falls out of the product rather than needing an institution behind it.
Implementation reality
Relay
easy — obfs4proxy in server mode is a TCP proxy; one container beside the relay, forwarding to it. Self-hosters add one service and one port.
Android
fine — ship the Go binary or gomobile it, as Tor's Android apps have for years.
iOS
🔴the wall. No fork/exec, so obfs4proxy cannot ship as an executable. Either a gomobile framework linked into the app (~10MB and a Go runtime in-process), or the obfs4 wire format implemented natively in Swift.
⚠️ The Swift option is more tractable here than it would be elsewhere: ntor handshake, HMAC, AES-CTR and probabilistic padding, on top of X25519/HKDF/AES-GCM we already have — in an engine with a cross-engine harness that proves Swift and Kotlin agree byte for byte. Rolling your own obfuscation is normally reckless because nobody checks it. That harness is the one piece of machinery that would check it, and it already exists.
The constraint no design escapes
APNs still wakes the iPhone. iOS will not hold a socket open in the background, so whatever the transport, the wake path stays visible to Apple and to anyone watching for pushes. Obfuscation buys the conversation, not the fact that one happened. Any design that assumes a persistent connection will work beautifully in testing and fail the moment a phone locks.
The spike, when we get to it
A prototype, not a migration:
obfs4proxy sidecar on one test-fleet relay, forwarding to it
Android client through it behind a setting — Android only, because that answers the questions without touching the iOS wall
Bridge line delivered as a ContactCard extension
Measure: added latency, battery over a day, and what a self-hoster has to do
That answers "does it work and what does it cost" before anyone writes Swift. If the answer is good, the iOS question becomes worth its own issue.
What would tell us to start
A real deployment reporting that it is blocked. Not before — this is a large surface, an arms race to maintain, and an extra thing for every self-hoster to run, in exchange for a threat nobody has yet reported.
**Not now — after there are users.** Filed so the thinking is not lost, and deliberately not started: this is a censorship-resistance feature, and nobody is being censored yet. Revisit when there is a deployment that reports being blocked.
## What we would actually be buying
Not message confidentiality. The ratchet and sealed sender already mean the relay holds ciphertext it cannot open. TLS is the outer wrapper, and it earns three things: it hides who is talking to which relay from the network path, it gets through middleboxes, and it authenticates the relay — the last of which we already do separately by pinning relay identity.
So the only prize here is **traffic shape**. Today an observer sees `SNI: relay.flamenet.io`, a DNS lookup for it, and a recognisable long-poll rhythm. They cannot read a word, and they can block all of it.
## The part of obfs4 worth taking
**Active probe resistance**, not "looks like random noise". A censor who spots a suspicious flow and then connects to that IP themselves learns nothing, because the handshake needs a node ID and public key they do not have. Without that, obfuscation only moves detection from passive to active: block what you cannot identify, then scan to confirm.
⚠️ **The "indistinguishable from random" half has aged.** Fully random traffic is itself a fingerprint — real traffic has structure — and obfs4 has been blocked at scale by entropy tests plus active probing. The field moved toward mimicry: uTLS to forge a browser's TLS fingerprint, REALITY to borrow a real site's handshake.
Which matters here, because we already have the best mimicry option on the shelf: **WebRTC data channels over the coturn already deployed for calls.** That is not "looks like" traffic we generate — it *is* traffic we generate, from infrastructure already running, and nobody blocks video calls. Worth prototyping alongside rather than after.
## Where this project beats Tor
Bridge distribution is the hard problem, not the crypto. A bridge works only while the censor lacks the line; Tor needs BridgeDB, moat, email distribution and domain-fronted rendezvous, and censors still enumerate bridges by running clients and harvesting.
🔴 **We have a contact graph and an encrypted channel between contacts.** A bridge line is a couple of hundred bytes and could ride exactly the rail `ContactCard` already uses — the thing people paste and scan today. Whoever invites you hands you a way in, sealed.
That makes enumeration social infiltration one contact at a time rather than scraping a list. It falls out of the product rather than needing an institution behind it.
## Implementation reality
| | |
|---|---|
| **Relay** | easy — `obfs4proxy` in server mode is a TCP proxy; one container beside the relay, forwarding to it. Self-hosters add one service and one port. |
| **Android** | fine — ship the Go binary or gomobile it, as Tor's Android apps have for years. |
| **iOS** | 🔴 **the wall.** No `fork`/`exec`, so obfs4proxy cannot ship as an executable. Either a gomobile framework linked into the app (~10MB and a Go runtime in-process), or the obfs4 wire format implemented natively in Swift. |
⚠️ The Swift option is more tractable here than it would be elsewhere: ntor handshake, HMAC, AES-CTR and probabilistic padding, on top of X25519/HKDF/AES-GCM we already have — in an engine with a **cross-engine harness that proves Swift and Kotlin agree byte for byte**. Rolling your own obfuscation is normally reckless because nobody checks it. That harness is the one piece of machinery that would check it, and it already exists.
## The constraint no design escapes
**APNs still wakes the iPhone.** iOS will not hold a socket open in the background, so whatever the transport, the wake path stays visible to Apple and to anyone watching for pushes. Obfuscation buys the conversation, not the fact that one happened. Any design that assumes a persistent connection will work beautifully in testing and fail the moment a phone locks.
## The spike, when we get to it
A prototype, not a migration:
1. `obfs4proxy` sidecar on one test-fleet relay, forwarding to it
2. Android client through it behind a setting — Android only, because that answers the questions without touching the iOS wall
3. Bridge line delivered as a `ContactCard` extension
4. Measure: added latency, battery over a day, and what a self-hoster has to do
That answers "does it work and what does it cost" before anyone writes Swift. If the answer is good, the iOS question becomes worth its own issue.
## What would tell us to start
A real deployment reporting that it is blocked. Not before — this is a large surface, an arms race to maintain, and an extra thing for every self-hoster to run, in exchange for a threat nobody has yet reported.
Status, 2026-09-06 — the transport half is built; the distribution half is not
Built ahead of the trigger this issue set ("a real deployment reporting that it is blocked. Not before"), at the owner's direction. Recording what exists so the scope left here is accurate.
Done
Native obfs4 in Swift — flamenet-elligator2 (private): Elligator2 both directions, cofactor-dirty scalar base mult, ntor, framing, handshake, connection plumbing, a NIO handler pair, and a deployable obfs4bridge binary.
Native obfs4 in Kotlin — :obfs4 module on BouncyCastle, sharing no code with the Swift port, only vectors.
Interop both directions, against the Tor Project's own lyrebird — each port as client and as bridge. This is what the issue's "the harness is the one piece of machinery that would check it" argument was resting on, and it holds.
Compose service on the relay, profiles: ["obfs4"], opt-in.
Settings UI on both apps, off by default, validating a pasted bridge line before storing it.
One correction to the text above
ntor handshake, HMAC, AES-CTR and probabilistic padding
obfs4's framing is NaCl secretbox — XSalsa20-Poly1305, not AES-CTR plus HMAC, and its length obfuscation is a SipHash-2-4 DRBG run cumulatively rather than re-keyed per frame. Both were only settled by generating vectors from the reference; the reference's own comments disagree with its code in three places, including ntor's transcript, which concatenates the identity key twice.
Not done — and step 4 is the gap that matters
Step 4 was never run. No measurement of added latency, of battery over a day, or of what a self-hoster actually has to do. The issue asked for that before anyone wrote Swift; the Swift got written first.
No bridge is deployed anywhere. The compose service is opt-in, builds from a private sibling checkout, and needs FN_OBFS4_ADVERTISE set or the line it prints is not dialable. Until one runs, the settings screen is a text box with nothing to put in it.
Bridge distribution over the contact rail is not built. This issue argues it is where the project beats Tor, and it is also the practical blocker above: today a bridge line has to travel by some other channel entirely.
iat-mode 1 and 2 are unimplemented. Mode 0 is what every real deployment uses; a line asking for 1 or 2 is accepted with a caveat rather than refused, because the bridge does not enforce it.
WebRTC-over-coturn mimicry — not prototyped. The argument for it in this issue is untouched by any of the above, and if the entropy-test point is right it may matter more than obfs4 does.
## Status, 2026-09-06 — the transport half is built; the distribution half is not
Built ahead of the trigger this issue set ("a real deployment reporting that it is blocked. Not before"), at the owner's direction. Recording what exists so the scope left here is accurate.
### Done
- **Native obfs4 in Swift** — `flamenet-elligator2` (private): Elligator2 both directions, cofactor-dirty scalar base mult, ntor, framing, handshake, connection plumbing, a NIO handler pair, and a deployable `obfs4bridge` binary.
- **Native obfs4 in Kotlin** — `:obfs4` module on BouncyCastle, sharing *no code* with the Swift port, only vectors.
- **Interop both directions, against the Tor Project's own lyrebird** — each port as client and as bridge. This is what the issue's "the harness is the one piece of machinery that would check it" argument was resting on, and it holds.
- **Compose service** on the relay, `profiles: ["obfs4"]`, opt-in.
- **Settings UI on both apps**, off by default, validating a pasted bridge line before storing it.
### One correction to the text above
> ntor handshake, HMAC, AES-CTR and probabilistic padding
obfs4's framing is **NaCl secretbox — XSalsa20-Poly1305**, not AES-CTR plus HMAC, and its length obfuscation is a SipHash-2-4 DRBG run *cumulatively* rather than re-keyed per frame. Both were only settled by generating vectors from the reference; the reference's own comments disagree with its code in three places, including ntor's transcript, which concatenates the identity key **twice**.
### Not done — and step 4 is the gap that matters
- **Step 4 was never run.** No measurement of added latency, of battery over a day, or of what a self-hoster actually has to do. The issue asked for that before anyone wrote Swift; the Swift got written first.
- **No bridge is deployed anywhere.** The compose service is opt-in, builds from a private sibling checkout, and needs `FN_OBFS4_ADVERTISE` set or the line it prints is not dialable. Until one runs, the settings screen is a text box with nothing to put in it.
- **Bridge distribution over the contact rail is not built.** This issue argues it is where the project beats Tor, and it is also the practical blocker above: today a bridge line has to travel by some other channel entirely.
- **`iat-mode` 1 and 2 are unimplemented.** Mode 0 is what every real deployment uses; a line asking for 1 or 2 is accepted with a caveat rather than refused, because the bridge does not enforce it.
- **WebRTC-over-coturn mimicry** — not prototyped. The argument for it in this issue is untouched by any of the above, and if the entropy-test point is right it may matter more than obfs4 does.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Not now — after there are users. Filed so the thinking is not lost, and deliberately not started: this is a censorship-resistance feature, and nobody is being censored yet. Revisit when there is a deployment that reports being blocked.
What we would actually be buying
Not message confidentiality. The ratchet and sealed sender already mean the relay holds ciphertext it cannot open. TLS is the outer wrapper, and it earns three things: it hides who is talking to which relay from the network path, it gets through middleboxes, and it authenticates the relay — the last of which we already do separately by pinning relay identity.
So the only prize here is traffic shape. Today an observer sees
SNI: relay.flamenet.io, a DNS lookup for it, and a recognisable long-poll rhythm. They cannot read a word, and they can block all of it.The part of obfs4 worth taking
Active probe resistance, not "looks like random noise". A censor who spots a suspicious flow and then connects to that IP themselves learns nothing, because the handshake needs a node ID and public key they do not have. Without that, obfuscation only moves detection from passive to active: block what you cannot identify, then scan to confirm.
⚠️ The "indistinguishable from random" half has aged. Fully random traffic is itself a fingerprint — real traffic has structure — and obfs4 has been blocked at scale by entropy tests plus active probing. The field moved toward mimicry: uTLS to forge a browser's TLS fingerprint, REALITY to borrow a real site's handshake.
Which matters here, because we already have the best mimicry option on the shelf: WebRTC data channels over the coturn already deployed for calls. That is not "looks like" traffic we generate — it is traffic we generate, from infrastructure already running, and nobody blocks video calls. Worth prototyping alongside rather than after.
Where this project beats Tor
Bridge distribution is the hard problem, not the crypto. A bridge works only while the censor lacks the line; Tor needs BridgeDB, moat, email distribution and domain-fronted rendezvous, and censors still enumerate bridges by running clients and harvesting.
🔴 We have a contact graph and an encrypted channel between contacts. A bridge line is a couple of hundred bytes and could ride exactly the rail
ContactCardalready uses — the thing people paste and scan today. Whoever invites you hands you a way in, sealed.That makes enumeration social infiltration one contact at a time rather than scraping a list. It falls out of the product rather than needing an institution behind it.
Implementation reality
obfs4proxyin server mode is a TCP proxy; one container beside the relay, forwarding to it. Self-hosters add one service and one port.fork/exec, so obfs4proxy cannot ship as an executable. Either a gomobile framework linked into the app (~10MB and a Go runtime in-process), or the obfs4 wire format implemented natively in Swift.⚠️ The Swift option is more tractable here than it would be elsewhere: ntor handshake, HMAC, AES-CTR and probabilistic padding, on top of X25519/HKDF/AES-GCM we already have — in an engine with a cross-engine harness that proves Swift and Kotlin agree byte for byte. Rolling your own obfuscation is normally reckless because nobody checks it. That harness is the one piece of machinery that would check it, and it already exists.
The constraint no design escapes
APNs still wakes the iPhone. iOS will not hold a socket open in the background, so whatever the transport, the wake path stays visible to Apple and to anyone watching for pushes. Obfuscation buys the conversation, not the fact that one happened. Any design that assumes a persistent connection will work beautifully in testing and fail the moment a phone locks.
The spike, when we get to it
A prototype, not a migration:
obfs4proxysidecar on one test-fleet relay, forwarding to itContactCardextensionThat answers "does it work and what does it cost" before anyone writes Swift. If the answer is good, the iOS question becomes worth its own issue.
What would tell us to start
A real deployment reporting that it is blocked. Not before — this is a large surface, an arms race to maintain, and an extra thing for every self-hoster to run, in exchange for a threat nobody has yet reported.
Status, 2026-09-06 — the transport half is built; the distribution half is not
Built ahead of the trigger this issue set ("a real deployment reporting that it is blocked. Not before"), at the owner's direction. Recording what exists so the scope left here is accurate.
Done
flamenet-elligator2(private): Elligator2 both directions, cofactor-dirty scalar base mult, ntor, framing, handshake, connection plumbing, a NIO handler pair, and a deployableobfs4bridgebinary.:obfs4module on BouncyCastle, sharing no code with the Swift port, only vectors.profiles: ["obfs4"], opt-in.One correction to the text above
obfs4's framing is NaCl secretbox — XSalsa20-Poly1305, not AES-CTR plus HMAC, and its length obfuscation is a SipHash-2-4 DRBG run cumulatively rather than re-keyed per frame. Both were only settled by generating vectors from the reference; the reference's own comments disagree with its code in three places, including ntor's transcript, which concatenates the identity key twice.
Not done — and step 4 is the gap that matters
FN_OBFS4_ADVERTISEset or the line it prints is not dialable. Until one runs, the settings screen is a text box with nothing to put in it.iat-mode1 and 2 are unimplemented. Mode 0 is what every real deployment uses; a line asking for 1 or 2 is accepted with a caveat rather than refused, because the bridge does not enforce it.