Merge changes I47606693,Ia51bc3ab,Idef1a6d1,Ie6fbc3ee,I55064ee7 into main am: 4c701fbfa0

Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/3115440

Change-Id: Ia3888cb7a911450dd0b4982e9af9e56e1799823d
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 01f5393..fe5a0c6 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -34,7 +34,6 @@
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
 import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
-import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
 import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
 import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
 
@@ -77,11 +76,7 @@
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.SyncStateMachine.StateInfo;
 import com.android.net.module.util.ip.InterfaceController;
-import com.android.net.module.util.ip.IpNeighborMonitor;
-import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
 import com.android.networkstack.tethering.BpfCoordinator;
-import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.networkstack.tethering.metrics.TetheringMetrics;
@@ -189,12 +184,6 @@
             return new DadProxy(handler, ifParams);
         }
 
-        /** Create an IpNeighborMonitor to be used by this IpServer */
-        public IpNeighborMonitor getIpNeighborMonitor(Handler handler, SharedLog log,
-                IpNeighborMonitor.NeighborEventConsumer consumer) {
-            return new IpNeighborMonitor(handler, log, consumer);
-        }
-
         /** Create a RouterAdvertisementDaemon instance to be used by IpServer.*/
         public RouterAdvertisementDaemon getRouterAdvertisementDaemon(InterfaceParams ifParams) {
             return new RouterAdvertisementDaemon(ifParams);
@@ -234,13 +223,11 @@
     public static final int CMD_TETHER_CONNECTION_CHANGED   = BASE_IPSERVER + 9;
     // new IPv6 tethering parameters need to be processed
     public static final int CMD_IPV6_TETHER_UPDATE          = BASE_IPSERVER + 10;
-    // new neighbor cache entry on our interface
-    public static final int CMD_NEIGHBOR_EVENT              = BASE_IPSERVER + 11;
     // request from DHCP server that it wants to have a new prefix
-    public static final int CMD_NEW_PREFIX_REQUEST          = BASE_IPSERVER + 12;
+    public static final int CMD_NEW_PREFIX_REQUEST          = BASE_IPSERVER + 11;
     // request from PrivateAddressCoordinator to restart tethering.
-    public static final int CMD_NOTIFY_PREFIX_CONFLICT      = BASE_IPSERVER + 13;
-    public static final int CMD_SERVICE_FAILED_TO_START     = BASE_IPSERVER + 14;
+    public static final int CMD_NOTIFY_PREFIX_CONFLICT      = BASE_IPSERVER + 12;
+    public static final int CMD_SERVICE_FAILED_TO_START     = BASE_IPSERVER + 13;
 
     private final State mInitialState;
     private final State mLocalHotspotState;
@@ -301,18 +288,9 @@
     private List<TetheredClient> mDhcpLeases = Collections.emptyList();
 
     private int mLastIPv6UpstreamIfindex = 0;
-    private boolean mUpstreamSupportsBpf = false;
     @NonNull
     private Set<IpPrefix> mLastIPv6UpstreamPrefixes = Collections.emptySet();
 
-    private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
-        public void accept(NeighborEvent e) {
-            sendMessage(CMD_NEIGHBOR_EVENT, e);
-        }
-    }
-
-    private final IpNeighborMonitor mIpNeighborMonitor;
-
     private LinkAddress mIpv4Address;
 
     private final TetheringMetrics mTetheringMetrics;
@@ -346,15 +324,6 @@
         mLastError = TETHER_ERROR_NO_ERROR;
         mServingMode = STATE_AVAILABLE;
 
-        mIpNeighborMonitor = mDeps.getIpNeighborMonitor(getHandler(), mLog,
-                new MyNeighborEventConsumer());
-
-        // IP neighbor monitor monitors the neighbor events for adding/removing IPv6 downstream rule
-        // per client. If BPF offload is not supported, don't start listening for neighbor events.
-        if (mBpfCoordinator.isUsingBpfOffload() && !mIpNeighborMonitor.start()) {
-            mLog.e("Failed to create IpNeighborMonitor on " + mIfaceName);
-        }
-
         mInitialState = new InitialState();
         mLocalHotspotState = new LocalHotspotState();
         mTetheredState = new TetheredState();
@@ -410,6 +379,22 @@
         return mIpv4Address;
     }
 
