From e3c6001c9d0679f3c0b5231c84aa4e92377ec8cc Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Mon, 16 Jul 2018 17:15:48 +0200 Subject: Implement exclude routes mechanism for OpenVPN 3 core and for IPv6 closes #902 --- .../main/java/de/blinkt/openvpn/core/CIDRIP.java | 8 ++- .../java/de/blinkt/openvpn/core/NetworkSpace.java | 69 ++++++++++---------- .../java/de/blinkt/openvpn/core/NetworkUtils.java | 75 ++++++++++++++++++++++ .../de/blinkt/openvpn/core/OpenVPNService.java | 65 +++++++++---------- .../openvpn/core/OpenVpnManagementThread.java | 3 +- .../de/blinkt/openvpn/core/OpenVPNThreadv3.java | 46 +++++++++---- .../java/de/blinkt/openvpn/core/TestIpParser.java | 2 +- 7 files changed, 185 insertions(+), 83 deletions(-) create mode 100644 main/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java (limited to 'main/src') diff --git a/main/src/main/java/de/blinkt/openvpn/core/CIDRIP.java b/main/src/main/java/de/blinkt/openvpn/core/CIDRIP.java index 799c68c9..ca3d1161 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/CIDRIP.java +++ b/main/src/main/java/de/blinkt/openvpn/core/CIDRIP.java @@ -14,6 +14,11 @@ class CIDRIP { public CIDRIP(String ip, String mask) { mIp = ip; + len = calculateLenFromMask(mask); + + } + + public static int calculateLenFromMask(String mask) { long netmask = getInt(mask); // Add 33. bit to ensure the loop terminates @@ -24,6 +29,7 @@ class CIDRIP { lenZeros++; netmask = netmask >> 1; } + int len; // Check if rest of netmask is only 1s if (netmask != (0x1ffffffffl >> lenZeros)) { // Asume no CIDR, set /32 @@ -31,7 +37,7 @@ class CIDRIP { } else { len = 32 - lenZeros; } - + return len; } public CIDRIP(String address, int prefix_length) { diff --git a/main/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java b/main/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java index 9ed49689..05fdff78 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java +++ b/main/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java @@ -7,7 +7,6 @@ package de.blinkt.openvpn.core; import android.os.Build; import android.support.annotation.NonNull; -import android.text.TextUtils; import java.math.BigInteger; import java.net.Inet6Address; @@ -29,7 +28,7 @@ public class NetworkSpace { throw new IllegalStateException(); } - static class ipAddress implements Comparable { + static class IpAddress implements Comparable { private BigInteger netAddress; public int networkMask; private boolean included; @@ -44,7 +43,7 @@ public class NetworkSpace { * 2. smaller networks are returned as smaller */ @Override - public int compareTo(@NonNull ipAddress another) { + public int compareTo(@NonNull IpAddress another) { int comp = getFirstAddress().compareTo(another.getFirstAddress()); if (comp != 0) return comp; @@ -65,22 +64,22 @@ public class NetworkSpace { */ @Override public boolean equals(Object o) { - if (!(o instanceof ipAddress)) + if (!(o instanceof IpAddress)) return super.equals(o); - ipAddress on = (ipAddress) o; + IpAddress on = (IpAddress) o; return (networkMask == on.networkMask) && on.getFirstAddress().equals(getFirstAddress()); } - public ipAddress(CIDRIP ip, boolean include) { + public IpAddress(CIDRIP ip, boolean include) { included = include; netAddress = BigInteger.valueOf(ip.getInt()); networkMask = ip.len; isV4 = true; } - public ipAddress(Inet6Address address, int mask, boolean include) { + public IpAddress(Inet6Address address, int mask, boolean include) { networkMask = mask; included = include; @@ -136,7 +135,7 @@ public class NetworkSpace { return String.format(Locale.US, "%s/%d", getIPv6Address(), networkMask); } - ipAddress(BigInteger baseAddress, int mask, boolean included, boolean isV4) { + IpAddress(BigInteger baseAddress, int mask, boolean included, boolean isV4) { this.netAddress = baseAddress; this.networkMask = mask; this.included = included; @@ -144,12 +143,12 @@ public class NetworkSpace { } - public ipAddress[] split() { - ipAddress firstHalf = new ipAddress(getFirstAddress(), networkMask + 1, included, isV4); - ipAddress secondHalf = new ipAddress(firstHalf.getLastAddress().add(BigInteger.ONE), networkMask + 1, included, isV4); + public IpAddress[] split() { + IpAddress firstHalf = new IpAddress(getFirstAddress(), networkMask + 1, included, isV4); + IpAddress secondHalf = new IpAddress(firstHalf.getLastAddress().add(BigInteger.ONE), networkMask + 1, included, isV4); if (BuildConfig.DEBUG) assertTrue(secondHalf.getLastAddress().equals(getLastAddress())); - return new ipAddress[]{firstHalf, secondHalf}; + return new IpAddress[]{firstHalf, secondHalf}; } String getIPv4Address() { @@ -192,7 +191,7 @@ public class NetworkSpace { return ipv6str; } - public boolean containsNet(ipAddress network) { + public boolean containsNet(IpAddress network) { // this.first >= net.first && this.last <= net.last BigInteger ourFirst = getFirstAddress(); BigInteger ourLast = getLastAddress(); @@ -207,12 +206,12 @@ public class NetworkSpace { } - TreeSet mIpAddresses = new TreeSet(); + TreeSet mIpAddresses = new TreeSet(); - public Collection getNetworks(boolean included) { - Vector ips = new Vector(); - for (ipAddress ip : mIpAddresses) { + public Collection getNetworks(boolean included) { + Vector ips = new Vector(); + for (IpAddress ip : mIpAddresses) { if (ip.included == included) ips.add(ip); } @@ -226,33 +225,33 @@ public class NetworkSpace { void addIP(CIDRIP cidrIp, boolean include) { - mIpAddresses.add(new ipAddress(cidrIp, include)); + mIpAddresses.add(new IpAddress(cidrIp, include)); } public void addIPSplit(CIDRIP cidrIp, boolean include) { - ipAddress newIP = new ipAddress(cidrIp, include); - ipAddress[] splitIps = newIP.split(); - for (ipAddress split : splitIps) + IpAddress newIP = new IpAddress(cidrIp, include); + IpAddress[] splitIps = newIP.split(); + for (IpAddress split : splitIps) mIpAddresses.add(split); } void addIPv6(Inet6Address address, int mask, boolean included) { - mIpAddresses.add(new ipAddress(address, mask, included)); + mIpAddresses.add(new IpAddress(address, mask, included)); } - TreeSet generateIPList() { + TreeSet generateIPList() { - PriorityQueue networks = new PriorityQueue(mIpAddresses); + PriorityQueue networks = new PriorityQueue(mIpAddresses); - TreeSet ipsDone = new TreeSet(); + TreeSet ipsDone = new TreeSet(); - ipAddress currentNet = networks.poll(); + IpAddress currentNet = networks.poll(); if (currentNet == null) return ipsDone; while (currentNet != null) { // Check if it and the next of it are compatible - ipAddress nextNet = networks.poll(); + IpAddress nextNet = networks.poll(); if (BuildConfig.DEBUG) assertTrue(currentNet!=null); if (nextNet == null || currentNet.getLastAddress().compareTo(nextNet.getFirstAddress()) == -1) { @@ -269,7 +268,7 @@ public class NetworkSpace { currentNet = nextNet; } else { // our currentNet is included in next and types differ. Need to split the next network - ipAddress[] newNets = nextNet.split(); + IpAddress[] newNets = nextNet.split(); // TODO: The contains method of the Priority is stupid linear search @@ -302,7 +301,7 @@ public class NetworkSpace { // simply ignore the next and move on } else { // We need to split our network - ipAddress[] newNets = currentNet.split(); + IpAddress[] newNets = currentNet.split(); if (newNets[1].networkMask == nextNet.networkMask) { @@ -328,11 +327,11 @@ public class NetworkSpace { return ipsDone; } - Collection getPositiveIPList() { - TreeSet ipsSorted = generateIPList(); + Collection getPositiveIPList() { + TreeSet ipsSorted = generateIPList(); - Vector ips = new Vector(); - for (ipAddress ia : ipsSorted) { + Vector ips = new Vector(); + for (IpAddress ia : ipsSorted) { if (ia.included) ips.add(ia); } @@ -340,7 +339,7 @@ public class NetworkSpace { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { // Include postive routes from the original set under < 4.4 since these might overrule the local // network but only if no smaller negative route exists - for (ipAddress origIp : mIpAddresses) { + for (IpAddress origIp : mIpAddresses) { if (!origIp.included) continue; @@ -351,7 +350,7 @@ public class NetworkSpace { boolean skipIp = false; // If there is any smaller net that is excluded we may not add the positive route back - for (ipAddress calculatedIp : ipsSorted) { + for (IpAddress calculatedIp : ipsSorted) { if (!calculatedIp.included && origIp.containsNet(calculatedIp)) { skipIp = true; break; diff --git a/main/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java b/main/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java new file mode 100644 index 00000000..40449118 --- /dev/null +++ b/main/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.Context; +import android.net.*; +import android.os.Build; +import android.text.TextUtils; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.util.Vector; + +public class NetworkUtils { + + public static Vector getLocalNetworks(Context c, boolean ipv6) { + Vector nets = new Vector<>(); + ConnectivityManager conn = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Network[] networks = conn.getAllNetworks(); + for (Network network : networks) { + NetworkInfo ni = conn.getNetworkInfo(network); + LinkProperties li = conn.getLinkProperties(network); + + NetworkCapabilities nc = conn.getNetworkCapabilities(network); + + // Skip VPN networks like ourselves + if (nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) + continue; + + // Also skip mobile networks + if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) + continue; + + + for (LinkAddress la : li.getLinkAddresses()) { + if ((la.getAddress() instanceof Inet4Address && !ipv6) || + (la.getAddress() instanceof Inet6Address && ipv6)) + nets.add(la.toString()); + } + } + } else { + // Old Android Version, use native utils via ifconfig instead + // Add local network interfaces + if (ipv6) + return nets; + + String[] localRoutes = NativeUtils.getIfconfig(); + + // The format of mLocalRoutes is kind of broken because I don't really like JNI + for (int i = 0; i < localRoutes.length; i += 3) { + String intf = localRoutes[i]; + String ipAddr = localRoutes[i + 1]; + String netMask = localRoutes[i + 2]; + + if (intf == null || intf.equals("lo") || + intf.startsWith("tun") || intf.startsWith("rmnet")) + continue; + + if (ipAddr == null || netMask == null) { + VpnStatus.logError("Local routes are broken?! (Report to author) " + TextUtils.join("|", localRoutes)); + continue; + } + nets.add(ipAddr + "/" + CIDRIP.calculateLenFromMask(netMask)); + + } + + } + return nets; + } + +} \ No newline at end of file diff --git a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java index 3a89a2cd..8ee8b8a6 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -58,7 +58,7 @@ import de.blinkt.openvpn.core.VpnStatus.StateListener; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; -import static de.blinkt.openvpn.core.NetworkSpace.ipAddress; +import static de.blinkt.openvpn.core.NetworkSpace.IpAddress; public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener, IOpenVPNServiceInternal { public static final String START_SERVICE = "de.blinkt.openvpn.START_SERVICE"; @@ -590,7 +590,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac // start a Thread that handles incoming messages of the managment socket OpenVpnManagementThread ovpnManagementThread = new OpenVpnManagementThread(mProfile, this); if (ovpnManagementThread.openManagementInterface(this)) { - Thread mSocketManagerThread = new Thread(ovpnManagementThread, "OpenVPNManagementThread"); mSocketManagerThread.start(); mManagement = ovpnManagementThread; @@ -736,7 +735,9 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } if (mLocalIP != null) { - addLocalNetworksToRoutes(); + // OpenVPN3 manages excluded local networks by callback + if (!VpnProfile.doUseOpenVPN3(this)) + addLocalNetworksToRoutes(); try { builder.addAddress(mLocalIP.mIp, mLocalIP.len); } catch (IllegalArgumentException iae) { @@ -775,15 +776,15 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac builder.setMtu(mMtu); } - Collection positiveIPv4Routes = mRoutes.getPositiveIPList(); - Collection positiveIPv6Routes = mRoutesv6.getPositiveIPList(); + Collection positiveIPv4Routes = mRoutes.getPositiveIPList(); + Collection positiveIPv6Routes = mRoutesv6.getPositiveIPList(); if ("samsung".equals(Build.BRAND) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mDnslist.size() >= 1) { // Check if the first DNS Server is in the VPN range try { - ipAddress dnsServer = new ipAddress(new CIDRIP(mDnslist.get(0), 32), true); + IpAddress dnsServer = new IpAddress(new CIDRIP(mDnslist.get(0), 32), true); boolean dnsIncluded = false; - for (ipAddress net : positiveIPv4Routes) { + for (IpAddress net : positiveIPv4Routes) { if (net.containsNet(dnsServer)) { dnsIncluded = true; } @@ -800,9 +801,9 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } } - ipAddress multicastRange = new ipAddress(new CIDRIP("224.0.0.0", 3), true); + IpAddress multicastRange = new IpAddress(new CIDRIP("224.0.0.0", 3), true); - for (NetworkSpace.ipAddress route : positiveIPv4Routes) { + for (IpAddress route : positiveIPv4Routes) { try { if (multicastRange.containsNet(route)) @@ -814,7 +815,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } } - for (NetworkSpace.ipAddress route6 : positiveIPv6Routes) { + for (IpAddress route6 : positiveIPv6Routes) { try { builder.addRoute(route6.getIPv6Address(), route6.networkMask); } catch (IllegalArgumentException ia) { @@ -899,25 +900,11 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } private void addLocalNetworksToRoutes() { - - // Add local network interfaces - String[] localRoutes = NativeUtils.getIfconfig(); - - // The format of mLocalRoutes is kind of broken because I don't really like JNI - for (int i = 0; i < localRoutes.length; i += 3) { - String intf = localRoutes[i]; - String ipAddr = localRoutes[i + 1]; - String netMask = localRoutes[i + 2]; - - if (intf == null || intf.equals("lo") || - intf.startsWith("tun") || intf.startsWith("rmnet")) - continue; - - if (ipAddr == null || netMask == null) { - VpnStatus.logError("Local routes are broken?! (Report to author) " + TextUtils.join("|", localRoutes)); - continue; - } - + for (String net: NetworkUtils.getLocalNetworks(this, false)) + { + String[] netparts = net.split("/"); + String ipAddr = netparts[0]; + int netMask = Integer.parseInt(netparts[1]); if (ipAddr.equals(mLocalIP.mIp)) continue; @@ -927,6 +914,15 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mProfile.mAllowLocalLAN) mRoutes.addIP(new CIDRIP(ipAddr, netMask), false); } + + // IPv6 is Lollipop+ only so we can skip the lower than KITKAT case + if (mProfile.mAllowLocalLAN) { + for (String net : NetworkUtils.getLocalNetworks(this, true)) { + addRoutev6(net, false);; + } + } + + } @@ -1006,13 +1002,13 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac CIDRIP route = new CIDRIP(dest, mask); boolean include = isAndroidTunDevice(device); - NetworkSpace.ipAddress gatewayIP = new NetworkSpace.ipAddress(new CIDRIP(gateway, 32), false); + IpAddress gatewayIP = new IpAddress(new CIDRIP(gateway, 32), false); if (mLocalIP == null) { VpnStatus.logError("Local IP address unset and received. Neither pushed server config nor local config specifies an IP addresses. Opening tun device is most likely going to fail."); return; } - NetworkSpace.ipAddress localNet = new NetworkSpace.ipAddress(mLocalIP, true); + IpAddress localNet = new IpAddress(mLocalIP, true); if (localNet.containsNet(gatewayIP)) include = true; @@ -1032,10 +1028,13 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } public void addRoutev6(String network, String device) { - String[] v6parts = network.split("/"); + // Tun is opened after ROUTE6, no device name may be present boolean included = isAndroidTunDevice(device); + addRoutev6(network, included); + } - // Tun is opened after ROUTE6, no device name may be present + public void addRoutev6(String network, boolean included) { + String[] v6parts = network.split("/"); try { Inet6Address ip = (Inet6Address) InetAddress.getAllByName(v6parts[0])[0]; diff --git a/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java b/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java index 5107ac88..5238880b 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java +++ b/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java @@ -262,7 +262,7 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { private void fdCloseLollipop(FileDescriptor fd) { try { Os.close(fd); - } catch (ErrnoException e) { + } catch (Exception e) { VpnStatus.logException("Failed to close fd (" + fd + ")", e); } } @@ -285,7 +285,6 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { private void processCommand(String command) { //Log.i(TAG, "Line from managment" + command); - if (command.startsWith(">") && command.contains(":")) { String[] parts = command.split(":", 2); String cmd = parts[0].substring(1); diff --git a/main/src/ovpn3/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java b/main/src/ovpn3/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java index 08c84558..3e7011e7 100644 --- a/main/src/ovpn3/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java +++ b/main/src/ovpn3/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java @@ -1,22 +1,30 @@ package de.blinkt.openvpn.core; +import android.net.*; +import android.os.Build; import de.blinkt.openvpn.R; -import net.openvpn.ovpn3.ClientAPI_Config; -import net.openvpn.ovpn3.ClientAPI_EvalConfig; -import net.openvpn.ovpn3.ClientAPI_Event; -import net.openvpn.ovpn3.ClientAPI_ExternalPKICertRequest; -import net.openvpn.ovpn3.ClientAPI_ExternalPKISignRequest; -import net.openvpn.ovpn3.ClientAPI_LogInfo; -import net.openvpn.ovpn3.ClientAPI_OpenVPNClient; -import net.openvpn.ovpn3.ClientAPI_ProvideCreds; -import net.openvpn.ovpn3.ClientAPI_Status; -import net.openvpn.ovpn3.ClientAPI_TransportStats; +import net.openvpn.ovpn3.*; import java.lang.Override; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.util.Locale; +import java.util.Vector; import de.blinkt.openvpn.VpnProfile; import android.content.Context; +import net.openvpn.ovpn3.*; +import net.openvpn.ovpn3.*; +import net.openvpn.ovpn3.*; +import net.openvpn.ovpn3.*; +import net.openvpn.ovpn3.*; +import net.openvpn.ovpn3.*; +import net.openvpn.ovpn3.*; +import net.openvpn.ovpn3.ClientAPI_Config; +import net.openvpn.ovpn3.ClientAPI_EvalConfig; +import net.openvpn.ovpn3.ClientAPI_Event; +import net.openvpn.ovpn3.ClientAPI_ExternalPKICertRequest; public class OpenVPNThreadv3 extends ClientAPI_OpenVPNClient implements Runnable, OpenVPNManagement { @@ -165,8 +173,12 @@ public class OpenVPNThreadv3 extends ClientAPI_OpenVPNClient implements Runnable } - @Override + final static long EmulateExcludeRoutes = (1 << 16); + + @Override public boolean tun_builder_reroute_gw(boolean ipv4, boolean ipv6, long flags) { + if ((flags & EmulateExcludeRoutes) != 0) + return true; if (ipv4) mService.addRoute("0.0.0.0", "0.0.0.0", "127.0.0.1", OpenVPNService.VPNSERVICE_TUN); @@ -191,6 +203,7 @@ public class OpenVPNThreadv3 extends ClientAPI_OpenVPNClient implements Runnable config.setExternalPkiAlias("extpki"); config.setCompressionMode("yes"); config.setInfo(true); + config.setAllowLocalLanAccess(mVp.mAllowLocalLAN); ClientAPI_EvalConfig ec = eval_config(config); if(ec.getExternalPki()) { @@ -205,6 +218,7 @@ public class OpenVPNThreadv3 extends ClientAPI_OpenVPNClient implements Runnable } } + @Override public void external_pki_cert_request(ClientAPI_ExternalPKICertRequest certreq) { VpnStatus.logDebug("Got external PKI certificate request from OpenVPN core"); @@ -295,6 +309,16 @@ public class OpenVPNThreadv3 extends ClientAPI_OpenVPNClient implements Runnable VpnStatus.logError(String.format("EVENT(Error): %s: %s", name, info)); } + @Override + public net.openvpn.ovpn3.ClientAPI_StringVec tun_builder_get_local_networks(boolean ipv6) + { + + net.openvpn.ovpn3.ClientAPI_StringVec nets = new net.openvpn.ovpn3.ClientAPI_StringVec(); + for (String net: NetworkUtils.getLocalNetworks(mService, ipv6)) + nets.add(net); + return nets; + } + // When a connection is close to timeout, the core will call this // method. If it returns false, the core will disconnect with a diff --git a/main/src/test/java/de/blinkt/openvpn/core/TestIpParser.java b/main/src/test/java/de/blinkt/openvpn/core/TestIpParser.java index 37f9fdcd..6b330b63 100644 --- a/main/src/test/java/de/blinkt/openvpn/core/TestIpParser.java +++ b/main/src/test/java/de/blinkt/openvpn/core/TestIpParser.java @@ -31,7 +31,7 @@ public class TestIpParser { void testAddress(String input, int mask, String output) throws UnknownHostException { Inet6Address ip = (Inet6Address) InetAddress.getByName(input); - NetworkSpace.ipAddress netIp = new NetworkSpace.ipAddress(ip, mask, true); + NetworkSpace.IpAddress netIp = new NetworkSpace.IpAddress(ip, mask, true); Assert.assertEquals(output, netIp.toString()); } -- cgit v1.2.3