Merge "[DK1]Add TCP polling mechanism"
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index 3b58823..9c36760 100644
--- a/service/src/com/android/server/connectivity/KeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -33,15 +33,27 @@
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;
@@ -55,18 +67,29 @@
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;
@@ -84,6 +107,7 @@
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 =
@@ -107,17 +131,35 @@
// 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;
- mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext);
+ mDependencies = dependencies;
+ mSupportedKeepalives = mDependencies.getSupportedKeepalives();
+ mNetd = mDependencies.getNetd();
- final ConnectivityResources res = new ConnectivityResources(mContext);
- mReservedPrivilegedSlots = res.get().getInteger(
+ final Resources res = mDependencies.newConnectivityResources();
+ mReservedPrivilegedSlots = res.getInteger(
R.integer.config_reservedPrivilegedKeepaliveSlots);
- mAllowedUnprivilegedSlotsForUid = res.get().getInteger(
+ mAllowedUnprivilegedSlotsForUid = res.getInteger(
R.integer.config_allowedUnprivilegedKeepalivePerUid);
}
@@ -739,6 +781,9 @@
return true;
}
+ /**
+ * Dump KeepaliveTracker state.
+ */
public void dump(IndentingPrintWriter pw) {
pw.println("Supported Socket keepalives: " + Arrays.toString(mSupportedKeepalives));
pw.println("Reserved Privileged keepalives: " + mReservedPrivilegedSlots);
@@ -756,4 +801,196 @@
}
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;
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java
new file mode 100644
index 0000000..b55ee67
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2022 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 org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+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;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.nio.ByteBuffer;
+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};
+ 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 HandlerThread mHandlerThread;
+
+ @Mock INetd mNetd;
+ @Mock KeepaliveTracker.Dependencies mDependencies;
+ @Mock Context mCtx;
+ @Mock Resources mResources;
+
+ // Hexadecimal representation of a SOCK_DIAG response with tcp info.
+ private static final String SOCK_DIAG_TCP_INET_HEX =
+ // struct nlmsghdr.
+ "14010000" + // length = 276
+ "1400" + // type = SOCK_DIAG_BY_FAMILY
+ "0301" + // flags = NLM_F_REQUEST | NLM_F_DUMP
+ "00000000" + // seqno
+ "00000000" + // pid (0 == kernel)
+ // struct inet_diag_req_v2
+ "02" + // family = AF_INET
+ "06" + // state
+ "00" + // timer
+ "00" + // retrans
+ // inet_diag_sockid
+ "DEA5" + // idiag_sport = 42462
+ "71B9" + // idiag_dport = 47473
+ "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2
+ "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
+ "00000000" + // idiag_if
+ "34ED000076270000" + // idiag_cookie = 43387759684916
+ "00000000" + // idiag_expires
+ "00000000" + // idiag_rqueue
+ "00000000" + // idiag_wqueue
+ "00000000" + // idiag_uid
+ "00000000" + // idiag_inode
+ // rtattr
+ "0500" + // len = 5
+ "0800" + // type = 8
+ "00000000" + // data
+ "0800" + // len = 8
+ "0F00" + // type = 15(INET_DIAG_MARK)
+ "850A0C00" + // data, socket mark=789125
+ "AC00" + // len = 172
+ "0200" + // type = 2(INET_DIAG_INFO)
+ // tcp_info
+ "01" + // state = TCP_ESTABLISHED
+ "00" + // ca_state = TCP_CA_OPEN
+ "05" + // retransmits = 5
+ "00" + // probes = 0
+ "00" + // backoff = 0
+ "07" + // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
+ "88" + // wscale = 8
+ "00" + // delivery_rate_app_limited = 0
+ "4A911B00" + // rto = 1806666
+ "00000000" + // ato = 0
+ "2E050000" + // sndMss = 1326
+ "18020000" + // rcvMss = 536
+ "00000000" + // unsacked = 0
+ "00000000" + // acked = 0
+ "00000000" + // lost = 0
+ "00000000" + // retrans = 0
+ "00000000" + // fackets = 0
+ "BB000000" + // lastDataSent = 187
+ "00000000" + // lastAckSent = 0
+ "BB000000" + // lastDataRecv = 187
+ "BB000000" + // lastDataAckRecv = 187
+ "DC050000" + // pmtu = 1500
+ "30560100" + // rcvSsthresh = 87600
+ "3E2C0900" + // rttt = 601150
+ "1F960400" + // rttvar = 300575
+ "78050000" + // sndSsthresh = 1400
+ "0A000000" + // sndCwnd = 10
+ "A8050000" + // advmss = 1448
+ "03000000" + // reordering = 3
+ "00000000" + // rcvrtt = 0
+ "30560100" + // rcvspace = 87600
+ "00000000" + // totalRetrans = 0
+ "53AC000000000000" + // pacingRate = 44115
+ "FFFFFFFFFFFFFFFF" + // maxPacingRate = 18446744073709551615
+ "0100000000000000" + // bytesAcked = 1
+ "0000000000000000" + // bytesReceived = 0
+ "0A000000" + // SegsOut = 10
+ "00000000" + // SegsIn = 0
+ "00000000" + // NotSentBytes = 0
+ "3E2C0900" + // minRtt = 601150
+ "00000000" + // DataSegsIn = 0
+ "00000000" + // DataSegsOut = 0
+ "0000000000000000"; // deliverRate = 0
+ private static final String SOCK_DIAG_NO_TCP_INET_HEX =
+ // struct nlmsghdr
+ "14000000" // length = 20
+ + "0300" // type = NLMSG_DONE
+ + "0301" // flags = NLM_F_REQUEST | NLM_F_DUMP
+ + "00000000" // seqno
+ + "00000000" // pid (0 == kernel)
+ // struct inet_diag_req_v2
+ + "02" // family = AF_INET
+ + "06" // state
+ + "00" // timer
+ + "00"; // retrans
+ private static final byte[] SOCK_DIAG_NO_TCP_INET_BYTES =
+ HexEncoding.decode(SOCK_DIAG_NO_TCP_INET_HEX.toCharArray(), false);
+ private static final String TEST_RESPONSE_HEX =
+ SOCK_DIAG_TCP_INET_HEX + SOCK_DIAG_NO_TCP_INET_HEX;
+ private static final byte[] TEST_RESPONSE_BYTES =
+ HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false);
+
+ @Before
+ public void setup() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ doReturn(mNetd).when(mDependencies).getNetd();
+ 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);
+ }
+
+ @Test
+ public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
+ setupResponseWithSocketExisting();
+ assertThrows(IllegalStateException.class,
+ () -> mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
+ }
+
+ @Test
+ public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
+ setupResponseWithSocketExisting();
+ mHandlerThread.getThreadHandler().post(
+ () -> assertTrue(mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+ }
+
+ @Test
+ public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
+ setupResponseWithSocketExisting();
+ mHandlerThread.getThreadHandler().post(
+ () -> assertFalse(mKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
+ }
+
+ @Test
+ public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
+ setupResponseWithoutSocketExisting();
+ mHandlerThread.getThreadHandler().post(
+ () -> assertFalse(mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+ }
+
+ private void setupResponseWithSocketExisting() throws Exception {
+ final ByteBuffer tcpBufferV6 = getByteBuffer(TEST_RESPONSE_BYTES);
+ final ByteBuffer tcpBufferV4 = getByteBuffer(TEST_RESPONSE_BYTES);
+ doReturn(tcpBufferV6, tcpBufferV4).when(mDependencies).recvSockDiagResponse(any());
+ }
+
+ private void setupResponseWithoutSocketExisting() throws Exception {
+ final ByteBuffer tcpBufferV6 = getByteBuffer(SOCK_DIAG_NO_TCP_INET_BYTES);
+ final ByteBuffer tcpBufferV4 = getByteBuffer(SOCK_DIAG_NO_TCP_INET_BYTES);
+ doReturn(tcpBufferV6, tcpBufferV4).when(mDependencies).recvSockDiagResponse(any());
+ }
+
+ private MarkMaskParcel makeMarkMaskParcel(final int mask, final int mark) {
+ final MarkMaskParcel parcel = new MarkMaskParcel();
+ parcel.mask = mask;
+ parcel.mark = mark;
+ return parcel;
+ }
+
+ private ByteBuffer getByteBuffer(final byte[] bytes) {
+ final ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ buffer.order(ByteOrder.nativeOrder());
+ return buffer;
+ }
+}