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