| /* |
| * 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.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 android.system.OsConstants.NETLINK_NETFILTER; |
| |
| 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.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED; |
| import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_UDP_TIMEOUT_STREAM; |
| import static com.android.networkstack.tethering.BpfCoordinator.POLLING_CONNTRACK_TIMEOUT_MS; |
| 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.BpfUtils.DOWNSTREAM; |
| import static com.android.networkstack.tethering.BpfUtils.UPSTREAM; |
| 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.assertTrue; |
| 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.Matchers.eq; |
| 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.spy; |
| 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.LinkAddress; |
| import android.net.LinkProperties; |
| import android.net.MacAddress; |
| import android.net.Network; |
| import android.net.NetworkCapabilities; |
| import android.net.NetworkStats; |
| import android.net.TetherOffloadRuleParcel; |
| import android.net.TetherStatsParcel; |
| import android.net.ip.ConntrackMonitor; |
| import android.net.ip.ConntrackMonitor.ConntrackEventConsumer; |
| import android.net.ip.IpServer; |
| import android.net.netlink.ConntrackMessage; |
| import android.net.netlink.NetlinkConstants; |
| import android.net.netlink.NetlinkSocket; |
| import android.net.util.InterfaceParams; |
| import android.net.util.SharedLog; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.test.TestLooper; |
| import android.system.ErrnoException; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.test.filters.SmallTest; |
| 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; |
| import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; |
| import com.android.testutils.TestableNetworkStatsProviderCbBinder; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| 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 org.mockito.MockitoSession; |
| |
| import java.net.Inet4Address; |
| import java.net.Inet6Address; |
| import java.net.InetAddress; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.function.BiConsumer; |
| |
| @RunWith(AndroidJUnit4.class) |
| @SmallTest |
| public class BpfCoordinatorTest { |
| @Rule |
| public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); |
| |
| private static final int TEST_NET_ID = 24; |
| |
| 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 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. |
| // TODO: consider moving to an individual file. |
| private class TestBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> { |
| private final HashMap<K, V> mMap = new HashMap<K, V>(); |
| |
| TestBpfMap(final Class<K> key, final Class<V> value) { |
| super(key, value); |
| } |
| |
| @Override |
| public void forEach(BiConsumer<K, V> action) throws ErrnoException { |
| // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to |
| // implement the entry deletion in the iteration if required. |
| for (Map.Entry<K, V> entry : mMap.entrySet()) { |
| action.accept(entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| @Override |
| public void updateEntry(K key, V value) throws ErrnoException { |
| mMap.put(key, value); |
| } |
| |
| @Override |
| public void insertEntry(K key, V value) throws ErrnoException, |
| IllegalArgumentException { |
| // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry. |
| if (mMap.get(key) != null) { |
| throw new IllegalArgumentException(key + " already exist"); |
| } |
| mMap.put(key, value); |
| } |
| |
| @Override |
| public boolean deleteEntry(Struct key) throws ErrnoException { |
| return mMap.remove(key) != null; |
| } |
| |
| @Override |
| public V getValue(@NonNull K key) throws ErrnoException { |
| // Return value for a given key. Otherwise, return null without an error ENOENT. |
| // BpfMap#getValue treats that the entry is not found as no error. |
| return mMap.get(key); |
| } |
| |
| @Override |
| public void clear() throws ErrnoException { |
| // TODO: consider using mocked #getFirstKey and #deleteEntry to implement. |
| mMap.clear(); |
| } |
| }; |
| |
| @Mock private NetworkStatsManager mStatsManager; |
| @Mock private INetd mNetd; |
| @Mock private IpServer mIpServer; |
| @Mock private IpServer mIpServer2; |
| @Mock private TetheringConfiguration mTetherConfig; |
| @Mock private ConntrackMonitor mConntrackMonitor; |
| @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map; |
| @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map; |
| @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map; |
| @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map; |
| @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap; |
| |
| // 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 long mElapsedRealtimeNanos = 0; |
| private final ArgumentCaptor<ArrayList> mStringArrayCaptor = |
| ArgumentCaptor.forClass(ArrayList.class); |
| private final TestLooper mTestLooper = new TestLooper(); |
| private final TestBpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap = |
| spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class)); |
| private final TestBpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap = |
| spy(new TestBpfMap<>(TetherLimitKey.class, TetherLimitValue.class)); |
| private BpfCoordinator.Dependencies mDeps = |
| spy(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; |
| } |
| |
| @NonNull |
| public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) { |
| return mConntrackMonitor; |
| } |
| |
| public long elapsedRealtimeNanos() { |
| return mElapsedRealtimeNanos; |
| } |
| |
| @Nullable |
| public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() { |
| return mBpfDownstream4Map; |
| } |
| |
| @Nullable |
| public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() { |
| return mBpfUpstream4Map; |
| } |
| |
| @Nullable |
| public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() { |
| return mBpfDownstream6Map; |
| } |
| |
| @Nullable |
| public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() { |
| return mBpfUpstream6Map; |
| } |
| |
| @Nullable |
| public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() { |
| return mBpfStatsMap; |
| } |
| |
| @Nullable |
| public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() { |
| return mBpfLimitMap; |
| } |
| |
| @Nullable |
| public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() { |
| return mBpfDevMap; |
| } |
| }); |
| |
| @Before public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */); |
| } |
| |
| private void waitForIdle() { |
| 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]); |
| } |
| |
| @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); |
| 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; |
| } |
| |
| // Update a stats entry or create if not exists. |
| private void updateStatsEntryToStatsMap(@NonNull TetherStatsParcel stats) throws Exception { |
| final TetherStatsKey key = new TetherStatsKey(stats.ifIndex); |
| final TetherStatsValue value = new TetherStatsValue(stats.rxPackets, stats.rxBytes, |
| 0L /* rxErrors */, stats.txPackets, stats.txBytes, 0L /* txErrors */); |
| mBpfStatsMap.updateEntry(key, value); |
| } |
| |
| private void updateStatsEntry(@NonNull TetherStatsParcel stats) throws Exception { |
| if (mDeps.isAtLeastS()) { |
| updateStatsEntryToStatsMap(stats); |
| } else { |
| when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[] {stats}); |
| } |
| } |
| |
| // Update 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. |
| // Note that the mocked tetherOffloadGetStats of netd replaces all stats entries because it |
| // doesn't store the previous entries. |
| private void updateStatsEntriesAndWaitForUpdate(@NonNull TetherStatsParcel[] tetherStatsList) |
| throws Exception { |
| if (mDeps.isAtLeastS()) { |
| for (TetherStatsParcel stats : tetherStatsList) { |
| updateStatsEntry(stats); |
| } |
| } else { |
| when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList); |
| } |
| |
| mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); |
| waitForIdle(); |
| } |
| |
| // In tests, the stats need to be set before deleting the last rule. |
| // The reason is that BpfCoordinator#tetherOffloadRuleRemove reads the stats |
| // of the deleting interface after the last rule deleted. #tetherOffloadRuleRemove |
| // does the interface cleanup failed if there is no stats for the deleting interface. |
| // Note that the mocked tetherOffloadGetAndClearStats of netd replaces all stats entries |
| // because it doesn't store the previous entries. |
| private void updateStatsEntryForTetherOffloadGetAndClearStats(TetherStatsParcel stats) |
| throws Exception { |
| if (mDeps.isAtLeastS()) { |
| updateStatsEntryToStatsMap(stats); |
| } else { |
| when(mNetd.tetherOffloadGetAndClearStats(stats.ifIndex)).thenReturn(stats); |
| } |
| } |
| |
| private void clearStatsInvocations() { |
| if (mDeps.isAtLeastS()) { |
| clearInvocations(mBpfStatsMap); |
| } else { |
| clearInvocations(mNetd); |
| } |
| } |
| |
| private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) { |
| if (inOrder != null) { |
| return inOrder.verify(t); |
| } else { |
| return verify(t); |
| } |
| } |
| |
| private void verifyTetherOffloadGetStats() throws Exception { |
| if (mDeps.isAtLeastS()) { |
| verify(mBpfStatsMap).forEach(any()); |
| } else { |
| verify(mNetd).tetherOffloadGetStats(); |
| } |
| } |
| |
| private void verifyNeverTetherOffloadGetStats() throws Exception { |
| if (mDeps.isAtLeastS()) { |
| verify(mBpfStatsMap, never()).forEach(any()); |
| } else { |
| verify(mNetd, never()).tetherOffloadGetStats(); |
| } |
| } |
| |
| private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex, |
| MacAddress downstreamMac, int upstreamIfindex) throws Exception { |
| if (!mDeps.isAtLeastS()) return; |
| final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac); |
| final Tether6Value value = new Tether6Value(upstreamIfindex, |
| MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS, |
| ETH_P_IPV6, NetworkStackConstants.ETHER_MTU); |
| verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value); |
| } |
| |
| private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex, |
| MacAddress downstreamMac) |
| throws Exception { |
| if (!mDeps.isAtLeastS()) return; |
| final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac); |
| verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key); |
| } |
| |
| private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception { |
| if (!mDeps.isAtLeastS()) return; |
| if (inOrder != null) { |
| inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any()); |
| inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any()); |
| inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any()); |
| } else { |
| verify(mBpfUpstream6Map, never()).deleteEntry(any()); |
| verify(mBpfUpstream6Map, never()).insertEntry(any(), any()); |
| verify(mBpfUpstream6Map, never()).updateEntry(any(), any()); |
| } |
| } |
| |
| private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, |
| @NonNull Ipv6ForwardingRule rule) throws Exception { |
| if (mDeps.isAtLeastS()) { |
| verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry( |
| rule.makeTetherDownstream6Key(), rule.makeTether6Value()); |
| } else { |
| verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(rule)); |
| } |
| } |
| |
| private void verifyNeverTetherOffloadRuleAdd() throws Exception { |
| if (mDeps.isAtLeastS()) { |
| verify(mBpfDownstream6Map, never()).updateEntry(any(), any()); |
| } else { |
| verify(mNetd, never()).tetherOffloadRuleAdd(any()); |
| } |
| } |
| |
| private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, |
| @NonNull final Ipv6ForwardingRule rule) throws Exception { |
| if (mDeps.isAtLeastS()) { |
| verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry( |
| rule.makeTetherDownstream6Key()); |
| } else { |
| verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(rule)); |
| } |
| } |
| |
| private void verifyNeverTetherOffloadRuleRemove() throws Exception { |
| if (mDeps.isAtLeastS()) { |
| verify(mBpfDownstream6Map, never()).deleteEntry(any()); |
| } else { |
| verify(mNetd, never()).tetherOffloadRuleRemove(any()); |
| } |
| } |
| |
| private void verifyTetherOffloadSetInterfaceQuota(@Nullable InOrder inOrder, int ifIndex, |
| long quotaBytes, boolean isInit) throws Exception { |
| if (mDeps.isAtLeastS()) { |
| final TetherStatsKey key = new TetherStatsKey(ifIndex); |
| verifyWithOrder(inOrder, mBpfStatsMap).getValue(key); |
| if (isInit) { |
| verifyWithOrder(inOrder, mBpfStatsMap).insertEntry(key, new TetherStatsValue( |
| 0L /* rxPackets */, 0L /* rxBytes */, 0L /* rxErrors */, |
| 0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */)); |
| } |
| verifyWithOrder(inOrder, mBpfLimitMap).updateEntry(new TetherLimitKey(ifIndex), |
| new TetherLimitValue(quotaBytes)); |
| } else { |
| verifyWithOrder(inOrder, mNetd).tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes); |
| } |
| } |
| |
| private void verifyNeverTetherOffloadSetInterfaceQuota(@NonNull InOrder inOrder) |
| throws Exception { |
| if (mDeps.isAtLeastS()) { |
| inOrder.verify(mBpfStatsMap, never()).getValue(any()); |
| inOrder.verify(mBpfStatsMap, never()).insertEntry(any(), any()); |
| inOrder.verify(mBpfLimitMap, never()).updateEntry(any(), any()); |
| } else { |
| inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong()); |
| } |
| } |
| |
| private void verifyTetherOffloadGetAndClearStats(@NonNull InOrder inOrder, int ifIndex) |
| throws Exception { |
| if (mDeps.isAtLeastS()) { |
| inOrder.verify(mBpfStatsMap).getValue(new TetherStatsKey(ifIndex)); |
| inOrder.verify(mBpfStatsMap).deleteEntry(new TetherStatsKey(ifIndex)); |
| inOrder.verify(mBpfLimitMap).deleteEntry(new TetherLimitKey(ifIndex)); |
| } else { |
| inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ifIndex); |
| } |
| } |
| |
| // S+ and R api minimum tests. |
| // The following tests are used to provide minimum checking for the APIs on different flow. |
| // The auto merge is not enabled on mainline prod. The code flow R may be verified at the |
| // late stage by manual cherry pick. It is risky if the R code flow has broken and be found at |
| // the last minute. |
| // TODO: remove once presubmit tests on R even the code is submitted on S. |
| private void checkTetherOffloadRuleAddAndRemove(boolean usingApiS) throws Exception { |
| setupFunctioningNetdInterface(); |
| |
| // Replace Dependencies#isAtLeastS() for testing R and S+ BPF map apis. Note that |mDeps| |
| // must be mocked before calling #makeBpfCoordinator which use |mDeps| to initialize the |
| // coordinator. |
| doReturn(usingApiS).when(mDeps).isAtLeastS(); |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| |
| final String mobileIface = "rmnet_data0"; |
| final Integer mobileIfIndex = 100; |
| coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); |
| |
| // InOrder is required because mBpfStatsMap may be accessed by both |
| // BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats. |
| // The #verifyTetherOffloadGetAndClearStats can't distinguish who has ever called |
| // mBpfStatsMap#getValue and get a wrong calling count which counts all. |
| final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap); |
| final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); |
| coordinator.tetherOffloadRuleAdd(mIpServer, rule); |
| verifyTetherOffloadRuleAdd(inOrder, rule); |
| verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED, |
| true /* isInit */); |
| |
| // Removing the last rule on current upstream immediately sends the cleanup stuff to netd. |
| updateStatsEntryForTetherOffloadGetAndClearStats( |
| buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); |
| coordinator.tetherOffloadRuleRemove(mIpServer, rule); |
| verifyTetherOffloadRuleRemove(inOrder, rule); |
| verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex); |
| } |
| |
| // TODO: remove once presubmit tests on R even the code is submitted on S. |
| @Test |
| public void testTetherOffloadRuleAddAndRemoveSdkR() throws Exception { |
| checkTetherOffloadRuleAddAndRemove(false /* R */); |
| } |
| |
| // TODO: remove once presubmit tests on R even the code is submitted on S. |
| @Test |
| public void testTetherOffloadRuleAddAndRemoveAtLeastSdkS() throws Exception { |
| checkTetherOffloadRuleAddAndRemove(true /* S+ */); |
| } |
| |
| // TODO: remove once presubmit tests on R even the code is submitted on S. |
| private void checkTetherOffloadGetStats(boolean usingApiS) throws Exception { |
| setupFunctioningNetdInterface(); |
| |
| doReturn(usingApiS).when(mDeps).isAtLeastS(); |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| coordinator.startPolling(); |
| |
| final String mobileIface = "rmnet_data0"; |
| final Integer mobileIfIndex = 100; |
| coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); |
| |
| updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] { |
| buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)}); |
| |
| final NetworkStats expectedIfaceStats = new NetworkStats(0L, 1) |
| .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 1000, 100, 2000, 200)); |
| |
| final NetworkStats expectedUidStats = new NetworkStats(0L, 1) |
| .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 1000, 100, 2000, 200)); |
| |
| mTetherStatsProvider.pushTetherStats(); |
| mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats); |
| } |
| |
| // TODO: remove once presubmit tests on R even the code is submitted on S. |
| @Test |
| public void testTetherOffloadGetStatsSdkR() throws Exception { |
| checkTetherOffloadGetStats(false /* R */); |
| } |
| |
| // TODO: remove once presubmit tests on R even the code is submitted on S. |
| @Test |
| public void testTetherOffloadGetStatsAtLeastSdkS() throws Exception { |
| checkTetherOffloadGetStats(true /* S+ */); |
| } |
| |
| @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. |
| updateStatsEntriesAndWaitForUpdate(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. |
| updateStatsEntriesAndWaitForUpdate(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(); |
| clearStatsInvocations(); |
| |
| // Verify the polling update thread stopped. |
| mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); |
| waitForIdle(); |
| verifyNeverTetherOffloadGetStats(); |
| } |
| |
| @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. |
| updateStatsEntry(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. |
| updateStatsEntry(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 testRuleMakeTetherDownstream6Key() throws Exception { |
| final Integer mobileIfIndex = 100; |
| final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); |
| |
| final TetherDownstream6Key key = rule.makeTetherDownstream6Key(); |
| assertEquals(key.iif, (long) mobileIfIndex); |
| assertEquals(key.dstMac, MacAddress.ALL_ZEROS_ADDRESS); // rawip upstream |
| assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress())); |
| // iif (4) + dstMac(6) + padding(2) + neigh6 (16) = 28. |
| assertEquals(28, key.writeToBytes().length); |
| } |
| |
| @Test |
| public void testRuleMakeTether6Value() throws Exception { |
| final Integer mobileIfIndex = 100; |
| final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); |
| |
| final Tether6Value value = rule.makeTether6Value(); |
| assertEquals(value.oif, DOWNSTREAM_IFINDEX); |
| assertEquals(value.ethDstMac, MAC_A); |
| assertEquals(value.ethSrcMac, DOWNSTREAM_MAC); |
| assertEquals(value.ethProto, ETH_P_IPV6); |
| assertEquals(value.pmtu, NetworkStackConstants.ETHER_MTU); |
| // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20. |
| assertEquals(20, value.writeToBytes().length); |
| } |
| |
| @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, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap); |
| coordinator.tetherOffloadRuleAdd(mIpServer, rule); |
| verifyTetherOffloadRuleAdd(inOrder, rule); |
| verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED, |
| true /* isInit */); |
| 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(); |
| verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, quota, |
| false /* isInit */); |
| 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 testSetDataLimitOnRule6Change() 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, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap); |
| mTetherStatsProvider.onSetLimit(mobileIface, limit); |
| waitForIdle(); |
| verifyNeverTetherOffloadSetInterfaceQuota(inOrder); |
| |
| // 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); |
| verifyTetherOffloadRuleAdd(inOrder, ruleA); |
| verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */); |
| 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); |
| verifyTetherOffloadRuleAdd(inOrder, ruleB); |
| verifyNeverTetherOffloadSetInterfaceQuota(inOrder); |
| |
| // Removing the second rule on current upstream does not send the quota to netd. |
| coordinator.tetherOffloadRuleRemove(mIpServer, ruleB); |
| verifyTetherOffloadRuleRemove(inOrder, ruleB); |
| verifyNeverTetherOffloadSetInterfaceQuota(inOrder); |
| |
| // Removing the last rule on current upstream immediately sends the cleanup stuff to netd. |
| updateStatsEntryForTetherOffloadGetAndClearStats( |
| buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); |
| coordinator.tetherOffloadRuleRemove(mIpServer, ruleA); |
| verifyTetherOffloadRuleRemove(inOrder, ruleA); |
| verifyTetherOffloadGetAndClearStats(inOrder, 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, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap, |
| mBpfStatsMap); |
| |
| // 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); |
| verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA); |
| verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED, |
| true /* isInit */); |
| verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, ethIfIndex); |
| coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB); |
| verifyTetherOffloadRuleAdd(inOrder, 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); |
| updateStatsEntryForTetherOffloadGetAndClearStats( |
| 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); |
| verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA); |
| verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB); |
| verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC); |
| verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex); |
| verifyTetherOffloadRuleAdd(inOrder, mobileRuleA); |
| verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED, |
| true /* isInit */); |
| verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, |
| mobileIfIndex); |
| verifyTetherOffloadRuleAdd(inOrder, mobileRuleB); |
| |
| // [3] Clear all rules for a given IpServer. |
| updateStatsEntryForTetherOffloadGetAndClearStats( |
| buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80)); |
| coordinator.tetherOffloadRuleClear(mIpServer); |
| verifyTetherOffloadRuleRemove(inOrder, mobileRuleA); |
| verifyTetherOffloadRuleRemove(inOrder, mobileRuleB); |
| verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC); |
| verifyTetherOffloadGetAndClearStats(inOrder, 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))); |
| } |
| |
| private void checkBpfDisabled() throws Exception { |
| // The caller may mock the global dependencies |mDeps| which is used in |
| // #makeBpfCoordinator for testing. |
| // See #testBpfDisabledbyNoBpfDownstream6Map. |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| coordinator.startPolling(); |
| |
| // The tether stats polling task should not be scheduled. |
| mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); |
| waitForIdle(); |
| verifyNeverTetherOffloadGetStats(); |
| |
| // 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); |
| verifyNeverTetherOffloadRuleAdd(); |
| 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); |
| verifyNeverTetherOffloadRuleRemove(); |
| rules = coordinator.getForwardingRulesForTesting().get(mIpServer); |
| assertNotNull(rules); |
| assertEquals(1, rules.size()); |
| |
| // The rule can't be cleared. |
| coordinator.tetherOffloadRuleClear(mIpServer); |
| verifyNeverTetherOffloadRuleRemove(); |
| rules = coordinator.getForwardingRulesForTesting().get(mIpServer); |
| assertNotNull(rules); |
| assertEquals(1, rules.size()); |
| |
| // The rule can't be updated. |
| coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */); |
| verifyNeverTetherOffloadRuleRemove(); |
| verifyNeverTetherOffloadRuleAdd(); |
| rules = coordinator.getForwardingRulesForTesting().get(mIpServer); |
| assertNotNull(rules); |
| assertEquals(1, rules.size()); |
| } |
| |
| @Test |
| public void testBpfDisabledbyConfig() throws Exception { |
| setupFunctioningNetdInterface(); |
| when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false); |
| |
| checkBpfDisabled(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testBpfDisabledbyNoBpfDownstream6Map() throws Exception { |
| setupFunctioningNetdInterface(); |
| doReturn(null).when(mDeps).getBpfDownstream6Map(); |
| |
| checkBpfDisabled(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testBpfDisabledbyNoBpfUpstream6Map() throws Exception { |
| setupFunctioningNetdInterface(); |
| doReturn(null).when(mDeps).getBpfUpstream6Map(); |
| |
| checkBpfDisabled(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testBpfDisabledbyNoBpfDownstream4Map() throws Exception { |
| setupFunctioningNetdInterface(); |
| doReturn(null).when(mDeps).getBpfDownstream4Map(); |
| |
| checkBpfDisabled(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testBpfDisabledbyNoBpfUpstream4Map() throws Exception { |
| setupFunctioningNetdInterface(); |
| doReturn(null).when(mDeps).getBpfUpstream4Map(); |
| |
| checkBpfDisabled(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testBpfDisabledbyNoBpfStatsMap() throws Exception { |
| setupFunctioningNetdInterface(); |
| doReturn(null).when(mDeps).getBpfStatsMap(); |
| |
| checkBpfDisabled(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testBpfDisabledbyNoBpfLimitMap() throws Exception { |
| setupFunctioningNetdInterface(); |
| doReturn(null).when(mDeps).getBpfLimitMap(); |
| |
| checkBpfDisabled(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testBpfMapClear() throws Exception { |
| setupFunctioningNetdInterface(); |
| |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| verify(mBpfDownstream4Map).clear(); |
| verify(mBpfUpstream4Map).clear(); |
| verify(mBpfDownstream6Map).clear(); |
| verify(mBpfUpstream6Map).clear(); |
| verify(mBpfStatsMap).clear(); |
| verify(mBpfLimitMap).clear(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testAttachDetachBpfProgram() throws Exception { |
| setupFunctioningNetdInterface(); |
| |
| // Static mocking for BpfUtils. |
| MockitoSession mockSession = ExtendedMockito.mockitoSession() |
| .mockStatic(BpfUtils.class) |
| .startMocking(); |
| try { |
| final String intIface1 = "wlan1"; |
| final String intIface2 = "rndis0"; |
| final String extIface = "rmnet_data0"; |
| final BpfUtils mockMarkerBpfUtils = staticMockMarker(BpfUtils.class); |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| |
| // [1] Add the forwarding pair <wlan1, rmnet_data0>. Expect that attach both wlan1 and |
| // rmnet_data0. |
| coordinator.maybeAttachProgram(intIface1, extIface); |
| ExtendedMockito.verify(() -> BpfUtils.attachProgram(extIface, DOWNSTREAM)); |
| ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface1, UPSTREAM)); |
| ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); |
| ExtendedMockito.clearInvocations(mockMarkerBpfUtils); |
| |
| // [2] Add the forwarding pair <wlan1, rmnet_data0> again. Expect no more action. |
| coordinator.maybeAttachProgram(intIface1, extIface); |
| ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); |
| ExtendedMockito.clearInvocations(mockMarkerBpfUtils); |
| |
| // [3] Add the forwarding pair <rndis0, rmnet_data0>. Expect that attach rndis0 only. |
| coordinator.maybeAttachProgram(intIface2, extIface); |
| ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface2, UPSTREAM)); |
| ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); |
| ExtendedMockito.clearInvocations(mockMarkerBpfUtils); |
| |
| // [4] Remove the forwarding pair <rndis0, rmnet_data0>. Expect detach rndis0 only. |
| coordinator.maybeDetachProgram(intIface2, extIface); |
| ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface2)); |
| ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); |
| ExtendedMockito.clearInvocations(mockMarkerBpfUtils); |
| |
| // [5] Remove the forwarding pair <wlan1, rmnet_data0>. Expect that detach both wlan1 |
| // and rmnet_data0. |
| coordinator.maybeDetachProgram(intIface1, extIface); |
| ExtendedMockito.verify(() -> BpfUtils.detachProgram(extIface)); |
| ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface1)); |
| ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); |
| ExtendedMockito.clearInvocations(mockMarkerBpfUtils); |
| } finally { |
| mockSession.finishMocking(); |
| } |
| } |
| |
| @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(); |
| clearStatsInvocations(); |
| |
| // Move time forward to 90% polling interval time. Expect that the polling thread has not |
| // scheduled yet. |
| mTestLooper.moveTimeForward((long) (pollingInterval * 0.9)); |
| waitForIdle(); |
| verifyNeverTetherOffloadGetStats(); |
| |
| // Move time forward to the remaining 10% polling interval time. Expect that the polling |
| // thread has scheduled. |
| mTestLooper.moveTimeForward((long) (pollingInterval * 0.1)); |
| waitForIdle(); |
| verifyTetherOffloadGetStats(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testStartStopConntrackMonitoring() throws Exception { |
| setupFunctioningNetdInterface(); |
| |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| |
| // [1] Don't stop monitoring if it has never started. |
| coordinator.stopMonitoring(mIpServer); |
| verify(mConntrackMonitor, never()).start(); |
| |
| // [2] Start monitoring. |
| coordinator.startMonitoring(mIpServer); |
| verify(mConntrackMonitor).start(); |
| clearInvocations(mConntrackMonitor); |
| |
| // [3] Stop monitoring. |
| coordinator.stopMonitoring(mIpServer); |
| verify(mConntrackMonitor).stop(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.Q) |
| @IgnoreAfter(Build.VERSION_CODES.R) |
| // Only run this test on Android R. |
| public void testStartStopConntrackMonitoring_R() throws Exception { |
| setupFunctioningNetdInterface(); |
| |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| |
| coordinator.startMonitoring(mIpServer); |
| verify(mConntrackMonitor, never()).start(); |
| |
| coordinator.stopMonitoring(mIpServer); |
| verify(mConntrackMonitor, never()).stop(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception { |
| setupFunctioningNetdInterface(); |
| |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| |
| // [1] Start monitoring at the first IpServer adding. |
| coordinator.startMonitoring(mIpServer); |
| verify(mConntrackMonitor).start(); |
| clearInvocations(mConntrackMonitor); |
| |
| // [2] Don't start monitoring at the second IpServer adding. |
| coordinator.startMonitoring(mIpServer2); |
| verify(mConntrackMonitor, never()).start(); |
| |
| // [3] Don't stop monitoring if any downstream interface exists. |
| coordinator.stopMonitoring(mIpServer2); |
| verify(mConntrackMonitor, never()).stop(); |
| |
| // [4] Stop monitoring if no downstream exists. |
| 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 Tether4Key makeDownstream4Key() { |
| return makeDownstream4Key(IPPROTO_TCP); |
| } |
| |
| @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 */)); |
| final NetworkCapabilities capabilities = new NetworkCapabilities() |
| .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); |
| coordinator.updateUpstreamNetworkState(new UpstreamNetworkState(lp, capabilities, |
| new Network(TEST_NET_ID))); |
| } |
| |
| 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); |
| } |
| |
| private void initBpfCoordinatorForRule4(final BpfCoordinator coordinator) throws Exception { |
| // Needed because addUpstreamIfindexToMap only updates upstream information when polling |
| // was started. |
| coordinator.startPolling(); |
| |
| // Needed because two reasons: (1) BpfConntrackEventConsumer#accept only performs cleanup |
| // when both upstream and downstream rules are removed. (2) tetherOffloadRuleRemove of |
| // api31.BpfCoordinatorShimImpl only decreases the count while the entry is deleted. |
| // In the other words, deleteEntry returns true. |
| doReturn(true).when(mBpfUpstream4Map).deleteEntry(any()); |
| 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); |
| } |
| |
| // 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(); |
| initBpfCoordinatorForRule4(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(); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testAddDevMapRule6() throws Exception { |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| |
| coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE); |
| final Ipv6ForwardingRule ruleA = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A); |
| final Ipv6ForwardingRule ruleB = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B); |
| |
| coordinator.tetherOffloadRuleAdd(mIpServer, ruleA); |
| verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)), |
| eq(new TetherDevValue(UPSTREAM_IFINDEX))); |
| verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)), |
| eq(new TetherDevValue(DOWNSTREAM_IFINDEX))); |
| clearInvocations(mBpfDevMap); |
| |
| coordinator.tetherOffloadRuleAdd(mIpServer, ruleB); |
| verify(mBpfDevMap, never()).updateEntry(any(), any()); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testAddDevMapRule4() throws Exception { |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| initBpfCoordinatorForRule4(coordinator); |
| |
| mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP)); |
| verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)), |
| eq(new TetherDevValue(UPSTREAM_IFINDEX))); |
| verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)), |
| eq(new TetherDevValue(DOWNSTREAM_IFINDEX))); |
| clearInvocations(mBpfDevMap); |
| |
| mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP)); |
| verify(mBpfDevMap, never()).updateEntry(any(), any()); |
| } |
| |
| private void setElapsedRealtimeNanos(long nanoSec) { |
| mElapsedRealtimeNanos = nanoSec; |
| } |
| |
| private void checkRefreshConntrackTimeout(final TestBpfMap<Tether4Key, Tether4Value> bpfMap, |
| final Tether4Key tcpKey, final Tether4Value tcpValue, final Tether4Key udpKey, |
| final Tether4Value udpValue) throws Exception { |
| // Both system elapsed time since boot and the rule last used time are used to measure |
| // the rule expiration. In this test, all test rules are fixed the last used time to 0. |
| // Set the different testing elapsed time to make the rule to be valid or expired. |
| // |
| // Timeline: |
| // 0 60 (seconds) |
| // +---+---+---+---+--...--+---+---+---+---+---+- .. |
| // | POLLING_CONNTRACK_TIMEOUT_MS | |
| // +---+---+---+---+--...--+---+---+---+---+---+- .. |
| // |<- valid diff ->| |
| // |<- expired diff ->| |
| // ^ ^ ^ |
| // last used time elapsed time (valid) elapsed time (expired) |
| final long validTime = (POLLING_CONNTRACK_TIMEOUT_MS - 1) * 1_000_000L; |
| final long expiredTime = (POLLING_CONNTRACK_TIMEOUT_MS + 1) * 1_000_000L; |
| |
| // Static mocking for NetlinkSocket. |
| MockitoSession mockSession = ExtendedMockito.mockitoSession() |
| .mockStatic(NetlinkSocket.class) |
| .startMocking(); |
| try { |
| final BpfCoordinator coordinator = makeBpfCoordinator(); |
| coordinator.startPolling(); |
| bpfMap.insertEntry(tcpKey, tcpValue); |
| bpfMap.insertEntry(udpKey, udpValue); |
| |
| // [1] Don't refresh contrack timeout. |
| setElapsedRealtimeNanos(expiredTime); |
| mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS); |
| waitForIdle(); |
| ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class)); |
| ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class)); |
| |
| // [2] Refresh contrack timeout. |
| setElapsedRealtimeNanos(validTime); |
| mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS); |
| waitForIdle(); |
| final byte[] expectedNetlinkTcp = ConntrackMessage.newIPv4TimeoutUpdateRequest( |
| IPPROTO_TCP, PRIVATE_ADDR, (int) PRIVATE_PORT, REMOTE_ADDR, |
| (int) REMOTE_PORT, NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED); |
| final byte[] expectedNetlinkUdp = ConntrackMessage.newIPv4TimeoutUpdateRequest( |
| IPPROTO_UDP, PRIVATE_ADDR, (int) PRIVATE_PORT, REMOTE_ADDR, |
| (int) REMOTE_PORT, NF_CONNTRACK_UDP_TIMEOUT_STREAM); |
| ExtendedMockito.verify(() -> NetlinkSocket.sendOneShotKernelMessage( |
| eq(NETLINK_NETFILTER), eq(expectedNetlinkTcp))); |
| ExtendedMockito.verify(() -> NetlinkSocket.sendOneShotKernelMessage( |
| eq(NETLINK_NETFILTER), eq(expectedNetlinkUdp))); |
| ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class)); |
| ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class)); |
| |
| // [3] Don't refresh contrack timeout if polling stopped. |
| coordinator.stopPolling(); |
| mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS); |
| waitForIdle(); |
| ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class)); |
| ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class)); |
| } finally { |
| mockSession.finishMocking(); |
| } |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testRefreshConntrackTimeout_Upstream4Map() throws Exception { |
| // TODO: Replace the dependencies BPF map with a non-mocked TestBpfMap object. |
| final TestBpfMap<Tether4Key, Tether4Value> bpfUpstream4Map = |
| new TestBpfMap<>(Tether4Key.class, Tether4Value.class); |
| doReturn(bpfUpstream4Map).when(mDeps).getBpfUpstream4Map(); |
| |
| final Tether4Key tcpKey = makeUpstream4Key(IPPROTO_TCP); |
| final Tether4Key udpKey = makeUpstream4Key(IPPROTO_UDP); |
| final Tether4Value tcpValue = makeUpstream4Value(); |
| final Tether4Value udpValue = makeUpstream4Value(); |
| |
| checkRefreshConntrackTimeout(bpfUpstream4Map, tcpKey, tcpValue, udpKey, udpValue); |
| } |
| |
| @Test |
| @IgnoreUpTo(Build.VERSION_CODES.R) |
| public void testRefreshConntrackTimeout_Downstream4Map() throws Exception { |
| // TODO: Replace the dependencies BPF map with a non-mocked TestBpfMap object. |
| final TestBpfMap<Tether4Key, Tether4Value> bpfDownstream4Map = |
| new TestBpfMap<>(Tether4Key.class, Tether4Value.class); |
| doReturn(bpfDownstream4Map).when(mDeps).getBpfDownstream4Map(); |
| |
| final Tether4Key tcpKey = makeDownstream4Key(IPPROTO_TCP); |
| final Tether4Key udpKey = makeDownstream4Key(IPPROTO_UDP); |
| final Tether4Value tcpValue = makeDownstream4Value(); |
| final Tether4Value udpValue = makeDownstream4Value(); |
| |
| checkRefreshConntrackTimeout(bpfDownstream4Map, tcpKey, tcpValue, udpKey, udpValue); |
| } |
| } |