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