+    /** The IPv6 upstream interface index */
+    public int getIpv6UpstreamIfindex() {
+        return mLastIPv6UpstreamIfindex;
+    }
+
+    /** The IPv6 upstream interface prefixes */
+    @NonNull
+    public Set<IpPrefix> getIpv6UpstreamPrefixes() {
+        return Collections.unmodifiableSet(mLastIPv6UpstreamPrefixes);
+    }
+
+    /** The interface parameters which IpServer is using */
+    public InterfaceParams getInterfaceParams() {
+        return mInterfaceParams;
+    }
+
     /**
      * Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper
      * thread.
@@ -813,14 +798,15 @@
         setRaParams(params);
 
         // Not support BPF on virtual upstream interface
-        final boolean upstreamSupportsBpf = upstreamIface != null && !isVcnInterface(upstreamIface);
         final Set<IpPrefix> upstreamPrefixes = params != null ? params.prefixes : Set.of();
-        updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamPrefixes,
-                upstreamIfIndex, upstreamPrefixes, upstreamSupportsBpf);
+        // mBpfCoordinator#updateIpv6UpstreamInterface must be called before updating
+        // mLastIPv6UpstreamIfindex and mLastIPv6UpstreamPrefixes because BpfCoordinator will call
+        // IpServer#getIpv6UpstreamIfindex and IpServer#getIpv6UpstreamPrefixes to retrieve current
+        // upstream interface index and prefixes when handling upstream changes.
+        mBpfCoordinator.updateIpv6UpstreamInterface(this, upstreamIfIndex, upstreamPrefixes);
         mLastIPv6LinkProperties = v6only;
         mLastIPv6UpstreamIfindex = upstreamIfIndex;
         mLastIPv6UpstreamPrefixes = upstreamPrefixes;
-        mUpstreamSupportsBpf = upstreamSupportsBpf;
         if (mDadProxy != null) {
             mDadProxy.setUpstreamIface(upstreamIfaceParams);
         }
@@ -964,77 +950,6 @@
         }
     }
 
-    private int getInterfaceIndexForRule(int ifindex, boolean supportsBpf) {
-        return supportsBpf ? ifindex : NO_UPSTREAM;
-    }
-
-    // Handles updates to IPv6 forwarding rules if the upstream or its prefixes change.
-    private void updateIpv6ForwardingRules(int prevUpstreamIfindex,
-            @NonNull Set<IpPrefix> prevUpstreamPrefixes, int upstreamIfindex,
-            @NonNull Set<IpPrefix> upstreamPrefixes, boolean upstreamSupportsBpf) {
-        // If the upstream interface has changed, remove all rules and re-add them with the new
-        // upstream interface. If upstream is a virtual network, treated as no upstream.
-        if (prevUpstreamIfindex != upstreamIfindex
-                || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
-            mBpfCoordinator.updateAllIpv6Rules(this, this.mInterfaceParams,
-                    getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
-                    upstreamPrefixes);
-        }
-    }
-
-    // Handles updates to IPv6 downstream rules if a neighbor event is received.
-    private void addOrRemoveIpv6Downstream(NeighborEvent e) {
-        // mInterfaceParams must be non-null or the event would not have arrived.
-        if (e == null) return;
-        if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress()
-                || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
-            return;
-        }
-
-        // When deleting rules, we still need to pass a non-null MAC, even though it's ignored.
-        // Do this here instead of in the Ipv6DownstreamRule constructor to ensure that we
-        // never add rules with a null MAC, only delete them.
-        MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
-        Ipv6DownstreamRule rule = new Ipv6DownstreamRule(
-                getInterfaceIndexForRule(mLastIPv6UpstreamIfindex, mUpstreamSupportsBpf),
-                mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
-        if (e.isValid()) {
-            mBpfCoordinator.addIpv6DownstreamRule(this, rule);
-        } else {
-            mBpfCoordinator.removeIpv6DownstreamRule(this, rule);
-        }
-    }
-
-    // TODO: consider moving into BpfCoordinator.
-    private void updateClientInfoIpv4(NeighborEvent e) {
-        if (e == null) return;
-        if (!(e.ip instanceof Inet4Address) || e.ip.isMulticastAddress()
-                || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
-            return;
-        }
-
-        // When deleting clients, IpServer still need to pass a non-null MAC, even though it's
-        // ignored. Do this here instead of in the ClientInfo constructor to ensure that
-        // IpServer never add clients with a null MAC, only delete them.
-        final MacAddress clientMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
-        final ClientInfo clientInfo = new ClientInfo(mInterfaceParams.index,
-                mInterfaceParams.macAddr, (Inet4Address) e.ip, clientMac);
-        if (e.isValid()) {
-            mBpfCoordinator.tetherOffloadClientAdd(this, clientInfo);
-        } else {
-            mBpfCoordinator.tetherOffloadClientRemove(this, clientInfo);
-        }
-    }
-
-    private void handleNeighborEvent(NeighborEvent e) {
-        if (mInterfaceParams != null
-                && mInterfaceParams.index == e.ifindex
-                && mInterfaceParams.hasMacAddress) {
-            addOrRemoveIpv6Downstream(e);
-            updateClientInfoIpv4(e);
-        }
-    }
-
     private byte getHopLimit(String upstreamIface, int adjustTTL) {
         try {
             int upstreamHopLimit = Integer.parseUnsignedInt(
@@ -1069,7 +984,6 @@
         switch (what) {
             // Suppress some CMD_* to avoid log flooding.
             case CMD_IPV6_TETHER_UPDATE:
-            case CMD_NEIGHBOR_EVENT:
                 break;
             default:
                 mLog.log(state.getName() + " got "
@@ -1141,14 +1055,6 @@
         }
     }
 
-    private void startConntrackMonitoring() {
-        mBpfCoordinator.startMonitoring(this);
-    }
-
-    private void stopConntrackMonitoring() {
-        mBpfCoordinator.stopMonitoring(this);
-    }
-
     abstract class BaseServingState extends State {
         private final int mDesiredInterfaceState;
 
@@ -1158,7 +1064,7 @@
 
         @Override
         public void enter() {
-            startConntrackMonitoring();
+            mBpfCoordinator.addIpServer(IpServer.this);
 
             startServingInterface();
 
@@ -1226,7 +1132,7 @@
             }
 
             stopIPv4();
-            stopConntrackMonitoring();
+            mBpfCoordinator.removeIpServer(IpServer.this);
 
             resetLinkProperties();
 
@@ -1397,8 +1303,8 @@
 
             for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
             mUpstreamIfaceSet = null;
-            mBpfCoordinator.updateAllIpv6Rules(
-                    IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM, Set.of());
+            mBpfCoordinator.updateIpv6UpstreamInterface(IpServer.this, NO_UPSTREAM,
+                    Collections.emptySet());
         }
 
         private void cleanupUpstreamInterface(String upstreamIface) {
@@ -1473,9 +1379,6 @@
                         }
                     }
                     break;
-                case CMD_NEIGHBOR_EVENT:
-                    handleNeighborEvent((NeighborEvent) message.obj);
-                    break;
                 default:
                     return false;
             }
@@ -1515,9 +1418,6 @@
     class UnavailableState extends State {
         @Override
         public void enter() {
-            // TODO: move mIpNeighborMonitor.stop() to TetheredState#exit, and trigger a neighbours
-            //       dump after starting mIpNeighborMonitor.
-            mIpNeighborMonitor.stop();
             mLastError = TETHER_ERROR_NO_ERROR;
             sendInterfaceState(STATE_UNAVAILABLE);
         }
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 00d9152..5c853f4 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -76,6 +76,9 @@
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.ip.ConntrackMonitor;
 import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEventConsumer;
+import com.android.net.module.util.ip.IpNeighborMonitor;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkUtils;
@@ -181,6 +184,10 @@
     private final BpfCoordinatorShim mBpfCoordinatorShim;
     @NonNull
     private final BpfConntrackEventConsumer mBpfConntrackEventConsumer;
+    @NonNull
+    private final IpNeighborMonitor mIpNeighborMonitor;
+    @NonNull
+    private final BpfNeighborEventConsumer mBpfNeighborEventConsumer;
 
     // 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
@@ -189,14 +196,6 @@
     // to make it simpler. See also TetheringConfiguration.
     private final boolean mIsBpfEnabled;
 
-    // Tracks whether BPF tethering is started or not. This is set by tethering before it
-    // starts the first IpServer and is cleared by tethering shortly before the last IpServer
-    // is stopped. Note that rule updates (especially deletions, but sometimes additions as
-    // well) may arrive when this is false. If they do, they must be communicated to netd.
-    // Changes in data limits may also arrive when this is false, and if they do, they must
-    // also be communicated to netd.
-    private boolean mPollingStarted = false;
-
     // Tracking remaining alert quota. Unlike limit quota is subject to interface, the alert
     // quota is interface independent and global for tether offload.
     private long mRemainingAlertQuota = QUOTA_UNLIMITED;
@@ -279,9 +278,6 @@
     private final HashMap<IpServer, HashMap<Inet4Address, ClientInfo>>
             mTetherClients = new HashMap<>();
 
-    // Set for which downstream is monitoring the conntrack netlink message.
-    private final Set<IpServer> mMonitoringIpServers = new HashSet<>();
-
     // Map of upstream interface IPv4 address to interface index.
     // TODO: consider making the key to be unique because the upstream address is not unique. It
     // is okay for now because there have only one upstream generally.
@@ -303,16 +299,19 @@
     @Nullable
     private UpstreamInfo mIpv4UpstreamInfo = null;
 
+    // The IpServers that are currently served by BpfCoordinator.
+    private final ArraySet<IpServer> mServedIpServers = new ArraySet<>();
+
     // Runnable that used by scheduling next polling of stats.
     private final Runnable mScheduledPollingStats = () -> {
         updateForwardedStats();
-        maybeSchedulePollingStats();
+        schedulePollingStats();
     };
 
     // Runnable that used by scheduling next refreshing of conntrack timeout.
     private final Runnable mScheduledConntrackTimeoutUpdate = () -> {
         refreshAllConntrackTimeouts();
-        maybeScheduleConntrackTimeoutUpdate();
+        scheduleConntrackTimeoutUpdate();
     };
 
     // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
@@ -338,6 +337,11 @@
             return new ConntrackMonitor(getHandler(), getSharedLog(), consumer);
         }
 
+        /** Get ip neighbor monitor */
+        @NonNull public IpNeighborMonitor getIpNeighborMonitor(NeighborEventConsumer consumer) {
+            return new IpNeighborMonitor(getHandler(), getSharedLog(), consumer);
+        }
+
         /** Get interface information for a given interface. */
         @NonNull public InterfaceParams getInterfaceParams(String ifName) {
             return InterfaceParams.getByName(ifName);
@@ -485,6 +489,9 @@
         mBpfConntrackEventConsumer = new BpfConntrackEventConsumer();
         mConntrackMonitor = mDeps.getConntrackMonitor(mBpfConntrackEventConsumer);
 
+        mBpfNeighborEventConsumer = new BpfNeighborEventConsumer();
+        mIpNeighborMonitor = mDeps.getIpNeighborMonitor(mBpfNeighborEventConsumer);
+
         BpfTetherStatsProvider provider = new BpfTetherStatsProvider();
         try {
             mDeps.getNetworkStatsManager().registerNetworkStatsProvider(
@@ -504,37 +511,25 @@
     }
 
     /**
-     * Start BPF tethering offload stats polling when the first upstream is started.
+     * Start BPF tethering offload stats and conntrack timeout polling.
      * Note that this can be only called on handler thread.
-     * TODO: Perhaps check BPF support before starting.
-     * TODO: Start the stats polling only if there is any client on the downstream.
      */
-    public void startPolling() {
-        if (mPollingStarted) return;
+    private void startStatsAndConntrackTimeoutPolling() {
+        schedulePollingStats();
+        scheduleConntrackTimeoutUpdate();
 
-        if (!isUsingBpf()) {
-            mLog.i("BPF is not using");
-            return;
-        }
-
-        mPollingStarted = true;
-        maybeSchedulePollingStats();
-        maybeScheduleConntrackTimeoutUpdate();
-
-        mLog.i("Polling started");
+        mLog.i("Polling started.");
     }
 
     /**
-     * Stop BPF tethering offload stats polling.
+     * Stop BPF tethering offload stats and conntrack timeout polling.
      * The data limit cleanup and the tether stats maps cleanup are not implemented here.
      * These cleanups rely on all IpServers calling #removeIpv6DownstreamRule. After the
      * last rule is removed from the upstream, #removeIpv6DownstreamRule does the cleanup
      * functionality.
      * Note that this can be only called on handler thread.
      */
-    public void stopPolling() {
-        if (!mPollingStarted) return;
-
+    private void stopStatsAndConntrackTimeoutPolling() {
         // Stop scheduled polling conntrack timeout.
         if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
             mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
@@ -544,9 +539,8 @@
             mHandler.removeCallbacks(mScheduledPollingStats);
         }
         updateForwardedStats();
-        mPollingStarted = false;
 
-        mLog.i("Polling stopped");
+        mLog.i("Polling stopped.");
     }
 
     /**
@@ -567,7 +561,6 @@
 
     /**
      * Start conntrack message monitoring.
-     * Note that this can be only called on handler thread.
      *
      * TODO: figure out a better logging for non-interesting conntrack message.
      * For example, the following logging is an IPCTNL_MSG_CT_GET message but looks scary.
@@ -587,45 +580,23 @@
      * +------------------+--------------------------------------------------------+
      * See NetlinkMonitor#handlePacket, NetlinkMessage#parseNfMessage.
      */
-    public void startMonitoring(@NonNull final IpServer ipServer) {
+    private void startConntrackMonitoring() {
         // TODO: Wrap conntrackMonitor starting function into mBpfCoordinatorShim.
-        if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
+        if (!mDeps.isAtLeastS()) return;
 
-        if (mMonitoringIpServers.contains(ipServer)) {
-            Log.wtf(TAG, "The same downstream " + ipServer.interfaceName()
-                    + " should not start monitoring twice.");
-            return;
-        }
-
-        if (mMonitoringIpServers.isEmpty()) {
-            mConntrackMonitor.start();
-            mLog.i("Monitoring started");
-        }
-
-        mMonitoringIpServers.add(ipServer);
+        mConntrackMonitor.start();
+        mLog.i("Conntrack monitoring started.");
     }
 
     /**
      * Stop conntrack event monitoring.
-     * Note that this can be only called on handler thread.
      */
-    public void stopMonitoring(@NonNull final IpServer ipServer) {
+    private void stopConntrackMonitoring() {
         // TODO: Wrap conntrackMonitor stopping function into mBpfCoordinatorShim.
-        if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
-
-        // Ignore stopping monitoring if the monitor has never started for a given IpServer.
-        if (!mMonitoringIpServers.contains(ipServer)) {
-            mLog.e("Ignore stopping monitoring because monitoring has never started for "
-                    + ipServer.interfaceName());
-            return;
-        }
-
-        mMonitoringIpServers.remove(ipServer);
-
-        if (!mMonitoringIpServers.isEmpty()) return;
+        if (!mDeps.isAtLeastS()) return;
 
         mConntrackMonitor.stop();
-        mLog.i("Monitoring stopped");
+        mLog.i("Conntrack monitoring stopped.");
     }
 
     /**
@@ -688,9 +659,8 @@
 
     /**
      * Add IPv6 downstream rule.
-     * Note that this can be only called on handler thread.
      */
-    public void addIpv6DownstreamRule(
+    private void addIpv6DownstreamRule(
             @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
         if (!isUsingBpf()) return;
 
@@ -706,9 +676,8 @@
 
     /**
      * Remove IPv6 downstream rule.
-     * Note that this can be only called on handler thread.
      */
-    public void removeIpv6DownstreamRule(
+    private void removeIpv6DownstreamRule(
             @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
         if (!isUsingBpf()) return;
 
@@ -762,9 +731,8 @@
     /**
      * Delete all upstream and downstream rules for the passed-in IpServer, and if the new upstream
      * is nonzero, reapply them to the new upstream.
-     * Note that this can be only called on handler thread.
      */
-    public void updateAllIpv6Rules(@NonNull final IpServer ipServer,
+    private void updateAllIpv6Rules(@NonNull final IpServer ipServer,
             final InterfaceParams interfaceParams, int newUpstreamIfindex,
             @NonNull final Set<IpPrefix> newUpstreamPrefixes) {
         if (!isUsingBpf()) return;
@@ -886,6 +854,141 @@
     }
 
     /**
+     * Register an IpServer (downstream).
+     * Note that this can be only called on handler thread.
+     */
+    public void addIpServer(@NonNull final IpServer ipServer) {
+        if (!isUsingBpf()) return;
+        if (mServedIpServers.contains(ipServer)) {
+            Log.wtf(TAG, "The same downstream " + ipServer.interfaceName()
+                    + " should not add twice.");
+            return;
+        }
+
+        // Start monitoring and polling when the first IpServer is added.
+        if (mServedIpServers.isEmpty()) {
+            startStatsAndConntrackTimeoutPolling();
+            startConntrackMonitoring();
+            mIpNeighborMonitor.start();
+            mLog.i("Neighbor monitoring started.");
+        }
+        mServedIpServers.add(ipServer);
+    }
+
+    /**
+     * Unregister an IpServer (downstream).
+     * Note that this can be only called on handler thread.
+     */
+    public void removeIpServer(@NonNull final IpServer ipServer) {
+        if (!isUsingBpf()) return;
+        if (!mServedIpServers.contains(ipServer)) {
+            mLog.e("Ignore removing because IpServer has never started for "
+                    + ipServer.interfaceName());
+            return;
+        }
+        mServedIpServers.remove(ipServer);
+
+        // Stop monitoring and polling when the last IpServer is removed.
+        if (mServedIpServers.isEmpty()) {
+            stopStatsAndConntrackTimeoutPolling();
+            stopConntrackMonitoring();
+            mIpNeighborMonitor.stop();
+            mLog.i("Neighbor monitoring stopped.");
+        }
+    }
+
+    /**
+     * Update upstream interface and its prefixes.
+     * Note that this can be only called on handler thread.
+     */
+    public void updateIpv6UpstreamInterface(@NonNull final IpServer ipServer, int upstreamIfindex,
+            @NonNull Set<IpPrefix> upstreamPrefixes) {
+        if (!isUsingBpf()) return;
+
+        // If the upstream interface has changed, remove all rules and re-add them with the new
+        // upstream interface. If upstream is a virtual network, treated as no upstream.
+        final int prevUpstreamIfindex = ipServer.getIpv6UpstreamIfindex();
+        final InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+        final Set<IpPrefix> prevUpstreamPrefixes = ipServer.getIpv6UpstreamPrefixes();
+        if (prevUpstreamIfindex != upstreamIfindex
+                || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
+            final boolean upstreamSupportsBpf = checkUpstreamSupportsBpf(upstreamIfindex);
+            updateAllIpv6Rules(ipServer, interfaceParams,
+                    getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
+                    upstreamPrefixes);
+        }
+    }
+
+    private boolean checkUpstreamSupportsBpf(int upstreamIfindex) {
+        final String iface = mInterfaceNames.get(upstreamIfindex);
+        return iface != null && !isVcnInterface(iface);
+    }
+
+    private int getInterfaceIndexForRule(int ifindex, boolean supportsBpf) {
+        return supportsBpf ? ifindex : NO_UPSTREAM;
+    }
+
+    // Handles updates to IPv6 downstream rules if a neighbor event is received.
+    private void addOrRemoveIpv6Downstream(@NonNull IpServer ipServer, NeighborEvent e) {
+        // mInterfaceParams must be non-null or the event would not have arrived.
+        if (e == null) return;
+        if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress()
+                || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
+            return;
+        }
+
+        // When deleting rules, we still need to pass a non-null MAC, even though it's ignored.
+        // Do this here instead of in the Ipv6DownstreamRule constructor to ensure that we
+        // never add rules with a null MAC, only delete them.
+        final InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+        if (interfaceParams == null || interfaceParams.macAddr == null) return;
+        final int lastIpv6UpstreamIfindex = ipServer.getIpv6UpstreamIfindex();
+        final boolean isUpstreamSupportsBpf = checkUpstreamSupportsBpf(lastIpv6UpstreamIfindex);
+        MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
+        Ipv6DownstreamRule rule = new Ipv6DownstreamRule(
+                getInterfaceIndexForRule(lastIpv6UpstreamIfindex, isUpstreamSupportsBpf),
+                interfaceParams.index, (Inet6Address) e.ip, interfaceParams.macAddr, dstMac);
+        if (e.isValid()) {
+            addIpv6DownstreamRule(ipServer, rule);
+        } else {
+            removeIpv6DownstreamRule(ipServer, rule);
+        }
+    }
+
+    private void updateClientInfoIpv4(@NonNull IpServer ipServer, NeighborEvent e) {
+        if (e == null) return;
+        if (!(e.ip instanceof Inet4Address) || e.ip.isMulticastAddress()
+                || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
+            return;
+        }
+
+        InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+        if (interfaceParams == null) return;
+
+        // When deleting clients, IpServer still need to pass a non-null MAC, even though it's
+        // ignored. Do this here instead of in the ClientInfo constructor to ensure that
+        // IpServer never add clients with a null MAC, only delete them.
+        final MacAddress clientMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
+        final ClientInfo clientInfo = new ClientInfo(interfaceParams.index,
+                interfaceParams.macAddr, (Inet4Address) e.ip, clientMac);
+        if (e.isValid()) {
+            tetherOffloadClientAdd(ipServer, clientInfo);
+        } else {
+            tetherOffloadClientRemove(ipServer, clientInfo);
+        }
+    }
+
+    private void handleNeighborEvent(@NonNull IpServer ipServer, NeighborEvent e) {
+        InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+        if (interfaceParams != null
+                && interfaceParams.index == e.ifindex
+                && interfaceParams.hasMacAddress) {
+            addOrRemoveIpv6Downstream(ipServer, e);
+            updateClientInfoIpv4(ipServer, e);
+        }
+    }
+
+    /**
      * Clear all forwarding IPv4 rules for a given client.
      * Note that this can be only called on handler thread.
      */
@@ -1136,7 +1239,7 @@
         // Note that EthernetTetheringTest#isTetherConfigBpfOffloadEnabled relies on
         // "mIsBpfEnabled" to check tethering config via dumpsys. Beware of the change if any.
         pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
-        pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
+        pw.println("Polling " + (mServedIpServers.isEmpty() ? "not started" : "started"));
         pw.println("Stats provider " + (mStatsProvider != null
                 ? "registered" : "not registered"));
         pw.println("Upstream quota: " + mInterfaceQuotas.toString());
@@ -2038,6 +2141,15 @@
         }
     }
 
+    @VisibleForTesting
+    private class BpfNeighborEventConsumer implements NeighborEventConsumer {
+        public void accept(NeighborEvent e) {
+            for (IpServer ipServer : mServedIpServers) {
+                handleNeighborEvent(ipServer, e);
+            }
+        }
+    }
+
     private boolean isBpfEnabled() {
         final TetheringConfiguration config = mDeps.getTetherConfig();
         return (config != null) ? config.isBpfOffloadEnabled() : true /* default value */;
@@ -2365,9 +2477,7 @@
         });
     }
 
-    private void maybeSchedulePollingStats() {
-        if (!mPollingStarted) return;
-
+    private void schedulePollingStats() {
         if (mHandler.hasCallbacks(mScheduledPollingStats)) {
             mHandler.removeCallbacks(mScheduledPollingStats);
         }
@@ -2375,9 +2485,7 @@
         mHandler.postDelayed(mScheduledPollingStats, getPollingInterval());
     }
 
-    private void maybeScheduleConntrackTimeoutUpdate() {
-        if (!mPollingStarted) return;
-
+    private void scheduleConntrackTimeoutUpdate() {
         if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
             mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
         }
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 29ced23..0ff89d3 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -2089,9 +2089,6 @@
                     chooseUpstreamType(true);
                     mTryCell = false;
                 }
-
-                // TODO: Check the upstream interface if it is managed by BPF offload.
-                mBpfCoordinator.startPolling();
             }
 
             @Override
@@ -2105,7 +2102,6 @@
                     reportUpstreamChanged(null);
                     mNotificationUpdater.onUpstreamCapabilitiesChanged(null);
                 }
-                mBpfCoordinator.stopPolling();
                 mTetheringMetrics.cleanup();
             }
 
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index a7064e8..00eb3b1 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -53,7 +53,6 @@
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -94,7 +93,6 @@
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.ip.IpNeighborMonitor;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
@@ -174,7 +172,6 @@
     @Mock private IDhcpServer mDhcpServer;
     @Mock private DadProxy mDadProxy;
     @Mock private RouterAdvertisementDaemon mRaDaemon;
-    @Mock private IpNeighborMonitor mIpNeighborMonitor;
     @Mock private IpServer.Dependencies mDependencies;
     @Mock private PrivateAddressCoordinator mAddressCoordinator;
     private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
@@ -213,20 +210,17 @@
             mInterfaceConfiguration.prefixLength = BLUETOOTH_DHCP_PREFIX_LENGTH;
         }
 
-        doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(), any());
-
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
         when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
         when(mBpfCoordinator.isUsingBpfOffload()).thenReturn(usingBpfOffload);
         mIpServer = createIpServer(interfaceType);
-        verify(mIpNeighborMonitor).start();
         mIpServer.start();
 
         // Starting the state machine always puts us in a consistent state and notifies
         // the rest of the world that we've changed from an unknown to available state.
         mLooper.dispatchAll();
-        reset(mNetd, mCallback, mIpNeighborMonitor);
+        reset(mNetd, mCallback);
 
         when(mRaDaemon.start()).thenReturn(true);
     }
