diff options
-rw-r--r-- | core/java/android/app/NotificationManager.java | 8 | ||||
-rw-r--r-- | core/java/android/net/ConnectivityManager.java | 9 | ||||
-rw-r--r-- | core/java/android/net/IConnectivityManager.aidl | 2 | ||||
-rw-r--r-- | core/java/com/android/internal/net/VpnProfile.java | 31 | ||||
-rw-r--r-- | core/res/res/values/public.xml | 5 | ||||
-rwxr-xr-x | core/res/res/values/strings.xml | 9 | ||||
-rw-r--r-- | keystore/java/android/security/Credentials.java | 3 | ||||
-rw-r--r-- | services/java/com/android/server/ConnectivityService.java | 131 | ||||
-rw-r--r-- | services/java/com/android/server/NetworkManagementService.java | 3 | ||||
-rw-r--r-- | services/java/com/android/server/connectivity/Vpn.java | 15 | ||||
-rw-r--r-- | services/java/com/android/server/net/LockdownVpnTracker.java | 271 |
11 files changed, 477 insertions, 10 deletions
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index bf83f5e..69c20b0 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -17,10 +17,9 @@ package android.app; import android.content.Context; -import android.os.Binder; -import android.os.RemoteException; import android.os.Handler; import android.os.IBinder; +import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; @@ -88,6 +87,11 @@ public class NotificationManager mContext = context; } + /** {@hide} */ + public static NotificationManager from(Context context) { + return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + /** * Post a notification to be shown in the status bar. If a notification with * the same id has already been posted by your application and has not yet been canceled, it diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index d30ef04..60bf4d6 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -912,4 +912,13 @@ public class ConnectivityManager { return false; } } + + /** {@hide} */ + public boolean updateLockdownVpn() { + try { + return mService.updateLockdownVpn(); + } catch (RemoteException e) { + return false; + } + } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index dea25dd..3614045 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -122,4 +122,6 @@ interface IConnectivityManager void startLegacyVpn(in VpnProfile profile); LegacyVpnInfo getLegacyVpnInfo(); + + boolean updateLockdownVpn(); } diff --git a/core/java/com/android/internal/net/VpnProfile.java b/core/java/com/android/internal/net/VpnProfile.java index d6c5702..7287327 100644 --- a/core/java/com/android/internal/net/VpnProfile.java +++ b/core/java/com/android/internal/net/VpnProfile.java @@ -18,7 +18,10 @@ package com.android.internal.net; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import java.net.InetAddress; import java.nio.charset.Charsets; /** @@ -31,6 +34,8 @@ import java.nio.charset.Charsets; * @hide */ public class VpnProfile implements Cloneable, Parcelable { + private static final String TAG = "VpnProfile"; + // Match these constants with R.array.vpn_types. public static final int TYPE_PPTP = 0; public static final int TYPE_L2TP_IPSEC_PSK = 1; @@ -124,6 +129,32 @@ public class VpnProfile implements Cloneable, Parcelable { return builder.toString().getBytes(Charsets.UTF_8); } + /** + * Test if profile is valid for lockdown, which requires IPv4 address for + * both server and DNS. Server hostnames would require using DNS before + * connection. + */ + public boolean isValidLockdownProfile() { + try { + InetAddress.parseNumericAddress(server); + + for (String dnsServer : dnsServers.split(" +")) { + InetAddress.parseNumericAddress(this.dnsServers); + } + if (TextUtils.isEmpty(dnsServers)) { + Log.w(TAG, "DNS required"); + return false; + } + + // Everything checked out above + return true; + + } catch (IllegalArgumentException e) { + Log.w(TAG, "Invalid address", e); + return false; + } + } + @Override public void writeToParcel(Parcel out, int flags) { out.writeString(key); diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index e17a05d..d761980 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1442,6 +1442,7 @@ <java-symbol type="drawable" name="stat_sys_tether_usb" /> <java-symbol type="drawable" name="stat_sys_throttled" /> <java-symbol type="drawable" name="vpn_connected" /> + <java-symbol type="drawable" name="vpn_disconnected" /> <java-symbol type="id" name="ask_checkbox" /> <java-symbol type="id" name="compat_checkbox" /> <java-symbol type="id" name="original_app_icon" /> @@ -1557,6 +1558,10 @@ <java-symbol type="string" name="vpn_text_long" /> <java-symbol type="string" name="vpn_title" /> <java-symbol type="string" name="vpn_title_long" /> + <java-symbol type="string" name="vpn_lockdown_connecting" /> + <java-symbol type="string" name="vpn_lockdown_connected" /> + <java-symbol type="string" name="vpn_lockdown_error" /> + <java-symbol type="string" name="vpn_lockdown_reset" /> <java-symbol type="string" name="wallpaper_binding_label" /> <java-symbol type="style" name="Theme.Dialog.AppError" /> <java-symbol type="style" name="Theme.Toast" /> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index b7ad1e9..e77dde7 100755 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3272,6 +3272,15 @@ <!-- The text of the notification when VPN is active with a session name. --> <string name="vpn_text_long">Connected to <xliff:g id="session" example="office">%s</xliff:g>. Touch to manage the network.</string> + <!-- Notification title when connecting to lockdown VPN. --> + <string name="vpn_lockdown_connecting">Always-on VPN connecting\u2026</string> + <!-- Notification title when connected to lockdown VPN. --> + <string name="vpn_lockdown_connected">Always-on VPN connected</string> + <!-- Notification title when error connecting to lockdown VPN. --> + <string name="vpn_lockdown_error">Always-on VPN error</string> + <!-- Notification body that indicates user can touch to cycle lockdown VPN connection. --> + <string name="vpn_lockdown_reset">Touch to reset connection</string> + <!-- Localized strings for WebView --> <!-- Label for button in a WebView that will open a chooser to choose a file to upload --> <string name="upload_file">Choose file</string> diff --git a/keystore/java/android/security/Credentials.java b/keystore/java/android/security/Credentials.java index f6bf432..b233ff6 100644 --- a/keystore/java/android/security/Credentials.java +++ b/keystore/java/android/security/Credentials.java @@ -61,6 +61,9 @@ public class Credentials { /** Key prefix for WIFI. */ public static final String WIFI = "WIFI_"; + /** Key containing suffix of lockdown VPN profile. */ + public static final String LOCKDOWN_VPN = "LOCKDOWN_VPN"; + /** Data type for public keys. */ public static final String EXTRA_PUBLIC_KEY = "KEY"; 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); + } +} |