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