blob: b5eafd6e890e07e0558dee15f9e63bb48bc9c117 [file] [log] [blame]
/*
* Copyright (C) 2019 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.networkstack.netlink;
import static android.net.util.DataStallUtils.CONFIG_MIN_PACKETS_THRESHOLD;
import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE;
import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD;
import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE;
import static android.net.util.DataStallUtils.TCP_MONITOR_STATE_FILTER;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.AF_NETLINK;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.NETLINK_INET_DIAG;
import static android.system.OsConstants.SOCK_CLOEXEC;
import static android.system.OsConstants.SOCK_DGRAM;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
import static com.android.net.module.util.netlink.InetDiagMessage.inetDiagReqV2;
import static com.android.net.module.util.netlink.NetlinkConstants.INET_DIAG_MEMINFO;
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.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
import android.content.Context;
import android.net.INetd;
import android.net.MarkMaskParcel;
import android.net.Network;
import android.net.util.NetworkStackUtils;
import android.net.util.SocketUtils;
import android.os.AsyncTask;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.provider.DeviceConfig;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkSocket;
import com.android.net.module.util.netlink.StructInetDiagMsg;
import com.android.net.module.util.netlink.StructNlMsgHdr;
import com.android.networkstack.apishim.NetworkShimImpl;
import com.android.networkstack.apishim.common.ShimUtils;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import java.io.FileDescriptor;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
/**
* Class for NetworkStack to send a SockDiag request and parse the returned tcp info.
*
* This is not thread-safe. This should be only accessed from one thread.
*/
public class TcpSocketTracker {
private static final String TAG = "TcpSocketTracker";
private static final boolean DBG = false;
private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
// Enough for parsing v1 tcp_info for more than 200 sockets per time.
private static final int DEFAULT_RECV_BUFSIZE = 60_000;
// Default I/O timeout time in ms of the socket request.
private static final long IO_TIMEOUT = 3_000L;
/** Cookie offset of an InetMagMessage header. */
private static final int IDIAG_COOKIE_OFFSET = 44;
private static final int UNKNOWN_MARK = 0xffffffff;
private static final int NULL_MASK = 0;
/**
* Gather the socket info.
*
* Key: The idiag_cookie value of the socket. See struct inet_diag_sockid in
* <linux_src>/include/uapi/linux/inet_diag.h
* Value: See {@Code SocketInfo}
*/
private final LongSparseArray<SocketInfo> mSocketInfos = new LongSparseArray<>();
// Number of packets sent since the last received packet
private int mSentSinceLastRecv;
// The latest fail rate calculated by the latest tcp info.
private int mLatestPacketFailPercentage;
// Number of packets received in the latest polling cycle.
private int mLatestReceivedCount;
/**
* Request to send to kernel to request tcp info.
*
* Key: Ip family type.
* Value: Bytes array represent the {@Code inetDiagReqV2}.
*/
private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>();
private final Dependencies mDependencies;
private final INetd mNetd;
private final Network mNetwork;
// The fwmark value of {@code mNetwork}.
private final int mNetworkMark;
// The network id mask of fwmark.
private final int mNetworkMask;
private int mMinPacketsThreshold = DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD;
private int mTcpPacketsFailRateThreshold = DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE;
@VisibleForTesting
protected final DeviceConfig.OnPropertiesChangedListener mConfigListener =
new DeviceConfig.OnPropertiesChangedListener() {
@Override
public void onPropertiesChanged(DeviceConfig.Properties properties) {
mMinPacketsThreshold = mDependencies.getDeviceConfigPropertyInt(
NAMESPACE_CONNECTIVITY,
CONFIG_MIN_PACKETS_THRESHOLD,
DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD);
mTcpPacketsFailRateThreshold = mDependencies.getDeviceConfigPropertyInt(
NAMESPACE_CONNECTIVITY,
CONFIG_TCP_PACKETS_FAIL_PERCENTAGE,
DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE);
}
};
public TcpSocketTracker(@NonNull final Dependencies dps, @NonNull final Network network) {
mDependencies = dps;
mNetwork = network;
mNetd = mDependencies.getNetd();
// If the parcel is null, nothing should be matched which is achieved by the combination of
// {@code NULL_MASK} and {@code UNKNOWN_MARK}.
final MarkMaskParcel parcel = getNetworkMarkMask();
mNetworkMark = (parcel != null) ? parcel.mark : UNKNOWN_MARK;
mNetworkMask = (parcel != null) ? parcel.mask : NULL_MASK;
// Request tcp info from NetworkStack directly needs extra SELinux permission added after Q
// release.
if (!mDependencies.isTcpInfoParsingSupported()) return;
// Build SocketDiag messages.
for (final int family : ADDRESS_FAMILIES) {
mSockDiagMsg.put(
family,
inetDiagReqV2(IPPROTO_TCP,
null /* local addr */,
null /* remote addr */,
family,
(short) (NLM_F_REQUEST | NLM_F_DUMP) /* flag */,
0 /* pad */,
1 << INET_DIAG_MEMINFO /* idiagExt */,
TCP_MONITOR_STATE_FILTER));
}
mDependencies.addDeviceConfigChangedListener(mConfigListener);
}
@Nullable
private MarkMaskParcel getNetworkMarkMask() {
try {
final int netId = NetworkShimImpl.newInstance(mNetwork).getNetId();
return mNetd.getFwmarkForNetwork(netId);
} catch (UnsupportedApiLevelException e) {
log("Get netId is not available in this API level.");
} catch (RemoteException e) {
Log.e(TAG, "Error getting fwmark for network, ", e);
}
return null;
}
/**
* Request to send a SockDiag Netlink request. Receive and parse the returned message. This
* function is not thread-safe and should only be called from only one thread.
*
* @Return if this polling request executes successfully or not.
*/
public boolean pollSocketsInfo() {
if (!mDependencies.isTcpInfoParsingSupported()) return false;
FileDescriptor fd = null;
try {
final long time = SystemClock.elapsedRealtime();
fd = mDependencies.connectToKernel();
final TcpStat stat = new TcpStat();
for (final int family : ADDRESS_FAMILIES) {
mDependencies.sendPollingRequest(fd, mSockDiagMsg.get(family));
// Messages are composed with the following format. Stop parsing when receiving
// message with nlmsg_type NLMSG_DONE.
// +------------------+---------------+--------------+--------+
// | Netlink Header | Family Header | Attributes | rtattr |
// | struct nlmsghdr | struct rtmsg | struct rtattr| data |
// +------------------+---------------+--------------+--------+
// : : :
// +------------------+---------------+--------------+--------+
// | Netlink Header | Family Header | Attributes | rtattr |
// | struct nlmsghdr | struct rtmsg | struct rtattr| data |
// +------------------+---------------+--------------+--------+
final ByteBuffer bytes = mDependencies.recvMessage(fd);
try {
while (enoughBytesRemainForValidNlMsg(bytes)) {
final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(bytes);
if (nlmsghdr == null) {
Log.e(TAG, "Badly formatted data.");
break;
}
final int nlmsgLen = nlmsghdr.nlmsg_len;
log("pollSocketsInfo: nlmsghdr=" + nlmsghdr + ", limit=" + bytes.limit());
// End of the message. Stop parsing.
if (nlmsghdr.nlmsg_type == NLMSG_DONE) break;
if (nlmsghdr.nlmsg_type != SOCK_DIAG_BY_FAMILY) {
Log.e(TAG, "Expect to get family " + family
+ " SOCK_DIAG_BY_FAMILY message but get "
+ nlmsghdr.nlmsg_type);
break;
}
if (isValidInetDiagMsgSize(nlmsgLen)) {
// Get the socket cookie value. Composed by two Integers value.
// Corresponds to inet_diag_sockid in
// &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
bytes.position(bytes.position() + IDIAG_COOKIE_OFFSET);
// It's stored in native with 2 int. Parse it as long for convenience.
final long cookie = bytes.getLong();
// Skip the rest part of StructInetDiagMsg.
bytes.position(bytes.position()
+ StructInetDiagMsg.STRUCT_SIZE - IDIAG_COOKIE_OFFSET
- Long.BYTES);
final SocketInfo info = parseSockInfo(bytes, family, nlmsgLen, time);
// Update TcpStats based on previous and current socket info.
stat.accumulate(
calculateLatestPacketsStat(info, mSocketInfos.get(cookie)));
mSocketInfos.put(cookie, info);
}
}
} catch (IllegalArgumentException | BufferUnderflowException e) {
Log.wtf(TAG, "Unexpected socket info parsing, family " + family
+ " buffer:" + bytes + " "
+ Base64.getEncoder().encodeToString(bytes.array()), e);
}
}
// Calculate mLatestReceiveCount, mSentSinceLastRecv and mLatestPacketFailPercentage.
mSentSinceLastRecv = (stat.receivedCount == 0)
? (mSentSinceLastRecv + stat.sentCount) : 0;
mLatestReceivedCount = stat.receivedCount;
mLatestPacketFailPercentage = ((stat.sentCount != 0)
? ((stat.retransmitCount + stat.lostCount) * 100 / stat.sentCount) : 0);
// Remove out-of-date socket info.
cleanupSocketInfo(time);
return true;
} catch (ErrnoException | SocketException | InterruptedIOException e) {
Log.e(TAG, "Fail to get TCP info via netlink.", e);
} finally {
NetworkStackUtils.closeSocketQuietly(fd);
}
return false;
}
private void cleanupSocketInfo(final long time) {
final int size = mSocketInfos.size();
final List<Long> toRemove = new ArrayList<Long>();
for (int i = 0; i < size; i++) {
final long key = mSocketInfos.keyAt(i);
if (mSocketInfos.get(key).updateTime < time) {
toRemove.add(key);
}
}
for (final Long key : toRemove) {
mSocketInfos.remove(key);
}
}
/** Parse a {@code SocketInfo} from the given position of the given byte buffer. */
@VisibleForTesting
@NonNull
SocketInfo parseSockInfo(@NonNull final ByteBuffer bytes, final int family,
final int nlmsgLen, final long time) {
final int remainingDataSize = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
TcpInfo tcpInfo = null;
int mark = SocketInfo.INIT_MARK_VALUE;
// Get a tcp_info.
while (bytes.position() < remainingDataSize) {
final RoutingAttribute rtattr =
new RoutingAttribute(bytes.getShort(), bytes.getShort());
final short dataLen = rtattr.getDataLength();
if (rtattr.rtaType == RoutingAttribute.INET_DIAG_INFO) {
tcpInfo = TcpInfo.parse(bytes, dataLen);
} else if (rtattr.rtaType == RoutingAttribute.INET_DIAG_MARK) {
mark = bytes.getInt();
} else {
// Data provided by kernel will include both valid data and padding data. The data
// len provided from kernel indicates the valid data size. Readers must deduce the
// alignment by themselves.
skipRemainingAttributesBytesAligned(bytes, dataLen);
}
}
final SocketInfo info = new SocketInfo(tcpInfo, family, mark, time);
log("parseSockInfo, " + info);
return info;
}
/**
* Return if data stall is suspected or not by checking the latest tcp connection fail rate.
* Expect to check after polling the latest status. This function should only be called from
* statemachine thread of NetworkMonitor.
*/
public boolean isDataStallSuspected() {
if (!mDependencies.isTcpInfoParsingSupported()) return false;
return (getLatestPacketFailPercentage() >= getTcpPacketsFailRateThreshold());
}
/** Calculate the change between the {@param current} and {@param previous}. */
@Nullable
private TcpStat calculateLatestPacketsStat(@NonNull final SocketInfo current,
@Nullable final SocketInfo previous) {
final TcpStat stat = new TcpStat();
// Ignore non-target network sockets.
if ((current.fwmark & mNetworkMask) != mNetworkMark) {
return null;
}
if (current.tcpInfo == null) {
log("Current tcpInfo is null.");
return null;
}
stat.sentCount = current.tcpInfo.mSegsOut;
stat.receivedCount = current.tcpInfo.mSegsIn;
stat.lostCount = current.tcpInfo.mLost;
stat.retransmitCount = current.tcpInfo.mRetransmits;
if (previous != null && previous.tcpInfo != null) {
stat.sentCount -= previous.tcpInfo.mSegsOut;
stat.receivedCount -= previous.tcpInfo.mSegsIn;
stat.lostCount -= previous.tcpInfo.mLost;
stat.retransmitCount -= previous.tcpInfo.mRetransmits;
}
return stat;
}
/**
* Get tcp connection fail rate based on packet lost and retransmission count.
*
* @return the latest packet fail percentage. -1 denotes that there is no available data.
*/
public int getLatestPacketFailPercentage() {
if (!mDependencies.isTcpInfoParsingSupported()) return -1;
// Only return fail rate if device sent enough packets.
if (getSentSinceLastRecv() < getMinPacketsThreshold()) return -1;
return mLatestPacketFailPercentage;
}
/**
* Return the number of packets sent since last received. Note that this number is calculated
* between each polling period, not an accurate number.
*/
public int getSentSinceLastRecv() {
if (!mDependencies.isTcpInfoParsingSupported()) return -1;
return mSentSinceLastRecv;
}
/** Return the number of the packets received in the latest polling cycle. */
public int getLatestReceivedCount() {
if (!mDependencies.isTcpInfoParsingSupported()) return -1;
return mLatestReceivedCount;
}
/** Check if the length and position of the given ByteBuffer is valid for a nlmsghdr message. */
@VisibleForTesting
static boolean enoughBytesRemainForValidNlMsg(@NonNull final ByteBuffer bytes) {
return bytes.remaining() >= StructNlMsgHdr.STRUCT_SIZE;
}
private static boolean isValidInetDiagMsgSize(final int nlMsgLen) {
return nlMsgLen >= SOCKDIAG_MSG_HEADER_SIZE;
}
private int getMinPacketsThreshold() {
return mMinPacketsThreshold;
}
private int getTcpPacketsFailRateThreshold() {
return mTcpPacketsFailRateThreshold;
}
/**
* Method to skip the remaining attributes bytes.
* Corresponds to NLMSG_NEXT in bionic/libc/kernel/uapi/linux/netlink.h.
*
* @param buffer the target ByteBuffer
* @param len the remaining length to skip.
*/
private void skipRemainingAttributesBytesAligned(@NonNull final ByteBuffer buffer,
final short len) {
// Data in {@Code RoutingAttribute} is followed after header with size {@Code NLA_ALIGNTO}
// bytes long for each block. Next attribute will start after the padding bytes if any.
// If all remaining bytes after header are valid in a data block, next attr will just start
// after valid bytes.
//
// E.g. With NLA_ALIGNTO(4), an attr struct with length 5 means 1 byte valid data remains
// after header and 3(4-1) padding bytes. Next attr with length 8 will start after the
// padding bytes and contain 4(8-4) valid bytes of data. The next attr start after the
// valid bytes, like:
//
// [HEADER(L=5)][ 4-Bytes DATA ][ HEADER(L=8) ][4 bytes DATA][Next attr]
// [ 5 valid bytes ][3 padding bytes ][ 8 valid bytes ] ...
final int cur = buffer.position();
buffer.position(cur + NetlinkConstants.alignedLengthOf(len));
}
private void log(final String str) {
if (DBG) Log.d(TAG, str);
}
/**
* Corresponds to {@code struct rtattr} from bionic/libc/kernel/uapi/linux/rtnetlink.h
*
* struct rtattr {
* unsigned short rta_len; // Length of option
* unsigned short rta_type; // Type of option
* // Data follows
* };
*/
class RoutingAttribute {
public static final int HEADER_LENGTH = 4;
// Corresponds to enum definition in bionic/libc/kernel/uapi/linux/inet_diag.h
public static final int INET_DIAG_INFO = 2;
public static final int INET_DIAG_MARK = 15;
public final short rtaLen; // The whole valid size of the struct.
public final short rtaType;
RoutingAttribute(final short len, final short type) {
rtaLen = len;
rtaType = type;
}
public short getDataLength() {
return (short) (rtaLen - HEADER_LENGTH);
}
}
/**
* Data class for keeping the socket info.
*/
@VisibleForTesting
class SocketInfo {
// Initial mark value corresponds to the initValue in system/netd/include/Fwmark.h.
public static final int INIT_MARK_VALUE = 0;
@Nullable
public final TcpInfo tcpInfo;
// One of {@code AF_INET6, AF_INET}.
public final int ipFamily;
// "fwmark" value of the socket queried from native.
public final int fwmark;
// Socket information updated elapsed real time.
public final long updateTime;
SocketInfo(@Nullable final TcpInfo info, final int family, final int mark,
final long time) {
tcpInfo = info;
ipFamily = family;
updateTime = time;
fwmark = mark;
}
@Override
public String toString() {
return "SocketInfo {Type:" + ipTypeToString(ipFamily) + ", "
+ tcpInfo + ", mark:" + fwmark + " updated at " + updateTime + "}";
}
private String ipTypeToString(final int type) {
if (type == AF_INET) {
return "IP";
} else if (type == AF_INET6) {
return "IPV6";
} else {
return "UNKNOWN";
}
}
}
/**
* private data class only for storing the Tcp statistic for calculating the fail rate and sent
* count
* */
private class TcpStat {
public int sentCount;
public int lostCount;
public int retransmitCount;
public int receivedCount;
void accumulate(@Nullable final TcpStat stat) {
if (stat == null) return;
sentCount += stat.sentCount;
lostCount += stat.lostCount;
receivedCount += stat.receivedCount;
retransmitCount += stat.retransmitCount;
}
}
/**
* Dependencies class for testing.
*/
@VisibleForTesting
public static class Dependencies {
private final Context mContext;
public Dependencies(final Context context) {
mContext = context;
}
/**
* Connect to kernel via netlink socket.
*
* @return fd the fileDescriptor of the socket.
* Throw ErrnoException, SocketException if the exception is thrown.
*/
public FileDescriptor connectToKernel() throws ErrnoException, SocketException {
final FileDescriptor fd =
Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_INET_DIAG);
Os.connect(
fd, SocketUtils.makeNetlinkSocketAddress(0 /* portId */, 0 /* groupMask */));
return fd;
}
/**
* Send composed message request to kernel.
* @param fd see {@Code FileDescriptor}
* @param msg the byte array represent the request message to write to kernel.
*
* Throw ErrnoException or InterruptedIOException if the exception is thrown.
*/
public void sendPollingRequest(@NonNull final FileDescriptor fd, @NonNull final byte[] msg)
throws ErrnoException, InterruptedIOException {
Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
StructTimeval.fromMillis(IO_TIMEOUT));
Os.write(fd, msg, 0 /* byteOffset */, msg.length);
}
/**
* Look up the value of a property in DeviceConfig.
* @param namespace The namespace containing the property to look up.
* @param name The name of the property to look up.
* @param defaultValue The value to return if the property does not exist or has no non-null
* value.
* @return the corresponding value, or defaultValue if none exists.
*/
public int getDeviceConfigPropertyInt(@NonNull final String namespace,
@NonNull final String name, final int defaultValue) {
return DeviceConfigUtils.getDeviceConfigPropertyInt(namespace, name, defaultValue);
}
/**
* Return if request tcp info via netlink socket is supported or not.
*/
public boolean isTcpInfoParsingSupported() {
// Request tcp info from NetworkStack directly needs extra SELinux permission added
// after Q release.
return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q);
}
/**
* Receive the request message from kernel via given fd.
*/
public ByteBuffer recvMessage(@NonNull final FileDescriptor fd)
throws ErrnoException, InterruptedIOException {
return NetlinkSocket.recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT);
}
public Context getContext() {
return mContext;
}
/**
* Get an INetd connector.
*/
public INetd getNetd() {
return INetd.Stub.asInterface(
(IBinder) mContext.getSystemService(Context.NETD_SERVICE));
}
/** Add device config change listener */
public void addDeviceConfigChangedListener(
@NonNull final DeviceConfig.OnPropertiesChangedListener listener) {
DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_CONNECTIVITY,
AsyncTask.THREAD_POOL_EXECUTOR, listener);
}
}
}