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>
52 lines
1.9 KiB
Kotlin
52 lines
1.9 KiB
Kotlin
plugins {
|
|
kotlin("jvm")
|
|
jacoco
|
|
}
|
|
|
|
repositories { mavenCentral() }
|
|
|
|
dependencies {
|
|
// 🔴 **The whole reason this port is small.** The Swift implementation had
|
|
// to hand-write SipHash-2-4, XSalsa20 and Poly1305 because CryptoKit
|
|
// exposes none of them, and its own field arithmetic because Apple gives
|
|
// nothing below X25519. BouncyCastle has all four, vetted and
|
|
// constant-time where it matters: `X25519Field` (including `sqrtRatioVar`,
|
|
// exactly the primitive Elligator2 needs), `XSalsa20Engine`, `Poly1305`.
|
|
//
|
|
// ⚠️ That means the two clients do NOT share an implementation. They share
|
|
// the vectors and the live interop harness, which is what actually keeps
|
|
// them honest — see `Obfs4VectorTest`.
|
|
//
|
|
// Already a dependency of `:engine`, for the same class of reason: taking a
|
|
// vetted constant-time implementation over hand-rolling arithmetic on a
|
|
// secret scalar.
|
|
implementation("org.bouncycastle:bcprov-jdk18on:1.79")
|
|
|
|
// ⚠️ compileOnly: the transport must not drag OkHttp into anything that
|
|
// does not already have it. `:client` supplies the real one.
|
|
compileOnly("com.squareup.okhttp3:okhttp:4.12.0")
|
|
testImplementation("com.squareup.okhttp3:okhttp:4.12.0")
|
|
testImplementation(kotlin("test"))
|
|
testImplementation("org.json:json:20240303")
|
|
}
|
|
|
|
kotlin {
|
|
compilerOptions {
|
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
|
}
|
|
}
|
|
java {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
|
|
tasks.test {
|
|
useJUnitPlatform()
|
|
testLogging { events("passed", "failed", "skipped"); showStandardStreams = true }
|
|
}
|
|
tasks.withType<Test>().configureEach { finalizedBy(tasks.named("jacocoTestReport")) }
|
|
tasks.named<JacocoReport>("jacocoTestReport") {
|
|
dependsOn(tasks.named("test"))
|
|
reports { xml.required.set(true); html.required.set(true) }
|
|
}
|