1 /*
2  * Copyright (C) 2012 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.Manifest.permission.NETWORK_STACK;
20 import static android.provider.Settings.ACTION_VPN_SETTINGS;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.Notification;
25 import android.app.NotificationManager;
26 import android.app.PendingIntent;
27 import android.content.BroadcastReceiver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.net.ConnectivityManager;
32 import android.net.LinkAddress;
33 import android.net.LinkProperties;
34 import android.net.NetworkInfo;
35 import android.net.NetworkInfo.DetailedState;
36 import android.net.NetworkInfo.State;
37 import android.os.Handler;
38 import android.security.Credentials;
39 import android.security.KeyStore;
40 import android.text.TextUtils;
41 import android.util.Slog;
42 
43 import com.android.internal.R;
44 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
45 import com.android.internal.net.VpnConfig;
46 import com.android.internal.net.VpnProfile;
47 import com.android.internal.notification.SystemNotificationChannels;
48 import com.android.server.ConnectivityService;
49 import com.android.server.EventLogTags;
50 import com.android.server.connectivity.Vpn;
51 
52 import java.util.List;
53 import java.util.Objects;
54 
55 /**
56  * State tracker for lockdown mode. Watches for normal {@link NetworkInfo} to be
57  * connected and kicks off VPN connection, managing any required {@code netd}
58  * firewall rules.
59  */
60 public class LockdownVpnTracker {
61     private static final String TAG = "LockdownVpnTracker";
62 
63     /** Number of VPN attempts before waiting for user intervention. */
64     private static final int MAX_ERROR_COUNT = 4;
65 
66     private static final String ACTION_LOCKDOWN_RESET = "com.android.server.action.LOCKDOWN_RESET";
67 
68     @NonNull private final Context mContext;
69     @NonNull private final ConnectivityService mConnService;
70     @NonNull private final Handler mHandler;
71     @NonNull private final Vpn mVpn;
72     @NonNull private final VpnProfile mProfile;
73 
74     @NonNull private final Object mStateLock = new Object();
75 
76     @NonNull private final PendingIntent mConfigIntent;
77     @NonNull private final PendingIntent mResetIntent;
78 
79     @Nullable
80     private String mAcceptedEgressIface;
81 
82     private int mErrorCount;
83 
isEnabled()84     public static boolean isEnabled() {
85         return KeyStore.getInstance().contains(Credentials.LOCKDOWN_VPN);
86     }
87 
LockdownVpnTracker(@onNull Context context, @NonNull ConnectivityService connService, @NonNull Handler handler, @NonNull Vpn vpn, @NonNull VpnProfile profile)88     public LockdownVpnTracker(@NonNull Context context,
89             @NonNull ConnectivityService connService,
90             @NonNull Handler handler,
91             @NonNull Vpn vpn,
92             @NonNull VpnProfile profile) {
93         mContext = Objects.requireNonNull(context);
94         mConnService = Objects.requireNonNull(connService);
95         mHandler = Objects.requireNonNull(handler);
96         mVpn = Objects.requireNonNull(vpn);
97         mProfile = Objects.requireNonNull(profile);
98 
99         final Intent configIntent = new Intent(ACTION_VPN_SETTINGS);
100         mConfigIntent = PendingIntent.getActivity(mContext, 0, configIntent, 0);
101 
102         final Intent resetIntent = new Intent(ACTION_LOCKDOWN_RESET);
103         resetIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
104         mResetIntent = PendingIntent.getBroadcast(mContext, 0, resetIntent, 0);
105     }
106 
107     private BroadcastReceiver mResetReceiver = new BroadcastReceiver() {
108         @Override
109         public void onReceive(Context context, Intent intent) {
110             reset();
111         }
112     };
113 
114     /**
115      * Watch for state changes to both active egress network, kicking off a VPN
116      * connection when ready, or setting firewall rules once VPN is connected.
117      */
handleStateChangedLocked()118     private void handleStateChangedLocked() {
119 
120         final NetworkInfo egressInfo = mConnService.getActiveNetworkInfoUnfiltered();
121         final LinkProperties egressProp = mConnService.getActiveLinkProperties();
122 
123         final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
124         final VpnConfig vpnConfig = mVpn.getLegacyVpnConfig();
125 
126         // Restart VPN when egress network disconnected or changed
127         final boolean egressDisconnected = egressInfo == null
128                 || State.DISCONNECTED.equals(egressInfo.getState());
129         final boolean egressChanged = egressProp == null
130                 || !TextUtils.equals(mAcceptedEgressIface, egressProp.getInterfaceName());
131 
132         final String egressTypeName = (egressInfo == null) ?
133                 null : ConnectivityManager.getNetworkTypeName(egressInfo.getType());
134         final String egressIface = (egressProp == null) ?
135                 null : egressProp.getInterfaceName();
136         Slog.d(TAG, "handleStateChanged: egress=" + egressTypeName +
137                 " " + mAcceptedEgressIface + "->" + egressIface);
138 
139         if (egressDisconnected || egressChanged) {
140             mAcceptedEgressIface = null;
141             mVpn.stopVpnRunnerPrivileged();
142         }
143         if (egressDisconnected) {
144             hideNotification();
145             return;
146         }
147 
148         final int egressType = egressInfo.getType();
149         if (vpnInfo.getDetailedState() == DetailedState.FAILED) {
150             EventLogTags.writeLockdownVpnError(egressType);
151         }
152 
153         if (mErrorCount > MAX_ERROR_COUNT) {
154             showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
155 
156         } else if (egressInfo.isConnected() && !vpnInfo.isConnectedOrConnecting()) {
157             if (mProfile.isValidLockdownProfile()) {
158                 Slog.d(TAG, "Active network connected; starting VPN");
159                 EventLogTags.writeLockdownVpnConnecting(egressType);
160                 showNotification(R.string.vpn_lockdown_connecting, R.drawable.vpn_disconnected);
161 
162                 mAcceptedEgressIface = egressProp.getInterfaceName();
163                 try {
164                     // Use the privileged method because Lockdown VPN is initiated by the system, so
165                     // no additional permission checks are necessary.
166                     mVpn.startLegacyVpnPrivileged(mProfile, KeyStore.getInstance(), egressProp);
167                 } catch (IllegalStateException e) {
168                     mAcceptedEgressIface = null;
169                     Slog.e(TAG, "Failed to start VPN", e);
170                     showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
171                 }
172             } else {
173                 Slog.e(TAG, "Invalid VPN profile; requires IP-based server and DNS");
174                 showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
175             }
176 
177         } else if (vpnInfo.isConnected() && vpnConfig != null) {
178             final String iface = vpnConfig.interfaze;
179             final List<LinkAddress> sourceAddrs = vpnConfig.addresses;
180 
181             Slog.d(TAG, "VPN connected using iface=" + iface +
182                     ", sourceAddr=" + sourceAddrs.toString());
183             EventLogTags.writeLockdownVpnConnected(egressType);
184             showNotification(R.string.vpn_lockdown_connected, R.drawable.vpn_connected);
185 
186             final NetworkInfo clone = new NetworkInfo(egressInfo);
187             augmentNetworkInfo(clone);
188             mConnService.sendConnectedBroadcast(clone);
189         }
190     }
191 
init()192     public void init() {
193         synchronized (mStateLock) {
194             initLocked();
195         }
196     }
197 
initLocked()198     private void initLocked() {
199         Slog.d(TAG, "initLocked()");
200 
201         mVpn.setEnableTeardown(false);
202         mVpn.setLockdown(true);
203 
204         final IntentFilter resetFilter = new IntentFilter(ACTION_LOCKDOWN_RESET);
205         mContext.registerReceiver(mResetReceiver, resetFilter, NETWORK_STACK, mHandler);
206         handleStateChangedLocked();
207     }
208 
shutdown()209     public void shutdown() {
210         synchronized (mStateLock) {
211             shutdownLocked();
212         }
213     }
214 
shutdownLocked()215     private void shutdownLocked() {
216         Slog.d(TAG, "shutdownLocked()");
217 
218         mAcceptedEgressIface = null;
219         mErrorCount = 0;
220 
221         mVpn.stopVpnRunnerPrivileged();
222         mVpn.setLockdown(false);
223         hideNotification();
224 
225         mContext.unregisterReceiver(mResetReceiver);
226         mVpn.setEnableTeardown(true);
227     }
228 
reset()229     public void reset() {
230         Slog.d(TAG, "reset()");
231         synchronized (mStateLock) {
232             // cycle tracker, reset error count, and trigger retry
233             shutdownLocked();
234             initLocked();
235             handleStateChangedLocked();
236         }
237     }
238 
onNetworkInfoChanged()239     public void onNetworkInfoChanged() {
240         synchronized (mStateLock) {
241             handleStateChangedLocked();
242         }
243     }
244 
onVpnStateChanged(NetworkInfo info)245     public void onVpnStateChanged(NetworkInfo info) {
246         if (info.getDetailedState() == DetailedState.FAILED) {
247             mErrorCount++;
248         }
249         synchronized (mStateLock) {
250             handleStateChangedLocked();
251         }
252     }
253 
augmentNetworkInfo(NetworkInfo info)254     public void augmentNetworkInfo(NetworkInfo info) {
255         if (info.isConnected()) {
256             final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
257             info.setDetailedState(vpnInfo.getDetailedState(), vpnInfo.getReason(), null);
258         }
259     }
260 
showNotification(int titleRes, int iconRes)261     private void showNotification(int titleRes, int iconRes) {
262         final Notification.Builder builder =
263                 new Notification.Builder(mContext, SystemNotificationChannels.VPN)
264                         .setWhen(0)
265                         .setSmallIcon(iconRes)
266                         .setContentTitle(mContext.getString(titleRes))
267                         .setContentText(mContext.getString(R.string.vpn_lockdown_config))
268                         .setContentIntent(mConfigIntent)
269                         .setOngoing(true)
270                         .addAction(R.drawable.ic_menu_refresh, mContext.getString(R.string.reset),
271                                 mResetIntent)
272                         .setColor(mContext.getColor(
273                                 com.android.internal.R.color.system_notification_accent_color));
274 
275         NotificationManager.from(mContext).notify(null, SystemMessage.NOTE_VPN_STATUS,
276                 builder.build());
277     }
278 
hideNotification()279     private void hideNotification() {
280         NotificationManager.from(mContext).cancel(null, SystemMessage.NOTE_VPN_STATUS);
281     }
282 }
283