@@ -242,6 +236,7 @@
             throws Exception {
         initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
         dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+        verify(mBpfCoordinator).addIpServer(mIpServer);
         if (upstreamIface != null) {
             InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
             assertNotNull("missing upstream interface: " + upstreamIface, interfaceParams);
@@ -250,8 +245,12 @@
             lp.setLinkAddresses(upstreamAddresses);
             dispatchTetherConnectionChanged(upstreamIface, lp, 0);
             Set<IpPrefix> upstreamPrefixes = getTetherableIpv6Prefixes(lp.getLinkAddresses());
-            verify(mBpfCoordinator).updateAllIpv6Rules(
-                    mIpServer, TEST_IFACE_PARAMS, interfaceParams.index, upstreamPrefixes);
+            // One is called when handling CMD_TETHER_CONNECTION_CHANGED and the other one is called
+            // when upstream's LinkProperties is updated (updateUpstreamIPv6LinkProperties)
+            verify(mBpfCoordinator, times(2)).maybeAddUpstreamToLookupTable(
+                    interfaceParams.index, upstreamIface);
+            verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                    mIpServer, interfaceParams.index, upstreamPrefixes);
         }
         reset(mNetd, mBpfCoordinator, mCallback, mAddressCoordinator);
         when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
@@ -314,8 +313,6 @@
 
     @Test
     public void startsOutAvailable() throws Exception {
-        when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
-                .thenReturn(mIpNeighborMonitor);
         mIpServer = createIpServer(TETHERING_BLUETOOTH);
         mIpServer.start();
         mLooper.dispatchAll();
@@ -557,8 +554,8 @@
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        inOrder.verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         // When tethering stops, upstream interface is set to zero and thus clearing all upstream
         // rules. Downstream rules are needed to be cleared explicitly by calling
         // BpfCoordinator#clearAllIpv6Rules in TetheredState#exit.
@@ -570,7 +567,7 @@
                 argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
         inOrder.verify(mAddressCoordinator).releaseDownstream(any());
         inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer);
