diff options
-rw-r--r-- | core/java/android/net/NetworkUtils.java | 81 | ||||
-rw-r--r-- | services/core/java/com/android/server/connectivity/Vpn.java | 80 | ||||
-rw-r--r-- | tests/net/java/android/net/NetworkUtilsTest.java | 128 |
3 files changed, 259 insertions, 30 deletions
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index d045549bd8c7..e6472e9c1d5f 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -16,19 +16,15 @@ package android.net; -import static android.system.OsConstants.AF_INET; -import static android.system.OsConstants.AF_INET6; - -import android.annotation.NonNull; import android.annotation.UnsupportedAppUsage; import android.net.shared.Inet4AddressUtils; import android.os.Build; import android.system.ErrnoException; -import android.system.Os; import android.util.Log; import android.util.Pair; import java.io.FileDescriptor; +import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; @@ -36,6 +32,7 @@ import java.net.SocketException; import java.net.UnknownHostException; import java.util.Collection; import java.util.Locale; +import java.util.TreeSet; /** * Native methods for managing network interfaces. @@ -390,30 +387,72 @@ public class NetworkUtils { return result; } - private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET, AF_INET6}; + /** + * Returns a prefix set without overlaps. + * + * This expects the src set to be sorted from shorter to longer. Results are undefined + * failing this condition. The returned prefix set is sorted in the same order as the + * passed set, with the same comparator. + */ + private static TreeSet<IpPrefix> deduplicatePrefixSet(final TreeSet<IpPrefix> src) { + final TreeSet<IpPrefix> dst = new TreeSet<>(src.comparator()); + // Prefixes match addresses that share their upper part up to their length, therefore + // the only kind of possible overlap in two prefixes is strict inclusion of the longer + // (more restrictive) in the shorter (including equivalence if they have the same + // length). + // Because prefixes in the src set are sorted from shorter to longer, deduplicating + // is done by simply iterating in order, and not adding any longer prefix that is + // already covered by a shorter one. + newPrefixes: + for (IpPrefix newPrefix : src) { + for (IpPrefix existingPrefix : dst) { + if (existingPrefix.containsPrefix(newPrefix)) { + continue newPrefixes; + } + } + dst.add(newPrefix); + } + return dst; + } /** - * Returns true if the hostname is weakly validated. - * @param hostname Name of host to validate. - * @return True if it's a valid-ish hostname. + * Returns how many IPv4 addresses match any of the prefixes in the passed ordered set. * - * @hide + * Obviously this returns an integral value between 0 and 2**32. + * The behavior is undefined if any of the prefixes is not an IPv4 prefix or if the + * set is not ordered smallest prefix to longer prefix. + * + * @param prefixes the set of prefixes, ordered by length */ - public static boolean isWeaklyValidatedHostname(@NonNull String hostname) { - // TODO(b/34953048): Use a validation method that permits more accurate, - // but still inexpensive, checking of likely valid DNS hostnames. - final String weakHostnameRegex = "^[a-zA-Z0-9_.-]+$"; - if (!hostname.matches(weakHostnameRegex)) { - return false; + public static long routedIPv4AddressCount(final TreeSet<IpPrefix> prefixes) { + long routedIPCount = 0; + for (final IpPrefix prefix : deduplicatePrefixSet(prefixes)) { + if (!prefix.isIPv4()) { + Log.wtf(TAG, "Non-IPv4 prefix in routedIPv4AddressCount"); + } + int rank = 32 - prefix.getPrefixLength(); + routedIPCount += 1L << rank; } + return routedIPCount; + } - for (int address_family : ADDRESS_FAMILIES) { - if (Os.inet_pton(address_family, hostname) != null) { - return false; + /** + * Returns how many IPv6 addresses match any of the prefixes in the passed ordered set. + * + * This returns a BigInteger between 0 and 2**128. + * The behavior is undefined if any of the prefixes is not an IPv6 prefix or if the + * set is not ordered smallest prefix to longer prefix. + */ + public static BigInteger routedIPv6AddressCount(final TreeSet<IpPrefix> prefixes) { + BigInteger routedIPCount = BigInteger.ZERO; + for (final IpPrefix prefix : deduplicatePrefixSet(prefixes)) { + if (!prefix.isIPv6()) { + Log.wtf(TAG, "Non-IPv6 prefix in routedIPv6AddressCount"); } + int rank = 128 - prefix.getPrefixLength(); + routedIPCount = routedIPCount.add(BigInteger.ONE.shiftLeft(rank)); } - - return true; + return routedIPCount; } private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET, AF_INET6}; diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 1240a002936d..e7a8b132d850 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -58,6 +58,7 @@ import android.net.NetworkFactory; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; import android.net.NetworkMisc; +import android.net.NetworkUtils; import android.net.RouteInfo; import android.net.UidRange; import android.net.VpnService; @@ -104,6 +105,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; @@ -112,6 +114,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Set; @@ -131,6 +134,31 @@ public class Vpn { // the device idle whitelist during service launch and VPN bootstrap. private static final long VPN_LAUNCH_IDLE_WHITELIST_DURATION_MS = 60 * 1000; + // Settings for how much of the address space should be routed so that Vpn considers + // "most" of the address space is routed. This is used to determine whether this Vpn + // should be marked with the INTERNET capability. + private static final long MOST_IPV4_ADDRESSES_COUNT; + private static final BigInteger MOST_IPV6_ADDRESSES_COUNT; + static { + // 85% of the address space must be routed for Vpn to consider this VPN to provide + // INTERNET access. + final int howManyPercentIsMost = 85; + + final long twoPower32 = 1L << 32; + MOST_IPV4_ADDRESSES_COUNT = twoPower32 * howManyPercentIsMost / 100; + final BigInteger twoPower128 = BigInteger.ONE.shiftLeft(128); + MOST_IPV6_ADDRESSES_COUNT = twoPower128 + .multiply(BigInteger.valueOf(howManyPercentIsMost)) + .divide(BigInteger.valueOf(100)); + } + // How many routes to evaluate before bailing and declaring this Vpn should provide + // the INTERNET capability. This is necessary because computing the address space is + // O(n²) and this is running in the system service, so a limit is needed to alleviate + // the risk of attack. + // This is taken as a total of IPv4 + IPV6 routes for simplicity, but the algorithm + // is actually O(n²)+O(n²). + private static final int MAX_ROUTES_TO_EVALUATE = 150; + // TODO: create separate trackers for each unique VPN to support // automated reconnection @@ -228,7 +256,7 @@ public class Vpn { } /** - * Update current state, dispatching event to listeners. + * Update current state, dispaching event to listeners. */ @VisibleForTesting protected void updateState(DetailedState detailedState, String reason) { @@ -273,7 +301,7 @@ public class Vpn { } @VisibleForTesting - static void applyUnderlyingCapabilities( + public static void applyUnderlyingCapabilities( ConnectivityManager cm, Network[] underlyingNetworks, NetworkCapabilities caps, @@ -387,7 +415,7 @@ public class Vpn { PackageManager pm = mContext.getPackageManager(); ApplicationInfo appInfo = null; try { - appInfo = pm.getApplicationInfoAsUser(packageName, 0 /* flags */, mUserHandle); + appInfo = pm.getApplicationInfoAsUser(packageName, 0 /*flags*/, mUserHandle); } catch (NameNotFoundException unused) { Log.w(TAG, "Can't find \"" + packageName + "\" when checking always-on support"); } @@ -548,7 +576,7 @@ public class Vpn { final String alwaysOnPackage = mSystemServices.settingsSecureGetStringForUser( Settings.Secure.ALWAYS_ON_VPN_APP, mUserHandle); final boolean alwaysOnLockdown = mSystemServices.settingsSecureGetIntForUser( - Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, 0 /* default */, mUserHandle) != 0; + Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, 0 /*default*/, mUserHandle) != 0; final String whitelistString = mSystemServices.settingsSecureGetStringForUser( Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST, mUserHandle); final List<String> whitelistedPackages = TextUtils.isEmpty(whitelistString) @@ -800,7 +828,7 @@ public class Vpn { PackageManager pm = mContext.getPackageManager(); try { ApplicationInfo appInfo = - pm.getApplicationInfoAsUser(packageName, 0 /* flags */, mUserHandle); + pm.getApplicationInfoAsUser(packageName, 0 /*flags*/, mUserHandle); return appInfo.targetSdkVersion >= VERSION_CODES.Q; } catch (NameNotFoundException unused) { Log.w(TAG, "Can't find \"" + packageName + "\""); @@ -874,6 +902,38 @@ public class Vpn { } /** + * Analyzes the passed LinkedProperties to figure out whether it routes to most of the IP space. + * + * This returns true if the passed LinkedProperties contains routes to either most of the IPv4 + * space or to most of the IPv6 address space, where "most" is defined by the value of the + * MOST_IPV{4,6}_ADDRESSES_COUNT constants : if more than this number of addresses are matched + * by any of the routes, then it's decided that most of the space is routed. + * @hide + */ + @VisibleForTesting + static boolean providesRoutesToMostDestinations(LinkProperties lp) { + final List<RouteInfo> routes = lp.getAllRoutes(); + if (routes.size() > MAX_ROUTES_TO_EVALUATE) return true; + final Comparator<IpPrefix> prefixLengthComparator = IpPrefix.lengthComparator(); + TreeSet<IpPrefix> ipv4Prefixes = new TreeSet<>(prefixLengthComparator); + TreeSet<IpPrefix> ipv6Prefixes = new TreeSet<>(prefixLengthComparator); + for (final RouteInfo route : routes) { + if (route.getType() == RouteInfo.RTN_UNREACHABLE) continue; + IpPrefix destination = route.getDestination(); + if (destination.isIPv4()) { + ipv4Prefixes.add(destination); + } else { + ipv6Prefixes.add(destination); + } + } + if (NetworkUtils.routedIPv4AddressCount(ipv4Prefixes) > MOST_IPV4_ADDRESSES_COUNT) { + return true; + } + return NetworkUtils.routedIPv6AddressCount(ipv6Prefixes) + .compareTo(MOST_IPV6_ADDRESSES_COUNT) >= 0; + } + + /** * Attempt to perform a seamless handover of VPNs by only updating LinkProperties without * registering a new NetworkAgent. This is not always possible if the new VPN configuration * has certain changes, in which case this method would just return {@code false}. @@ -1598,8 +1658,8 @@ public class Vpn { */ public PendingIntent pendingIntentGetActivityAsUser( Intent intent, int flags, UserHandle user) { - return PendingIntent.getActivityAsUser(mContext, 0 /* request */, intent, flags, - null /* options */, user); + return PendingIntent.getActivityAsUser(mContext, 0 /*request*/, intent, flags, + null /*options*/, user); } /** @@ -1705,7 +1765,7 @@ public class Vpn { byte[] value = keyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert); serverCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); } - if (userCert == null || caCert == null || serverCert == null) { + if (privateKey == null || userCert == null || caCert == null || serverCert == null) { throw new IllegalStateException("Cannot load credentials"); } @@ -1824,7 +1884,7 @@ public class Vpn { * Return the information of the current ongoing legacy VPN. * Callers are responsible for checking permissions if needed. */ - private synchronized LegacyVpnInfo getLegacyVpnInfoPrivileged() { + public synchronized LegacyVpnInfo getLegacyVpnInfoPrivileged() { if (mLegacyVpnRunner == null) return null; final LegacyVpnInfo info = new LegacyVpnInfo(); @@ -1978,6 +2038,7 @@ public class Vpn { private void bringup() { // Catch all exceptions so we can clean up a few things. + boolean initFinished = false; try { // Initialize the timer. mBringupStartTime = SystemClock.elapsedRealtime(); @@ -1996,6 +2057,7 @@ public class Vpn { throw new IllegalStateException("Cannot delete the state"); } new File("/data/misc/vpn/abort").delete(); + initFinished = true; // Check if we need to restart any of the daemons. boolean restart = false; diff --git a/tests/net/java/android/net/NetworkUtilsTest.java b/tests/net/java/android/net/NetworkUtilsTest.java new file mode 100644 index 000000000000..7748288aeb05 --- /dev/null +++ b/tests/net/java/android/net/NetworkUtilsTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static junit.framework.Assert.assertEquals; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.util.TreeSet; + +@RunWith(AndroidJUnit4.class) +@androidx.test.filters.SmallTest +public class NetworkUtilsTest { + @Test + public void testRoutedIPv4AddressCount() { + final TreeSet<IpPrefix> set = new TreeSet<>(IpPrefix.lengthComparator()); + // No routes routes to no addresses. + assertEquals(0, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("0.0.0.0/0")); + assertEquals(1l << 32, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("20.18.0.0/16")); + set.add(new IpPrefix("20.18.0.0/24")); + set.add(new IpPrefix("20.18.0.0/8")); + // There is a default route, still covers everything + assertEquals(1l << 32, NetworkUtils.routedIPv4AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("20.18.0.0/24")); + set.add(new IpPrefix("20.18.0.0/8")); + // The 8-length includes the 24-length prefix + assertEquals(1l << 24, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("10.10.10.126/25")); + // The 8-length does not include this 25-length prefix + assertEquals((1l << 24) + (1 << 7), NetworkUtils.routedIPv4AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + assertEquals(1l, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("1.2.3.5/32")); + set.add(new IpPrefix("1.2.3.6/32")); + + set.add(new IpPrefix("1.2.3.7/32")); + set.add(new IpPrefix("1.2.3.8/32")); + set.add(new IpPrefix("1.2.3.9/32")); + set.add(new IpPrefix("1.2.3.0/32")); + assertEquals(7l, NetworkUtils.routedIPv4AddressCount(set)); + + // 1.2.3.4/30 eats 1.2.3.{4-7}/32 + set.add(new IpPrefix("1.2.3.4/30")); + set.add(new IpPrefix("6.2.3.4/28")); + set.add(new IpPrefix("120.2.3.4/16")); + assertEquals(7l - 4 + 4 + 16 + 65536, NetworkUtils.routedIPv4AddressCount(set)); + } + + @Test + public void testRoutedIPv6AddressCount() { + final TreeSet<IpPrefix> set = new TreeSet<>(IpPrefix.lengthComparator()); + // No routes routes to no addresses. + assertEquals(BigInteger.ZERO, NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("::/0")); + assertEquals(BigInteger.ONE.shiftLeft(128), NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("1234:622a::18/64")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/96")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/8")); + // There is a default route, still covers everything + assertEquals(BigInteger.ONE.shiftLeft(128), NetworkUtils.routedIPv6AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/96")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/8")); + // The 8-length includes the 96-length prefix + assertEquals(BigInteger.ONE.shiftLeft(120), NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("10::26/64")); + // The 8-length does not include this 64-length prefix + assertEquals(BigInteger.ONE.shiftLeft(120).add(BigInteger.ONE.shiftLeft(64)), + NetworkUtils.routedIPv6AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + assertEquals(BigInteger.ONE, NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad5/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad6/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad7/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad8/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad9/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad0/128")); + assertEquals(BigInteger.valueOf(7), NetworkUtils.routedIPv6AddressCount(set)); + + // add4:f00:80:f7:1111::6ad4/126 eats add4:f00:8[:f7:1111::6ad{4-7}/128 + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/126")); + set.add(new IpPrefix("d00d:f00:80:f7:1111::6ade/124")); + set.add(new IpPrefix("f00b:a33::/112")); + assertEquals(BigInteger.valueOf(7l - 4 + 4 + 16 + 65536), + NetworkUtils.routedIPv6AddressCount(set)); + } +} |