Files
frameworks_base/services/java/com/android/server/net/LockdownVpnTracker.java
Jeff Sharkey 69ddab4575 Always-on VPN.
Adds support for always-on VPN profiles, also called "lockdown." When
enabled, LockdownVpnTracker manages the netd firewall to prevent
unencrypted traffic from leaving the device. It creates narrow rules
to only allow traffic to the selected VPN server. When an egress
network becomes available, LockdownVpnTracker will try bringing up
the VPN connection, and will reconnect if disconnected.

ConnectivityService augments any NetworkInfo based on the lockdown
VPN status to help apps wait until the VPN is connected.

This feature requires that VPN profiles use an IP address for both
VPN server and DNS. It also blocks non-default APN access when
enabled. Waits for USER_PRESENT after boot to check KeyStore status.

Bug: 5756357
Change-Id: If615f206b1634000d78a8350a17e88bfcac8e0d0
2012-08-27 12:35:05 -07:00

272 lines
9.8 KiB
Java

/*
* 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.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.connectivity.Vpn;
/**
* 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 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 PendingIntent mResetIntent;
private String mAcceptedEgressIface;
private String mAcceptedIface;
private String 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 intent = new Intent(ACTION_LOCKDOWN_RESET);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
mResetIntent = PendingIntent.getBroadcast(mContext, 0, intent, 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) {
clearSourceRules();
mAcceptedEgressIface = null;
mVpn.stopLegacyVpn();
}
if (egressDisconnected) return;
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");
showNotification(R.string.vpn_lockdown_connecting, R.drawable.vpn_disconnected);
mAcceptedEgressIface = egressProp.getInterfaceName();
mVpn.startLegacyVpn(mProfile, KeyStore.getInstance(), egressProp);
} 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 String sourceAddr = vpnConfig.addresses;
if (TextUtils.equals(iface, mAcceptedIface)
&& TextUtils.equals(sourceAddr, mAcceptedSourceAddr)) {
return;
}
Slog.d(TAG, "VPN connected using iface=" + iface + ", sourceAddr=" + sourceAddr);
showNotification(R.string.vpn_lockdown_connected, R.drawable.vpn_connected);
try {
clearSourceRules();
mNetService.setFirewallInterfaceRule(iface, true);
mNetService.setFirewallEgressSourceRule(sourceAddr, true);
mErrorCount = 0;
mAcceptedIface = iface;
mAcceptedSourceAddr = sourceAddr;
} catch (RemoteException e) {
throw new RuntimeException("Problem setting firewall rules", e);
}
mConnService.sendConnectedBroadcast(augmentNetworkInfo(egressInfo));
}
}
public void init() {
Slog.d(TAG, "init()");
mVpn.setEnableNotifications(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);
} catch (RemoteException e) {
throw new RuntimeException("Problem setting firewall rules", e);
}
synchronized (mStateLock) {
handleStateChangedLocked();
}
}
public void shutdown() {
Slog.d(TAG, "shutdown()");
mAcceptedEgressIface = null;
mErrorCount = 0;
mVpn.stopLegacyVpn();
try {
mNetService.setFirewallEgressDestRule(mProfile.server, 500, false);
mNetService.setFirewallEgressDestRule(mProfile.server, 4500, false);
} catch (RemoteException e) {
throw new RuntimeException("Problem setting firewall rules", e);
}
clearSourceRules();
hideNotification();
mContext.unregisterReceiver(mResetReceiver);
mVpn.setEnableNotifications(true);
}
public void reset() {
// cycle tracker, reset error count, and trigger retry
shutdown();
init();
synchronized (mStateLock) {
handleStateChangedLocked();
}
}
private void clearSourceRules() {
try {
if (mAcceptedIface != null) {
mNetService.setFirewallInterfaceRule(mAcceptedIface, false);
mAcceptedIface = null;
}
if (mAcceptedSourceAddr != null) {
mNetService.setFirewallEgressSourceRule(mAcceptedSourceAddr, 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) {
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_reset));
builder.setContentIntent(mResetIntent);
builder.setPriority(Notification.PRIORITY_LOW);
builder.setOngoing(true);
NotificationManager.from(mContext).notify(TAG, 0, builder.build());
}
private void hideNotification() {
NotificationManager.from(mContext).cancel(TAG, 0);
}
}