Add support for ECH on Android 37#9383
Open
yschimke wants to merge 62 commits into
Open
Conversation
# Conflicts: # android-test/build.gradle.kts # android-test/src/androidDeviceTest/java/okhttp/android/test/EchTest.kt # okhttp/build.gradle.kts
This comment has been minimized.
This comment has been minimized.
# Conflicts: # gradle/libs.versions.toml # okhttp/build.gradle.kts
EchAware exposes the internal EchConfig type, so the interface must be internal too; it was unintentionally left public when moved to the okhttp3.internal package.
Reintroduces @ExperimentalOkHttpApi and an AsyncDns resolver that returns rich DnsResult values rather than bare addresses: - DnsResult.Address carries A/AAAA addresses; DnsResult.HttpsService carries an HTTPS/SVCB record (RFC 9460) with hints, ALPN, port and the ECHConfigList. Because results flow by value through DnsCallback, a resolver that wraps and forwards another preserves ECH/SVCB data automatically (it survives decoration, unlike the EchAware side-channel). - AsyncDns.asBlocking() bridges to the blocking Dns interface. - AndroidAsyncDns resolves both addresses and ECH from a single DnsResolver HTTPS query on API 36+. - OkHttpClient gains an experimental asyncDns, defaulting to the platform resolver (Platform.platformAsyncDns(); AndroidAsyncDns on API 37+). This is the API foundation. The connect path does not yet consume asyncDns; wiring route selection to read ECH off DnsResult.HttpsService (and removing the EchAware cache) is the next step. Builds: compileKotlinJvm, compileAndroidMain, compileAndroidHostTest, apiDump.
Revert the android37 job to the inline sdkmanager/avdmanager/emulator script (the android-emulator-runner action can't select the only available API 37 image, google_apis_playstore_ps16k, the 16 KB page-size variant).
Add api-level 37.0 (target playstore_ps16k -> google_apis_playstore_ps16k, the 16 KB page-size image) to the android matrix and drop the standalone android37 job.
Lint flags the DnsResolver(Context, Looper) constructor as requiring an SDK extension; suppress at the class level like AndroidDnsResolver, since the resolver is gated at runtime by @RequiresApi(36).
The single HttpsEndpoint query relied on the HTTPS answer for addresses, which is unreliable for hosts without an HTTPS record. Issue independent A, AAAA and HTTPS queries instead, delivering each as its own DnsResult batch; the last to complete reports hasMore = false (atomic counter). Add AsyncDns.newCall(hostname, addressesOnly): when true the resolver may skip the HTTPS/SVCB query. asBlocking() (which feeds Dns.lookup) passes addressesOnly = true since Dns cannot carry ECH.
Promote okhttp3.ech.EchConfig, EchMode and EchModeConfiguration from internal to public @ExperimentalOkHttpApi, consistent with AsyncDns/ DnsResult. Opt the internal consumers and tests in via file-level @OptIn, and regenerate the API dumps.
Revert the @ExperimentalOkHttpApi approach: AsyncDns, DnsResult and the okhttp3.ech types (EchConfig, EchMode, EchModeConfiguration) go back to internal, OkHttpClient.asyncDns becomes an internal field (no public builder), and the reintroduced ExperimentalOkHttpApi marker is removed. This matches the reviewer's guidance to keep unstable ECH/DNS APIs out of the public surface (cf. the EchAware "internal package" comment, and the team having removed @ExperimentalOkHttpApi / stashed AsyncDns rather than ship incomplete public API). API dumps return to baseline.
Replace call.tag(EchMode::class) / call.tag(EchConfig::class) with internal echMode / echConfig fields on RealCall. Tags are an end-user extension point; OkHttp's own per-call state belongs in fields (and setting echMode = Fallback on retry is now an unambiguous overwrite rather than tag compute-if-absent). EchAware is unchanged: it still bridges Dns.lookup (which has no Call) to applyEch; only the tag-based carrying is removed.
Introduce okhttp3.internal.RealCall.resolveAddresses(dns, hostname): the internal, call-aware DNS entry point. When the platform has an AsyncDns it resolves with it and captures the HTTPS/SVCB ECH config onto the call's echConfig field; otherwise it drops the call and uses the public, call-less Dns. RouteSelector now goes through it. With ECH carried on the call field, applyEch becomes apply-only (reconstructing EchConfigList via EchConfigList.fromBytes), and EchAware, its per-host cache, AndroidDnsResolverDns and AndroidEchConfig are removed. Android17Platform shares one AndroidAsyncDns for both platformDns (asBlocking) and platformAsyncDns. Follows the reviewer's guidance: state on a field (not a tag), and the new DNS API lives in the okhttp3.internal package.
Address ECH PR review feedback
yschimke
commented
Jun 17, 2026
yschimke
commented
Jun 17, 2026
| * Typical implementations are backed by Android's `DnsResolver`, OkHttp's DnsOverHttps, or other | ||
| * resolver libraries. Implementations must be safe for concurrent use. | ||
| */ | ||
| internal fun interface AsyncDns { |
Collaborator
Author
There was a problem hiding this comment.
Should we make this public? Probably as follow up.
yschimke
commented
Jun 17, 2026
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package okhttp3 |
Collaborator
Author
There was a problem hiding this comment.
Should move to internal package.
Collaborator
Author
There was a problem hiding this comment.
Maybe awkward if it's exposed on public APIs
yschimke
commented
Jun 17, 2026
| * The asynchronous DNS resolver, or null to resolve with [dns] only. When set, the connection | ||
| * path can use HTTPS/SVCB records it returns (including Encrypted Client Hello configuration). | ||
| */ | ||
| internal val asyncDns: AsyncDns? = builder.asyncDns |
Collaborator
Author
There was a problem hiding this comment.
Should setting dns null this out?
yschimke
commented
Jun 17, 2026
| }, | ||
| // executor is only used for handoff, so a minimal direct Executor | ||
| private val executor: Executor = Executor { it.run() }, | ||
| private val timeoutMillis: Int = 5_000, |
Collaborator
Author
There was a problem hiding this comment.
TODO - move outside AndroidAsyncDns, something Call timeout related?
Bump the API 37 matrix entry to -memory 3072 (others stay at 2048) via a per-entry matrix.memory with a 2048 fallback; both the snapshot-create and run steps use it so the cached snapshot still matches.
Make the -gpu flag a per-entry matrix value: the other levels keep -gpu swiftshader_indirect, API 37 sets gpu: "" so it boots with no -gpu flag. Both emulator steps use it so the snapshot still matches.
Bump the API 37 matrix entry from 3072 to 4096 in case system_server was starved bringing up the package service.
The android job's cache-warming step ran ./gradlew :android-test:test, i.e. the Robolectric unit tests, which fetch the android-all artifact and take ~30 min (and fail under SDK 37 via MavenArtifactFetcher) — timing out the API 37 emulator job before Create AVD / Run Tests. Build the instrumentation APK instead so the emulator step actually runs.
Robolectric stalls/fails fetching the android-all artifacts for the @config SDKs under this SDK 37 build (MavenArtifactFetcher), which also timed out the emulator job's cache-warm step. Disable the android-test Robolectric unit tests until Robolectric supports it; the instrumentation tests aren't Test tasks, so connectedCheck still runs.
The no-gpu experiment made the emulator unstable right after boot (device offline / broken pipe), failing the snapshot step. swiftshader is needed for a stable headless emulator, so revert to -gpu swiftshader_indirect (keep the 4 GB headroom on API 37).
CI: tune the API 37 emulator job
Robolectric has no android-all artifact for the pre-release SDK 37, so its unit tests stall/fail (MavenArtifactFetcher). Gate the disable on a compileApi constant (shared with compileSdk/targetSdk) so the tests run again once we compile against a Robolectric-supported SDK.
The android-emulator-runner waits for sys.boot_completed, but on API 37 the package/activity services can still be initializing, so APK install fails with 'Can't find service: package'. Poll for those services before connectedCheck; it's a no-op on levels where they're already up.
Restoring the cached snapshot leaves the API 37 emulator unstable mid-run:
system_server drops the package service while installing the second APK
('Can't find service: package' / 'install-write all apks' after tests have
already started). Boot fresh (-no-snapshot) for API 37 via matrix.coldBoot;
other levels keep the snapshot.
The Play image's system_server drops the package service mid-run on the runner. Switch API 37 to system-images;android-37.0;google_apis_ps16k;x86_64 (no Play services) to see if the lighter image is more stable.
The pre-release API 37 emulator image's system_server is unstable on CI runners (drops package/activity/settings services mid-run) regardless of snapshot/cold-boot, memory, GPU, or image variant. Mark the entry experimental and continue-on-error so it runs as a canary without blocking the build; revert to the snapshot path (drop cold-boot) since that got furthest. Re-evaluate when the image matures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DnsResolverHTTPS/SVCB lookups to capture ECH configuration records alongside address resolution.Dnsimplementations can expose platform-specific ECH data without returning rawAnyvalues.EchConfigListvalues to TLS sockets when the activeEchModeattempts ECH.NetworkSecurityPolicy.getDomainEncryptionMode()to select the ECH mode for each host.Remoteso they don't run in normal CI.Validation is mostly local Android/JVM compile and API checks, plus host-side tests for the DNS/ECH policy plumbing. The live ECH tests remain opt-in because
they depend on external servers.