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 7576330e..c87e3dd7 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 @@ -55,7 +55,7 @@ private List 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; } 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..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.equals("0.0.0.0/0"); + return address != null && (address.contains("0.0.0.0/0") || address.contains("::/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 c25aa711..9bcb7cf0 100644 --- a/app/src/main/res/layout/fragment_advanced.xml +++ b/app/src/main/res/layout/fragment_advanced.xml @@ -520,6 +520,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 7bd292b6..25b8e31e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ Connecting Connected Copy IP + Copy IPv6 Copy FQDN Filter @@ -89,6 +90,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 332c624c..3537e223 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit 332c624c55a486c0ab7b981a26cf8e00a6fa0774 +Subproject commit 3537e2234f065036ea64513e49096ffef52a77fe 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 { 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..137fe8ce 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); @@ -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")); + } + for (Route r : routes) { builder.addRoute(r.addr, r.prefixLength); Log.d(LOGTAG, "add route: "+r.addr+"/"+r.prefixLength); @@ -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 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) { 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 548ae3ce..731f7472 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -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,