[PM-40402] SocketTimeoutException trying to connect to directly attached IPv6 addresses
- Dominant language
- Kotlin
- Stars
- 9.4k
- Forks
- 1.1k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 67
Description
### Steps To Reproduce
1. Host a bitwarden server on your local network, accessible only over IPv6 (for example, some IPv6 ULA such as `fdd4:aa51:eed9:426:9f99:938b:ebf9:7433`, accessible via `bitwarden.example.com`, which you'll see the in stacktrace below.).
2. With your android phone connected to the same local network: open bitwarden, on the login screen, select "Self-hosted", tap "Self-hosted", and enter the url of your server (in this example: `https://bitwarden.example.com`).
3. Try to log in. You'll experience a delay after pressing "Continue" on the login screen, but you will eventually make it to the master password prompt screen.
4. On the master password prompt screen: enter your master password and click "Log in with master password". After 10 seconds of "Logging in...", you'll get a popup that says "An error has occurred. We were unable to process your request. Please try again or contact us." If you click "Share error details", you'll see a stacktrace of a `SocketTimeoutException` (full trace below).
### Expected Result
I expect to be able to log into my bitwarden server.
### Actual Result
Here's the stack trace:
```
Stacktrace:
java.net.SocketTimeoutException: failed to connect to bitwarden.example.com/fdd4:aa51:eed9:426:9f99:938b:ebf9:7433 (port 443) from /fdd4:aa51:eed9:426:9f99:93b5:4d8:4a2e (port 38658) after 10000ms
libcore.io.IoBridge.connectErrno(IoBridge.java:235)
libcore.io.IoBridge.connect(IoBridge.java:179)
java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:142)
java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:390)
java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:230)
java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:212)
java.net.SocksSocketImpl.connect(SocksSocketImpl.java:436)
java.net.Socket.connect(Socket.java:646)
ev1.i(r8-map-id-01e002bd0632bb5cc7de20747ff74afc7c2ccce2c9b98107678ea7daf8b79b97:77)
ev1.d(r8-map-id-01e002bd0632bb5cc7de20747ff74afc7c2ccce2c9b98107678ea7daf8b79b97:38)
r63.a(r8-map-id-01e002bd0632bb5cc7de20747ff74afc7c2ccce2c9b98107678ea7daf8b79b97:3)
ia.a(r8-map-id-01e002bd0632bb5cc7de20747ff74afc7c2ccce2c9b98107678ea7daf8b79b97:67)
ia.run(r8-map-id-01e002bd0632bb5cc7de20747ff74afc7c2ccce2c9b98107678ea7daf8b79b97:82)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1100)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
java.lang.Thread.run(Thread.java:1572)
Version: 2026.6.1 (21713)
Device: 📱 google Pixel 6 🤖 17@37 📦 prod -fdroid
CI: 🧱 commit: bitwarden/android/release/2026.6-rc57@e5ee43f68773f9fe20bec3dd664781d65b804967
💻 build source: bitwarden/android/actions/runs/28253991408/attempts/1
```
### Screenshots or Videos
_No response_
### Additional Context
The last version of the app where this worked was 2026.5.0. I tested 2026.6.1, 2026.6.0, and 2026.5.1 and confirmed they do not work.
I bisected and found that this was introduced by https://github.com/bitwarden/android/pull/6916, which upgraded the android sdk from 36 to 37. One consequence of sdk version 37 is that [local network permission](https://developer.android.com/privacy-and-security/local-network-permission) is now disabled by default. The bitwarden android app only requests local network access if the address we're connecting to [is a "site local address", as returned by `InetAddress.isSiteLocalAddress`](https://github.com/bitwarden/android/blob/v2026.6.1-bwpm/network/src/main/kotlin/com/bitwarden/network/interceptor/PermissionInterceptor.kt#L42). Here are the implementations for ipv4 and ipv6:
- ipv4: https://android.googlesource.com/platform/libcore/+/android-17.0.0_r1/ojluni/src/main/java/java/net/Inet4Address.java#239
- RFC 1918 calls these "private" addresses
- ipv6: https://android.googlesource.com/platform/libcore/+/android-17.0.0_r1/ojluni/src/main/java/java/net/Inet6Address.java#365
- This implements ipv6's long-deprecated "site local addresses". The modern replacement are unique local addresses (ULAs). Wikipedia has a decent history: https://en.wikipedia.org/wiki/Unique_local_address#History).
However, I don't think the correct fix here is to just add "is ula?" to the check. I believe the intent of the code is to check "would android block connections to this ip address?", which android conveniently documents on their [Local Network Definition](https://developer.android.com/privacy-and-security/local-network-definition) page. Fun fact: this reveals that our ipv4 check is likely wrong as well (android blocks link local and CGNAT addresses, which are not detected by [`Inet4Address.isSiteLocalAddress`](https://android.googlesource.com/platform/libcore/+/android-17.0.0_r1/ojluni/src/main/java/java/net/Inet4Address.java#239)). The IPv6 check is even harder to implement, as it's not just a comparison against static subnets, it depends on your routing table.
AFAICT, there's no easy way to implement this check (I've asked android for advice on this in https://issuetracker.google.com/issues/534973523). I think one correct way to do it is to just try and check for the error reason afterwards with `android_getnetworkblockedreason`, but I guess that requires trying and waiting for a timeout, and it requires using the NDK.
Perhaps the least odious approach would be to prompt the user for local network permission if they select "Self-hosted"? Or catching `SocketTimeoutException` and warning the user that they might need to grant the "Nearby devices" permission?
### Build Version
2026.6.1 (21713)
### What server are you connecting to?
Self-host
### Self-host Server Version
I promise this is not relevant
### Environment Details
- Device: Google Pixel 6
- Android version: 17
### Issue Tracking Info
- [x] I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.
Contributor guide
Research direction
Start in network/src/main/kotlin/com/bitwarden/network/interceptor/PermissionInterceptor.kt and review the Android local-network permission definition plus PR #6916. Reproduce the self-hosted IPv6 ULA login failure on the reported Android version, then determine and implement the permission or error handling that avoids the timeout. Done means the reported self-hosted IPv6 login succeeds and behavior is verified for the affected address cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 38/100