Skip to content

Add support for ECH on Android 37#9383

Open
yschimke wants to merge 62 commits into
square:masterfrom
yschimke:testing_ech
Open

Add support for ECH on Android 37#9383
yschimke wants to merge 62 commits into
square:masterfrom
yschimke:testing_ech

Conversation

@yschimke

@yschimke yschimke commented Mar 19, 2026

Copy link
Copy Markdown
Collaborator
  • Adds an Android 17 platform path that uses public Android APIs for ALPN, session tickets, domain encryption policy, and ECH socket configuration.
  • Uses Android DnsResolver HTTPS/SVCB lookups to capture ECH configuration records alongside address resolution.
  • Introduces typed ECH DNS records so Dns implementations can expose platform-specific ECH data without returning raw Any values.
  • Applies Android EchConfigList values to TLS sockets when the active EchMode attempts ECH.
  • Uses NetworkSecurityPolicy.getDomainEncryptionMode() to select the ECH mode for each host.
  • Retries once without ECH when the platform reports an ECH configuration mismatch and the active mode permits fallback.
  • Tags live external ECH checks as Remote so they don't run in normal CI.
  • Includes CI cleanup for the AGP source-set API change and Android localhost cleartext test traffic.

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.

yschimke and others added 13 commits January 16, 2026 11:23
@github-advanced-security

This comment has been minimized.

@yschimke yschimke changed the title Testing ech Add support for ECH on Android 37 May 4, 2026
@yschimke yschimke marked this pull request as ready for review May 4, 2026 10:19
@yschimke yschimke requested a review from swankjesse May 4, 2026 10:51
yschimke added 11 commits June 16, 2026 11:46
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.
Comment thread okhttp/src/androidMain/kotlin/okhttp3/android/AndroidAsyncDns.kt
* 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 {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this public? Probably as follow up.

* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should move to internal package.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe awkward if it's exposed on public APIs

* 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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should setting dns null this out?

},
// executor is only used for handoff, so a minimal direct Executor
private val executor: Executor = Executor { it.run() },
private val timeoutMillis: Int = 5_000,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO - move outside AndroidAsyncDns, something Call timeout related?

yschimke added 10 commits June 17, 2026 12:47
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).
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.
yschimke added 2 commits June 17, 2026 16:04
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants