diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml index b832e162bb..23467e78d9 100644 --- a/Tethering/AndroidManifest.xml +++ b/Tethering/AndroidManifest.xml @@ -43,7 +43,9 @@ + + { try { mService.startNattKeepaliveWithFd(mNetwork, mPfd, mResourceId, - intervalSec, mCallback, - mSource.getHostAddress(), mDestination.getHostAddress()); + intervalSec, mCallback, mSource.getHostAddress(), + mDestination.getHostAddress(), automaticOnOffKeepalives); } catch (RemoteException e) { Log.e(TAG, "Error starting socket keepalive: ", e); throw e.rethrowFromSystemServer(); diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java index 148661942c..732bd871f5 100644 --- a/framework/src/android/net/NetworkAgent.java +++ b/framework/src/android/net/NetworkAgent.java @@ -483,6 +483,20 @@ public abstract class NetworkAgent { */ public static final int EVENT_UNREGISTER_AFTER_REPLACEMENT = BASE + 29; + /** + * Sent by AutomaticOnOffKeepaliveTracker periodically (when relevant) to trigger monitor + * automatic keepalive request. + * + * NATT keepalives have an automatic mode where the system only sends keepalive packets when + * TCP sockets are open over a VPN. The system will check periodically for presence of + * such open sockets, and this message is what triggers the re-evaluation. + * + * arg1 = hardware slot number of the keepalive + * obj = {@link Network} that the keepalive is started on. + * @hide + */ + public static final int CMD_MONITOR_AUTOMATIC_KEEPALIVE = BASE + 30; + private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) { final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType, config.legacyTypeName, config.legacySubTypeName); diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java index 57cf5e3782..90e5e9b34f 100644 --- a/framework/src/android/net/SocketKeepalive.java +++ b/framework/src/android/net/SocketKeepalive.java @@ -16,6 +16,8 @@ package android.net; +import static android.annotation.SystemApi.Client.PRIVILEGED_APPS; + import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -173,6 +175,27 @@ public abstract class SocketKeepalive implements AutoCloseable { }) public @interface KeepaliveEvent {} + /** + * Whether the system automatically toggles keepalive when no TCP connection is open on the VPN. + * + * If this flag is present, the system will monitor the VPN(s) running on top of the specified + * network for open TCP connections. When no such connections are open, it will turn off the + * keepalives to conserve battery power. When there is at least one such connection it will + * turn on the keepalives to make sure functionality is preserved. + * + * This only works with {@link NattSocketKeepalive}. + * @hide + */ + @SystemApi + public static final int FLAG_AUTOMATIC_ON_OFF = 1 << 0; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "FLAG_"}, flag = true, value = { + FLAG_AUTOMATIC_ON_OFF + }) + public @interface StartFlags {} + /** * The minimum interval in seconds between keepalive packet transmissions. * @@ -294,13 +317,15 @@ public abstract class SocketKeepalive implements AutoCloseable { } /** - * Request that keepalive be started with the given {@code intervalSec}. See - * {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an exception - * when invoking start or stop of the {@link SocketKeepalive}, a {@link RemoteException} will be - * thrown into the {@code executor}. This is typically not important to catch because the remote - * party is the system, so if it is not in shape to communicate through binder the system is - * probably going down anyway. If the caller cares regardless, it can use a custom - * {@link Executor} to catch the {@link RemoteException}. + * Request that keepalive be started with the given {@code intervalSec}. + * + * See {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an + * exception when invoking start or stop of the {@link SocketKeepalive}, a + * {@link RuntimeException} caused by a {@link RemoteException} will be thrown into the + * {@link Executor}. This is typically not important to catch because the remote party is + * the system, so if it is not in shape to communicate through binder the system is going + * down anyway. If the caller still cares, it can use a custom {@link Executor} to catch the + * {@link RuntimeException}. * * @param intervalSec The target interval in seconds between keepalive packet transmissions. * The interval should be between 10 seconds and 3600 seconds, otherwise @@ -308,11 +333,35 @@ public abstract class SocketKeepalive implements AutoCloseable { */ public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC) int intervalSec) { - startImpl(intervalSec); + startImpl(intervalSec, 0 /* flags */); + } + + /** + * Request that keepalive be started with the given {@code intervalSec}. + * + * See {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an + * exception when invoking start or stop of the {@link SocketKeepalive}, a + * {@link RuntimeException} caused by a {@link RemoteException} will be thrown into the + * {@link Executor}. This is typically not important to catch because the remote party is + * the system, so if it is not in shape to communicate through binder the system is going + * down anyway. If the caller still cares, it can use a custom {@link Executor} to catch the + * {@link RuntimeException}. + * + * @param intervalSec The target interval in seconds between keepalive packet transmissions. + * The interval should be between 10 seconds and 3600 seconds. Otherwise, + * the supplied {@link Callback} will see a call to + * {@link Callback#onError(int)} with {@link #ERROR_INVALID_INTERVAL}. + * @param flags Flags to enable/disable available options on this keepalive. + * @hide + */ + @SystemApi(client = PRIVILEGED_APPS) + public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC) + int intervalSec, @StartFlags int flags) { + startImpl(intervalSec, flags); } /** @hide */ - protected abstract void startImpl(int intervalSec); + protected abstract void startImpl(int intervalSec, @StartFlags int flags); /** * Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped} diff --git a/framework/src/android/net/TcpSocketKeepalive.java b/framework/src/android/net/TcpSocketKeepalive.java index 7131784f78..51d805e9aa 100644 --- a/framework/src/android/net/TcpSocketKeepalive.java +++ b/framework/src/android/net/TcpSocketKeepalive.java @@ -50,7 +50,11 @@ public final class TcpSocketKeepalive extends SocketKeepalive { * acknowledgement. */ @Override - protected void startImpl(int intervalSec) { + protected void startImpl(int intervalSec, int flags) { + if (0 != flags) { + throw new IllegalArgumentException("Illegal flag value for " + + this.getClass().getSimpleName() + " : " + flags); + } mExecutor.execute(() -> { try { mService.startTcpKeepalive(mNetwork, mPfd, intervalSec, mCallback); diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 87ac0a874d..b9d276070c 100755 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -101,7 +101,6 @@ import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValida import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf; import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission; import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr; -import static com.android.server.connectivity.KeepaliveTracker.PERMISSION; import static java.util.Map.Entry; @@ -278,6 +277,7 @@ import com.android.server.connectivity.DnsManager; import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate; import com.android.server.connectivity.DscpPolicyTracker; import com.android.server.connectivity.FullScore; +import com.android.server.connectivity.KeepaliveTracker; import com.android.server.connectivity.LingerMonitor; import com.android.server.connectivity.MockableSystemProperties; import com.android.server.connectivity.MultinetworkPolicyTracker; @@ -2999,7 +2999,7 @@ public class ConnectivityService extends IConnectivityManager.Stub } private void enforceKeepalivePermission() { - mContext.enforceCallingOrSelfPermission(PERMISSION, "ConnectivityService"); + mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService"); } private boolean checkLocalMacAddressPermission(int pid, int uid) { @@ -5545,6 +5545,33 @@ public class ConnectivityService extends IConnectivityManager.Stub mKeepaliveTracker.handleStartKeepalive(msg); break; } + case NetworkAgent.CMD_MONITOR_AUTOMATIC_KEEPALIVE: { + final Network network = (Network) msg.obj; + final int slot = msg.arg1; + + boolean networkFound = false; + final ArrayList vpnsRunningOnThisNetwork = new ArrayList<>(); + for (NetworkAgentInfo n : mNetworkAgentInfos) { + if (n.network.equals(network)) networkFound = true; + if (n.isVPN() && n.everConnected() && hasUnderlyingNetwork(n, network)) { + vpnsRunningOnThisNetwork.add(n); + } + } + + // If the network no longer exists, then the keepalive should have been + // cleaned up already. There is no point trying to resume keepalives. + if (!networkFound) return; + + if (!vpnsRunningOnThisNetwork.isEmpty()) { + mKeepaliveTracker.handleMonitorAutomaticKeepalive(network, slot, + // TODO: check all the VPNs running on top of this network + vpnsRunningOnThisNetwork.get(0).network.netId); + } else { + // If no VPN, then make sure the keepalive is running. + mKeepaliveTracker.handleMaybeResumeKeepalive(network, slot); + } + break; + } // Sent by KeepaliveTracker to process an app request on the state machine thread. case NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE: { NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj); @@ -9789,20 +9816,23 @@ public class ConnectivityService extends IConnectivityManager.Stub enforceKeepalivePermission(); mKeepaliveTracker.startNattKeepalive( getNetworkAgentInfoForNetwork(network), null /* fd */, - intervalSeconds, cb, - srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT); + intervalSeconds, cb, srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT, + // Keep behavior of the deprecated method as it is. Set automaticOnOffKeepalives to + // false because there is no way and no plan to configure automaticOnOffKeepalives + // in this deprecated method. + false /* automaticOnOffKeepalives */); } @Override public void startNattKeepaliveWithFd(Network network, ParcelFileDescriptor pfd, int resourceId, int intervalSeconds, ISocketKeepaliveCallback cb, String srcAddr, - String dstAddr) { + String dstAddr, boolean automaticOnOffKeepalives) { try { final FileDescriptor fd = pfd.getFileDescriptor(); mKeepaliveTracker.startNattKeepalive( getNetworkAgentInfoForNetwork(network), fd, resourceId, intervalSeconds, cb, - srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT); + srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT, automaticOnOffKeepalives); } finally { // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks. // startNattKeepalive calls Os.dup(fd) before returning, so we can close immediately. diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java index 85ec5e3739..5d1d378474 100644 --- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java +++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java @@ -16,6 +16,9 @@ package com.android.server.connectivity; +import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE; +import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET; +import static android.net.SocketKeepalive.SUCCESS; import static android.system.OsConstants.AF_INET; import static android.system.OsConstants.AF_INET6; import static android.system.OsConstants.SOL_SOCKET; @@ -26,16 +29,28 @@ import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_ import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY; import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.net.INetd; import android.net.ISocketKeepaliveCallback; import android.net.MarkMaskParcel; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.SocketKeepalive; +import android.net.SocketKeepalive.InvalidSocketException; +import android.os.FileUtils; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; +import android.os.SystemClock; import android.system.ErrnoException; import android.system.Os; import android.system.StructTimeval; @@ -44,6 +59,7 @@ import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; +import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.HexDump; import com.android.net.module.util.SocketUtils; import com.android.net.module.util.netlink.InetDiagMessage; @@ -52,23 +68,57 @@ import com.android.net.module.util.netlink.StructNlAttr; import java.io.FileDescriptor; import java.io.InterruptedIOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.SocketException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; import java.util.Objects; /** * Manages automatic on/off socket keepalive requests. * * Provides methods to stop and start automatic keepalive requests, and keeps track of keepalives - * across all networks. For non-automatic on/off keepalive request, this class bypass the requests - * and send to KeepaliveTrakcer. This class is tightly coupled to ConnectivityService. It is not + * across all networks. For non-automatic on/off keepalive request, this class just forwards the + * requests to KeepaliveTracker. This class is tightly coupled to ConnectivityService. It is not * thread-safe and its handle* methods must be called only from the ConnectivityService handler * thread. */ public class AutomaticOnOffKeepaliveTracker { private static final String TAG = "AutomaticOnOffKeepaliveTracker"; private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET}; + private static final String ACTION_TCP_POLLING_ALARM = + "com.android.server.connectivity.KeepaliveTracker.TCP_POLLING_ALARM"; + private static final String EXTRA_NETWORK = "network_id"; + private static final String EXTRA_SLOT = "slot"; + private static final long DEFAULT_TCP_POLLING_INTERVAL_MS = 120_000L; + /** + * States for {@code #AutomaticOnOffKeepalive}. + * + * A new AutomaticOnOffKeepalive starts with STATE_ENABLED. The system will monitor + * the TCP sockets on VPN networks running on top of the specified network, and turn off + * keepalive if there is no TCP socket any of the VPN networks. Conversely, it will turn + * keepalive back on if any TCP socket is open on any of the VPN networks. + * + * When there is no TCP socket on any of the VPN networks, the state becomes STATE_SUSPENDED. + * The {@link KeepaliveTracker.KeepaliveInfo} object is kept to remember the parameters so it + * is possible to resume keepalive later with the same parameters. + * + * When the system detects some TCP socket is open on one of the VPNs while in STATE_SUSPENDED, + * this AutomaticOnOffKeepalive goes to STATE_ENABLED again. + * + * When finishing keepalive, this object is deleted. + */ + private static final int STATE_ENABLED = 0; + private static final int STATE_SUSPENDED = 1; + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "STATE_" }, value = { + STATE_ENABLED, + STATE_SUSPENDED + }) + private @interface AutomaticOnOffState {} @NonNull private final Handler mConnectivityServiceHandler; @@ -76,6 +126,8 @@ public class AutomaticOnOffKeepaliveTracker { private final KeepaliveTracker mKeepaliveTracker; @NonNull private final Context mContext; + @NonNull + private final AlarmManager mAlarmManager; /** * The {@code inetDiagReqV2} messages for different IP family. @@ -88,8 +140,73 @@ public class AutomaticOnOffKeepaliveTracker { private final SparseArray mSockDiagMsg = new SparseArray<>(); private final Dependencies mDependencies; private final INetd mNetd; + /** + * Keeps track of automatic on/off keepalive requests. + * This should be only updated in ConnectivityService handler thread. + */ + private final ArrayList mAutomaticOnOffKeepalives = new ArrayList<>(); - public AutomaticOnOffKeepaliveTracker(Context context, Handler handler) { + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_TCP_POLLING_ALARM.equals(intent.getAction())) { + Log.d(TAG, "Received TCP polling intent"); + final Network network = intent.getParcelableExtra(EXTRA_NETWORK); + final int slot = intent.getIntExtra(EXTRA_SLOT, -1); + mConnectivityServiceHandler.obtainMessage( + NetworkAgent.CMD_MONITOR_AUTOMATIC_KEEPALIVE, + slot, 0 , network).sendToTarget(); + } + } + }; + + private static class AutomaticOnOffKeepalive { + @NonNull + private final KeepaliveTracker.KeepaliveInfo mKi; + @NonNull + private final FileDescriptor mFd; + @NonNull + private final PendingIntent mTcpPollingAlarm; + private final int mSlot; + @AutomaticOnOffState + private int mAutomaticOnOffState = STATE_ENABLED; + + AutomaticOnOffKeepalive(@NonNull KeepaliveTracker.KeepaliveInfo ki, + @NonNull Context context) throws InvalidSocketException { + this.mKi = Objects.requireNonNull(ki); + // A null fd is acceptable in KeepaliveInfo for backward compatibility of + // PacketKeepalive API, but it should not happen here because legacy API cannot setup + // automatic keepalive. + Objects.requireNonNull(ki.mFd); + + // Get the slot from keepalive because the slot information may be missing when the + // keepalive is stopped. + this.mSlot = ki.getSlot(); + try { + this.mFd = Os.dup(ki.mFd); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot dup fd: ", e); + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + mTcpPollingAlarm = createTcpPollingAlarmIntent( + context, ki.getNai().network(), ki.getSlot()); + } + + public boolean match(Network network, int slot) { + return this.mKi.getNai().network().equals(network) && this.mSlot == slot; + } + + private static PendingIntent createTcpPollingAlarmIntent(@NonNull Context context, + @NonNull Network network, int slot) { + final Intent intent = new Intent(ACTION_TCP_POLLING_ALARM); + intent.putExtra(EXTRA_NETWORK, network); + intent.putExtra(EXTRA_SLOT, slot); + return PendingIntent.getBroadcast( + context, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE); + } + } + + public AutomaticOnOffKeepaliveTracker(@NonNull Context context, @NonNull Handler handler) { this(context, handler, new Dependencies(context)); } @@ -97,15 +214,111 @@ public class AutomaticOnOffKeepaliveTracker { public AutomaticOnOffKeepaliveTracker(@NonNull Context context, @NonNull Handler handler, @NonNull Dependencies dependencies) { mContext = Objects.requireNonNull(context); - mDependencies = dependencies; - this.mConnectivityServiceHandler = Objects.requireNonNull(handler); + mDependencies = Objects.requireNonNull(dependencies); + mConnectivityServiceHandler = Objects.requireNonNull(handler); mNetd = mDependencies.getNetd(); mKeepaliveTracker = mDependencies.newKeepaliveTracker( mContext, mConnectivityServiceHandler); + + if (SdkLevel.isAtLeastU()) { + mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_TCP_POLLING_ALARM), + null, handler); + } + mAlarmManager = mContext.getSystemService(AlarmManager.class); + } + + private void startTcpPollingAlarm(@NonNull PendingIntent alarm) { + final long triggerAtMillis = + SystemClock.elapsedRealtime() + DEFAULT_TCP_POLLING_INTERVAL_MS; + // Setup a non-wake up alarm. + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, triggerAtMillis, alarm); + } + + /** + * Determine if any state transition is needed for the specific automatic keepalive. + */ + public void handleMonitorAutomaticKeepalive(@NonNull Network network, int slot, int vpnNetId) { + final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(network, slot); + // This may happen if the keepalive is removed by the app, and the alarm is fired at the + // same time. + if (autoKi == null) return; + + handleMonitorTcpConnections(autoKi, vpnNetId); + } + + /** + * Determine if disable or re-enable keepalive is needed or not based on TCP sockets status. + */ + private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId) { + if (!isAnyTcpSocketConnected(vpnNetId)) { + // No TCP socket exists. Stop keepalive if ENABLED, and remain SUSPENDED if currently + // SUSPENDED. + if (ki.mAutomaticOnOffState == STATE_ENABLED) { + ki.mAutomaticOnOffState = STATE_SUSPENDED; + handleSuspendKeepalive(ki.mKi.mNai, ki.mSlot, SUCCESS); + } + } else { + handleMaybeResumeKeepalive(ki); + } + // TODO: listen to socket status instead of periodically check. + startTcpPollingAlarm(ki.mTcpPollingAlarm); + } + + /** + * Resume keepalive for this slot on this network, if it wasn't already resumed. + */ + public void handleMaybeResumeKeepalive(@NonNull final Network network, final int slot) { + final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(network, slot); + // This may happen if the keepalive is removed by the app, and the alarm is fired at + // the same time. + if (autoKi == null) return; + handleMaybeResumeKeepalive(autoKi); + } + + private void handleMaybeResumeKeepalive(@NonNull AutomaticOnOffKeepalive autoKi) { + if (autoKi.mAutomaticOnOffState == STATE_ENABLED) return; + KeepaliveTracker.KeepaliveInfo newKi; + try { + // Get fd from AutomaticOnOffKeepalive since the fd in the original + // KeepaliveInfo should be closed. + newKi = autoKi.mKi.withFd(autoKi.mFd); + } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) { + Log.e(TAG, "Fail to construct keepalive", e); + mKeepaliveTracker.notifyErrorCallback(autoKi.mKi.mCallback, ERROR_INVALID_SOCKET); + return; + } + autoKi.mAutomaticOnOffState = STATE_ENABLED; + handleResumeKeepalive(mConnectivityServiceHandler.obtainMessage( + NetworkAgent.CMD_START_SOCKET_KEEPALIVE, + autoKi.mAutomaticOnOffState, 0, newKi)); + } + + private int findAutomaticOnOffKeepaliveIndex(@NonNull Network network, int slot) { + ensureRunningOnHandlerThread(); + + int index = 0; + for (AutomaticOnOffKeepalive ki : mAutomaticOnOffKeepalives) { + if (ki.match(network, slot)) { + return index; + } + index++; + } + return -1; + } + + @Nullable + private AutomaticOnOffKeepalive findAutomaticOnOffKeepalive(@NonNull Network network, + int slot) { + ensureRunningOnHandlerThread(); + + final int index = findAutomaticOnOffKeepaliveIndex(network, slot); + return (index >= 0) ? mAutomaticOnOffKeepalives.get(index) : null; } /** * Handle keepalive events from lower layer. + * + * Forward to KeepaliveTracker. */ public void handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) { mKeepaliveTracker.handleEventSocketKeepalive(nai, slot, reason); @@ -116,27 +329,86 @@ public class AutomaticOnOffKeepaliveTracker { */ public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) { mKeepaliveTracker.handleStopAllKeepalives(nai, reason); + final Iterator iterator = mAutomaticOnOffKeepalives.iterator(); + while (iterator.hasNext()) { + final AutomaticOnOffKeepalive autoKi = iterator.next(); + if (autoKi.mKi.getNai() == nai) { + cleanupAutoOnOffKeepalive(autoKi); + iterator.remove(); + } + } } /** - * Handle start keepalives with the message. + * Handle start keepalive contained within a message. * - * The message is expected to be a KeepaliveTracker.KeepaliveInfo. + * The message is expected to contain a KeepaliveTracker.KeepaliveInfo. */ public void handleStartKeepalive(Message message) { mKeepaliveTracker.handleStartKeepalive(message); + + // Add automatic on/off request into list to track its life cycle. + final boolean automaticOnOff = message.arg1 != 0; + if (automaticOnOff) { + final KeepaliveTracker.KeepaliveInfo ki = (KeepaliveTracker.KeepaliveInfo) message.obj; + AutomaticOnOffKeepalive autoKi; + try { + // CAREFUL : mKeepaliveTracker.handleStartKeepalive will assign |ki.mSlot| after + // pulling |ki| from the message. The constructor below will read this member + // (through ki.getSlot()) and therefore actively relies on handleStartKeepalive + // having assigned this member before this is called. + // TODO : clean this up by assigning the slot at the start of this method instead + // and ideally removing the mSlot member from KeepaliveInfo. + autoKi = new AutomaticOnOffKeepalive(ki, mContext); + } catch (SocketKeepalive.InvalidSocketException | IllegalArgumentException e) { + Log.e(TAG, "Fail to construct keepalive", e); + mKeepaliveTracker.notifyErrorCallback(ki.mCallback, ERROR_INVALID_SOCKET); + return; + } + mAutomaticOnOffKeepalives.add(autoKi); + startTcpPollingAlarm(autoKi.mTcpPollingAlarm); + } + } + + private void handleResumeKeepalive(Message message) { + mKeepaliveTracker.handleStartKeepalive(message); + } + + private void handleSuspendKeepalive(NetworkAgentInfo nai, int slot, int reason) { + mKeepaliveTracker.handleStopKeepalive(nai, slot, reason); } /** * Handle stop keepalives on the specific network with given slot. */ public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) { - mKeepaliveTracker.handleStopKeepalive(nai, slot, reason); + final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(nai.network, slot); + + // Let the original keepalive do the stop first, and then clean up the keepalive if it's an + // automatic keepalive. + if (autoKi == null || autoKi.mAutomaticOnOffState == STATE_ENABLED) { + mKeepaliveTracker.handleStopKeepalive(nai, slot, reason); + } + + // Not an AutomaticOnOffKeepalive. + if (autoKi == null) return; + + cleanupAutoOnOffKeepalive(autoKi); + mAutomaticOnOffKeepalives.remove(autoKi); + } + + private void cleanupAutoOnOffKeepalive(@NonNull final AutomaticOnOffKeepalive autoKi) { + ensureRunningOnHandlerThread(); + mAlarmManager.cancel(autoKi.mTcpPollingAlarm); + // Close the duplicated fd that maintains the lifecycle of socket. + FileUtils.closeQuietly(autoKi.mFd); } /** * Called when requesting that keepalives be started on a IPsec NAT-T socket. See * {@link android.net.SocketKeepalive}. + * + * Forward to KeepaliveTracker. **/ public void startNattKeepalive(@Nullable NetworkAgentInfo nai, @Nullable FileDescriptor fd, @@ -145,14 +417,21 @@ public class AutomaticOnOffKeepaliveTracker { @NonNull String srcAddrString, int srcPort, @NonNull String dstAddrString, - int dstPort) { - mKeepaliveTracker.startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString, - srcPort, dstAddrString, dstPort); + int dstPort, boolean automaticOnOffKeepalives) { + final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeNattKeepaliveInfo(nai, fd, + intervalSeconds, cb, srcAddrString, srcPort, dstAddrString, dstPort); + if (null != ki) { + mConnectivityServiceHandler.obtainMessage(NetworkAgent.CMD_START_SOCKET_KEEPALIVE, + // TODO : move ConnectivityService#encodeBool to a static lib. + automaticOnOffKeepalives ? 1 : 0, 0, ki).sendToTarget(); + } } /** * Called when requesting that keepalives be started on a IPsec NAT-T socket. See * {@link android.net.SocketKeepalive}. + * + * Forward to KeepaliveTracker. **/ public void startNattKeepalive(@Nullable NetworkAgentInfo nai, @Nullable FileDescriptor fd, @@ -161,9 +440,15 @@ public class AutomaticOnOffKeepaliveTracker { @NonNull ISocketKeepaliveCallback cb, @NonNull String srcAddrString, @NonNull String dstAddrString, - int dstPort) { - mKeepaliveTracker.startNattKeepalive(nai, fd, resourceId, intervalSeconds, cb, - srcAddrString, dstAddrString, dstPort); + int dstPort, + boolean automaticOnOffKeepalives) { + final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeNattKeepaliveInfo(nai, fd, + resourceId, intervalSeconds, cb, srcAddrString, dstAddrString, dstPort); + if (null != ki) { + mConnectivityServiceHandler.obtainMessage(NetworkAgent.CMD_START_SOCKET_KEEPALIVE, + // TODO : move ConnectivityService#encodeBool to a static lib. + automaticOnOffKeepalives ? 1 : 0, 0, ki).sendToTarget(); + } } /** @@ -173,26 +458,34 @@ public class AutomaticOnOffKeepaliveTracker { * other fields are needed to form the keepalive packet. Thus, this function synchronously * puts the socket into repair mode to get the necessary information. After the socket has been * put into repair mode, the application cannot access the socket until reverted to normal. - * * See {@link android.net.SocketKeepalive}. + * + * Forward to KeepaliveTracker. **/ public void startTcpKeepalive(@Nullable NetworkAgentInfo nai, @NonNull FileDescriptor fd, int intervalSeconds, @NonNull ISocketKeepaliveCallback cb) { - mKeepaliveTracker.startTcpKeepalive(nai, fd, intervalSeconds, cb); + final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeTcpKeepaliveInfo(nai, fd, + intervalSeconds, cb); + if (null != ki) { + mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki) + .sendToTarget(); + } } /** * Dump AutomaticOnOffKeepaliveTracker state. */ public void dump(IndentingPrintWriter pw) { - // TODO: Dump the necessary information for automatic on/off keepalive. + // TODO: Dump the necessary information for automatic on/off keepalive. mKeepaliveTracker.dump(pw); } /** - * Check all keeplaives on the network are still valid. + * Check all keepalives on the network are still valid. + * + * Forward to KeepaliveTracker. */ public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) { mKeepaliveTracker.handleCheckKeepalivesStillValid(nai); diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java index 23fdfd4080..03f8f3e54f 100644 --- a/service/src/com/android/server/connectivity/KeepaliveTracker.java +++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java @@ -18,7 +18,6 @@ package com.android.server.connectivity; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.net.NattSocketKeepalive.NATT_PORT; -import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE; import static android.net.SocketKeepalive.BINDER_DIED; import static android.net.SocketKeepalive.DATA_RECEIVED; import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES; @@ -88,7 +87,6 @@ public class KeepaliveTracker { /** Keeps track of keepalive requests. */ private final HashMap > mKeepalives = new HashMap<> (); - private final Handler mConnectivityServiceHandler; @NonNull private final TcpKeepaliveController mTcpController; @NonNull @@ -109,7 +107,6 @@ public class KeepaliveTracker { private final int mAllowedUnprivilegedSlotsForUid; public KeepaliveTracker(Context context, Handler handler) { - mConnectivityServiceHandler = handler; mTcpController = new TcpKeepaliveController(handler); mContext = context; @@ -130,13 +127,13 @@ public class KeepaliveTracker { */ class KeepaliveInfo implements IBinder.DeathRecipient { // Bookkeeping data. - private final ISocketKeepaliveCallback mCallback; + public final ISocketKeepaliveCallback mCallback; private final int mUid; private final int mPid; private final boolean mPrivileged; - private final NetworkAgentInfo mNai; + public final NetworkAgentInfo mNai; private final int mType; - private final FileDescriptor mFd; + public final FileDescriptor mFd; public static final int TYPE_NATT = 1; public static final int TYPE_TCP = 2; @@ -244,6 +241,10 @@ public class KeepaliveTracker { } } + public int getSlot() { + return mSlot; + } + private int checkNetworkConnected() { if (!mNai.networkInfo.isConnectedOrConnecting()) { return ERROR_INVALID_NETWORK; @@ -416,6 +417,13 @@ public class KeepaliveTracker { void onFileDescriptorInitiatedStop(final int socketKeepaliveReason) { handleStopKeepalive(mNai, mSlot, socketKeepaliveReason); } + + /** + * Construct a new KeepaliveInfo from existing KeepaliveInfo with a new fd. + */ + public KeepaliveInfo withFd(@NonNull FileDescriptor fd) throws InvalidSocketException { + return new KeepaliveInfo(mCallback, mNai, mPacket, mInterval, mType, fd); + } } void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) { @@ -445,6 +453,9 @@ public class KeepaliveTracker { return slot; } + /** + * Handle start keepalives with the message. + */ public void handleStartKeepalive(Message message) { KeepaliveInfo ki = (KeepaliveInfo) message.obj; NetworkAgentInfo nai = ki.getNai(); @@ -605,7 +616,8 @@ public class KeepaliveTracker { * Called when requesting that keepalives be started on a IPsec NAT-T socket. See * {@link android.net.SocketKeepalive}. **/ - public void startNattKeepalive(@Nullable NetworkAgentInfo nai, + @Nullable + public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai, @Nullable FileDescriptor fd, int intervalSeconds, @NonNull ISocketKeepaliveCallback cb, @@ -615,7 +627,7 @@ public class KeepaliveTracker { int dstPort) { if (nai == null) { notifyErrorCallback(cb, ERROR_INVALID_NETWORK); - return; + return null; } InetAddress srcAddress, dstAddress; @@ -624,7 +636,7 @@ public class KeepaliveTracker { dstAddress = InetAddresses.parseNumericAddress(dstAddrString); } catch (IllegalArgumentException e) { notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS); - return; + return null; } KeepalivePacketData packet; @@ -633,7 +645,7 @@ public class KeepaliveTracker { srcAddress, srcPort, dstAddress, NATT_PORT); } catch (InvalidPacketException e) { notifyErrorCallback(cb, e.getError()); - return; + return null; } KeepaliveInfo ki = null; try { @@ -642,15 +654,14 @@ public class KeepaliveTracker { } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) { Log.e(TAG, "Fail to construct keepalive", e); notifyErrorCallback(cb, ERROR_INVALID_SOCKET); - return; + return null; } - Log.d(TAG, "Created keepalive: " + ki.toString()); - mConnectivityServiceHandler.obtainMessage( - NetworkAgent.CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget(); + Log.d(TAG, "Created keepalive: " + ki); + return ki; } /** - * Called by ConnectivityService to start TCP keepalive on a file descriptor. + * Make a KeepaliveInfo for a TCP socket. * * In order to offload keepalive for application correctly, sequence number, ack number and * other fields are needed to form the keepalive packet. Thus, this function synchronously @@ -659,13 +670,14 @@ public class KeepaliveTracker { * * See {@link android.net.SocketKeepalive}. **/ - public void startTcpKeepalive(@Nullable NetworkAgentInfo nai, + @Nullable + public KeepaliveInfo makeTcpKeepaliveInfo(@Nullable NetworkAgentInfo nai, @NonNull FileDescriptor fd, int intervalSeconds, @NonNull ISocketKeepaliveCallback cb) { if (nai == null) { notifyErrorCallback(cb, ERROR_INVALID_NETWORK); - return; + return null; } final TcpKeepalivePacketData packet; @@ -673,10 +685,10 @@ public class KeepaliveTracker { packet = TcpKeepaliveController.getTcpKeepalivePacket(fd); } catch (InvalidSocketException e) { notifyErrorCallback(cb, e.error); - return; + return null; } catch (InvalidPacketException e) { notifyErrorCallback(cb, e.getError()); - return; + return null; } KeepaliveInfo ki = null; try { @@ -685,20 +697,22 @@ public class KeepaliveTracker { } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) { Log.e(TAG, "Fail to construct keepalive e=" + e); notifyErrorCallback(cb, ERROR_INVALID_SOCKET); - return; + return null; } Log.d(TAG, "Created keepalive: " + ki.toString()); - mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget(); + return ki; } - /** - * Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is - * identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the - * resource index bound to the {@link UdpEncapsulationSocket} when creating by - * {@link com.android.server.IpSecService} to verify whether the given - * {@link UdpEncapsulationSocket} is legitimate. - **/ - public void startNattKeepalive(@Nullable NetworkAgentInfo nai, + /** + * Make a KeepaliveInfo for an IPSec NAT-T socket. + * + * This function is identical to {@link #makeNattKeepaliveInfo}, but also takes a + * {@code resourceId}, which is the resource index bound to the {@link UdpEncapsulationSocket} + * when creating by {@link com.android.server.IpSecService} to verify whether the given + * {@link UdpEncapsulationSocket} is legitimate. + **/ + @Nullable + public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai, @Nullable FileDescriptor fd, int resourceId, int intervalSeconds, @@ -709,6 +723,7 @@ public class KeepaliveTracker { // Ensure that the socket is created by IpSecService. if (!isNattKeepaliveSocketValid(fd, resourceId)) { notifyErrorCallback(cb, ERROR_INVALID_SOCKET); + return null; } // Get src port to adopt old API. @@ -718,10 +733,11 @@ public class KeepaliveTracker { srcPort = ((InetSocketAddress) srcSockAddr).getPort(); } catch (ErrnoException e) { notifyErrorCallback(cb, ERROR_INVALID_SOCKET); + return null; } // Forward request to old API. - startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString, srcPort, + return makeNattKeepaliveInfo(nai, fd, intervalSeconds, cb, srcAddrString, srcPort, dstAddrString, dstPort); }