From 5fd76d347d31dae96cfb72f68e2b371c570b37dd Mon Sep 17 00:00:00 2001 From: Xiao Ma Date: Mon, 17 Mar 2025 16:06:43 +0900 Subject: [PATCH] Implement the private DNS bypass logic for captive portal over CCT. Flag: EXEMPT the API caller is guarded by a legacy experiment flag Bug: 368416967 Test: atest ConnectivityCoverageTests:android.net.connectivity.com.android.server.CSNetdBypassRulesTests Change-Id: I0e24bf37c412e6f5676924269ca98db1f9dfd6df --- service/Android.bp | 2 +- .../server/connectivity/NetworkAgentInfo.java | 73 +++- .../CSNetdBypassRulesTests.kt | 375 ++++++++++++++++++ 3 files changed, 438 insertions(+), 12 deletions(-) create mode 100644 tests/unit/java/com/android/server/connectivityservice/CSNetdBypassRulesTests.kt diff --git a/service/Android.bp b/service/Android.bp index fb74238caf..62fe9fd516 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -191,7 +191,7 @@ java_library { "androidx.annotation_annotation", "connectivity-net-module-utils-bpf", "connectivity_native_aidl_interface-lateststable-java", - "dnsresolver_aidl_interface-V15-java", + "dnsresolver_aidl_interface-V16-java", "modules-utils-shell-command-handler", "net-utils-service-connectivity", "netd-client", diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java index abab6ab66d..6fac75bf10 100644 --- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java +++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java @@ -30,7 +30,9 @@ import static android.system.OsConstants.EIO; import static android.system.OsConstants.ENOENT; import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_ALLOW_BYPASS_PRIVATE_DNS_FOR_DELEGATE_UID_ERROR; import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_VPN_FOR_DELEGATE_UID_ENOENT; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_PRIVATE_DNS_FOR_DELEGATE_UID_ENOENT; import android.annotation.NonNull; import android.annotation.Nullable; @@ -586,7 +588,7 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { // For fast lookups. Indexes into mInactivityTimers by request ID. private final SparseArray mInactivityTimerForRequest = new SparseArray<>(); - // Map of delegated UIDs used to bypass VPN and its captive portal app caller. + // Map of delegated UIDs used to bypass VPN and private DNS and its captive portal app caller. private final ArrayMap mCaptivePortalDelegateUids = new ArrayMap<>(); @@ -645,6 +647,7 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { private final NetworkAgentMessageHandler mRegistry; private final QosCallbackTracker mQosCallbackTracker; private final INetd mNetd; + private final IDnsResolver mDnsResolver; private final long mCreationTime; @@ -675,6 +678,7 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { setScore(score); // uses members connService, networkCapabilities and networkAgentConfig clatd = new Nat464Xlat(this, netd, dnsResolver, deps); mNetd = netd; + mDnsResolver = dnsResolver; mContext = context; mHandler = handler; mRegistry = new NetworkAgentMessageHandler(mHandler); @@ -1608,7 +1612,7 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { } } - private int allowBypassVpnOnNetwork(boolean allow, int uid, int netId) { + private int setAllowBypassVpnOnNetwork(boolean allow, int uid, int netId) { try { mNetd.networkAllowBypassVpnOnNetwork(allow, uid, netId); return 0; @@ -1620,20 +1624,52 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { } } + private int setAllowBypassPrivateDnsOnNetwork(boolean allow, int uid, int netId) { + try { + mDnsResolver.setAllowBypassPrivateDnsOnNetwork(netId, uid, allow); + return 0; + } catch (RemoteException e) { + // DnsResolver has crashed, and this process is about to crash as well. + return EIO; + } catch (ServiceSpecificException e) { + return e.errorCode; + } + } + /** * Set the delegate UID of the app that is allowed to perform network traffic for captive * portal login, and configure the netd bypass rule with this delegated UID. * * @param caller the captive portal app to that delegated UID * @param uid the delegated UID of the captive portal app. - * @return Return 0 if set the UID and VPN bypass rule successfully or bypass rule corresponding - * to this UID already exists otherwise return errno. + * @return Return 0 if set the UID and VPN/private DNS bypass rules successfully or bypass rules + * corresponding to this UID already exists otherwise return errno. */ public int setCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller, int uid) { - final int errorCode = allowBypassVpnOnNetwork(true /* allow */, uid, network.netId); - if (errorCode == 0 || errorCode == EEXIST) { - mCaptivePortalDelegateUids.put(caller, uid); + HandlerUtils.ensureRunningOnHandlerThread(mHandler); + int errorCode = setAllowBypassVpnOnNetwork(true /* allow */, uid, network.netId); + if (errorCode != 0 && errorCode != EEXIST) return errorCode; + if (errorCode == EEXIST) { + Log.wtf(TAG, "allow VPN bypass rule for delegated UID " + uid + " already exists."); } + + errorCode = setAllowBypassPrivateDnsOnNetwork(true /* allow */, uid, network.netId); + if (errorCode != 0 && errorCode != EEXIST) { + // TODO: add a specific error code in CoreNetworkingTerribleErrorOccurred proto + // and log it. + FrameworkConnectivityStatsLog.write( + CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED, + CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_ALLOW_BYPASS_PRIVATE_DNS_FOR_DELEGATE_UID_ERROR + ); + // Also remove VPN allow bypass rule if failing to set private DNS allow bypass rule. + setAllowBypassVpnOnNetwork(false /* allow */, uid, network.netId); + return errorCode; + } + if (errorCode == EEXIST) { + Log.wtf(TAG, "allow private DNS bypass rule for delegated UID " + uid + + " already exists."); + } + mCaptivePortalDelegateUids.put(caller, uid); return errorCode == EEXIST ? 0 : errorCode; } @@ -1642,21 +1678,36 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { * portal login, and remove the netd bypass rule if no other caller is delegating this UID. * * @param caller the captive portal app to that delegated UID. - * @return Return 0 if remove the UID and VPN bypass rule successfully or bypass rule - * corresponding to this UID doesn't exist otherwise return errno. + * @return Return 0 if remove the UID and VPN/private DNS bypass rules successfully or bypass + * rules corresponding to this UID doesn't exist otherwise return errno. */ public int removeCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller) { + HandlerUtils.ensureRunningOnHandlerThread(mHandler); final Integer maybeDelegateUid = mCaptivePortalDelegateUids.remove(caller); if (maybeDelegateUid == null) return 0; if (mCaptivePortalDelegateUids.values().contains(maybeDelegateUid)) return 0; - final int errorCode = - allowBypassVpnOnNetwork(false /* allow */, maybeDelegateUid, network.netId); + + int errorCode = + setAllowBypassVpnOnNetwork(false /* allow */, maybeDelegateUid, network.netId); if (errorCode == ENOENT) { FrameworkConnectivityStatsLog.write( CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED, CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_VPN_FOR_DELEGATE_UID_ENOENT ); } + if (errorCode == EIO) { + // Indicate that netd has crashed, nothing can be done. + Log.wtf(TAG, "netd has crashed when setting disallow bypass VPN rule."); + } + + errorCode = setAllowBypassPrivateDnsOnNetwork(false /* allow */, maybeDelegateUid, + network.netId); + if (errorCode == ENOENT) { + FrameworkConnectivityStatsLog.write( + CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED, + CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_PRIVATE_DNS_FOR_DELEGATE_UID_ENOENT + ); + } return errorCode == ENOENT ? 0 : errorCode; } diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetdBypassRulesTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetdBypassRulesTests.kt new file mode 100644 index 0000000000..09a3b19718 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/CSNetdBypassRulesTests.kt @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2025 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 com.android.server + +import android.net.INetworkAgent +import android.net.LinkProperties +import android.net.LocalNetworkConfig +import android.net.Network +import android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.NetworkInfo +import android.net.NetworkProvider +import android.net.NetworkScore +import android.os.Binder +import android.os.Build +import android.os.RemoteException +import android.os.ServiceSpecificException +import android.system.OsConstants +import androidx.test.filters.SmallTest +import com.android.server.ConnectivityService.CaptivePortalImpl +import com.android.server.connectivity.NetworkAgentInfo +import com.android.server.connectivity.QosCallbackTracker +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.postAndWait +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.InOrder +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doThrow +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` + +private const val TEST_UID = 101 +private const val TEST_TIMEOUT_MS = 500L + +@DevSdkIgnoreRunner.MonitorThreadLeak +@RunWith(DevSdkIgnoreRunner::class) +@SmallTest +@IgnoreUpTo(Build.VERSION_CODES.R) +class CSNetdBypassRulesTests : CSTest() { + private val TEST_LINGER_DELAY_MS = 400 + + private val caller = mock(CaptivePortalImpl::class.java) + private val na = mock(INetworkAgent::class.java) + private val info = mock(NetworkInfo::class.java) + private val lp = mock(LinkProperties::class.java) + private val nc = mock(NetworkCapabilities::class.java) + private val score = mock(NetworkScore::class.java) + private val config = mock(NetworkAgentConfig::class.java) + private val localNetworkConfig = FromS(mock(LocalNetworkConfig::class.java)) + private val qosCallbackTracker = mock(QosCallbackTracker::class.java) + + private fun createNetworkAgentInfo(network: Network): NetworkAgentInfo { + return NetworkAgentInfo(na, network, info, lp, nc, localNetworkConfig?.value, score, + context, csHandler, config, service, netd, dnsResolver, NetworkProvider.ID_NONE, + Binder.getCallingUid(), TEST_LINGER_DELAY_MS, qosCallbackTracker, CSDeps()) + } + + private fun verifySetAllowBypassVpnOnNetwork( + inOrder: InOrder, + allow: Boolean, + netId: Int + ) { + inOrder.verify(netd).networkAllowBypassVpnOnNetwork(eq(allow), eq(TEST_UID), eq(netId)) + } + + private fun verifySetAllowBypassPrivateDnsOnNetwork( + inOrder: InOrder, + allow: Boolean, + netId: Int + ) { + inOrder.verify( + dnsResolver + ).setAllowBypassPrivateDnsOnNetwork(eq(netId), eq(TEST_UID), eq(allow)) + } + + private fun verifyNeverSetAllowBypassPrivateDnsOnNetwork(inOrder: InOrder) { + inOrder.verify( + dnsResolver, + never() + ).setAllowBypassPrivateDnsOnNetwork(anyInt(), anyInt(), anyBoolean()) + } + + // Always connect a wifi NetworkAgent before running each test case. This guarantees + // that the handlers (like the DNS resolver or netd) have become idle and completed + // any pending tasks. + // + // Otherwise, a running service might interfere with a mocked dependency. For example, + // if the mocked netd is still active when a test tries to simulate an API call and throw + // exception, then it will break the mock. + private fun createWifiAgentAndConnect() = Agent( + "wlan0", + TRANSPORT_WIFI, + NET_CAPABILITY_INTERNET + ).also { it.connect() } + + private fun withWifiAgent(what: (CSAgentWrapper) -> Unit) { + val wifiAgent = createWifiAgentAndConnect() + what(wifiAgent) + wifiAgent.disconnect() + } + + private fun throwExceptionIfAllowingBypassVpn(e: Exception, allow: Boolean, netId: Int) { + doThrow( + e + ).`when`(netd).networkAllowBypassVpnOnNetwork( + eq(allow), + eq(TEST_UID), + eq(netId) + ) + } + + private fun throwExceptionIfAllowingBypassPrivateDns(e: Exception, allow: Boolean, netId: Int) { + doThrow( + e + ).`when`(dnsResolver).setAllowBypassPrivateDnsOnNetwork( + eq(netId), + eq(TEST_UID), + eq(allow) + ) + } + + @Test + fun testNetdBypassRules_allowBypass() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(0, res) + verifySetAllowBypassVpnOnNetwork(inOrder, true /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, true /* allow */, netId) + inOrder.verifyNoMoreInteractions() + } + } + + @Test + fun testNetdBypassRules_allowBypassVpnFailure_remoteException() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + throwExceptionIfAllowingBypassVpn(RemoteException(), true /* allow */, netId) + + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(OsConstants.EIO, res) + verifyNeverSetAllowBypassPrivateDnsOnNetwork(inOrder) + } + } + + @Test + fun testNetdBypassRules_allowBypassVpnFailure_delegateUidAlreadyExists() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + throwExceptionIfAllowingBypassVpn( + ServiceSpecificException(OsConstants.EEXIST), + true /* allow */, + netId + ) + + val nai = createNetworkAgentInfo(wifiAgent.network) + // EEXIST indicates that there is already a delegate UID corresponding to the captive + // portal caller, therefore, this API will return 0 for success. + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(0, res) + + verifySetAllowBypassVpnOnNetwork(inOrder, true /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, true /* allow */, netId) + } + } + + @Test + fun testNetdBypassRules_allowBypassPrivateDnsFailure_remoteException() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + throwExceptionIfAllowingBypassPrivateDns(RemoteException(), true /* allow */, netId) + + // Verify that VPN bypass rule will be restored if the private DNS bypass fails. + // While this order is not really contractual, the current implementation will + // set VPN to true first and call it again with false later if private DNS bypass + // fails. + // TODO : don't depend on the order + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(OsConstants.EIO, res) + verifySetAllowBypassVpnOnNetwork(inOrder, false /* allow */, netId) + inOrder.verifyNoMoreInteractions() + } + } + + @Test + fun testNetdBypassRules_allowBypassPrivateDnsFailure_delegateUidAlreadyExists() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + throwExceptionIfAllowingBypassPrivateDns( + ServiceSpecificException(OsConstants.EEXIST), + true /* allow */, + netId + ) + + // EEXIST indicates that there is already a delegate UID corresponding to the captive + // portal caller, therefore, this API will return 0 for success. + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(0, res) + + verifySetAllowBypassVpnOnNetwork(inOrder, true /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, true /* allow */, netId) + } + } + + @Test + fun testNetdBypassRules_disallowBypass() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(0, res) + + verifySetAllowBypassVpnOnNetwork(inOrder, true /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, true /* allow */, netId) + + val res2 = csHandler.postAndWait { nai.removeCaptivePortalDelegateUid(caller) } + assertEquals(0, res) + + verifySetAllowBypassVpnOnNetwork(inOrder, false /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, false /* allow */, netId) + } + } + + @Test + fun testNetdBypassRules_disallowBypassVpnFailure_remoteException() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + throwExceptionIfAllowingBypassVpn(RemoteException(), false /* allow */, netId) + + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(0, res) + + verifySetAllowBypassVpnOnNetwork(inOrder, true /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, true /* allow */, netId) + + // If a remote exception is thrown while disallowing the bypass VPN rule, the function + // call will return an EIO and log this failure. But the operation will continue to + // attempt restoring the bypass rule for private DNS. The final return value will then + // be determined by the outcome of that particular call. + val res2 = csHandler.postAndWait { nai.removeCaptivePortalDelegateUid(caller) } + assertEquals(0, res2) + verifySetAllowBypassVpnOnNetwork(inOrder, false /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, false /* allow */, netId) + } + } + + @Test + fun testNetdBypassRules_disallowBypassVpnFailure_delegateUidNotExist() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + throwExceptionIfAllowingBypassVpn( + ServiceSpecificException(OsConstants.ENOENT), + false /* allow */, + netId + ) + + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(0, res) + + verifySetAllowBypassVpnOnNetwork(inOrder, true /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, true /* allow */, netId) + + // In rare cases, attempts to disallow the VPN bypass rule will throw an ENOENT error, this + // indicates that there is no delegated UID corresponding to the captive portal caller. + // Since the delegated UID does not exist, this is not considered an error (return 0), but + // a terrible failure is logged to see how often this issue happens, see + // {@link NetworkAgentInfo#setCaptivePortalDelegateUid}. Besides, also restore the private + // DNS bypass rule to its default state (disallowed) before returning. + val res2 = csHandler.postAndWait { nai.removeCaptivePortalDelegateUid(caller) } + assertEquals(0, res2) + + verifySetAllowBypassVpnOnNetwork(inOrder, false /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, false /* allow */, netId) + } + } + + @Test + fun testNetdBypassRules_disallowBypassPrivateDnsFailure_remoteException() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + throwExceptionIfAllowingBypassPrivateDns(RemoteException(), false /* allow */, netId) + + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(0, res) + + verifySetAllowBypassVpnOnNetwork(inOrder, true /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, true /* allow */, netId) + + val res2 = csHandler.postAndWait { nai.removeCaptivePortalDelegateUid(caller) } + assertEquals(OsConstants.EIO, res2) + + verifySetAllowBypassVpnOnNetwork(inOrder, false /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, false /* allow */, netId) + } + } + + @Test + fun testNetdBypassRules_disallowBypassPrivateDnsFailure_delegateUidNotExist() { + withWifiAgent { wifiAgent -> + val netId = wifiAgent.network.netId + val inOrder = inOrder(netd, dnsResolver) + + throwExceptionIfAllowingBypassPrivateDns( + ServiceSpecificException(OsConstants.ENOENT), + false /* allow */, + netId + ) + + val nai = createNetworkAgentInfo(wifiAgent.network) + val res = csHandler.postAndWait { nai.setCaptivePortalDelegateUid(caller, TEST_UID) } + assertEquals(0, res) + + verifySetAllowBypassVpnOnNetwork(inOrder, true /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, true /* allow */, netId) + + // In rare cases, attempts to disallow the VPN bypass rule will throw an ENOENT error, this + // indicates that there is no delegated UID corresponding to the captive portal caller. + // Since the delegated UID does not exist, this is not considered an error (return 0), but + // a terrible failure is logged, see {@link NetworkAgentInfo#removeCaptivePortalDelegateUid}. + val res2 = csHandler.postAndWait { nai.removeCaptivePortalDelegateUid(caller) } + assertEquals(0, res2) + + verifySetAllowBypassVpnOnNetwork(inOrder, false /* allow */, netId) + verifySetAllowBypassPrivateDnsOnNetwork(inOrder, false /* allow */, netId) + } + } +}