From 3f573e8612a84014319ed3b268be76dbaea03c20 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Sun, 22 Mar 2026 06:07:39 +0100 Subject: [PATCH 1/7] Add IPv6 support --- .../client/ui/advanced/AdvancedFragment.java | 16 +++++- .../java/io/netbird/client/ui/home/Peer.java | 10 +++- .../netbird/client/ui/home/PeersAdapter.java | 12 ++++- .../ui/home/PeersFragmentViewModel.java | 2 +- .../io/netbird/client/ui/home/Resource.java | 2 +- app/src/main/res/layout/fragment_advanced.xml | 51 ++++++++++++++++++- app/src/main/res/menu/peer_clipboard_menu.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ netbird | 2 +- .../java/io/netbird/client/tool/IFace.java | 16 ++++-- .../client/tool/NetworkChangeNotifier.java | 3 ++ .../io/netbird/client/tool/TUNParameters.java | 4 +- .../io/netbird/client/tool/VPNService.java | 1 + 13 files changed, 112 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java b/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java index ed060c57..40ea934d 100644 --- a/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java +++ b/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java @@ -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) -> { @@ -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(); @@ -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); } diff --git a/app/src/main/java/io/netbird/client/ui/home/Peer.java b/app/src/main/java/io/netbird/client/ui/home/Peer.java index 7e876c96..27ca7231 100644 --- a/app/src/main/java/io/netbird/client/ui/home/Peer.java +++ b/app/src/main/java/io/netbird/client/ui/home/Peer.java @@ -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; } @@ -19,7 +21,11 @@ public String getIp() { return ip; } + public String getIpv6() { + return ipv6; + } + public String getFqdn() { return fqdn; } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/netbird/client/ui/home/PeersAdapter.java b/app/src/main/java/io/netbird/client/ui/home/PeersAdapter.java index 438047c2..deb44998 100644 --- a/app/src/main/java/io/netbird/client/ui/home/PeersAdapter.java +++ b/app/src/main/java/io/netbird/client/ui/home/PeersAdapter.java @@ -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) { @@ -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; }); @@ -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) { diff --git a/app/src/main/java/io/netbird/client/ui/home/PeersFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/home/PeersFragmentViewModel.java index 4175c901..fe0c0075 100644 --- a/app/src/main/java/io/netbird/client/ui/home/PeersFragmentViewModel.java +++ b/app/src/main/java/io/netbird/client/ui/home/PeersFragmentViewModel.java @@ -61,7 +61,7 @@ private List getPeers(PeerInfoArray peersInfo) { } status = Status.fromString(connStatus); - peers.add(new Peer(status, peerInfo.getIP(), peerInfo.getFQDN())); + peers.add(new Peer(status, peerInfo.getIP(), peerInfo.getIPv6(), peerInfo.getFQDN())); } return peers; } diff --git a/app/src/main/java/io/netbird/client/ui/home/Resource.java b/app/src/main/java/io/netbird/client/ui/home/Resource.java index 813904fe..1a352469 100644 --- a/app/src/main/java/io/netbird/client/ui/home/Resource.java +++ b/app/src/main/java/io/netbird/client/ui/home/Resource.java @@ -36,7 +36,7 @@ public Status getStatus() { } public boolean isExitNode() { - return address.equals("0.0.0.0/0"); + return address.contains("0.0.0.0/0") || address.equals("::/0"); } public boolean isSelected() { diff --git a/app/src/main/res/layout/fragment_advanced.xml b/app/src/main/res/layout/fragment_advanced.xml index 9c74605d..b82c4f39 100644 --- a/app/src/main/res/layout/fragment_advanced.xml +++ b/app/src/main/res/layout/fragment_advanced.xml @@ -519,6 +519,55 @@ + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/layout_disable_ipv6" /> + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 62e73474..7e88ad84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,6 +39,7 @@ Connecting Connected Copy IP + Copy IPv6 Copy FQDN Filter @@ -88,6 +89,8 @@ Disables NetBird DNS configuration on this device Disable firewall Disables the NetBird firewall + Disable IPv6 + Disables IPv6 overlay addressing on the tunnel interface Enable SSH Allows SSH connections to this device Block inbound connections diff --git a/netbird b/netbird index 67211010..a457faa8 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit 67211010f7240d53734abd922777c32fccb02754 +Subproject commit a457faa876262141b3d1e9da43085f5a00020e6a diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java index 918d2acb..54423668 100644 --- a/tool/src/main/java/io/netbird/client/tool/IFace.java +++ b/tool/src/main/java/io/netbird/client/tool/IFace.java @@ -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 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; @@ -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 routes) throws Exception { + private int createTun(String ip, int prefixLength, InetNetwork addrV6, int mtu, String dns, String[] searchDomains, LinkedList 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); diff --git a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java index f19c4221..e250092c 100644 --- a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java +++ b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java @@ -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) { diff --git a/tool/src/main/java/io/netbird/client/tool/TUNParameters.java b/tool/src/main/java/io/netbird/client/tool/TUNParameters.java index e5d72578..6ba8202e 100644 --- a/tool/src/main/java/io/netbird/client/tool/TUNParameters.java +++ b/tool/src/main/java/io/netbird/client/tool/TUNParameters.java @@ -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; diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index c24ea0d4..8a52420d 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -300,6 +300,7 @@ private void recreateTUN(String routes) { try { int fd = (int)iface.configureInterface( currentTUNParameters.address, + currentTUNParameters.addressV6, currentTUNParameters.mtu, currentTUNParameters.dns, currentTUNParameters.searchDomainsString, From 297b61860959c5ad5ca194bbf860e10a88d62e6e Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Mon, 30 Mar 2026 19:53:23 +0200 Subject: [PATCH 2/7] Fix isExitNode null safety and IPv6 detection Use contains() for IPv6 check to match IPv4 logic, so multi-address strings like "0.0.0.0/0,::/0" are handled correctly. Add null guard for address field. --- app/src/main/java/io/netbird/client/ui/home/Resource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/netbird/client/ui/home/Resource.java b/app/src/main/java/io/netbird/client/ui/home/Resource.java index 1a352469..3a4e3217 100644 --- a/app/src/main/java/io/netbird/client/ui/home/Resource.java +++ b/app/src/main/java/io/netbird/client/ui/home/Resource.java @@ -36,7 +36,7 @@ public Status getStatus() { } public boolean isExitNode() { - return address.contains("0.0.0.0/0") || address.equals("::/0"); + return address != null && (address.contains("0.0.0.0/0") || address.contains("::/0")); } public boolean isSelected() { From 090b6a016f07a6d99eb975f2ffacbbe25c379596 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Thu, 9 Apr 2026 11:08:49 +0200 Subject: [PATCH 3/7] Update netbird submodule to latest proto-ipv6-overlay --- netbird | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbird b/netbird index a457faa8..1c4e5e71 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit a457faa876262141b3d1e9da43085f5a00020e6a +Subproject commit 1c4e5e71d7027815b67b1539705a86a4cf217b7a From bce03cbea8635e638a762dbe1f2c07512d4474d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Wed, 15 Apr 2026 12:55:39 +0200 Subject: [PATCH 4/7] Bump submodule version --- netbird | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbird b/netbird index 1c4e5e71..f86ecea2 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit 1c4e5e71d7027815b67b1539705a86a4cf217b7a +Subproject commit f86ecea26fc605b896d01c397ef8aee6f4d5ff96 From 746510429cf8450c41819aade432ee5da7201b4c Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Sat, 18 Apr 2026 12:28:53 +0200 Subject: [PATCH 5/7] Add IPv6 blackhole when interface lacks IPv6 and IPv4 default present --- netbird | 2 +- .../main/java/io/netbird/client/tool/IFace.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/netbird b/netbird index f86ecea2..02c0b20f 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit f86ecea26fc605b896d01c397ef8aee6f4d5ff96 +Subproject commit 02c0b20f21e187184eb201ac028a9471ead3b73f diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java index 54423668..137fe8ce 100644 --- a/tool/src/main/java/io/netbird/client/tool/IFace.java +++ b/tool/src/main/java/io/netbird/client/tool/IFace.java @@ -77,6 +77,10 @@ private int createTun(String ip, int prefixLength, InetNetwork addrV6, int mtu, Log.d(LOGTAG,"add search domain: "+ sd); } + if (addrV6 == null && hasIPv4DefaultRoute(routes)) { + routes.add(new Route("::/0")); + } + for (Route r : routes) { builder.addRoute(r.addr, r.prefixLength); Log.d(LOGTAG, "add route: "+r.addr+"/"+r.prefixLength); @@ -157,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 routes) { + for (Route r : routes) { + if ("0.0.0.0".equals(r.addr) && r.prefixLength == 0) { + return true; + } + } + return false; + } + private LinkedList toRoutes(String routesString) { LinkedList routesList = new LinkedList<>(); if(routesString == null) { From 70eb8be0918bc28f5b33e3f3900650afec5158e4 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 21 Apr 2026 10:36:32 +0200 Subject: [PATCH 6/7] Bump submodule to latest proto-ipv6-overlay --- netbird | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbird b/netbird index 02c0b20f..3537e223 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit 02c0b20f21e187184eb201ac028a9471ead3b73f +Subproject commit 3537e2234f065036ea64513e49096ffef52a77fe From 8f3adecae79d9eb521005cbc235645268458286a Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 21 Apr 2026 10:55:27 +0200 Subject: [PATCH 7/7] Implement cacheDir() in AndroidPlatformFiles New PlatformFiles.CacheDir() method in the netbird submodule. Pass context.getCacheDir() through from EngineRunner. --- .../io/netbird/client/tool/AndroidPlatformFiles.java | 9 ++++++++- .../main/java/io/netbird/client/tool/EngineRunner.java | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/AndroidPlatformFiles.java b/tool/src/main/java/io/netbird/client/tool/AndroidPlatformFiles.java index cbac637b..b65e2440 100644 --- a/tool/src/main/java/io/netbird/client/tool/AndroidPlatformFiles.java +++ b/tool/src/main/java/io/netbird/client/tool/AndroidPlatformFiles.java @@ -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 @@ -20,4 +22,9 @@ public String configurationFilePath() { public String stateFilePath() { return stateFilePath; } + + @Override + public String cacheDir() { + return cacheDir; + } } diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index 703cbd4d..fb1fd4cc 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -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 {