-        inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
+        inOrder.verify(mBpfCoordinator).removeIpServer(mIpServer);
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
@@ -765,8 +762,8 @@
         lp.setInterfaceName(UPSTREAM_IFACE2);
         lp.setLinkAddresses(UPSTREAM_ADDRESSES);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
         reset(mBpfCoordinator);
 
         // Upstream link addresses change result in updating the rules.
@@ -774,8 +771,8 @@
         lp2.setInterfaceName(UPSTREAM_IFACE2);
         lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, -1);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
         reset(mBpfCoordinator);
 
         // When the upstream is lost, rules are removed.
@@ -784,53 +781,54 @@
         // - processMessage CMD_TETHER_CONNECTION_CHANGED for the upstream is lost.
         // - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost.
         // See dispatchTetherConnectionChanged.
-        verify(mBpfCoordinator, times(2)).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        verify(mBpfCoordinator, times(2)).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         reset(mBpfCoordinator);
 
         // If the upstream is IPv4-only, no rules are added.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        verify(mBpfCoordinator, never()).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        verify(mBpfCoordinator, never()).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         reset(mBpfCoordinator);
 
         // Rules are added again once upstream IPv6 connectivity is available.
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
         reset(mBpfCoordinator);
 
         // If upstream IPv6 connectivity is lost, rules are removed.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         reset(mBpfCoordinator);
 
         // When upstream IPv6 connectivity comes back, rules are added.
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
         reset(mBpfCoordinator);
 
         // When the downstream interface goes down, rules are removed.
         mIpServer.stop();
         mLooper.dispatchAll();
         verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        verify(mBpfCoordinator).removeIpServer(mIpServer);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         reset(mBpfCoordinator);
     }
 
     @Test
