Merge "Add test for NetworkRequest.Builder(NetworkRequest)"
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index edd1ebb..105bab1 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -27,6 +27,8 @@
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
     field public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED";
+    field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
+    field public static final int CONNECTIVITY_SCOPE_LOCAL = 2; // 0x2
     field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
     field public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
     field public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
@@ -72,6 +74,7 @@
   public static interface TetheringManager.TetheringEventCallback {
     method public default void onClientsChanged(@NonNull java.util.Collection<android.net.TetheredClient>);
     method public default void onError(@NonNull String, int);
+    method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.List<java.lang.String>);
     method public default void onOffloadStatusChanged(int);
     method public default void onTetherableInterfacesChanged(@NonNull java.util.List<java.lang.String>);
     method public default void onTetheredInterfacesChanged(@NonNull java.util.List<java.lang.String>);
@@ -81,6 +84,7 @@
 
   public static class TetheringManager.TetheringRequest {
     method @Nullable public android.net.LinkAddress getClientStaticIpv4Address();
+    method public int getConnectivityScope();
     method @Nullable public android.net.LinkAddress getLocalIpv4Address();
     method public boolean getShouldShowEntitlementUi();
     method public int getTetheringType();
@@ -90,6 +94,7 @@
   public static class TetheringManager.TetheringRequest.Builder {
     ctor public TetheringManager.TetheringRequest.Builder(int);
     method @NonNull public android.net.TetheringManager.TetheringRequest build();
+    method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 97fb497..c64da8a 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -557,6 +557,28 @@
     }
 
     /**
+     * Indicates that this tethering connection will provide connectivity beyond this device (e.g.,
+     * global Internet access).
+     */
+    public static final int CONNECTIVITY_SCOPE_GLOBAL = 1;
+
+    /**
+     * Indicates that this tethering connection will only provide local connectivity.
+     */
+    public static final int CONNECTIVITY_SCOPE_LOCAL = 2;
+
+    /**
+     * Connectivity scopes for {@link TetheringRequest.Builder#setConnectivityScope}.
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "CONNECTIVITY_SCOPE_", value = {
+            CONNECTIVITY_SCOPE_GLOBAL,
+            CONNECTIVITY_SCOPE_LOCAL,
+    })
+    public @interface ConnectivityScope {}
+
+    /**
      *  Use with {@link #startTethering} to specify additional parameters when starting tethering.
      */
     public static class TetheringRequest {
@@ -579,6 +601,7 @@
                 mBuilderParcel.staticClientAddress = null;
                 mBuilderParcel.exemptFromEntitlementCheck = false;
                 mBuilderParcel.showProvisioningUi = true;
+                mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
             }
 
             /**
@@ -624,7 +647,21 @@
                 return this;
             }
 
-            /** Build {@link TetheringRequest] with the currently set configuration. */
+            /**
+             * Sets the connectivity scope to be provided by this tethering downstream.
+             */
+            @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+            @NonNull
+            public Builder setConnectivityScope(@ConnectivityScope int scope) {
+                if (!checkConnectivityScope(mBuilderParcel.tetheringType, scope)) {
+                    throw new IllegalArgumentException("Invalid connectivity scope " + scope);
+                }
+
+                mBuilderParcel.connectivityScope = scope;
+                return this;
+            }
+
+            /** Build {@link TetheringRequest} with the currently set configuration. */
             @NonNull
             public TetheringRequest build() {
                 return new TetheringRequest(mBuilderParcel);
@@ -655,6 +692,12 @@
             return mRequestParcel.tetheringType;
         }
 
+        /** Get connectivity type */
+        @ConnectivityScope
+        public int getConnectivityScope() {
+            return mRequestParcel.connectivityScope;
+        }
+
         /** Check if exempt from entitlement check. */
         public boolean isExemptFromEntitlementCheck() {
             return mRequestParcel.exemptFromEntitlementCheck;
@@ -679,6 +722,26 @@
         }
 
         /**
+         * Returns the default connectivity scope for the given tethering type. Usually this is
+         * CONNECTIVITY_SCOPE_GLOBAL, except for NCM which for historical reasons defaults to local.
+         * @hide
+         */
+        public static @ConnectivityScope int getDefaultConnectivityScope(int tetheringType) {
+            return tetheringType != TETHERING_NCM
+                    ? CONNECTIVITY_SCOPE_GLOBAL
+                    : CONNECTIVITY_SCOPE_LOCAL;
+        }
+
+        /**
+         * Checks whether the requested connectivity scope is allowed.
+         * @hide
+         */
+        private static boolean checkConnectivityScope(int type, int scope) {
+            if (scope == CONNECTIVITY_SCOPE_GLOBAL) return true;
+            return type == TETHERING_USB || type == TETHERING_ETHERNET || type == TETHERING_NCM;
+        }
+
+        /**
          * Get a TetheringRequestParcel from the configuration
          * @hide
          */
@@ -940,6 +1003,15 @@
         default void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {}
 
         /**
+         * Called when there was a change in the list of local-only interfaces.
+         *
+         * <p>This will be called immediately after the callback is registered, and may be called
+         * multiple times later upon changes.
+         * @param interfaces The list of 0 or more String of active local-only interface names.
+         */
+        default void onLocalOnlyInterfacesChanged(@NonNull List<String> interfaces) {}
+
+        /**
          * Called when an error occurred configuring tethering.
          *
          * <p>This will be called immediately after the callback is registered if the latest status
@@ -1045,6 +1117,7 @@
                 private final HashMap<String, Integer> mErrorStates = new HashMap<>();
                 private String[] mLastTetherableInterfaces = null;
                 private String[] mLastTetheredInterfaces = null;
+                private String[] mLastLocalOnlyInterfaces = null;
 
                 @Override
                 public void onUpstreamChanged(Network network) throws RemoteException {
@@ -1082,6 +1155,14 @@
                             Collections.unmodifiableList(Arrays.asList(mLastTetheredInterfaces)));
                 }
 
+                private synchronized void maybeSendLocalOnlyIfacesChangedCallback(
+                        final TetherStatesParcel newStates) {
+                    if (Arrays.equals(mLastLocalOnlyInterfaces, newStates.localOnlyList)) return;
+                    mLastLocalOnlyInterfaces = newStates.localOnlyList.clone();
+                    callback.onLocalOnlyInterfacesChanged(
+                            Collections.unmodifiableList(Arrays.asList(mLastLocalOnlyInterfaces)));
+                }
+
                 // Called immediately after the callbacks are registered.
                 @Override
                 public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
@@ -1092,6 +1173,7 @@
                         sendRegexpsChanged(parcel.config);
                         maybeSendTetherableIfacesChangedCallback(parcel.states);
                         maybeSendTetheredIfacesChangedCallback(parcel.states);
+                        maybeSendLocalOnlyIfacesChangedCallback(parcel.states);
                         callback.onClientsChanged(parcel.tetheredClients);
                         callback.onOffloadStatusChanged(parcel.offloadStatus);
                     });
@@ -1122,6 +1204,7 @@
                         sendErrorCallbacks(states);
                         maybeSendTetherableIfacesChangedCallback(states);
                         maybeSendTetheredIfacesChangedCallback(states);
+                        maybeSendLocalOnlyIfacesChangedCallback(states);
                     });
                 }
 
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index c0280d3..f13c970 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -28,4 +28,5 @@
     LinkAddress staticClientAddress;
     boolean exemptFromEntitlementCheck;
     boolean showProvisioningUi;
+    int connectivityScope;
 }
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index da15fa8..c45ce83 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -1426,7 +1426,7 @@
                     break;
                 case CMD_INTERFACE_DOWN:
                     transitionTo(mUnavailableState);
-                    mLog.i("Untethered (interface down) and restarting" + mIfaceName);
+                    mLog.i("Untethered (interface down) and restarting " + mIfaceName);
                     mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
                     break;
                 default:
diff --git a/Tethering/src/android/net/util/TetheringUtils.java b/Tethering/src/android/net/util/TetheringUtils.java
index 9e7cc2f..29900d9 100644
--- a/Tethering/src/android/net/util/TetheringUtils.java
+++ b/Tethering/src/android/net/util/TetheringUtils.java
@@ -162,7 +162,8 @@
                 && Objects.equals(request.localIPv4Address, otherRequest.localIPv4Address)
                 && Objects.equals(request.staticClientAddress, otherRequest.staticClientAddress)
                 && request.exemptFromEntitlementCheck == otherRequest.exemptFromEntitlementCheck
-                && request.showProvisioningUi == otherRequest.showProvisioningUi;
+                && request.showProvisioningUi == otherRequest.showProvisioningUi
+                && request.connectivityScope == otherRequest.connectivityScope;
     }
 
     /** Get inet6 address for all nodes given scope ID. */
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index f795747..acbfa8c 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -29,6 +29,7 @@
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
 import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
 import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER;
@@ -90,6 +91,7 @@
 import android.net.TetheredClient;
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
+import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringRequestParcel;
 import android.net.ip.IpServer;
 import android.net.shared.NetdUtils;
@@ -731,7 +733,7 @@
                     return;
                 }
                 maybeTrackNewInterfaceLocked(iface, TETHERING_ETHERNET);
