Merge "[NFCT.TETHER.2] Migrate tetherOffloadRuleAdd from netd to mainline"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 761e098..7427481 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -19,6 +19,7 @@
     sdk_version: "module_current",
     min_sdk_version: "30",
     srcs: [
+        "apishim/**/*.java",
         "src/**/*.java",
         ":framework-tethering-shared-srcs",
         ":tethering-module-utils-srcs",
@@ -26,6 +27,7 @@
     ],
     static_libs: [
         "androidx.annotation_annotation",
+        "modules-utils-build",
         "netlink-client",
         // TODO: use networkstack-client instead of just including the AIDL interface
         "networkstack-aidl-interfaces-unstable-java",
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
new file mode 100644
index 0000000..5cf0384
--- /dev/null
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -0,0 +1,67 @@
+/*
+ * 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.apishim.api30;
+
+import android.net.INetd;
+import android.net.util.SharedLog;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import androidx.annotation.NonNull;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public class BpfCoordinatorShimImpl
+        extends com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim {
+    private static final String TAG = "api30.BpfCoordinatorShimImpl";
+
+    @NonNull
+    private final SharedLog mLog;
+    @NonNull
+    private final INetd mNetd;
+
+    public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
+        mLog = deps.getSharedLog().forSubComponent(TAG);
+        mNetd = deps.getNetd();
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return true;
+    };
+
+    @Override
+    public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+        try {
+            mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel());
+        } catch (RemoteException | ServiceSpecificException e) {
+            mLog.e("Could not add IPv6 forwarding rule: ", e);
+            return false;
+        }
+
+        return true;
+    };
+
+    @Override
+    public String toString() {
+        return "Netd used";
+    }
+}
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
new file mode 100644
index 0000000..03616ca
--- /dev/null
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -0,0 +1,79 @@
+/*
+ * 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.apishim.api31;
+
+import android.net.util.SharedLog;
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfMap;
+import com.android.networkstack.tethering.TetherIngressKey;
+import com.android.networkstack.tethering.TetherIngressValue;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public class BpfCoordinatorShimImpl
+        extends com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim {
+    private static final String TAG = "api31.BpfCoordinatorShimImpl";
+
+    @NonNull
+    private final SharedLog mLog;
+
+    // BPF map of ingress queueing discipline which pre-processes the packets by the IPv6
+    // forwarding rules.
+    @Nullable
+    private final BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
+
+    public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
+        mLog = deps.getSharedLog().forSubComponent(TAG);
+        mBpfIngressMap = deps.getBpfIngressMap();
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return mBpfIngressMap != null;
+    }
+
+    @Override
+    public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+        if (!isInitialized()) return false;
+
+        final TetherIngressKey key = rule.makeTetherIngressKey();
+        final TetherIngressValue value = rule.makeTetherIngressValue();
+
+        try {
+            mBpfIngressMap.updateEntry(key, value);
+        } catch (ErrnoException e) {
+            mLog.e("Could not update entry: ", e);
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "mBpfIngressMap{"
+                + (mBpfIngressMap != 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
new file mode 100644
index 0000000..bcb644c
--- /dev/null
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -0,0 +1,58 @@
+/*
+ * 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.apishim.common;
+
+import androidx.annotation.NonNull;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public abstract class BpfCoordinatorShim {
+    /**
+     * Get BpfCoordinatorShim object by OS build version.
+     */
+    @NonNull
+    public static BpfCoordinatorShim getBpfCoordinatorShim(@NonNull final Dependencies deps) {
+        if (deps.isAtLeastS()) {
+            return new com.android.networkstack.tethering.apishim.api31.BpfCoordinatorShimImpl(
+                    deps);
+        } else {
+            return new com.android.networkstack.tethering.apishim.api30.BpfCoordinatorShimImpl(
+                    deps);
+        }
+    }
+
+    /**
+     * Return true if this class has been initialized, otherwise return false.
+     */
+    public abstract boolean isInitialized();
+
+    /**
+     * Adds a tethering offload rule to BPF map, or updates it if it already exists.
+     *
+     * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be updated
+     * if the input interface and destination prefix match. Otherwise, a new rule will be created.
+     * Note that this can be only called on handler thread.
+     *
+     * @param rule The rule to add or update.
+     */
+    public abstract boolean tetherOffloadRuleAdd(@NonNull Ipv6ForwardingRule rule);
+}
+
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 20f30ea..d890e08 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -24,6 +24,7 @@
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStats.UID_TETHERING;
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
 
