From 6a9f0db983ed87cfd66396b9fed86535fd458965 Mon Sep 17 00:00:00 2001 From: chiachangwang Date: Wed, 14 Dec 2022 08:40:42 +0000 Subject: [PATCH] [DK1]Add TCP polling mechanism Add TCP polling mechanism in KeepaliveTracker to understand if there are any TCP sockets in the target network. This is a preparation commit for dynamically control keepalive based on the existence of TCP sockets. This is non-functional now since there is no caller to retrieve the information now. Bug: 259000745 Test: atest FrameworksNetTests Test: Manually test by creating TCP sockets on the target network in device and check if deisgn works. Change-Id: I355ac340cad2fac618bb9d65fb1b1539ea644959 --- .../server/connectivity/KeepaliveTracker.java | 245 +++++++++++++++++- .../connectivity/KeepaliveTrackerTest.java | 243 +++++++++++++++++ 2 files changed, 484 insertions(+), 4 deletions(-) create mode 100644 tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java index 3b58823304..9c36760fae 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.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; @@ -55,18 +67,29 @@ 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; @@ -84,6 +107,7 @@ 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 > mKeepalives = @@ -107,17 +131,35 @@ 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 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 @@ public class KeepaliveTracker { 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 @@ 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; + } } 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 0000000000..b55ee67a7a --- /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; + } +}