From 53e8a267ab1688ec4e7444340f22a26014af37de Mon Sep 17 00:00:00 2001 From: lucaslin Date: Tue, 8 Jun 2021 01:43:59 +0800 Subject: [PATCH] Send a proxy broadcast when apps moved from/to a VPN When the apps moved from/to a VPN, a proxy broadcast is needed to inform the apps that the proxy might be changed since the default network satisfied by the apps might also changed. Since the framework does not track the defautlt network of every apps, thus, this is done when: 1. VPN connects/disconnects. 2. List of uids that apply to the VPN has changed. While 1 is already covered by the current design, the CL implements 2 in order to fulfill the case that different networks have different proxies. Bug: 178727215 Test: atest FrameworksNetTests Change-Id: Ifa103dd66394026d752b407a1bee740c9fcdad2b --- .../src/android/net/NetworkCapabilities.java | 36 ++++-- .../android/server/ConnectivityService.java | 26 ++++ .../server/ConnectivityServiceTest.java | 119 +++++++++++++++++- 3 files changed, 162 insertions(+), 19 deletions(-) diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java index 90d821bd3b..be9426e988 100644 --- a/framework/src/android/net/NetworkCapabilities.java +++ b/framework/src/android/net/NetworkCapabilities.java @@ -1564,6 +1564,28 @@ public final class NetworkCapabilities implements Parcelable { return false; } + /** + * Compare if the given NetworkCapabilities have the same UIDs. + * + * @hide + */ + public static boolean hasSameUids(@Nullable NetworkCapabilities nc1, + @Nullable NetworkCapabilities nc2) { + final Set uids1 = (nc1 == null) ? null : nc1.mUids; + final Set uids2 = (nc2 == null) ? null : nc2.mUids; + if (null == uids1) return null == uids2; + if (null == uids2) return false; + // Make a copy so it can be mutated to check that all ranges in uids2 also are in uids. + final Set uids = new ArraySet<>(uids2); + for (UidRange range : uids1) { + if (!uids.contains(range)) { + return false; + } + uids.remove(range); + } + return uids.isEmpty(); + } + /** * Tests if the set of UIDs that this network applies to is the same as the passed network. *

@@ -1580,19 +1602,7 @@ public final class NetworkCapabilities implements Parcelable { */ @VisibleForTesting public boolean equalsUids(@NonNull NetworkCapabilities nc) { - Set comparedUids = nc.mUids; - if (null == comparedUids) return null == mUids; - if (null == mUids) return false; - // Make a copy so it can be mutated to check that all ranges in mUids - // also are in uids. - final Set uids = new ArraySet<>(mUids); - for (UidRange range : comparedUids) { - if (!uids.contains(range)) { - return false; - } - uids.remove(range); - } - return uids.isEmpty(); + return hasSameUids(nc, this); } /** diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index aa1648f9e7..0692d40b1e 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -7285,6 +7285,8 @@ public class ConnectivityService extends IConnectivityManager.Stub mDnsManager.updateTransportsForNetwork( nai.network.getNetId(), newNc.getTransportTypes()); } + + maybeSendProxyBroadcast(nai, prevNc, newNc); } /** Convenience method to update the capabilities for a given network. */ @@ -7373,6 +7375,30 @@ public class ConnectivityService extends IConnectivityManager.Stub maybeCloseSockets(nai, ranges, exemptUids); } + private boolean isProxySetOnAnyDefaultNetwork() { + ensureRunningOnConnectivityServiceThread(); + for (final NetworkRequestInfo nri : mDefaultNetworkRequests) { + final NetworkAgentInfo nai = nri.getSatisfier(); + if (nai != null && nai.linkProperties.getHttpProxy() != null) { + return true; + } + } + return false; + } + + private void maybeSendProxyBroadcast(NetworkAgentInfo nai, NetworkCapabilities prevNc, + NetworkCapabilities newNc) { + // When the apps moved from/to a VPN, a proxy broadcast is needed to inform the apps that + // the proxy might be changed since the default network satisfied by the apps might also + // changed. + // TODO: Try to track the default network that apps use and only send a proxy broadcast when + // that happens to prevent false alarms. + if (nai.isVPN() && nai.everConnected && !NetworkCapabilities.hasSameUids(prevNc, newNc) + && (nai.linkProperties.getHttpProxy() != null || isProxySetOnAnyDefaultNetwork())) { + mProxyTracker.sendProxyBroadcast(); + } + } + private void updateUids(NetworkAgentInfo nai, NetworkCapabilities prevNc, NetworkCapabilities newNc) { Set prevRanges = null == prevNc ? null : prevNc.getUidRanges(); diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index 04e63fc626..3a4cc1d77b 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -477,6 +477,7 @@ public class ConnectivityServiceTest { @Mock VpnProfileStore mVpnProfileStore; @Mock SystemConfigManager mSystemConfigManager; @Mock Resources mResources; + @Mock ProxyTracker mProxyTracker; private ArgumentCaptor mResolverParamsParcelCaptor = ArgumentCaptor.forClass(ResolverParamsParcel.class); @@ -1280,10 +1281,14 @@ public class ConnectivityServiceTest { return mMockNetworkAgent; } - public void establish(LinkProperties lp, int uid, Set ranges, boolean validated, - boolean hasInternet, boolean isStrictMode) throws Exception { + private void setOwnerAndAdminUid(int uid) throws Exception { mNetworkCapabilities.setOwnerUid(uid); mNetworkCapabilities.setAdministratorUids(new int[]{uid}); + } + + public void establish(LinkProperties lp, int uid, Set ranges, boolean validated, + boolean hasInternet, boolean isStrictMode) throws Exception { + setOwnerAndAdminUid(uid); registerAgent(false, ranges, lp); connect(validated, hasInternet, isStrictMode); waitForIdle(); @@ -1634,7 +1639,7 @@ public class ConnectivityServiceTest { doReturn(mNetIdManager).when(deps).makeNetIdManager(); doReturn(mNetworkStack).when(deps).getNetworkStack(); doReturn(mSystemProperties).when(deps).getSystemProperties(); - doReturn(mock(ProxyTracker.class)).when(deps).makeProxyTracker(any(), any()); + doReturn(mProxyTracker).when(deps).makeProxyTracker(any(), any()); doReturn(true).when(deps).queryUserAccess(anyInt(), any(), any()); doAnswer(inv -> { mPolicyTracker = new WrappedMultinetworkPolicyTracker( @@ -10092,16 +10097,23 @@ public class ConnectivityServiceTest { @Test public void testVpnUidRangesUpdate() throws Exception { - LinkProperties lp = new LinkProperties(); + // Set up a WiFi network without proxy. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + assertNull(mService.getProxyForNetwork(null)); + assertNull(mCm.getDefaultProxy()); + + final 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)); final UidRange vpnRange = PRIMARY_UIDRANGE; - Set vpnRanges = Collections.singleton(vpnRange); + final Set vpnRanges = Collections.singleton(vpnRange); mMockVpn.establish(lp, VPN_UID, vpnRanges); assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID); + // VPN is connected but proxy is not set, so there is no need to send proxy broadcast. + verify(mProxyTracker, never()).sendProxyBroadcast(); - reset(mMockNetd); // Update to new range which is old range minus APP1, i.e. only APP2 final Set newRanges = new HashSet<>(Arrays.asList( new UidRange(vpnRange.start, APP1_UID - 1), @@ -10111,6 +10123,101 @@ public class ConnectivityServiceTest { assertVpnUidRangesUpdated(true, newRanges, VPN_UID); assertVpnUidRangesUpdated(false, vpnRanges, VPN_UID); + + // Uid has changed but proxy is not set, so there is no need to send proxy broadcast. + verify(mProxyTracker, never()).sendProxyBroadcast(); + + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + lp.setHttpProxy(testProxyInfo); + mMockVpn.sendLinkProperties(lp); + waitForIdle(); + // Proxy is set, so send a proxy broadcast. + verify(mProxyTracker, times(1)).sendProxyBroadcast(); + reset(mProxyTracker); + + mMockVpn.setUids(vpnRanges); + waitForIdle(); + // Uid has changed and proxy is already set, so send a proxy broadcast. + verify(mProxyTracker, times(1)).sendProxyBroadcast(); + reset(mProxyTracker); + + // Proxy is removed, send a proxy broadcast. + lp.setHttpProxy(null); + mMockVpn.sendLinkProperties(lp); + waitForIdle(); + verify(mProxyTracker, times(1)).sendProxyBroadcast(); + reset(mProxyTracker); + + // Proxy is added in WiFi(default network), setDefaultProxy will be called. + final LinkProperties wifiLp = mCm.getLinkProperties(mWiFiNetworkAgent.getNetwork()); + assertNotNull(wifiLp); + wifiLp.setHttpProxy(testProxyInfo); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + waitForIdle(); + verify(mProxyTracker, times(1)).setDefaultProxy(eq(testProxyInfo)); + reset(mProxyTracker); + } + + @Test + public void testProxyBroadcastWillBeSentWhenVpnHasProxyAndConnects() throws Exception { + // Set up a WiFi network without proxy. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + assertNull(mService.getProxyForNetwork(null)); + assertNull(mCm.getDefaultProxy()); + + final 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)); + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + lp.setHttpProxy(testProxyInfo); + final UidRange vpnRange = PRIMARY_UIDRANGE; + final Set vpnRanges = Collections.singleton(vpnRange); + mMockVpn.setOwnerAndAdminUid(VPN_UID); + mMockVpn.registerAgent(false, vpnRanges, lp); + // In any case, the proxy broadcast won't be sent before VPN goes into CONNECTED state. + // Otherwise, the app that calls ConnectivityManager#getDefaultProxy() when it receives the + // proxy broadcast will get null. + verify(mProxyTracker, never()).sendProxyBroadcast(); + mMockVpn.connect(true /* validated */, true /* hasInternet */, false /* isStrictMode */); + waitForIdle(); + assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID); + // Vpn is connected with proxy, so the proxy broadcast will be sent to inform the apps to + // update their proxy data. + verify(mProxyTracker, times(1)).sendProxyBroadcast(); + } + + @Test + public void testProxyBroadcastWillBeSentWhenTheProxyOfNonDefaultNetworkHasChanged() + throws Exception { + // Set up a CELLULAR network without proxy. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + assertNull(mService.getProxyForNetwork(null)); + assertNull(mCm.getDefaultProxy()); + // CELLULAR network should be the default network. + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Set up a WiFi network without proxy. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + assertNull(mService.getProxyForNetwork(null)); + assertNull(mCm.getDefaultProxy()); + // WiFi network should be the default network. + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + // CELLULAR network is not the default network. + assertNotEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // CELLULAR network is not the system default network, but it might be a per-app default + // network. The proxy broadcast should be sent once its proxy has changed. + final LinkProperties cellularLp = new LinkProperties(); + cellularLp.setInterfaceName(MOBILE_IFNAME); + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + cellularLp.setHttpProxy(testProxyInfo); + mCellNetworkAgent.sendLinkProperties(cellularLp); + waitForIdle(); + verify(mProxyTracker, times(1)).sendProxyBroadcast(); } @Test