1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.net; 18 19 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; 20 import static android.net.NetworkCapabilities.TRANSPORT_WIFI; 21 import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN; 22 23 import static org.mockito.ArgumentMatchers.any; 24 import static org.mockito.ArgumentMatchers.argThat; 25 import static org.mockito.ArgumentMatchers.eq; 26 import static org.mockito.Mockito.clearInvocations; 27 import static org.mockito.Mockito.doReturn; 28 import static org.mockito.Mockito.never; 29 import static org.mockito.Mockito.verify; 30 31 import android.app.Notification; 32 import android.app.NotificationManager; 33 import android.content.Context; 34 import android.content.ContextWrapper; 35 import android.net.ConnectivityManager; 36 import android.net.ConnectivityManager.NetworkCallback; 37 import android.net.LinkAddress; 38 import android.net.LinkProperties; 39 import android.net.Network; 40 import android.net.NetworkCapabilities; 41 import android.net.NetworkInfo; 42 import android.os.Handler; 43 import android.os.HandlerThread; 44 import android.text.TextUtils; 45 46 import androidx.test.InstrumentationRegistry; 47 import androidx.test.ext.junit.runners.AndroidJUnit4; 48 import androidx.test.filters.SmallTest; 49 50 import com.android.internal.R; 51 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 52 import com.android.internal.net.VpnConfig; 53 import com.android.internal.net.VpnProfile; 54 import com.android.server.connectivity.Vpn; 55 56 import org.junit.After; 57 import org.junit.Before; 58 import org.junit.Test; 59 import org.junit.runner.RunWith; 60 import org.mockito.ArgumentCaptor; 61 import org.mockito.Mock; 62 import org.mockito.MockitoAnnotations; 63 64 import java.util.ArrayList; 65 66 @RunWith(AndroidJUnit4.class) 67 @SmallTest 68 public class LockdownVpnTrackerTest { 69 private static final NetworkCapabilities TEST_CELL_NC = new NetworkCapabilities.Builder() 70 .addTransportType(TRANSPORT_CELLULAR) 71 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) 72 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) 73 .build(); 74 private static final LinkProperties TEST_CELL_LP = new LinkProperties(); 75 76 static { 77 TEST_CELL_LP.setInterfaceName("rmnet0"); TEST_CELL_LP.addLinkAddress(new LinkAddress("192.0.2.2/25"))78 TEST_CELL_LP.addLinkAddress(new LinkAddress("192.0.2.2/25")); 79 } 80 81 // Use a context wrapper instead of a mock since LockdownVpnTracker builds notifications which 82 // is tedious and currently unnecessary to mock. 83 private final Context mContext = new ContextWrapper(InstrumentationRegistry.getContext()) { 84 @Override 85 public Object getSystemService(String name) { 86 if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm; 87 if (Context.NOTIFICATION_SERVICE.equals(name)) return mNotificationManager; 88 89 return super.getSystemService(name); 90 } 91 }; 92 @Mock private ConnectivityManager mCm; 93 @Mock private Vpn mVpn; 94 @Mock private NotificationManager mNotificationManager; 95 @Mock private NetworkInfo mVpnNetworkInfo; 96 @Mock private VpnConfig mVpnConfig; 97 @Mock private Network mNetwork; 98 @Mock private Network mNetwork2; 99 @Mock private Network mVpnNetwork; 100 101 private HandlerThread mHandlerThread; 102 private Handler mHandler; 103 private VpnProfile mProfile; 104 createTestVpnProfile()105 private VpnProfile createTestVpnProfile() { 106 final String profileName = "testVpnProfile"; 107 final VpnProfile profile = new VpnProfile(profileName); 108 profile.name = "My VPN"; 109 profile.server = "192.0.2.1"; 110 profile.dnsServers = "8.8.8.8"; 111 profile.ipsecIdentifier = "My ipsecIdentifier"; 112 profile.ipsecSecret = "My PSK"; 113 profile.type = VpnProfile.TYPE_IKEV2_IPSEC_PSK; 114 115 return profile; 116 } 117 getDefaultNetworkCallback()118 private NetworkCallback getDefaultNetworkCallback() { 119 final ArgumentCaptor<NetworkCallback> callbackCaptor = 120 ArgumentCaptor.forClass(NetworkCallback.class); 121 verify(mCm).registerSystemDefaultNetworkCallback(callbackCaptor.capture(), eq(mHandler)); 122 return callbackCaptor.getValue(); 123 } 124 getVpnNetworkCallback()125 private NetworkCallback getVpnNetworkCallback() { 126 final ArgumentCaptor<NetworkCallback> callbackCaptor = 127 ArgumentCaptor.forClass(NetworkCallback.class); 128 verify(mCm).registerNetworkCallback(any(), callbackCaptor.capture(), eq(mHandler)); 129 return callbackCaptor.getValue(); 130 } 131 132 @Before setUp()133 public void setUp() throws Exception { 134 MockitoAnnotations.initMocks(this); 135 136 mHandlerThread = new HandlerThread("LockdownVpnTrackerTest"); 137 mHandlerThread.start(); 138 mHandler = mHandlerThread.getThreadHandler(); 139 140 doReturn(mVpnNetworkInfo).when(mVpn).getNetworkInfo(); 141 doReturn(false).when(mVpnNetworkInfo).isConnectedOrConnecting(); 142 doReturn(mVpnConfig).when(mVpn).getLegacyVpnConfig(); 143 // mVpnConfig is a mock but the production code will try to add addresses in this array 144 // assuming it's non-null, so it needs to be initialized. 145 mVpnConfig.addresses = new ArrayList<>(); 146 147 mProfile = createTestVpnProfile(); 148 } 149 150 @After tearDown()151 public void tearDown() throws Exception { 152 if (mHandlerThread != null) { 153 mHandlerThread.quitSafely(); 154 mHandlerThread.join(); 155 } 156 } 157 initAndVerifyLockdownVpnTracker()158 private LockdownVpnTracker initAndVerifyLockdownVpnTracker() { 159 final LockdownVpnTracker lockdownVpnTracker = 160 new LockdownVpnTracker(mContext, mHandler, mVpn, mProfile); 161 lockdownVpnTracker.init(); 162 verify(mVpn).setEnableTeardown(false); 163 verify(mVpn).setLockdown(true); 164 verify(mCm).setLegacyLockdownVpnEnabled(true); 165 verify(mVpn).stopVpnRunnerPrivileged(); 166 verify(mNotificationManager).cancel(any(), eq(SystemMessage.NOTE_VPN_STATUS)); 167 168 return lockdownVpnTracker; 169 } 170 callCallbacksForNetworkConnect(NetworkCallback callback, Network network, NetworkCapabilities nc, LinkProperties lp, boolean blocked)171 private void callCallbacksForNetworkConnect(NetworkCallback callback, Network network, 172 NetworkCapabilities nc, LinkProperties lp, boolean blocked) { 173 callback.onAvailable(network); 174 callback.onCapabilitiesChanged(network, nc); 175 callback.onLinkPropertiesChanged(network, lp); 176 callback.onBlockedStatusChanged(network, blocked); 177 } 178 callCallbacksForNetworkConnect(NetworkCallback callback, Network network)179 private void callCallbacksForNetworkConnect(NetworkCallback callback, Network network) { 180 callCallbacksForNetworkConnect( 181 callback, network, TEST_CELL_NC, TEST_CELL_LP, true /* blocked */); 182 } 183 isExpectedNotification(Notification notification, int titleRes, int iconRes)184 private boolean isExpectedNotification(Notification notification, int titleRes, int iconRes) { 185 if (!NOTIFICATION_CHANNEL_VPN.equals(notification.getChannelId())) { 186 return false; 187 } 188 final CharSequence expectedTitle = mContext.getString(titleRes); 189 final CharSequence actualTitle = notification.extras.getCharSequence( 190 Notification.EXTRA_TITLE); 191 if (!TextUtils.equals(expectedTitle, actualTitle)) { 192 return false; 193 } 194 return notification.getSmallIcon().getResId() == iconRes; 195 } 196 197 @Test testShutdown()198 public void testShutdown() { 199 final LockdownVpnTracker lockdownVpnTracker = initAndVerifyLockdownVpnTracker(); 200 final NetworkCallback defaultCallback = getDefaultNetworkCallback(); 201 final NetworkCallback vpnCallback = getVpnNetworkCallback(); 202 clearInvocations(mVpn, mCm, mNotificationManager); 203 204 lockdownVpnTracker.shutdown(); 205 verify(mVpn).stopVpnRunnerPrivileged(); 206 verify(mVpn).setLockdown(false); 207 verify(mCm).setLegacyLockdownVpnEnabled(false); 208 verify(mNotificationManager).cancel(any(), eq(SystemMessage.NOTE_VPN_STATUS)); 209 verify(mVpn).setEnableTeardown(true); 210 verify(mCm).unregisterNetworkCallback(defaultCallback); 211 verify(mCm).unregisterNetworkCallback(vpnCallback); 212 } 213 214 @Test testDefaultNetworkConnected()215 public void testDefaultNetworkConnected() { 216 initAndVerifyLockdownVpnTracker(); 217 final NetworkCallback defaultCallback = getDefaultNetworkCallback(); 218 clearInvocations(mVpn, mCm, mNotificationManager); 219 220 // mNetwork connected and available. 221 callCallbacksForNetworkConnect(defaultCallback, mNetwork); 222 223 // Vpn is starting 224 verify(mVpn).startLegacyVpnPrivileged(mProfile); 225 verify(mNotificationManager).notify(any(), eq(SystemMessage.NOTE_VPN_STATUS), 226 argThat(notification -> isExpectedNotification(notification, 227 R.string.vpn_lockdown_connecting, R.drawable.vpn_disconnected))); 228 } 229 doTestDefaultLpChanged(LinkProperties startingLp, LinkProperties newLp)230 private void doTestDefaultLpChanged(LinkProperties startingLp, LinkProperties newLp) { 231 initAndVerifyLockdownVpnTracker(); 232 final NetworkCallback defaultCallback = getDefaultNetworkCallback(); 233 callCallbacksForNetworkConnect( 234 defaultCallback, mNetwork, TEST_CELL_NC, startingLp, true /* blocked */); 235 clearInvocations(mVpn, mCm, mNotificationManager); 236 237 // LockdownVpnTracker#handleStateChangedLocked() is not called on the same network even if 238 // the LinkProperties change. 239 defaultCallback.onLinkPropertiesChanged(mNetwork, newLp); 240 241 // Ideally the VPN should start if it hasn't already, but it doesn't because nothing calls 242 // LockdownVpnTracker#handleStateChangedLocked. This is a bug. 243 // TODO: consider fixing this. 244 verify(mVpn, never()).stopVpnRunnerPrivileged(); 245 verify(mVpn, never()).startLegacyVpnPrivileged(any()); 246 verify(mNotificationManager, never()).cancel(any(), eq(SystemMessage.NOTE_VPN_STATUS)); 247 } 248 249 @Test testDefaultLPChanged_V4AddLinkAddressV4()250 public void testDefaultLPChanged_V4AddLinkAddressV4() { 251 final LinkProperties lp = new LinkProperties(TEST_CELL_LP); 252 lp.setInterfaceName("rmnet0"); 253 lp.addLinkAddress(new LinkAddress("192.0.2.3/25")); 254 doTestDefaultLpChanged(TEST_CELL_LP, lp); 255 } 256 257 @Test testDefaultLPChanged_V4AddLinkAddressV6()258 public void testDefaultLPChanged_V4AddLinkAddressV6() { 259 final LinkProperties lp = new LinkProperties(); 260 lp.setInterfaceName("rmnet0"); 261 lp.addLinkAddress(new LinkAddress("192.0.2.3/25")); 262 final LinkProperties newLp = new LinkProperties(lp); 263 newLp.addLinkAddress(new LinkAddress("2001:db8::1/64")); 264 doTestDefaultLpChanged(lp, newLp); 265 } 266 267 @Test testDefaultLPChanged_V6AddLinkAddressV4()268 public void testDefaultLPChanged_V6AddLinkAddressV4() { 269 final LinkProperties lp = new LinkProperties(); 270 lp.setInterfaceName("rmnet0"); 271 lp.addLinkAddress(new LinkAddress("2001:db8::1/64")); 272 final LinkProperties newLp = new LinkProperties(lp); 273 newLp.addLinkAddress(new LinkAddress("192.0.2.3/25")); 274 doTestDefaultLpChanged(lp, newLp); 275 } 276 277 @Test testDefaultLPChanged_AddLinkAddressV4()278 public void testDefaultLPChanged_AddLinkAddressV4() { 279 final LinkProperties lp = new LinkProperties(); 280 lp.setInterfaceName("rmnet0"); 281 doTestDefaultLpChanged(lp, TEST_CELL_LP); 282 } 283 284 @Test testDefaultNetworkChanged()285 public void testDefaultNetworkChanged() { 286 initAndVerifyLockdownVpnTracker(); 287 final NetworkCallback defaultCallback = getDefaultNetworkCallback(); 288 final NetworkCallback vpnCallback = getVpnNetworkCallback(); 289 callCallbacksForNetworkConnect(defaultCallback, mNetwork); 290 clearInvocations(mVpn, mCm, mNotificationManager); 291 292 // New network and LinkProperties received 293 final NetworkCapabilities wifiNc = new NetworkCapabilities.Builder() 294 .addTransportType(TRANSPORT_WIFI) 295 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) 296 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) 297 .build(); 298 final LinkProperties wifiLp = new LinkProperties(); 299 wifiLp.setInterfaceName("wlan0"); 300 callCallbacksForNetworkConnect( 301 defaultCallback, mNetwork2, wifiNc, wifiLp, true /* blocked */); 302 303 // Vpn is restarted. 304 verify(mVpn).stopVpnRunnerPrivileged(); 305 verify(mVpn).startLegacyVpnPrivileged(mProfile); 306 verify(mNotificationManager, never()).cancel(any(), eq(SystemMessage.NOTE_VPN_STATUS)); 307 verify(mNotificationManager).notify(any(), eq(SystemMessage.NOTE_VPN_STATUS), 308 argThat(notification -> isExpectedNotification(notification, 309 R.string.vpn_lockdown_connecting, R.drawable.vpn_disconnected))); 310 311 // Vpn is Connected 312 doReturn(true).when(mVpnNetworkInfo).isConnectedOrConnecting(); 313 doReturn(true).when(mVpnNetworkInfo).isConnected(); 314 vpnCallback.onAvailable(mVpnNetwork); 315 verify(mNotificationManager).notify(any(), eq(SystemMessage.NOTE_VPN_STATUS), 316 argThat(notification -> isExpectedNotification(notification, 317 R.string.vpn_lockdown_connected, R.drawable.vpn_connected))); 318 319 } 320 321 @Test testSystemDefaultLost()322 public void testSystemDefaultLost() { 323 initAndVerifyLockdownVpnTracker(); 324 final NetworkCallback defaultCallback = getDefaultNetworkCallback(); 325 // mNetwork connected 326 callCallbacksForNetworkConnect(defaultCallback, mNetwork); 327 clearInvocations(mVpn, mCm, mNotificationManager); 328 329 defaultCallback.onLost(mNetwork); 330 331 // Vpn is stopped 332 verify(mVpn).stopVpnRunnerPrivileged(); 333 verify(mNotificationManager).cancel(any(), eq(SystemMessage.NOTE_VPN_STATUS)); 334 } 335 } 336