-                changeInterfaceState(iface, IpServer.STATE_TETHERED);
+                changeInterfaceState(iface, getRequestedState(TETHERING_ETHERNET));
                 mConfiguredEthernetIface = iface;
             }
         }
@@ -748,8 +750,12 @@
         }
     }
 
-    int tether(String iface) {
-        return tether(iface, IpServer.STATE_TETHERED);
+    void tether(String iface, int requestedState, final IIntResultListener listener) {
+        mHandler.post(() -> {
+            try {
+                listener.onResult(tether(iface, requestedState));
+            } catch (RemoteException e) { }
+        });
     }
 
     private int tether(String iface, int requestedState) {
@@ -782,6 +788,14 @@
         }
     }
 
+    void untether(String iface, final IIntResultListener listener) {
+        mHandler.post(() -> {
+            try {
+                listener.onResult(untether(iface));
+            } catch (RemoteException e) { }
+        });
+    }
+
     int untether(String iface) {
         if (DBG) Log.d(TAG, "Untethering " + iface);
         synchronized (mPublicSync) {
@@ -843,6 +857,22 @@
         return true;
     }
 
+    private int getRequestedState(int type) {
+        final TetheringRequestParcel request = mActiveTetheringRequests.get(type);
+
+        // The request could have been deleted before we had a chance to complete it.
+        // If so, assume that the scope is the default scope for this tethering type.
+        // This likely doesn't matter - if the request has been deleted, then tethering is
+        // likely going to be stopped soon anyway.
+        final int connectivityScope = (request != null)
+                ? request.connectivityScope
+                : TetheringRequest.getDefaultConnectivityScope(type);
+
+        return connectivityScope == CONNECTIVITY_SCOPE_LOCAL
+                ? IpServer.STATE_LOCAL_ONLY
+                : IpServer.STATE_TETHERED;
+    }
+
     // TODO: Figure out how to update for local hotspot mode interfaces.
     private void sendTetherStateChangedBroadcast() {
         if (!isTetheringSupported()) return;
@@ -982,9 +1012,11 @@
                     mEntitlementMgr.stopProvisioningIfNeeded(TETHERING_USB);
                 } else if (usbConfigured && rndisEnabled) {
                     // Tether if rndis is enabled and usb is configured.
-                    tetherMatchingInterfaces(IpServer.STATE_TETHERED, TETHERING_USB);
+                    final int state = getRequestedState(TETHERING_USB);
+                    tetherMatchingInterfaces(state, TETHERING_USB);
                 } else if (usbConnected && ncmEnabled) {
-                    tetherMatchingInterfaces(IpServer.STATE_LOCAL_ONLY, TETHERING_NCM);
+                    final int state = getRequestedState(TETHERING_NCM);
+                    tetherMatchingInterfaces(state, TETHERING_NCM);
                 }
                 mRndisEnabled = usbConfigured && rndisEnabled;
             }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index c69dc49..e36df7f 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -105,9 +105,7 @@
                 IIntResultListener listener) {
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
 
-            try {
-                listener.onResult(mTethering.tether(iface));
-            } catch (RemoteException e) { }
+            mTethering.tether(iface, IpServer.STATE_TETHERED, listener);
         }
 
         @Override
