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
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ private void initializeEngineConfigSwitches() {
binding.switchDisableFirewall.setChecked(goPreferences.getDisableFirewall());
binding.switchAllowSsh.setChecked(goPreferences.getServerSSHAllowed());
binding.switchBlockInbound.setChecked(goPreferences.getBlockInbound());
binding.switchDisableIpv6.setChecked(goPreferences.getDisableIPv6());

// Set up change listeners
binding.switchDisableClientRoutes.setOnCheckedChangeListener((buttonView, isChecked) -> {
Expand Down Expand Up @@ -275,7 +276,16 @@ private void initializeEngineConfigSwitches() {
Log.e(LOGTAG, "Failed to set block inbound", e);
}
});


binding.switchDisableIpv6.setOnCheckedChangeListener((buttonView, isChecked) -> {
try {
goPreferences.setDisableIPv6(isChecked);
goPreferences.commit();
} catch (Exception e) {
Log.e(LOGTAG, "Failed to set disable IPv6", e);
}
});

// Make parent layouts clickable to toggle switches (for TV remote)
binding.layoutAllowSsh.setOnClickListener(v -> {
binding.switchAllowSsh.toggle();
Expand All @@ -301,6 +311,10 @@ private void initializeEngineConfigSwitches() {
binding.switchDisableFirewall.toggle();
});

binding.layoutDisableIpv6.setOnClickListener(v -> {
binding.switchDisableIpv6.toggle();
});

} catch (Exception e) {
Log.e(LOGTAG, "Failed to initialize engine config switches", e);
}
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/io/netbird/client/ui/home/Peer.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
public class Peer {
private final Status status;
private final String ip;
private final String ipv6;
private final String fqdn;

public Peer(Status status, String ip, String fqdn) {
public Peer(Status status, String ip, String ipv6, String fqdn) {
this.status = status;
this.ip = ip;
this.ipv6 = ipv6;
this.fqdn = fqdn;
}

Expand All @@ -19,7 +21,11 @@ public String getIp() {
return ip;
}

public String getIpv6() {
return ipv6;
}

public String getFqdn() {
return fqdn;
}
}
}
12 changes: 11 additions & 1 deletion app/src/main/java/io/netbird/client/ui/home/PeersAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ private static void showPopup(View view, Peer peer) {
PopupMenu popup = new PopupMenu(view.getContext(), view);
popup.getMenuInflater().inflate(R.menu.peer_clipboard_menu, popup.getMenu());

boolean hasIpv6 = peer.getIpv6() != null && !peer.getIpv6().isEmpty();
popup.getMenu().findItem(R.id.copy_ipv6).setVisible(hasIpv6);

popup.setOnMenuItemClickListener(menuItem -> {
int id = menuItem.getItemId();
if (id == R.id.copy_fqdn) {
Expand All @@ -128,6 +131,9 @@ private static void showPopup(View view, Peer peer) {
} else if (id == R.id.copy_ip) {
copyToClipboard(view.getContext(), "IP Address", peer.getIp());
return true;
} else if (id == R.id.copy_ipv6) {
copyToClipboard(view.getContext(), "IPv6 Address", peer.getIpv6());
return true;
}
return false;
});
Expand Down Expand Up @@ -165,7 +171,11 @@ public PeerViewHolder(ListItemPeerBinding binding) {

public void bind(Peer peer) {
binding.status.setText(peer.getStatus().toString());
binding.ip.setText(peer.getIp());
String ipDisplay = peer.getIp();
if (peer.getIpv6() != null && !peer.getIpv6().isEmpty()) {
ipDisplay = ipDisplay + "\n" + peer.getIpv6();
}
binding.ip.setText(ipDisplay);
binding.fqdn.setText(peer.getFqdn());

if (peer.getStatus() == Status.CONNECTED) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private List<Peer> getPeers(PeerInfoArray peersInfo) {
}

status = Status.fromLong(peerInfo.getConnStatus());
peers.add(new Peer(status, peerInfo.getIP(), peerInfo.getFQDN()));
peers.add(new Peer(status, peerInfo.getIP(), peerInfo.getIPv6(), peerInfo.getFQDN()));
}
return peers;
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/io/netbird/client/ui/home/Resource.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public Status getStatus() {
}

public boolean isExitNode() {
return address.equals("0.0.0.0/0");
return address != null && (address.contains("0.0.0.0/0") || address.contains("::/0"));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public boolean isSelected() {
Expand Down
51 changes: 50 additions & 1 deletion app/src/main/res/layout/fragment_advanced.xml
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,55 @@

</LinearLayout>

<LinearLayout
android:id="@+id/layout_disable_ipv6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/focus_highlight"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="false"
android:orientation="vertical"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_disable_firewall">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/advanced_disable_ipv6"
android:textColor="@color/nb_txt_light" />

<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_disable_ipv6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false" />

</LinearLayout>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="48dp"
android:text="@string/advanced_disable_ipv6_desc"
android:textColor="@color/nb_txt_light"
android:textSize="12sp" />

</LinearLayout>

<include
android:id="@+id/layout_force_relay_connection"
layout="@layout/component_switch"
Expand All @@ -529,7 +578,7 @@
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_disable_firewall" />
app:layout_constraintTop_toBottomOf="@id/layout_disable_ipv6" />

<LinearLayout
android:id="@+id/layout_theme"
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/menu/peer_clipboard_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
<item
android:id="@+id/copy_ip"
android:title="@string/peers_clipboard_copy_ip" />
<item
android:id="@+id/copy_ipv6"
android:title="@string/peers_clipboard_copy_ipv6" />
</menu>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<string name="peer_filter_connecting">Connecting</string>
<string name="peer_filter_connected">Connected</string>
<string name="peers_clipboard_copy_ip">Copy IP</string>
<string name="peers_clipboard_copy_ipv6">Copy IPv6</string>
<string name="peers_clipboard_copy_fqdn">Copy FQDN</string>
<string name="peers_desc_filter">Filter</string>

Expand Down Expand Up @@ -89,6 +90,8 @@
<string name="advanced_disable_dns_desc">Disables NetBird DNS configuration on this device</string>
<string name="advanced_disable_firewall">Disable firewall</string>
<string name="advanced_disable_firewall_desc">Disables the NetBird firewall</string>
<string name="advanced_disable_ipv6">Disable IPv6</string>
<string name="advanced_disable_ipv6_desc">Disables IPv6 overlay addressing on the tunnel interface</string>
<string name="advanced_allow_ssh">Enable SSH</string>
<string name="advanced_allow_ssh_desc">Allows SSH connections to this device</string>
<string name="advanced_block_inbound">Block inbound connections</string>
Expand Down
2 changes: 1 addition & 1 deletion netbird
Submodule netbird updated 345 files
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
public class AndroidPlatformFiles implements PlatformFiles {
private final String configurationFilePath;
private final String stateFilePath;
private final String cacheDir;

public AndroidPlatformFiles(String configurationFilePath, String stateFilePath) {
public AndroidPlatformFiles(String configurationFilePath, String stateFilePath, String cacheDir) {
this.configurationFilePath = configurationFilePath;
this.stateFilePath = stateFilePath;
this.cacheDir = cacheDir;
}

@Override
Expand All @@ -20,4 +22,9 @@ public String configurationFilePath() {
public String stateFilePath() {
return stateFilePath;
}

@Override
public String cacheDir() {
return cacheDir;
}
}
3 changes: 2 additions & 1 deletion tool/src/main/java/io/netbird/client/tool/EngineRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ private synchronized void runClient(@Nullable URLOpener urlOpener, boolean isAnd

// Create fresh PlatformFiles with current config/state paths
// This allows profile switching without recreating the entire Client
var platformFiles = new AndroidPlatformFiles(configurationFilePath, stateFilePath);
String cacheDir = context.getCacheDir().getAbsolutePath();
var platformFiles = new AndroidPlatformFiles(configurationFilePath, stateFilePath, cacheDir);
Log.d(LOGTAG, "Running engine with config: " + configurationFilePath + ", state: " + stateFilePath);

try {
Expand Down
31 changes: 27 additions & 4 deletions tool/src/main/java/io/netbird/client/tool/IFace.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,26 @@ public IFace(VPNService vpnService) {
}

@Override
public long configureInterface(String address, long mtu, String dns, String searchDomainsString, String routesString) throws Exception {
public long configureInterface(String address, String addressV6, long mtu, String dns, String searchDomainsString, String routesString) throws Exception {
String[] searchDomains = toSearchDomains(searchDomainsString);
LinkedList<Route> routes = toRoutes(routesString);

InetNetwork addr = InetNetwork.parse(address);
InetNetwork addrV6 = null;
if (addressV6 != null && !addressV6.isEmpty()) {
addrV6 = InetNetwork.parse(addressV6);
}
long fd = -1;

try {
fd = createTun(addr.getAddress().getHostAddress(), addr.getMask(), (int) mtu, dns, searchDomains, routes);
fd = createTun(addr.getAddress().getHostAddress(), addr.getMask(), addrV6, (int) mtu, dns, searchDomains, routes);
} catch (Exception e) {
Log.e(LOGTAG, "failed to create tunnel", e);
}

// only set the currently used TUN parameters if createTun didn't throw exceptions
if (fd != -1) {
this.vpnService.setCurrentTUNParameters(new TUNParameters(address, mtu, dns, searchDomainsString, routesString));
this.vpnService.setCurrentTUNParameters(new TUNParameters(address, addressV6, mtu, dns, searchDomainsString, routesString));
}

return fd;
Expand All @@ -57,9 +61,13 @@ public boolean protectSocket(int fd) {
return true;
}

private int createTun(String ip, int prefixLength, int mtu, String dns, String[] searchDomains, LinkedList<Route> routes) throws Exception {
private int createTun(String ip, int prefixLength, InetNetwork addrV6, int mtu, String dns, String[] searchDomains, LinkedList<Route> routes) throws Exception {
VpnService.Builder builder = vpnService.getBuilder();
builder.addAddress(ip, prefixLength);
if (addrV6 != null) {
builder.addAddress(addrV6.getAddress().getHostAddress(), addrV6.getMask());
Log.d(LOGTAG, "add IPv6 address: " + addrV6.getAddress().getHostAddress() + "/" + addrV6.getMask());
}
builder.allowFamily(OsConstants.AF_INET);
builder.allowFamily(OsConstants.AF_INET6);
builder.setMtu(mtu);
Expand All @@ -69,6 +77,10 @@ private int createTun(String ip, int prefixLength, int mtu, String dns, String[]
Log.d(LOGTAG,"add search domain: "+ sd);
}

if (addrV6 == null && hasIPv4DefaultRoute(routes)) {
routes.add(new Route("::/0"));
}
Comment on lines +80 to +82
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n tool/src/main/java/io/netbird/client/tool/IFace.java | head -90

Repository: netbirdio/android-client

Length of output: 4092


🏁 Script executed:

cat -n tool/src/main/java/io/netbird/client/tool/IFace.java | sed -n '60,180p'

Repository: netbirdio/android-client

Length of output: 5157


🏁 Script executed:

fd -t f "Route\.java" --exec cat {} \;

Repository: netbirdio/android-client

Length of output: 396


Blackhole IPv6 based on missing ::/0 route, not missing IPv6 address.

The presence of an IPv6 interface address does not guarantee a ::/0 default route exists. With allowFamily(AF_INET6) enabled and an IPv4 default route (0.0.0.0/0), non-matching IPv6 traffic can fall through to the underlying network if no ::/0 route is present. Add the blackhole whenever 0.0.0.0/0 exists and no IPv6 /0 route exists, regardless of whether an IPv6 address is configured on the interface.

Proposed fix
-        if (addrV6 == null && hasIPv4DefaultRoute(routes)) {
+        if (hasIPv4DefaultRoute(routes) && !hasIPv6DefaultRoute(routes)) {
             routes.add(new Route("::/0"));
         }
-    // Blackhole IPv6 when the tunnel has an IPv4 default route but no IPv6
-    // address on the interface, to prevent IPv6 leaks around the tunnel.
+    // Blackhole IPv6 when the tunnel has an IPv4 default route but no IPv6
+    // default route, to prevent IPv6 leaks around the tunnel.
     private boolean hasIPv4DefaultRoute(LinkedList<Route> routes) {
         for (Route r : routes) {
             if ("0.0.0.0".equals(r.addr) && r.prefixLength == 0) {
                 return true;
             }
         }
         return false;
     }
+
+    private boolean hasIPv6DefaultRoute(LinkedList<Route> routes) {
+        for (Route r : routes) {
+            if (r.addr != null && r.addr.contains(":") && r.prefixLength == 0) {
+                return true;
+            }
+        }
+        return false;
+    }
🤖 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 80 - 82,
The current logic only adds a blackhole ::/0 when addrV6 == null and
hasIPv4DefaultRoute(routes) is true, but the presence of an IPv6 address is not
the right signal — we must add the ::/0 blackhole whenever there is an IPv4
default route and there is no IPv6 default route. Change the condition in IFace
where routes are built: replace the addrV6 == null check with a check for
absence of an IPv6 default route (e.g., !hasIPv6DefaultRoute(routes) or
!routes.stream().anyMatch(r -> r.prefix.equals("::/0"))), so that if
hasIPv4DefaultRoute(routes) && no IPv6 /0 route then routes.add(new
Route("::/0")). Use or add a helper like hasIPv6DefaultRoute(routes) to mirror
hasIPv4DefaultRoute for clarity; keep Route and routes usage unchanged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In the hasIpv6 case mgmt will send a proper non black hole route as well

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@lixmal, thanks for the clarification! That makes sense — since the management server will always supply a proper IPv6 route when an IPv6 address is assigned to the interface, the addrV6 == null guard is the correct signal for when the blackhole is needed. The existing logic is intentional and correct. My apologies for the noise.


✏️ Learnings added
Learnt from: lixmal
Repo: netbirdio/android-client PR: 148
File: tool/src/main/java/io/netbird/client/tool/IFace.java:80-82
Timestamp: 2026-04-21T11:08:22.712Z
Learning: In `tool/src/main/java/io/netbird/client/tool/IFace.java` (netbirdio/android-client), the blackhole `::/0` route is intentionally added only when `addrV6 == null && hasIPv4DefaultRoute(routes)`. When an IPv6 address IS configured (`addrV6 != null`), the NetBird management server always sends a proper (non-blackhole) IPv6 route, so no blackhole is needed in that case. The `addrV6 == null` check is the correct condition to detect when mgmt won't send an IPv6 route.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


for (Route r : routes) {
builder.addRoute(r.addr, r.prefixLength);
Log.d(LOGTAG, "add route: "+r.addr+"/"+r.prefixLength);
Expand Down Expand Up @@ -149,6 +161,17 @@ private String[] toSearchDomains(String searchDomains) {
return searchDomains.split(";");
}

// Blackhole IPv6 when the tunnel has an IPv4 default route but no IPv6
// address on the interface, to prevent IPv6 leaks around the tunnel.
private boolean hasIPv4DefaultRoute(LinkedList<Route> routes) {
for (Route r : routes) {
if ("0.0.0.0".equals(r.addr) && r.prefixLength == 0) {
return true;
}
}
return false;
}

private LinkedList<Route> toRoutes(String routesString) {
LinkedList<Route> routesList = new LinkedList<>();
if(routesString == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ public void onNetworkChanged(String routes) {

@Override
public void setInterfaceIP(String ip) {
}

@Override
public void setInterfaceIPv6(String ip) {
}

public void addRouteChangeListener(RouteChangeListener routeChangeListener) {
Expand Down
4 changes: 3 additions & 1 deletion tool/src/main/java/io/netbird/client/tool/TUNParameters.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

public class TUNParameters {
String address;
String addressV6;
long mtu;
String dns;
String searchDomainsString;
String routesString;

public TUNParameters(String address, long mtu, String dns, String searchDomainsString, String routesString) {
public TUNParameters(String address, String addressV6, long mtu, String dns, String searchDomainsString, String routesString) {
this.address = address;
this.addressV6 = addressV6;
this.mtu = mtu;
this.dns = dns;
this.searchDomainsString = searchDomainsString;
Expand Down
1 change: 1 addition & 0 deletions tool/src/main/java/io/netbird/client/tool/VPNService.java
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ private void recreateTUN(String routes) {
try {
int fd = (int)iface.configureInterface(
currentTUNParameters.address,
currentTUNParameters.addressV6,
currentTUNParameters.mtu,
currentTUNParameters.dns,
currentTUNParameters.searchDomainsString,
Expand Down
Loading