/* * Copyright (C) 2015 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.connectivity; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.net.NattSocketKeepalive.NATT_PORT; import static android.net.SocketKeepalive.BINDER_DIED; import static android.net.SocketKeepalive.DATA_RECEIVED; import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES; import static android.net.SocketKeepalive.ERROR_INVALID_INTERVAL; import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS; import static android.net.SocketKeepalive.ERROR_INVALID_NETWORK; import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET; import static android.net.SocketKeepalive.ERROR_NO_SUCH_SLOT; import static android.net.SocketKeepalive.ERROR_STOP_REASON_UNINITIALIZED; import static android.net.SocketKeepalive.ERROR_UNSUPPORTED; import static android.net.SocketKeepalive.MAX_INTERVAL_SEC; import static android.net.SocketKeepalive.MIN_INTERVAL_SEC; import static android.net.SocketKeepalive.NO_KEEPALIVE; import static android.net.SocketKeepalive.SUCCESS; import static android.net.SocketKeepalive.SUCCESS_PAUSED; import static com.android.net.module.util.FeatureVersions.FEATURE_CLAT_ADDRESS_TRANSLATE; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.net.ISocketKeepaliveCallback; import android.net.InetAddresses; import android.net.InvalidPacketException; import android.net.KeepalivePacketData; import android.net.NattKeepalivePacketData; import android.net.NetworkAgent; import android.net.SocketKeepalive.InvalidSocketException; import android.net.TcpKeepalivePacketData; import android.net.util.KeepaliveUtils; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.system.ErrnoException; import android.system.Os; import android.util.Log; import android.util.Pair; import com.android.connectivity.resources.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.net.module.util.DeviceConfigUtils; import com.android.net.module.util.HexDump; import com.android.net.module.util.IpUtils; import java.io.FileDescriptor; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; /** * Manages socket keepalive requests. * * Provides methods to stop and start keepalive requests, and keeps track of keepalives across all * networks. 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 KeepaliveTracker { private static final String TAG = "KeepaliveTracker"; private static final boolean DBG = false; public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD; private static final String CONFIG_DISABLE_CLAT_ADDRESS_TRANSLATE = "disable_clat_address_translate"; /** Keeps track of keepalive requests. */ private final HashMap > mKeepalives = new HashMap<> (); @NonNull private final TcpKeepaliveController mTcpController; @NonNull private final Context mContext; // Supported keepalive count for each transport type, can be configured through // config_networkSupportedKeepaliveCount. For better error handling, use // {@link getSupportedKeepalivesForNetworkCapabilities} instead of direct access. @NonNull private final int[] mSupportedKeepalives; // Reserved privileged keepalive slots per transport. Caller's permission will be enforced if // the number of remaining keepalive slots is less than or equal to the threshold. private final int mReservedPrivilegedSlots; // Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if // the number of remaining keepalive slots is less than or equal to the threshold. private final int mAllowedUnprivilegedSlotsForUid; private final Dependencies mDependencies; public KeepaliveTracker(Context context, Handler handler) { this(context, handler, new TcpKeepaliveController(handler), new Dependencies()); } @VisibleForTesting public KeepaliveTracker(Context context, Handler handler, TcpKeepaliveController tcpController, Dependencies deps) { mTcpController = tcpController; mContext = context; mDependencies = deps; mSupportedKeepalives = mDependencies.getSupportedKeepalives(mContext); final ConnectivityResources res = mDependencies.createConnectivityResources(mContext); mReservedPrivilegedSlots = res.get().getInteger( R.integer.config_reservedPrivilegedKeepaliveSlots); mAllowedUnprivilegedSlotsForUid = res.get().getInteger( R.integer.config_allowedUnprivilegedKeepalivePerUid); } /** * Tracks information about a socket keepalive. * * All information about this keepalive is known at construction time except the slot number, * which is only returned when the hardware has successfully started the keepalive. */ @VisibleForTesting public class KeepaliveInfo implements IBinder.DeathRecipient { // TODO : remove this member. Only AutoOnOffKeepalive should have a reference to this. public final ISocketKeepaliveCallback mCallback; // Bookkeeping data. private final int mUid; private final int mPid; private final boolean mPrivileged; public final NetworkAgentInfo mNai; private final int mType; public final FileDescriptor mFd; // True if this was resumed from a previously turned off keepalive, otherwise false. // This is necessary to send the correct callbacks. public final boolean mResumed; public static final int TYPE_NATT = 1; public static final int TYPE_TCP = 2; // Keepalive slot. A small integer that identifies this keepalive among the ones handled // by this network. This is initialized to NO_KEEPALIVE for new keepalives, but to the // old slot for resumed keepalives. private int mSlot; // Packet data. private final KeepalivePacketData mPacket; private final int mInterval; // Whether the keepalive is started or not. The initial state is NOT_STARTED. private static final int NOT_STARTED = 1; private static final int STARTING = 2; private static final int STARTED = 3; private static final int STOPPING = 4; private int mStartedState = NOT_STARTED; private int mStopReason = ERROR_STOP_REASON_UNINITIALIZED; KeepaliveInfo(@NonNull ISocketKeepaliveCallback callback, @NonNull NetworkAgentInfo nai, @NonNull KeepalivePacketData packet, int interval, int type, @Nullable FileDescriptor fd) throws InvalidSocketException { this(callback, nai, packet, Binder.getCallingPid(), Binder.getCallingUid(), interval, type, fd, NO_KEEPALIVE /* slot */, false /* resumed */); } KeepaliveInfo(@NonNull ISocketKeepaliveCallback callback, @NonNull NetworkAgentInfo nai, @NonNull KeepalivePacketData packet, int pid, int uid, int interval, int type, @Nullable FileDescriptor fd, int slot, boolean resumed) throws InvalidSocketException { mCallback = callback; mPid = pid; mUid = uid; mPrivileged = (PERMISSION_GRANTED == mContext.checkPermission(PERMISSION, mPid, mUid)); mNai = nai; mPacket = packet; mInterval = interval; mType = type; mSlot = slot; mResumed = resumed; // For SocketKeepalive, a dup of fd is kept in mFd so the source port from which the // keepalives are sent cannot be reused by another app even if the fd gets closed by // the user. A null is acceptable here for backward compatibility of PacketKeepalive // API. try { if (fd != null) { mFd = Os.dup(fd); } else { Log.d(TAG, toString() + " calls with null fd"); if (!mPrivileged) { throw new SecurityException( "null fd is not allowed for unprivileged access."); } if (mType == TYPE_TCP) { throw new IllegalArgumentException( "null fd is not allowed for tcp socket keepalives."); } mFd = null; } } catch (ErrnoException e) { Log.e(TAG, "Cannot dup fd: ", e); throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); } try { mCallback.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { binderDied(); } } public NetworkAgentInfo getNai() { return mNai; } private String startedStateString(final int state) { switch (state) { case NOT_STARTED : return "NOT_STARTED"; case STARTING : return "STARTING"; case STARTED : return "STARTED"; case STOPPING : return "STOPPING"; } throw new IllegalArgumentException("Unknown state"); } public String toString() { return "KeepaliveInfo [" + " type=" + mType + " network=" + mNai.network + " startedState=" + startedStateString(mStartedState) + " " + IpUtils.addressAndPortToString(mPacket.getSrcAddress(), mPacket.getSrcPort()) + "->" + IpUtils.addressAndPortToString(mPacket.getDstAddress(), mPacket.getDstPort()) + " interval=" + mInterval + " uid=" + mUid + " pid=" + mPid + " privileged=" + mPrivileged + " packetData=" + HexDump.toHexString(mPacket.getPacket()) + " ]"; } /** Called when the application process is killed. */ public void binderDied() { // TODO b/267106526 : this is not called on the handler thread but stop() happily // assumes it is, which means this is a pretty dangerous race condition. stop(BINDER_DIED); } void unlinkDeathRecipient() { if (mCallback != null) { mCallback.asBinder().unlinkToDeath(this, 0); } } public int getSlot() { return mSlot; } int getKeepaliveIntervalSec() { return mInterval; } public int getUid() { return mUid; } private int checkNetworkConnected() { if (!mNai.networkInfo.isConnectedOrConnecting()) { return ERROR_INVALID_NETWORK; } return SUCCESS; } private int checkSourceAddress() { // Check that we have the source address. for (InetAddress address : mNai.linkProperties.getAllAddresses()) { if (address.equals(mPacket.getSrcAddress())) { return SUCCESS; } } // Or the address is the clat source address. if (mPacket.getSrcAddress().equals(mNai.getClatv6SrcAddress())) { return SUCCESS; } return ERROR_INVALID_IP_ADDRESS; } private int checkInterval() { if (mInterval < MIN_INTERVAL_SEC || mInterval > MAX_INTERVAL_SEC) { return ERROR_INVALID_INTERVAL; } return SUCCESS; } private int checkPermission() { final HashMap networkKeepalives = mKeepalives.get(mNai); if (networkKeepalives == null) { return ERROR_INVALID_NETWORK; } if (mPrivileged) return SUCCESS; final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities( mSupportedKeepalives, mNai.networkCapabilities); int takenUnprivilegedSlots = 0; for (final KeepaliveInfo ki : networkKeepalives.values()) { if (!ki.mPrivileged) ++takenUnprivilegedSlots; } if (takenUnprivilegedSlots > supported - mReservedPrivilegedSlots) { return ERROR_INSUFFICIENT_RESOURCES; } // Count unprivileged keepalives for the same uid across networks. int unprivilegedCountSameUid = 0; for (final HashMap kaForNetwork : mKeepalives.values()) { for (final KeepaliveInfo ki : kaForNetwork.values()) { if (ki.mUid == mUid) { unprivilegedCountSameUid++; } } } if (unprivilegedCountSameUid > mAllowedUnprivilegedSlotsForUid) { return ERROR_INSUFFICIENT_RESOURCES; } return SUCCESS; } private int checkLimit() { final HashMap networkKeepalives = mKeepalives.get(mNai); if (networkKeepalives == null) { return ERROR_INVALID_NETWORK; } final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities( mSupportedKeepalives, mNai.networkCapabilities); if (supported == 0) return ERROR_UNSUPPORTED; if (networkKeepalives.size() > supported) return ERROR_INSUFFICIENT_RESOURCES; return SUCCESS; } /** * Checks if the keepalive info is valid to start. * * @return SUCCESS if the keepalive is valid and the error reason otherwise. */ public int isValid() { synchronized (mNai) { int error = checkInterval(); if (error == SUCCESS) error = checkLimit(); if (error == SUCCESS) error = checkPermission(); if (error == SUCCESS) error = checkNetworkConnected(); if (error == SUCCESS) error = checkSourceAddress(); return error; } } /** * Attempt to start the keepalive on the given slot. * * @param slot the slot to start the keepalive on. * @return SUCCESS if the keepalive is successfully starting and the error reason otherwise. */ int start(int slot) { // BINDER_DIED can happen if the binder died before the KeepaliveInfo was created and // the constructor set the state to BINDER_DIED. If that's the case, the KI is already // cleaned up. if (BINDER_DIED == mStartedState) return BINDER_DIED; mSlot = slot; int error = isValid(); if (error == SUCCESS) { Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.toShortString()); switch (mType) { case TYPE_NATT: final NattKeepalivePacketData nattData = (NattKeepalivePacketData) mPacket; mNai.onAddNattKeepalivePacketFilter(slot, nattData); mNai.onStartNattSocketKeepalive(slot, mInterval, nattData); break; case TYPE_TCP: try { mTcpController.startSocketMonitor(mFd, mCallback, mSlot); } catch (InvalidSocketException e) { handleStopKeepalive(mNai, mSlot, ERROR_INVALID_SOCKET); return ERROR_INVALID_SOCKET; } final TcpKeepalivePacketData tcpData = (TcpKeepalivePacketData) mPacket; mNai.onAddTcpKeepalivePacketFilter(slot, tcpData); // TODO: check result from apf and notify of failure as needed. mNai.onStartTcpSocketKeepalive(slot, mInterval, tcpData); break; default: Log.wtf(TAG, "Starting keepalive with unknown type: " + mType); handleStopKeepalive(mNai, mSlot, ERROR_UNSUPPORTED); return ERROR_UNSUPPORTED; } mStartedState = STARTING; return SUCCESS; } else { handleStopKeepalive(mNai, mSlot, error); return error; } } void stop(int reason) { int uid = Binder.getCallingUid(); if (uid != mUid && uid != Process.SYSTEM_UID) { if (DBG) { Log.e(TAG, "Cannot stop unowned keepalive " + mSlot + " on " + mNai.network); } } // To prevent races from re-entrance of stop(), return if the state is already stopping. // This might happen if multiple event sources stop keepalive in a short time. Such as // network disconnect after user calls stop(), or tear down socket after binder died. // Note that it's always possible this method is called by the auto keepalive timer // or any other way after the binder died, hence the check for BINDER_DIED. If the // binder has died, then the KI has already been cleaned up. if (mStartedState == STOPPING || mStartedState == BINDER_DIED) return; // Store the reason of stopping, and report it after the keepalive is fully stopped. if (mStopReason != ERROR_STOP_REASON_UNINITIALIZED) { throw new IllegalStateException("Unexpected stop reason: " + mStopReason); } mStopReason = reason; Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.toShortString() + ": " + reason); switch (mStartedState) { case NOT_STARTED: // Remove the reference to this keepalive that had an error before starting, // e.g. invalid parameter. cleanupStoppedKeepalive(mNai, mSlot); if (BINDER_DIED == reason) mStartedState = BINDER_DIED; break; default: mStartedState = STOPPING; switch (mType) { case TYPE_TCP: mTcpController.stopSocketMonitor(mSlot); // fall through case TYPE_NATT: mNai.onStopSocketKeepalive(mSlot); mNai.onRemoveKeepalivePacketFilter(mSlot); break; default: Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType); } } // Close the duplicated fd that maintains the lifecycle of socket whenever // keepalive is running. if (mFd != null) { try { Os.close(mFd); } catch (ErrnoException e) { // This should not happen since system server controls the lifecycle of fd when // keepalive offload is running. Log.wtf(TAG, "Error closing fd for keepalive " + mSlot + ": " + e); } } } /** * 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, mPid, mUid, mInterval, mType, fd, mSlot, true /* resumed */); } /** * Construct a new KeepaliveInfo from existing KeepaliveInfo with a new KeepalivePacketData. */ public KeepaliveInfo withPacketData(@NonNull KeepalivePacketData packet) throws InvalidSocketException { return new KeepaliveInfo(mCallback, mNai, packet, mPid, mUid, mInterval, mType, mFd, mSlot, mResumed); } } void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) { if (DBG) Log.w(TAG, "Sending onError(" + error + ") callback"); try { cb.onError(error); } catch (RemoteException e) { Log.w(TAG, "Discarded onError(" + error + ") callback"); } } private int findFirstFreeSlot(NetworkAgentInfo nai) { HashMap networkKeepalives = mKeepalives.get(nai); if (networkKeepalives == null) { networkKeepalives = new HashMap(); mKeepalives.put(nai, networkKeepalives); } // Find the lowest-numbered free slot. Slot numbers start from 1, because that's what two // separate chipset implementations independently came up with. int slot; for (slot = 1; slot <= networkKeepalives.size(); slot++) { if (networkKeepalives.get(slot) == null) { return slot; } } return slot; } /** * Handle start keepalives with the message. * * @param ki the keepalive to start. * @return Pair of (SUCCESS if the keepalive is successfully starting and the error reason * otherwise, the started KeepaliveInfo object) */ public Pair handleStartKeepalive(KeepaliveInfo ki) { final KeepaliveInfo newKi; try { newKi = handleUpdateKeepaliveForClat(ki); } catch (InvalidSocketException | InvalidPacketException e) { Log.e(TAG, "Fail to construct keepalive packet"); notifyErrorCallback(ki.mCallback, ERROR_INVALID_IP_ADDRESS); // Fail to create new keepalive packet for clat. Return the original keepalive info. return new Pair<>(ERROR_INVALID_IP_ADDRESS, ki); } final NetworkAgentInfo nai = newKi.getNai(); // If this was a paused keepalive, then reuse the same slot that was kept for it. Otherwise, // use the first free slot for this network agent. final int slot = NO_KEEPALIVE != newKi.mSlot ? newKi.mSlot : findFirstFreeSlot(nai); mKeepalives.get(nai).put(slot, newKi); return new Pair<>(newKi.start(slot), newKi); } private KeepaliveInfo handleUpdateKeepaliveForClat(KeepaliveInfo ki) throws InvalidSocketException, InvalidPacketException { if (!mDependencies.isAddressTranslationEnabled(mContext)) return ki; // Translation applies to only NAT-T keepalive if (ki.mType != KeepaliveInfo.TYPE_NATT) return ki; // Only try to translate address if the packet source address is the clat's source address. if (!ki.mPacket.getSrcAddress().equals(ki.getNai().getClatv4SrcAddress())) return ki; final InetAddress dstAddr = ki.mPacket.getDstAddress(); // Do not perform translation for a v6 dst address. if (!(dstAddr instanceof Inet4Address)) return ki; final Inet6Address address = ki.getNai().translateV4toClatV6((Inet4Address) dstAddr); if (address == null) return ki; final int srcPort = ki.mPacket.getSrcPort(); final KeepaliveInfo newInfo = ki.withPacketData(NattKeepalivePacketData.nattKeepalivePacket( ki.getNai().getClatv6SrcAddress(), srcPort, address, NATT_PORT)); Log.d(TAG, "Src is clat v4 address. Convert from " + ki + " to " + newInfo); return newInfo; } public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) { final HashMap networkKeepalives = mKeepalives.get(nai); if (networkKeepalives != null) { final ArrayList kalist = new ArrayList(networkKeepalives.values()); for (KeepaliveInfo ki : kalist) { // If the keepalive is paused, then it is already stopped with the hardware and so // continue. Note that to send the appropriate stop reason callback, // AutomaticOnOffKeepaliveTracker will call finalizePausedKeepalive which will also // finally remove this keepalive slot from the array. if (ki.mStopReason == SUCCESS_PAUSED) continue; ki.stop(reason); // Clean up keepalives since the network agent is disconnected and unable to pass // back asynchronous result of stop(). cleanupStoppedKeepalive(nai, ki.mSlot); } } } public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) { final String networkName = NetworkAgentInfo.toShortString(nai); HashMap networkKeepalives = mKeepalives.get(nai); if (networkKeepalives == null) { Log.e(TAG, "Attempt to stop keepalive on nonexistent network " + networkName); return; } KeepaliveInfo ki = networkKeepalives.get(slot); if (ki == null) { Log.e(TAG, "Attempt to stop nonexistent keepalive " + slot + " on " + networkName); return; } ki.stop(reason); // Clean up keepalives will be done as a result of calling ki.stop() after the slots are // freed. } private void cleanupStoppedKeepalive(NetworkAgentInfo nai, int slot) { final String networkName = NetworkAgentInfo.toShortString(nai); HashMap networkKeepalives = mKeepalives.get(nai); if (networkKeepalives == null) { Log.e(TAG, "Attempt to remove keepalive on nonexistent network " + networkName); return; } KeepaliveInfo ki = networkKeepalives.get(slot); if (ki == null) { Log.e(TAG, "Attempt to remove nonexistent keepalive " + slot + " on " + networkName); return; } // If the keepalive was stopped for good, remove it from the hash table so the slot can // be considered available when reusing it. If it was only a pause, let it sit in the map // so it sits on the slot. final int reason = ki.mStopReason; if (reason != SUCCESS_PAUSED) { networkKeepalives.remove(slot); Log.d(TAG, "Remove keepalive " + slot + " on " + networkName + ", " + networkKeepalives.size() + " remains."); } else { Log.d(TAG, "Pause keepalive " + slot + " on " + networkName + ", keep slot reserved"); } if (networkKeepalives.isEmpty()) { mKeepalives.remove(nai); } // Notify app that the keepalive is stopped. if (reason == SUCCESS) { try { ki.mCallback.onStopped(); } catch (RemoteException e) { Log.w(TAG, "Discarded onStop callback: " + reason); } } else if (reason == SUCCESS_PAUSED) { try { ki.mCallback.onPaused(); } catch (RemoteException e) { Log.w(TAG, "Discarded onPaused callback: " + reason); } } else if (reason == DATA_RECEIVED) { try { ki.mCallback.onDataReceived(); } catch (RemoteException e) { Log.w(TAG, "Discarded onDataReceived callback: " + reason); } } else if (reason == ERROR_STOP_REASON_UNINITIALIZED) { throw new IllegalStateException("Unexpected stop reason: " + reason); } else if (reason == ERROR_NO_SUCH_SLOT) { // There are multiple independent reasons a keepalive can stop. Some // are software (e.g. the app stops the keepalive) and some are hardware // (e.g. the SIM card gets removed). Therefore, there is a very low // probability that both of these happen at the same time, which would // result in the first stop attempt returning SUCCESS and the second // stop attempt returning NO_SUCH_SLOT. Such a race condition can be // ignored with a log. // This should still be reported because if it happens with any frequency // it probably means there is a bug where the system server is trying // to use a non-existing hardware slot. // TODO : separate the non-existing hardware slot from the case where // there is no keepalive running on this slot. Log.wtf(TAG, "Keepalive on slot " + slot + " can't be stopped : " + reason); notifyErrorCallback(ki.mCallback, reason); } else { notifyErrorCallback(ki.mCallback, reason); } ki.unlinkDeathRecipient(); } /** * Finalize a paused keepalive. * * This will send the appropriate callback after checking that this keepalive is indeed paused, * and free the slot. * * @param ki the keepalive to finalize * @param reason the reason the keepalive is stopped */ public void finalizePausedKeepalive(@NonNull final KeepaliveInfo ki, int reason) { if (SUCCESS_PAUSED != ki.mStopReason) { throw new IllegalStateException("Keepalive is not paused"); } if (reason == SUCCESS) { try { ki.mCallback.onStopped(); } catch (RemoteException e) { Log.w(TAG, "Discarded onStopped callback while finalizing paused keepalive"); } } else { notifyErrorCallback(ki.mCallback, reason); } final HashMap networkKeepalives = mKeepalives.get(ki.mNai); if (networkKeepalives == null) { Log.e(TAG, "Attempt to finalize keepalive on nonexistent network " + ki.mNai); return; } networkKeepalives.remove(ki.mSlot); } /** * Handle keepalive events from lower layer. * * @return false if the event caused handleStopKeepalive to be called, i.e. the keepalive is * forced to stop. Otherwise, return true. */ public boolean handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) { KeepaliveInfo ki = null; try { ki = mKeepalives.get(nai).get(slot); } catch(NullPointerException e) {} if (ki == null) { Log.e(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason + " for unknown keepalive " + slot + " on " + nai.toShortString()); return true; } // This can be called in a number of situations : // - startedState is STARTING. // - reason is SUCCESS => go to STARTED. // - reason isn't SUCCESS => it's an error starting. Go to NOT_STARTED and stop keepalive. // - startedState is STARTED. // - reason is SUCCESS => it's a success stopping. Go to NOT_STARTED and stop keepalive. // - reason isn't SUCCESS => it's an error in exec. Go to NOT_STARTED and stop keepalive. // The control is not supposed to ever come here if the state is NOT_STARTED. This is // because in NOT_STARTED state, the code will switch to STARTING before sending messages // to start, and the only way to NOT_STARTED is this function, through the edges outlined // above : in all cases, keepalive gets stopped and can't restart without going into // STARTING as messages are ordered. This also depends on the hardware processing the // messages in order. // TODO : clarify this code and get rid of mStartedState. Using a StateMachine is an // option. if (KeepaliveInfo.STARTING == ki.mStartedState) { if (SUCCESS == reason) { // Keepalive successfully started. Log.d(TAG, "Started keepalive " + slot + " on " + nai.toShortString()); ki.mStartedState = KeepaliveInfo.STARTED; try { if (ki.mResumed) { ki.mCallback.onResumed(); } else { ki.mCallback.onStarted(); } } catch (RemoteException e) { Log.w(TAG, "Discarded " + (ki.mResumed ? "onResumed" : "onStarted") + " callback for slot " + slot); } return true; } else { Log.d(TAG, "Failed to start keepalive " + slot + " on " + nai.toShortString() + ": " + reason); // The message indicated some error trying to start: do call handleStopKeepalive. handleStopKeepalive(nai, slot, reason); return false; } } else if (KeepaliveInfo.STOPPING == ki.mStartedState) { // The message indicated result of stopping : clean up keepalive slots. Log.d(TAG, "Stopped keepalive " + slot + " on " + nai.toShortString() + " stopped: " + reason); ki.mStartedState = KeepaliveInfo.NOT_STARTED; cleanupStoppedKeepalive(nai, slot); return true; } else { Log.wtf(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason + " for keepalive in wrong state: " + ki.toString()); // Although this is an unexpected event, the keepalive is not stopped here. return true; } } /** * Called when requesting that keepalives be started on a IPsec NAT-T socket. See * {@link android.net.SocketKeepalive}. **/ @Nullable public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai, @Nullable FileDescriptor fd, int intervalSeconds, @NonNull ISocketKeepaliveCallback cb, @NonNull String srcAddrString, int srcPort, @NonNull String dstAddrString, int dstPort) { if (nai == null) { notifyErrorCallback(cb, ERROR_INVALID_NETWORK); return null; } InetAddress srcAddress, dstAddress; try { srcAddress = InetAddresses.parseNumericAddress(srcAddrString); dstAddress = InetAddresses.parseNumericAddress(dstAddrString); } catch (IllegalArgumentException e) { Log.e(TAG, "Fail to construct address", e); notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS); return null; } KeepalivePacketData packet; try { packet = NattKeepalivePacketData.nattKeepalivePacket( srcAddress, srcPort, dstAddress, NATT_PORT); } catch (InvalidPacketException e) { Log.e(TAG, "Fail to construct keepalive packet", e); notifyErrorCallback(cb, e.getError()); return null; } KeepaliveInfo ki = null; try { ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds, KeepaliveInfo.TYPE_NATT, fd); } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) { Log.e(TAG, "Fail to construct keepalive", e); notifyErrorCallback(cb, ERROR_INVALID_SOCKET); return null; } Log.d(TAG, "Created keepalive: " + ki); return ki; } /** * 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 * 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}. **/ @Nullable public KeepaliveInfo makeTcpKeepaliveInfo(@Nullable NetworkAgentInfo nai, @NonNull FileDescriptor fd, int intervalSeconds, @NonNull ISocketKeepaliveCallback cb) { if (nai == null) { notifyErrorCallback(cb, ERROR_INVALID_NETWORK); return null; } final TcpKeepalivePacketData packet; try { packet = TcpKeepaliveController.getTcpKeepalivePacket(fd); } catch (InvalidSocketException e) { notifyErrorCallback(cb, e.error); return null; } catch (InvalidPacketException e) { notifyErrorCallback(cb, e.getError()); return null; } KeepaliveInfo ki = null; try { ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds, KeepaliveInfo.TYPE_TCP, fd); } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) { Log.e(TAG, "Fail to construct keepalive e=" + e); notifyErrorCallback(cb, ERROR_INVALID_SOCKET); return null; } Log.d(TAG, "Created keepalive: " + ki.toString()); return ki; } /** * 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, @NonNull ISocketKeepaliveCallback cb, @NonNull String srcAddrString, @NonNull String dstAddrString, int dstPort) { // 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. int srcPort = 0; try { final SocketAddress srcSockAddr = Os.getsockname(fd); srcPort = ((InetSocketAddress) srcSockAddr).getPort(); } catch (ErrnoException e) { notifyErrorCallback(cb, ERROR_INVALID_SOCKET); return null; } // Forward request to old API. return makeNattKeepaliveInfo(nai, fd, intervalSeconds, cb, srcAddrString, srcPort, dstAddrString, dstPort); } /** * Verify if the IPsec NAT-T file descriptor and resource Id hold for IPsec keepalive is valid. **/ public static boolean isNattKeepaliveSocketValid(@Nullable FileDescriptor fd, int resourceId) { // TODO: 1. confirm whether the fd is called from system api or created by IpSecService. // 2. If the fd is created from the system api, check that it's bounded. And // call dup to keep the fd open. // 3. If the fd is created from IpSecService, check if the resource ID is valid. And // hold the resource needed in IpSecService. if (null == fd) { return false; } return true; } /** * Dump KeepaliveTracker state. */ public void dump(IndentingPrintWriter pw) { pw.println("Supported Socket keepalives: " + Arrays.toString(mSupportedKeepalives)); pw.println("Reserved Privileged keepalives: " + mReservedPrivilegedSlots); pw.println("Allowed Unprivileged keepalives per uid: " + mAllowedUnprivilegedSlotsForUid); pw.println("Socket keepalives:"); pw.increaseIndent(); for (NetworkAgentInfo nai : mKeepalives.keySet()) { pw.println(nai.toShortString()); pw.increaseIndent(); for (int slot : mKeepalives.get(nai).keySet()) { KeepaliveInfo ki = mKeepalives.get(nai).get(slot); pw.println(slot + ": " + ki.toString()); } pw.decreaseIndent(); } pw.decreaseIndent(); } /** * Dependencies class for testing. */ @VisibleForTesting public static class Dependencies { /** * Read supported keepalive count for each transport type from overlay resource. This should * be used to create a local variable store of resource customization, and set as the * input for {@link getSupportedKeepalivesForNetworkCapabilities}. * * @param context The context to read resource from. * @return An array of supported keepalive count for each transport type. */ @NonNull public int[] getSupportedKeepalives(@NonNull Context context) { return KeepaliveResourceUtil.getSupportedKeepalives(context); } /** * Create a new {@link ConnectivityResources}. */ @NonNull public ConnectivityResources createConnectivityResources(@NonNull Context context) { return new ConnectivityResources(context); } /** * Return if keepalive address translation with clat feature is supported or not. * * This is controlled by both isFeatureSupported() and isFeatureEnabled(). The * isFeatureSupported() checks whether device contains the minimal required module * version for FEATURE_CLAT_ADDRESS_TRANSLATE. The isTetheringFeatureForceDisabled() * checks the DeviceConfig flag that can be updated via DeviceConfig push to control * the overall feature. */ public boolean isAddressTranslationEnabled(@NonNull Context context) { return DeviceConfigUtils.isFeatureSupported(context, FEATURE_CLAT_ADDRESS_TRANSLATE) && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, CONFIG_DISABLE_CLAT_ADDRESS_TRANSLATE); } } }