Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.type.pc"
android:required="false" />

<application
android:name=".MyApplication"
Expand Down
64 changes: 64 additions & 0 deletions tool/src/main/java/io/netbird/client/tool/IFace.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@

import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkProperties;
import android.net.Network;
import android.net.RouteInfo;
import android.net.VpnService;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.system.OsConstants;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.RequiresApi;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import io.netbird.gomobile.android.TunAdapter;
Expand Down Expand Up @@ -74,6 +84,10 @@ private int createTun(String ip, int prefixLength, int mtu, String dns, String[]
Log.d(LOGTAG, "add route: "+r.addr+"/"+r.prefixLength);
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
excludeLocalRoutes(builder);
}

disallowApp(builder, "com.google.android.projection.gearhead");
disallowApp(builder, "com.google.android.apps.chromecast.app");
disallowApp(builder, "com.google.android.apps.messaging");
Expand All @@ -95,6 +109,56 @@ private int createTun(String ip, int prefixLength, int mtu, String dns, String[]
}
}

@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
private void excludeLocalRoutes(VpnService.Builder builder) {
Log.i(LOGTAG, "excludeLocalRoutes: checking local routes for exclusion (API 33+)");

ConnectivityManager cm = vpnService.getSystemService(ConnectivityManager.class);
if (cm == null) {
Log.w(LOGTAG, "excludeLocalRoutes: ConnectivityManager is null, skipping");
return;
}

Network activeNetwork = cm.getActiveNetwork();
if (activeNetwork == null) {
Log.w(LOGTAG, "excludeLocalRoutes: no active network found, skipping");
return;
}
Comment on lines +122 to +126
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getActiveNetwork() may return the VPN network itself during reconnects, yielding wrong routes.

When this app is reconnecting (e.g., network change triggers a re-key), an existing VPN interface can still be the "active" default network at the moment excludeLocalRoutes is called — before builder.establish() creates the replacement tunnel. In that scenario, getActiveNetwork() returns the VPN network, and getNetworkCapabilities would show it has TRANSPORT_VPN, so getLinkProperties() returns the VPN's own routes rather than the physical interface routes, completely defeating the exclusion logic.

Filter for the physical underlying network by skipping any network that carries TRANSPORT_VPN:

🐛 Proposed fix
-        Network activeNetwork = cm.getActiveNetwork();
-        if (activeNetwork == null) {
-            Log.w(LOGTAG, "excludeLocalRoutes: no active network found, skipping");
-            return;
+        Network activeNetwork = null;
+        for (Network net : cm.getAllNetworks()) {
+            NetworkCapabilities caps = cm.getNetworkCapabilities(net);
+            if (caps != null
+                    && !caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
+                    && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+                activeNetwork = net;
+                break;
+            }
         }
+        if (activeNetwork == null) {
+            Log.w(LOGTAG, "excludeLocalRoutes: no non-VPN active network found, skipping");
+            return;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tool/src/main/java/io/netbird/client/tool/IFace.java` around lines 122 - 126,
excludeLocalRoutes currently uses cm.getActiveNetwork() which can return the VPN
network during reconnects; detect and skip networks that carry TRANSPORT_VPN by
calling getNetworkCapabilities(activeNetwork) and checking
hasTransport(NetworkCapabilities.TRANSPORT_VPN) before using getLinkProperties.
In excludeLocalRoutes, when cm.getActiveNetwork() yields a network with
TRANSPORT_VPN, ignore it (log/warn) and either find the next non-VPN active
network or return early so you use the physical underlying interface routes
instead of the VPN's routes; reference getActiveNetwork(),
getNetworkCapabilities(), hasTransport(NetworkCapabilities.TRANSPORT_VPN),
getLinkProperties(), and the excludeLocalRoutes method to locate where to add
this check.


LinkProperties lp = cm.getLinkProperties(activeNetwork);
if (lp == null) {
Log.w(LOGTAG, "excludeLocalRoutes: no link properties for active network, skipping");
return;
}

Log.i(LOGTAG, "excludeLocalRoutes: active network interface=" + lp.getInterfaceName() + " routes=" + lp.getRoutes().size());

List<String> excluded = new ArrayList<>();
for (RouteInfo routeInfo : lp.getRoutes()) {
IpPrefix dest = routeInfo.getDestination();
if (dest.getPrefixLength() == 0) {
Log.d(LOGTAG, "excludeLocalRoutes: skipping default route " + dest);
continue;
}

try {
builder.excludeRoute(dest);
excluded.add(dest.toString());
Log.i(LOGTAG, "excludeLocalRoutes: excluded " + dest + " (iface=" + routeInfo.getInterface() + ")");
} catch (Exception e) {
Log.e(LOGTAG, "excludeLocalRoutes: failed to exclude " + dest + ": " + e.getMessage());
}
}

Log.i(LOGTAG, "excludeLocalRoutes: total excluded=" + excluded.size() + " routes=" + excluded);
if (!excluded.isEmpty()) {
String msg = "Excluding " + excluded.size() + " local routes: " + String.join(", ", excluded);
new Handler(Looper.getMainLooper()).post(() ->
Toast.makeText(vpnService, msg, Toast.LENGTH_LONG).show()
);
}
}

private void prepareDnsSetting(VpnService.Builder builder, String dns) {
if(dns == null) {
return;
Expand Down
Loading