[NFCT.TETHER.5] Migrate tetherOffloadSetInterfaceQuota from netd to mainline

A preparation for updating BPF map in mainline module.

Test: atest TetheringCoverageTests
Change-Id: I67dfba750c7303e4aeaf65f5086db1290d176b4d
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index dc5fd6d..8ef6d6f 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -92,6 +92,17 @@
         return toTetherStatsValueSparseArray(tetherStatsList);
     }
 
+    @Override
+    public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
+        try {
+            mNetd.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
+        } catch (RemoteException | ServiceSpecificException e) {
+            mLog.e("Exception when updating quota " + quotaBytes + ": ", e);
+            return false;
+        }
+        return true;
+    }
+
     @NonNull
     private SparseArray<TetherStatsValue> toTetherStatsValueSparseArray(
             @NonNull final TetherStatsParcel[] parcels) {
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index f6630d6..f94b5a9 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.networkstack.tethering.apishim.api31;
 
+import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+
 import android.net.util.SharedLog;
 import android.system.ErrnoException;
 import android.system.OsConstants;
@@ -29,6 +31,8 @@
 import com.android.networkstack.tethering.BpfMap;
 import com.android.networkstack.tethering.TetherIngressKey;
 import com.android.networkstack.tethering.TetherIngressValue;
+import com.android.networkstack.tethering.TetherLimitKey;
+import com.android.networkstack.tethering.TetherLimitValue;
 import com.android.networkstack.tethering.TetherStatsKey;
 import com.android.networkstack.tethering.TetherStatsValue;
 
@@ -51,15 +55,20 @@
     @Nullable
     private final BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
 
+    // BPF map of per-interface quota for tethering offload.
+    @Nullable
+    private final BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
+
     public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
         mLog = deps.getSharedLog().forSubComponent(TAG);
         mBpfIngressMap = deps.getBpfIngressMap();
         mBpfStatsMap = deps.getBpfStatsMap();
+        mBpfLimitMap = deps.getBpfLimitMap();
     }
 
     @Override
     public boolean isInitialized() {
-        return mBpfIngressMap != null && mBpfStatsMap != null;
+        return mBpfIngressMap != null && mBpfStatsMap != null  && mBpfLimitMap != null;
     }
 
     @Override
@@ -113,11 +122,69 @@
     }
 
     @Override
+    public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
+        if (!isInitialized()) return false;
+
+        // The common case is an update, where the stats already exist,
+        // hence we read first, even though writing with BPF_NOEXIST
+        // first would make the code simpler.
+        long rxBytes, txBytes;
+        TetherStatsValue statsValue = null;
+
+        try {
+            statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex));
+        } catch (ErrnoException e) {
+            // The BpfMap#getValue doesn't throw an errno ENOENT exception. Catch other error
+            // while trying to get stats entry.
+            mLog.e("Could not get stats entry of interface index " + ifIndex + ": ", e);
+            return false;
+        }
+
+        if (statsValue != null) {
+            // Ok, there was a stats entry.
+            rxBytes = statsValue.rxBytes;
+            txBytes = statsValue.txBytes;
+        } else {
+            // No stats entry - create one with zeroes.
+            try {
+                // This function is the *only* thing that can create entries.
+                // BpfMap#insertEntry use BPF_NOEXIST to create the entry. The entry is created
+                // if and only if it doesn't exist.
+                mBpfStatsMap.insertEntry(new TetherStatsKey(ifIndex), new TetherStatsValue(
+                        0 /* rxPackets */, 0 /* rxBytes */, 0 /* rxErrors */, 0 /* txPackets */,
+                        0 /* txBytes */, 0 /* txErrors */));
+            } catch (ErrnoException | IllegalArgumentException e) {
+                mLog.e("Could not create stats entry: ", e);
+                return false;
+            }
+            rxBytes = 0;
+            txBytes = 0;
+        }
+
+        // rxBytes + txBytes won't overflow even at 5gbps for ~936 years.
+        long newLimit = rxBytes + txBytes + quotaBytes;
+
+        // if adding limit (e.g., if limit is QUOTA_UNLIMITED) caused overflow: clamp to 'infinity'
+        if (newLimit < rxBytes + txBytes) newLimit = QUOTA_UNLIMITED;
+
+        try {
+            mBpfLimitMap.updateEntry(new TetherLimitKey(ifIndex), new TetherLimitValue(newLimit));
+        } catch (ErrnoException e) {
+            mLog.e("Fail to set quota " + quotaBytes + " for interface index " + ifIndex + ": ", e);
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
     public String toString() {
         return "mBpfIngressMap{"
                 + (mBpfIngressMap != null ? "initialized" : "not initialized") + "}, "
                 + "mBpfStatsMap{"
-                + (mBpfStatsMap != null ? "initialized" : "not initialized") + "} "
+                + (mBpfStatsMap != null ? "initialized" : "not initialized") + "}, "
+                + "mBpfLimitMap{"
+                + (mBpfLimitMap != null ? "initialized" : "not initialized") + "} "
                 + "}";
     }
 }
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 2ce3252..de65ea5 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -79,5 +79,15 @@
      */
     @Nullable
     public abstract SparseArray<TetherStatsValue> tetherOffloadGetStats();
