App Lounge downloads fail at 0% when a non-bypassable VPN is active
- /e/OS version: 3.5-a15-20260211580868-official-FP6
- Device model(s): Fairphone 6 (FP6)
- Impacted Application: App Lounge
- Affected application/URL: App Lounge (foundation.e.apps) 2.15.1, system DownloadManager (com.android.providers.downloads)
- Browser/client and version: N/A
## The problem
App Lounge downloads stall permanently at 0% when a non-bypassable VPN (e.g. Tailscale) is active. Disabling the VPN allows downloads to complete normally. All other network activity (browsing, ping, curl, other apps) works fine through the VPN.
The root cause is an AOSP bug: the system `DownloadManager` obtains its `Network` object from `JobParameters.getNetwork()`, which returns the underlying WiFi network instead of the VPN network. The DNS proxy then immediately rejects DNS queries because the DownloadManager's UID is in the VPN's restricted range but is trying to use the non-VPN network directly.
{width=270 height=600}
## Steps to reproduce
1. Install Tailscale and connect (no exit node, default settings, VPN is `bypassable=false`)
2. Open App Lounge
3. Search for any app (e.g. "Easer") and tap Install
4. Download stays at 0% indefinitely
5. Disable Tailscale → retry → download completes normally
Any non-bypassable VPN should trigger this, not just Tailscale.
## Technical details
### What happens
App Lounge enqueues downloads via the system `DownloadManager`, which schedules a `JobScheduler` job. The `DownloadThread` gets its `Network` from `JobParameters.getNetwork()`:
```java
// DownloadThread.java (AOSP, unmodified in /e/OS)
mNetwork = mSystemFacade.getNetwork(mParams);
// RealSystemFacade.java
public Network getNetwork(JobParameters params) {
return params.getNetwork(); // provided by JobScheduler, NOT ConnectivityManager
}
```
The `JobScheduler`'s `ConnectivityController` sees both the VPN and the underlying WiFi as "available networks":
```
ConnectivityController Available networks:
103: WIFI|VPN [NOT_METERED, INTERNET, VALIDATED, ...] Uids: {0-10080, 10082-10258, ...}
105: WIFI [NOT_METERED, INTERNET, VALIDATED, NOT_VPN, ...]
```
The download job's constraint is satisfied by both. The `JobScheduler` selects WiFi (105) and provides it via `params.getNetwork()`. The `DownloadManager` (UID 10068) then calls `network.openConnection(url)`, DNS resolution is attempted on the WiFi network, and the DNS proxy immediately rejects it because UID 10068 is supposed to use the VPN.
### DownloadManager logcat (reproducible every attempt)
```
02-23 12:22:21.526 D/DownloadManager(27549): [301] Starting
02-23 12:22:21.546 W/DownloadManager(27549): [301] Stop requested with status HTTP_DATA_ERROR: Unable to resolve host "gitlab.e.foundation": No address associated with hostname
02-23 12:22:21.546 D/DownloadManager(27549): [301] Finished with status WAITING_TO_RETRY
02-23 12:22:21.551 D/DownloadManager(27549): [301] rescheduling the job id:301
02-23 12:22:21.553 D/DownloadManager(27549): onStopJob id=301, reason=cancel() called by app, callingUid=10068 uid=10068 jobId=301
02-23 12:22:31.844 D/DownloadManager(27549): [302] Starting
02-23 12:22:31.855 W/DownloadManager(27549): [302] Stop requested with status HTTP_DATA_ERROR: Unable to resolve host "apk.cleanapk.org": No address associated with hostname
02-23 12:22:31.855 D/DownloadManager(27549): [302] Finished with status WAITING_TO_RETRY
02-23 12:22:31.858 D/DownloadManager(27549): [302] rescheduling the job id:302
02-23 12:22:31.859 D/DownloadManager(27549): onStopJob id=302, reason=cancel() called by app, callingUid=10068 uid=10068 jobId=302
```
DNS fails in \~12ms (instant rejection, not a timeout). Both download sources fail (gitlab.e.foundation and apk.cleanapk.org).
### JobScheduler job details
```
#u0a68/299 DownloadManager:com.android.providers.downloads
Source: uid=u0a125 user=0 pkg=foundation.e.apps
Network type: NetworkRequest [ NONE id=0, [ Capabilities: NOT_METERED&INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10125 UnderlyingNetworks: Null] ]
Required constraints: TIMING_DELAY CONNECTIVITY FLEXIBILITY
Satisfied constraints: TIMING_DELAY CONNECTIVITY FLEXIBILITY DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED WITHIN_QUOTA
```
CONNECTIVITY shows as "Satisfied" — the JobScheduler found a matching network. But it provides the wrong one (WiFi instead of VPN) to the DownloadManager at runtime.
### DNS works for everything else through the VPN
```
$ adb shell ping -c 2 apk.cleanapk.org
PING apk.cleanapk.org (135.181.54.45) 56(84) bytes of data.
64 bytes from apk.cleanapk.org: icmp_seq=1 ttl=57 time=94.0 ms
$ adb shell curl -sS -o /dev/null https://f-droid.org/repo/index-v1.jar
(downloads 10.3MB successfully)
```
Opening `https://apk.cleanapk.org` in the /e/ browser also works (returns 403 as expected for root URL). All of these use `ConnectivityManager.getActiveNetwork()`, which correctly returns the VPN.
### VPN network state
```
Network 103 (VPN/Tailscale): VALIDATED, bypassable=false, DNS=100.100.100.100
Transports: WIFI|VPN
Uids: {0-10080, 10082-10258, 10260-20080, 20082-20258, 20260-99999}
UnderlyingNetworks: [105]
Network 105 (WiFi): VALIDATED, DNS=192.168.10.1
Transports: WIFI
DownloadManager UID: 10068 (within VPN UID range)
```
### What was ruled out
| Hypothesis | Result |
|------------|--------|
| Exit node routing | No exit node — only specific Tailscale /32 routes |
| DNS resolution via MagicDNS | Works for ping, curl, browser — all resolve correctly |
| Network validation failure | VPN network is VALIDATED |
| CDN/endpoint blocking | curl connects to gitlab.e.foundation and apk.cleanapk.org |
| Private DNS (DoT) conflict | Same failure with `private_dns_mode=off` |
| Stale DownloadManager process | Same failure after force-stop + fresh PID |
| App Lounge network config | Uses standard `DownloadManager.enqueue()`, no VPN-specific code |
| Firewall rules | UID 10068 allowed on all chains |
## Upstream context
This is an AOSP framework bug: `JobParameters.getNetwork()` does not respect `VpnService` UID routing when the VPN is non-bypassable. The DownloadProvider is unmodified AOSP/LineageOS code. Google Play Store is not affected because it uses its own download engine with `ConnectivityManager.getActiveNetwork()` (which correctly returns the VPN).
/e/OS is disproportionately affected because App Lounge is the primary app installation method and it relies on the system `DownloadManager`.
## Suggested workaround for App Lounge
App Lounge could download APKs directly using `HttpURLConnection` on the active network instead of delegating to the system `DownloadManager`:
```kotlin
val cm = getSystemService(ConnectivityManager::class.java)
val network = cm.activeNetwork // correctly returns VPN
val connection = network.openConnection(url) as HttpURLConnection
```
This bypasses the `JobScheduler` network selection and uses the correct VPN-aware network.
issue