Resolve IPv6-only hostnames for WebView and OkHttp on API 23+#6906
Resolve IPv6-only hostnames for WebView and OkHttp on API 23+#6906uptake4385 wants to merge 10 commits into
Conversation
Android's default InetAddress.getAllByName() (used by OkHttp's Dns.SYSTEM) may skip AAAA (IPv6) DNS queries based on a routing-table probe for the 2000:: prefix. On IPv6-mostly or DNS64 networks this causes connection failures because only A records are queried and no A record exists. This change introduces NetworkAwareDns, a custom OkHttp DNS resolver that uses ConnectivityManager.activeNetwork.getAllByName() instead, which resolves addresses through an independent code path in Android's netd that does not apply the AI_ADDRCONFIG / 2000:: heuristic. Changes: - Add NetworkAwareDns class implementing both Dns and OkHttpConfigurator - Bind NetworkAwareDns to the shared OkHttpClient via the existing OkHttpConfigurator multibinding set in NetworkModule - Update DefaultConnectivityChecker.dns() to use network-aware DNS - Add unit tests covering IPv4, IPv6-only, mixed, fallback, and error scenarios Fixes an issue where the app fails to connect to HA instances that only have AAAA DNS records (IPv6-only / IPv6-mostly networks).
Route WebView through a local CONNECT proxy when PROXY_OVERRIDE is available, with OkHttp request interception and explicit AAAA DNS on older API levels as fallback. Add design documentation for the networking workarounds.
There was a problem hiding this comment.
Hi @uptake4385
It seems you haven't yet signed a CLA. Please do so here.
Once you do that we will be able to review and accept this pull request.
Thanks!
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a documented, test-covered networking stack to reliably resolve IPv6-only hostnames on Android (including WebView traffic), avoiding Android’s AAAA-skipping behavior and preserving TLS hostname validation.
Changes:
- Introduces
NetworkAwareDns+NetworkBoundDnsLookupand wires them into the shared OkHttp configuration path. - Adds a localhost HTTP CONNECT proxy + WebView proxy override manager, with a GET/HEAD interception fallback when PROXY_OVERRIDE is unavailable.
- Adds design docs and new/updated unit tests around DNS parsing, CONNECT parsing, and WebView error matching.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/network-ipv6-webview-dns.md | New design/debug doc explaining IPv6 AAAA resolution + WebView proxying architecture. |
| common/src/test/kotlin/io/homeassistant/companion/android/common/data/network/NetworkBoundDnsLookupTest.kt | Unit tests for DNS query encoding + A/AAAA response parsing. |
| common/src/test/kotlin/io/homeassistant/companion/android/common/data/network/NetworkAwareDnsTest.kt | Unit tests for active network/system fallback resolution and OkHttp integration. |
| common/src/test/kotlin/io/homeassistant/companion/android/common/data/network/LocalConnectProxyTest.kt | Unit tests for CONNECT target parsing and line reading. |
| common/src/test/kotlin/io/homeassistant/companion/android/common/data/network/HostnameWebViewRequestProxyTest.kt | Unit tests for intercept fallback guard conditions. |
| common/src/main/kotlin/io/homeassistant/companion/android/di/NetworkModule.kt | Registers NetworkAwareDns into the OkHttp configurator multibinding set. |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/network/WebViewHostnameExtensions.kt | Adds URL → logical hostname helper for WebView error matching. |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/network/NetworkBoundDnsLookup.kt | Implements network-bound raw UDP DNS for API 23–28. |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/network/NetworkAwareDns.kt | Adds active-network DNS resolver with DnsResolver/UDP fallback and main-thread dispatching. |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/network/LocalConnectProxy.kt | Adds localhost CONNECT proxy for WebView tunneling and DNS control. |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/network/HostnameWebViewRequestProxy.kt | Adds WebView request interception fallback using OkHttp + custom DNS. |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/connectivity/DefaultConnectivityChecker.kt | Uses NetworkAwareDns and OkHttp for connectivity checks. |
| app/src/test/kotlin/io/homeassistant/companion/android/util/HAWebViewClientTest.kt | Adds test for logical-hostname-based main-frame error matching. |
| app/src/test/kotlin/io/homeassistant/companion/android/onboarding/connection/ConnectionViewModelTest.kt | Updates tests for new WebView proxy manager + logical hostname wiring. |
| app/src/test/kotlin/io/homeassistant/companion/android/frontend/webview/WebViewConnectProxyManagerTest.kt | Adds tests for PROXY_OVERRIDE support detection and active-state behavior. |
| app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt | Updates tests for proxy setup injection + logical hostname flow wiring. |
| app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt | Ensures WebView CONNECT proxy is configured before loading URLs. |
| app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebViewClient.kt | Adds request interception fallback + logical-hostname main-frame error matching. |
| app/src/main/kotlin/io/homeassistant/companion/android/onboarding/connection/ConnectionViewModel.kt | Configures CONNECT proxy before emitting URL and tracks logical hostname. |
| app/src/main/kotlin/io/homeassistant/companion/android/onboarding/connection/ConnectionScreen.kt | Moves loadUrl into a LaunchedEffect to decouple configuration from loading. |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/webview/WebViewConnectProxyManager.kt | Implements WebView PROXY_OVERRIDE configuration via LocalConnectProxy. |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewState.kt | Adds logicalHostname to states and exposes logicalHostnameOrNull. |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt | Configures CONNECT proxy on successful URL load and propagates logical hostname. |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendScreen.kt | Minor cleanup (removes stray blank line). |
| README.md | Links networking contributors to the new detailed IPv6/WebView DNS doc. |
| AGENTS.md | Adds index for AI tools/contributors pointing to the new design doc. |
| .github/copilot-instructions.md | Documents the intended DNS/WebView proxy architecture for contributors/AI tools. |
Bound DNS/TCP on the active network, serialize PROXY_OVERRIDE configure/clear, and cover Additional-section DNS parsing plus edge-case unit tests.
4be7fa9 to
cc9479e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
common/src/test/kotlin/io/homeassistant/companion/android/common/data/network/NetworkAwareDnsTest.kt:1
Executors.newSingleThreadExecutor()creates a non-daemon thread by default and the test doesn’t shut it down. This can cause JVM test runs to hang or leak threads across the suite. Prefer a daemon-thread executor for tests, reuse an existing test executor, or store the returnedExecutorServiceandshutdownNow()it in an@AfterEach.
…checks Use full 16-bit DNS transaction IDs, validate QNAME encoding, reject incomplete CONNECT headers, half-close relay streams, and cancel OkHttp calls when connectivity coroutines are cancelled.
54ee0a4 to
6af2d4d
Compare
Cancel unused CONNECT proxy on configure cancellation, guard stale URL state updates with a generation token, and deduplicate test dispatcher setup.
|
Without looking at the whole code that seems quite complicated for something that should just work. I don't think we can afford maintaining this for a very specific use case, if even the maintainer of the WebView are not doing it TBH. |
|
Hey @TimoPtr, thanks for the response. I had the same concern while working on this PR, which is also why I added the note:
The difficulty here is that the issue itself is actually quite simple, but the workaround unfortunately is not. In dual-stack or IPv6-mostly environments, the application never attempts to resolve/use AAAA records in this scenario, which makes IPv6-only services appear unreachable even when everything is configured correctly. I completely understand the concern about maintenance cost and complexity. I just could not find a significantly simpler way to solve this cleanly within the current constraints. If you or the maintainers have ideas for a simpler approach, I’d absolutely be open to reworking it. Even if this specific approach is not the right solution, I think the underlying IPv6/AAAA handling issue still needs some kind of solution long-term. |
Just to be sure to understand the setup. You have HA hosted on a IPv6 only? and you have a DNS that only contains a AAAA record and no A record? Does it works on Chrome on Android? Do you have other services like HA that supports this, like Immich or anything we could look at for instance? |
Yes, HA is hosted IPv6-only and my local DNS server only provides a AAAA record for it.
I just tested it and it works fine in Chrome on Android. The device is a Pixel 6 running Android 16.
I have many other services hosted the same way, but I still need to test which of them work or do not work in this situation. The phone is actually my wife's device, not mine, and she also does not use all of those services regularly, so I do not yet know which apps are affected and which are not. I can test some more of them later. |
To be transparent I'm not feeling comfortable merging this it's too big for the use case. I think it is only limited of the case of ULA right? And if you were using a global IPv6 address it would work. I've asked about the other app because they might have something in their codebase that we can look at that helps solve the issue without 3k line of code. Also if it's only your local network why not using IPv4 fallback here? I know it's not ideal but we can't handle the burden of an issue from the upstream like this. |
Thanks for the clarification, that makes sense and I can completely understand the maintenance concern. Just to clarify one detail: this does not only affect ULA addresses. I tested this with both ULA and GUA IPv6 addresses and in both cases the AAAA record was never queried/used in this scenario. My HA setup is behind a reverse proxy, so as a temporary workaround I was able to simply add an IPv4 address, which solved the immediate issue for me. The main reason I looked into this at all is because the network is intentionally designed to avoid IPv4 wherever possible. I also fully agree that the current PR is probably too large and invasive to realistically maintain long-term. Honestly, I was already hesitant to open it for that exact reason. My intention was less "this exact implementation must be merged" and more to start a discussion around the underlying issue and maybe find a cleaner/common approach together. Regarding other applications, I understood your point there as well. I need to check which apps on the device actually handle this correctly so we maybe have some implementations to compare against. |
|
I tested a few more examples now and both Jellyfin (with the Jellyfin Android app) and Vaultwarden/Bitwarden (using the Bitwarden Android app) work correctly in the same IPv6-only setup with AAAA-only DNS records. So there are definitely Android applications that handle this scenario properly, which may give us something useful to compare against. |
Then I encourage you to look at the codebase of each project to see how they handle it and come back with some details like links to where it is handle to see how it could fit in our project. |
Summary
Motivation
This has been a persistent issue for me for a long time. My Home Assistant instance is set up in an IPv6-mostly network using ULA (
fd00::/8) addresses, and the companion app would frequently fail to connect — while every other device and browser on the same network worked perfectly. The root cause is the same as described in #4953: "android isn't asking AAAA questions on the v4 DNS resolvers (like every other platform does)".Why this is complex: Android's
getaddrinfo()withAI_ADDRCONFIGdetermines IPv6 availability using a two-step probe in bionic (see Chromium issue #40309810):connect()a UDP socket to the hard-coded address2000::— succeeds if any route to global IPv6 space existsgetsockname()— checks whether the source address assigned to the socket is a global IPv6 address (2000::/3)If the source address is a ULA (
fd00::/8), link-local (fe80::/10), or the socket has no global IPv6 address at all, bionic considers IPv6 "not available" and skips AAAA DNS queries entirely — even when the network is otherwise IPv6-capable. Having a route to2000::is not sufficient; the device needs a global IPv6 source address.Google has marked the upstream issue as Won't Fix (#36955694), and Chromium has a related long-standing bug (#40435291).
The problem affects two completely independent networking stacks within the app:
DnsimplementationThere is no single, simple fix. The solution involves:
DnsResolver(API 29+) to bypassgetaddrinfo()entirelyDnsResolveris unavailableshouldInterceptRequestfallback for devices that don't support WebView proxy overrideWe understand this adds complexity to the codebase, but after evaluating all alternatives — URL rewriting to IPv6 literals breaks TLS/SNI,
Network.getAllByName()goes through the samegetaddrinfo()on older platforms,shouldInterceptRequestalone doesn't cover WebSockets — we believe this is the most robust approach that covers all API levels and traffic types.Architecture
OkHttp (
NetworkAwareDns):DnsResolver.query()for explicit TYPE_AAAA + TYPE_ANetworkBoundDnsLookup(raw UDP DNS on the active network's DNS servers)Network.getAllByName()→Dns.SYSTEMWebView:
WebViewFeature.PROXY_OVERRIDEsupported:LocalConnectProxy— localhost HTTP CONNECT proxy resolving viaNetworkAwareDns, connecting viaNetwork.socketFactoryHostnameWebViewRequestProxyviashouldInterceptRequestfor GET/HEADKey Design Decisions
BufferedReaderfor CONNECT parsing — would lose TLS ClientHello bytesDnsResolverdelivers I/O on the main looperPROXY_OVERRIDE— depends on WebView version, notminSdkRelated Issues
Documentation
See
docs/network-ipv6-webview-dns.mdfor the full design, architecture diagrams, debugging guide, and known limitations.Checklist
Screenshots
Not applicable — this is a networking infrastructure change, not a user-facing UI change.
Link to pull request in documentation repositories
User Documentation: N/A (no user-facing functionality changed)
Developer Documentation: home-assistant/developers.home-assistant#
Any other notes
We recognize that this is a significant change with substantial complexity. Android's platform-level DNS limitation (marked Won't Fix by Google) leaves apps with no simple alternative. We've aimed to make the implementation as self-contained as possible, with clear boundaries between the DNS resolver, the proxy, and the existing app code. The
docs/network-ipv6-webview-dns.mddocument explains the full rationale, alternatives considered, and maintenance guidelines.We're open to discussing whether the complexity is justified and would appreciate maintainer feedback on the approach.