Only apply VPN isolation if it's fully routed

VPN is considered fully routed if both IPv4 and IPv6 have
either a default route or a prohibit route.

Bug: 145332510
Test: atest FrameworksNetTests
Change-Id: I59cf48552bca98092d1212e3d718fd420add5458
diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java
index 7ff954b..651494d 100644
--- a/core/java/android/net/LinkProperties.java
+++ b/core/java/android/net/LinkProperties.java
@@ -1074,6 +1074,21 @@
     }
 
     /**
+     * Returns true if this link has an IPv4 unreachable default route.
+     *
+     * @return {@code true} if there is an IPv4 unreachable default route, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIpv4UnreachableDefaultRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.isIPv4UnreachableDefault()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * For backward compatibility.
      * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
      * just yet.
@@ -1102,6 +1117,21 @@
     }
 
     /**
+     * Returns true if this link has an IPv6 unreachable default route.
+     *
+     * @return {@code true} if there is an IPv6 unreachable default route, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIpv6UnreachableDefaultRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.isIPv6UnreachableDefault()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * For backward compatibility.
      * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
      * just yet.
diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java
index dbdaa4c..e550f85 100644
--- a/core/java/android/net/RouteInfo.java
+++ b/core/java/android/net/RouteInfo.java
@@ -426,6 +426,16 @@
     }
 
     /**
+     * Indicates if this route is an unreachable default route.
+     *
+     * @return {@code true} if it's an unreachable route with prefix length of 0.
+     * @hide
+     */
+    private boolean isUnreachableDefaultRoute() {
+        return mType == RTN_UNREACHABLE && mDestination.getPrefixLength() == 0;
+    }
+
+    /**
      * Indicates if this route is an IPv4 default route.
      * @hide
      */
@@ -434,6 +444,14 @@
     }
 
     /**
+     * Indicates if this route is an IPv4 unreachable default route.
+     * @hide
+     */
+    public boolean isIPv4UnreachableDefault() {
+        return isUnreachableDefaultRoute() && mDestination.getAddress() instanceof Inet4Address;
+    }
+
+    /**
      * Indicates if this route is an IPv6 default route.
      * @hide
      */
@@ -442,6 +460,14 @@
     }
 
     /**
+     * Indicates if this route is an IPv6 unreachable default route.
+     * @hide
+     */
+    public boolean isIPv6UnreachableDefault() {
+        return isUnreachableDefaultRoute() && mDestination.getAddress() instanceof Inet6Address;
+    }
+
+    /**
      * Indicates if this route is a host route (ie, matches only a single host address).
      *
      * @return {@code true} if the destination has a prefix length of 32 or 128 for IPv4 or IPv6,
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 1484fe7..db0ea44 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -6304,7 +6304,8 @@
                 && !nai.networkAgentConfig.allowBypass
                 && nc.getOwnerUid() != Process.SYSTEM_UID
                 && lp.getInterfaceName() != null
-                && (lp.hasIPv4DefaultRoute() || lp.hasIPv6DefaultRoute());
+                && (lp.hasIPv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
+                && (lp.hasIPv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute());
     }
 
     private void updateUids(NetworkAgentInfo nai, NetworkCapabilities prevNc,
diff --git a/tests/net/common/java/android/net/LinkPropertiesTest.java b/tests/net/common/java/android/net/LinkPropertiesTest.java
index 8de27e8..0fc9be3 100644
--- a/tests/net/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/net/common/java/android/net/LinkPropertiesTest.java
@@ -16,6 +16,8 @@
 
 package android.net;
 
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+
 import static com.android.testutils.ParcelUtilsKt.assertParcelSane;
 import static com.android.testutils.ParcelUtilsKt.assertParcelingIsLossless;
 import static com.android.testutils.ParcelUtilsKt.parcelingRoundTrip;
@@ -46,6 +48,7 @@
 import org.junit.runner.RunWith;
 
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -1257,4 +1260,26 @@
         final LinkProperties Ipv6 = makeIpv6LinkProperties();
         assertTrue(Ipv6.hasIpv6DnsServer());
     }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testHasIpv4UnreachableDefaultRoute() {
+        final LinkProperties lp = makeTestObject();
+        assertFalse(lp.hasIpv4UnreachableDefaultRoute());
+        assertFalse(lp.hasIpv6UnreachableDefaultRoute());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE));
+        assertTrue(lp.hasIpv4UnreachableDefaultRoute());
+        assertFalse(lp.hasIpv6UnreachableDefaultRoute());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testHasIpv6UnreachableDefaultRoute() {
+        final LinkProperties lp = makeTestObject();
+        assertFalse(lp.hasIpv6UnreachableDefaultRoute());
+        assertFalse(lp.hasIpv4UnreachableDefaultRoute());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
+        assertTrue(lp.hasIpv6UnreachableDefaultRoute());
+        assertFalse(lp.hasIpv4UnreachableDefaultRoute());
+    }
 }
diff --git a/tests/net/common/java/android/net/RouteInfoTest.java b/tests/net/common/java/android/net/RouteInfoTest.java
index 1658262..8204b49 100644
--- a/tests/net/common/java/android/net/RouteInfoTest.java
+++ b/tests/net/common/java/android/net/RouteInfoTest.java
@@ -31,6 +31,7 @@
 
 import android.os.Build;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -62,6 +63,11 @@
         return new IpPrefix(prefix);
     }
 
+    private static boolean isAtLeastR() {
+        // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R)
+        return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR();
+    }
+
     @Test
     public void testConstructor() {
         RouteInfo r;
@@ -195,78 +201,130 @@
         assertTrue(r.isDefaultRoute());
         assertTrue(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("::/0"), Address("::"), "wlan0");
         assertFalse(r.isHostRoute());
         assertTrue(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertTrue(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0");
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("2001:db8::/48"), null, "wlan0");
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("192.0.2.0/32"), Address("0.0.0.0"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("2001:db8::/128"), Address("::"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("192.0.2.0/32"), null, "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("2001:db8::/128"), null, "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("::/128"), Address("fe80::"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE);
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertTrue(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
 
         r = new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE);
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertTrue(r.isIPv6UnreachableDefault());
+        }
     }
 
     @Test
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 83399b8..75c71bb 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -6328,6 +6328,7 @@
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
         // The uid range needs to cover the test app so the network is visible to it.
         final Set<UidRange> vpnRange = Collections.singleton(UidRange.createForUser(VPN_USER));
         final TestNetworkAgentWrapper vpnNetworkAgent = establishVpn(lp, VPN_UID, vpnRange);
@@ -6353,6 +6354,7 @@
     public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
         lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
         // The uid range needs to cover the test app so the network is visible to it.
         final Set<UidRange> vpnRange = Collections.singleton(UidRange.createForUser(VPN_USER));
@@ -6384,6 +6386,7 @@
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
         // The uid range needs to cover the test app so the network is visible to it.
         final Set<UidRange> vpnRange = Collections.singleton(UidRange.createForUser(VPN_USER));
         final TestNetworkAgentWrapper vpnNetworkAgent = establishVpn(lp, VPN_UID, vpnRange);
@@ -6420,6 +6423,7 @@
         reset(mMockNetd);
         lp = new LinkProperties();
         lp.setInterfaceName("tun1");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE));
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
         vpnNetworkAgent.sendLinkProperties(lp);
         waitForIdle();
@@ -6432,6 +6436,7 @@
     public void testUidUpdateChangesInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE));
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
         // The uid range needs to cover the test app so the network is visible to it.
         final UidRange vpnRange = UidRange.createForUser(VPN_USER);