Merge "Creating interface name to index tracking for Bpf Maps." into main
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index 5f66f47..5ff708d 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -21,7 +21,6 @@
 import static android.net.NetworkStats.UID_ALL;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.net.NetworkStats;
 import android.net.UnderlyingNetworkInfo;
@@ -31,6 +30,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.BpfNetMaps;
+import com.android.server.connectivity.InterfaceTracker;
 
 import java.io.IOException;
 import java.net.ProtocolException;
@@ -108,7 +108,7 @@
 
         /** Create a new {@link BpfNetMaps}. */
         public BpfNetMaps createBpfNetMaps(@NonNull Context ctx) {
-            return new BpfNetMaps(ctx);
+            return new BpfNetMaps(ctx, new InterfaceTracker(ctx));
         }
     }
 
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 36c0cf9..25c0617 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -49,6 +49,8 @@
 import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
 import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.StatsManager;
 import android.content.Context;
 import android.net.BpfNetMapsUtils;
@@ -85,12 +87,14 @@
 import com.android.net.module.util.bpf.IngressDiscardKey;
 import com.android.net.module.util.bpf.IngressDiscardValue;
 import com.android.net.module.util.bpf.LocalNetAccessKey;
+import com.android.server.connectivity.InterfaceTracker;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.StringJoiner;
 
@@ -142,6 +146,7 @@
             Pair.create(TRAFFIC_PERMISSION_INTERNET, "PERMISSION_INTERNET"),
             Pair.create(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
     );
