am 116ee10b: Merge "Always-on VPN." into jb-mr1-dev
* commit '116ee10b95ffff658618be42544ce80971ce28e8': Always-on VPN.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -912,4 +912,13 @@ public class ConnectivityManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** {@hide} */
|
||||
public boolean updateLockdownVpn() {
|
||||
try {
|
||||
return mService.updateLockdownVpn();
|
||||
} catch (RemoteException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,4 +122,6 @@ interface IConnectivityManager
|
||||
void startLegacyVpn(in VpnProfile profile);
|
||||
|
||||
LegacyVpnInfo getLegacyVpnInfo();
|
||||
|
||||
boolean updateLockdownVpn();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
271
services/java/com/android/server/net/LockdownVpnTracker.java
Normal file
271
services/java/com/android/server/net/LockdownVpnTracker.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user