Merge "[NFCT.TETHER.11] Test TetherOffloadRule{Add, Remove} and set limit for IPv4" am: 68f6f5fe79 am: f0b3bb6e4e

Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/1563332

Change-Id: I56c1250280473dd1e10693c9cae65e6d2e5e808d
This commit is contained in:
Nucca Chen
2021-04-08 13:30:30 +00:00
committed by Automerger Merge Worker
2 changed files with 259 additions and 10 deletions

View File

@@ -137,6 +137,8 @@ public class BpfCoordinator {
private final BpfTetherStatsProvider mStatsProvider;
@NonNull
private final BpfCoordinatorShim mBpfCoordinatorShim;
@NonNull
private final BpfConntrackEventConsumer mBpfConntrackEventConsumer;
// True if BPF offload is supported, false otherwise. The BPF offload could be disabled by
// a runtime resource overlay package or device configuration. This flag is only initialized
@@ -248,6 +250,11 @@ public class BpfCoordinator {
return new ConntrackMonitor(getHandler(), getSharedLog(), consumer);
}
/** Get interface information for a given interface. */
@NonNull public InterfaceParams getInterfaceParams(String ifName) {
return InterfaceParams.getByName(ifName);
}
/**
* Check OS Build at least S.
*
@@ -339,7 +346,14 @@ public class BpfCoordinator {
mNetd = mDeps.getNetd();
mLog = mDeps.getSharedLog().forSubComponent(TAG);
mIsBpfEnabled = isBpfEnabled();
mConntrackMonitor = mDeps.getConntrackMonitor(new BpfConntrackEventConsumer());
// The conntrack consummer needs to be initialized in BpfCoordinator constructor because it
// have to access the data members of BpfCoordinator which is not a static class. The
// consumer object is also needed for initializing the conntrack monitor which may be
// mocked for testing.
mBpfConntrackEventConsumer = new BpfConntrackEventConsumer();
mConntrackMonitor = mDeps.getConntrackMonitor(mBpfConntrackEventConsumer);
BpfTetherStatsProvider provider = new BpfTetherStatsProvider();
try {
mDeps.getNetworkStatsManager().registerNetworkStatsProvider(
@@ -662,7 +676,7 @@ public class BpfCoordinator {
if (lp == null || !lp.hasIpv4Address()) return;
// Support raw ip upstream interface only.
final InterfaceParams params = InterfaceParams.getByName(lp.getInterfaceName());
final InterfaceParams params = mDeps.getInterfaceParams(lp.getInterfaceName());
if (params == null || params.hasMacAddress) return;
Collection<InetAddress> addresses = lp.getAddresses();
@@ -1148,7 +1162,8 @@ public class BpfCoordinator {
// TODO: add ether ip support.
// TODO: parse CTA_PROTOINFO of conntrack event in ConntrackMonitor. For TCP, only add rules
// while TCP status is established.
private class BpfConntrackEventConsumer implements ConntrackEventConsumer {
@VisibleForTesting
class BpfConntrackEventConsumer implements ConntrackEventConsumer {
@NonNull
private Tether4Key makeTetherUpstream4Key(
@NonNull ConntrackEvent e, @NonNull ClientInfo c) {
@@ -1497,5 +1512,13 @@ public class BpfCoordinator {
return mInterfaceNames;
}
// Return BPF conntrack event consumer. This is used for testing only.
// Note that this can be only called on handler thread.
@NonNull
@VisibleForTesting
final BpfConntrackEventConsumer getBpfConntrackEventConsumerForTesting() {
return mBpfConntrackEventConsumer;
}
private static native String[] getBpfCounterNames();
}

View File

@@ -23,9 +23,21 @@ import static android.net.NetworkStats.SET_DEFAULT;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
import static android.net.ip.ConntrackMonitor.ConntrackEvent;
import static android.net.netlink.ConntrackMessage.DYING_MASK;
import static android.net.netlink.ConntrackMessage.ESTABLISHED_MASK;
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.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
import static android.system.OsConstants.ETH_P_IP;
import static android.system.OsConstants.ETH_P_IPV6;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
@@ -43,9 +55,9 @@ import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -55,6 +67,8 @@ import static org.mockito.Mockito.when;
import android.app.usage.NetworkStatsManager;
import android.net.INetd;
import android.net.InetAddresses;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.NetworkStats;
import android.net.TetherOffloadRuleParcel;
@@ -62,6 +76,8 @@ import android.net.TetherStatsParcel;
import android.net.ip.ConntrackMonitor;
import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
import android.net.ip.IpServer;
import android.net.netlink.NetlinkConstants;
import android.net.util.InterfaceParams;
import android.net.util.SharedLog;
import android.os.Build;
import android.os.Handler;
@@ -76,6 +92,8 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.net.module.util.NetworkStackConstants;
import com.android.net.module.util.Struct;
import com.android.networkstack.tethering.BpfCoordinator.BpfConntrackEventConsumer;
import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -93,6 +111,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.ArrayList;
@@ -108,13 +127,22 @@ public class BpfCoordinatorTest {
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private static final int DOWNSTREAM_IFINDEX = 10;
private static final int UPSTREAM_IFINDEX = 1001;
private static final int DOWNSTREAM_IFINDEX = 1002;
private static final String UPSTREAM_IFACE = "rmnet0";
private static final MacAddress DOWNSTREAM_MAC = MacAddress.fromString("12:34:56:78:90:ab");
private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams(
UPSTREAM_IFACE, UPSTREAM_IFINDEX, null /* macAddr, rawip */,
NetworkStackConstants.ETHER_MTU);
// The test fake BPF map class is needed because the test has no privilege to access the BPF
// map. All member functions which eventually call JNI to access the real native BPF map need
// to be overridden.
@@ -183,6 +211,11 @@ public class BpfCoordinatorTest {
// Late init since methods must be called by the thread that created this object.
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
// Late init since the object must be initialized by the BPF coordinator instance because
// it has to access the non-static function of BPF coordinator.
private BpfConntrackEventConsumer mConsumer;
private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
private final TestLooper mTestLooper = new TestLooper();
@@ -262,6 +295,8 @@ public class BpfCoordinatorTest {
mTestLooper.dispatchAll();
}
// TODO: Remove unnecessary calling on R because the BPF map accessing has been moved into
// module.
private void setupFunctioningNetdInterface() throws Exception {
when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
}
@@ -269,6 +304,8 @@ public class BpfCoordinatorTest {
@NonNull
private BpfCoordinator makeBpfCoordinator() throws Exception {
final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
mConsumer = coordinator.getBpfConntrackEventConsumerForTesting();
final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider>
tetherStatsProviderCaptor =
ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class);
@@ -278,6 +315,7 @@ public class BpfCoordinatorTest {
assertNotNull(mTetherStatsProvider);
mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
return coordinator;
}
@@ -466,7 +504,7 @@ public class BpfCoordinatorTest {
}
}
private void verifyNeverTetherOffloadSetInterfaceQuota(@Nullable InOrder inOrder)
private void verifyNeverTetherOffloadSetInterfaceQuota(@NonNull InOrder inOrder)
throws Exception {
if (mDeps.isAtLeastS()) {
inOrder.verify(mBpfStatsMap, never()).getValue(any());
@@ -477,7 +515,7 @@ public class BpfCoordinatorTest {
}
}
private void verifyTetherOffloadGetAndClearStats(@Nullable InOrder inOrder, int ifIndex)
private void verifyTetherOffloadGetAndClearStats(@NonNull InOrder inOrder, int ifIndex)
throws Exception {
if (mDeps.isAtLeastS()) {
inOrder.verify(mBpfStatsMap).getValue(new TetherStatsKey(ifIndex));
@@ -799,7 +837,7 @@ public class BpfCoordinatorTest {
// TODO: Test the case in which the rules are changed from different IpServer objects.
@Test
public void testSetDataLimitOnRuleChange() throws Exception {
public void testSetDataLimitOnRule6Change() throws Exception {
setupFunctioningNetdInterface();
final BpfCoordinator coordinator = makeBpfCoordinator();
@@ -1219,4 +1257,192 @@ public class BpfCoordinatorTest {
coordinator.stopMonitoring(mIpServer);
verify(mConntrackMonitor).stop();
}
// Test network topology:
//
// public network (rawip) private network
// | UE |
// +------------+ V +------------+------------+ V +------------+
// | Sever +---------+ Upstream | Downstream +---------+ Client |
// +------------+ +------------+------------+ +------------+
// remote ip public ip private ip
// 140.112.8.116:443 100.81.179.1:62449 192.168.80.12:62449
//
private static final Inet4Address REMOTE_ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
private static final Inet4Address PUBLIC_ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
private static final Inet4Address PRIVATE_ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
// IPv4-mapped IPv6 addresses
// Remote addrress ::ffff:140.112.8.116
// Public addrress ::ffff:100.81.179.1
// Private addrress ::ffff:192.168.80.12
private static final byte[] REMOTE_ADDR_V4MAPPED_BYTES = new byte[] {
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
(byte) 0x8c, (byte) 0x70, (byte) 0x08, (byte) 0x74 };
private static final byte[] PUBLIC_ADDR_V4MAPPED_BYTES = new byte[] {
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
(byte) 0x64, (byte) 0x51, (byte) 0xb3, (byte) 0x01 };
private static final byte[] PRIVATE_ADDR_V4MAPPED_BYTES = new byte[] {
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
(byte) 0xc0, (byte) 0xa8, (byte) 0x50, (byte) 0x0c };
// Generally, public port and private port are the same in the NAT conntrack message.
// TODO: consider using different private port and public port for testing.
private static final short REMOTE_PORT = (short) 443;
private static final short PUBLIC_PORT = (short) 62449;
private static final short PRIVATE_PORT = (short) 62449;
@NonNull
private Tether4Key makeUpstream4Key(int proto) {
if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
fail("Not support protocol " + proto);
}
return new Tether4Key(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, (short) proto,
PRIVATE_ADDR.getAddress(), REMOTE_ADDR.getAddress(), PRIVATE_PORT, REMOTE_PORT);
}
@NonNull
private Tether4Key makeDownstream4Key(int proto) {
if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
fail("Not support protocol " + proto);
}
return new Tether4Key(UPSTREAM_IFINDEX,
MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */, (short) proto,
REMOTE_ADDR.getAddress(), PUBLIC_ADDR.getAddress(), REMOTE_PORT, PUBLIC_PORT);
}
@NonNull
private Tether4Value makeUpstream4Value() {
return new Tether4Value(UPSTREAM_IFINDEX,
MacAddress.ALL_ZEROS_ADDRESS /* ethDstMac (rawip) */,
MacAddress.ALL_ZEROS_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP,
NetworkStackConstants.ETHER_MTU, PUBLIC_ADDR_V4MAPPED_BYTES,
REMOTE_ADDR_V4MAPPED_BYTES, PUBLIC_PORT, REMOTE_PORT, 0 /* lastUsed */);
}
@NonNull
private Tether4Value makeDownstream4Value() {
return new Tether4Value(DOWNSTREAM_IFINDEX, MAC_A /* client mac */, DOWNSTREAM_MAC,
ETH_P_IP, NetworkStackConstants.ETHER_MTU, REMOTE_ADDR_V4MAPPED_BYTES,
PRIVATE_ADDR_V4MAPPED_BYTES, REMOTE_PORT, PRIVATE_PORT, 0 /* lastUsed */);
}
@NonNull
private ConntrackEvent makeTestConntrackEvent(short msgType, int proto) {
if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) {
fail("Not support message type " + msgType);
}
if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
fail("Not support protocol " + proto);
}
final int status = (msgType == IPCTNL_MSG_CT_NEW) ? ESTABLISHED_MASK : DYING_MASK;
final int timeoutSec = (msgType == IPCTNL_MSG_CT_NEW) ? 100 /* nonzero, new */
: 0 /* unused, delete */;
return new ConntrackEvent(
(short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
new Tuple(new TupleIpv4(PRIVATE_ADDR, REMOTE_ADDR),
new TupleProto((byte) proto, PRIVATE_PORT, REMOTE_PORT)),
new Tuple(new TupleIpv4(REMOTE_ADDR, PUBLIC_ADDR),
new TupleProto((byte) proto, REMOTE_PORT, PUBLIC_PORT)),
status,
timeoutSec);
}
private void setUpstreamInformationTo(final BpfCoordinator coordinator) {
final LinkProperties lp = new LinkProperties();
lp.setInterfaceName(UPSTREAM_IFACE);
lp.addLinkAddress(new LinkAddress(PUBLIC_ADDR, 32 /* prefix length */));
coordinator.addUpstreamIfindexToMap(lp);
}
private void setDownstreamAndClientInformationTo(final BpfCoordinator coordinator) {
final ClientInfo clientInfo = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
PRIVATE_ADDR, MAC_A /* client mac */);
coordinator.tetherOffloadClientAdd(mIpServer, clientInfo);
}
// TODO: Test the IPv4 and IPv6 exist concurrently.
// TODO: Test the IPv4 rule delete failed.
@Test
@IgnoreUpTo(Build.VERSION_CODES.R)
public void testSetDataLimitOnRule4Change() throws Exception {
final BpfCoordinator coordinator = makeBpfCoordinator();
coordinator.startPolling();
// Needed because tetherOffloadRuleRemove of api31.BpfCoordinatorShimImpl only decreases
// the count while the entry is deleted. In the other words, deleteEntry returns true.
doReturn(true).when(mBpfDownstream4Map).deleteEntry(any());
// Needed because BpfCoordinator#addUpstreamIfindexToMap queries interface parameter for
// interface index.
doReturn(UPSTREAM_IFACE_PARAMS).when(mDeps).getInterfaceParams(UPSTREAM_IFACE);
coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
setUpstreamInformationTo(coordinator);
setDownstreamAndClientInformationTo(coordinator);
// Applying a data limit to the current upstream does not take any immediate action.
// The data limit could be only set on an upstream which has rules.
final long limit = 12345;
final InOrder inOrder = inOrder(mNetd, mBpfUpstream4Map, mBpfDownstream4Map, mBpfLimitMap,
mBpfStatsMap);
mTetherStatsProvider.onSetLimit(UPSTREAM_IFACE, limit);
waitForIdle();
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Build TCP and UDP rules for testing. Note that the values of {TCP, UDP} are the same
// because the protocol is not an element of the value. Consider using different address
// or port to make them different for better testing.
// TODO: Make the values of {TCP, UDP} rules different.
final Tether4Key expectedUpstream4KeyTcp = makeUpstream4Key(IPPROTO_TCP);
final Tether4Key expectedDownstream4KeyTcp = makeDownstream4Key(IPPROTO_TCP);
final Tether4Value expectedUpstream4ValueTcp = makeUpstream4Value();
final Tether4Value expectedDownstream4ValueTcp = makeDownstream4Value();
final Tether4Key expectedUpstream4KeyUdp = makeUpstream4Key(IPPROTO_UDP);
final Tether4Key expectedDownstream4KeyUdp = makeDownstream4Key(IPPROTO_UDP);
final Tether4Value expectedUpstream4ValueUdp = makeUpstream4Value();
final Tether4Value expectedDownstream4ValueUdp = makeDownstream4Value();
// [1] Adding the first rule on current upstream immediately sends the quota.
mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP));
verifyTetherOffloadSetInterfaceQuota(inOrder, UPSTREAM_IFINDEX, limit, true /* isInit */);
inOrder.verify(mBpfUpstream4Map)
.insertEntry(eq(expectedUpstream4KeyTcp), eq(expectedUpstream4ValueTcp));
inOrder.verify(mBpfDownstream4Map)
.insertEntry(eq(expectedDownstream4KeyTcp), eq(expectedDownstream4ValueTcp));
inOrder.verifyNoMoreInteractions();
// [2] Adding the second rule on current upstream does not send the quota.
mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
inOrder.verify(mBpfUpstream4Map)
.insertEntry(eq(expectedUpstream4KeyUdp), eq(expectedUpstream4ValueUdp));
inOrder.verify(mBpfDownstream4Map)
.insertEntry(eq(expectedDownstream4KeyUdp), eq(expectedDownstream4ValueUdp));
inOrder.verifyNoMoreInteractions();
// [3] Removing the second rule on current upstream does not send the quota.
mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_UDP));
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyUdp));
inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyUdp));
inOrder.verifyNoMoreInteractions();
// [4] Removing the last rule on current upstream immediately sends the cleanup stuff.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(UPSTREAM_IFINDEX, 0, 0, 0, 0));
mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_TCP));
inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyTcp));
inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyTcp));
verifyTetherOffloadGetAndClearStats(inOrder, UPSTREAM_IFINDEX);
inOrder.verifyNoMoreInteractions();
}
}