@@ -42,6 +43,7 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
@@ -51,6 +53,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
 
 import java.net.Inet6Address;
 import java.util.ArrayList;
@@ -71,6 +76,8 @@
 public class BpfCoordinator {
     private static final String TAG = BpfCoordinator.class.getSimpleName();
     private static final int DUMP_TIMEOUT_MS = 10_000;
+    private static final String TETHER_INGRESS_FS_PATH =
+            "/sys/fs/bpf/map_offload_tether_ingress_map";
 
     @VisibleForTesting
     enum StatsType {
@@ -88,6 +95,8 @@
     private final Dependencies mDeps;
     @Nullable
     private final BpfTetherStatsProvider mStatsProvider;
+    @NonNull
+    private final BpfCoordinatorShim mBpfCoordinatorShim;
 
     // True if BPF offload is supported, false otherwise. The BPF offload could be disabled by
     // a runtime resource overlay package or device configuration. This flag is only initialized
@@ -168,6 +177,28 @@
 
         /** Get tethering configuration. */
         @Nullable public abstract TetheringConfiguration getTetherConfig();
+
+        /**
+         * Check OS Build at least S.
+         *
+         * TODO: move to BpfCoordinatorShim once the test doesn't need the mocked OS build for
+         * testing different code flows concurrently.
+         */
+        public boolean isAtLeastS() {
+            // TODO: consider using ShimUtils.isAtLeastS.
+            return SdkLevel.isAtLeastS();
+        }
+
+        /** Get ingress BPF map. */
+        @Nullable public BpfMap<TetherIngressKey, TetherIngressValue> getBpfIngressMap() {
+            try {
+                return new BpfMap<>(TETHER_INGRESS_FS_PATH,
+                    BpfMap.BPF_F_RDWR, TetherIngressKey.class, TetherIngressValue.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create ingress map: " + e);
+                return null;
+            }
+        }
     }
 
     @VisibleForTesting
@@ -188,6 +219,11 @@
             provider = null;
         }
         mStatsProvider = provider;
+
+        mBpfCoordinatorShim = BpfCoordinatorShim.getBpfCoordinatorShim(deps);
+        if (!mBpfCoordinatorShim.isInitialized()) {
+            mLog.e("Bpf shim not initialized");
+        }
     }
 
     /**
@@ -199,8 +235,8 @@
     public void startPolling() {
         if (mPollingStarted) return;
 
-        if (!mIsBpfEnabled) {
-            mLog.i("Offload disabled");
+        if (!isUsingBpf()) {
+            mLog.i("BPF is not using");
             return;
         }
 
@@ -231,6 +267,10 @@
         mLog.i("Polling stopped");
     }
 
+    private boolean isUsingBpf() {
+        return mIsBpfEnabled && mBpfCoordinatorShim.isInitialized();
+    }
+
     /**
      * Add forwarding rule. After adding the first rule on a given upstream, must add the data
      * limit on the given upstream.
@@ -238,15 +278,10 @@
      */
     public void tetherOffloadRuleAdd(
             @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
-        if (!mIsBpfEnabled) return;
+        if (!isUsingBpf()) return;
 
-        try {
-            // TODO: Perhaps avoid to add a duplicate rule.
-            mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel());
-        } catch (RemoteException | ServiceSpecificException e) {
-            mLog.e("Could not add IPv6 forwarding rule: ", e);
-            return;
-        }
+        // TODO: Perhaps avoid to add a duplicate rule.
+        if (!mBpfCoordinatorShim.tetherOffloadRuleAdd(rule)) return;
 
         if (!mIpv6ForwardingRules.containsKey(ipServer)) {
             mIpv6ForwardingRules.put(ipServer, new LinkedHashMap<Inet6Address,
@@ -279,7 +314,7 @@
      */
     public void tetherOffloadRuleRemove(
             @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
-        if (!mIsBpfEnabled) return;
+        if (!isUsingBpf()) return;
 
         try {
             // TODO: Perhaps avoid to remove a non-existent rule.
@@ -324,7 +359,7 @@
      * Note that this can be only called on handler thread.
      */
     public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
-        if (!mIsBpfEnabled) return;
+        if (!isUsingBpf()) return;
 
         final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
                 ipServer);
@@ -341,7 +376,7 @@
      * Note that this can be only called on handler thread.
      */
     public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) {
-        if (!mIsBpfEnabled) return;
+        if (!isUsingBpf()) return;
 
         final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
                 ipServer);
@@ -365,7 +400,7 @@
      * Note that this can be only called on handler thread.
      */
     public void addUpstreamNameToLookupTable(int upstreamIfindex, @NonNull String upstreamIface) {
-        if (!mIsBpfEnabled) return;
+        if (!isUsingBpf()) return;
 
         if (upstreamIfindex == 0 || TextUtils.isEmpty(upstreamIface)) return;
 
@@ -396,6 +431,7 @@
                     ? "registered" : "not registered"));
             pw.println("Upstream quota: " + mInterfaceQuotas.toString());
             pw.println("Polling interval: " + getPollingInterval() + " ms");
+            pw.println("Bpf shim: " + mBpfCoordinatorShim.toString());
 
             pw.println("Forwarding stats:");
             pw.increaseIndent();
@@ -497,6 +533,23 @@
             return parcel;
         }
 
