[DK2-0]Create AutomaticOnOffKeepaliveTracker
Add a new AutomaticOnOffKeepaliveTracker class between ConnectivityService and KeepaliveTracker to handle the automatic on/off keepalive. This commit only creates this new class and move the TCP polling code to the new class as a preparation for the following commit. The original test file was created for testing the TCP polling mechanism, so rename it to match the new class. Bug: 259000745 Test: m ; atest FrameworksNetTests Change-Id: I1b229f906283c0f5ef7a3efdb0572fcbfc5df72b
This commit is contained in:
committed by
Chalard Jean
parent
9ba6190f9b
commit
3d60bacfa0
@@ -101,6 +101,7 @@ 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;
|
||||
|
||||
@@ -269,6 +270,7 @@ import com.android.networkstack.apishim.ConstantsShim;
|
||||
import com.android.networkstack.apishim.common.BroadcastOptionsShim;
|
||||
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
|
||||
import com.android.server.connectivity.AutodestructReference;
|
||||
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
|
||||
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
|
||||
import com.android.server.connectivity.ClatCoordinator;
|
||||
import com.android.server.connectivity.ConnectivityFlags;
|
||||
@@ -276,7 +278,6 @@ 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;
|
||||
@@ -843,7 +844,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
|
||||
private final LocationPermissionChecker mLocationPermissionChecker;
|
||||
|
||||
private final KeepaliveTracker mKeepaliveTracker;
|
||||
private final AutomaticOnOffKeepaliveTracker mKeepaliveTracker;
|
||||
private final QosCallbackTracker mQosCallbackTracker;
|
||||
private final NetworkNotificationManager mNotifier;
|
||||
private final LingerMonitor mLingerMonitor;
|
||||
@@ -1565,7 +1566,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
mSettingsObserver = new SettingsObserver(mContext, mHandler);
|
||||
registerSettingsCallbacks();
|
||||
|
||||
mKeepaliveTracker = new KeepaliveTracker(mContext, mHandler);
|
||||
mKeepaliveTracker = new AutomaticOnOffKeepaliveTracker(mContext, mHandler);
|
||||
mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager);
|
||||
mQosCallbackTracker = new QosCallbackTracker(mHandler, mNetworkRequestCounter);
|
||||
|
||||
@@ -2998,7 +2999,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
}
|
||||
|
||||
private void enforceKeepalivePermission() {
|
||||
mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
|
||||
mContext.enforceCallingOrSelfPermission(PERMISSION, "ConnectivityService");
|
||||
}
|
||||
|
||||
private boolean checkLocalMacAddressPermission(int pid, int uid) {
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.system.OsConstants.AF_INET;
|
||||
import static android.system.OsConstants.AF_INET6;
|
||||
import static android.system.OsConstants.SOL_SOCKET;
|
||||
import static android.system.OsConstants.SO_SNDTIMEO;
|
||||
|
||||
import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
|
||||
import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
|
||||
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.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.net.INetd;
|
||||
import android.net.ISocketKeepaliveCallback;
|
||||
import android.net.MarkMaskParcel;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.StructTimeval;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.IndentingPrintWriter;
|
||||
import com.android.net.module.util.HexDump;
|
||||
import com.android.net.module.util.SocketUtils;
|
||||
import com.android.net.module.util.netlink.InetDiagMessage;
|
||||
import com.android.net.module.util.netlink.NetlinkUtils;
|
||||
import com.android.net.module.util.netlink.StructNlAttr;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
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
|
||||
* 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};
|
||||
|
||||
@NonNull
|
||||
private final Handler mConnectivityServiceHandler;
|
||||
@NonNull
|
||||
private final KeepaliveTracker mKeepaliveTracker;
|
||||
@NonNull
|
||||
private final Context mContext;
|
||||
|
||||
/**
|
||||
* The {@code inetDiagReqV2} messages for different IP family.
|
||||
*
|
||||
* Key: Ip family type.
|
||||
* Value: Bytes array represent the {@code inetDiagReqV2}.
|
||||
*
|
||||
* This should only be accessed in the connectivity service handler thread.
|
||||
*/
|
||||
private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>();
|
||||
private final Dependencies mDependencies;
|
||||
private final INetd mNetd;
|
||||
|
||||
public AutomaticOnOffKeepaliveTracker(Context context, Handler handler) {
|
||||
this(context, handler, new Dependencies(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public AutomaticOnOffKeepaliveTracker(@NonNull Context context, @NonNull Handler handler,
|
||||
@NonNull Dependencies dependencies) {
|
||||
mContext = Objects.requireNonNull(context);
|
||||
mDependencies = dependencies;
|
||||
this.mConnectivityServiceHandler = Objects.requireNonNull(handler);
|
||||
mNetd = mDependencies.getNetd();
|
||||
mKeepaliveTracker = mDependencies.newKeepaliveTracker(
|
||||
mContext, mConnectivityServiceHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keepalive events from lower layer.
|
||||
*/
|
||||
public void handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) {
|
||||
mKeepaliveTracker.handleEventSocketKeepalive(nai, slot, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle stop all keepalives on the specific network.
|
||||
*/
|
||||
public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
|
||||
mKeepaliveTracker.handleStopAllKeepalives(nai, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle start keepalives with the message.
|
||||
*
|
||||
* The message is expected to be a KeepaliveTracker.KeepaliveInfo.
|
||||
*/
|
||||
public void handleStartKeepalive(Message message) {
|
||||
mKeepaliveTracker.handleStartKeepalive(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle stop keepalives on the specific network with given slot.
|
||||
*/
|
||||
public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
|
||||
mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 FileDescriptor fd,
|
||||
int intervalSeconds,
|
||||
@NonNull ISocketKeepaliveCallback cb,
|
||||
@NonNull String srcAddrString,
|
||||
int srcPort,
|
||||
@NonNull String dstAddrString,
|
||||
int dstPort) {
|
||||
mKeepaliveTracker.startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString,
|
||||
srcPort, dstAddrString, dstPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 FileDescriptor fd,
|
||||
int resourceId,
|
||||
int intervalSeconds,
|
||||
@NonNull ISocketKeepaliveCallback cb,
|
||||
@NonNull String srcAddrString,
|
||||
@NonNull String dstAddrString,
|
||||
int dstPort) {
|
||||
mKeepaliveTracker.startNattKeepalive(nai, fd, resourceId, intervalSeconds, cb,
|
||||
srcAddrString, dstAddrString, dstPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ConnectivityService to start TCP keepalive on a file descriptor.
|
||||
*
|
||||
* 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}.
|
||||
**/
|
||||
public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
|
||||
@NonNull FileDescriptor fd,
|
||||
int intervalSeconds,
|
||||
@NonNull ISocketKeepaliveCallback cb) {
|
||||
mKeepaliveTracker.startTcpKeepalive(nai, fd, intervalSeconds, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump AutomaticOnOffKeepaliveTracker state.
|
||||
*/
|
||||
public void dump(IndentingPrintWriter pw) {
|
||||
// TODO: Dump the necessary information for automatic on/off keepalive.
|
||||
mKeepaliveTracker.dump(pw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all keeplaives on the network are still valid.
|
||||
*/
|
||||
public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) {
|
||||
mKeepaliveTracker.handleCheckKeepalivesStillValid(nai);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean isAnyTcpSocketConnected(int netId) {
|
||||
FileDescriptor fd = null;
|
||||
|
||||
try {
|
||||
fd = mDependencies.createConnectedNetlinkSocket();
|
||||
|
||||
// Get network mask
|
||||
final MarkMaskParcel parcel = mNetd.getFwmarkForNetwork(netId);
|
||||
final int networkMark = (parcel != null) ? parcel.mark : NetlinkUtils.UNKNOWN_MARK;
|
||||
final int networkMask = (parcel != null) ? parcel.mask : NetlinkUtils.NULL_MASK;
|
||||
|
||||
// Send request for each IP family
|
||||
for (final int family : ADDRESS_FAMILIES) {
|
||||
if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (ErrnoException | SocketException | InterruptedIOException | RemoteException e) {
|
||||
Log.e(TAG, "Fail to get socket info via netlink.", e);
|
||||
} finally {
|
||||
SocketUtils.closeSocketQuietly(fd);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
|
||||
int networkMask) throws ErrnoException, InterruptedIOException {
|
||||
ensureRunningOnHandlerThread();
|
||||
// Build SocketDiag messages and cache it.
|
||||
if (mSockDiagMsg.get(family) == null) {
|
||||
mSockDiagMsg.put(family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
|
||||
}
|
||||
mDependencies.sendRequest(fd, mSockDiagMsg.get(family));
|
||||
|
||||
// Iteration limitation as a protection to avoid possible infinite loops.
|
||||
// DEFAULT_RECV_BUFSIZE could read more than 20 sockets per time. Max iteration
|
||||
// should be enough to go through reasonable TCP sockets in the device.
|
||||
final int maxIteration = 100;
|
||||
int parsingIteration = 0;
|
||||
while (parsingIteration < maxIteration) {
|
||||
final ByteBuffer bytes = mDependencies.recvSockDiagResponse(fd);
|
||||
|
||||
try {
|
||||
while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
|
||||
final int startPos = bytes.position();
|
||||
|
||||
final int nlmsgLen = bytes.getInt();
|
||||
final int nlmsgType = bytes.getShort();
|
||||
if (isEndOfMessageOrError(nlmsgType)) return false;
|
||||
// TODO: Parse InetDiagMessage to get uid and dst address information to filter
|
||||
// socket via NetlinkMessage.parse.
|
||||
|
||||
// Skip the header to move to data part.
|
||||
bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
|
||||
|
||||
if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (BufferUnderflowException e) {
|
||||
// The exception happens in random place in either header position or any data
|
||||
// position. Partial bytes from the middle of the byte buffer may not be enough to
|
||||
// clarify, so print out the content before the error to possibly prevent printing
|
||||
// the whole 8K buffer.
|
||||
final int exceptionPos = bytes.position();
|
||||
final String hex = HexDump.dumpHexString(bytes.array(), 0, exceptionPos);
|
||||
Log.e(TAG, "Unexpected socket info parsing: " + hex, e);
|
||||
}
|
||||
|
||||
parsingIteration++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isEndOfMessageOrError(int nlmsgType) {
|
||||
return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
|
||||
}
|
||||
|
||||
private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
|
||||
int networkMask) {
|
||||
final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
|
||||
return (mark & networkMask) == networkMark;
|
||||
}
|
||||
|
||||
private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
|
||||
final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
|
||||
int mark = NetlinkUtils.INIT_MARK_VALUE;
|
||||
// Get socket mark
|
||||
// TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
|
||||
// data.
|
||||
while (bytes.position() < nextMsgOffset) {
|
||||
final StructNlAttr nlattr = StructNlAttr.parse(bytes);
|
||||
if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
|
||||
mark = nlattr.getValueAsInteger();
|
||||
}
|
||||
}
|
||||
return mark;
|
||||
}
|
||||
|
||||
private void ensureRunningOnHandlerThread() {
|
||||
if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
|
||||
throw new IllegalStateException(
|
||||
"Not running on handler thread: " + Thread.currentThread().getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependencies class for testing.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static class Dependencies {
|
||||
private final Context mContext;
|
||||
|
||||
public Dependencies(final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a netlink socket connected to the kernel.
|
||||
*
|
||||
* @return fd the fileDescriptor of the socket.
|
||||
*/
|
||||
public FileDescriptor createConnectedNetlinkSocket()
|
||||
throws ErrnoException, SocketException {
|
||||
final FileDescriptor fd = NetlinkUtils.createNetLinkInetDiagSocket();
|
||||
NetlinkUtils.connectSocketToNetlink(fd);
|
||||
Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
|
||||
StructTimeval.fromMillis(IO_TIMEOUT_MS));
|
||||
return fd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send composed message request to kernel.
|
||||
*
|
||||
* The given FileDescriptor is expected to be created by
|
||||
* {@link #createConnectedNetlinkSocket} or equivalent way.
|
||||
*
|
||||
* @param fd a netlink socket {@code FileDescriptor} connected to the kernel.
|
||||
* @param msg the byte array representing the request message to write to kernel.
|
||||
*/
|
||||
public void sendRequest(@NonNull final FileDescriptor fd,
|
||||
@NonNull final byte[] msg)
|
||||
throws ErrnoException, InterruptedIOException {
|
||||
Os.write(fd, msg, 0 /* byteOffset */, msg.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an INetd connector.
|
||||
*/
|
||||
public INetd getNetd() {
|
||||
return INetd.Stub.asInterface(
|
||||
(IBinder) mContext.getSystemService(Context.NETD_SERVICE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive the response message from kernel via given {@code FileDescriptor}.
|
||||
* The usage should follow the {@code #sendRequest} call with the same
|
||||
* FileDescriptor.
|
||||
*
|
||||
* The overall response may be large but the individual messages should not be
|
||||
* excessively large(8-16kB) because trying to get the kernel to return
|
||||
* everything in one big buffer is inefficient as it forces the kernel to allocate
|
||||
* large chunks of linearly physically contiguous memory. The usage should iterate the
|
||||
* call of this method until the end of the overall message.
|
||||
*
|
||||
* The default receiving buffer size should be small enough that it is always
|
||||
* processed within the {@link NetlinkUtils#IO_TIMEOUT_MS} timeout.
|
||||
*/
|
||||
public ByteBuffer recvSockDiagResponse(@NonNull final FileDescriptor fd)
|
||||
throws ErrnoException, InterruptedIOException {
|
||||
return NetlinkUtils.recvMessage(
|
||||
fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, NetlinkUtils.IO_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new KeepaliveTracker.
|
||||
*/
|
||||
public KeepaliveTracker newKeepaliveTracker(@NonNull Context context,
|
||||
@NonNull Handler connectivityserviceHander) {
|
||||
return new KeepaliveTracker(mContext, connectivityserviceHander);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,27 +33,15 @@ 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.system.OsConstants.AF_INET;
|
||||
import static android.system.OsConstants.AF_INET6;
|
||||
import static android.system.OsConstants.SOL_SOCKET;
|
||||
import static android.system.OsConstants.SO_SNDTIMEO;
|
||||
|
||||
import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
|
||||
import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
|
||||
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.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.net.ConnectivityResources;
|
||||
import android.net.INetd;
|
||||
import android.net.ISocketKeepaliveCallback;
|
||||
import android.net.InetAddresses;
|
||||
import android.net.InvalidPacketException;
|
||||
import android.net.KeepalivePacketData;
|
||||
import android.net.MarkMaskParcel;
|
||||
import android.net.NattKeepalivePacketData;
|
||||
import android.net.NetworkAgent;
|
||||
import android.net.SocketKeepalive.InvalidSocketException;
|
||||
@@ -67,29 +55,18 @@ import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.StructTimeval;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.connectivity.resources.R;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.IndentingPrintWriter;
|
||||
import com.android.net.module.util.HexDump;
|
||||
import com.android.net.module.util.IpUtils;
|
||||
import com.android.net.module.util.SocketUtils;
|
||||
import com.android.net.module.util.netlink.InetDiagMessage;
|
||||
import com.android.net.module.util.netlink.NetlinkUtils;
|
||||
import com.android.net.module.util.netlink.StructNlAttr;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -107,7 +84,6 @@ public class KeepaliveTracker {
|
||||
private static final boolean DBG = false;
|
||||
|
||||
public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
|
||||
private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
|
||||
|
||||
/** Keeps track of keepalive requests. */
|
||||
private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
|
||||
@@ -131,35 +107,18 @@ public class KeepaliveTracker {
|
||||
// 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;
|
||||
/**
|
||||
* The {@code inetDiagReqV2} messages for different IP family.
|
||||
*
|
||||
* Key: Ip family type.
|
||||
* Value: Bytes array represent the {@code inetDiagReqV2}.
|
||||
*
|
||||
* This should only be accessed in the connectivity service handler thread.
|
||||
*/
|
||||
private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>();
|
||||
private final Dependencies mDependencies;
|
||||
private final INetd mNetd;
|
||||
|
||||
public KeepaliveTracker(Context context, Handler handler) {
|
||||
this(context, handler, new Dependencies(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public KeepaliveTracker(Context context, Handler handler, Dependencies dependencies) {
|
||||
mConnectivityServiceHandler = handler;
|
||||
mTcpController = new TcpKeepaliveController(handler);
|
||||
mContext = context;
|
||||
mDependencies = dependencies;
|
||||
mSupportedKeepalives = mDependencies.getSupportedKeepalives();
|
||||
mNetd = mDependencies.getNetd();
|
||||
|
||||
final Resources res = mDependencies.newConnectivityResources();
|
||||
mReservedPrivilegedSlots = res.getInteger(
|
||||
mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext);
|
||||
|
||||
final ConnectivityResources res = new ConnectivityResources(mContext);
|
||||
mReservedPrivilegedSlots = res.get().getInteger(
|
||||
R.integer.config_reservedPrivilegedKeepaliveSlots);
|
||||
mAllowedUnprivilegedSlotsForUid = res.getInteger(
|
||||
mAllowedUnprivilegedSlotsForUid = res.get().getInteger(
|
||||
R.integer.config_allowedUnprivilegedKeepalivePerUid);
|
||||
}
|
||||
|
||||
@@ -801,196 +760,4 @@ public class KeepaliveTracker {
|
||||
}
|
||||
pw.decreaseIndent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependencies class for testing.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static class Dependencies {
|
||||
private final Context mContext;
|
||||
|
||||
public Dependencies(final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a netlink socket connected to the kernel.
|
||||
*
|
||||
* @return fd the fileDescriptor of the socket.
|
||||
*/
|
||||
public FileDescriptor createConnectedNetlinkSocket()
|
||||
throws ErrnoException, SocketException {
|
||||
final FileDescriptor fd = NetlinkUtils.createNetLinkInetDiagSocket();
|
||||
NetlinkUtils.connectSocketToNetlink(fd);
|
||||
Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
|
||||
StructTimeval.fromMillis(IO_TIMEOUT_MS));
|
||||
return fd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send composed message request to kernel.
|
||||
*
|
||||
* The given FileDescriptor is expected to be created by
|
||||
* {@link #createConnectedNetlinkSocket} or equivalent way.
|
||||
*
|
||||
* @param fd a netlink socket {@code FileDescriptor} connected to the kernel.
|
||||
* @param msg the byte array representing the request message to write to kernel.
|
||||
*/
|
||||
public void sendRequest(@NonNull final FileDescriptor fd,
|
||||
@NonNull final byte[] msg)
|
||||
throws ErrnoException, InterruptedIOException {
|
||||
Os.write(fd, msg, 0 /* byteOffset */, msg.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an INetd connector.
|
||||
*/
|
||||
public INetd getNetd() {
|
||||
return INetd.Stub.asInterface(
|
||||
(IBinder) mContext.getSystemService(Context.NETD_SERVICE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive the response message from kernel via given {@code FileDescriptor}.
|
||||
* The usage should follow the {@code #sendRequest} call with the same
|
||||
* FileDescriptor.
|
||||
*
|
||||
* The overall response may be large but the individual messages should not be
|
||||
* excessively large(8-16kB) because trying to get the kernel to return
|
||||
* everything in one big buffer is inefficient as it forces the kernel to allocate
|
||||
* large chunks of linearly physically contiguous memory. The usage should iterate the
|
||||
* call of this method until the end of the overall message.
|
||||
*
|
||||
* The default receiving buffer size should be small enough that it is always
|
||||
* processed within the {@link NetlinkUtils#IO_TIMEOUT_MS} timeout.
|
||||
*/
|
||||
public ByteBuffer recvSockDiagResponse(@NonNull final FileDescriptor fd)
|
||||
throws ErrnoException, InterruptedIOException {
|
||||
return NetlinkUtils.recvMessage(
|
||||
fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, NetlinkUtils.IO_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read supported keepalive count for each transport type from overlay resource.
|
||||
*/
|
||||
public int[] getSupportedKeepalives() {
|
||||
return KeepaliveUtils.getSupportedKeepalives(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new Resource from a new ConnectivityResources.
|
||||
*/
|
||||
public Resources newConnectivityResources() {
|
||||
final ConnectivityResources resources = new ConnectivityResources(mContext);
|
||||
return resources.get();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRunningOnHandlerThread() {
|
||||
if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
|
||||
throw new IllegalStateException(
|
||||
"Not running on handler thread: " + Thread.currentThread().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean isAnyTcpSocketConnected(int netId) {
|
||||
FileDescriptor fd = null;
|
||||
|
||||
try {
|
||||
fd = mDependencies.createConnectedNetlinkSocket();
|
||||
|
||||
// Get network mask
|
||||
final MarkMaskParcel parcel = mNetd.getFwmarkForNetwork(netId);
|
||||
final int networkMark = (parcel != null) ? parcel.mark : NetlinkUtils.UNKNOWN_MARK;
|
||||
final int networkMask = (parcel != null) ? parcel.mask : NetlinkUtils.NULL_MASK;
|
||||
|
||||
// Send request for each IP family
|
||||
for (final int family : ADDRESS_FAMILIES) {
|
||||
if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (ErrnoException | SocketException | InterruptedIOException | RemoteException e) {
|
||||
Log.e(TAG, "Fail to get socket info via netlink.", e);
|
||||
} finally {
|
||||
SocketUtils.closeSocketQuietly(fd);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
|
||||
int networkMask) throws ErrnoException, InterruptedIOException {
|
||||
ensureRunningOnHandlerThread();
|
||||
// Build SocketDiag messages and cache it.
|
||||
if (mSockDiagMsg.get(family) == null) {
|
||||
mSockDiagMsg.put(family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
|
||||
}
|
||||
mDependencies.sendRequest(fd, mSockDiagMsg.get(family));
|
||||
|
||||
// Iteration limitation as a protection to avoid possible infinite loops.
|
||||
// DEFAULT_RECV_BUFSIZE could read more than 20 sockets per time. Max iteration
|
||||
// should be enough to go through reasonable TCP sockets in the device.
|
||||
final int maxIteration = 100;
|
||||
int parsingIteration = 0;
|
||||
while (parsingIteration < maxIteration) {
|
||||
final ByteBuffer bytes = mDependencies.recvSockDiagResponse(fd);
|
||||
|
||||
try {
|
||||
while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
|
||||
final int startPos = bytes.position();
|
||||
|
||||
final int nlmsgLen = bytes.getInt();
|
||||
final int nlmsgType = bytes.getShort();
|
||||
if (isEndOfMessageOrError(nlmsgType)) return false;
|
||||
// TODO: Parse InetDiagMessage to get uid and dst address information to filter
|
||||
// socket via NetlinkMessage.parse.
|
||||
|
||||
// Skip the header to move to data part.
|
||||
bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
|
||||
|
||||
if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (BufferUnderflowException e) {
|
||||
// The exception happens in random place in either header position or any data
|
||||
// position. Partial bytes from the middle of the byte buffer may not be enough to
|
||||
// clarify, so print out the content before the error to possibly prevent printing
|
||||
// the whole 8K buffer.
|
||||
final int exceptionPos = bytes.position();
|
||||
final String hex = HexDump.dumpHexString(bytes.array(), 0, exceptionPos);
|
||||
Log.e(TAG, "Unexpected socket info parsing: " + hex, e);
|
||||
}
|
||||
|
||||
parsingIteration++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isEndOfMessageOrError(int nlmsgType) {
|
||||
return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
|
||||
}
|
||||
|
||||
private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
|
||||
int networkMask) {
|
||||
final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
|
||||
return (mark & networkMask) == networkMark;
|
||||
}
|
||||
|
||||
private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
|
||||
final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
|
||||
int mark = NetlinkUtils.INIT_MARK_VALUE;
|
||||
// Get socket mark
|
||||
// TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
|
||||
// data.
|
||||
while (bytes.position() < nextMsgOffset) {
|
||||
final StructNlAttr nlattr = StructNlAttr.parse(bytes);
|
||||
if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
|
||||
mark = nlattr.getValueAsInteger();
|
||||
}
|
||||
}
|
||||
return mark;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,12 @@ import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.net.INetd;
|
||||
import android.net.MarkMaskParcel;
|
||||
import android.os.Build;
|
||||
import android.os.HandlerThread;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
|
||||
import com.android.connectivity.resources.R;
|
||||
import com.android.testutils.DevSdkIgnoreRule;
|
||||
import com.android.testutils.DevSdkIgnoreRunner;
|
||||
|
||||
@@ -48,21 +46,19 @@ import java.nio.ByteOrder;
|
||||
|
||||
@RunWith(DevSdkIgnoreRunner.class)
|
||||
@SmallTest
|
||||
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
|
||||
public class KeepaliveTrackerTest {
|
||||
private static final int[] TEST_SUPPORTED_KEEPALIVES = {1, 3, 0, 0, 0, 0, 0, 0, 0};
|
||||
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
|
||||
public class AutomaticOnOffKeepaliveTrackerTest {
|
||||
private static final int TEST_NETID = 0xA85;
|
||||
private static final int TEST_NETID_FWMARK = 0x0A85;
|
||||
private static final int OTHER_NETID = 0x1A85;
|
||||
private static final int NETID_MASK = 0xffff;
|
||||
private static final int SUPPORTED_SLOT_COUNT = 2;
|
||||
private KeepaliveTracker mKeepaliveTracker;
|
||||
private AutomaticOnOffKeepaliveTracker mAOOKeepaliveTracker;
|
||||
private HandlerThread mHandlerThread;
|
||||
|
||||
@Mock INetd mNetd;
|
||||
@Mock KeepaliveTracker.Dependencies mDependencies;
|
||||
@Mock AutomaticOnOffKeepaliveTracker.Dependencies mDependencies;
|
||||
@Mock Context mCtx;
|
||||
@Mock Resources mResources;
|
||||
@Mock KeepaliveTracker mKeepaliveTracker;
|
||||
|
||||
// Hexadecimal representation of a SOCK_DIAG response with tcp info.
|
||||
private static final String SOCK_DIAG_TCP_INET_HEX =
|
||||
@@ -169,51 +165,42 @@ public class KeepaliveTrackerTest {
|
||||
doReturn(makeMarkMaskParcel(NETID_MASK, TEST_NETID_FWMARK)).when(mNetd)
|
||||
.getFwmarkForNetwork(TEST_NETID);
|
||||
|
||||
doReturn(TEST_SUPPORTED_KEEPALIVES).when(mDependencies).getSupportedKeepalives();
|
||||
doReturn(mResources).when(mDependencies).newConnectivityResources();
|
||||
mockResource();
|
||||
doNothing().when(mDependencies).sendRequest(any(), any());
|
||||
|
||||
mHandlerThread = new HandlerThread("KeepaliveTrackerTest");
|
||||
mHandlerThread.start();
|
||||
|
||||
mKeepaliveTracker = new KeepaliveTracker(mCtx, mHandlerThread.getThreadHandler(),
|
||||
mDependencies);
|
||||
}
|
||||
|
||||
private void mockResource() {
|
||||
doReturn(SUPPORTED_SLOT_COUNT).when(mResources).getInteger(
|
||||
R.integer.config_reservedPrivilegedKeepaliveSlots);
|
||||
doReturn(SUPPORTED_SLOT_COUNT).when(mResources).getInteger(
|
||||
R.integer.config_allowedUnprivilegedKeepalivePerUid);
|
||||
doReturn(mKeepaliveTracker).when(mDependencies).newKeepaliveTracker(
|
||||
mCtx, mHandlerThread.getThreadHandler());
|
||||
mAOOKeepaliveTracker = new AutomaticOnOffKeepaliveTracker(
|
||||
mCtx, mHandlerThread.getThreadHandler(), mDependencies);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
|
||||
setupResponseWithSocketExisting();
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
|
||||
() -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
|
||||
setupResponseWithSocketExisting();
|
||||
mHandlerThread.getThreadHandler().post(
|
||||
() -> assertTrue(mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
|
||||
() -> assertTrue(mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
|
||||
setupResponseWithSocketExisting();
|
||||
mHandlerThread.getThreadHandler().post(
|
||||
() -> assertFalse(mKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
|
||||
() -> assertFalse(mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
|
||||
setupResponseWithoutSocketExisting();
|
||||
mHandlerThread.getThreadHandler().post(
|
||||
() -> assertFalse(mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
|
||||
() -> assertFalse(mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
|
||||
}
|
||||
|
||||
private void setupResponseWithSocketExisting() throws Exception {
|
||||
Reference in New Issue
Block a user