⚠️ Before posting ⚠️
Steps to reproduce
- On an Android 16 device, go to Settings → Network & Internet → VPN
- Configure a VPN and enable Always-On VPN (optionally also enable Block connections without VPN)
- Connect the VPN and confirm that a browser can reach external sites
- Open the Nextcloud app and attempt to add an account or sync
NOTE: The VPN network hosts a NextCloud server however, is isolated from the Internet so the NextCloud does not have Internet access. The VPN app I am using is the Samsung secure VPN (com.samsung.sVpn) but I believe this would happen with any VPN client on an Internet isolated network.
Expected behaviour
The app connects to the Nextcloud server normally. VPN transport should be treated as a valid, connected network — the same way a Wi-Fi or cellular connection is treated.
Actual behaviour
The app displays a "No network connection" error. No network request is attempted. The login/sync flow is completely blocked despite the device having working internet connectivity through the VPN.
Android version
Android 16 (April 5th Patch level)
Device brand and model
Samsung XCover6 Pro
Stock or custom OS?
Stock
Nextcloud android app version
33.1.0
Nextcloud server version
30.0.6
Using a reverse proxy?
Yes
Android logs
No response
Server error logs
Additional information
Root Cause
There are two distinct failure paths, both stemming from how GetStatusRemoteOperation (in the android-library) determines whether the device is online.
Path 1 — isOnline() returns false before any network request is made
GetStatusRemoteOperation.run() calls isOnline() as a pre-flight check and returns NO_NETWORK_CONNECTION immediately if it returns false.
The current isOnline() implementation on Android M+ uses the deprecated ConnectivityManager.getActiveNetworkInfo()?.isConnectedOrConnecting(). Under Always-On VPN, the active network is the VPN interface. In certain VPN configurations (particularly lockdown/strict mode) isConnectedOrConnecting() can
return false for the VPN interface even when it is carrying traffic — causing isOnline() to return false and the operation to abort before touching the network.
Additionally, ConnectivityServiceImpl.isNetworkAndServerAvailable() (in the app itself) checks for NET_CAPABILITY_INTERNET on the active network. In strict Always-On VPN lockdown mode, some VPN implementations do not advertise NET_CAPABILITY_INTERNET on the VPN network object, causing this check to fail
and connectivity to be reported as unavailable.
Path 2 — DNS resolution fails through the VPN tunnel
Even when isOnline() returns true, the HTTP client may throw UnknownHostException when trying to resolve the server hostname. The RemoteOperationResult(Exception e) constructor maps UnknownHostException directly to NO_NETWORK_CONNECTION. Under Always-On VPN lockdown, DNS traffic that bypasses the VPN
tunnel is blocked by Android, so if the VPN's DNS configuration is strict, hostname resolution can fail.
Analysis
The modern, correct way to check connectivity on Android M+ is via NetworkCapabilities, not the deprecated NetworkInfo APIs. Specifically, a device connected through a VPN will have:
- NetworkCapabilities.TRANSPORT_VPN set on the active network's capabilities
- NetworkCapabilities.NET_CAPABILITY_INTERNET may or may not be set depending on VPN implementation
The pre-flight isOnline() check should treat a network with TRANSPORT_VPN as online (provided it also has NET_CAPABILITY_INTERNET), rather than falling through to the deprecated getActiveNetworkInfo() path which misreports VPN state.
// Current (problematic) approach in GetStatusRemoteOperation:
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnectedOrConnecting(); // unreliable for VPN
// Corrected approach on API 23+:
Network activeNetwork = connectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
boolean hasInternet = capabilities != null && capabilities.hasCapability(NET_CAPABILITY_INTERNET);
boolean hasVpn = capabilities != null && capabilities.hasTransport(TRANSPORT_VPN);
return hasInternet || hasVpn; // treat VPN transport as online
Device / Environment
Additionally, ConnectivityServiceImpl.isNetworkAndServerAvailable() (in the app itself) checks for NET_CAPABILITY_INTERNET on the active network. In strict Always-On VPN lockdown mode, some VPN implementations do not advertise NET_CAPABILITY_INTERNET on the VPN network object, causing this check to fail
and connectivity to be reported as unavailable.
Path 2 — DNS resolution fails through the VPN tunnel
Even when isOnline() returns true, the HTTP client may throw UnknownHostException when trying to resolve the server hostname. The RemoteOperationResult(Exception e) constructor maps UnknownHostException directly to NO_NETWORK_CONNECTION. Under Always-On VPN lockdown, DNS traffic that bypasses the VPN
tunnel is blocked by Android, so if the VPN's DNS configuration is strict, hostname resolution can fail.
Analysis
The modern, correct way to check connectivity on Android M+ is via NetworkCapabilities, not the deprecated NetworkInfo APIs. Specifically, a device connected through a VPN will have:
- NetworkCapabilities.TRANSPORT_VPN set on the active network's capabilities
- NetworkCapabilities.NET_CAPABILITY_INTERNET may or may not be set depending on VPN implementation
The pre-flight isOnline() check should treat a network with TRANSPORT_VPN as online (provided it also has NET_CAPABILITY_INTERNET), rather than falling through to the deprecated getActiveNetworkInfo() path which misreports VPN state.
// Current (problematic) approach in GetStatusRemoteOperation:
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnectedOrConnecting(); // unreliable for VPN
// Corrected approach on API 23+:
Network activeNetwork = connectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
boolean hasInternet = capabilities != null && capabilities.hasCapability(NET_CAPABILITY_INTERNET);
boolean hasVpn = capabilities != null && capabilities.hasTransport(TRANSPORT_VPN);
return hasInternet || hasVpn; // treat VPN transport as online
Device / Environment
- Android version: 16 — uses NetworkCapabilities API path
- Nextcloud app version: reproducible on current master
- VPN type: Any Always-On VPN; more likely to trigger with "Block connections without VPN" (lockdown) enabled
- Nextcloud server version: Any
Additional Context
ConnectivityServiceImpl.isConnected() in the app already correctly handles VPN — it explicitly checks hasTransport(NetworkCapabilities.TRANSPORT_VPN) and returns true. The inconsistency is that the library's GetStatusRemoteOperation.isOnline() does not apply the same logic.
This issue is distinct from the walled-garden / captive portal check (isInternetWalled()), which correctly skips active probing for non-Wi-Fi connections including VPN.
Steps to reproduce
NOTE: The VPN network hosts a NextCloud server however, is isolated from the Internet so the NextCloud does not have Internet access. The VPN app I am using is the Samsung secure VPN (com.samsung.sVpn) but I believe this would happen with any VPN client on an Internet isolated network.
Expected behaviour
The app connects to the Nextcloud server normally. VPN transport should be treated as a valid, connected network — the same way a Wi-Fi or cellular connection is treated.
Actual behaviour
The app displays a "No network connection" error. No network request is attempted. The login/sync flow is completely blocked despite the device having working internet connectivity through the VPN.
Android version
Android 16 (April 5th Patch level)
Device brand and model
Samsung XCover6 Pro
Stock or custom OS?
Stock
Nextcloud android app version
33.1.0
Nextcloud server version
30.0.6
Using a reverse proxy?
Yes
Android logs
No response
Server error logs
Additional information
Root Cause
There are two distinct failure paths, both stemming from how GetStatusRemoteOperation (in the android-library) determines whether the device is online.
Path 1 — isOnline() returns false before any network request is made
GetStatusRemoteOperation.run() calls isOnline() as a pre-flight check and returns NO_NETWORK_CONNECTION immediately if it returns false.
The current isOnline() implementation on Android M+ uses the deprecated ConnectivityManager.getActiveNetworkInfo()?.isConnectedOrConnecting(). Under Always-On VPN, the active network is the VPN interface. In certain VPN configurations (particularly lockdown/strict mode) isConnectedOrConnecting() can
return false for the VPN interface even when it is carrying traffic — causing isOnline() to return false and the operation to abort before touching the network.
Additionally, ConnectivityServiceImpl.isNetworkAndServerAvailable() (in the app itself) checks for NET_CAPABILITY_INTERNET on the active network. In strict Always-On VPN lockdown mode, some VPN implementations do not advertise NET_CAPABILITY_INTERNET on the VPN network object, causing this check to fail
and connectivity to be reported as unavailable.
Path 2 — DNS resolution fails through the VPN tunnel
Even when isOnline() returns true, the HTTP client may throw UnknownHostException when trying to resolve the server hostname. The RemoteOperationResult(Exception e) constructor maps UnknownHostException directly to NO_NETWORK_CONNECTION. Under Always-On VPN lockdown, DNS traffic that bypasses the VPN
tunnel is blocked by Android, so if the VPN's DNS configuration is strict, hostname resolution can fail.
Analysis
The modern, correct way to check connectivity on Android M+ is via NetworkCapabilities, not the deprecated NetworkInfo APIs. Specifically, a device connected through a VPN will have:
The pre-flight isOnline() check should treat a network with TRANSPORT_VPN as online (provided it also has NET_CAPABILITY_INTERNET), rather than falling through to the deprecated getActiveNetworkInfo() path which misreports VPN state.
// Current (problematic) approach in GetStatusRemoteOperation:
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnectedOrConnecting(); // unreliable for VPN
// Corrected approach on API 23+:
Network activeNetwork = connectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
boolean hasInternet = capabilities != null && capabilities.hasCapability(NET_CAPABILITY_INTERNET);
boolean hasVpn = capabilities != null && capabilities.hasTransport(TRANSPORT_VPN);
return hasInternet || hasVpn; // treat VPN transport as online
Device / Environment
Additionally, ConnectivityServiceImpl.isNetworkAndServerAvailable() (in the app itself) checks for NET_CAPABILITY_INTERNET on the active network. In strict Always-On VPN lockdown mode, some VPN implementations do not advertise NET_CAPABILITY_INTERNET on the VPN network object, causing this check to fail
and connectivity to be reported as unavailable.
Path 2 — DNS resolution fails through the VPN tunnel
Even when isOnline() returns true, the HTTP client may throw UnknownHostException when trying to resolve the server hostname. The RemoteOperationResult(Exception e) constructor maps UnknownHostException directly to NO_NETWORK_CONNECTION. Under Always-On VPN lockdown, DNS traffic that bypasses the VPN
tunnel is blocked by Android, so if the VPN's DNS configuration is strict, hostname resolution can fail.
Analysis
The modern, correct way to check connectivity on Android M+ is via NetworkCapabilities, not the deprecated NetworkInfo APIs. Specifically, a device connected through a VPN will have:
The pre-flight isOnline() check should treat a network with TRANSPORT_VPN as online (provided it also has NET_CAPABILITY_INTERNET), rather than falling through to the deprecated getActiveNetworkInfo() path which misreports VPN state.
// Current (problematic) approach in GetStatusRemoteOperation:
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnectedOrConnecting(); // unreliable for VPN
// Corrected approach on API 23+:
Network activeNetwork = connectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
boolean hasInternet = capabilities != null && capabilities.hasCapability(NET_CAPABILITY_INTERNET);
boolean hasVpn = capabilities != null && capabilities.hasTransport(TRANSPORT_VPN);
return hasInternet || hasVpn; // treat VPN transport as online
Device / Environment
Additional Context
ConnectivityServiceImpl.isConnected() in the app already correctly handles VPN — it explicitly checks hasTransport(NetworkCapabilities.TRANSPORT_VPN) and returns true. The inconsistency is that the library's GetStatusRemoteOperation.isOnline() does not apply the same logic.
This issue is distinct from the walled-garden / captive portal check (isInternetWalled()), which correctly skips active probing for non-Wi-Fi connections including VPN.