+        /**
+         * Return a TetherIngressKey object built from the rule.
+         */
+        @NonNull
+        public TetherIngressKey makeTetherIngressKey() {
+            return new TetherIngressKey(upstreamIfindex, address.getAddress());
+        }
+
+        /**
+         * Return a TetherIngressValue object built from the rule.
+         */
+        @NonNull
+        public TetherIngressValue makeTetherIngressValue() {
+            return new TetherIngressValue(downstreamIfindex, dstMac, srcMac, ETH_P_IPV6,
+                    NetworkStackConstants.ETHER_MTU);
+        }
+
         @Override
         public boolean equals(Object o) {
             if (!(o instanceof Ipv6ForwardingRule)) return false;
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 2eb7589..dae19b7 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -36,6 +36,7 @@
 import static android.net.netlink.StructNdMsg.NUD_FAILED;
 import static android.net.netlink.StructNdMsg.NUD_REACHABLE;
 import static android.net.netlink.StructNdMsg.NUD_STALE;
+import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 
@@ -98,9 +99,13 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.NetworkStackConstants;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfMap;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
+import com.android.networkstack.tethering.TetherIngressKey;
+import com.android.networkstack.tethering.TetherIngressValue;
 import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -163,6 +168,7 @@
     @Mock private PrivateAddressCoordinator mAddressCoordinator;
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
+    @Mock private BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
 
     @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
 
@@ -173,6 +179,7 @@
     private InterfaceConfigurationParcel mInterfaceConfiguration;
     private NeighborEventConsumer mNeighborEventConsumer;
     private BpfCoordinator mBpfCoordinator;
+    private BpfCoordinator.Dependencies mBpfDeps;
 
     private void initStateMachine(int interfaceType) throws Exception {
         initStateMachine(interfaceType, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
@@ -256,8 +263,7 @@
                 mTestAddress);
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
 
-        mBpfCoordinator = spy(new BpfCoordinator(
-                new BpfCoordinator.Dependencies() {
+        mBpfDeps = new BpfCoordinator.Dependencies() {
                     @NonNull
                     public Handler getHandler() {
                         return new Handler(mLooper.getLooper());
@@ -282,7 +288,13 @@
                     public TetheringConfiguration getTetherConfig() {
                         return mTetherConfig;
                     }
-                }));
+
+                    @Nullable
+                    public BpfMap<TetherIngressKey, TetherIngressValue> getBpfIngressMap() {
+                        return mBpfIngressMap;
+                    }
+                };
+        mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
 
         setUpDhcpServer();
     }
@@ -742,14 +754,63 @@
     }
 
     @NonNull
+    private static TetherIngressKey makeIngressKey(int upstreamIfindex,
+            @NonNull final InetAddress dst) {
+        return new TetherIngressKey(upstreamIfindex, dst.getAddress());
+    }
+
+    @NonNull
+    private static TetherIngressValue makeIngressValue(@NonNull final MacAddress dstMac) {
+        return new TetherIngressValue(TEST_IFACE_PARAMS.index, dstMac, TEST_IFACE_PARAMS.macAddr,
+                ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+    }
+
+    private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t);
+        } else {
+            return verify(t);
+        }
+    }
+
+    private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, int upstreamIfindex,
+            @NonNull final InetAddress dst, @NonNull final MacAddress dstMac) throws Exception {
+        if (mBpfDeps.isAtLeastS()) {
+            verifyWithOrder(inOrder, mBpfIngressMap).updateEntry(
+                    makeIngressKey(upstreamIfindex, dst), makeIngressValue(dstMac));
+        } else {
+            verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(upstreamIfindex, dst,
+                    dstMac));
+        }
+    }
+
+    private void verifyNeverTetherOffloadRuleAdd(int upstreamIfindex,
+            @NonNull final InetAddress dst, @NonNull final MacAddress dstMac) throws Exception {
+        if (mBpfDeps.isAtLeastS()) {
+            verify(mBpfIngressMap, never()).updateEntry(makeIngressKey(upstreamIfindex, dst),
+                    makeIngressValue(dstMac));
+        } else {
+            verify(mNetd, never()).tetherOffloadRuleAdd(matches(upstreamIfindex, dst, dstMac));
+        }
+    }
+
+    private void verifyNeverTetherOffloadRuleAdd() throws Exception {
+        if (mBpfDeps.isAtLeastS()) {
+            verify(mBpfIngressMap, never()).updateEntry(any(), any());
+        } else {
+            verify(mNetd, never()).tetherOffloadRuleAdd(any());
+        }
+    }
+
+    @NonNull
     private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) {
         TetherStatsParcel parcel = new TetherStatsParcel();
         parcel.ifIndex = ifIndex;
         return parcel;
     }
 