@@ -115,9 +113,7 @@
                 IIntResultListener listener) {
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
 
-            try {
-                listener.onResult(mTethering.untether(iface));
-            } catch (RemoteException e) { }
+            mTethering.untether(iface, listener);
         }
 
         @Override
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index d206ea0..fe4e696 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -19,7 +19,14 @@
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.TETHER_PRIVILEGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -50,6 +57,10 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TapPacketReader;
 
@@ -60,6 +71,7 @@
 
 import java.io.FileDescriptor;
 import java.net.Inet4Address;
+import java.net.InetAddress;
 import java.net.InterfaceAddress;
 import java.net.NetworkInterface;
 import java.net.SocketException;
@@ -229,6 +241,82 @@
 
     }
 
+    private static boolean isRouterAdvertisement(byte[] pkt) {
+        if (pkt == null) return false;
+
+        ByteBuffer buf = ByteBuffer.wrap(pkt);
+
+        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+        if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
+
+        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+        if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
+
+        final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+        return icmpv6Hdr.type == (short) ICMPV6_ROUTER_ADVERTISEMENT;
+    }
+
+    private static void expectRouterAdvertisement(TapPacketReader reader, String iface,
+            long timeoutMs) {
+        final long deadline = SystemClock.uptimeMillis() + timeoutMs;
+        do {
+            byte[] pkt = reader.popPacket(timeoutMs);
+            if (isRouterAdvertisement(pkt)) return;
+            timeoutMs = deadline - SystemClock.uptimeMillis();
+        } while (timeoutMs > 0);
+        fail("Did not receive router advertisement on " + iface + " after "
+                +  timeoutMs + "ms idle");
+    }
+
+    private static void expectLocalOnlyAddresses(String iface) throws Exception {
+        final List<InterfaceAddress> interfaceAddresses =
+                NetworkInterface.getByName(iface).getInterfaceAddresses();
+
+        boolean foundIpv6Ula = false;
+        for (InterfaceAddress ia : interfaceAddresses) {
+            final InetAddress addr = ia.getAddress();
+            if (isIPv6ULA(addr)) {
+                foundIpv6Ula = true;
+            }
+            final int prefixlen = ia.getNetworkPrefixLength();
+            final LinkAddress la = new LinkAddress(addr, prefixlen);
+            if (la.isIpv6() && la.isGlobalPreferred()) {
+                fail("Found global IPv6 address on local-only interface: " + interfaceAddresses);
+            }
+        }
+
+        assertTrue("Did not find IPv6 ULA on local-only interface " + iface,
+                foundIpv6Ula);
+    }
+
+    @Test
+    public void testLocalOnlyTethering() throws Exception {
+        assumeFalse(mEm.isAvailable());
+
+        mEm.setIncludeTestInterfaces(true);
+
+        mTestIface = createTestInterface();
+
+        final String iface = mTetheredInterfaceRequester.getInterface();
+        assertEquals("TetheredInterfaceCallback for unexpected interface",
+                mTestIface.getInterfaceName(), iface);
+
+        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET)
+                .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build();
+        mTetheringEventCallback = enableEthernetTethering(iface, request);
+        mTetheringEventCallback.awaitInterfaceLocalOnly();
+
+        // makePacketReader only works after tethering is started, because until then the interface
+        // does not have an IP address, and unprivileged apps cannot see interfaces without IP
+        // addresses. This shouldn't be flaky because the TAP interface will buffer all packets even
+        // before the reader is started.
+        FileDescriptor fd = mTestIface.getFileDescriptor().getFileDescriptor();
+        mTapPacketReader = makePacketReader(fd, getMTU(mTestIface));
+
+        expectRouterAdvertisement(mTapPacketReader, iface, 2000 /* timeoutMs */);
+        expectLocalOnlyAddresses(iface);
+    }
+
     private boolean isAdbOverNetwork() {
         // If adb TCP port opened, this test may running by adb over network.
         return (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1)
@@ -257,10 +345,13 @@
         private final TetheringManager mTm;
         private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
         private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
+        private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
+        private final CountDownLatch mLocalOnlyStoppedLatch = new CountDownLatch(1);
         private final CountDownLatch mClientConnectedLatch = new CountDownLatch(1);
         private final String mIface;
 
         private volatile boolean mInterfaceWasTethered = false;
+        private volatile boolean mInterfaceWasLocalOnly = false;
         private volatile boolean mUnregistered = false;
         private volatile Collection<TetheredClient> mClients = null;
 
@@ -279,7 +370,6 @@
             // Ignore stale callbacks registered by previous test cases.
             if (mUnregistered) return;
 
-            final boolean wasTethered = mTetheringStartedLatch.getCount() == 0;
             if (!mInterfaceWasTethered && (mIface == null || interfaces.contains(mIface))) {
                 // This interface is being tethered for the first time.
                 Log.d(TAG, "Tethering started: " + interfaces);
@@ -291,20 +381,48 @@
             }
         }
 