+    private final InterfaceTracker mInterfaceTracker;
 
     /**
      * Set configurationMap for test.
@@ -423,23 +428,27 @@
 
     /** Constructor used after T that doesn't need to use netd anymore. */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public BpfNetMaps(final Context context) {
-        this(context, null);
+    public BpfNetMaps(final Context context, @NonNull final InterfaceTracker interfaceTracker) {
+        this(context, null, interfaceTracker);
 
         if (!SdkLevel.isAtLeastT()) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
     }
 
-    public BpfNetMaps(final Context context, final INetd netd) {
-        this(context, netd, new Dependencies());
+    public BpfNetMaps(final Context context, final INetd netd, @NonNull final InterfaceTracker
+            interfaceTracker) {
+        this(context, netd, new Dependencies(), interfaceTracker);
     }
 
     @VisibleForTesting
-    public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps) {
+    public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps,
+            @NonNull final  InterfaceTracker interfaceTracker) {
+        Objects.requireNonNull(interfaceTracker);
         if (SdkLevel.isAtLeastT()) {
             ensureInitialized(context);
         }
         mNetd = netd;
         mDeps = deps;
+        mInterfaceTracker = interfaceTracker;
     }
 
     private void maybeThrow(final int err, final String msg) {
@@ -902,7 +911,7 @@
      * @param isAllowed is the local network call allowed or blocked.
      */
     @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-    public void addLocalNetAccess(final int lpmBitlen, final String iface,
+    public void addLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
             final InetAddress address, final int protocol, final int remotePort,
             final boolean isAllowed) {
         throwIfPre25Q2("addLocalNetAccess is not available on pre-B devices");
@@ -910,7 +919,7 @@
         if (iface == null) {
             ifIndex = 0;
         } else {
-            ifIndex = mDeps.getIfIndex(iface);
+            ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
         }
         if (ifIndex == 0) {
             Log.e(TAG, "Failed to get if index, skip addLocalNetAccess for " + address
@@ -937,14 +946,14 @@
      * @param remotePort src/dst port for ingress/egress
      */
     @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-    public void removeLocalNetAccess(final int lpmBitlen, final String iface,
+    public void removeLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
             final InetAddress address, final int protocol, final int remotePort) {
         throwIfPre25Q2("removeLocalNetAccess is not available on pre-B devices");
         final int ifIndex;
         if (iface == null) {
             ifIndex = 0;
         } else {
-            ifIndex = mDeps.getIfIndex(iface);
+            ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
         }
         if (ifIndex == 0) {
             Log.e(TAG, "Failed to get if index, skip removeLocalNetAccess for " + address
@@ -973,14 +982,14 @@
      * is not local network or if configuration is allowed like local dns servers.
      */
     @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-    public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
+    public boolean getLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
             final InetAddress address, final int protocol, final int remotePort) {
         throwIfPre25Q2("getLocalNetAccess is not available on pre-B devices");
         final int ifIndex;
         if (iface == null) {
             ifIndex = 0;
         } else {
-            ifIndex = mDeps.getIfIndex(iface);
+            ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
         }
         if (ifIndex == 0) {
             Log.e(TAG, "Failed to get if index, returning default from getLocalNetAccess for "
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b2e49e7..3ce3f02 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -365,6 +365,7 @@
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
 import com.android.server.connectivity.DscpPolicyTracker;
 import com.android.server.connectivity.FullScore;
+import com.android.server.connectivity.InterfaceTracker;
 import com.android.server.connectivity.InvalidTagException;
 import com.android.server.connectivity.KeepaliveResourceUtil;
 import com.android.server.connectivity.KeepaliveTracker;
@@ -577,6 +578,7 @@
     private final NetworkStatsManager mStatsManager;
     private final NetworkPolicyManager mPolicyManager;
     private final BpfNetMaps mBpfNetMaps;
+    private final InterfaceTracker mInterfaceTracker;
 
     /**
      * TestNetworkService (lazily) created upon first usage. Locked to prevent creation of multiple
@@ -1662,8 +1664,17 @@
          * @param netd a netd binder
          * @return BpfNetMaps implementation.
          */
-        public BpfNetMaps getBpfNetMaps(Context context, INetd netd) {
-            return new BpfNetMaps(context, netd);
+        public BpfNetMaps getBpfNetMaps(Context context, INetd netd,
+                InterfaceTracker interfaceTracker) {
+            return new BpfNetMaps(context, netd, interfaceTracker);
+        }
+
+        /**
+         * Get the InterfaceTracker implementation to use in ConnectivityService.
+         * @return InterfaceTracker implementation.
+         */
+        public InterfaceTracker getInterfaceTracker(Context context) {
+            return new InterfaceTracker(context);
         }
 
         /**
@@ -1886,7 +1897,8 @@
         mWakeUpMask = mask;
 
         mNetd = netd;
-        mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
+        mInterfaceTracker = mDeps.getInterfaceTracker(mContext);
+        mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd, mInterfaceTracker);
         mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
         mPermissionMonitorDeps = mPermDeps;
         mPermissionMonitor =
@@ -9605,8 +9617,6 @@
 
         updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
 
-        updateLocalNetworkAddresses(newLp, oldLp);
-
         updateMtu(newLp, oldLp);
         // TODO - figure out what to do for clat
 //        for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -9769,16 +9779,23 @@
                     wakeupModifyInterface(iface, nai, true);
                     mDeps.reportNetworkInterfaceForTransports(mContext, iface,
                             nai.networkCapabilities.getTransportTypes());
+                    mInterfaceTracker.addInterface(iface);
                 } catch (Exception e) {
                     logw("Exception adding interface: " + e);
                 }
             }
         }
+
+        // The local network addresses needs to be updated before interfaces are removed because
+        // modifying bpf map local_net_access requires mapping interface name to index.
+        updateLocalNetworkAddresses(newLp, oldLp);
+
         for (final String iface : interfaceDiff.removed) {
             try {
                 if (DBG) log("Removing iface " + iface + " from network " + netId);
                 wakeupModifyInterface(iface, nai, false);
                 mRoutingCoordinatorService.removeInterfaceFromNetwork(netId, iface);
+                mInterfaceTracker.removeInterface(iface);
             } catch (Exception e) {
                 loge("Exception removing interface: " + e);
             }
diff --git a/service/src/com/android/server/connectivity/InterfaceTracker.java b/service/src/com/android/server/connectivity/InterfaceTracker.java
new file mode 100644
index 0000000..0b4abeb
--- /dev/null
+++ b/service/src/com/android/server/connectivity/InterfaceTracker.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2025 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.server.connectivity;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.BpfNetMaps;
+
+import java.util.Map;
+
+/**
+ * InterfaceTracker is responsible for providing interface mapping and tracking.
+ * @hide
+ */
+public class InterfaceTracker {
+    static {
+        if (BpfNetMaps.isAtLeast25Q2()) {
+            System.loadLibrary("service-connectivity");
+        }
+    }
+    private static final String TAG = "InterfaceTracker";
+    private final Dependencies mDeps;
+    private final Map<String, Integer> mInterfaceMap;
+
+    public InterfaceTracker(final Context context) {
+        this(context, new Dependencies());
+    }
+
+    @VisibleForTesting
+    public InterfaceTracker(final Context context, final Dependencies deps) {
+        this.mInterfaceMap = new ArrayMap<>();
+        this.mDeps = deps;
+    }
+
+    /**
+     * To add interface to tracking
+     * @param interfaceName name of interface added.
+     */
+    public void addInterface(@Nullable final String interfaceName) {
+        final int interfaceIndex;
+        if (interfaceName == null) {
+            interfaceIndex = 0;
+        } else {
+            interfaceIndex = mDeps.getIfIndex(interfaceName);
+        }
+        if (interfaceIndex == 0) {
+            Log.e(TAG, "Failed to get interface index for " + interfaceName);
+            return;
+        }
+        synchronized (mInterfaceMap) {
+            mInterfaceMap.put(interfaceName, interfaceIndex);
+        }
+    }
+
+    /**
+     * To remove interface from tracking
+     * @param interfaceName name of interface removed.
+     * @return true if the value was present and now removed.
+     */
+    public boolean removeInterface(@Nullable final String interfaceName) {
+        if (interfaceName == null) return false;
+        synchronized (mInterfaceMap) {
+            return mInterfaceMap.remove(interfaceName) != null;
+        }
+    }
+
+    /**
+     * Get interface index from interface name.
+     * @param interfaceName name of interface
+     * @return interface index for given interface name or 0 if interface is not found.
+     */
+    public int getInterfaceIndex(@Nullable final String interfaceName) {
+        final int interfaceIndex;
+        if (interfaceName != null) {
+            synchronized (mInterfaceMap) {
+                interfaceIndex = mInterfaceMap.getOrDefault(interfaceName, 0);
+            }
+        } else {
+            interfaceIndex = 0;
+        }
+        return interfaceIndex;
+    }
+
+    /**
+     * Dependencies of InterfaceTracker, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * Get interface index.
+         */
+        public int getIfIndex(final String ifName) {
+            return Os.if_nametoindex(ifName);
+        }
+
+        /**
+         * Get interface name
+         */
+        public String getIfName(final int ifIndex) {
+            return Os.if_indextoname(ifIndex);
+        }
+
+    }
+}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 437eb81..de39215 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -55,11 +55,11 @@
 import com.android.networkstack.apishim.TelephonyManagerShimImpl
 import com.android.server.BpfNetMaps
 import com.android.server.ConnectivityService
-import com.android.server.L2capNetworkProvider
 import com.android.server.NetworkAgentWrapper
 import com.android.server.TestNetIdManager
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator
 import com.android.server.connectivity.ConnectivityResources
+import com.android.server.connectivity.InterfaceTracker
 import com.android.server.connectivity.MockableSystemProperties
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.PermissionMonitor
@@ -114,6 +114,8 @@
     @Mock
     private lateinit var netd: INetd
     @Mock
+    private lateinit var interfaceTracker: InterfaceTracker
+    @Mock
     private lateinit var dnsResolver: IDnsResolver
     @Mock
     private lateinit var systemConfigManager: SystemConfigManager
@@ -140,11 +142,15 @@
 
         private val realContext get() = InstrumentationRegistry.getInstrumentation().context
         private val httpProbeUrl get() =
-            realContext.getResources().getString(com.android.server.net.integrationtests.R.string
-                    .config_captive_portal_http_url)
+            realContext.getResources().getString(
+                com.android.server.net.integrationtests.R.string
+                    .config_captive_portal_http_url
+            )
         private val httpsProbeUrl get() =
-            realContext.getResources().getString(com.android.server.net.integrationtests.R.string
-                    .config_captive_portal_https_url)
+            realContext.getResources().getString(
+                com.android.server.net.integrationtests.R.string
+                    .config_captive_portal_https_url
+            )
 
         private class InstrumentationServiceConnection : ServiceConnection {
             override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@@ -166,12 +172,19 @@
         fun setUpClass() {
             val intent = Intent(realContext, NetworkStackInstrumentationService::class.java)
             intent.action = INetworkStackInstrumentation::class.qualifiedName
-            assertTrue(realContext.bindService(intent, InstrumentationServiceConnection(),
-                    BIND_AUTO_CREATE or BIND_IMPORTANT),
-                    "Error binding to instrumentation service")
-            assertTrue(bindingCondition.block(SERVICE_BIND_TIMEOUT_MS),
+            assertTrue(
+                realContext.bindService(
+                    intent,
+                    InstrumentationServiceConnection(),
+                    BIND_AUTO_CREATE or BIND_IMPORTANT
+                ),
+                    "Error binding to instrumentation service"
+            )
+            assertTrue(
+                bindingCondition.block(SERVICE_BIND_TIMEOUT_MS),
                     "Timed out binding to instrumentation service " +
-                            "after $SERVICE_BIND_TIMEOUT_MS ms")
+                            "after $SERVICE_BIND_TIMEOUT_MS ms"
+            )
         }
     }
 
@@ -201,7 +214,8 @@
         // We don't test the actual notification value strings, so just return an empty array.
         // It doesn't matter what the values are as long as it's not null.
         doReturn(emptyArray<String>()).`when`(resources).getStringArray(
-                R.array.network_switch_type_name)
+                R.array.network_switch_type_name
+        )
         doReturn(1).`when`(resources).getInteger(R.integer.config_networkAvoidBadWifi)
         doReturn(R.array.config_networkSupportedKeepaliveCount).`when`(resources)
                 .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any())
@@ -223,7 +237,13 @@
     }
 
     private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
-            context, dnsResolver, log, netd, deps, PermissionMonitorDependencies())
+            context,
+        dnsResolver,
+        log,
+        netd,
+        deps,
+        PermissionMonitorDependencies()
+    )
 
     private inner class TestDependencies : ConnectivityService.Dependencies() {
         override fun getNetworkStack() = networkStackClient
@@ -231,7 +251,11 @@
             mock(ProxyTracker::class.java)
         override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
         override fun makeNetIdManager() = TestNetIdManager()
-        override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+        override fun getBpfNetMaps(
+            context: Context?,
+            netd: INetd?,
+                                   interfaceTracker: InterfaceTracker?
+        ) = mock(BpfNetMaps::class.java)
                 .also {
                     doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
                 }
@@ -241,13 +265,17 @@
             c: Context,
             h: Handler,
             r: Runnable
-        ) = MultinetworkPolicyTracker(c, h, r,
+        ) = MultinetworkPolicyTracker(
+            c,
+            h,
+            r,
             object : MultinetworkPolicyTracker.Dependencies() {
                 override fun getResourcesForActiveSubId(
                     connResources: ConnectivityResources,
                     activeSubId: Int
                 ) = resources
-            })
+            }
+        )
 
         override fun makeHandlerThread(tag: String): HandlerThread =
             super.makeHandlerThread(tag).also { handlerThreads.add(it) }
@@ -259,13 +287,18 @@
                 listener: BiConsumer<Int, Int>,
                 handler: Handler
         ): CarrierPrivilegeAuthenticator {
-            return CarrierPrivilegeAuthenticator(context,
+            return CarrierPrivilegeAuthenticator(
+                context,
                     object : CarrierPrivilegeAuthenticator.Dependencies() {
                         override fun makeHandlerThread(): HandlerThread =
                                 super.makeHandlerThread().also { handlerThreads.add(it) }
                     },
-                    tm, TelephonyManagerShimImpl.newInstance(tm),
-                    requestRestrictedWifiEnabled, listener, handler)
+                    tm,
+                TelephonyManagerShimImpl.newInstance(tm),
+                    requestRestrictedWifiEnabled,
+                listener,
+                handler
+            )
         }
 
         override fun makeSatelliteAccessController(
@@ -273,7 +306,8 @@
             updateSatellitePreferredUid: Consumer<MutableSet<Int>>?,
             connectivityServiceInternalHandler: Handler
         ): SatelliteAccessController? = mock(
-            SatelliteAccessController::class.java)
+            SatelliteAccessController::class.java
+        )
 
         override fun makeL2capNetworkProvider(context: Context) = null
     }