-    public void stopNeighborMonitoringWhenInterfaceDown() throws Exception {
+    public void removeIpServerWhenInterfaceDown() throws Exception {
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
                 false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
 
         mIpServer.stop();
         mLooper.dispatchAll();
-        verify(mIpNeighborMonitor).stop();
+        verify(mBpfCoordinator).removeIpServer(mIpServer);
     }
 
     private LinkProperties buildIpv6OnlyLinkProperties(final String iface) {
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 47ecf58..e54a7e0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -25,8 +25,6 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStats.UID_TETHERING;
-import static android.net.TetheringManager.TETHERING_WIFI;
-import static android.net.ip.IpServer.STATE_TETHERED;
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
@@ -79,9 +77,7 @@
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
@@ -100,11 +96,9 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkStats;
-import android.net.RoutingCoordinatorManager;
 import android.net.TetherOffloadRuleParcel;
 import android.net.TetherStatsParcel;
 import android.net.ip.IpServer;
-import android.net.ip.RouterAdvertisementDaemon;
 import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
@@ -119,12 +113,10 @@
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
-import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.bpf.Tether4Key;
@@ -143,8 +135,6 @@
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
-import com.android.networkstack.tethering.metrics.TetheringMetrics;
-import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -209,11 +199,6 @@
     private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
     private static final MacAddress MAC_NULL = MacAddress.fromString("00:00:00:00:00:00");
 
-    private static final LinkAddress UPSTREAM_ADDRESS = new LinkAddress("2001:db8:0:1234::168/64");
-    private static final LinkAddress UPSTREAM_ADDRESS2 = new LinkAddress("2001:db8:0:abcd::168/64");
-    private static final Set<LinkAddress> UPSTREAM_ADDRESSES = Set.of(UPSTREAM_ADDRESS);
-    private static final Set<LinkAddress> UPSTREAM_ADDRESSES2 =
-            Set.of(UPSTREAM_ADDRESS, UPSTREAM_ADDRESS2);
     private static final IpPrefix UPSTREAM_PREFIX = new IpPrefix("2001:db8:0:1234::/64");
     private static final IpPrefix UPSTREAM_PREFIX2 = new IpPrefix("2001:db8:0:abcd::/64");
     private static final Set<IpPrefix> UPSTREAM_PREFIXES = Set.of(UPSTREAM_PREFIX);
@@ -449,13 +434,6 @@
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
     @Mock private IpNeighborMonitor mIpNeighborMonitor;
-    @Mock private RouterAdvertisementDaemon mRaDaemon;
-    @Mock private IpServer.Dependencies mIpServerDeps;
-    @Mock private IpServer.Callback mIpServerCallback;
-    @Mock private PrivateAddressCoordinator mAddressCoordinator;
-    private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
-            new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
-    @Mock private TetheringMetrics mTetheringMetrics;
 
     // Late init since methods must be called by the thread that created this object.
     private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
@@ -573,24 +551,8 @@
     @Before public void setUp() {
         MockitoAnnotations.initMocks(this);
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
-
-        // Simulate the behavior of RoutingCoordinator
-        if (null != mRoutingCoordinatorManager.value) {
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.tetherAddForward(fromIface, toIface);
-                mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
-                mNetd.tetherRemoveForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
-        }
+        when(mIpServer.getInterfaceParams()).thenReturn(DOWNSTREAM_IFACE_PARAMS);
+        when(mIpServer2.getInterfaceParams()).thenReturn(DOWNSTREAM_IFACE_PARAMS2);
     }
 
     private void waitForIdle() {
@@ -603,70 +565,39 @@
         when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
     }
 
-    @NonNull
-    private IpServer makeAndStartIpServer(String interfaceName, BpfCoordinator bpfCoordinator)
-            throws Exception {
-        final LinkAddress testAddress = new LinkAddress("192.168.42.5/24");
-        when(mIpServerDeps.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
-        when(mIpServerDeps.getInterfaceParams(DOWNSTREAM_IFACE)).thenReturn(
-                DOWNSTREAM_IFACE_PARAMS);
-        when(mIpServerDeps.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
-        when(mIpServerDeps.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
-        when(mIpServerDeps.getInterfaceParams(IPSEC_IFACE)).thenReturn(IPSEC_IFACE_PARAMS);
-        when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
-                anyBoolean())).thenReturn(testAddress);
-        when(mRaDaemon.start()).thenReturn(true);
-        ArgumentCaptor<NeighborEventConsumer> neighborEventCaptor =
-                ArgumentCaptor.forClass(NeighborEventConsumer.class);
-        doReturn(mIpNeighborMonitor).when(mIpServerDeps).getIpNeighborMonitor(any(), any(),
-                neighborEventCaptor.capture());
-        final IpServer ipServer = new IpServer(
-                interfaceName, mHandler, TETHERING_WIFI, new SharedLog("test"), mNetd,
-                bpfCoordinator, mRoutingCoordinatorManager, mIpServerCallback, mTetherConfig,
-                mAddressCoordinator, mTetheringMetrics, mIpServerDeps);
-        ipServer.start();
-        ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
-        mTestLooper.dispatchAll();
-
-        LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(UPSTREAM_IFACE);
-        lp.setLinkAddresses(UPSTREAM_ADDRESSES);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, 0);
-
-        mNeighborEventConsumer = neighborEventCaptor.getValue();
-        return ipServer;
-    }
-
-    private void dispatchTetherConnectionChanged(IpServer ipServer, String upstreamIface,
-            LinkProperties v6lp, int ttlAdjustment) {
-        dispatchTetherConnectionChanged(ipServer, upstreamIface);
-        ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, ttlAdjustment, 0, v6lp);
-        mTestLooper.dispatchAll();
-    }
-
-    private void dispatchTetherConnectionChanged(IpServer ipServer, String upstreamIface) {
-        final InterfaceSet ifs = (upstreamIface != null) ? new InterfaceSet(upstreamIface) : null;
-        ipServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifs);
-        mTestLooper.dispatchAll();
+    private void dispatchIpv6UpstreamChanged(BpfCoordinator bpfCoordinator, IpServer ipServer,
+            int upstreamIfindex, String upstreamIface, Set<IpPrefix> upstreamPrefixes) {
+        bpfCoordinator.maybeAddUpstreamToLookupTable(upstreamIfindex, upstreamIface);
+        bpfCoordinator.updateIpv6UpstreamInterface(ipServer, upstreamIfindex, upstreamPrefixes);
+        when(ipServer.getIpv6UpstreamIfindex()).thenReturn(upstreamIfindex);
+        when(ipServer.getIpv6UpstreamPrefixes()).thenReturn(upstreamPrefixes);
     }
 
     private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
         mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr,
                 nudState, mac));