-    private void resetNetdAndBpfCoordinator() throws Exception {
-        reset(mNetd, mBpfCoordinator);
+    private void resetNetdBpfMapAndCoordinator() throws Exception {
+        reset(mNetd, mBpfIngressMap, mBpfCoordinator);
         when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
         when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX))
                 .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX));
@@ -774,34 +835,34 @@
         final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
         final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b");
 
-        resetNetdAndBpfCoordinator();
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+        resetNetdBpfMapAndCoordinator();
+        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfIngressMap);
 
         // TODO: Perhaps verify the interaction of tetherOffloadSetInterfaceQuota and
         // tetherOffloadGetAndClearStats in netd while the rules are changed.
 
         // Events on other interfaces are ignored.
         recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfIngressMap);
 
         // Events on this interface are received and sent to netd.
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
         verify(mBpfCoordinator).tetherOffloadRuleAdd(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
-        verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighA, macA));
-        resetNetdAndBpfCoordinator();
+        verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighA, macA);
+        resetNetdBpfMapAndCoordinator();
 
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
         verify(mBpfCoordinator).tetherOffloadRuleAdd(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
-        verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighB, macB));
-        resetNetdAndBpfCoordinator();
+        verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighB, macB);
+        resetNetdBpfMapAndCoordinator();
 
         // Link-local and multicast neighbors are ignored.
         recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfIngressMap);
         recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfIngressMap);
 
         // A neighbor that is no longer valid causes the rule to be removed.
         // NUD_FAILED events do not have a MAC address.
@@ -809,30 +870,30 @@
         verify(mBpfCoordinator).tetherOffloadRuleRemove(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macNull));
         verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macNull));
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
 
         // A neighbor that is deleted causes the rule to be removed.
         recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
         verify(mBpfCoordinator).tetherOffloadRuleRemove(
                 mIpServer,  makeForwardingRule(UPSTREAM_IFINDEX, neighB, macNull));
         verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macNull));
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
 
         // Upstream changes result in updating the rules.
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
 
