blob: 9f9be91b7e86431f6604975d18b96767347005fa [file] [log] [blame]
/*
* Copyright (C) 2020 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 android.net.ip;
import static android.net.ip.ConntrackMonitor.ConntrackEvent;
import static android.net.netlink.ConntrackMessage.Tuple;
import static android.net.netlink.ConntrackMessage.TupleIpv4;
import static android.net.netlink.ConntrackMessage.TupleProto;
import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
import static android.system.OsConstants.AF_UNIX;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.SOCK_DGRAM;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import android.net.InetAddresses;
import android.net.netlink.NetlinkConstants;
import android.net.netlink.NetlinkSocket;
import android.net.util.SharedLog;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.system.ErrnoException;
import android.system.Os;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import libcore.util.HexEncoding;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.FileDescriptor;
import java.io.InterruptedIOException;
import java.net.Inet4Address;
/**
* Tests for ConntrackMonitor.
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ConntrackMonitorTest {
private static final long TIMEOUT_MS = 10_000L;
@Mock private SharedLog mLog;
@Mock private ConntrackMonitor.ConntrackEventConsumer mConsumer;
private final HandlerThread mHandlerThread = new HandlerThread(
ConntrackMonitorTest.class.getSimpleName());
// Late init since the handler thread has been started.
private Handler mHandler;
private TestConntrackMonitor mConntrackMonitor;
// A version of [ConntrackMonitor] that reads packets from the socket pair, and instead
// allows the test to write test packets to the socket pair via [sendMessage].
private class TestConntrackMonitor extends ConntrackMonitor {
TestConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
@NonNull ConntrackEventConsumer cb) {
super(h, log, cb);
mReadFd = new FileDescriptor();
mWriteFd = new FileDescriptor();
try {
Os.socketpair(AF_UNIX, SOCK_DGRAM, 0, mWriteFd, mReadFd);
} catch (ErrnoException e) {
fail("Could not create socket pair: " + e);
}
}
@Override
protected FileDescriptor createFd() {
return mReadFd;
}
private void sendMessage(byte[] msg) {
mHandler.post(() -> {
try {
NetlinkSocket.sendMessage(mWriteFd, msg, 0 /* offset */, msg.length,
TIMEOUT_MS);
} catch (ErrnoException | InterruptedIOException e) {
fail("Unable to send netfilter message: " + e);
}
});
}
private final FileDescriptor mReadFd;
private final FileDescriptor mWriteFd;
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
// ConntrackMonitor needs to be started from the handler thread.
final ConditionVariable initDone = new ConditionVariable();
mHandler.post(() -> {
TestConntrackMonitor m = new TestConntrackMonitor(mHandler, mLog, mConsumer);
m.start();
mConntrackMonitor = m;
initDone.open();
});
if (!initDone.block(TIMEOUT_MS)) {
fail("... init monitor timed-out after " + TIMEOUT_MS + "ms");
}
}
@After
public void tearDown() throws Exception {
mHandlerThread.quitSafely();
}
public static final String CT_V4NEW_TCP_HEX =
// CHECKSTYLE:OFF IndentationCheck
// struct nlmsghdr
"8C000000" + // length = 140
"0001" + // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
"0006" + // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
"00000000" + // seqno = 0
"00000000" + // pid = 0
// struct nfgenmsg
"02" + // nfgen_family = AF_INET
"00" + // version = NFNETLINK_V0
"1234" + // res_id = 0x1234 (big endian)
// struct nlattr
"3400" + // nla_len = 52
"0180" + // nla_type = nested CTA_TUPLE_ORIG
// struct nlattr
"1400" + // nla_len = 20
"0180" + // nla_type = nested CTA_TUPLE_IP
"0800 0100 C0A8500C" + // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
"0800 0200 8C700874" + // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
// struct nlattr
"1C00" + // nla_len = 28
"0280" + // nla_type = nested CTA_TUPLE_PROTO
"0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
"0600 0200 F3F1 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
"0600 0300 01BB 0000" + // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
// struct nlattr
"3400" + // nla_len = 52
"0280" + // nla_type = nested CTA_TUPLE_REPLY
// struct nlattr
"1400" + // nla_len = 20
"0180" + // nla_type = nested CTA_TUPLE_IP
"0800 0100 8C700874" + // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
"0800 0200 6451B301" + // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
// struct nlattr
"1C00" + // nla_len = 28
"0280" + // nla_type = nested CTA_TUPLE_PROTO
"0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
"0600 0200 01BB 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
"0600 0300 F3F1 0000" + // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
// struct nlattr
"0800" + // nla_len = 8
"0300" + // nla_type = CTA_STATUS
"0000019e" + // nla_value = 0b110011110 (big endian)
// IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
// IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
// IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
// struct nlattr
"0800" + // nla_len = 8
"0700" + // nla_type = CTA_TIMEOUT
"00000078"; // nla_value = 120 (big endian)
// CHECKSTYLE:ON IndentationCheck
public static final byte[] CT_V4NEW_TCP_BYTES =
HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
@NonNull
private ConntrackEvent makeTestConntrackEvent(short msgType, int status, int timeoutSec) {
final Inet4Address privateIp =
(Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
final Inet4Address remoteIp =
(Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
final Inet4Address publicIp =
(Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
return new ConntrackEvent(
(short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
new Tuple(new TupleIpv4(privateIp, remoteIp),
new TupleProto((byte) IPPROTO_TCP, (short) 62449, (short) 443)),
new Tuple(new TupleIpv4(remoteIp, publicIp),
new TupleProto((byte) IPPROTO_TCP, (short) 443, (short) 62449)),
status,
timeoutSec);
}
@Test
public void testConntrackEventNew() throws Exception {
final ConntrackEvent expectedEvent = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
0x19e /* status */, 120 /* timeoutSec */);
mConntrackMonitor.sendMessage(CT_V4NEW_TCP_BYTES);
verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
}
@Test
public void testConntrackEventEquals() {
final ConntrackEvent event1 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
5678 /* timeoutSec*/);
final ConntrackEvent event2 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
5678 /* timeoutSec*/);
assertEquals(event1, event2);
}
@Test
public void testConntrackEventNotEquals() {
final ConntrackEvent e = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
5678 /* timeoutSec*/);
final ConntrackEvent typeNotEqual = new ConntrackEvent((short) (e.msgType + 1) /* diff */,
e.tupleOrig, e.tupleReply, e.status, e.timeoutSec);
assertNotEquals(e, typeNotEqual);
final ConntrackEvent tupleOrigNotEqual = new ConntrackEvent(e.msgType,
null /* diff */, e.tupleReply, e.status, e.timeoutSec);
assertNotEquals(e, tupleOrigNotEqual);
final ConntrackEvent tupleReplyNotEqual = new ConntrackEvent(e.msgType,
e.tupleOrig, null /* diff */, e.status, e.timeoutSec);
assertNotEquals(e, tupleReplyNotEqual);
final ConntrackEvent statusNotEqual = new ConntrackEvent(e.msgType,
e.tupleOrig, e.tupleReply, e.status + 1 /* diff */, e.timeoutSec);
assertNotEquals(e, statusNotEqual);
final ConntrackEvent timeoutSecNotEqual = new ConntrackEvent(e.msgType,
e.tupleOrig, e.tupleReply, e.status, e.timeoutSec + 1 /* diff */);
assertNotEquals(e, timeoutSecNotEqual);
}
// TODO: consider moving to a common file for sharing.
private static void assertContains(String actualValue, String expectedSubstring) {
if (actualValue.contains(expectedSubstring)) return;
fail("\"" + actualValue + "\" does not contain \"" + expectedSubstring + "\"");
}
@Test
public void testToString() {
final ConntrackEvent event = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
0x198 /* status */, 120 /* timeoutSec */);
final String s = event.toString();
// Verify the converted string of msg_type, tuple_orig, tuple_reply, status and timeout.
assertContains(s, "IPCTNL_MSG_CT_NEW");
assertContains(s, "IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443");
assertContains(s, "IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449");
assertContains(s, "IPS_CONFIRMED|IPS_SRC_NAT|IPS_SRC_NAT_DONE|IPS_DST_NAT_DONE");
assertContains(s, "timeout_sec{120}");
}
public static final String CT_V4DELETE_TCP_HEX =
// CHECKSTYLE:OFF IndentationCheck
// struct nlmsghdr
"84000000" + // length = 132
"0201" + // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_DELETE (2)
"0000" + // flags = 0
"00000000" + // seqno = 0
"00000000" + // pid = 0
// struct nfgenmsg
"02" + // nfgen_family = AF_INET
"00" + // version = NFNETLINK_V0
"1234" + // res_id = 0x1234 (big endian)
// struct nlattr
"3400" + // nla_len = 52
"0180" + // nla_type = nested CTA_TUPLE_ORIG
// struct nlattr
"1400" + // nla_len = 20
"0180" + // nla_type = nested CTA_TUPLE_IP
"0800 0100 C0A8500C" + // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
"0800 0200 8C700874" + // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
// struct nlattr
"1C00" + // nla_len = 28
"0280" + // nla_type = nested CTA_TUPLE_PROTO
"0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
"0600 0200 F3F1 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
"0600 0300 01BB 0000" + // nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)
// struct nlattr
"3400" + // nla_len = 52
"0280" + // nla_type = nested CTA_TUPLE_REPLY
// struct nlattr
"1400" + // nla_len = 20
"0180" + // nla_type = nested CTA_TUPLE_IP
"0800 0100 8C700874" + // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
"0800 0200 6451B301" + // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
// struct nlattr
"1C00" + // nla_len = 28
"0280" + // nla_type = nested CTA_TUPLE_PROTO
"0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
"0600 0200 01BB 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=433 (big endian)
"0600 0300 F3F1 0000" + // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
// struct nlattr
"0800" + // nla_len = 8
"0300" + // nla_type = CTA_STATUS
"0000039E"; // nla_value = 0b1110011110 (big endian)
// IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
// IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
// IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8) |
// IPS_DYING (1 << 9)
// CHECKSTYLE:ON IndentationCheck
public static final byte[] CT_V4DELETE_TCP_BYTES =
HexEncoding.decode(CT_V4DELETE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
@Test
public void testConntrackEventDelete() throws Exception {
final ConntrackEvent expectedEvent =
makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, 0x39e /* status */,
0 /* timeoutSec (absent) */);
mConntrackMonitor.sendMessage(CT_V4DELETE_TCP_BYTES);
verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
}
}