/* * 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 static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NONE; import static android.net.NetworkPolicyManager.FIREWALL_RULE_ALLOW; import static android.net.NetworkPolicyManager.FIREWALL_RULE_DEFAULT; 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.ConnectivityManager; import android.net.LinkProperties; import android.net.LinkAddress; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; import android.net.NetworkInfo.State; import android.net.NetworkPolicyManager; import android.os.INetworkManagementService; import android.os.RemoteException; import android.security.Credentials; import android.security.KeyStore; import android.system.Os; 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 static final int ROOT_UID = 0; 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() { 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()); final String egressTypeName = (egressInfo == null) ? null : ConnectivityManager.getNetworkTypeName(egressInfo.getType()); final String egressIface = (egressProp == null) ? null : egressProp.getInterfaceName(); Slog.d(TAG, "handleStateChanged: egress=" + egressTypeName + " " + mAcceptedEgressIface + "->" + egressIface); if (egressDisconnected || egressChanged) { clearSourceRulesLocked(); mAcceptedEgressIface = null; mVpn.stopLegacyVpnPrivileged(); } 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 { // Use the privileged method because Lockdown VPN is initiated by the system, so // no additional permission checks are necessary. mVpn.startLegacyVpnPrivileged(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) { setFirewallEgressSourceRule(addr, true); } mNetService.setFirewallUidRule(FIREWALL_CHAIN_NONE, ROOT_UID, FIREWALL_RULE_ALLOW); mNetService.setFirewallUidRule(FIREWALL_CHAIN_NONE, Os.getuid(), FIREWALL_RULE_ALLOW); 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.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.stopLegacyVpnPrivileged(); 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.setEnableTeardown(true); } public void reset() { Slog.d(TAG, "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) { setFirewallEgressSourceRule(addr, false); } mNetService.setFirewallUidRule(FIREWALL_CHAIN_NONE, ROOT_UID, FIREWALL_RULE_DEFAULT); mNetService.setFirewallUidRule(FIREWALL_CHAIN_NONE,Os.getuid(), FIREWALL_RULE_DEFAULT); mAcceptedSourceAddr = null; } } catch (RemoteException e) { throw new RuntimeException("Problem setting firewall rules", e); } } private void setFirewallEgressSourceRule( LinkAddress address, boolean allow) throws RemoteException { // Our source address based firewall rules must only cover our own source address, not the // whole subnet final String addrString = address.getAddress().getHostAddress(); mNetService.setFirewallEgressSourceRule(addrString, allow); } public void onNetworkInfoChanged() { 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) .setWhen(0) .setSmallIcon(iconRes) .setContentTitle(mContext.getString(titleRes)) .setContentText(mContext.getString(R.string.vpn_lockdown_config)) .setContentIntent(mConfigIntent) .setPriority(Notification.PRIORITY_LOW) .setOngoing(true) .addAction(R.drawable.ic_menu_refresh, mContext.getString(R.string.reset), mResetIntent) .setColor(mContext.getColor( com.android.internal.R.color.system_notification_accent_color)); NotificationManager.from(mContext).notify(TAG, 0, builder.build()); } private void hideNotification() { NotificationManager.from(mContext).cancel(TAG, 0); } }