-        InOrder inOrder = inOrder(mNetd);
+        InOrder inOrder = inOrder(mNetd, mBpfIngressMap);
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(UPSTREAM_IFACE2);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
         verify(mBpfCoordinator).tetherOffloadRuleUpdate(mIpServer, UPSTREAM_IFINDEX2);
         inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macA));
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX2, neighA, macA));
+        verifyTetherOffloadRuleAdd(inOrder, UPSTREAM_IFINDEX2, neighA, macA);
         inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB));
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX2, neighB, macB));
-        resetNetdAndBpfCoordinator();
+        verifyTetherOffloadRuleAdd(inOrder, UPSTREAM_IFINDEX2, neighB, macB);
+        resetNetdBpfMapAndCoordinator();
 
         // When the upstream is lost, rules are removed.
         dispatchTetherConnectionChanged(null, null, 0);
@@ -843,15 +904,15 @@
         verify(mBpfCoordinator, times(2)).tetherOffloadRuleClear(mIpServer);
         verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX2, neighA, macA));
         verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX2, neighB, macB));
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
 
         // If the upstream is IPv4-only, no rules are added.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
         // Clear function is called by #updateIpv6ForwardingRules for the IPv6 upstream is lost.
         verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfIngressMap);
 
         // Rules can be added again once upstream IPv6 connectivity is available.
         lp.setInterfaceName(UPSTREAM_IFACE);
@@ -859,13 +920,13 @@
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
         verify(mBpfCoordinator).tetherOffloadRuleAdd(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
-        verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighB, macB));
+        verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighB, macB);
         verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
-        verify(mNetd, never()).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighA, macA));
+        verifyNeverTetherOffloadRuleAdd(UPSTREAM_IFINDEX, neighA, macA);
 
         // If upstream IPv6 connectivity is lost, rules are removed.
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
         verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
         verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB));
@@ -877,11 +938,11 @@
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
         verify(mBpfCoordinator).tetherOffloadRuleAdd(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
-        verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighA, macA));
+        verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighA, macA);
         verify(mBpfCoordinator).tetherOffloadRuleAdd(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
-        verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighB, macB));
-        resetNetdAndBpfCoordinator();
+        verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighB, macB);
+        resetNetdBpfMapAndCoordinator();
 
         mIpServer.stop();
         mLooper.dispatchAll();
@@ -889,7 +950,7 @@
         verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macA));
         verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB));
         verify(mIpNeighborMonitor).stop();
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
     }
 
     @Test
@@ -910,35 +971,35 @@
         // A neighbor that is added or deleted causes the rule to be added or removed.
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
                 true /* usingBpfOffload */);
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
 
         recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
         verify(mBpfCoordinator).tetherOffloadRuleAdd(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macA));
-        verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neigh, macA));
-        resetNetdAndBpfCoordinator();
+        verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neigh, macA);
+        resetNetdBpfMapAndCoordinator();
 
         recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
         verify(mBpfCoordinator).tetherOffloadRuleRemove(
                 mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macNull));
         verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neigh, macNull));
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
 
         // [2] Disable BPF offload.
         // A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
                 false /* usingBpfOffload */);
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
 
         recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
         verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(any(), any());
-        verify(mNetd, never()).tetherOffloadRuleAdd(any());
-        resetNetdAndBpfCoordinator();
+        verifyNeverTetherOffloadRuleAdd();
+        resetNetdBpfMapAndCoordinator();
 
         recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
         verify(mBpfCoordinator, never()).tetherOffloadRuleRemove(any(), any());
         verify(mNetd, never()).tetherOffloadRuleRemove(any());
-        resetNetdAndBpfCoordinator();
+        resetNetdBpfMapAndCoordinator();
     }
 
     @Test
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 64242ae..b920fa8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -24,6 +24,7 @@
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStats.UID_TETHERING;
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
@@ -33,6 +34,7 @@
 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;
@@ -40,8 +42,10 @@
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
 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;
 
@@ -54,6 +58,7 @@
 import android.net.TetherStatsParcel;
 import android.net.ip.IpServer;
 import android.net.util.SharedLog;
+import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
 
@@ -62,7 +67,9 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.NetworkStackConstants;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.TestableNetworkStatsProviderCbBinder;
 
 import org.junit.Before;
@@ -94,6 +101,7 @@
     @Mock private INetd mNetd;
     @Mock private IpServer mIpServer;
     @Mock private TetheringConfiguration mTetherConfig;
