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