+        @Override
+        public void onLocalOnlyInterfacesChanged(List<String> interfaces) {
+            // Ignore stale callbacks registered by previous test cases.
+            if (mUnregistered) return;
+
+            if (!mInterfaceWasLocalOnly && (mIface == null || interfaces.contains(mIface))) {
+                // This interface is being put into local-only mode for the first time.
+                Log.d(TAG, "Local-only started: " + interfaces);
+                mInterfaceWasLocalOnly = true;
+                mLocalOnlyStartedLatch.countDown();
+            } else if (mInterfaceWasLocalOnly && !interfaces.contains(mIface)) {
+                Log.d(TAG, "Local-only stopped: " + interfaces);
+                mLocalOnlyStoppedLatch.countDown();
+            }
+        }
+
         public void awaitInterfaceTethered() throws Exception {
             assertTrue("Ethernet not tethered after " + TIMEOUT_MS + "ms",
                     mTetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
 
+        public void awaitInterfaceLocalOnly() throws Exception {
+            assertTrue("Ethernet not local-only after " + TIMEOUT_MS + "ms",
+                    mLocalOnlyStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+
         public void awaitInterfaceUntethered() throws Exception {
             // Don't block teardown if the interface was never tethered.
             // This is racy because the interface might become tethered right after this check, but
             // that can only happen in tearDown if startTethering timed out, which likely means
             // the test has already failed.
-            if (!mInterfaceWasTethered) return;
+            if (!mInterfaceWasTethered && !mInterfaceWasLocalOnly) return;
 
-            assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
-                    mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            if (mInterfaceWasTethered) {
+                assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
+                        mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            } else if (mInterfaceWasLocalOnly) {
+                assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
+                        mLocalOnlyStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            } else {
+                fail(mIface + " cannot be both tethered and local-only. Update this test class.");
+            }
         }
 
         @Override
@@ -347,7 +465,19 @@
         };
         Log.d(TAG, "Starting Ethernet tethering");
         mTm.startTethering(request, mHandler::post /* executor */,  startTetheringCallback);
-        callback.awaitInterfaceTethered();
+
+        final int connectivityType = request.getConnectivityScope();
+        switch (connectivityType) {
+            case CONNECTIVITY_SCOPE_GLOBAL:
+                callback.awaitInterfaceTethered();
+                break;
+            case CONNECTIVITY_SCOPE_LOCAL:
+                callback.awaitInterfaceLocalOnly();
+                break;
+            default:
+                fail("Unexpected connectivity type requested: " + connectivityType);
+        }
+
         return callback;
     }
 
@@ -444,7 +574,6 @@
     }
 
     private static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
-        private final CountDownLatch mInterfaceAvailableLatch = new CountDownLatch(1);
         private final Handler mHandler;
         private final EthernetManager mEm;
 
diff --git a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
index 9968b5f..e5d0b1c 100644
--- a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
+++ b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
@@ -15,6 +15,7 @@
  */
 package android.net.util;
 
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.system.OsConstants.AF_UNIX;
@@ -78,7 +79,7 @@
     }
 
     @Test