+    @Mock private BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
 
     // Late init since methods must be called by the thread that created this object.
     private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
@@ -102,32 +110,37 @@
             ArgumentCaptor.forClass(ArrayList.class);
     private final TestLooper mTestLooper = new TestLooper();
     private BpfCoordinator.Dependencies mDeps =
-            new BpfCoordinator.Dependencies() {
-            @NonNull
-            public Handler getHandler() {
-                return new Handler(mTestLooper.getLooper());
-            }
+            spy(new BpfCoordinator.Dependencies() {
+                    @NonNull
+                    public Handler getHandler() {
+                        return new Handler(mTestLooper.getLooper());
+                    }
 
-            @NonNull
-            public INetd getNetd() {
-                return mNetd;
-            }
+                    @NonNull
+                    public INetd getNetd() {
+                        return mNetd;
+                    }
 
-            @NonNull
-            public NetworkStatsManager getNetworkStatsManager() {
-                return mStatsManager;
-            }
+                    @NonNull
+                    public NetworkStatsManager getNetworkStatsManager() {
+                        return mStatsManager;
+                    }
 
-            @NonNull
-            public SharedLog getSharedLog() {
-                return new SharedLog("test");
-            }
+                    @NonNull
+                    public SharedLog getSharedLog() {
+                        return new SharedLog("test");
+                    }
 
-            @Nullable
-            public TetheringConfiguration getTetherConfig() {
-                return mTetherConfig;
-            }
-    };
+                    @Nullable
+                    public TetheringConfiguration getTetherConfig() {
+                        return mTetherConfig;
+                    }
+
+                    @Nullable
+                    public BpfMap<TetherIngressKey, TetherIngressValue> getBpfIngressMap() {
+                        return mBpfIngressMap;
+                    }
+            });
 
     @Before public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -185,6 +198,63 @@
         waitForIdle();
     }
 
+    private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t);
+        } else {
+            return verify(t);
+        }
+    }
+
+    private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder,
+            @NonNull Ipv6ForwardingRule rule) throws Exception {
+        if (mDeps.isAtLeastS()) {
+            verifyWithOrder(inOrder, mBpfIngressMap).updateEntry(
+                    rule.makeTetherIngressKey(), rule.makeTetherIngressValue());
+        } else {
+            verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(rule));
+        }
+    }
+
+    private void verifyNeverTetherOffloadRuleAdd() throws Exception {
+        if (mDeps.isAtLeastS()) {
+            verify(mBpfIngressMap, never()).updateEntry(any(), any());
+        } else {
+            verify(mNetd, never()).tetherOffloadRuleAdd(any());
+        }
+    }
+
+    // TODO: remove once presubmit tests on R even the code is submitted on S.
+    private void checkTetherOffloadRuleAdd(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);
+
+        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+        coordinator.tetherOffloadRuleAdd(mIpServer, rule);
+        verifyTetherOffloadRuleAdd(null, rule);
+    }
+
+    // TODO: remove once presubmit tests on R even the code is submitted on S.
+    @Test
+    public void testTetherOffloadRuleAddSdkR() throws Exception {
+        checkTetherOffloadRuleAdd(false /* R */);
+    }
+
+    // TODO: remove once presubmit tests on R even the code is submitted on S.
+    @Test
+    public void testTetherOffloadRuleAddAtLeastSdkS() throws Exception {
+        checkTetherOffloadRuleAdd(true /* S+ */);
+    }
+
     @Test
     public void testGetForwardedStats() throws Exception {
         setupFunctioningNetdInterface();
@@ -339,6 +409,33 @@
     }
 
     @Test
+    public void testRuleMakeTetherIngressKey() throws Exception {
+        final Integer mobileIfIndex = 100;
+        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+
+        final TetherIngressKey key = rule.makeTetherIngressKey();
+        assertEquals(key.iif, (long) mobileIfIndex);
+        assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress()));
+        // iif (4) + neigh6 (16) = 20.
+        assertEquals(20, key.writeToBytes().length);
+    }
+
+    @Test
+    public void testRuleMakeTetherIngressValue() throws Exception {
+        final Integer mobileIfIndex = 100;
+        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+
+        final TetherIngressValue value = rule.makeTetherIngressValue();
+        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();
 
@@ -352,9 +449,9 @@
         // 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);
+        final InOrder inOrder = inOrder(mNetd, mBpfIngressMap);
         coordinator.tetherOffloadRuleAdd(mIpServer, rule);
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(rule));
+        verifyTetherOffloadRuleAdd(inOrder, rule);
         inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
         inOrder.verifyNoMoreInteractions();
 