-        mTestLooper.dispatchAll();
     }
 
     private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
         mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr,
                 nudState, mac));
-        mTestLooper.dispatchAll();
     }
 
     @NonNull
     private BpfCoordinator makeBpfCoordinator() throws Exception {
+        return makeBpfCoordinator(true /* addDefaultIpServer */);
+    }
+
+    @NonNull
+    private BpfCoordinator makeBpfCoordinator(boolean addDefaultIpServer) throws Exception {
         // mStatsManager will be invoked twice if BpfCoordinator is created the second time.
         clearInvocations(mStatsManager);
+        ArgumentCaptor<NeighborEventConsumer> neighborCaptor =
+                ArgumentCaptor.forClass(NeighborEventConsumer.class);
+        doReturn(mIpNeighborMonitor).when(mDeps).getIpNeighborMonitor(neighborCaptor.capture());
         final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
+        mNeighborEventConsumer = neighborCaptor.getValue();
+        assertNotNull(mNeighborEventConsumer);
 
         mConsumer = coordinator.getBpfConntrackEventConsumerForTesting();
         mTetherClients = coordinator.getTetherClientsForTesting();
@@ -681,6 +612,10 @@
         mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
         mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
 
+        if (addDefaultIpServer) {
+            coordinator.addIpServer(mIpServer);
+        }
+
         return coordinator;
     }
 
@@ -1008,7 +943,6 @@
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
-        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // InOrder is required because mBpfStatsMap may be accessed by both
         // BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats.
@@ -1020,19 +954,18 @@
                 mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
         final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(
                 mobileIfIndex, NEIGH_A, MAC_A);
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         verifyAddUpstreamRule(inOrder, upstreamRule);
-        coordinator.addIpv6DownstreamRule(mIpServer, downstreamRule);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
         verifyAddDownstreamRule(inOrder, downstreamRule);
 
         // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         verifyRemoveDownstreamRule(inOrder, downstreamRule);
         verifyRemoveUpstreamRule(inOrder, upstreamRule);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
@@ -1056,7 +989,6 @@
 
         doReturn(usingApiS).when(mDeps).isAtLeastS();
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
@@ -1092,7 +1024,6 @@
         setupFunctioningNetdInterface();
 
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String wlanIface = "wlan0";
         final Integer wlanIfIndex = 100;
@@ -1148,7 +1079,7 @@
         // [3] Stop coordinator.
         // Shutdown the coordinator and clear the invocation history, especially the
         // tetherOffloadGetStats() calls.
-        coordinator.stopPolling();
+        coordinator.removeIpServer(mIpServer);
         clearStatsInvocations();
 
         // Verify the polling update thread stopped.
@@ -1162,7 +1093,6 @@
         setupFunctioningNetdInterface();
 
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
@@ -1350,7 +1280,6 @@
 
         final String mobileIface = "rmnet_data0";
         final int mobileIfIndex = 100;
-        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // [1] Default limit.
         // Set the unlimited quota as default if the service has never applied a data limit for a
@@ -1358,8 +1287,8 @@
         final Ipv6UpstreamRule rule = buildTestUpstreamRule(
                 mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
         final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         verifyAddUpstreamRule(inOrder, rule);
@@ -1395,7 +1324,6 @@
 
         final String mobileIface = "rmnet_data0";
         final int mobileIfIndex = 100;
-        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // Applying a data limit to the current upstream does not take any immediate action.
         // The data limit could be only set on an upstream which has rules.
@@ -1408,31 +1336,30 @@
         // Adding the first rule on current upstream immediately sends the quota to BPF.
         final Ipv6UpstreamRule ruleA = buildTestUpstreamRule(
                 mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
         verifyAddUpstreamRule(inOrder, ruleA);
         inOrder.verifyNoMoreInteractions();
 
         // Adding the second rule on current upstream does not send the quota to BPF.
+        coordinator.addIpServer(mIpServer2);
         final Ipv6UpstreamRule ruleB = buildTestUpstreamRule(
                 mobileIfIndex, DOWNSTREAM_IFINDEX2, UPSTREAM_PREFIX, DOWNSTREAM_MAC2);
-        coordinator.updateAllIpv6Rules(
-                mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer2, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyAddUpstreamRule(inOrder, ruleB);
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Removing the second rule on current upstream does not send the quota to BPF.
-        coordinator.updateAllIpv6Rules(
-                mIpServer2, DOWNSTREAM_IFACE_PARAMS2, NO_UPSTREAM, NO_PREFIXES);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer2, NO_UPSTREAM, null, NO_PREFIXES);
         verifyRemoveUpstreamRule(inOrder, ruleB);
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         verifyRemoveUpstreamRule(inOrder, ruleA);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
         inOrder.verifyNoMoreInteractions();
@@ -1448,8 +1375,6 @@
         final String mobileIface = "rmnet_data0";
         final Integer ethIfIndex = 100;
         final Integer mobileIfIndex = 101;
-        coordinator.maybeAddUpstreamToLookupTable(ethIfIndex, ethIface);
-        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap,
                 mBpfStatsMap);
@@ -1470,14 +1395,14 @@
         final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
                 ethIfIndex, NEIGH_B, MAC_B);
 
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, ethIfIndex, ethIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         verifyAddUpstreamRule(inOrder, ethernetUpstreamRule);
-        coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleA);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
         verifyAddDownstreamRule(inOrder, ethernetRuleA);
