2 * Copyright (C) 2012 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.server.net;
19 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
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;
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;
50 import java.util.List;
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}
57 public class LockdownVpnTracker {
58 private static final String TAG = "LockdownVpnTracker";
60 /** Number of VPN attempts before waiting for user intervention. */
61 private static final int MAX_ERROR_COUNT = 4;
63 private static final String ACTION_LOCKDOWN_RESET = "com.android.server.action.LOCKDOWN_RESET";
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";
68 private static final int ROOT_UID = 0;
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;
76 private final Object mStateLock = new Object();
78 private final PendingIntent mConfigIntent;
79 private final PendingIntent mResetIntent;
81 private String mAcceptedEgressIface;
82 private String mAcceptedIface;
83 private List<LinkAddress> mAcceptedSourceAddr;
85 private int mErrorCount;
87 public static boolean isEnabled() {
88 return KeyStore.getInstance().contains(Credentials.LOCKDOWN_VPN);
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);
99 final Intent configIntent = new Intent(ACTION_VPN_SETTINGS);
100 configIntent.putExtra(EXTRA_PICK_LOCKDOWN, true);
101 mConfigIntent = PendingIntent.getActivity(mContext, 0, configIntent, 0);
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);
108 private BroadcastReceiver mResetReceiver = new BroadcastReceiver() {
110 public void onReceive(Context context, Intent intent) {
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.
119 private void handleStateChangedLocked() {
121 final NetworkInfo egressInfo = mConnService.getActiveNetworkInfoUnfiltered();
122 final LinkProperties egressProp = mConnService.getActiveLinkProperties();
124 final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
125 final VpnConfig vpnConfig = mVpn.getLegacyVpnConfig();
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());
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);
140 if (egressDisconnected || egressChanged) {
141 clearSourceRulesLocked();
142 mAcceptedEgressIface = null;
143 mVpn.stopLegacyVpn();
145 if (egressDisconnected) {
150 final int egressType = egressInfo.getType();
151 if (vpnInfo.getDetailedState() == DetailedState.FAILED) {
152 EventLogTags.writeLockdownVpnError(egressType);
155 if (mErrorCount > MAX_ERROR_COUNT) {
156 showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
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);
164 mAcceptedEgressIface = egressProp.getInterfaceName();
166 mVpn.startLegacyVpn(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);
173 Slog.e(TAG, "Invalid VPN profile; requires IP-based server and DNS");
174 showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
177 } else if (vpnInfo.isConnected() && vpnConfig != null) {
178 final String iface = vpnConfig.interfaze;
179 final List<LinkAddress> sourceAddrs = vpnConfig.addresses;
181 if (TextUtils.equals(iface, mAcceptedIface)
182 && sourceAddrs.equals(mAcceptedSourceAddr)) {
186 Slog.d(TAG, "VPN connected using iface=" + iface +
187 ", sourceAddr=" + sourceAddrs.toString());
188 EventLogTags.writeLockdownVpnConnected(egressType);
189 showNotification(R.string.vpn_lockdown_connected, R.drawable.vpn_connected);
192 clearSourceRulesLocked();
194 mNetService.setFirewallInterfaceRule(iface, true);
195 for (LinkAddress addr : sourceAddrs) {
196 setFirewallEgressSourceRule(addr, true);
199 mNetService.setFirewallUidRule(ROOT_UID, true);
200 mNetService.setFirewallUidRule(Os.getuid(), true);
203 mAcceptedIface = iface;
204 mAcceptedSourceAddr = sourceAddrs;
205 } catch (RemoteException e) {
206 throw new RuntimeException("Problem setting firewall rules", e);
209 mConnService.sendConnectedBroadcast(augmentNetworkInfo(egressInfo));
214 synchronized (mStateLock) {
219 private void initLocked() {
220 Slog.d(TAG, "initLocked()");
222 mVpn.setEnableTeardown(false);
224 final IntentFilter resetFilter = new IntentFilter(ACTION_LOCKDOWN_RESET);
225 mContext.registerReceiver(mResetReceiver, resetFilter, CONNECTIVITY_INTERNAL, null);
228 // TODO: support non-standard port numbers
229 mNetService.setFirewallEgressDestRule(mProfile.server, 500, true);
230 mNetService.setFirewallEgressDestRule(mProfile.server, 4500, true);
231 mNetService.setFirewallEgressDestRule(mProfile.server, 1701, true);
232 } catch (RemoteException e) {
233 throw new RuntimeException("Problem setting firewall rules", e);
236 synchronized (mStateLock) {
237 handleStateChangedLocked();
241 public void shutdown() {
242 synchronized (mStateLock) {
247 private void shutdownLocked() {
248 Slog.d(TAG, "shutdownLocked()");
250 mAcceptedEgressIface = null;
253 mVpn.stopLegacyVpn();
255 mNetService.setFirewallEgressDestRule(mProfile.server, 500, false);
256 mNetService.setFirewallEgressDestRule(mProfile.server, 4500, false);
257 mNetService.setFirewallEgressDestRule(mProfile.server, 1701, false);
258 } catch (RemoteException e) {
259 throw new RuntimeException("Problem setting firewall rules", e);
261 clearSourceRulesLocked();
264 mContext.unregisterReceiver(mResetReceiver);
265 mVpn.setEnableTeardown(true);
268 public void reset() {
269 Slog.d(TAG, "reset()");
270 synchronized (mStateLock) {
271 // cycle tracker, reset error count, and trigger retry
274 handleStateChangedLocked();
278 private void clearSourceRulesLocked() {
280 if (mAcceptedIface != null) {
281 mNetService.setFirewallInterfaceRule(mAcceptedIface, false);
282 mAcceptedIface = null;
284 if (mAcceptedSourceAddr != null) {
285 for (LinkAddress addr : mAcceptedSourceAddr) {
286 setFirewallEgressSourceRule(addr, false);
289 mNetService.setFirewallUidRule(ROOT_UID, false);
290 mNetService.setFirewallUidRule(Os.getuid(), false);
292 mAcceptedSourceAddr = null;
294 } catch (RemoteException e) {
295 throw new RuntimeException("Problem setting firewall rules", e);
299 private void setFirewallEgressSourceRule(
300 LinkAddress address, boolean allow) throws RemoteException {
301 // Our source address based firewall rules must only cover our own source address, not the
303 final String addrString = address.getAddress().getHostAddress();
304 mNetService.setFirewallEgressSourceRule(addrString, allow);
307 public void onNetworkInfoChanged() {
308 synchronized (mStateLock) {
309 handleStateChangedLocked();
313 public void onVpnStateChanged(NetworkInfo info) {
314 if (info.getDetailedState() == DetailedState.FAILED) {
317 synchronized (mStateLock) {
318 handleStateChangedLocked();
322 public NetworkInfo augmentNetworkInfo(NetworkInfo info) {
323 if (info.isConnected()) {
324 final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
325 info = new NetworkInfo(info);
326 info.setDetailedState(vpnInfo.getDetailedState(), vpnInfo.getReason(), null);
331 private void showNotification(int titleRes, int iconRes) {
332 final Notification.Builder builder = new Notification.Builder(mContext)
334 .setSmallIcon(iconRes)
335 .setContentTitle(mContext.getString(titleRes))
336 .setContentText(mContext.getString(R.string.vpn_lockdown_config))
337 .setContentIntent(mConfigIntent)
338 .setPriority(Notification.PRIORITY_LOW)
340 .addAction(R.drawable.ic_menu_refresh, mContext.getString(R.string.reset),
342 .setColor(mContext.getResources().getColor(
343 com.android.internal.R.color.system_notification_accent_color));
345 NotificationManager.from(mContext).notify(TAG, 0, builder.build());
348 private void hideNotification() {
349 NotificationManager.from(mContext).cancel(TAG, 0);