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
Original-Change: https://android-review.googlesource.com/1717735
Merged-In: Ifa103dd66394026d752b407a1bee740c9fcdad2b
Change-Id: Ifa103dd66394026d752b407a1bee740c9fcdad2b
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index ed9df56..ec71d3d 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -1585,6 +1585,28 @@
}
/**
+ * Compare if the given NetworkCapabilities have the same UIDs.
+ *
+ * @hide
+ */
+ public static boolean hasSameUids(@Nullable NetworkCapabilities nc1,
+ @Nullable NetworkCapabilities nc2) {
+ final Set<UidRange> uids1 = (nc1 == null) ? null : nc1.mUids;
+ final Set<UidRange> 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<UidRange> 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.
* <p>
* This test only checks whether equal range objects are in both sets. It will
@@ -1600,19 +1622,7 @@
*/
@VisibleForTesting
public boolean equalsUids(@NonNull NetworkCapabilities nc) {
- Set<UidRange> 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<UidRange> 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 58cd7cc..55725b5 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -7340,6 +7340,8 @@
mDnsManager.updateTransportsForNetwork(
nai.network.getNetId(), newNc.getTransportTypes());
}
+
+ maybeSendProxyBroadcast(nai, prevNc, newNc);
}
/** Convenience method to update the capabilities for a given network. */
@@ -7432,6 +7434,30 @@
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<UidRange> 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 308b038..89eb2b1 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -484,6 +484,7 @@
@Mock VpnProfileStore mVpnProfileStore;
@Mock SystemConfigManager mSystemConfigManager;
@Mock Resources mResources;
+ @Mock ProxyTracker mProxyTracker;
private ArgumentCaptor<ResolverParamsParcel> mResolverParamsParcelCaptor =
ArgumentCaptor.forClass(ResolverParamsParcel.class);
@@ -1278,10 +1279,14 @@
return mMockNetworkAgent;
}
- public void establish(LinkProperties lp, int uid, Set<UidRange> 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<UidRange> ranges, boolean validated,
+ boolean hasInternet, boolean isStrictMode) throws Exception {
+ setOwnerAndAdminUid(uid);
registerAgent(false, ranges, lp);
connect(validated, hasInternet, isStrictMode);
waitForIdle();
@@ -1636,7 +1641,7 @@
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(
@@ -10380,16 +10385,23 @@
@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<UidRange> vpnRanges = Collections.singleton(vpnRange);
+ final Set<UidRange> 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<UidRange> newRanges = new HashSet<>(Arrays.asList(
new UidRange(vpnRange.start, APP1_UID - 1),
@@ -10399,6 +10411,101 @@
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<UidRange> 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