Add blocked reason for internet permission

Currently, even if apps don't have INTERNET permission and can not use
network, network access is considered not blocked and
ConnectivityService send onBlockedStatusChanged callback with
blocked=false.
This CL introduced BLOCKED_REASON_NETWORK_RESTRICTED and consider
network access from apps targetting W+ on V+ releases is blocked if apps
don't have INTERNET permission.
Permission is set when the user is added or package is installed and
permission is removed when the user is removed or packages is
uninstalled.
So ConnectivityService does not need to monitor permission change to
send onBlockedStatusChanged callback by permission change.

Test: CSBlockedReasonsTest
Bug: 339559837

Change-Id: I58d2a4eddc714e205f5b96219f95b637f2826c58
This commit is contained in:
Motomu Utsumi 2024-05-09 16:22:11 +09:00
commit ee96ba0e60
4 changed files with 163 additions and 13 deletions

View file

@ -53,6 +53,7 @@ import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.EXTRA_DEVICE_TYPE;
@ -9880,6 +9881,28 @@ public class ConnectivityServiceTest {
assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
assertExtraInfoFromCmPresent(mCellAgent);
// Remove PERMISSION_INTERNET and disable NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION
doReturn(INetd.PERMISSION_NONE).when(mBpfNetMaps).getNetPermForUid(Process.myUid());
mDeps.setChangeIdEnabled(false,
NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, Process.myUid());
setBlockedReasonChanged(BLOCKED_REASON_DOZE);
if (mDeps.isAtLeastV()) {
// On V+, network access from app that does not have INTERNET permission is considered
// not blocked if NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION is disabled.
// So blocked status does not change from BLOCKED_REASON_NONE
cellNetworkCallback.assertNoCallback();
detailedCallback.assertNoCallback();
} else {
// On U-, onBlockedStatusChanged callback is called with blocked reasons CS receives
// from NPMS callback regardless of permission app has.
// Note that this cannot actually happen because on U-, NPMS will never notify any
// blocked reasons for apps that don't have the INTERNET permission.
cellNetworkCallback.expect(BLOCKED_STATUS, mCellAgent, cb -> cb.getBlocked());
detailedCallback.expect(BLOCKED_STATUS_INT, mCellAgent,
cb -> cb.getReason() == BLOCKED_REASON_DOZE);
}
mCm.unregisterNetworkCallback(cellNetworkCallback);
}

View file

@ -20,6 +20,7 @@ import android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER
import android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED
import android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND
import android.net.ConnectivityManager.BLOCKED_REASON_DOZE
import android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED
import android.net.ConnectivityManager.BLOCKED_REASON_NONE
import android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND
import android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE
@ -27,6 +28,7 @@ import android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER
import android.net.ConnectivityManager.FIREWALL_RULE_ALLOW
import android.net.ConnectivityManager.FIREWALL_RULE_DENY
import android.net.ConnectivitySettingsManager
import android.net.INetd.PERMISSION_NONE
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
@ -36,6 +38,7 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION
import android.os.Build
import android.os.Process
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@ -144,6 +147,12 @@ class CSBlockedReasonsTest : CSTest() {
cm.setDataSaverEnabled(false)
cellCb.expectBlockedStatusChanged(cellAgent.network, BLOCKED_REASON_APP_BACKGROUND)
// waitForIdle since stubbing bpfNetMaps while CS handler thread calls
// bpfNetMaps.getNetPermForUid throws exception.
// The expectBlockedStatusChanged just above guarantees that the onBlockedStatusChanged
// method on this callback was called, but it does not guarantee that ConnectivityService
// has finished processing all onBlockedStatusChanged callbacks for all requests.
waitForIdle()
// Enable data saver
doReturn(BLOCKED_REASON_APP_BACKGROUND or BLOCKED_METERED_REASON_DATA_SAVER)
.`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
@ -186,6 +195,12 @@ class CSBlockedReasonsTest : CSTest() {
blockedReason = BLOCKED_REASON_DOZE
)
// waitForIdle since stubbing bpfNetMaps while CS handler thread calls
// bpfNetMaps.getNetPermForUid throws exception.
// The expectBlockedStatusChanged just above guarantees that the onBlockedStatusChanged
// method on this callback was called, but it does not guarantee that ConnectivityService
// has finished processing all onBlockedStatusChanged callbacks for all requests.
waitForIdle()
// Set RULE_ALLOW on metered deny chain
doReturn(BLOCKED_REASON_DOZE)
.`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
@ -343,4 +358,61 @@ class CSBlockedReasonsTest : CSTest() {
cm.unregisterNetworkCallback(wifiCb)
cm.unregisterNetworkCallback(cb)
}
private fun doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission: Boolean) {
doReturn(PERMISSION_NONE).`when`(bpfNetMaps).getNetPermForUid(Process.myUid())
val wifiCb = DetailedBlockedStatusCallback()
cm.requestNetwork(wifiRequest(), wifiCb)
val wifiAgent = Agent(nc = wifiNc())
wifiAgent.connect()
val expectedBlockedReason = if (blockedByNoInternetPermission) {
BLOCKED_REASON_NETWORK_RESTRICTED
} else {
BLOCKED_REASON_NONE
}
wifiCb.expectAvailableCallbacks(
wifiAgent.network,
validated = false,
blockedReason = expectedBlockedReason
)
// Enable background firewall chain
doReturn(BLOCKED_REASON_APP_BACKGROUND)
.`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true)
if (blockedByNoInternetPermission) {
wifiCb.expectBlockedStatusChanged(
wifiAgent.network,
BLOCKED_REASON_NETWORK_RESTRICTED or BLOCKED_REASON_APP_BACKGROUND
)
}
// Disable background firewall chain
doReturn(BLOCKED_REASON_NONE)
.`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, false)
if (blockedByNoInternetPermission) {
wifiCb.expectBlockedStatusChanged(
wifiAgent.network,
BLOCKED_REASON_NETWORK_RESTRICTED
)
} else {
// No callback is expected since blocked reasons does not change from
// BLOCKED_REASON_NONE.
wifiCb.assertNoCallback()
}
}
@Test
fun testBlockedReasonsNoInternetPermission_changeDisabled() {
deps.setChangeIdEnabled(false, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission = false)
}
@Test
fun testBlockedReasonsNoInternetPermission_changeEnabled() {
deps.setChangeIdEnabled(true, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission = true)
}
}