+
+   /**
+    * Set a per-interface quota for tethering offload.
+    *
+    * @param ifIndex Index of upstream interface
+    * @param quotaBytes The quota defined as the number of bytes, starting from zero and counting
+    *       from *now*. A value of QUOTA_UNLIMITED (-1) indicates there is no limit.
+    */
+    @Nullable
+    public abstract boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
 }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 4f918ec..d5ab550 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -80,6 +80,8 @@
             "/sys/fs/bpf/map_offload_tether_ingress_map";
     private static final String TETHER_STATS_MAP_PATH =
             "/sys/fs/bpf/map_offload_tether_stats_map";
+    private static final String TETHER_LIMIT_MAP_PATH =
+            "/sys/fs/bpf/map_offload_tether_limit_map";
 
     @VisibleForTesting
     enum StatsType {
@@ -212,6 +214,17 @@
                 return null;
             }
         }
+
+        /** Get limit BPF map. */
+        @Nullable public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+            try {
+                return new BpfMap<>(TETHER_LIMIT_MAP_PATH,
+                    BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create limit map: " + e);
+                return null;
+            }
+        }
     }
 
     @VisibleForTesting
@@ -673,20 +686,13 @@
         return quotaBytes;
     }
 
-    private boolean sendDataLimitToNetd(int ifIndex, long quotaBytes) {
+    private boolean sendDataLimitToBpfMap(int ifIndex, long quotaBytes) {
         if (ifIndex == 0) {
             Log.wtf(TAG, "Invalid interface index.");
             return false;
         }
 
-        try {
-            mNetd.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
-        } catch (RemoteException | ServiceSpecificException e) {
-            mLog.e("Exception when updating quota " + quotaBytes + ": ", e);
-            return false;
-        }
-
-        return true;
+        return mBpfCoordinatorShim.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
     }
 
     // Handle the data limit update from the service which is the stats provider registered for.
@@ -699,7 +705,7 @@
         if (ifIndex == 0) return;
 
         final long quotaBytes = getQuotaBytes(iface);
-        sendDataLimitToNetd(ifIndex, quotaBytes);
+        sendDataLimitToBpfMap(ifIndex, quotaBytes);
     }
 
     // Handle the data limit update while adding forwarding rules.
@@ -710,7 +716,7 @@
             return false;
         }
         final long quotaBytes = getQuotaBytes(iface);
-        return sendDataLimitToNetd(ifIndex, quotaBytes);
+        return sendDataLimitToBpfMap(ifIndex, quotaBytes);
     }
 
     private boolean isAnyRuleOnUpstream(int upstreamIfindex) {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java
new file mode 100644
index 0000000..bc9bb47
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering per-interface limit. */
+public class TetherLimitKey extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long ifindex;  // upstream interface index
+
+    public TetherLimitKey(final long ifindex) {
+        this.ifindex = ifindex;
+    }
+
+    // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof TetherLimitKey)) return false;
+
+        final TetherLimitKey that = (TetherLimitKey) obj;
+
+        return ifindex == that.ifindex;
+    }
+
+    @Override
+    public int hashCode() {
+        return Long.hashCode(ifindex);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("ifindex: %d", ifindex);
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java
new file mode 100644
index 0000000..ed7e7d4
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The value of BpfMap which is used for tethering per-interface limit. */
+public class TetherLimitValue extends Struct {
+    // Use the signed long variable to store the int64 limit on limit BPF map.
+    // S64 is enough for each interface limit even at 5Gbps for ~468 years.
+    // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468.
+    // Note that QUOTA_UNLIMITED (-1) indicates there is no limit.
+    @Field(order = 0, type = Type.S64)
+    public final long limit;
+
+    public TetherLimitValue(final long limit) {
+        this.limit = limit;
+    }
+
+    // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof TetherLimitValue)) return false;
+
+        final TetherLimitValue that = (TetherLimitValue) obj;
+
+        return limit == that.limit;
+    }
+
+    @Override
+    public int hashCode() {
+        return Long.hashCode(limit);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("limit: %d", limit);
+    }
+}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 91a518e..4763558 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -106,6 +106,8 @@
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetherIngressKey;
 import com.android.networkstack.tethering.TetherIngressValue;
