Merge "[BOT.10] Add unit test for data limit and rule change in BpfCoordinator" am: 8392e11329 am: 0d295a8642 am: b079e02a39
Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1311659 Change-Id: I4605077e115f200f1808403307131ff8c2a82d01
This commit is contained in:
@@ -31,10 +31,16 @@ import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
|
|||||||
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
|
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
|
||||||
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
|
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.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.Matchers.anyString;
|
||||||
|
import static org.mockito.Mockito.argThat;
|
||||||
import static org.mockito.Mockito.clearInvocations;
|
import static org.mockito.Mockito.clearInvocations;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -42,8 +48,12 @@ import static org.mockito.Mockito.when;
|
|||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
import android.app.usage.NetworkStatsManager;
|
import android.app.usage.NetworkStatsManager;
|
||||||
import android.net.INetd;
|
import android.net.INetd;
|
||||||
|
import android.net.InetAddresses;
|
||||||
|
import android.net.MacAddress;
|
||||||
import android.net.NetworkStats;
|
import android.net.NetworkStats;
|
||||||
|
import android.net.TetherOffloadRuleParcel;
|
||||||
import android.net.TetherStatsParcel;
|
import android.net.TetherStatsParcel;
|
||||||
|
import android.net.ip.IpServer;
|
||||||
import android.net.util.SharedLog;
|
import android.net.util.SharedLog;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.test.TestLooper;
|
import android.os.test.TestLooper;
|
||||||
@@ -51,22 +61,37 @@ import android.os.test.TestLooper;
|
|||||||
import androidx.test.filters.SmallTest;
|
import androidx.test.filters.SmallTest;
|
||||||
import androidx.test.runner.AndroidJUnit4;
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
|
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
|
||||||
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
|
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.ArgumentMatcher;
|
||||||
|
import org.mockito.InOrder;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
|
import java.net.Inet6Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
@SmallTest
|
@SmallTest
|
||||||
public class BpfCoordinatorTest {
|
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 NetworkStatsManager mStatsManager;
|
||||||
@Mock private INetd mNetd;
|
@Mock private INetd mNetd;
|
||||||
|
@Mock private IpServer mIpServer;
|
||||||
|
|
||||||
// Late init since methods must be called by the thread that created this object.
|
// Late init since methods must be called by the thread that created this object.
|
||||||
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
|
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
|
||||||
private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
|
private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
|
||||||
@@ -243,4 +268,210 @@ public class BpfCoordinatorTest {
|
|||||||
waitForIdle();
|
waitForIdle();
|
||||||
mTetherStatsProviderCb.assertNoCallback();
|
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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user