-    public void testIsTetheringRequestEquals() throws Exception {
+    public void testIsTetheringRequestEquals() {
         TetheringRequestParcel request = makeTetheringRequestParcel();
 
         assertTrue(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, mTetheringRequest));
@@ -104,7 +105,11 @@
         request.showProvisioningUi = false;
         assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
 
-        MiscAsserts.assertFieldCountEquals(5, TetheringRequestParcel.class);
+        request = makeTetheringRequestParcel();
+        request.connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
+        assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
+
+        MiscAsserts.assertFieldCountEquals(6, TetheringRequestParcel.class);
     }
 
     // Writes the specified packet to a filedescriptor, skipping the Ethernet header.
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index 7bba67b..7204ff6 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -37,6 +37,7 @@
 import android.net.ITetheringConnector;
 import android.net.ITetheringEventCallback;
 import android.net.TetheringRequestParcel;
+import android.net.ip.IpServer;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.ResultReceiver;
@@ -156,11 +157,9 @@
     }
 
     private void runTether(final TestTetheringResult result) throws Exception {
-        when(mTethering.tether(TEST_IFACE_NAME)).thenReturn(TETHER_ERROR_NO_ERROR);
         mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
         verify(mTethering).isTetheringSupported();
-        verify(mTethering).tether(TEST_IFACE_NAME);
-        result.assertResult(TETHER_ERROR_NO_ERROR);
+        verify(mTethering).tether(TEST_IFACE_NAME, IpServer.STATE_TETHERED, result);
     }
 
     @Test