-        coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleB);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_B, NUD_REACHABLE, MAC_B);
         verifyAddDownstreamRule(inOrder, ethernetRuleB);
 
         // [2] Update the existing rules from Ethernet to cellular.
@@ -1494,8 +1419,8 @@
 
         // Update the existing rules for upstream changes. The rules are removed and re-added one
         // by one for updating upstream interface index and prefixes by #tetherOffloadRuleUpdate.
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES2);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES2);
         verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
         verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
         verifyRemoveUpstreamRule(inOrder, ethernetUpstreamRule);
@@ -1532,7 +1457,6 @@
         // #makeBpfCoordinator for testing.
         // See #testBpfDisabledbyNoBpfDownstream6Map.
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         // The tether stats polling task should not be scheduled.
         mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
@@ -1549,7 +1473,7 @@
         final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
         final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
         final Ipv6DownstreamRule rule = buildTestDownstreamRule(ifIndex, neigh, mac);
-        coordinator.addIpv6DownstreamRule(mIpServer, rule);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, neigh, NUD_REACHABLE, mac);
         verifyNeverAddDownstreamRule();
         LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
                 coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1561,7 +1485,7 @@
         rules = new LinkedHashMap<Inet6Address, Ipv6DownstreamRule>();
         rules.put(rule.address, rule);
         coordinator.getIpv6DownstreamRulesForTesting().put(mIpServer, rules);
-        coordinator.removeIpv6DownstreamRule(mIpServer, rule);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, neigh, NUD_STALE, mac);
         verifyNeverRemoveDownstreamRule();
         rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNotNull(rules);
@@ -1575,8 +1499,8 @@
         assertEquals(1, rules.size());
 
         // The rule can't be updated.
-        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS,
-                rule.upstreamIfindex + 1 /* new */, UPSTREAM_PREFIXES);
+        coordinator.updateIpv6UpstreamInterface(mIpServer, rule.upstreamIfindex + 1 /* new */,
+                UPSTREAM_PREFIXES);
         verifyNeverRemoveDownstreamRule();
         verifyNeverAddDownstreamRule();
         rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1753,18 +1677,14 @@
         final BpfCoordinator coordinator = makeBpfCoordinator();
 
         // [1] The default polling interval.
-        coordinator.startPolling();
         assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval());
-        coordinator.stopPolling();
 
         // [2] Expect the invalid polling interval isn't applied. The valid range of interval is
         // DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long.
         for (final int interval
                 : new int[] {0, 100, DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS - 1}) {
             when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval);
-            coordinator.startPolling();
             assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval());
-            coordinator.stopPolling();
         }
 
         // [3] Set a specific polling interval which is larger than default value.
@@ -1772,7 +1692,6 @@
         // approximation is used to verify the scheduled time of the polling thread.
         final int pollingInterval = 100_000;
         when(mTetherConfig.getOffloadPollInterval()).thenReturn(pollingInterval);
-        coordinator.startPolling();
 
         // Expect the specific polling interval to be applied.
         assertEquals(pollingInterval, coordinator.getPollingInterval());
@@ -1800,19 +1719,19 @@
     public void testStartStopConntrackMonitoring() throws Exception {
         setupFunctioningNetdInterface();
 
-        final BpfCoordinator coordinator = makeBpfCoordinator();
+        final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
 
         // [1] Don't stop monitoring if it has never started.
-        coordinator.stopMonitoring(mIpServer);
+        coordinator.removeIpServer(mIpServer);
         verify(mConntrackMonitor, never()).stop();
 
         // [2] Start monitoring.
-        coordinator.startMonitoring(mIpServer);
+        coordinator.addIpServer(mIpServer);
         verify(mConntrackMonitor).start();
         clearInvocations(mConntrackMonitor);
 
         // [3] Stop monitoring.
-        coordinator.stopMonitoring(mIpServer);
+        coordinator.removeIpServer(mIpServer);
         verify(mConntrackMonitor).stop();
     }
 
@@ -1823,12 +1742,12 @@
     public void testStartStopConntrackMonitoring_R() throws Exception {
         setupFunctioningNetdInterface();
 
-        final BpfCoordinator coordinator = makeBpfCoordinator();
+        final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
 
-        coordinator.startMonitoring(mIpServer);
+        coordinator.addIpServer(mIpServer);
         verify(mConntrackMonitor, never()).start();
 
-        coordinator.stopMonitoring(mIpServer);
+        coordinator.removeIpServer(mIpServer);
         verify(mConntrackMonitor, never()).stop();
     }
 
@@ -1837,23 +1756,23 @@
     public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception {
         setupFunctioningNetdInterface();
 
-        final BpfCoordinator coordinator = makeBpfCoordinator();
+        final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
 
         // [1] Start monitoring at the first IpServer adding.
-        coordinator.startMonitoring(mIpServer);
+        coordinator.addIpServer(mIpServer);
         verify(mConntrackMonitor).start();
         clearInvocations(mConntrackMonitor);
 
         // [2] Don't start monitoring at the second IpServer adding.
-        coordinator.startMonitoring(mIpServer2);
+        coordinator.addIpServer(mIpServer2);
         verify(mConntrackMonitor, never()).start();
 
         // [3] Don't stop monitoring if any downstream interface exists.
-        coordinator.stopMonitoring(mIpServer2);
+        coordinator.removeIpServer(mIpServer2);
         verify(mConntrackMonitor, never()).stop();
 
         // [4] Stop monitoring if no downstream exists.
-        coordinator.stopMonitoring(mIpServer);
+        coordinator.removeIpServer(mIpServer);
         verify(mConntrackMonitor).stop();
     }
 
@@ -2016,9 +1935,8 @@
     public void testAddDevMapRule6() throws Exception {
         final BpfCoordinator coordinator = makeBpfCoordinator();
 
-        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
                 eq(new TetherDevValue(UPSTREAM_IFINDEX)));
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
@@ -2027,8 +1945,9 @@
 
         // Adding the second downstream, only the second downstream ifindex is added to DevMap,
         // the existing upstream ifindex won't be added again.
-        coordinator.updateAllIpv6Rules(
-                mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        coordinator.addIpServer(mIpServer2);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer2, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX2)),
                 eq(new TetherDevValue(DOWNSTREAM_IFINDEX2)));
         verify(mBpfDevMap, never()).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
@@ -2087,7 +2006,6 @@
                 .startMocking();
         try {
             final BpfCoordinator coordinator = makeBpfCoordinator();
-            coordinator.startPolling();
             bpfMap.insertEntry(tcpKey, tcpValue);
             bpfMap.insertEntry(udpKey, udpValue);
 
@@ -2116,7 +2034,7 @@
             ExtendedMockito.clearInvocations(staticMockMarker(NetlinkUtils.class));
 
             // [3] Don't refresh conntrack timeout if polling stopped.
-            coordinator.stopPolling();
+            coordinator.removeIpServer(mIpServer);
             mTestLooper.moveTimeForward(CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
             waitForIdle();
             ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkUtils.class));