@@ -305,8 +339,12 @@
         nsInstrumentation.addHttpResponse(HttpResponse(httpProbeUrl, responseCode = 204))
         nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204))
 
-        val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), null /* ncTemplate */,
-                context)
+        val na = NetworkAgentWrapper(
+            TRANSPORT_CELLULAR,
+            LinkProperties(),
+            null /* ncTemplate */,
+                context
+        )
         networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
 
         na.addCapability(NET_CAPABILITY_INTERNET)
@@ -344,7 +382,8 @@
                     |  "user-portal-url": "https://login.capport.android.com",
                     |  "venue-info-url": "https://venueinfo.capport.android.com"
                     |}
-                """.trimMargin()))
+                """.trimMargin()
+        ))
 
         // Tests will fail if a non-mocked query is received: mock the HTTPS probe, but not the
         // HTTP probe as it should not be sent.
@@ -398,8 +437,10 @@
                 BpfUtils.BPF_CGROUP_INET_EGRESS,
                 BpfUtils.BPF_CGROUP_INET_SOCK_CREATE
         ).forEach {
-            val ret = SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
-                    "cmd connectivity bpf-get-cgroup-program-id $it").trim()
+            val ret = SystemUtil.runShellCommand(
+                InstrumentationRegistry.getInstrumentation(),
+                    "cmd connectivity bpf-get-cgroup-program-id $it"
+            ).trim()
 
             assertTrue(Integer.parseInt(ret) > 0, "Unexpected output $ret for type $it")
         }
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index caf1765..1d2e8b0 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -109,6 +109,7 @@
 import com.android.net.module.util.bpf.IngressDiscardKey;
 import com.android.net.module.util.bpf.IngressDiscardValue;
 import com.android.net.module.util.bpf.LocalNetAccessKey;
