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.CONNECTIVITY_INTERNAL;
20 
21 import android.app.Notification;
22 import android.app.NotificationManager;
23 import android.app.PendingIntent;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.net.ConnectivityManager;
29 import android.net.LinkProperties;
30 import android.net.LinkAddress;
31 import android.net.NetworkInfo;
32 import android.net.NetworkInfo.DetailedState;
33 import android.net.NetworkInfo.State;
34 import android.os.INetworkManagementService;
35 import android.os.RemoteException;
36 import android.security.Credentials;
37 import android.security.KeyStore;
38 import android.system.Os;
39 import android.text.TextUtils;
40 import android.util.Slog;
41 
42 import com.android.internal.R;
43 import com.android.internal.net.VpnConfig;
44 import com.android.internal.net.VpnProfile;
45 import com.android.internal.util.Preconditions;
46 import com.android.server.ConnectivityService;
47 import com.android.server.EventLogTags;
48 import com.android.server.connectivity.Vpn;
49 
50 import java.util.List;
51 
52 /**
53  * State tracker for lockdown mode. Watches for normal {@link NetworkInfo} to be
54  * connected and kicks off VPN connection, managing any required {@code netd}
55  * firewall rules.
56  */
57 public class LockdownVpnTracker {
58     private static final String TAG = "LockdownVpnTracker";
59 
60     /** Number of VPN attempts before waiting for user intervention. */
61     private static final int MAX_ERROR_COUNT = 4;
62 
63     private static final String ACTION_LOCKDOWN_RESET = "com.android.server.action.LOCKDOWN_RESET";
64 
65     private static final String ACTION_VPN_SETTINGS = "android.net.vpn.SETTINGS";
66     private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN";
67 
68     private static final int ROOT_UID = 0;
69 
70     private final Context mContext;
71     private final INetworkManagementService mNetService;
72     private final ConnectivityService mConnService;
73     private final Vpn mVpn;
74     private final VpnProfile mProfile;
75 
76     private final Object mStateLock = new Object();
77 
78     private final PendingIntent mConfigIntent;
79     private final PendingIntent mResetIntent;
80 
81     private String mAcceptedEgressIface;
82     private String mAcceptedIface;
83     private List<LinkAddress> mAcceptedSourceAddr;
84 
85     private int mErrorCount;
86 
isEnabled()87     public static boolean isEnabled() {
88         return KeyStore.getInstance().contains(Credentials.LOCKDOWN_VPN);
89     }
90 
LockdownVpnTracker(Context context, INetworkManagementService netService, ConnectivityService connService, Vpn vpn, VpnProfile profile)91     public LockdownVpnTracker(Context context, INetworkManagementService netService,
92             ConnectivityService connService, Vpn vpn, VpnProfile profile) {
93         mContext = Preconditions.checkNotNull(context);
94         mNetService = Preconditions.checkNotNull(netService);
95         mConnService = Preconditions.checkNotNull(connService);
96         mVpn = Preconditions.checkNotNull(vpn);
97         mProfile = Preconditions.checkNotNull(profile);
98 
99         final Intent configIntent = new Intent(ACTION_VPN_SETTINGS);
100         configIntent.putExtra(EXTRA_PICK_LOCKDOWN, true);
101         mConfigIntent = PendingIntent.getActivity(mContext, 0, configIntent, 0);
102 
103         final Intent resetIntent = new Intent(ACTION_LOCKDOWN_RESET);
104         resetIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
105         mResetIntent = PendingIntent.getBroadcast(mContext, 0, resetIntent, 0);
106     }
107 
108     private BroadcastReceiver mResetReceiver = new BroadcastReceiver() {
109         @Override
110         public void onReceive(Context context, Intent intent) {
111             reset();
112         }
113     };
114 
115     /**
116      * Watch for state changes to both active egress network, kicking off a VPN
117      * connection when ready, or setting firewall rules once VPN is connected.
118      */
handleStateChangedLocked()119     private void handleStateChangedLocked() {
120 
121         final NetworkInfo egressInfo = mConnService.getActiveNetworkInfoUnfiltered();
122         final LinkProperties egressProp = mConnService.getActiveLinkProperties();
123 
124         final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
125         final VpnConfig vpnConfig = mVpn.getLegacyVpnConfig();
126 
127         // Restart VPN when egress network disconnected or changed
128         final boolean egressDisconnected = egressInfo == null
129                 || State.DISCONNECTED.equals(egressInfo.getState());
130         final boolean egressChanged = egressProp == null
131                 || !TextUtils.equals(mAcceptedEgressIface, egressProp.getInterfaceName());
132 
133         final String egressTypeName = (egressInfo == null) ?
134                 null : ConnectivityManager.getNetworkTypeName(egressInfo.getType());
135         final String egressIface = (egressProp == null) ?
136                 null : egressProp.getInterfaceName();
137         Slog.d(TAG, "handleStateChanged: egress=" + egressTypeName +
138                 " " + mAcceptedEgressIface + "->" + egressIface);
139 
140         if (egressDisconnected || egressChanged) {
141             clearSourceRulesLocked();
142             mAcceptedEgressIface = null;
143             mVpn.stopLegacyVpnPrivileged();
144         }
145         if (egressDisconnected) {
146             hideNotification();
147             return;
148         }
149 
150         final int egressType = egressInfo.getType();
151         if (vpnInfo.getDetailedState() == DetailedState.FAILED) {
152             EventLogTags.writeLockdownVpnError(egressType);
153         }
154 
155         if (mErrorCount > MAX_ERROR_COUNT) {
156             showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
157 
158         } else if (egressInfo.isConnected() && !vpnInfo.isConnectedOrConnecting()) {
159             if (mProfile.isValidLockdownProfile()) {
160                 Slog.d(TAG, "Active network connected; starting VPN");
161                 EventLogTags.writeLockdownVpnConnecting(egressType);
162                 showNotification(R.string.vpn_lockdown_connecting, R.drawable.vpn_disconnected);
163 
164                 mAcceptedEgressIface = egressProp.getInterfaceName();
165                 try {
166                     // Use the privileged method because Lockdown VPN is initiated by the system, so
167                     // no additional permission checks are necessary.
168                     mVpn.startLegacyVpnPrivileged(mProfile, KeyStore.getInstance(), egressProp);
169                 } catch (IllegalStateException e) {
170                     mAcceptedEgressIface = null;
171                     Slog.e(TAG, "Failed to start VPN", e);
172                     showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
173                 }
174             } else {
175                 Slog.e(TAG, "Invalid VPN profile; requires IP-based server and DNS");
176                 showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
177             }
178 
179         } else if (vpnInfo.isConnected() && vpnConfig != null) {
180             final String iface = vpnConfig.interfaze;
181             final List<LinkAddress> sourceAddrs = vpnConfig.addresses;
182 
183             if (TextUtils.equals(iface, mAcceptedIface)
184                   && sourceAddrs.equals(mAcceptedSourceAddr)) {
185                 return;
186             }
187 
188             Slog.d(TAG, "VPN connected using iface=" + iface +
189                     ", sourceAddr=" + sourceAddrs.toString());
190             EventLogTags.writeLockdownVpnConnected(egressType);
191             showNotification(R.string.vpn_lockdown_connected, R.drawable.vpn_connected);
192 
193             try {
194                 clearSourceRulesLocked();
195 
196                 mNetService.setFirewallInterfaceRule(iface, true);
197                 for (LinkAddress addr : sourceAddrs) {
198                     setFirewallEgressSourceRule(addr, true);
199                 }
200 
201                 mNetService.setFirewallUidRule(ROOT_UID, true);
202                 mNetService.setFirewallUidRule(Os.getuid(), true);
203 
204                 mErrorCount = 0;
205                 mAcceptedIface = iface;
206                 mAcceptedSourceAddr = sourceAddrs;
207             } catch (RemoteException e) {
208                 throw new RuntimeException("Problem setting firewall rules", e);
209             }
210 
211             mConnService.sendConnectedBroadcast(augmentNetworkInfo(egressInfo));
212         }
213     }
214 
init()215     public void init() {
216         synchronized (mStateLock) {
217             initLocked();
218         }
219     }
220 
initLocked()221     private void initLocked() {
222         Slog.d(TAG, "initLocked()");
223 
224         mVpn.setEnableTeardown(false);
225 
226         final IntentFilter resetFilter = new IntentFilter(ACTION_LOCKDOWN_RESET);
227         mContext.registerReceiver(mResetReceiver, resetFilter, CONNECTIVITY_INTERNAL, null);
228 
229         try {
230             // TODO: support non-standard port numbers
231             mNetService.setFirewallEgressDestRule(mProfile.server, 500, true);
232             mNetService.setFirewallEgressDestRule(mProfile.server, 4500, true);
233             mNetService.setFirewallEgressDestRule(mProfile.server, 1701, true);
234         } catch (RemoteException e) {
235             throw new RuntimeException("Problem setting firewall rules", e);
236         }
237 
238         synchronized (mStateLock) {
239             handleStateChangedLocked();
240         }
241     }
242 
shutdown()243     public void shutdown() {
244         synchronized (mStateLock) {
245             shutdownLocked();
246         }
247     }
248 
shutdownLocked()249     private void shutdownLocked() {
250         Slog.d(TAG, "shutdownLocked()");
251 
252         mAcceptedEgressIface = null;
253         mErrorCount = 0;
254 
255         mVpn.stopLegacyVpnPrivileged();
256         try {
257             mNetService.setFirewallEgressDestRule(mProfile.server, 500, false);
258             mNetService.setFirewallEgressDestRule(mProfile.server, 4500, false);
259             mNetService.setFirewallEgressDestRule(mProfile.server, 1701, false);
260         } catch (RemoteException e) {
261             throw new RuntimeException("Problem setting firewall rules", e);
262         }
263         clearSourceRulesLocked();
264         hideNotification();
265 
266         mContext.unregisterReceiver(mResetReceiver);
267         mVpn.setEnableTeardown(true);
268     }
269 
reset()270     public void reset() {
271         Slog.d(TAG, "reset()");
272         synchronized (mStateLock) {
273             // cycle tracker, reset error count, and trigger retry
274             shutdownLocked();
275             initLocked();
276             handleStateChangedLocked();
277         }
278     }
279 
clearSourceRulesLocked()280     private void clearSourceRulesLocked() {
281         try {
282             if (mAcceptedIface != null) {
283                 mNetService.setFirewallInterfaceRule(mAcceptedIface, false);
284                 mAcceptedIface = null;
285             }
286             if (mAcceptedSourceAddr != null) {
287                 for (LinkAddress addr : mAcceptedSourceAddr) {
288                     setFirewallEgressSourceRule(addr, false);
289                 }
290 
291                 mNetService.setFirewallUidRule(ROOT_UID, false);
292                 mNetService.setFirewallUidRule(Os.getuid(), false);
293 
294                 mAcceptedSourceAddr = null;
295             }
296         } catch (RemoteException e) {
297             throw new RuntimeException("Problem setting firewall rules", e);
298         }
299     }
300 
setFirewallEgressSourceRule( LinkAddress address, boolean allow)301     private void setFirewallEgressSourceRule(
302             LinkAddress address, boolean allow) throws RemoteException {
303         // Our source address based firewall rules must only cover our own source address, not the
304         // whole subnet
305         final String addrString = address.getAddress().getHostAddress();
306         mNetService.setFirewallEgressSourceRule(addrString, allow);
307     }
308 
onNetworkInfoChanged()309     public void onNetworkInfoChanged() {
310         synchronized (mStateLock) {
311             handleStateChangedLocked();
312         }
313     }
314 
onVpnStateChanged(NetworkInfo info)315     public void onVpnStateChanged(NetworkInfo info) {
316         if (info.getDetailedState() == DetailedState.FAILED) {
317             mErrorCount++;
318         }
319         synchronized (mStateLock) {
320             handleStateChangedLocked();
321         }
322     }
323 
augmentNetworkInfo(NetworkInfo info)324     public NetworkInfo augmentNetworkInfo(NetworkInfo info) {
325         if (info.isConnected()) {
326             final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
327             info = new NetworkInfo(info);
328             info.setDetailedState(vpnInfo.getDetailedState(), vpnInfo.getReason(), null);
329         }
330         return info;
331     }
332 
showNotification(int titleRes, int iconRes)333     private void showNotification(int titleRes, int iconRes) {
334         final Notification.Builder builder = new Notification.Builder(mContext)
335                 .setWhen(0)
336                 .setSmallIcon(iconRes)
337                 .setContentTitle(mContext.getString(titleRes))
338                 .setContentText(mContext.getString(R.string.vpn_lockdown_config))
339                 .setContentIntent(mConfigIntent)
340                 .setPriority(Notification.PRIORITY_LOW)
341                 .setOngoing(true)
342                 .addAction(R.drawable.ic_menu_refresh, mContext.getString(R.string.reset),
343                         mResetIntent)
344                 .setColor(mContext.getResources().getColor(
345                         com.android.internal.R.color.system_notification_accent_color));
346 
347         NotificationManager.from(mContext).notify(TAG, 0, builder.build());
348     }
349 
hideNotification()350     private void hideNotification() {
351         NotificationManager.from(mContext).cancel(TAG, 0);
352     }
353 }
354