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