+import com.android.server.connectivity.InterfaceTracker;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -170,6 +171,8 @@
     @Mock INetd mNetd;
     @Mock BpfNetMaps.Dependencies mDeps;
     @Mock Context mContext;
+
+    @Mock InterfaceTracker mInterfaceTracker;
     private final IBpfMap<S32, U32> mConfigurationMap = new TestBpfMap<>(S32.class, U32.class);
     private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap =
             new TestBpfMap<>(S32.class, UidOwnerValue.class);
@@ -188,6 +191,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+        doReturn(TEST_IF_INDEX).when(mInterfaceTracker).getInterfaceIndex(TEST_IF_NAME);
         doReturn(TEST_IF_NAME).when(mDeps).getIfName(TEST_IF_INDEX);
         doReturn(0).when(mDeps).synchronizeKernelRCU();
         BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
@@ -202,7 +206,7 @@
         BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
         mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
         BpfNetMaps.setIngressDiscardMapForTest(mIngressDiscardMap);
-        mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps);
+        mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps, mInterfaceTracker);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index c4944b6..c28a0f8 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -413,6 +413,7 @@
 import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.connectivity.InterfaceTracker;
 import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.MultinetworkPolicyTracker;
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies;
@@ -2262,7 +2263,8 @@
         }
 
         @Override