View file

@ -37,6 +37,7 @@ import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChange
import com.android.testutils.TestableNetworkCallback
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.InOrder
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.timeout
@ -96,6 +97,11 @@ class CSIngressDiscardRuleTests : CSTest() {
private val LOCAL_IPV6_ADDRRESS = InetAddresses.parseNumericAddress("fe80::1234")
private val LOCAL_IPV6_LINK_ADDRRESS = LinkAddress(LOCAL_IPV6_ADDRRESS, 64)
fun verifyNoMoreIngressDiscardRuleChange(inorder: InOrder) {
inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
inorder.verify(bpfNetMaps, never()).removeIngressDiscardRule(any())
}
@Test
fun testVpnIngressDiscardRule_UpdateVpnAddress() {
// non-VPN network whose address will be not duplicated with VPN address
@ -148,7 +154,7 @@ class CSIngressDiscardRuleTests : CSTest() {
// IngressDiscardRule is added to the VPN address
inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
inorder.verifyNoMoreInteractions()
verifyNoMoreIngressDiscardRuleChange(inorder)
// The VPN interface name is changed
val newlp = lp(VPN_IFNAME2, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
@ -157,7 +163,7 @@ class CSIngressDiscardRuleTests : CSTest() {
// IngressDiscardRule is updated with the new interface name
inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME2)
inorder.verifyNoMoreInteractions()
verifyNoMoreIngressDiscardRuleChange(inorder)
agent.disconnect()
inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS)
@ -206,10 +212,10 @@ class CSIngressDiscardRuleTests : CSTest() {
// IngressDiscardRule for IPV6_ADDRESS2 is removed but IngressDiscardRule for
// IPV6_LINK_ADDRESS is not added since Wi-Fi also uses IPV6_LINK_ADDRESS
inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS2)
inorder.verifyNoMoreInteractions()
verifyNoMoreIngressDiscardRuleChange(inorder)
vpnAgent.disconnect()
inorder.verifyNoMoreInteractions()
verifyNoMoreIngressDiscardRuleChange(inorder)
cm.unregisterNetworkCallback(cb)
}
@ -225,7 +231,7 @@ class CSIngressDiscardRuleTests : CSTest() {
// IngressDiscardRule is added to the VPN address
inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
inorder.verifyNoMoreInteractions()
verifyNoMoreIngressDiscardRuleChange(inorder)
val nr = nr(TRANSPORT_WIFI)
val cb = TestableNetworkCallback()
@ -247,7 +253,7 @@ class CSIngressDiscardRuleTests : CSTest() {
// IngressDiscardRule is added to the VPN address since the VPN address is not duplicated
// with the Wi-Fi address
inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
inorder.verifyNoMoreInteractions()
verifyNoMoreIngressDiscardRuleChange(inorder)
// The Wi-Fi address is changed back to the same address as the VPN interface
wifiAgent.sendLinkProperties(wifiLp)

View file

@ -27,6 +27,7 @@ import android.content.pm.UserInfo
import android.content.res.Resources
import android.net.ConnectivityManager
import android.net.INetd
import android.net.INetd.PERMISSION_INTERNET
import android.net.InetAddresses
import android.net.LinkProperties
import android.net.LocalNetworkConfig
@ -89,6 +90,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.rules.TestName
import org.mockito.AdditionalAnswers.delegatesTo
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
@ -187,7 +189,9 @@ open class CSTest {
val connResources = makeMockConnResources(sysResources, packageManager)
val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val bpfNetMaps = mock<BpfNetMaps>().also {
doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
}
val clatCoordinator = mock<ClatCoordinator>()
val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
val proxyTracker = ProxyTracker(