obfs4 for Android, part 7: the OkHttp seam, off by default
RelayClient.defaultHttpClient(bridge) now carries every request inside obfs4 when a bridge is configured. The parameter defaults to null, which is the shipping configuration: nothing changes for anyone who does not ask for it. A real HTTP request now travels through obfs4 end to end via OkHttp. That test is deliberately Kotlin on both ends, and says so: the wire format is already pinned to real lyrebird in both directions by InteropTest. What is unproven until here is the plumbing -- that the handshake happens before any stream is handed out, that bytes arriving alongside the handshake are not dropped, and that a frame decoding to nothing does not read as end-of-stream. Each of those loses data without producing a wrong byte anywhere. The handshake runs inside connect(), not on first write. OkHttp starts writing a request the moment it has a connected socket, so a deferred handshake would interleave with request bytes and desynchronise the frame decoder on the first request of every connection. Obfs4Socket ignores the endpoint OkHttp asks for. That endpoint is the RELAY's address; the socket goes to the bridge, which proxies on. Connecting to the endpoint would defeat the entire transport while appearing to work. All five SocketFactory overloads are implemented. Leaving any of them returning a plain socket means some path silently bypasses the transport -- and the traffic still works, which is exactly why nobody would notice. A wrong bridge key fails as a refusal rather than hanging. obfs4 bridges stay silent for anyone they cannot verify, so without the timeout this is an app that never finishes a request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a7ce010403
commit
dd412b8680
@@ -6,6 +6,8 @@ import io.flamenet.e2e.PeerBundle
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import io.flamenet.obfs4.Obfs4Bridge
|
||||
import io.flamenet.obfs4.Obfs4SocketFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
@@ -62,13 +64,40 @@ class RelayClient(
|
||||
/** The relay's own 10 MiB per-blob limit, with slack (audit M5). */
|
||||
const val HARD_ATTACHMENT_CAP = 12L * 1024 * 1024
|
||||
|
||||
fun defaultHttpClient(): OkHttpClient = OkHttpClient.Builder()
|
||||
.callTimeout(25, TimeUnit.SECONDS)
|
||||
// No response cache, deliberately. The payloads are ciphertext, but
|
||||
// sealed blobs and envelope traffic surviving on disk after the user
|
||||
// deletes a chat is exactly what "deleted means deleted" forbids.
|
||||
.cache(null)
|
||||
.build()
|
||||
/**
|
||||
* @param bridge when set, every request is carried inside obfs4 to that
|
||||
* bridge, which proxies on to the relay.
|
||||
*
|
||||
* 🔴 **Null by default, and that is the shipping configuration.** obfs4
|
||||
* is for reaching a relay on a network that blocks or fingerprints TLS
|
||||
* to it. Turning it on for everyone would change the network path for
|
||||
* every user to solve a problem most of them do not have, and — on a
|
||||
* host where the bridge shares the relay's address — would not even help
|
||||
* against IP blocking.
|
||||
*
|
||||
* ⚠️ **The bridge is a separate hop, so obfs4 hides traffic from the
|
||||
* NETWORK, not from us.** It is not a privacy feature against the
|
||||
* operator, and must never be described as one.
|
||||
*/
|
||||
fun defaultHttpClient(bridge: Obfs4Bridge? = null): OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
.callTimeout(25, TimeUnit.SECONDS)
|
||||
// No response cache, deliberately. The payloads are ciphertext, but
|
||||
// sealed blobs and envelope traffic surviving on disk after the user
|
||||
// deletes a chat is exactly what "deleted means deleted" forbids.
|
||||
.cache(null)
|
||||
.apply {
|
||||
if (bridge != null) {
|
||||
socketFactory(Obfs4SocketFactory(bridge))
|
||||
// ⚠️ **Connections must not be pooled across a transport
|
||||
// change**, and the handshake makes each one expensive to
|
||||
// establish, so the pool is kept but the idle window is
|
||||
// short: a bridge that goes away should not leave requests
|
||||
// queued behind a dead tunnel for minutes.
|
||||
connectionPool(okhttp3.ConnectionPool(4, 2, TimeUnit.MINUTES))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user