1 /* 2 * Copyright (C) 2011 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.settings.vpn2; 18 19 import static android.app.AppOpsManager.OP_ACTIVATE_VPN; 20 21 import android.annotation.UiThread; 22 import android.annotation.WorkerThread; 23 import android.app.Activity; 24 import android.app.AppOpsManager; 25 import android.app.settings.SettingsEnums; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageInfo; 29 import android.content.pm.PackageManager; 30 import android.net.ConnectivityManager; 31 import android.net.ConnectivityManager.NetworkCallback; 32 import android.net.IConnectivityManager; 33 import android.net.Network; 34 import android.net.NetworkCapabilities; 35 import android.net.NetworkRequest; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.HandlerThread; 39 import android.os.Message; 40 import android.os.RemoteException; 41 import android.os.ServiceManager; 42 import android.os.UserHandle; 43 import android.os.UserManager; 44 import android.security.Credentials; 45 import android.security.KeyStore; 46 import android.util.ArrayMap; 47 import android.util.ArraySet; 48 import android.util.Log; 49 import android.view.Menu; 50 import android.view.MenuInflater; 51 import android.view.MenuItem; 52 53 import androidx.annotation.VisibleForTesting; 54 import androidx.preference.Preference; 55 import androidx.preference.PreferenceGroup; 56 57 import com.android.internal.annotations.GuardedBy; 58 import com.android.internal.net.LegacyVpnInfo; 59 import com.android.internal.net.VpnConfig; 60 import com.android.internal.net.VpnProfile; 61 import com.android.internal.util.ArrayUtils; 62 import com.android.settings.R; 63 import com.android.settings.RestrictedSettingsFragment; 64 import com.android.settings.widget.GearPreference; 65 import com.android.settings.widget.GearPreference.OnGearClickListener; 66 import com.android.settingslib.RestrictedLockUtilsInternal; 67 68 import com.google.android.collect.Lists; 69 70 import java.util.ArrayList; 71 import java.util.Collection; 72 import java.util.Collections; 73 import java.util.List; 74 import java.util.Map; 75 import java.util.Set; 76 77 /** 78 * Settings screen listing VPNs. Configured VPNs and networks managed by apps 79 * are shown in the same list. 80 */ 81 public class VpnSettings extends RestrictedSettingsFragment implements 82 Handler.Callback, Preference.OnPreferenceClickListener { 83 private static final String LOG_TAG = "VpnSettings"; 84 85 private static final int RESCAN_MESSAGE = 0; 86 private static final int RESCAN_INTERVAL_MS = 1000; 87 88 private static final NetworkRequest VPN_REQUEST = new NetworkRequest.Builder() 89 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 90 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) 91 .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) 92 .build(); 93 94 private final IConnectivityManager mConnectivityService = IConnectivityManager.Stub 95 .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); 96 private ConnectivityManager mConnectivityManager; 97 private UserManager mUserManager; 98 99 private final KeyStore mKeyStore = KeyStore.getInstance(); 100 101 private Map<String, LegacyVpnPreference> mLegacyVpnPreferences = new ArrayMap<>(); 102 private Map<AppVpnInfo, AppPreference> mAppPreferences = new ArrayMap<>(); 103 104 @GuardedBy("this") 105 private Handler mUpdater; 106 private HandlerThread mUpdaterThread; 107 private LegacyVpnInfo mConnectedLegacyVpn; 108 109 private boolean mUnavailable; 110 VpnSettings()111 public VpnSettings() { 112 super(UserManager.DISALLOW_CONFIG_VPN); 113 } 114 115 @Override getMetricsCategory()116 public int getMetricsCategory() { 117 return SettingsEnums.VPN; 118 } 119 120 @Override onActivityCreated(Bundle savedInstanceState)121 public void onActivityCreated(Bundle savedInstanceState) { 122 super.onActivityCreated(savedInstanceState); 123 124 mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); 125 mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 126 127 mUnavailable = isUiRestricted(); 128 setHasOptionsMenu(!mUnavailable); 129 130 addPreferencesFromResource(R.xml.vpn_settings2); 131 } 132 133 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)134 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 135 super.onCreateOptionsMenu(menu, inflater); 136 inflater.inflate(R.menu.vpn, menu); 137 } 138 139 @Override onPrepareOptionsMenu(Menu menu)140 public void onPrepareOptionsMenu(Menu menu) { 141 super.onPrepareOptionsMenu(menu); 142 143 // Disable all actions if VPN configuration has been disallowed 144 for (int i = 0; i < menu.size(); i++) { 145 if (isUiRestrictedByOnlyAdmin()) { 146 RestrictedLockUtilsInternal.setMenuItemAsDisabledByAdmin(getPrefContext(), 147 menu.getItem(i), getRestrictionEnforcedAdmin()); 148 } else { 149 menu.getItem(i).setEnabled(!mUnavailable); 150 } 151 } 152 } 153 154 @Override onOptionsItemSelected(MenuItem item)155 public boolean onOptionsItemSelected(MenuItem item) { 156 switch (item.getItemId()) { 157 case R.id.vpn_create: { 158 // Generate a new key. Here we just use the current time. 159 long millis = System.currentTimeMillis(); 160 while (mLegacyVpnPreferences.containsKey(Long.toHexString(millis))) { 161 ++millis; 162 } 163 VpnProfile profile = new VpnProfile(Long.toHexString(millis)); 164 ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */); 165 return true; 166 } 167 } 168 return super.onOptionsItemSelected(item); 169 } 170 171 @Override onResume()172 public void onResume() { 173 super.onResume(); 174 175 mUnavailable = mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN); 176 if (mUnavailable) { 177 // Show a message to explain that VPN settings have been disabled 178 if (!isUiRestrictedByOnlyAdmin()) { 179 getEmptyTextView().setText(R.string.vpn_settings_not_available); 180 } 181 getPreferenceScreen().removeAll(); 182 return; 183 } else { 184 setEmptyView(getEmptyTextView()); 185 getEmptyTextView().setText(R.string.vpn_no_vpns_added); 186 } 187 188 // Start monitoring 189 mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback); 190 191 // Trigger a refresh 192 mUpdaterThread = new HandlerThread("Refresh VPN list in background"); 193 mUpdaterThread.start(); 194 mUpdater = new Handler(mUpdaterThread.getLooper(), this); 195 mUpdater.sendEmptyMessage(RESCAN_MESSAGE); 196 } 197 198 @Override onPause()199 public void onPause() { 200 if (mUnavailable) { 201 super.onPause(); 202 return; 203 } 204 205 // Stop monitoring 206 mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); 207 208 synchronized (this) { 209 mUpdater.removeCallbacksAndMessages(null); 210 mUpdater = null; 211 mUpdaterThread.quit(); 212 mUpdaterThread = null; 213 } 214 215 super.onPause(); 216 } 217 218 @Override @WorkerThread handleMessage(Message message)219 public boolean handleMessage(Message message) { 220 //Return if activity has been recycled 221 final Activity activity = getActivity(); 222 if (activity == null) { 223 return true; 224 } 225 final Context context = activity.getApplicationContext(); 226 227 // Run heavy RPCs before switching to UI thread 228 final List<VpnProfile> vpnProfiles = loadVpnProfiles(mKeyStore); 229 final List<AppVpnInfo> vpnApps = getVpnApps(context, /* includeProfiles */ true); 230 231 final Map<String, LegacyVpnInfo> connectedLegacyVpns = getConnectedLegacyVpns(); 232 final Set<AppVpnInfo> connectedAppVpns = getConnectedAppVpns(); 233 234 final Set<AppVpnInfo> alwaysOnAppVpnInfos = getAlwaysOnAppVpnInfos(); 235 final String lockdownVpnKey = VpnUtils.getLockdownVpn(); 236 237 // Refresh list of VPNs 238 activity.runOnUiThread(new UpdatePreferences(this) 239 .legacyVpns(vpnProfiles, connectedLegacyVpns, lockdownVpnKey) 240 .appVpns(vpnApps, connectedAppVpns, alwaysOnAppVpnInfos)); 241 242 synchronized (this) { 243 if (mUpdater != null) { 244 mUpdater.removeMessages(RESCAN_MESSAGE); 245 mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS); 246 } 247 } 248 return true; 249 } 250 251 @VisibleForTesting 252 static class UpdatePreferences implements Runnable { 253 private List<VpnProfile> vpnProfiles = Collections.<VpnProfile>emptyList(); 254 private List<AppVpnInfo> vpnApps = Collections.<AppVpnInfo>emptyList(); 255 256 private Map<String, LegacyVpnInfo> connectedLegacyVpns = 257 Collections.<String, LegacyVpnInfo>emptyMap(); 258 private Set<AppVpnInfo> connectedAppVpns = Collections.<AppVpnInfo>emptySet(); 259 260 private Set<AppVpnInfo> alwaysOnAppVpnInfos = Collections.<AppVpnInfo>emptySet(); 261 private String lockdownVpnKey = null; 262 263 private final VpnSettings mSettings; 264 UpdatePreferences(VpnSettings settings)265 public UpdatePreferences(VpnSettings settings) { 266 mSettings = settings; 267 } 268 legacyVpns(List<VpnProfile> vpnProfiles, Map<String, LegacyVpnInfo> connectedLegacyVpns, String lockdownVpnKey)269 public final UpdatePreferences legacyVpns(List<VpnProfile> vpnProfiles, 270 Map<String, LegacyVpnInfo> connectedLegacyVpns, String lockdownVpnKey) { 271 this.vpnProfiles = vpnProfiles; 272 this.connectedLegacyVpns = connectedLegacyVpns; 273 this.lockdownVpnKey = lockdownVpnKey; 274 return this; 275 } 276 appVpns(List<AppVpnInfo> vpnApps, Set<AppVpnInfo> connectedAppVpns, Set<AppVpnInfo> alwaysOnAppVpnInfos)277 public final UpdatePreferences appVpns(List<AppVpnInfo> vpnApps, 278 Set<AppVpnInfo> connectedAppVpns, Set<AppVpnInfo> alwaysOnAppVpnInfos) { 279 this.vpnApps = vpnApps; 280 this.connectedAppVpns = connectedAppVpns; 281 this.alwaysOnAppVpnInfos = alwaysOnAppVpnInfos; 282 return this; 283 } 284 285 @Override @UiThread run()286 public void run() { 287 if (!mSettings.canAddPreferences()) { 288 return; 289 } 290 291 // Find new VPNs by subtracting existing ones from the full set 292 final Set<Preference> updates = new ArraySet<>(); 293 294 // Add legacy VPNs 295 for (VpnProfile profile : vpnProfiles) { 296 LegacyVpnPreference p = mSettings.findOrCreatePreference(profile, true); 297 if (connectedLegacyVpns.containsKey(profile.key)) { 298 p.setState(connectedLegacyVpns.get(profile.key).state); 299 } else { 300 p.setState(LegacyVpnPreference.STATE_NONE); 301 } 302 p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(profile.key)); 303 updates.add(p); 304 } 305 306 // Show connected VPNs even if the original entry in keystore is gone 307 for (LegacyVpnInfo vpn : connectedLegacyVpns.values()) { 308 final VpnProfile stubProfile = new VpnProfile(vpn.key); 309 LegacyVpnPreference p = mSettings.findOrCreatePreference(stubProfile, false); 310 p.setState(vpn.state); 311 p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(vpn.key)); 312 updates.add(p); 313 } 314 315 // Add VpnService VPNs 316 for (AppVpnInfo app : vpnApps) { 317 AppPreference p = mSettings.findOrCreatePreference(app); 318 if (connectedAppVpns.contains(app)) { 319 p.setState(AppPreference.STATE_CONNECTED); 320 } else { 321 p.setState(AppPreference.STATE_DISCONNECTED); 322 } 323 p.setAlwaysOn(alwaysOnAppVpnInfos.contains(app)); 324 updates.add(p); 325 } 326 327 // Trim out deleted VPN preferences 328 mSettings.setShownPreferences(updates); 329 } 330 } 331 332 @VisibleForTesting canAddPreferences()333 public boolean canAddPreferences() { 334 return isAdded(); 335 } 336 337 @VisibleForTesting @UiThread setShownPreferences(final Collection<Preference> updates)338 public void setShownPreferences(final Collection<Preference> updates) { 339 mLegacyVpnPreferences.values().retainAll(updates); 340 mAppPreferences.values().retainAll(updates); 341 342 // Change {@param updates} in-place to only contain new preferences that were not already 343 // added to the preference screen. 344 final PreferenceGroup vpnGroup = getPreferenceScreen(); 345 for (int i = vpnGroup.getPreferenceCount() - 1; i >= 0; i--) { 346 Preference p = vpnGroup.getPreference(i); 347 if (updates.contains(p)) { 348 updates.remove(p); 349 } else { 350 vpnGroup.removePreference(p); 351 } 352 } 353 354 // Show any new preferences on the screen 355 for (Preference pref : updates) { 356 vpnGroup.addPreference(pref); 357 } 358 } 359 360 @Override onPreferenceClick(Preference preference)361 public boolean onPreferenceClick(Preference preference) { 362 if (preference instanceof LegacyVpnPreference) { 363 LegacyVpnPreference pref = (LegacyVpnPreference) preference; 364 VpnProfile profile = pref.getProfile(); 365 if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) && 366 mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) { 367 try { 368 mConnectedLegacyVpn.intent.send(); 369 return true; 370 } catch (Exception e) { 371 Log.w(LOG_TAG, "Starting config intent failed", e); 372 } 373 } 374 ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */); 375 return true; 376 } else if (preference instanceof AppPreference) { 377 AppPreference pref = (AppPreference) preference; 378 boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED); 379 380 if (!connected) { 381 try { 382 UserHandle user = UserHandle.of(pref.getUserId()); 383 Context userContext = getActivity().createPackageContextAsUser( 384 getActivity().getPackageName(), 0 /* flags */, user); 385 PackageManager pm = userContext.getPackageManager(); 386 Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName()); 387 if (appIntent != null) { 388 userContext.startActivityAsUser(appIntent, user); 389 return true; 390 } 391 } catch (PackageManager.NameNotFoundException nnfe) { 392 Log.w(LOG_TAG, "VPN provider does not exist: " + pref.getPackageName(), nnfe); 393 } 394 } 395 396 // Already connected or no launch intent available - show an info dialog 397 PackageInfo pkgInfo = pref.getPackageInfo(); 398 AppDialogFragment.show(this, pkgInfo, pref.getLabel(), false /* editing */, connected); 399 return true; 400 } 401 return false; 402 } 403 404 @Override getHelpResource()405 public int getHelpResource() { 406 return R.string.help_url_vpn; 407 } 408 409 private OnGearClickListener mGearListener = new OnGearClickListener() { 410 @Override 411 public void onGearClick(GearPreference p) { 412 if (p instanceof LegacyVpnPreference) { 413 LegacyVpnPreference pref = (LegacyVpnPreference) p; 414 ConfigDialogFragment.show(VpnSettings.this, pref.getProfile(), true /* editing */, 415 true /* exists */); 416 } else if (p instanceof AppPreference) { 417 AppPreference pref = (AppPreference) p; 418 AppManagementFragment.show(getPrefContext(), pref, getMetricsCategory()); 419 } 420 } 421 }; 422 423 private NetworkCallback mNetworkCallback = new NetworkCallback() { 424 @Override 425 public void onAvailable(Network network) { 426 if (mUpdater != null) { 427 mUpdater.sendEmptyMessage(RESCAN_MESSAGE); 428 } 429 } 430 431 @Override 432 public void onLost(Network network) { 433 if (mUpdater != null) { 434 mUpdater.sendEmptyMessage(RESCAN_MESSAGE); 435 } 436 } 437 }; 438 439 @VisibleForTesting @UiThread findOrCreatePreference(VpnProfile profile, boolean update)440 public LegacyVpnPreference findOrCreatePreference(VpnProfile profile, boolean update) { 441 LegacyVpnPreference pref = mLegacyVpnPreferences.get(profile.key); 442 boolean created = false; 443 if (pref == null ) { 444 pref = new LegacyVpnPreference(getPrefContext()); 445 pref.setOnGearClickListener(mGearListener); 446 pref.setOnPreferenceClickListener(this); 447 mLegacyVpnPreferences.put(profile.key, pref); 448 created = true; 449 } 450 if (created || update) { 451 // This can change call-to-call because the profile can update and keep the same key. 452 pref.setProfile(profile); 453 } 454 return pref; 455 } 456 457 @VisibleForTesting @UiThread findOrCreatePreference(AppVpnInfo app)458 public AppPreference findOrCreatePreference(AppVpnInfo app) { 459 AppPreference pref = mAppPreferences.get(app); 460 if (pref == null) { 461 pref = new AppPreference(getPrefContext(), app.userId, app.packageName); 462 pref.setOnGearClickListener(mGearListener); 463 pref.setOnPreferenceClickListener(this); 464 mAppPreferences.put(app, pref); 465 } 466 return pref; 467 } 468 469 @WorkerThread getConnectedLegacyVpns()470 private Map<String, LegacyVpnInfo> getConnectedLegacyVpns() { 471 try { 472 mConnectedLegacyVpn = mConnectivityService.getLegacyVpnInfo(UserHandle.myUserId()); 473 if (mConnectedLegacyVpn != null) { 474 return Collections.singletonMap(mConnectedLegacyVpn.key, mConnectedLegacyVpn); 475 } 476 } catch (RemoteException e) { 477 Log.e(LOG_TAG, "Failure updating VPN list with connected legacy VPNs", e); 478 } 479 return Collections.emptyMap(); 480 } 481 482 @WorkerThread getConnectedAppVpns()483 private Set<AppVpnInfo> getConnectedAppVpns() { 484 // Mark connected third-party services 485 Set<AppVpnInfo> connections = new ArraySet<>(); 486 try { 487 for (UserHandle profile : mUserManager.getUserProfiles()) { 488 VpnConfig config = mConnectivityService.getVpnConfig(profile.getIdentifier()); 489 if (config != null && !config.legacy) { 490 connections.add(new AppVpnInfo(profile.getIdentifier(), config.user)); 491 } 492 } 493 } catch (RemoteException e) { 494 Log.e(LOG_TAG, "Failure updating VPN list with connected app VPNs", e); 495 } 496 return connections; 497 } 498 499 @WorkerThread getAlwaysOnAppVpnInfos()500 private Set<AppVpnInfo> getAlwaysOnAppVpnInfos() { 501 Set<AppVpnInfo> result = new ArraySet<>(); 502 for (UserHandle profile : mUserManager.getUserProfiles()) { 503 final int profileId = profile.getIdentifier(); 504 final String packageName = mConnectivityManager.getAlwaysOnVpnPackageForUser(profileId); 505 if (packageName != null) { 506 result.add(new AppVpnInfo(profileId, packageName)); 507 } 508 } 509 return result; 510 } 511 getVpnApps(Context context, boolean includeProfiles)512 static List<AppVpnInfo> getVpnApps(Context context, boolean includeProfiles) { 513 List<AppVpnInfo> result = Lists.newArrayList(); 514 515 final Set<Integer> profileIds; 516 if (includeProfiles) { 517 profileIds = new ArraySet<>(); 518 for (UserHandle profile : UserManager.get(context).getUserProfiles()) { 519 profileIds.add(profile.getIdentifier()); 520 } 521 } else { 522 profileIds = Collections.singleton(UserHandle.myUserId()); 523 } 524 525 // Fetch VPN-enabled apps from AppOps. 526 AppOpsManager aom = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 527 List<AppOpsManager.PackageOps> apps = aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN}); 528 if (apps != null) { 529 for (AppOpsManager.PackageOps pkg : apps) { 530 int userId = UserHandle.getUserId(pkg.getUid()); 531 if (!profileIds.contains(userId)) { 532 // Skip packages for users outside of our profile group. 533 continue; 534 } 535 // Look for a MODE_ALLOWED permission to activate VPN. 536 boolean allowed = false; 537 for (AppOpsManager.OpEntry op : pkg.getOps()) { 538 if (op.getOp() == OP_ACTIVATE_VPN && 539 op.getMode() == AppOpsManager.MODE_ALLOWED) { 540 allowed = true; 541 } 542 } 543 if (allowed) { 544 result.add(new AppVpnInfo(userId, pkg.getPackageName())); 545 } 546 } 547 } 548 549 Collections.sort(result); 550 return result; 551 } 552 loadVpnProfiles(KeyStore keyStore, int... excludeTypes)553 static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) { 554 final ArrayList<VpnProfile> result = Lists.newArrayList(); 555 556 for (String key : keyStore.list(Credentials.VPN)) { 557 final VpnProfile profile = VpnProfile.decode(key, keyStore.get(Credentials.VPN + key)); 558 if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) { 559 result.add(profile); 560 } 561 } 562 return result; 563 } 564 } 565