@@ -186,12 +185,10 @@
     }
 
     private void runUnTether(final TestTetheringResult result) throws Exception {
-        when(mTethering.untether(TEST_IFACE_NAME)).thenReturn(TETHER_ERROR_NO_ERROR);
         mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
                 result);
         verify(mTethering).isTetheringSupported();
-        verify(mTethering).untether(TEST_IFACE_NAME);
-        result.assertResult(TETHER_ERROR_NO_ERROR);
+        verify(mTethering).untether(eq(TEST_IFACE_NAME), eq(result));
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index e042df4..7c3dd23 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -33,9 +33,11 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
 import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
 import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringManager.TETHERING_NCM;
 import static android.net.TetheringManager.TETHERING_USB;
@@ -93,6 +95,9 @@
 
 import android.app.usage.NetworkStatsManager;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -238,6 +243,8 @@
     @Mock private TetheringNotificationUpdater mNotificationUpdater;
     @Mock private BpfCoordinator mBpfCoordinator;
     @Mock private PackageManager mPackageManager;
+    @Mock private BluetoothAdapter mBluetoothAdapter;
+    @Mock private BluetoothPan mBluetoothPan;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -338,7 +345,8 @@
                             || ifName.equals(TEST_MOBILE_IFNAME)
                             || ifName.equals(TEST_P2P_IFNAME)
                             || ifName.equals(TEST_NCM_IFNAME)
-                            || ifName.equals(TEST_ETH_IFNAME));
+                            || ifName.equals(TEST_ETH_IFNAME)
+                            || ifName.equals(TEST_BT_IFNAME));
             final String[] ifaces = new String[] {
                     TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_WIFI_IFNAME, TEST_MOBILE_IFNAME,
                     TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME};
@@ -478,8 +486,7 @@
 
         @Override
         public BluetoothAdapter getBluetoothAdapter() {
-            // TODO: add test for bluetooth tethering.
-            return null;
+            return mBluetoothAdapter;
         }
 
         @Override
@@ -610,7 +617,7 @@
         when(mNetd.interfaceGetList())
                 .thenReturn(new String[] {
                         TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME,
-                        TEST_NCM_IFNAME, TEST_ETH_IFNAME});
+                        TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME});
         when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
         mInterfaceConfiguration = new InterfaceConfigurationParcel();
         mInterfaceConfiguration.flags = new String[0];
@@ -674,11 +681,11 @@
         when(mResources.getStringArray(R.array.config_tether_usb_regexs))
                 .thenReturn(new String[] { "test_rndis\\d" });
         when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
