blob: ab9816cfb62fd73aad7382fbb7b36c570464b80b [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_TCP_PACKETS_FAIL_PERCENTAGE;
import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE;
import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.system.OsConstants.AF_INET;
import static com.android.net.module.util.NetworkStackConstants.DNS_OVER_TLS_PORT;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.INetd;
import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.MarkMaskParcel;
import android.net.Network;
import android.os.Build;
import android.os.PowerManager;
import android.util.Log;
import android.util.Log.TerribleFailureHandler;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.StructNlMsgHdr;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import libcore.util.HexEncoding;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.FileDescriptor;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
// TODO: Add more tests for missing coverage.
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TcpSocketTrackerTest {
private static final int TEST_BUFFER_SIZE = 1024;
private static final String DIAG_MSG_HEX =
// struct nlmsghdr.
"10000000" + // length = 16
"1400" + // type = SOCK_DIAG_BY_FAMILY
"0301" + // flags = NLM_F_REQUEST | NLM_F_DUMP
"00000000" + // seqno
"00000000"; // pid (0 == kernel)
private static final byte[] SOCK_DIAG_MSG_BYTES =
HexEncoding.decode(DIAG_MSG_HEX.toCharArray(), false);
// Hexadecimal representation of a SOCK_DIAG response with tcp info.
private static final String SOCK_DIAG_TCP_TEST_HEX =
composeSockDiagTcpHex(5 /* retrans */, 10 /* sent */);
private static final byte[] SOCK_DIAG_TCP_INET_TEST_BYTES =
HexEncoding.decode(SOCK_DIAG_TCP_TEST_HEX.toCharArray(), false);
private static final TcpInfo TEST_TCPINFO =
new TcpInfo(10 /* segsOut */, 0 /* segsIn */, 5 /* totalRetrans */);
private static final String NLMSG_DONE_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 String TEST_RESPONSE_HEX = SOCK_DIAG_TCP_TEST_HEX + NLMSG_DONE_HEX;
private static final byte[] TEST_RESPONSE_BYTES =
HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false);
private static final int TEST_NETID1 = 0xA85;
private static final int TEST_NETID2 = 0x1A85;
private static final int TEST_NETID1_FWMARK = 0x0A85;
private static final int TEST_NETID2_FWMARK = 0x1A85;
private static final int NETID_MASK = 0xffff;
private static final int TEST_UID1 = 1234;
private static final short TEST_DST_PORT = 29113;
private static final long TEST_COOKIE1 = 43387759684916L;
private static final long TEST_COOKIE2 = TEST_COOKIE1 + 1;
private static final InetAddress TEST_DNS1 = InetAddresses.parseNumericAddress("8.8.8.8");
@Mock private TcpSocketTracker.Dependencies mDependencies;
@Mock private INetd mNetd;
private final Network mNetwork = new Network(TEST_NETID1);
private final Network mOtherNetwork = new Network(TEST_NETID2);
private TerribleFailureHandler mOldWtfHandler;
@Mock private Context mContext;
@Mock private PowerManager mPowerManager;
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
// Override the default TerribleFailureHandler, as that handler might terminate the process
// (if we're on an eng build).
mOldWtfHandler =
Log.setWtfHandler((tag, what, system) -> Log.e(tag, what.getMessage(), what));
when(mDependencies.getNetd()).thenReturn(mNetd);
when(mDependencies.connectToKernel()).thenReturn(new FileDescriptor());
when(mDependencies.getDeviceConfigPropertyInt(
eq(NAMESPACE_CONNECTIVITY),
eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE),
anyInt())).thenReturn(DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE);
when(mDependencies.shouldDisableInLightDoze()).thenReturn(true);
when(mNetd.getFwmarkForNetwork(eq(TEST_NETID1)))
.thenReturn(makeMarkMaskParcel(NETID_MASK, TEST_NETID1_FWMARK));
doReturn(mPowerManager).when(mContext).getSystemService(PowerManager.class);
}
@After
public void tearDown() {
Log.setWtfHandler(mOldWtfHandler);
}
private MarkMaskParcel makeMarkMaskParcel(final int mask, final int mark) {
final MarkMaskParcel parcel = new MarkMaskParcel();
parcel.mask = mask;
parcel.mark = mark;
return parcel;
}
private ByteBuffer getByteBufferFromHexString(String hexStr) {
final byte[] bytes = HexEncoding.decode(hexStr.toCharArray(), false);
return getByteBuffer(bytes);
}
private ByteBuffer getByteBuffer(final byte[] bytes) {
final ByteBuffer buffer = ByteBuffer.wrap(bytes);
buffer.order(ByteOrder.nativeOrder());
return buffer;
}
@Test
public void testParseSockInfo() {
final ByteBuffer buffer = getByteBuffer(SOCK_DIAG_TCP_INET_TEST_BYTES);
final ArrayList<TcpSocketTracker.SocketInfo> infoList = new ArrayList<>();
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
tst.parseMessage(buffer, AF_INET, infoList, 100L);
assertEquals(1, infoList.size());
final TcpSocketTracker.SocketInfo parsed = infoList.get(0);
assertEquals(parsed.tcpInfo, TEST_TCPINFO);
assertEquals(parsed.fwmark, 789125);
assertEquals(parsed.updateTime, 100);
assertEquals(parsed.ipFamily, AF_INET);
assertEquals(parsed.uid, TEST_UID1);
assertEquals(parsed.cookie, TEST_COOKIE1);
assertEquals(parsed.dstPort, TEST_DST_PORT);
}
@Test
public void testEnoughBytesRemainForValidNlMsg() {
final ByteBuffer buffer = ByteBuffer.allocate(TEST_BUFFER_SIZE);
buffer.position(TEST_BUFFER_SIZE - StructNlMsgHdr.STRUCT_SIZE);
assertTrue(NetlinkUtils.enoughBytesRemainForValidNlMsg(buffer));
// Remaining buffer size is less than a valid StructNlMsgHdr size.
buffer.position(TEST_BUFFER_SIZE - StructNlMsgHdr.STRUCT_SIZE + 1);
assertFalse(NetlinkUtils.enoughBytesRemainForValidNlMsg(buffer));
buffer.position(TEST_BUFFER_SIZE);
assertFalse(NetlinkUtils.enoughBytesRemainForValidNlMsg(buffer));
}
@Test
public void testPollSocketsInfo() throws Exception {
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
// No enough bytes remain for a valid NlMsg.
final ByteBuffer invalidBuffer = ByteBuffer.allocate(1);
invalidBuffer.order(ByteOrder.nativeOrder());
when(mDependencies.recvMessage(any())).thenReturn(invalidBuffer);
assertTrue(tst.pollSocketsInfo());
assertEquals(-1, tst.getLatestPacketFailPercentage());
assertEquals(0, tst.getSentSinceLastRecv());
// Header only.
final ByteBuffer headerBuffer = getByteBuffer(SOCK_DIAG_MSG_BYTES);
when(mDependencies.recvMessage(any())).thenReturn(headerBuffer);
assertTrue(tst.pollSocketsInfo());
assertEquals(-1, tst.getLatestPacketFailPercentage());
assertEquals(0, tst.getSentSinceLastRecv());
setupNormalTestTcpInfo();
assertTrue(tst.pollSocketsInfo());
assertEquals(10, tst.getSentSinceLastRecv());
assertEquals(50, tst.getLatestPacketFailPercentage());
assertFalse(tst.isDataStallSuspected());
// Lower the threshold.
when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE),
anyInt())).thenReturn(40);
// No device config change. Using cache value.
assertFalse(tst.isDataStallSuspected());
// Trigger a config update
tst.mConfigListener.onPropertiesChanged(null /* properties */);
assertTrue(tst.isDataStallSuspected());
}
@Test
public void testPollSocketsInfo_ignorePrivateDnsPort() throws Exception {
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
// Simulate 1 message with data stall happened.
doReturn(getByteBufferFromHexString(
composeSockDiagTcpHex(9, 10) + NLMSG_DONE_HEX))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// 9 retrans / 10 sent = 90 percent.
assertEquals(90, tst.getLatestPacketFailPercentage());
assertEquals(10, tst.getSentSinceLastRecv());
assertTrue(tst.isDataStallSuspected());
// Append another message with private dns port which is generated
// in opportunistic mode. Also simulated the private dns probe is not finished.
tst.setOpportunisticMode(true);
final LinkProperties testLp = new LinkProperties();
testLp.addDnsServer(TEST_DNS1);
tst.setLinkProperties(testLp);
doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10)
+ composeSockDiagTcpHex(9, 10, DNS_OVER_TLS_PORT, TEST_COOKIE2)
+ NLMSG_DONE_HEX))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Verify that when in opportunistic mode, the message with private dns
// port won't get involved with the calculation.
// While there is no packet sent in this polling cycle, 0 percentage is expected while the
// sent counter remains the same.
assertEquals(0, tst.getLatestPacketFailPercentage());
assertEquals(10, tst.getSentSinceLastRecv());
assertFalse(tst.isDataStallSuspected());
// Verify that when private dns servers are all validated, the message with private dns port
// will be counted.
testLp.addValidatedPrivateDnsServer(TEST_DNS1);
tst.setLinkProperties(testLp);
doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(10, 12)
+ composeSockDiagTcpHex(11, 12, DNS_OVER_TLS_PORT, TEST_COOKIE2)
+ NLMSG_DONE_HEX))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Retrans ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent.
assertEquals(75, tst.getLatestPacketFailPercentage());
assertEquals(14, tst.getSentSinceLastRecv());
assertFalse(tst.isDataStallSuspected());
// Verify that when exited opportunistic mode, the message with private dns port will be
// counted. And the stat is correctly subtracted from the stat ignored in the previous
// polling cycle.
tst.setOpportunisticMode(false);
doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(11, 14)
+ composeSockDiagTcpHex(13, 14, DNS_OVER_TLS_PORT, TEST_COOKIE2)
+ NLMSG_DONE_HEX))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Retrans ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent.
assertEquals(75, tst.getLatestPacketFailPercentage());
assertEquals(18, tst.getSentSinceLastRecv());
assertFalse(tst.isDataStallSuspected());
}
@Test
public void testTcpInfoParsingWithMultipleMsgs() throws Exception {
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
// Case 1: A message about 5 sockets, then a message about 2 sockets,
// then a message about 2 sockets together with DONE
//
// Mocking 6 return results for different IP families(3 for IPv6; 3 for Ipv4). Use the same
// message for different IP families to reduce the complexity.
doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 5)),
getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 2)),
getByteBufferFromHexString(
repeat(composeSockDiagTcpHex(5, 10), 2) + NLMSG_DONE_HEX),
getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 5)),
getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 2)),
getByteBufferFromHexString(
repeat(composeSockDiagTcpHex(5, 10), 2) + NLMSG_DONE_HEX))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Verify that code reads all the messages. (3 times for IPv4, 3 times for IPv6)
verify(mDependencies, times(6)).recvMessage(any());
// Calculated from totalRetrans / segsout.
// Note that the counters cannot be verified given that the cookie of the mocked sockets
// are the same, the latest SocketInfo would overwrite previous reported ones.
assertEquals(50, tst.getLatestPacketFailPercentage());
// Lower than the 80% threshold
assertFalse(tst.isDataStallSuspected());
// Case 2: A message about 1 socket, then a message about 5 sockets,
// then a message about 1 socket with DONE.
// "Sent" increases by 5. No change for lost and retrans.
//
// Mocking 6 return results for different IP families(3 for IPv6; 3 for Ipv4). Use the same
// message for different IP families to reduce the complexity.
doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)),
getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)),
getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX),
getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)),
getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)),
getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Not reset mDependencies because it will reset other mocks.
// Another 3 times for IPv6 and 3 times for IPv4
verify(mDependencies, times(12)).recvMessage(any());
// (5 lost + 0 retrans)/5 sent
assertEquals(100, tst.getLatestPacketFailPercentage());
assertTrue(tst.isDataStallSuspected());
// Case 3: A message about 5 sockets, then a message about 1 socket,
// then a message about 1 socket with DONE.
// No change for sent, lost and retrans.
//
// Mocking 4 return results for different IP families(2 for IPv6; 2 for Ipv4). Use the same
// message for different IP families to reduce the complexity.
doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)),
getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)),
getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX),
getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)),
getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)),
getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Another 3 times for IPv6 and 3 times for IPv4
verify(mDependencies, times(18)).recvMessage(any());
// (0 lost + 0 retrans)/0 sent
assertEquals(0, tst.getLatestPacketFailPercentage());
// Lower than the 80% threshold
assertFalse(tst.isDataStallSuspected());
// Case 4: A message about 8 sockets with DONE.
// "lost" increases by 3 and "sent" increases by 5
//
// Mocking 2 return results for different IP families(1 for IPv6; 1 for Ipv4). Use the same
// message for different IP families to reduce the complexity.
doReturn(getByteBufferFromHexString(
repeat(composeSockDiagTcpHex(14, 20), 8) + NLMSG_DONE_HEX),
getByteBufferFromHexString(
repeat(composeSockDiagTcpHex(14, 20), 8) + NLMSG_DONE_HEX))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Another 1 time for IPv6 and 1 time for IPv4
verify(mDependencies, times(20)).recvMessage(any());
// (4 lost + 0 retrans)/5 sent
assertEquals(80, tst.getLatestPacketFailPercentage());
//Reach 80% threshold
assertTrue(tst.isDataStallSuspected());
// Case 5: A message about DONE with 2 sockets.
// No socket information will be parsed though "lost" increases by 6 and "sent"
// increases by 6.
//
// Mocking 2 return results for different IP families(1 for IPv6; 1 for Ipv4). Use the same
// message for different IP families to reduce the complexity.
doReturn(getByteBufferFromHexString(
NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(20, 26), 2)),
getByteBufferFromHexString(
NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(20, 26), 2)))
.when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Another 1 time for IPv6 and 1 time for IPv4
verify(mDependencies, times(22)).recvMessage(any());
// (0 lost + 0 retrans)/0 sent.
// Parsing will be stopped in DONE message. No socket information will be parsed.
assertEquals(0, tst.getLatestPacketFailPercentage());
// Lower than the 80% threshold
assertFalse(tst.isDataStallSuspected());
}
private String repeat(String orig, int times) {
if (SdkLevel.isAtLeastT()) {
// Only supported from Java 11
return orig.repeat(times);
} else {
String repeated = "";
for (int i = 0; i < times; i++) {
repeated += orig;
}
return repeated;
}
}
private static String getHexStringFromInt(int v) {
// Android is always little-endian. Refer to https://developer.android.com/ndk/guides/abis.
return getHexStringOfSize(v, ByteOrder.nativeOrder(), Integer.BYTES);
}
private static String getHexStringFromShort(short v, ByteOrder order) {
return getHexStringOfSize(v, order, Short.BYTES);
}
private static String getHexStringFromLong(long v) {
// Android is always little-endian. Refer to https://developer.android.com/ndk/guides/abis.
return getHexStringOfSize(v, ByteOrder.nativeOrder(), Long.BYTES);
}
private static String getHexStringOfSize(long v, ByteOrder order, int size) {
final ByteBuffer bb = ByteBuffer.allocate(size);
bb.order(order);
switch (size) {
case Short.BYTES:
bb.putShort((short) v);
break;
case Integer.BYTES:
bb.putInt((int) v);
break;
case Long.BYTES:
bb.putLong(v);
break;
default:
throw new IllegalArgumentException("Unsupported size: " + size);
}
String s = "";
for (byte b : bb.array()) {
s += String.format("%02X", b);
}
return s;
}
private static String composeSockDiagTcpHex(int retrans, int sent) {
return composeSockDiagTcpHex(retrans, sent, TEST_DST_PORT, TEST_COOKIE1);
}
private static String composeSockDiagTcpHex(int retrans, int sent, short dstPort, long cookie) {
return // 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: ports and addresses are always in big endian,
// see StructInetDiagSockId.
+ "DEA5" // idiag_sport = 56997
+ getHexStringFromShort(dstPort, ByteOrder.BIG_ENDIAN) // idiag_dport
+ "0a006402000000000000000000000000" // idiag_src = 10.0.100.2
+ "08080808000000000000000000000000" // idiag_dst = 8.8.8.8
+ "00000000" // idiag_if
+ getHexStringFromLong(cookie) // idiag_cookie
+ "00000000" // idiag_expires
+ "00000000" // idiag_rqueue
+ "00000000" // idiag_wqueue
+ getHexStringFromInt(TEST_UID1) // 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
+ "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
+ getHexStringFromInt(retrans) // totalRetrans
+ "53AC000000000000" // pacingRate = 44115
+ "FFFFFFFFFFFFFFFF" // maxPacingRate = 18446744073709551615
+ "0100000000000000" // bytesAcked = 1
+ "0000000000000000" // bytesReceived = 0
+ getHexStringFromInt(sent) // SegsOut
+ "00000000" // SegsIn = 0
+ "00000000" // NotSentBytes = 0
+ "3E2C0900" // minRtt = 601150
+ "00000000" // DataSegsIn = 0
+ "00000000" // DataSegsOut = 0
+ "0000000000000000"; // deliverRate = 0
}
@Test
public void testTcpInfoParsingWithDozeMode() throws Exception {
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
ArgumentCaptor.forClass(BroadcastReceiver.class);
verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
setupNormalTestTcpInfo();
assertTrue(tst.pollSocketsInfo());
// Lower the threshold.
when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE),
anyInt())).thenReturn(40);
// Trigger a config update.
tst.mConfigListener.onPropertiesChanged(null /* properties */);
assertEquals(10, tst.getSentSinceLastRecv());
assertEquals(50, tst.getLatestPacketFailPercentage());
assertTrue(tst.isDataStallSuspected());
// Enable doze mode, verify counters are not updated.
doReturn(true).when(mPowerManager).isDeviceIdleMode();
final BroadcastReceiver receiver = receiverCaptor.getValue();
receiver.onReceive(mContext, new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
assertFalse(tst.pollSocketsInfo());
assertEquals(10, tst.getSentSinceLastRecv());
assertEquals(50, tst.getLatestPacketFailPercentage());
assertFalse(tst.isDataStallSuspected());
}
@Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testTcpInfoDisableParsingWithLightDozeMode_enabled() throws Exception {
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
ArgumentCaptor.forClass(BroadcastReceiver.class);
// Enable light doze mode with 1 netlink message.
verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
final BroadcastReceiver receiver = receiverCaptor.getValue();
doReturn(true).when(mPowerManager).isDeviceLightIdleMode();
receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10)
+ NLMSG_DONE_HEX)).when(mDependencies).recvMessage(any());
// Verify counters are not updated.
assertFalse(tst.pollSocketsInfo());
assertEquals(0, tst.getSentSinceLastRecv());
// -1 if not enough packets.
assertEquals(-1, tst.getLatestPacketFailPercentage());
assertFalse(tst.isDataStallSuspected());
// Disable light doze mode, verify polling are processed and counters are updated.
doReturn(false).when(mPowerManager).isDeviceLightIdleMode();
receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
assertTrue(tst.pollSocketsInfo());
assertEquals(10, tst.getSentSinceLastRecv());
// Lost 4 + default 5 retrans / 10 sent.
assertEquals(90, tst.getLatestPacketFailPercentage());
assertTrue(tst.isDataStallSuspected());
}
@Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testTcpInfoDisableParsingWithLightDozeMode_disabled() throws Exception {
when(mDependencies.shouldDisableInLightDoze()).thenReturn(false);
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
ArgumentCaptor.forClass(BroadcastReceiver.class);
// Enable light doze mode with 1 netlink message.
verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
final BroadcastReceiver receiver = receiverCaptor.getValue();
doReturn(true).when(mPowerManager).isDeviceLightIdleMode();
receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10)
+ NLMSG_DONE_HEX)).when(mDependencies).recvMessage(any());
// Verify TcpInfo is still processed.
assertTrue(tst.pollSocketsInfo());
assertEquals(10, tst.getSentSinceLastRecv());
assertEquals(90, tst.getLatestPacketFailPercentage());
assertTrue(tst.isDataStallSuspected());
}
private void setupNormalTestTcpInfo() throws Exception {
final ByteBuffer tcpBufferV6 = getByteBuffer(TEST_RESPONSE_BYTES);
final ByteBuffer tcpBufferV4 = getByteBuffer(TEST_RESPONSE_BYTES);
doReturn(tcpBufferV6, tcpBufferV4).when(mDependencies).recvMessage(any());
}
private static final String BAD_DIAG_MSG_HEX =
// struct nlmsghdr.
"00000058" // length = 1476395008
+ "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
private static final byte[] BAD_SOCK_DIAG_MSG_BYTES =
HexEncoding.decode(BAD_DIAG_MSG_HEX.toCharArray(), false);
@Test
public void testPollSocketsInfo_BadFormat() throws Exception {
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
setupNormalTestTcpInfo();
assertTrue(tst.pollSocketsInfo());
assertEquals(10, tst.getSentSinceLastRecv());
assertEquals(50, tst.getLatestPacketFailPercentage());
final ByteBuffer badTcpBufferV6 = getByteBuffer(BAD_SOCK_DIAG_MSG_BYTES);
final ByteBuffer badTcpBufferV4 = getByteBuffer(BAD_SOCK_DIAG_MSG_BYTES);
doReturn(badTcpBufferV6, badTcpBufferV4).when(mDependencies).recvMessage(any());
assertTrue(tst.pollSocketsInfo());
// Expect no additional packets, so still 10.
assertEquals(10, tst.getSentSinceLastRecv());
// Expect to reset to 0.
assertEquals(0, tst.getLatestPacketFailPercentage());
}
@Test
public void testUnMatchNetwork() throws Exception {
when(mNetd.getFwmarkForNetwork(eq(TEST_NETID2)))
.thenReturn(makeMarkMaskParcel(NETID_MASK, TEST_NETID2_FWMARK));
final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mOtherNetwork);
setupNormalTestTcpInfo();
assertTrue(tst.pollSocketsInfo());
assertEquals(0, tst.getSentSinceLastRecv());
assertEquals(-1, tst.getLatestPacketFailPercentage());
assertFalse(tst.isDataStallSuspected());
}
}