diff options
Diffstat (limited to 'services/core/java/com/android/server/net/LockdownVpnTracker.java')
-rw-r--r-- | services/core/java/com/android/server/net/LockdownVpnTracker.java | 324 |
1 files changed, 324 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/net/LockdownVpnTracker.java b/services/core/java/com/android/server/net/LockdownVpnTracker.java new file mode 100644 index 0000000..a2e9d67 --- /dev/null +++ b/services/core/java/com/android/server/net/LockdownVpnTracker.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net; + +import static android.Manifest.permission.CONNECTIVITY_INTERNAL; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.LinkProperties; +import android.net.LinkAddress; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkInfo.State; +import android.os.INetworkManagementService; +import android.os.RemoteException; +import android.security.Credentials; +import android.security.KeyStore; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; +import com.android.internal.util.Preconditions; +import com.android.server.ConnectivityService; +import com.android.server.EventLogTags; +import com.android.server.connectivity.Vpn; + +import java.util.List; + +/** + * State tracker for lockdown mode. Watches for normal {@link NetworkInfo} to be + * connected and kicks off VPN connection, managing any required {@code netd} + * firewall rules. + */ +public class LockdownVpnTracker { + private static final String TAG = "LockdownVpnTracker"; + + /** Number of VPN attempts before waiting for user intervention. */ + private static final int MAX_ERROR_COUNT = 4; + + private static final String ACTION_LOCKDOWN_RESET = "com.android.server.action.LOCKDOWN_RESET"; + + private static final String ACTION_VPN_SETTINGS = "android.net.vpn.SETTINGS"; + private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN"; + + private final Context mContext; + private final INetworkManagementService mNetService; + private final ConnectivityService mConnService; + private final Vpn mVpn; + private final VpnProfile mProfile; + + private final Object mStateLock = new Object(); + + private final PendingIntent mConfigIntent; + private final PendingIntent mResetIntent; + + private String mAcceptedEgressIface; + private String mAcceptedIface; + private List<LinkAddress> mAcceptedSourceAddr; + + private int mErrorCount; + + public static boolean isEnabled() { + return KeyStore.getInstance().contains(Credentials.LOCKDOWN_VPN); + } + + public LockdownVpnTracker(Context context, INetworkManagementService netService, + ConnectivityService connService, Vpn vpn, VpnProfile profile) { + mContext = Preconditions.checkNotNull(context); + mNetService = Preconditions.checkNotNull(netService); + mConnService = Preconditions.checkNotNull(connService); + mVpn = Preconditions.checkNotNull(vpn); + mProfile = Preconditions.checkNotNull(profile); + + final Intent configIntent = new Intent(ACTION_VPN_SETTINGS); + configIntent.putExtra(EXTRA_PICK_LOCKDOWN, true); + mConfigIntent = PendingIntent.getActivity(mContext, 0, configIntent, 0); + + final Intent resetIntent = new Intent(ACTION_LOCKDOWN_RESET); + resetIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mResetIntent = PendingIntent.getBroadcast(mContext, 0, resetIntent, 0); + } + + private BroadcastReceiver mResetReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + reset(); + } + }; + + /** + * Watch for state changes to both active egress network, kicking off a VPN + * connection when ready, or setting firewall rules once VPN is connected. + */ + private void handleStateChangedLocked() { + Slog.d(TAG, "handleStateChanged()"); + + final NetworkInfo egressInfo = mConnService.getActiveNetworkInfoUnfiltered(); + final LinkProperties egressProp = mConnService.getActiveLinkProperties(); + + final NetworkInfo vpnInfo = mVpn.getNetworkInfo(); + final VpnConfig vpnConfig = mVpn.getLegacyVpnConfig(); + + // Restart VPN when egress network disconnected or changed + final boolean egressDisconnected = egressInfo == null + || State.DISCONNECTED.equals(egressInfo.getState()); + final boolean egressChanged = egressProp == null + || !TextUtils.equals(mAcceptedEgressIface, egressProp.getInterfaceName()); + if (egressDisconnected || egressChanged) { + clearSourceRulesLocked(); + mAcceptedEgressIface = null; + mVpn.stopLegacyVpn(); + } + if (egressDisconnected) { + hideNotification(); + return; + } + + final int egressType = egressInfo.getType(); + if (vpnInfo.getDetailedState() == DetailedState.FAILED) { + EventLogTags.writeLockdownVpnError(egressType); + } + + if (mErrorCount > MAX_ERROR_COUNT) { + showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected); + + } else if (egressInfo.isConnected() && !vpnInfo.isConnectedOrConnecting()) { + if (mProfile.isValidLockdownProfile()) { + Slog.d(TAG, "Active network connected; starting VPN"); + EventLogTags.writeLockdownVpnConnecting(egressType); + showNotification(R.string.vpn_lockdown_connecting, R.drawable.vpn_disconnected); + + mAcceptedEgressIface = egressProp.getInterfaceName(); + try { + mVpn.startLegacyVpn(mProfile, KeyStore.getInstance(), egressProp); + } catch (IllegalStateException e) { + mAcceptedEgressIface = null; + Slog.e(TAG, "Failed to start VPN", e); + showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected); + } + } else { + Slog.e(TAG, "Invalid VPN profile; requires IP-based server and DNS"); + showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected); + } + + } else if (vpnInfo.isConnected() && vpnConfig != null) { + final String iface = vpnConfig.interfaze; + final List<LinkAddress> sourceAddrs = vpnConfig.addresses; + + if (TextUtils.equals(iface, mAcceptedIface) + && sourceAddrs.equals(mAcceptedSourceAddr)) { + return; + } + + Slog.d(TAG, "VPN connected using iface=" + iface + + ", sourceAddr=" + sourceAddrs.toString()); + EventLogTags.writeLockdownVpnConnected(egressType); + showNotification(R.string.vpn_lockdown_connected, R.drawable.vpn_connected); + + try { + clearSourceRulesLocked(); + + mNetService.setFirewallInterfaceRule(iface, true); + for (LinkAddress addr : sourceAddrs) { + mNetService.setFirewallEgressSourceRule(addr.toString(), true); + } + + mErrorCount = 0; + mAcceptedIface = iface; + mAcceptedSourceAddr = sourceAddrs; + } catch (RemoteException e) { + throw new RuntimeException("Problem setting firewall rules", e); + } + + mConnService.sendConnectedBroadcast(augmentNetworkInfo(egressInfo)); + } + } + + public void init() { + synchronized (mStateLock) { + initLocked(); + } + } + + private void initLocked() { + Slog.d(TAG, "initLocked()"); + + mVpn.setEnableNotifications(false); + mVpn.setEnableTeardown(false); + + final IntentFilter resetFilter = new IntentFilter(ACTION_LOCKDOWN_RESET); + mContext.registerReceiver(mResetReceiver, resetFilter, CONNECTIVITY_INTERNAL, null); + + try { + // TODO: support non-standard port numbers + mNetService.setFirewallEgressDestRule(mProfile.server, 500, true); + mNetService.setFirewallEgressDestRule(mProfile.server, 4500, true); + mNetService.setFirewallEgressDestRule(mProfile.server, 1701, true); + } catch (RemoteException e) { + throw new RuntimeException("Problem setting firewall rules", e); + } + + synchronized (mStateLock) { + handleStateChangedLocked(); + } + } + + public void shutdown() { + synchronized (mStateLock) { + shutdownLocked(); + } + } + + private void shutdownLocked() { + Slog.d(TAG, "shutdownLocked()"); + + mAcceptedEgressIface = null; + mErrorCount = 0; + + mVpn.stopLegacyVpn(); + try { + mNetService.setFirewallEgressDestRule(mProfile.server, 500, false); + mNetService.setFirewallEgressDestRule(mProfile.server, 4500, false); + mNetService.setFirewallEgressDestRule(mProfile.server, 1701, false); + } catch (RemoteException e) { + throw new RuntimeException("Problem setting firewall rules", e); + } + clearSourceRulesLocked(); + hideNotification(); + + mContext.unregisterReceiver(mResetReceiver); + mVpn.setEnableNotifications(true); + mVpn.setEnableTeardown(true); + } + + public void reset() { + synchronized (mStateLock) { + // cycle tracker, reset error count, and trigger retry + shutdownLocked(); + initLocked(); + handleStateChangedLocked(); + } + } + + private void clearSourceRulesLocked() { + try { + if (mAcceptedIface != null) { + mNetService.setFirewallInterfaceRule(mAcceptedIface, false); + mAcceptedIface = null; + } + if (mAcceptedSourceAddr != null) { + for (LinkAddress addr : mAcceptedSourceAddr) { + mNetService.setFirewallEgressSourceRule(addr.toString(), false); + } + mAcceptedSourceAddr = null; + } + } catch (RemoteException e) { + throw new RuntimeException("Problem setting firewall rules", e); + } + } + + public void onNetworkInfoChanged(NetworkInfo info) { + synchronized (mStateLock) { + handleStateChangedLocked(); + } + } + + public void onVpnStateChanged(NetworkInfo info) { + if (info.getDetailedState() == DetailedState.FAILED) { + mErrorCount++; + } + synchronized (mStateLock) { + handleStateChangedLocked(); + } + } + + public NetworkInfo augmentNetworkInfo(NetworkInfo info) { + if (info.isConnected()) { + final NetworkInfo vpnInfo = mVpn.getNetworkInfo(); + info = new NetworkInfo(info); + info.setDetailedState(vpnInfo.getDetailedState(), vpnInfo.getReason(), null); + } + return info; + } + + private void showNotification(int titleRes, int iconRes) { + final Notification.Builder builder = new Notification.Builder(mContext); + builder.setWhen(0); + builder.setSmallIcon(iconRes); + builder.setContentTitle(mContext.getString(titleRes)); + builder.setContentText(mContext.getString(R.string.vpn_lockdown_config)); + builder.setContentIntent(mConfigIntent); + builder.setPriority(Notification.PRIORITY_LOW); + builder.setOngoing(true); + builder.addAction( + R.drawable.ic_menu_refresh, mContext.getString(R.string.reset), mResetIntent); + + NotificationManager.from(mContext).notify(TAG, 0, builder.build()); + } + + private void hideNotification() { + NotificationManager.from(mContext).cancel(TAG, 0); + } +} |