diff options
Diffstat (limited to 'services/java/com')
4 files changed, 412 insertions, 8 deletions
diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java index 3c2ab16..776a1a4 100644 --- a/services/java/com/android/server/ConnectivityService.java +++ b/services/java/com/android/server/ConnectivityService.java @@ -16,6 +16,7 @@ package com.android.server; +import static android.Manifest.permission.CONNECTIVITY_INTERNAL; import static android.Manifest.permission.MANAGE_NETWORK_POLICY; import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; @@ -31,13 +32,13 @@ import static android.net.ConnectivityManager.isNetworkTypeValid; import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL; import static android.net.NetworkPolicyManager.RULE_REJECT_METERED; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.bluetooth.BluetoothTetheringDataTracker; +import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; @@ -80,6 +81,7 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; +import android.security.Credentials; import android.security.KeyStore; import android.text.TextUtils; import android.util.EventLog; @@ -91,11 +93,11 @@ import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnProfile; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; -import com.android.internal.util.Preconditions; import com.android.server.am.BatteryStatsService; import com.android.server.connectivity.Tethering; import com.android.server.connectivity.Vpn; import com.android.server.net.BaseNetworkObserver; +import com.android.server.net.LockdownVpnTracker; import com.google.android.collect.Lists; import com.google.android.collect.Sets; @@ -142,11 +144,14 @@ public class ConnectivityService extends IConnectivityManager.Stub { private Tethering mTethering; private boolean mTetheringConfigValid = false; - private final KeyStore mKeyStore; + private KeyStore mKeyStore; private Vpn mVpn; private VpnCallback mVpnCallback = new VpnCallback(); + private boolean mLockdownEnabled; + private LockdownVpnTracker mLockdownTracker; + /** Lock around {@link #mUidRules} and {@link #mMeteredIfaces}. */ private Object mRulesLock = new Object(); /** Currently active network rules by UID. */ @@ -276,6 +281,8 @@ public class ConnectivityService extends IConnectivityManager.Stub { */ private static final int EVENT_SET_POLICY_DATA_ENABLE = 13; + private static final int EVENT_VPN_STATE_CHANGED = 14; + /** Handler used for internal events. */ private InternalHandler mHandler; /** Handler used for incoming {@link NetworkStateTracker} events. */ @@ -786,6 +793,9 @@ public class ConnectivityService extends IConnectivityManager.Stub { info = new NetworkInfo(info); info.setDetailedState(DetailedState.BLOCKED, null, null); } + if (mLockdownTracker != null) { + info = mLockdownTracker.augmentNetworkInfo(info); + } return info; } @@ -803,6 +813,17 @@ public class ConnectivityService extends IConnectivityManager.Stub { return getNetworkInfo(mActiveDefaultNetwork, uid); } + public NetworkInfo getActiveNetworkInfoUnfiltered() { + enforceAccessPermission(); + if (isNetworkTypeValid(mActiveDefaultNetwork)) { + final NetworkStateTracker tracker = mNetTrackers[mActiveDefaultNetwork]; + if (tracker != null) { + return tracker.getNetworkInfo(); + } + } + return null; + } + @Override public NetworkInfo getActiveNetworkInfoForUid(int uid) { enforceConnectivityInternalPermission(); @@ -1062,6 +1083,12 @@ public class ConnectivityService extends IConnectivityManager.Stub { // TODO - move this into individual networktrackers int usedNetworkType = convertFeatureToNetworkType(networkType, feature); + if (mLockdownEnabled) { + // Since carrier APNs usually aren't available from VPN + // endpoint, mark them as unavailable. + return PhoneConstants.APN_TYPE_NOT_AVAILABLE; + } + if (mProtectedNetworks.contains(usedNetworkType)) { enforceConnectivityInternalPermission(); } @@ -1769,7 +1796,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { } } - private void sendConnectedBroadcast(NetworkInfo info) { + public void sendConnectedBroadcast(NetworkInfo info) { sendGeneralBroadcast(info, CONNECTIVITY_ACTION_IMMEDIATE); sendGeneralBroadcast(info, CONNECTIVITY_ACTION); } @@ -1784,6 +1811,10 @@ public class ConnectivityService extends IConnectivityManager.Stub { } private Intent makeGeneralIntent(NetworkInfo info, String bcastType) { + if (mLockdownTracker != null) { + info = mLockdownTracker.augmentNetworkInfo(info); + } + Intent intent = new Intent(bcastType); intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, info); intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType()); @@ -1915,8 +1946,26 @@ public class ConnectivityService extends IConnectivityManager.Stub { } // load the global proxy at startup mHandler.sendMessage(mHandler.obtainMessage(EVENT_APPLY_GLOBAL_HTTP_PROXY)); + + // Try bringing up tracker, but if KeyStore isn't ready yet, wait + // for user to unlock device. + if (!updateLockdownVpn()) { + final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT); + mContext.registerReceiver(mUserPresentReceiver, filter); + } } + private BroadcastReceiver mUserPresentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Try creating lockdown tracker, since user present usually means + // unlocked keystore. + if (updateLockdownVpn()) { + mContext.unregisterReceiver(this); + } + } + }; + private void handleConnect(NetworkInfo info) { final int type = info.getType(); @@ -2595,6 +2644,9 @@ public class ConnectivityService extends IConnectivityManager.Stub { } else if (state == NetworkInfo.State.CONNECTED) { handleConnect(info); } + if (mLockdownTracker != null) { + mLockdownTracker.onNetworkInfoChanged(info); + } break; case NetworkStateTracker.EVENT_CONFIGURATION_CHANGED: info = (NetworkInfo) msg.obj; @@ -2692,6 +2744,13 @@ public class ConnectivityService extends IConnectivityManager.Stub { final int networkType = msg.arg1; final boolean enabled = msg.arg2 == ENABLED; handleSetPolicyDataEnable(networkType, enabled); + break; + } + case EVENT_VPN_STATE_CHANGED: { + if (mLockdownTracker != null) { + mLockdownTracker.onVpnStateChanged((NetworkInfo) msg.obj); + } + break; } } } @@ -3090,6 +3149,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { */ @Override public boolean protectVpn(ParcelFileDescriptor socket) { + throwIfLockdownEnabled(); try { int type = mActiveDefaultNetwork; if (ConnectivityManager.isNetworkTypeValid(type)) { @@ -3116,6 +3176,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { */ @Override public boolean prepareVpn(String oldPackage, String newPackage) { + throwIfLockdownEnabled(); return mVpn.prepare(oldPackage, newPackage); } @@ -3128,6 +3189,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { */ @Override public ParcelFileDescriptor establishVpn(VpnConfig config) { + throwIfLockdownEnabled(); return mVpn.establish(config); } @@ -3137,6 +3199,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { */ @Override public void startLegacyVpn(VpnProfile profile) { + throwIfLockdownEnabled(); final LinkProperties egress = getActiveLinkProperties(); if (egress == null) { throw new IllegalStateException("Missing active network connection"); @@ -3152,6 +3215,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { */ @Override public LegacyVpnInfo getLegacyVpnInfo() { + throwIfLockdownEnabled(); return mVpn.getLegacyVpnInfo(); } @@ -3170,8 +3234,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { } public void onStateChanged(NetworkInfo info) { - // TODO: if connected, release delayed broadcast - // TODO: if disconnected, consider kicking off reconnect + mHandler.obtainMessage(EVENT_VPN_STATE_CHANGED, info).sendToTarget(); } public void override(List<String> dnsServers, List<String> searchDomains) { @@ -3240,4 +3303,58 @@ public class ConnectivityService extends IConnectivityManager.Stub { } } } + + @Override + public boolean updateLockdownVpn() { + mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG); + + // Tear down existing lockdown if profile was removed + mLockdownEnabled = LockdownVpnTracker.isEnabled(); + if (mLockdownEnabled) { + if (mKeyStore.state() != KeyStore.State.UNLOCKED) { + Slog.w(TAG, "KeyStore locked; unable to create LockdownTracker"); + return false; + } + + final String profileName = new String(mKeyStore.get(Credentials.LOCKDOWN_VPN)); + final VpnProfile profile = VpnProfile.decode( + profileName, mKeyStore.get(Credentials.VPN + profileName)); + setLockdownTracker(new LockdownVpnTracker(mContext, mNetd, this, mVpn, profile)); + } else { + setLockdownTracker(null); + } + + return true; + } + + /** + * Internally set new {@link LockdownVpnTracker}, shutting down any existing + * {@link LockdownVpnTracker}. Can be {@code null} to disable lockdown. + */ + private void setLockdownTracker(LockdownVpnTracker tracker) { + // Shutdown any existing tracker + final LockdownVpnTracker existing = mLockdownTracker; + mLockdownTracker = null; + if (existing != null) { + existing.shutdown(); + } + + try { + if (tracker != null) { + mNetd.setFirewallEnabled(true); + mLockdownTracker = tracker; + mLockdownTracker.init(); + } else { + mNetd.setFirewallEnabled(false); + } + } catch (RemoteException e) { + // ignored; NMS lives inside system_server + } + } + + private void throwIfLockdownEnabled() { + if (mLockdownEnabled) { + throw new IllegalStateException("Unavailable in lockdown mode"); + } + } } diff --git a/services/java/com/android/server/NetworkManagementService.java b/services/java/com/android/server/NetworkManagementService.java index fbd45a0..efa16af 100644 --- a/services/java/com/android/server/NetworkManagementService.java +++ b/services/java/com/android/server/NetworkManagementService.java @@ -58,6 +58,7 @@ import android.util.SparseBooleanArray; import com.android.internal.net.NetworkStatsFactory; import com.android.internal.util.Preconditions; import com.android.server.NativeDaemonConnector.Command; +import com.android.server.net.LockdownVpnTracker; import com.google.android.collect.Maps; import java.io.BufferedReader; @@ -370,7 +371,7 @@ public class NetworkManagementService extends INetworkManagementService.Stub } // TODO: Push any existing firewall state - setFirewallEnabled(mFirewallEnabled); + setFirewallEnabled(mFirewallEnabled || LockdownVpnTracker.isEnabled()); } // diff --git a/services/java/com/android/server/connectivity/Vpn.java b/services/java/com/android/server/connectivity/Vpn.java index d96bd0d..b3cbb84 100644 --- a/services/java/com/android/server/connectivity/Vpn.java +++ b/services/java/com/android/server/connectivity/Vpn.java @@ -90,6 +90,7 @@ public class Vpn extends BaseNetworkStateTracker { private Connection mConnection; private LegacyVpnRunner mLegacyVpnRunner; private PendingIntent mStatusIntent; + private boolean mEnableNotif = true; public Vpn(Context context, VpnCallback callback, INetworkManagementService netService) { // TODO: create dedicated TYPE_VPN network type @@ -104,6 +105,10 @@ public class Vpn extends BaseNetworkStateTracker { } } + public void setEnableNotifications(boolean enableNotif) { + mEnableNotif = enableNotif; + } + @Override protected void startMonitoringInternal() { // Ignored; events are sent through callbacks for now @@ -394,6 +399,7 @@ public class Vpn extends BaseNetworkStateTracker { } private void showNotification(VpnConfig config, String label, Bitmap icon) { + if (!mEnableNotif) return; mStatusIntent = VpnConfig.getIntentForStatusPanel(mContext, config); NotificationManager nm = (NotificationManager) @@ -420,6 +426,7 @@ public class Vpn extends BaseNetworkStateTracker { } private void hideNotification() { + if (!mEnableNotif) return; mStatusIntent = null; NotificationManager nm = (NotificationManager) @@ -598,6 +605,14 @@ public class Vpn extends BaseNetworkStateTracker { return info; } + public VpnConfig getLegacyVpnConfig() { + if (mLegacyVpnRunner != null) { + return mLegacyVpnRunner.mConfig; + } else { + return null; + } + } + /** * Bringing up a VPN connection takes time, and that is all this thread * does. Here we have plenty of time. The only thing we need to take diff --git a/services/java/com/android/server/net/LockdownVpnTracker.java b/services/java/com/android/server/net/LockdownVpnTracker.java new file mode 100644 index 0000000..541650e --- /dev/null +++ b/services/java/com/android/server/net/LockdownVpnTracker.java @@ -0,0 +1,271 @@ +/* + * 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); + } +} |