@@ -2855,8 +2773,9 @@
         final int myIfindex = DOWNSTREAM_IFINDEX;
         final int notMyIfindex = myIfindex - 1;
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
 
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         resetNetdAndBpfMaps();
         verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
 
@@ -2905,10 +2824,8 @@
         resetNetdAndBpfMaps();
 
         InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-        LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(UPSTREAM_IFACE2);
-        lp.setLinkAddresses(UPSTREAM_ADDRESSES);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp, -1);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES);
         final Ipv6DownstreamRule ruleA2 = buildTestDownstreamRule(
                 UPSTREAM_IFINDEX2, NEIGH_A, MAC_A);
         final Ipv6DownstreamRule ruleB2 = buildTestDownstreamRule(
@@ -2922,11 +2839,9 @@
         verifyNoUpstreamIpv6ForwardingChange(inOrder);
         resetNetdAndBpfMaps();
 
-        // Upstream link addresses change result in updating the rules.
-        LinkProperties lp2 = new LinkProperties();
-        lp2.setInterfaceName(UPSTREAM_IFACE2);
-        lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp2, -1);
+        // Upstream prefixes change result in updating the rules.
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES2);
         verifyRemoveDownstreamRule(inOrder, ruleA2);
         verifyRemoveDownstreamRule(inOrder, ruleB2);
         verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
@@ -2936,7 +2851,7 @@
         resetNetdAndBpfMaps();
 
         // When the upstream is lost, rules are removed.
-        dispatchTetherConnectionChanged(ipServer, null, null, 0);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES2);
         verifyRemoveDownstreamRule(ruleA2);
         verifyRemoveDownstreamRule(ruleB2);
@@ -2947,7 +2862,7 @@
         resetNetdAndBpfMaps();
 
         // If the upstream is IPv4-only, no IPv6 rules are added to BPF map.
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         resetNetdAndBpfMaps();
         recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
         verifyNoUpstreamIpv6ForwardingChange(null);
@@ -2957,8 +2872,8 @@
 
         // Rules can be added again once upstream IPv6 connectivity is available. The existing rules
         // with an upstream of NO_UPSTREAM are reapplied.
-        lp.setInterfaceName(UPSTREAM_IFACE);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, -1);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
         verifyAddDownstreamRule(ruleA);
         recvNewNeigh(myIfindex, NEIGH_B, NUD_REACHABLE, MAC_B);
@@ -2966,23 +2881,27 @@
 
         // If upstream IPv6 connectivity is lost, rules are removed.
         resetNetdAndBpfMaps();
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, null, 0);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         verifyRemoveDownstreamRule(ruleA);
         verifyRemoveDownstreamRule(ruleB);
         verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
 
         // When upstream IPv6 connectivity comes back, upstream rules are added and downstream rules
         // are reapplied.
-        lp.setInterfaceName(UPSTREAM_IFACE);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, -1);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
         verifyAddDownstreamRule(ruleA);
         verifyAddDownstreamRule(ruleB);
         resetNetdAndBpfMaps();
 
         // When the downstream interface goes down, rules are removed.
-        ipServer.stop();
-        mTestLooper.dispatchAll();
+        // Simulate receiving CMD_INTERFACE_DOWN in the BaseServingState of IpServer.
+        reset(mIpNeighborMonitor);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
+        coordinator.tetherOffloadClientClear(mIpServer);
+        coordinator.removeIpServer(mIpServer);
+
         verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
         verifyRemoveDownstreamRule(ruleA);
         verifyRemoveDownstreamRule(ruleB);
@@ -3004,7 +2923,8 @@
         // [1] Enable BPF offload.
         // A neighbor that is added or deleted causes the rule to be added or removed.
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         resetNetdAndBpfMaps();
 
         recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
@@ -3019,10 +2939,8 @@
         resetNetdAndBpfMaps();
 
         // Upstream IPv6 connectivity change causes upstream rules change.
-        LinkProperties lp2 = new LinkProperties();
-        lp2.setInterfaceName(UPSTREAM_IFACE2);
-        lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp2, 0);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES2);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
         resetNetdAndBpfMaps();
 
@@ -3030,7 +2948,6 @@
         // A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
         final BpfCoordinator coordinator2 = makeBpfCoordinator();
-        final IpServer ipServer2 = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator2);
         verifyNoUpstreamIpv6ForwardingChange(null);
         resetNetdAndBpfMaps();
 
@@ -3043,7 +2960,8 @@
         resetNetdAndBpfMaps();
 
         // Upstream IPv6 connectivity change doesn't cause the rule to be added or removed.
-        dispatchTetherConnectionChanged(ipServer2, UPSTREAM_IFACE2, lp2, 0);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer2, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, NO_PREFIXES);
         verifyNoUpstreamIpv6ForwardingChange(null);
         verifyNeverRemoveDownstreamRule();
         resetNetdAndBpfMaps();
@@ -3053,7 +2971,6 @@
     public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
 
         // IP neighbor monitor doesn't start if BPF offload is disabled.
         verify(mIpNeighborMonitor, never()).start();
@@ -3062,15 +2979,10 @@
     @Test
     public void testSkipVirtualNetworkInBpf() throws Exception {
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
-        final LinkProperties v6Only = new LinkProperties();
-        v6Only.setInterfaceName(IPSEC_IFACE);
-        v6Only.setLinkAddresses(UPSTREAM_ADDRESSES);
 
         resetNetdAndBpfMaps();
-        dispatchTetherConnectionChanged(ipServer, IPSEC_IFACE, v6Only, 0);
-        verify(mNetd).tetherAddForward(DOWNSTREAM_IFACE, IPSEC_IFACE);
-        verify(mNetd).ipfwdAddInterfaceForward(DOWNSTREAM_IFACE, IPSEC_IFACE);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, IPSEC_IFINDEX, IPSEC_IFACE, UPSTREAM_PREFIXES);
         verifyNeverAddUpstreamRule();
 
         recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
@@ -3080,7 +2992,6 @@
     @Test
     public void addRemoveTetherClient() throws Exception {
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
         final int myIfindex = DOWNSTREAM_IFINDEX;
         final int notMyIfindex = myIfindex - 1;
 
@@ -3089,33 +3000,36 @@
         final InetAddress neighLL = InetAddresses.parseNumericAddress("169.254.0.1");
         final InetAddress neighMC = InetAddresses.parseNumericAddress("224.0.0.1");
 
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
+
         // Events on other interfaces are ignored.
         recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, MAC_A);
-        assertNull(mTetherClients.get(ipServer));
+        assertNull(mTetherClients.get(mIpServer));
 
         // Events on this interface are received and sent to BpfCoordinator.
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, MAC_A);
-        assertClientInfoExists(ipServer,
+        assertClientInfoExists(mIpServer,
                 new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighA, MAC_A));
 
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, MAC_B);
-        assertClientInfoExists(ipServer,
+        assertClientInfoExists(mIpServer,
                 new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighB, MAC_B));
 
         // Link-local and multicast neighbors are ignored.
         recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, MAC_A);
-        assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighLL);
+        assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighLL);
         recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, MAC_A);
-        assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighMC);
+        assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighMC);
 
         // A neighbor that is no longer valid causes the client to be removed.
         // NUD_FAILED events do not have a MAC address.
         recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
-        assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighA);
+        assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighA);
 
         // A neighbor that is deleted causes the client to be removed.
         recvDelNeigh(myIfindex, neighB, NUD_STALE, MAC_B);
         // When last client information is deleted, IpServer will be removed from mTetherClients
-        assertNull(mTetherClients.get(ipServer));
+        assertNull(mTetherClients.get(mIpServer));
     }
 }