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