-        public BpfNetMaps getBpfNetMaps(Context context, INetd netd) {
+        public BpfNetMaps getBpfNetMaps(Context context, INetd netd,
+                InterfaceTracker interfaceTracker) {
             return mBpfNetMaps;
         }
 
diff --git a/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java b/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java
new file mode 100644
index 0000000..8a9ada0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2025 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.server.connectivity;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.ConnectivityModuleTest;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@ConnectivityModuleTest
+public class InterfaceTrackerTest {
+    private static final String TAG = "InterfaceTrackerTest";
+    private static final String TEST_IF_NAME = "wlan10";
+    private static final String TEST_INCORRECT_IF_NAME = "wlan20";
+    private static final int TEST_IF_INDEX = 7;
+
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+    private InterfaceTracker mInterfaceTracker;
+
+    @Mock Context mContext;
+    @Mock InterfaceTracker.Dependencies mDeps;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+        mInterfaceTracker = new InterfaceTracker(mContext, mDeps);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddingInterface_InterfaceNameIndexMappingAdded() {
+        mInterfaceTracker.addInterface(TEST_IF_NAME);
+        assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddingNullInterface_InterfaceNameIndexMappingNotAdded() {
+        mInterfaceTracker.addInterface(null);
+        assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddingIncorrectInterface_InterfaceNameIndexMappingNotAdded() {
+        mInterfaceTracker.addInterface(TEST_INCORRECT_IF_NAME);
+
+        assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_INCORRECT_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemovingInterface_InterfaceNameIndexMappingRemoved() {
+        mInterfaceTracker.addInterface(TEST_IF_NAME);
+        assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+        mInterfaceTracker.removeInterface(TEST_IF_NAME);
+        assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemovingNullInterface_InterfaceNameIndexMappingNotRemoved() {
+        mInterfaceTracker.addInterface(TEST_IF_NAME);
+        mInterfaceTracker.removeInterface(null);
+        assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemovingIncorrectInterface_InterfaceNameIndexMappingNotRemoved() {
+        mInterfaceTracker.addInterface(TEST_IF_NAME);
+        mInterfaceTracker.removeInterface(TEST_INCORRECT_IF_NAME);
+        assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 557bfd6..d7e781e 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -68,6 +68,7 @@
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator
 import com.android.server.connectivity.ClatCoordinator
 import com.android.server.connectivity.ConnectivityFlags
+import com.android.server.connectivity.InterfaceTracker
 import com.android.server.connectivity.MulticastRoutingCoordinatorService
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
@@ -193,6 +194,7 @@
     val connResources = makeMockConnResources(sysResources, packageManager)
 
     val netd = mock<INetd>()
+    val interfaceTracker = mock<InterfaceTracker>()
     val bpfNetMaps = mock<BpfNetMaps>().also {
         doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
     }
@@ -279,7 +281,11 @@
 
     inner class CSDeps : ConnectivityService.Dependencies() {
         override fun getResources(ctx: Context) = connResources
-        override fun getBpfNetMaps(context: Context, netd: INetd) = this@CSTest.bpfNetMaps
+        override fun getBpfNetMaps(
+            context: Context,
+            netd: INetd,
+            interfaceTracker: InterfaceTracker
+        ) = this@CSTest.bpfNetMaps
         override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator
         override fun getNetworkStack() = this@CSTest.networkStack