Skip to content

Resolve IPv6-only hostnames for WebView and OkHttp on API 23+#6906

Draft
uptake4385 wants to merge 10 commits into
home-assistant:mainfrom
uptake4385:feat/network-aware-dns
Draft

Resolve IPv6-only hostnames for WebView and OkHttp on API 23+#6906
uptake4385 wants to merge 10 commits into
home-assistant:mainfrom
uptake4385:feat/network-aware-dns

Conversation

@uptake4385
Copy link
Copy Markdown

@uptake4385 uptake4385 commented May 26, 2026

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() with AI_ADDRCONFIG determines IPv6 availability using a two-step probe in bionic (see Chromium issue #40309810):

  1. connect() a UDP socket to the hard-coded address 2000:: — succeeds if any route to global IPv6 space exists
  2. getsockname() — 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 to 2000:: 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:

  1. OkHttp (REST API, WebSocket, connectivity checks) — can be fixed via a custom Dns implementation
  2. WebView (Home Assistant frontend) — uses Chromium's own DNS/TLS stack, which does not go through OkHttp

There is no single, simple fix. The solution involves:

  • Explicit DNS queries via DnsResolver (API 29+) to bypass getaddrinfo() entirely
  • Raw UDP DNS for API 23–28 where DnsResolver is unavailable
  • A localhost HTTP CONNECT proxy to route WebView traffic through our DNS resolver
  • A shouldInterceptRequest fallback for devices that don't support WebView proxy override

We 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 same getaddrinfo() on older platforms, shouldInterceptRequest alone doesn't cover WebSockets — we believe this is the most robust approach that covers all API levels and traffic types.

Architecture

OkHttp (NetworkAwareDns):

  • API 29+: DnsResolver.query() for explicit TYPE_AAAA + TYPE_A
  • API 23–28: NetworkBoundDnsLookup (raw UDP DNS on the active network's DNS servers)
  • Fallback: Network.getAllByName()Dns.SYSTEM

WebView:

  • WebViewFeature.PROXY_OVERRIDE supported: LocalConnectProxy — localhost HTTP CONNECT proxy resolving via NetworkAwareDns, connecting via Network.socketFactory
  • PROXY_OVERRIDE unavailable: HostnameWebViewRequestProxy via shouldInterceptRequest for GET/HEAD

Key Design Decisions

  • No URL rewriting to IPv6 literals — breaks TLS certificate validation (SNI expects domain name)
  • No BufferedReader for CONNECT parsing — would lose TLS ClientHello bytes
  • DNS work dispatched off the main threadDnsResolver delivers I/O on the main looper
  • Runtime feature detection for PROXY_OVERRIDE — depends on WebView version, not minSdk
  • User-facing strings are in English only — existing pattern in the project

Related Issues

Documentation

See docs/network-ipv6-webview-dns.md for the full design, architecture diagrams, debugging guide, and known limitations.

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

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.md document 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.

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.
Copy link
Copy Markdown

@home-assistant home-assistant Bot left a comment

Choose a reason for hiding this comment

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

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!

@uptake4385 uptake4385 marked this pull request as ready for review May 26, 2026 19:13
Copilot AI review requested due to automatic review settings May 26, 2026 19:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 + NetworkBoundDnsLookup and 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.
@uptake4385 uptake4385 force-pushed the feat/network-aware-dns branch from 4be7fa9 to cc9479e Compare May 27, 2026 10:35
@uptake4385 uptake4385 requested a review from Copilot May 27, 2026 10:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 returned ExecutorService and shutdownNow() 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.
@uptake4385 uptake4385 force-pushed the feat/network-aware-dns branch from 54ee0a4 to 6af2d4d Compare May 27, 2026 11:07
@uptake4385 uptake4385 requested a review from Copilot May 27, 2026 11:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 3 comments.

Cancel unused CONNECT proxy on configure cancellation, guard stale URL
state updates with a generation token, and deduplicate test dispatcher setup.
@uptake4385 uptake4385 requested a review from Copilot May 27, 2026 11:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot wasn't able to review any files in this pull request.

@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented May 27, 2026

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.

@uptake4385
Copy link
Copy Markdown
Author

Hey @TimoPtr, thanks for the response.

I had the same concern while working on this PR, which is also why I added the note:

"We're open to discussing whether the complexity is justified and would appreciate maintainer feedback on the approach."

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.

@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented May 27, 2026

Hey @TimoPtr, thanks for the response.

I had the same concern while working on this PR, which is also why I added the note:

"We're open to discussing whether the complexity is justified and would appreciate maintainer feedback on the approach."

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?

@uptake4385
Copy link
Copy Markdown
Author

Hey @TimoPtr, thanks for the response.
I had the same concern while working on this PR, which is also why I added the note:

"We're open to discussing whether the complexity is justified and would appreciate maintainer feedback on the approach."

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.

Does it works on Chrome on Android?

I just tested it and it works fine in Chrome on Android. The device is a Pixel 6 running Android 16.

Do you have other services like HA that supports this, like Immich or anything we could look at for instance?

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.

@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented May 27, 2026

Hey @TimoPtr, thanks for the response.
I had the same concern while working on this PR, which is also why I added the note:

"We're open to discussing whether the complexity is justified and would appreciate maintainer feedback on the approach."

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.

Does it works on Chrome on Android?

I just tested it and it works fine in Chrome on Android. The device is a Pixel 6 running Android 16.

Do you have other services like HA that supports this, like Immich or anything we could look at for instance?

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.

@TimoPtr TimoPtr marked this pull request as draft May 27, 2026 15:39
@uptake4385
Copy link
Copy Markdown
Author

Hey @TimoPtr, thanks for the response.
I had the same concern while working on this PR, which is also why I added the note:

"We're open to discussing whether the complexity is justified and would appreciate maintainer feedback on the approach."

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.

Does it works on Chrome on Android?

I just tested it and it works fine in Chrome on Android. The device is a Pixel 6 running Android 16.

Do you have other services like HA that supports this, like Immich or anything we could look at for instance?

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.

@uptake4385
Copy link
Copy Markdown
Author

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.

@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented May 27, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Companion app fails to connect to IPv6-only HA instance

3 participants