[BOT.10] Add unit test for data limit and rule change in BpfCoordinator
The applying data limit is based on the forwarding rule changes.
Add the tests for verifying their interactions with netd.
Bug: 150736748
Test: BpfCoordinatorTest
Change-Id: I5a98c4cd74e2de6005ee05defa761f6af3fd4e75
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index e2d7aab..ba0f41c 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -31,10 +31,16 @@
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
+import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.fail;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -42,8 +48,12 @@
import android.annotation.NonNull;
import android.app.usage.NetworkStatsManager;
import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.MacAddress;
import android.net.NetworkStats;
+import android.net.TetherOffloadRuleParcel;
import android.net.TetherStatsParcel;
+import android.net.ip.IpServer;
import android.net.util.SharedLog;
import android.os.Handler;
import android.os.test.TestLooper;
@@ -51,22 +61,37 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.net.Inet6Address;
+import java.net.InetAddress;
import java.util.ArrayList;
+import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BpfCoordinatorTest {
+ private static final int DOWNSTREAM_IFINDEX = 10;
+ private static final MacAddress DOWNSTREAM_MAC = MacAddress.ALL_ZEROS_ADDRESS;
+ 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");
+
@Mock private NetworkStatsManager mStatsManager;
@Mock private INetd mNetd;
+ @Mock private IpServer mIpServer;
+
// Late init since methods must be called by the thread that created this object.
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
@@ -243,4 +268,210 @@
waitForIdle();
mTetherStatsProviderCb.assertNoCallback();
}
+
+ // The custom ArgumentMatcher simply comes from IpServerTest.
+ // TODO: move both of them into a common utility class for reusing the code.
+ private static class TetherOffloadRuleParcelMatcher implements
+ ArgumentMatcher<TetherOffloadRuleParcel> {
+ public final int upstreamIfindex;
+ public final int downstreamIfindex;
+ public final Inet6Address address;
+ public final MacAddress srcMac;
+ public final MacAddress dstMac;
+
+ TetherOffloadRuleParcelMatcher(@NonNull Ipv6ForwardingRule rule) {
+ upstreamIfindex = rule.upstreamIfindex;
+ downstreamIfindex = rule.downstreamIfindex;
+ address = rule.address;
+ srcMac = rule.srcMac;
+ dstMac = rule.dstMac;
+ }
+
+ public boolean matches(@NonNull TetherOffloadRuleParcel parcel) {
+ return upstreamIfindex == parcel.inputInterfaceIndex
+ && (downstreamIfindex == parcel.outputInterfaceIndex)
+ && Arrays.equals(address.getAddress(), parcel.destination)
+ && (128 == parcel.prefixLength)
+ && Arrays.equals(srcMac.toByteArray(), parcel.srcL2Address)
+ && Arrays.equals(dstMac.toByteArray(), parcel.dstL2Address);
+ }
+
+ public String toString() {
+ return String.format("TetherOffloadRuleParcelMatcher(%d, %d, %s, %s, %s",
+ upstreamIfindex, downstreamIfindex, address.getHostAddress(), srcMac, dstMac);
+ }
+ }
+
+ @NonNull
+ private TetherOffloadRuleParcel matches(@NonNull Ipv6ForwardingRule rule) {
+ return argThat(new TetherOffloadRuleParcelMatcher(rule));
+ }
+
+ @NonNull
+ private static Ipv6ForwardingRule buildTestForwardingRule(
+ int upstreamIfindex, @NonNull InetAddress address, @NonNull MacAddress dstMac) {
+ return new Ipv6ForwardingRule(upstreamIfindex, DOWNSTREAM_IFINDEX, (Inet6Address) address,
+ DOWNSTREAM_MAC, dstMac);
+ }
+
+ @Test
+ public void testSetDataLimit() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ // [1] Default limit.
+ // Set the unlimited quota as default if the service has never applied a data limit for a
+ // given upstream. Note that the data limit only be applied on an upstream which has rules.
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+ final InOrder inOrder = inOrder(mNetd);
+ coordinator.tetherOffloadRuleAdd(mIpServer, rule);
+ inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(rule));
+ inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
+ inOrder.verifyNoMoreInteractions();
+
+ // [2] Specific limit.
+ // Applying the data limit boundary {min, max, infinity} on current upstream.
+ for (final long quota : new long[] {0, Long.MAX_VALUE, QUOTA_UNLIMITED}) {
+ mTetherStatsProvider.onSetLimit(mobileIface, quota);
+ waitForIdle();
+ inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, quota);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ // [3] Invalid limit.
+ // The valid range of quota is 0..max_int64 or -1 (unlimited).
+ final long invalidLimit = Long.MIN_VALUE;
+ try {
+ mTetherStatsProvider.onSetLimit(mobileIface, invalidLimit);
+ waitForIdle();
+ fail("No exception thrown for invalid limit " + invalidLimit + ".");
+ } catch (IllegalArgumentException expected) {
+ assertEquals(expected.getMessage(), "invalid quota value " + invalidLimit);
+ }
+ }
+
+ // TODO: Test the case in which the rules are changed from different IpServer objects.
+ @Test
+ public void testSetDataLimitOnRuleChange() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ // 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);
+ mTetherStatsProvider.onSetLimit(mobileIface, limit);
+ waitForIdle();
+ inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+
+ // Adding the first rule on current upstream immediately sends the quota to netd.
+ final Ipv6ForwardingRule ruleA = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+ coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
+ inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ruleA));
+ inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, limit);
+ inOrder.verifyNoMoreInteractions();
+
+ // Adding the second rule on current upstream does not send the quota to netd.
+ final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B);
+ coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
+ inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ruleB));
+ inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+
+ // Removing the second rule on current upstream does not send the quota to netd.
+ coordinator.tetherOffloadRuleRemove(mIpServer, ruleB);
+ inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ruleB));
+ inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+
+ // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
+ when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex))
+ .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
+ coordinator.tetherOffloadRuleRemove(mIpServer, ruleA);
+ inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ruleA));
+ inOrder.verify(mNetd).tetherOffloadGetAndClearStats(mobileIfIndex);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void testTetherOffloadRuleUpdateAndClear() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ final String ethIface = "eth1";
+ final String mobileIface = "rmnet_data0";
+ final Integer ethIfIndex = 100;
+ final Integer mobileIfIndex = 101;
+ coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ final InOrder inOrder = inOrder(mNetd);
+
+ // Before the rule test, here are the additional actions while the rules are changed.
+ // - After adding the first rule on a given upstream, the coordinator adds a data limit.
+ // If the service has never applied the data limit, set an unlimited quota as default.
+ // - After removing the last rule on a given upstream, the coordinator gets the last stats.
+ // Then, it clears the stats and the limit entry from BPF maps.
+ // See tetherOffloadRule{Add, Remove, Clear, Clean}.
+
+ // [1] Adding rules on the upstream Ethernet.
+ // Note that the default data limit is applied after the first rule is added.
+ final Ipv6ForwardingRule ethernetRuleA = buildTestForwardingRule(
+ ethIfIndex, NEIGH_A, MAC_A);
+ final Ipv6ForwardingRule ethernetRuleB = buildTestForwardingRule(
+ ethIfIndex, NEIGH_B, MAC_B);
+
+ coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA);
+ inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ethernetRuleA));
+ inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(ethIfIndex, QUOTA_UNLIMITED);
+
+ coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB);
+ inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ethernetRuleB));
+
+ // [2] Update the existing rules from Ethernet to cellular.
+ final Ipv6ForwardingRule mobileRuleA = buildTestForwardingRule(
+ mobileIfIndex, NEIGH_A, MAC_A);
+ final Ipv6ForwardingRule mobileRuleB = buildTestForwardingRule(
+ mobileIfIndex, NEIGH_B, MAC_B);
+ when(mNetd.tetherOffloadGetAndClearStats(ethIfIndex))
+ .thenReturn(buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
+
+ // Update the existing rules for upstream changes. The rules are removed and re-added one
+ // by one for updating upstream interface index by #tetherOffloadRuleUpdate.
+ coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex);
+ inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleA));
+ inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(mobileRuleA));
+ inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
+ inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleB));
+ inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ethIfIndex);
+ inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(mobileRuleB));
+
+ // [3] Clear all rules for a given IpServer.
+ when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex))
+ .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80));
+ coordinator.tetherOffloadRuleClear(mIpServer);
+ inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(mobileRuleA));
+ inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(mobileRuleB));
+ inOrder.verify(mNetd).tetherOffloadGetAndClearStats(mobileIfIndex);
+
+ // [4] Force pushing stats update to verify that the last diff of stats is reported on all
+ // upstreams.
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(
+ new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, ethIface, 10, 20, 30, 40))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 50, 60, 70, 80)),
+ new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, ethIface, 10, 20, 30, 40))
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 50, 60, 70, 80)));
+ }
}