-                .thenReturn(new String[]{ "test_wlan\\d" });
+                .thenReturn(new String[] { "test_wlan\\d" });
         when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
-                .thenReturn(new String[]{ "test_p2p-p2p\\d-.*" });
+                .thenReturn(new String[] { "test_p2p-p2p\\d-.*" });
         when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
-                .thenReturn(new String[0]);
+                .thenReturn(new String[] { "test_pan\\d" });
         when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
                 .thenReturn(new String[] { "test_ncm\\d" });
         when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(new int[0]);
@@ -696,17 +703,19 @@
     }
 
     private TetheringRequestParcel createTetheringRequestParcel(final int type) {
-        return createTetheringRequestParcel(type, null, null, false);
+        return createTetheringRequestParcel(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
     }
 
     private TetheringRequestParcel createTetheringRequestParcel(final int type,
-            final LinkAddress serverAddr, final LinkAddress clientAddr, final boolean exempt) {
+            final LinkAddress serverAddr, final LinkAddress clientAddr, final boolean exempt,
+            final int scope) {
         final TetheringRequestParcel request = new TetheringRequestParcel();
         request.tetheringType = type;
         request.localIPv4Address = serverAddr;
         request.staticClientAddress = clientAddr;
         request.exemptFromEntitlementCheck = exempt;
         request.showProvisioningUi = false;
+        request.connectivityScope = scope;
 
         return request;
     }
@@ -1967,16 +1976,14 @@
         final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR);
 
         // Enable USB tethering and check that Tethering starts USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  null, null, false), firstResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), firstResult);
         mLooper.dispatchAll();
         firstResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
         verifyNoMoreInteractions(mUsbManager);
 
         // Enable USB tethering again with the same request and expect no change to USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  null, null, false), secondResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), secondResult);
         mLooper.dispatchAll();
         secondResult.assertHasResult();
         verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -1985,7 +1992,7 @@
         // Enable USB tethering with a different request and expect that USB is stopped and
         // started.
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false), thirdResult);
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), thirdResult);
         mLooper.dispatchAll();
         thirdResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2008,7 +2015,7 @@
         final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
                 ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false), null);
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
         mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
@@ -2074,7 +2081,8 @@
     public void testExemptFromEntitlementCheck() throws Exception {
         setupForRequiredProvisioning();
         final TetheringRequestParcel wifiNotExemptRequest =
-                createTetheringRequestParcel(TETHERING_WIFI, null, null, false);
+                createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
+                        CONNECTIVITY_SCOPE_GLOBAL);
         mTethering.startTethering(wifiNotExemptRequest, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
@@ -2087,7 +2095,8 @@
 
         setupForRequiredProvisioning();
         final TetheringRequestParcel wifiExemptRequest =
-                createTetheringRequestParcel(TETHERING_WIFI, null, null, true);
+                createTetheringRequestParcel(TETHERING_WIFI, null, null, true,
+                        CONNECTIVITY_SCOPE_GLOBAL);
         mTethering.startTethering(wifiExemptRequest, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
@@ -2365,6 +2374,70 @@
         return lease;
     }
 
+    @Test
+    public void testBluetoothTethering() throws Exception {
+        final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mLooper.dispatchAll();
+        verifySetBluetoothTethering(true);
+        result.assertHasResult();
+
+        mTethering.interfaceAdded(TEST_BT_IFNAME);
+        mLooper.dispatchAll();
+
+        mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
+        mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
+        final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+        mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+        mLooper.dispatchAll();
+        tetherResult.assertHasResult();
+
+        verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME);
+        verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
+                anyString(), anyString());
+        verify(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
+        verify(mNetd).tetherStartWithConfiguration(any());
+        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
+                anyString(), anyString());
+        verifyNoMoreInteractions(mNetd);
+        reset(mNetd);
+
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        mTethering.stopTethering(TETHERING_BLUETOOTH);
+        mLooper.dispatchAll();
+        final ResultListener untetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+        mTethering.untether(TEST_BT_IFNAME, untetherResult);
+        mLooper.dispatchAll();
+        untetherResult.assertHasResult();
+        verifySetBluetoothTethering(false);
+
+        verify(mNetd).tetherApplyDnsInterfaces();
+        verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME);
+        verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+        verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+        verify(mNetd).tetherStop();
+        verify(mNetd).ipfwdDisableForwarding(TETHERING_NAME);
+        verifyNoMoreInteractions(mNetd);
+    }
+
+    private void verifySetBluetoothTethering(final boolean enable) {
+        final ArgumentCaptor<ServiceListener> listenerCaptor =
+                ArgumentCaptor.forClass(ServiceListener.class);
+        verify(mBluetoothAdapter).isEnabled();
+        verify(mBluetoothAdapter).getProfileProxy(eq(mServiceContext), listenerCaptor.capture(),
+                eq(BluetoothProfile.PAN));
+        final ServiceListener listener = listenerCaptor.getValue();
+        when(mBluetoothPan.isTetheringOn()).thenReturn(enable);
+        listener.onServiceConnected(BluetoothProfile.PAN, mBluetoothPan);
+        verify(mBluetoothPan).setBluetoothTethering(enable);
+        verify(mBluetoothPan).isTetheringOn();
+        verify(mBluetoothAdapter).closeProfileProxy(eq(BluetoothProfile.PAN), eq(mBluetoothPan));
+        verifyNoMoreInteractions(mBluetoothAdapter, mBluetoothPan);
+        reset(mBluetoothAdapter, mBluetoothPan);
+    }
+
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
 }
diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING
index 02d2d6e..fcec483 100644
--- a/tests/cts/hostside/TEST_MAPPING
+++ b/tests/cts/hostside/TEST_MAPPING
@@ -1,5 +1,5 @@
 {
-  "presubmit": [
+  "presubmit-large": [
     {
       "name": "CtsHostsideNetworkTests",
       "options": [
@@ -15,4 +15,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 36e2ffe..0715e32 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -17,6 +17,7 @@
 package com.android.cts.net.hostside;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
 
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities;
@@ -204,9 +205,12 @@
         // Mark network as metered.
         mMeterednessConfiguration.configureNetworkMeteredness(true);
 
-        // Register callback
+        // Register callback, copy the capabilities from the active network to expect the "original"
+        // network before disconnecting, but null out some fields to prevent over-specified.
         registerNetworkCallback(new NetworkRequest.Builder()
-                        .setCapabilities(networkCapabilities).build(), mTestNetworkCallback);
+                .setCapabilities(networkCapabilities.setTransportInfo(null))
+                .removeCapability(NET_CAPABILITY_NOT_METERED)
+                .setSignalStrength(SIGNAL_STRENGTH_UNSPECIFIED).build(), mTestNetworkCallback);
         // Wait for onAvailable() callback to ensure network is available before the test
         // and store the default network.
         mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork();
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index 8a5e00f..717ccb1 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -95,7 +95,7 @@
                 Log.d(TAG, "unregister previous network callback: " + mNetworkCallback);
                 unregisterNetworkCallback();
             }
-            Log.d(TAG, "registering network callback");
+            Log.d(TAG, "registering network callback for " + request);
 
             mNetworkCallback = new ConnectivityManager.NetworkCallback() {
                 @Override
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index b879356..0255f59 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -1745,14 +1745,14 @@
         final TestableNetworkCallback callback = new TestableNetworkCallback();
         final Handler handler = new Handler(Looper.getMainLooper());
         assertThrows(SecurityException.class,
-                () -> mCmShim.requestBackgroundNetwork(testRequest, handler, callback));
+                () -> mCmShim.requestBackgroundNetwork(testRequest, callback, handler));
 
         Network testNetwork = null;
         try {
             // Request background test network via Shell identity which has NETWORK_SETTINGS
             // permission granted.
             runWithShellPermissionIdentity(
-                    () -> mCmShim.requestBackgroundNetwork(testRequest, handler, callback),
+                    () -> mCmShim.requestBackgroundNetwork(testRequest, callback, handler),
                     new String[] { android.Manifest.permission.NETWORK_SETTINGS });
 
             // Register the test network agent which has no foreground request associated to it.