+import com.android.networkstack.tethering.TetherLimitKey;
+import com.android.networkstack.tethering.TetherLimitValue;
 import com.android.networkstack.tethering.TetherStatsKey;
 import com.android.networkstack.tethering.TetherStatsValue;
 import com.android.networkstack.tethering.TetheringConfiguration;
@@ -172,6 +174,7 @@
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
     @Mock private BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
+    @Mock private BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
 
     @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
 
@@ -301,6 +304,11 @@
                     public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
                         return mBpfStatsMap;
                     }
+
+                    @Nullable
+                    public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+                        return mBpfLimitMap;
+                    }
                 };
         mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 5ca220c..183571f 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -126,6 +126,23 @@
         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 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);
+        }
     };
 
     @Mock private NetworkStatsManager mStatsManager;
@@ -142,6 +159,8 @@
     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
@@ -178,6 +197,11 @@
                     public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
                         return mBpfStatsMap;
                     }
+
+                    @Nullable
+                    public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+                        return mBpfLimitMap;
+                    }
             });
 
     @Before public void setUp() {
@@ -325,6 +349,34 @@
         }
     }
 
+    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(@Nullable 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());
+        }
+    }
+
     // 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
@@ -347,6 +399,8 @@
         final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
         coordinator.tetherOffloadRuleAdd(mIpServer, rule);
         verifyTetherOffloadRuleAdd(null, rule);
+        verifyTetherOffloadSetInterfaceQuota(null, mobileIfIndex, QUOTA_UNLIMITED,
+                true /* isInit */);
 
         // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
         when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex))
@@ -596,10 +650,11 @@
         // 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, mBpfIngressMap);
+        final InOrder inOrder = inOrder(mNetd, mBpfIngressMap, mBpfLimitMap, mBpfStatsMap);
         coordinator.tetherOffloadRuleAdd(mIpServer, rule);
         verifyTetherOffloadRuleAdd(inOrder, rule);
-        inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
+        verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
+                true /* isInit */);
         inOrder.verifyNoMoreInteractions();
 
         // [2] Specific limit.
@@ -607,7 +662,8 @@
         for (final long quota : new long[] {0, 1048576000, Long.MAX_VALUE, QUOTA_UNLIMITED}) {
             mTetherStatsProvider.onSetLimit(mobileIface, quota);
             waitForIdle();
-            inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, quota);
+            verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, quota,
+                    false /* isInit */);
             inOrder.verifyNoMoreInteractions();
         }
 
@@ -637,28 +693,28 @@
         // 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, mBpfIngressMap);
+        final InOrder inOrder = inOrder(mNetd, mBpfIngressMap, mBpfLimitMap, mBpfStatsMap);
         mTetherStatsProvider.onSetLimit(mobileIface, limit);
         waitForIdle();
-        inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+        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);
-        inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, limit);
+        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);
-        inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+        verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Removing the second rule on current upstream does not send the quota to netd.
         coordinator.tetherOffloadRuleRemove(mIpServer, ruleB);
         verifyTetherOffloadRuleRemove(inOrder, ruleB);
-        inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+        verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
         when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex))
@@ -682,7 +738,7 @@
         coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
         coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
 
-        final InOrder inOrder = inOrder(mNetd, mBpfIngressMap);
+        final InOrder inOrder = inOrder(mNetd, mBpfIngressMap, 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.
@@ -700,7 +756,8 @@
 
         coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA);
         verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA);
-        inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(ethIfIndex, QUOTA_UNLIMITED);
+        verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
+                true /* isInit */);
 
         coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB);
         verifyTetherOffloadRuleAdd(inOrder, ethernetRuleB);
@@ -718,7 +775,8 @@
         coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex);
         verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA);
         verifyTetherOffloadRuleAdd(inOrder, mobileRuleA);
-        inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
+        verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
+                true /* isInit */);
         verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB);
         inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ethIfIndex);
         verifyTetherOffloadRuleAdd(inOrder, mobileRuleB);
@@ -826,6 +884,15 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testBpfDisabledbyNoBpfLimitMap() throws Exception {
+        setupFunctioningNetdInterface();
+        doReturn(null).when(mDeps).getBpfLimitMap();
+
+        checkBpfDisabled();
+    }
+
+    @Test
     public void testTetheringConfigSetPollingInterval() throws Exception {
         setupFunctioningNetdInterface();