Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 92317855 authored by Nucca Chen's avatar Nucca Chen Committed by Automerger Merge Worker
Browse files

[BOT.10] Add unit test for data limit and rule change in BpfCoordinator am:...

[BOT.10] Add unit test for data limit and rule change in BpfCoordinator am: 796ee2f3 am: 1cc5f033

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11882403

Change-Id: Ie8fc9fbe6e4fc9211939b01381b1b9eb3f433292
parents b5fec0aa 1cc5f033
Loading
Loading
Loading
Loading
+231 −0
Original line number Diff line number Diff line
@@ -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_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 static org.mockito.Mockito.when;
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 android.os.test.TestLooper;
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 @@ public class BpfCoordinatorTest {
        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)));
    }
}