blob: 64242ae8255fd62ea176700f12483e422a447e5e [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 com.android.networkstack.tethering;
import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
import static android.net.NetworkStats.METERED_NO;
import static android.net.NetworkStats.ROAMING_NO;
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.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
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_UID;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
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.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;
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;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
import java.util.LinkedHashMap;
@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;
@Mock private TetheringConfiguration mTetherConfig;
// Late init since methods must be called by the thread that created this object.
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
private final TestLooper mTestLooper = new TestLooper();
private BpfCoordinator.Dependencies mDeps =
new BpfCoordinator.Dependencies() {
@NonNull
public Handler getHandler() {
return new Handler(mTestLooper.getLooper());
}
@NonNull
public INetd getNetd() {
return mNetd;
}
@NonNull
public NetworkStatsManager getNetworkStatsManager() {
return mStatsManager;
}
@NonNull
public SharedLog getSharedLog() {
return new SharedLog("test");
}
@Nullable
public TetheringConfiguration getTetherConfig() {
return mTetherConfig;
}
};
@Before public void setUp() {
MockitoAnnotations.initMocks(this);
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
}
private void waitForIdle() {
mTestLooper.dispatchAll();
}
private void setupFunctioningNetdInterface() throws Exception {
when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
}
@NonNull
private BpfCoordinator makeBpfCoordinator() throws Exception {
final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider>
tetherStatsProviderCaptor =
ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class);
verify(mStatsManager).registerNetworkStatsProvider(anyString(),
tetherStatsProviderCaptor.capture());
mTetherStatsProvider = tetherStatsProviderCaptor.getValue();
assertNotNull(mTetherStatsProvider);
mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
return coordinator;
}
@NonNull
private static NetworkStats.Entry buildTestEntry(@NonNull StatsType how,
@NonNull String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) {
return new NetworkStats.Entry(iface, how == STATS_PER_IFACE ? UID_ALL : UID_TETHERING,
SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes,
rxPackets, txBytes, txPackets, 0L);
}
@NonNull
private static TetherStatsParcel buildTestTetherStatsParcel(@NonNull Integer ifIndex,
long rxBytes, long rxPackets, long txBytes, long txPackets) {
final TetherStatsParcel parcel = new TetherStatsParcel();
parcel.ifIndex = ifIndex;
parcel.rxBytes = rxBytes;
parcel.rxPackets = rxPackets;
parcel.txBytes = txBytes;
parcel.txPackets = txPackets;
return parcel;
}
// Set up specific tether stats list and wait for the stats cache is updated by polling thread
// in the coordinator. Beware of that it is only used for the default polling interval.
private void setTetherOffloadStatsList(TetherStatsParcel[] tetherStatsList) throws Exception {
when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList);
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
}
@Test
public void testGetForwardedStats() throws Exception {
setupFunctioningNetdInterface();
final BpfCoordinator coordinator = makeBpfCoordinator();
coordinator.startPolling();
final String wlanIface = "wlan0";
final Integer wlanIfIndex = 100;
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 101;
// Add interface name to lookup table. In realistic case, the upstream interface name will
// be added by IpServer when IpServer has received with a new IPv6 upstream update event.
coordinator.addUpstreamNameToLookupTable(wlanIfIndex, wlanIface);
coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
// [1] Both interface stats are changed.
// Setup the tether stats of wlan and mobile interface. Note that move forward the time of
// the looper to make sure the new tether stats has been updated by polling update thread.
setTetherOffloadStatsList(new TetherStatsParcel[] {
buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
buildTestTetherStatsParcel(mobileIfIndex, 3000, 300, 4000, 400)});
final NetworkStats expectedIfaceStats = new NetworkStats(0L, 2)
.addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 1000, 100, 2000, 200))
.addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 3000, 300, 4000, 400));
final NetworkStats expectedUidStats = new NetworkStats(0L, 2)
.addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 1000, 100, 2000, 200))
.addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 3000, 300, 4000, 400));
// Force pushing stats update to verify the stats reported.
// TODO: Perhaps make #expectNotifyStatsUpdated to use test TetherStatsParcel object for
// verifying the notification.
mTetherStatsProvider.pushTetherStats();
mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
// [2] Only one interface stats is changed.
// The tether stats of mobile interface is accumulated and The tether stats of wlan
// interface is the same.
setTetherOffloadStatsList(new TetherStatsParcel[] {
buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
buildTestTetherStatsParcel(mobileIfIndex, 3010, 320, 4030, 440)});
final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2)
.addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 0, 0, 0, 0))
.addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 10, 20, 30, 40));
final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2)
.addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 0, 0, 0, 0))
.addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 10, 20, 30, 40));
// Force pushing stats update to verify that only diff of stats is reported.
mTetherStatsProvider.pushTetherStats();
mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff,
expectedUidStatsDiff);
// [3] Stop coordinator.
// Shutdown the coordinator and clear the invocation history, especially the
// tetherOffloadGetStats() calls.
coordinator.stopPolling();
clearInvocations(mNetd);
// Verify the polling update thread stopped.
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
verify(mNetd, never()).tetherOffloadGetStats();
}
@Test
public void testOnSetAlert() throws Exception {
setupFunctioningNetdInterface();
final BpfCoordinator coordinator = makeBpfCoordinator();
coordinator.startPolling();
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 100;
coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
// Verify that set quota to 0 will immediately triggers a callback.
mTetherStatsProvider.onSetAlert(0);
waitForIdle();
mTetherStatsProviderCb.expectNotifyAlertReached();
// Verify that notifyAlertReached never fired if quota is not yet reached.
when(mNetd.tetherOffloadGetStats()).thenReturn(
new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)});
mTetherStatsProvider.onSetAlert(100);
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
mTetherStatsProviderCb.assertNoCallback();
// Verify that notifyAlertReached fired when quota is reached.
when(mNetd.tetherOffloadGetStats()).thenReturn(
new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0)});
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
mTetherStatsProviderCb.expectNotifyAlertReached();
// Verify that set quota with UNLIMITED won't trigger any callback.
mTetherStatsProvider.onSetAlert(QUOTA_UNLIMITED);
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
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, 1gb, max, infinity} on current upstream.
for (final long quota : new long[] {0, 1048576000, 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)));
}
@Test
public void testTetheringConfigDisable() throws Exception {
setupFunctioningNetdInterface();
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
final BpfCoordinator coordinator = makeBpfCoordinator();
coordinator.startPolling();
// The tether stats polling task should not be scheduled.
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
verify(mNetd, never()).tetherOffloadGetStats();
// The interface name lookup table can't be added.
final String iface = "rmnet_data0";
final Integer ifIndex = 100;
coordinator.addUpstreamNameToLookupTable(ifIndex, iface);
assertEquals(0, coordinator.getInterfaceNamesForTesting().size());
// The rule can't be added.
final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
final Ipv6ForwardingRule rule = buildTestForwardingRule(ifIndex, neigh, mac);
coordinator.tetherOffloadRuleAdd(mIpServer, rule);
verify(mNetd, never()).tetherOffloadRuleAdd(any());
LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules =
coordinator.getForwardingRulesForTesting().get(mIpServer);
assertNull(rules);
// The rule can't be removed. This is not a realistic case because adding rule is not
// allowed. That implies no rule could be removed, cleared or updated. Verify these
// cases just in case.
rules = new LinkedHashMap<Inet6Address, Ipv6ForwardingRule>();
rules.put(rule.address, rule);
coordinator.getForwardingRulesForTesting().put(mIpServer, rules);
coordinator.tetherOffloadRuleRemove(mIpServer, rule);
verify(mNetd, never()).tetherOffloadRuleRemove(any());
rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
assertNotNull(rules);
assertEquals(1, rules.size());
// The rule can't be cleared.
coordinator.tetherOffloadRuleClear(mIpServer);
verify(mNetd, never()).tetherOffloadRuleRemove(any());
rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
assertNotNull(rules);
assertEquals(1, rules.size());
// The rule can't be updated.
coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */);
verify(mNetd, never()).tetherOffloadRuleRemove(any());
verify(mNetd, never()).tetherOffloadRuleAdd(any());
rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
assertNotNull(rules);
assertEquals(1, rules.size());
}
@Test
public void testTetheringConfigSetPollingInterval() throws Exception {
setupFunctioningNetdInterface();
final BpfCoordinator coordinator = makeBpfCoordinator();
// [1] The default polling interval.
coordinator.startPolling();
assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval());
coordinator.stopPolling();
// [2] Expect the invalid polling interval isn't applied. The valid range of interval is
// DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long.
for (final int interval
: new int[] {0, 100, DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS - 1}) {
when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval);
coordinator.startPolling();
assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval());
coordinator.stopPolling();
}
// [3] Set a specific polling interval which is larger than default value.
// Use a large polling interval to avoid flaky test because the time forwarding
// approximation is used to verify the scheduled time of the polling thread.
final int pollingInterval = 100_000;
when(mTetherConfig.getOffloadPollInterval()).thenReturn(pollingInterval);
coordinator.startPolling();
// Expect the specific polling interval to be applied.
assertEquals(pollingInterval, coordinator.getPollingInterval());
// Start on a new polling time slot.
mTestLooper.moveTimeForward(pollingInterval);
waitForIdle();
clearInvocations(mNetd);
// Move time forward to 90% polling interval time. Expect that the polling thread has not
// scheduled yet.
mTestLooper.moveTimeForward((long) (pollingInterval * 0.9));
waitForIdle();
verify(mNetd, never()).tetherOffloadGetStats();
// Move time forward to the remaining 10% polling interval time. Expect that the polling
// thread has scheduled.
mTestLooper.moveTimeForward((long) (pollingInterval * 0.1));
waitForIdle();
verify(mNetd).tetherOffloadGetStats();
}
}