Files
flamenet-messenger-android/obfs4
Paul HittandClaude Opus 5 6cd7a0d5dc A settings screen for the bridge line, and a path for it to reach
Two gaps, and the second was the bigger one.

obfs4 was reachable from nothing. The socket-factory seam landed in !77,
but every client in the app takes `defaultHttpClient()`'s null default —
`RelayClient`, `MessengerApi`, `AccountsClient`, `RelayAccounts`,
`SignupProofOfWork`, `RelayDiscovery` and the background service, ten
sites in all, several from default parameters evaluated at
field-initialisation time. The transport had been built, tested and
proven against real lyrebird in both directions, and no user could
switch it on.

Threading a bridge through ten constructors would have worked until an
eleventh was added, and the eleventh would connect directly,
successfully, with nothing to show that it had. A missed site does not
fail; it silently does not protect. So the factory is installed
unconditionally and consults `Obfs4Routing` per socket. With no bridge it
delegates to the platform's own factory, which is what OkHttp would have
used anyway — nothing changes for the overwhelming majority who never set
one.

Pooled connections are the trap underneath that. OkHttp keeps a
connection alive across requests, so one opened through the old bridge
serves requests for minutes after the user switches — or after they turn
a bridge OFF, which is the dangerous direction. The pool is shared so
`Obfs4Routing.use` can empty it.

The other gap is the one flagged when the wiring merged: a malformed line
parses to nothing, nothing means no bridge, and no bridge is a direct
connection. So `BridgeLine.parse` reports which part is wrong, and
`Obfs4Settings` stores only what it accepted.

Java's decoder made that worse here than on iOS. `fromCert` appended
`==`, which THROWS for most truncation lengths — 20, 29, 30, 50 and 51
bytes among them — so a short paste came back as unreadable gibberish
while iOS, whose decoder is padding-lenient, called the same paste "cut
short". That is the likeliest real mistake by a distance: bridge lines
wrap in every chat app and terminal, and copying only the first line is
ordinary. Decoding the bare string works at every length and lets the
size check give the honest answer. Both forms produce identical bytes for
a real 52-byte cert, so nothing that worked before changes.

The messages are the same sentences iOS shows, pinned by a test. The two
apps are handed the same lines by the same operators, and one support
answer should cover both. None of them says obfs4, base64 or node id —
whoever reaches this screen was handed a line and told to paste it, and
an error in the protocol's vocabulary reads as "you are not qualified to
fix this".

`iat-mode` 1 and 2 are a caveat, not a rejection: they ask the client to
space its writes and the bridge does not enforce them, so such a line
connects and works. Refusing it would take a working bridge away from
somebody who has no other one.

The status line reads a handshake counter, never the stored preference. A
saved bridge that is not there leaves the app connecting directly, and
only a completed handshake can tell you otherwise — so a bridge that has
carried nothing says exactly that, and the words "protected", "secure",
"hidden" and "safe" are asserted absent from it.

Live interop re-run against real lyrebird after the factory change: both
directions still pass. 448 unit tests green, 145 in the app, and
reverting the unconditional factory reddens the routing test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 15:12:12 -04:00
..