@@ -393,7 +490,7 @@
         // 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);
+        final InOrder inOrder = inOrder(mNetd, mBpfIngressMap);
         mTetherStatsProvider.onSetLimit(mobileIface, limit);
         waitForIdle();
         inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
@@ -401,14 +498,14 @@
         // Adding the first rule on current upstream immediately sends the quota to netd.
         final Ipv6ForwardingRule ruleA = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
         coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ruleA));
+        verifyTetherOffloadRuleAdd(inOrder, ruleA);
         inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, limit);
         inOrder.verifyNoMoreInteractions();
 
         // Adding the second rule on current upstream does not send the quota to netd.
         final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B);
         coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ruleB));
+        verifyTetherOffloadRuleAdd(inOrder, ruleB);
         inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
 
         // Removing the second rule on current upstream does not send the quota to netd.
@@ -438,7 +535,7 @@
         coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
         coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
 
-        final InOrder inOrder = inOrder(mNetd);
+        final InOrder inOrder = inOrder(mNetd, mBpfIngressMap);
 
         // 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.
@@ -455,11 +552,11 @@
                 ethIfIndex, NEIGH_B, MAC_B);
 
         coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA);
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ethernetRuleA));
+        verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA);
         inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(ethIfIndex, QUOTA_UNLIMITED);
 
         coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB);
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ethernetRuleB));
+        verifyTetherOffloadRuleAdd(inOrder, ethernetRuleB);
 
         // [2] Update the existing rules from Ethernet to cellular.
         final Ipv6ForwardingRule mobileRuleA = buildTestForwardingRule(
@@ -473,11 +570,11 @@
         // by one for updating upstream interface index by #tetherOffloadRuleUpdate.
         coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex);
         inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleA));
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(mobileRuleA));
+        verifyTetherOffloadRuleAdd(inOrder, mobileRuleA);
         inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
         inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleB));
         inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ethIfIndex);
-        inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(mobileRuleB));
+        verifyTetherOffloadRuleAdd(inOrder, mobileRuleB);
 
         // [3] Clear all rules for a given IpServer.
         when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex))
@@ -499,11 +596,10 @@
                 .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 50, 60, 70, 80)));
     }
 
-    @Test
-    public void testTetheringConfigDisable() throws Exception {
-        setupFunctioningNetdInterface();
-        when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
-
+    private void checkBpfDisabled() throws Exception {
+        // The caller may mock the global dependencies |mDeps| which is used in
+        // #makeBpfCoordinator for testing.
+        // See #testBpfDisabledbyNoBpfIngressMap.
         final BpfCoordinator coordinator = makeBpfCoordinator();
         coordinator.startPolling();
 
@@ -523,7 +619,7 @@
         final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
         final Ipv6ForwardingRule rule = buildTestForwardingRule(ifIndex, neigh, mac);
         coordinator.tetherOffloadRuleAdd(mIpServer, rule);
-        verify(mNetd, never()).tetherOffloadRuleAdd(any());
+        verifyNeverTetherOffloadRuleAdd();
         LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules =
                 coordinator.getForwardingRulesForTesting().get(mIpServer);
         assertNull(rules);
@@ -550,13 +646,30 @@
         // The rule can't be updated.
         coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */);
         verify(mNetd, never()).tetherOffloadRuleRemove(any());
-        verify(mNetd, never()).tetherOffloadRuleAdd(any());
+        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 testBpfDisabledbyNoBpfIngressMap() throws Exception {
+        setupFunctioningNetdInterface();
+        doReturn(null).when(mDeps).getBpfIngressMap();
+
+        checkBpfDisabled();
+    }
+
+    @Test
     public void testTetheringConfigSetPollingInterval() throws Exception {
         setupFunctioningNetdInterface();