diff --git a/framework/Android.bp b/framework/Android.bp index f3d8689bed..432cabf992 100644 --- a/framework/Android.bp +++ b/framework/Android.bp @@ -95,6 +95,7 @@ java_defaults { ], impl_only_static_libs: [ "net-utils-device-common-bpf", + "net-utils-device-common-struct", ], libs: [ "androidx.annotation_annotation", @@ -126,6 +127,7 @@ java_library { // Even if the library is included in "impl_only_static_libs" of defaults. This is still // needed because java_library which doesn't understand "impl_only_static_libs". "net-utils-device-common-bpf", + "net-utils-device-common-struct", ], libs: [ // This cannot be in the defaults clause above because if it were, it would be used diff --git a/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java new file mode 100644 index 0000000000..4d5001bf83 --- /dev/null +++ b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java @@ -0,0 +1,820 @@ +/* + * Copyright (C) 2023 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 android.net.MulticastRoutingConfig.FORWARD_NONE; +import static android.net.MulticastRoutingConfig.FORWARD_SELECTED; +import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.EADDRINUSE; +import static android.system.OsConstants.IPPROTO_ICMPV6; +import static android.system.OsConstants.IPPROTO_IPV6; +import static android.system.OsConstants.SOCK_CLOEXEC; +import static android.system.OsConstants.SOCK_NONBLOCK; +import static android.system.OsConstants.SOCK_RAW; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.MulticastRoutingConfig; +import android.net.NetworkUtils; +import android.os.Handler; +import android.os.Looper; +import android.system.ErrnoException; +import android.system.Os; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.LinkPropertiesUtils.CompareResult; +import com.android.net.module.util.PacketReader; +import com.android.net.module.util.SocketUtils; +import com.android.net.module.util.netlink.NetlinkUtils; +import com.android.net.module.util.netlink.RtNetlinkRouteMessage; +import com.android.net.module.util.structs.StructMf6cctl; +import com.android.net.module.util.structs.StructMif6ctl; +import com.android.net.module.util.structs.StructMrt6Msg; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Class to coordinate multicast routing between network interfaces. + * + *

Supports IPv6 multicast routing. + * + *

Note that usage of this class is not thread-safe. All public methods must be called from the + * same thread that the handler from {@code dependencies.getHandler} is associated. + */ +public class MulticastRoutingCoordinatorService { + private static final String TAG = MulticastRoutingCoordinatorService.class.getSimpleName(); + private static final int ICMP6_FILTER = 1; + private static final int MRT6_INIT = 200; + private static final int MRT6_ADD_MIF = 202; + private static final int MRT6_DEL_MIF = 203; + private static final int MRT6_ADD_MFC = 204; + private static final int MRT6_DEL_MFC = 205; + private static final int ONE = 1; + + private final Dependencies mDependencies; + + private final Handler mHandler; + private final MulticastNocacheUpcallListener mMulticastNoCacheUpcallListener; + @NonNull private final FileDescriptor mMulticastRoutingFd; // For multicast routing config + @NonNull private final MulticastSocket mMulticastSocket; // For join group and leave group + + @VisibleForTesting public static final int MFC_INACTIVE_CHECK_INTERVAL_MS = 60_000; + @VisibleForTesting public static final int MFC_INACTIVE_TIMEOUT_MS = 300_000; + @VisibleForTesting public static final int MFC_MAX_NUMBER_OF_ENTRIES = 1_000; + + // The kernel supports max 32 virtual interfaces per multicast routing table. + private static final int MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES = 32; + + /** Tracks if checking for inactive MFC has been scheduled */ + private boolean mMfcPollingScheduled = false; + + /** Mapping from multicast virtual interface index to interface name */ + private SparseArray mVirtualInterfaces = + new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES); + /** Mapping from physical interface index to interface name */ + private SparseArray mInterfaces = + new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES); + + /** Mapping of iif to PerInterfaceMulticastRoutingConfig */ + private Map mMulticastRoutingConfigs = + new HashMap(); + + private static final class PerInterfaceMulticastRoutingConfig { + // mapping of oif name to MulticastRoutingConfig + public Map oifConfigs = + new HashMap(); + } + + /** Tracks the MFCs added to kernel. Using LinkedHashMap to keep the added order, so + // when the number of MFCs reaches the max limit then the earliest added one is removed. */ + private LinkedHashMap mMfcs = new LinkedHashMap<>(); + + public MulticastRoutingCoordinatorService(Handler h) { + this(h, new Dependencies()); + } + + @VisibleForTesting + /* @throws UnsupportedOperationException if multicast routing is not supported */ + public MulticastRoutingCoordinatorService(Handler h, Dependencies dependencies) { + mDependencies = dependencies; + mMulticastRoutingFd = mDependencies.createMulticastRoutingSocket(); + mMulticastSocket = mDependencies.createMulticastSocket(); + mHandler = h; + mMulticastNoCacheUpcallListener = + new MulticastNocacheUpcallListener(mHandler, mMulticastRoutingFd); + mHandler.post(() -> mMulticastNoCacheUpcallListener.start()); + } + + private void checkOnHandlerThread() { + if (Looper.myLooper() != mHandler.getLooper()) { + throw new IllegalStateException( + "Not running on ConnectivityService thread (" + mHandler.getLooper() + ") : " + + Looper.myLooper()); + } + } + + private Integer getInterfaceIndex(String ifName) { + int mapIndex = mInterfaces.indexOfValue(ifName); + if (mapIndex < 0) return null; + return mInterfaces.keyAt(mapIndex); + } + + /** + * Apply multicast routing configuration + * + * @param iifName name of the incoming interface + * @param oifName name of the outgoing interface + * @param newConfig the multicast routing configuration to be applied from iif to oif + * @throws MulticastRoutingException when failed to apply the config + */ + public void applyMulticastRoutingConfig( + final String iifName, final String oifName, final MulticastRoutingConfig newConfig) { + checkOnHandlerThread(); + + if (newConfig.getForwardingMode() != FORWARD_NONE) { + // Make sure iif and oif are added as multicast forwarding interfaces + try { + maybeAddAndTrackInterface(iifName); + maybeAddAndTrackInterface(oifName); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to apply multicast routing config, ", e); + return; + } + } + + final MulticastRoutingConfig oldConfig = getMulticastRoutingConfig(iifName, oifName); + + if (oldConfig.equals(newConfig)) return; + + int oldMode = oldConfig.getForwardingMode(); + int newMode = newConfig.getForwardingMode(); + Integer iifIndex = getInterfaceIndex(iifName); + if (iifIndex == null) { + // This cannot happen unless the new config has FORWARD_NONE but is not the same + // as the old config. This is not possible in current code. + Log.wtf(TAG, "Adding multicast configuration on null interface?"); + return; + } + + // When new addresses are added to FORWARD_SELECTED mode, join these multicast groups + // on their upstream interface, so upstream multicast routers know about the subscription. + // When addresses are removed from FORWARD_SELECTED mode, leave the multicast groups. + final Set oldListeningAddresses = + (oldMode == FORWARD_SELECTED) + ? oldConfig.getListeningAddresses() + : new ArraySet<>(); + final Set newListeningAddresses = + (newMode == FORWARD_SELECTED) + ? newConfig.getListeningAddresses() + : new ArraySet<>(); + final CompareResult addressDiff = + new CompareResult<>(oldListeningAddresses, newListeningAddresses); + joinGroups(iifIndex, addressDiff.added); + leaveGroups(iifIndex, addressDiff.removed); + + setMulticastRoutingConfig(iifName, oifName, newConfig); + Log.d( + TAG, + "Applied multicast routing config for iif " + + iifName + + " to oif " + + oifName + + " with Config " + + newConfig); + + // Update existing MFCs to make sure they align with the updated configuration + updateMfcs(); + + if (newConfig.getForwardingMode() == FORWARD_NONE) { + if (!hasActiveMulticastConfig(iifName)) { + removeInterfaceFromMulticastRouting(iifName); + } + if (!hasActiveMulticastConfig(oifName)) { + removeInterfaceFromMulticastRouting(oifName); + } + } + } + + /** + * Removes an network interface from multicast routing. + * + *

Remove the network interface from multicast configs and remove it from the list of + * multicast routing interfaces in the kernel + * + * @param ifName name of the interface that should be removed + */ + @VisibleForTesting + public void removeInterfaceFromMulticastRouting(final String ifName) { + checkOnHandlerThread(); + final Integer virtualIndex = getVirtualInterfaceIndex(ifName); + if (virtualIndex == null) return; + + updateMfcs(); + mInterfaces.removeAt(mInterfaces.indexOfValue(ifName)); + mVirtualInterfaces.remove(virtualIndex); + try { + mDependencies.setsockoptMrt6DelMif(mMulticastRoutingFd, virtualIndex); + Log.d(TAG, "Removed mifi " + virtualIndex + " from MIF"); + } catch (ErrnoException e) { + Log.e(TAG, "failed to remove multicast virtual interface" + virtualIndex, e); + } + } + + private int getNextAvailableVirtualIndex() { + if (mVirtualInterfaces.size() >= MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES) { + throw new IllegalStateException("Can't allocate new multicast virtual interface"); + } + for (int i = 0; i < mVirtualInterfaces.size(); i++) { + if (!mVirtualInterfaces.contains(i)) { + return i; + } + } + return mVirtualInterfaces.size(); + } + + @VisibleForTesting + public Integer getVirtualInterfaceIndex(String ifName) { + int mapIndex = mVirtualInterfaces.indexOfValue(ifName); + if (mapIndex < 0) return null; + return mVirtualInterfaces.keyAt(mapIndex); + } + + private Integer getVirtualInterfaceIndex(int physicalIndex) { + String ifName = mInterfaces.get(physicalIndex); + if (ifName == null) { + // This is only used to match MFCs from kernel to MFCs we know about. + // Unknown MFCs should be ignored. + return null; + } + return getVirtualInterfaceIndex(ifName); + } + + private String getInterfaceName(int virtualIndex) { + return mVirtualInterfaces.get(virtualIndex); + } + + private void maybeAddAndTrackInterface(String ifName) { + checkOnHandlerThread(); + if (mVirtualInterfaces.indexOfValue(ifName) >= 0) return; + + int nextVirtualIndex = getNextAvailableVirtualIndex(); + int ifIndex = mDependencies.getInterfaceIndex(ifName); + final StructMif6ctl mif6ctl = + new StructMif6ctl( + nextVirtualIndex, + (short) 0 /* mif6c_flags */, + (short) 1 /* vifc_threshold */, + ifIndex, + 0 /* vifc_rate_limit */); + try { + mDependencies.setsockoptMrt6AddMif(mMulticastRoutingFd, mif6ctl); + Log.d(TAG, "Added mifi " + nextVirtualIndex + " to MIF"); + } catch (ErrnoException e) { + Log.e(TAG, "failed to add multicast virtual interface", e); + return; + } + mVirtualInterfaces.put(nextVirtualIndex, ifName); + mInterfaces.put(ifIndex, ifName); + } + + @VisibleForTesting + public MulticastRoutingConfig getMulticastRoutingConfig(String iifName, String oifName) { + PerInterfaceMulticastRoutingConfig configs = mMulticastRoutingConfigs.get(iifName); + final MulticastRoutingConfig defaultConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE; + if (configs == null) { + return defaultConfig; + } else { + return configs.oifConfigs.getOrDefault(oifName, defaultConfig); + } + } + + private void setMulticastRoutingConfig( + final String iifName, final String oifName, final MulticastRoutingConfig config) { + checkOnHandlerThread(); + PerInterfaceMulticastRoutingConfig iifConfig = mMulticastRoutingConfigs.get(iifName); + + if (config.getForwardingMode() == FORWARD_NONE) { + if (iifConfig != null) { + iifConfig.oifConfigs.remove(oifName); + } + if (iifConfig.oifConfigs.isEmpty()) { + mMulticastRoutingConfigs.remove(iifName); + } + return; + } + + if (iifConfig == null) { + iifConfig = new PerInterfaceMulticastRoutingConfig(); + mMulticastRoutingConfigs.put(iifName, iifConfig); + } + iifConfig.oifConfigs.put(oifName, config); + } + + /** Returns whether an interface has multicast routing config */ + private boolean hasActiveMulticastConfig(final String ifName) { + // FORWARD_NONE configs are not saved in the config tables, so + // any existing config is an active multicast routing config + if (mMulticastRoutingConfigs.containsKey(ifName)) return true; + for (var pic : mMulticastRoutingConfigs.values()) { + if (pic.oifConfigs.containsKey(ifName)) return true; + } + return false; + } + + /** + * A multicast forwarding cache (MFC) entry holds a multicast forwarding route where packet from + * incoming interface(iif) with source address(S) to group address (G) are forwarded to outgoing + * interfaces(oifs). + * + *

iif, S and G identifies an MFC entry. For example an MFC1 is added: [iif1, S1, G1, oifs1] + * Adding another MFC2 of [iif1, S1, G1, oifs2] to the kernel overwrites MFC1. + */ + private static final class MfcKey { + public final int mIifVirtualIdx; + public final Inet6Address mSrcAddr; + public final Inet6Address mDstAddr; + + public MfcKey(int iif, Inet6Address src, Inet6Address dst) { + mIifVirtualIdx = iif; + mSrcAddr = src; + mDstAddr = dst; + } + + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (!(other instanceof MfcKey)) { + return false; + } else { + MfcKey otherKey = (MfcKey) other; + return mIifVirtualIdx == otherKey.mIifVirtualIdx + && mSrcAddr.equals(otherKey.mSrcAddr) + && mDstAddr.equals(otherKey.mDstAddr); + } + } + + public int hashCode() { + return Objects.hash(mIifVirtualIdx, mSrcAddr, mDstAddr); + } + + public String toString() { + return "{iifVirtualIndex: " + + Integer.toString(mIifVirtualIdx) + + ", sourceAddress: " + + mSrcAddr.toString() + + ", destinationAddress: " + + mDstAddr.toString() + + "}"; + } + } + + private static final class MfcValue { + private Set mOifVirtualIndices; + // timestamp of when the mfc was last used in the kernel + // (e.g. created, or used to forward a packet) + private Instant mLastUsedAt; + + public MfcValue(Set oifs, Instant timestamp) { + mOifVirtualIndices = oifs; + mLastUsedAt = timestamp; + } + + public boolean hasSameOifsAs(MfcValue other) { + return this.mOifVirtualIndices.equals(other.mOifVirtualIndices); + } + + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (!(other instanceof MfcValue)) { + return false; + } else { + MfcValue otherValue = (MfcValue) other; + return mOifVirtualIndices.equals(otherValue.mOifVirtualIndices) + && mLastUsedAt.equals(otherValue.mLastUsedAt); + } + } + + public int hashCode() { + return Objects.hash(mOifVirtualIndices, mLastUsedAt); + } + + public Set getOifIndices() { + return mOifVirtualIndices; + } + + public void setLastUsedAt(Instant timestamp) { + mLastUsedAt = timestamp; + } + + public Instant getLastUsedAt() { + return mLastUsedAt; + } + + public String toString() { + return "{oifVirtualIdxes: " + + mOifVirtualIndices.toString() + + ", lastUsedAt: " + + mLastUsedAt.toString() + + "}"; + } + } + + /** + * Returns the MFC value for the given MFC key according to current multicast routing config. If + * the MFC should be removed return null. + */ + private MfcValue computeMfcValue(int iif, Inet6Address dst) { + final int dstScope = getGroupAddressScope(dst); + Set forwardingOifs = new ArraySet<>(); + + PerInterfaceMulticastRoutingConfig iifConfig = + mMulticastRoutingConfigs.get(getInterfaceName(iif)); + + if (iifConfig == null) { + // An iif may have been removed from multicast routing, in this + // case remove the MFC directly + return null; + } + + for (var config : iifConfig.oifConfigs.entrySet()) { + if ((config.getValue().getForwardingMode() == FORWARD_WITH_MIN_SCOPE + && config.getValue().getMinimumScope() <= dstScope) + || (config.getValue().getForwardingMode() == FORWARD_SELECTED + && config.getValue().getListeningAddresses().contains(dst))) { + forwardingOifs.add(getVirtualInterfaceIndex(config.getKey())); + } + } + + return new MfcValue(forwardingOifs, Instant.now(mDependencies.getClock())); + } + + /** + * Given the iif, source address and group destination address, add an MFC entry or update the + * existing MFC according to the multicast routing config. If such an MFC should not exist, + * return null for caller of the function to remove it. + * + *

Note that if a packet has no matching MFC entry in the kernel, kernel creates an + * unresolved route and notifies multicast socket with a NOCACHE upcall message. The unresolved + * route is kept for no less than 10s. If packets with the same source and destination arrives + * before the 10s timeout, they will not be notified. Thus we need to add a 'blocking' MFC which + * is an MFC with an empty oif list. When the multicast configs changes, the 'blocking' MFC + * will be updated to a 'forwarding' MFC so that corresponding multicast traffic can be + * forwarded instantly. + * + * @return {@code true} if the MFC is updated and no operation is needed from caller. + * {@code false} if the MFC should not be added, caller of the function should remove + * the MFC if needed. + */ + private boolean addOrUpdateMfc(int vif, Inet6Address src, Inet6Address dst) { + checkOnHandlerThread(); + final MfcKey key = new MfcKey(vif, src, dst); + final MfcValue value = mMfcs.get(key); + final MfcValue updatedValue = computeMfcValue(vif, dst); + + if (updatedValue == null) { + return false; + } + + if (value != null && value.hasSameOifsAs(updatedValue)) { + // no updates to make + return true; + } + + final StructMf6cctl mf6cctl = + new StructMf6cctl(src, dst, vif, updatedValue.getOifIndices()); + try { + mDependencies.setsockoptMrt6AddMfc(mMulticastRoutingFd, mf6cctl); + } catch (ErrnoException e) { + Log.e(TAG, "failed to add MFC: " + e); + return false; + } + mMfcs.put(key, updatedValue); + String operation = (value == null ? "Added" : "Updated"); + Log.d(TAG, operation + " MFC key: " + key + " value: " + updatedValue); + return true; + } + + private void checkMfcsExpiration() { + checkOnHandlerThread(); + // Check if there are inactive MFCs that can be removed + refreshMfcInactiveDuration(); + maybeExpireMfcs(); + if (mMfcs.size() > 0) { + mHandler.postDelayed(() -> checkMfcsExpiration(), MFC_INACTIVE_CHECK_INTERVAL_MS); + mMfcPollingScheduled = true; + } else { + mMfcPollingScheduled = false; + } + } + + private void checkMfcEntriesLimit() { + checkOnHandlerThread(); + // If the max number of MFC entries is reached, remove the first MFC entry. This can be + // any entry, as if this entry is needed again there will be a NOCACHE upcall to add it + // back. + if (mMfcs.size() == MFC_MAX_NUMBER_OF_ENTRIES) { + Log.w(TAG, "Reached max number of MFC entries " + MFC_MAX_NUMBER_OF_ENTRIES); + var iter = mMfcs.entrySet().iterator(); + MfcKey firstMfcKey = iter.next().getKey(); + removeMfcFromKernel(firstMfcKey); + iter.remove(); + } + } + + /** + * Reads multicast routes information from the kernel, and update the last used timestamp for + * each multicast route save in this class. + */ + private void refreshMfcInactiveDuration() { + checkOnHandlerThread(); + final List multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes(); + + for (var route : multicastRoutes) { + if (!route.isResolved()) { + continue; // Don't handle unresolved mfc, the kernel will recycle in 10s + } + Integer iif = getVirtualInterfaceIndex(route.getIifIndex()); + if (iif == null) { + Log.e(TAG, "Can't find kernel returned IIF " + route.getIifIndex()); + return; + } + final MfcKey key = + new MfcKey( + iif, + (Inet6Address) route.getSource().getAddress(), + (Inet6Address) route.getDestination().getAddress()); + MfcValue value = mMfcs.get(key); + if (value == null) { + Log.e(TAG, "Can't find kernel returned MFC " + key); + continue; + } + value.setLastUsedAt( + Instant.now(mDependencies.getClock()) + .minusMillis(route.getSinceLastUseMillis())); + } + } + + /** Remove MFC entry from mMfcs map and the kernel if exists. */ + private void removeMfcFromKernel(MfcKey key) { + checkOnHandlerThread(); + + final MfcValue value = mMfcs.get(key); + final Set oifs = new ArraySet<>(); + final StructMf6cctl mf6cctl = + new StructMf6cctl(key.mSrcAddr, key.mDstAddr, key.mIifVirtualIdx, oifs); + try { + mDependencies.setsockoptMrt6DelMfc(mMulticastRoutingFd, mf6cctl); + } catch (ErrnoException e) { + Log.e(TAG, "failed to remove MFC: " + e); + return; + } + Log.d(TAG, "Removed MFC key: " + key + " value: " + value); + } + + /** + * This is called every MFC_INACTIVE_CHECK_INTERVAL_MS milliseconds to remove any MFC that is + * inactive for more than MFC_INACTIVE_TIMEOUT_MS milliseconds. + */ + private void maybeExpireMfcs() { + checkOnHandlerThread(); + + for (var it = mMfcs.entrySet().iterator(); it.hasNext(); ) { + var entry = it.next(); + if (entry.getValue() + .getLastUsedAt() + .plusMillis(MFC_INACTIVE_TIMEOUT_MS) + .isBefore(Instant.now(mDependencies.getClock()))) { + removeMfcFromKernel(entry.getKey()); + it.remove(); + } + } + } + + private void updateMfcs() { + checkOnHandlerThread(); + + for (Iterator> it = mMfcs.entrySet().iterator(); + it.hasNext(); ) { + MfcKey key = it.next().getKey(); + if (!addOrUpdateMfc(key.mIifVirtualIdx, key.mSrcAddr, key.mDstAddr)) { + removeMfcFromKernel(key); + it.remove(); + } + } + + refreshMfcInactiveDuration(); + } + + private void joinGroups(int ifIndex, List addresses) { + for (Inet6Address address : addresses) { + InetSocketAddress socketAddress = new InetSocketAddress(address, 0); + try { + mMulticastSocket.joinGroup( + socketAddress, mDependencies.getNetworkInterface(ifIndex)); + } catch (IOException e) { + if (e.getCause() instanceof ErrnoException) { + ErrnoException ee = (ErrnoException) e.getCause(); + if (ee.errno == EADDRINUSE) { + // The list of added address are calculated from address changes, + // repeated join group is unexpected + Log.e(TAG, "Already joined group" + e); + continue; + } + } + Log.e(TAG, "failed to join group: " + e); + } + } + } + + private void leaveGroups(int ifIndex, List addresses) { + for (Inet6Address address : addresses) { + InetSocketAddress socketAddress = new InetSocketAddress(address, 0); + try { + mMulticastSocket.leaveGroup( + socketAddress, mDependencies.getNetworkInterface(ifIndex)); + } catch (IOException e) { + Log.e(TAG, "failed to leave group: " + e); + } + } + } + + private int getGroupAddressScope(Inet6Address address) { + return address.getAddress()[1] & 0xf; + } + + /** + * Handles a NoCache upcall that indicates a multicast packet is received and requires + * a multicast forwarding cache to be added. + * + * A forwarding or blocking MFC is added according to the multicast config. + * + * The number of MFCs is checked to make sure it doesn't exceed the + * {@code MFC_MAX_NUMBER_OF_ENTRIES} limit. + */ + @VisibleForTesting + public void handleMulticastNocacheUpcall(final StructMrt6Msg mrt6Msg) { + final int iifVid = mrt6Msg.mif; + + // add MFC to forward the packet or add blocking MFC to not forward the packet + // If the packet comes from an interface the service doesn't care about, the + // addOrUpdateMfc function will return null and not MFC will be added. + if (!addOrUpdateMfc(iifVid, mrt6Msg.src, mrt6Msg.dst)) return; + // If the list of MFCs is not empty and there is no MFC check scheduled, + // schedule one now + if (!mMfcPollingScheduled) { + mHandler.postDelayed(() -> checkMfcsExpiration(), MFC_INACTIVE_CHECK_INTERVAL_MS); + mMfcPollingScheduled = true; + } + + checkMfcEntriesLimit(); + } + + /** + * A packet reader that handles the packets sent to the multicast routing socket + */ + private final class MulticastNocacheUpcallListener extends PacketReader { + private final FileDescriptor mFd; + + public MulticastNocacheUpcallListener(Handler h, FileDescriptor fd) { + super(h); + mFd = fd; + } + + @Override + protected FileDescriptor createFd() { + return mFd; + } + + @Override + protected void handlePacket(byte[] recvbuf, int length) { + final ByteBuffer buf = ByteBuffer.wrap(recvbuf); + final StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf); + if (mrt6Msg.msgType != StructMrt6Msg.MRT6MSG_NOCACHE) { + return; + } + handleMulticastNocacheUpcall(mrt6Msg); + } + } + + /** Dependencies of RoutingCoordinatorService, for test injections. */ + @VisibleForTesting + public static class Dependencies { + private final Clock mClock = Clock.system(ZoneId.systemDefault()); + + /** + * Creates a socket to configure multicast routing in the kernel. + * + *

If the kernel doesn't support multicast routing, then the {@code setsockoptInt} with + * {@code MRT6_INIT} method would fail. + * + * @return the multicast routing socket, or null if it fails to be created/configured. + */ + public FileDescriptor createMulticastRoutingSocket() { + FileDescriptor sock = null; + byte[] filter = new byte[32]; // filter all ICMPv6 messages + try { + sock = Os.socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_ICMPV6); + Os.setsockoptInt(sock, IPPROTO_IPV6, MRT6_INIT, ONE); + NetworkUtils.setsockoptBytes(sock, IPPROTO_ICMPV6, ICMP6_FILTER, filter); + } catch (ErrnoException e) { + Log.e(TAG, "failed to create multicast socket: " + e); + if (sock != null) { + SocketUtils.closeSocketQuietly(sock); + } + throw new UnsupportedOperationException("Multicast routing is not supported ", e); + } + Log.i(TAG, "socket created for multicast routing: " + sock); + return sock; + } + + public MulticastSocket createMulticastSocket() { + try { + return new MulticastSocket(); + } catch (IOException e) { + Log.wtf(TAG, "Failed to create multicast socket " + e); + throw new IllegalStateException(e); + } + } + + public void setsockoptMrt6AddMif(FileDescriptor fd, StructMif6ctl mif6ctl) + throws ErrnoException { + final byte[] bytes = mif6ctl.writeToBytes(); + NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_ADD_MIF, bytes); + } + + public void setsockoptMrt6DelMif(FileDescriptor fd, int virtualIfIndex) + throws ErrnoException { + Os.setsockoptInt(fd, IPPROTO_IPV6, MRT6_DEL_MIF, virtualIfIndex); + } + + public void setsockoptMrt6AddMfc(FileDescriptor fd, StructMf6cctl mf6cctl) + throws ErrnoException { + final byte[] bytes = mf6cctl.writeToBytes(); + NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_ADD_MFC, bytes); + } + + public void setsockoptMrt6DelMfc(FileDescriptor fd, StructMf6cctl mf6cctl) + throws ErrnoException { + final byte[] bytes = mf6cctl.writeToBytes(); + NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_DEL_MFC, bytes); + } + + public Integer getInterfaceIndex(String ifName) { + try { + NetworkInterface ni = NetworkInterface.getByName(ifName); + return ni.getIndex(); + } catch (NullPointerException | SocketException e) { + return null; + } + } + + public NetworkInterface getNetworkInterface(int physicalIndex) { + try { + return NetworkInterface.getByIndex(physicalIndex); + } catch (SocketException e) { + return null; + } + } + + public Clock getClock() { + return mClock; + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt new file mode 100644 index 0000000000..6c2c2560c4 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2023 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.net.MulticastRoutingConfig +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.ParcelFileDescriptor +import android.os.SystemClock +import android.os.test.TestLooper +import android.system.Os +import android.system.OsConstants.AF_INET6 +import android.system.OsConstants.IPPROTO_UDP +import android.system.OsConstants.SOCK_DGRAM +import android.util.Log +import androidx.test.filters.LargeTest +import com.android.net.module.util.structs.StructMf6cctl +import com.android.net.module.util.structs.StructMrt6Msg +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.tryTest +import com.google.common.truth.Truth.assertThat +import java.io.FileDescriptor +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress +import java.net.Inet6Address +import java.net.InetSocketAddress +import java.net.MulticastSocket +import java.net.NetworkInterface +import java.time.Clock +import java.time.Instant +import java.time.ZoneId +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.test.assertTrue +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.timeout +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +private const val TIMEOUT_MS = 2_000L + +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) +class MulticastRoutingCoordinatorServiceTest { + + // mocks are lateinit as they need to be setup between tests + @Mock private lateinit var mDeps: MulticastRoutingCoordinatorService.Dependencies + @Mock private lateinit var mMulticastSocket: MulticastSocket + + val mSock = DatagramSocket() + val mPfd = ParcelFileDescriptor.fromDatagramSocket(mSock) + val mFd = mPfd.getFileDescriptor() + val mIfName1 = "interface1" + val mIfName2 = "interface2" + val mIfName3 = "interface3" + val mIfPhysicalIndex1 = 10 + val mIfPhysicalIndex2 = 11 + val mIfPhysicalIndex3 = 12 + val mSourceAddress = Inet6Address.getByName("2000::8888") as Inet6Address + val mGroupAddressScope5 = Inet6Address.getByName("ff05::1234") as Inet6Address + val mGroupAddressScope4 = Inet6Address.getByName("ff04::1234") as Inet6Address + val mGroupAddressScope3 = Inet6Address.getByName("ff03::1234") as Inet6Address + val mSocketAddressScope5 = InetSocketAddress(mGroupAddressScope5, 0) + val mSocketAddressScope4 = InetSocketAddress(mGroupAddressScope4, 0) + val mEmptyOifs = setOf() + val mClock = FakeClock() + val mNetworkInterface1 = createEmptyNetworkInterface() + val mNetworkInterface2 = createEmptyNetworkInterface() + // MulticastRoutingCoordinatorService needs to be initialized after the dependencies + // are mocked. + lateinit var mService: MulticastRoutingCoordinatorService + lateinit var mLooper: TestLooper + + class FakeClock() : Clock() { + private var offsetMs = 0L + + fun fastForward(ms: Long) { + offsetMs += ms + } + + override fun instant(): Instant { + return Instant.now().plusMillis(offsetMs) + } + + override fun getZone(): ZoneId { + throw RuntimeException("Not implemented"); + } + + override fun withZone(zone: ZoneId): Clock { + throw RuntimeException("Not implemented"); + } + + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + doReturn(mClock).`when`(mDeps).getClock() + doReturn(mFd).`when`(mDeps).createMulticastRoutingSocket() + doReturn(mMulticastSocket).`when`(mDeps).createMulticastSocket() + doReturn(mIfPhysicalIndex1).`when`(mDeps).getInterfaceIndex(mIfName1) + doReturn(mIfPhysicalIndex2).`when`(mDeps).getInterfaceIndex(mIfName2) + doReturn(mIfPhysicalIndex3).`when`(mDeps).getInterfaceIndex(mIfName3) + doReturn(mNetworkInterface1).`when`(mDeps).getNetworkInterface(mIfPhysicalIndex1) + doReturn(mNetworkInterface2).`when`(mDeps).getNetworkInterface(mIfPhysicalIndex2) + } + + @After + fun tearDown() { + mSock.close() + } + + // Functions under @Before and @Test run in different threads, + // (i.e. androidx.test.runner.AndroidJUnitRunner vs Time-limited test) + // MulticastRoutingCoordinatorService requires the jobs are run on the thread looper, + // so TestLooper needs to be created inside each test case to install the + // correct looper. + fun prepareService() { + mLooper = TestLooper() + val handler = Handler(mLooper.getLooper()) + + mService = MulticastRoutingCoordinatorService(handler, mDeps) + } + + private fun createEmptyNetworkInterface(): NetworkInterface { + val constructor = NetworkInterface::class.java.getDeclaredConstructor() + constructor.isAccessible = true + return constructor.newInstance() + } + + private fun createStructMf6cctl(src: Inet6Address, dst: Inet6Address, iifIdx: Int, + oifSet: Set): StructMf6cctl { + return StructMf6cctl(src, dst, iifIdx, oifSet) + } + + // Send a MRT6MSG_NOCACHE packet to sock, to indicate a packet has arrived without matching MulticastRoutingCache + private fun sendMrt6msgNocachePacket(interfaceVirtualIndex: Int, + source: Inet6Address, destination: Inet6Address) { + mLooper.dispatchAll() // let MulticastRoutingCoordinatorService handle all msgs first to + // apply any possible multicast routing config changes + val mrt6Msg = StructMrt6Msg(0 /* mbz must be 0 */, StructMrt6Msg.MRT6MSG_NOCACHE, + interfaceVirtualIndex, source, destination) + mLooper.getNewExecutor().execute({ mService.handleMulticastNocacheUpcall(mrt6Msg) }) + mLooper.dispatchAll() + } + + private fun applyMulticastForwardNone(fromIf: String, toIf: String) { + val configNone = MulticastRoutingConfig.CONFIG_FORWARD_NONE + + mService.applyMulticastRoutingConfig(fromIf, toIf, configNone) + } + + private fun applyMulticastForwardMinimumScope(fromIf: String, toIf: String, minScope: Int) { + val configMinimumScope = MulticastRoutingConfig.Builder( + MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, minScope).build() + + mService.applyMulticastRoutingConfig(fromIf, toIf, configMinimumScope) + } + + private fun applyMulticastForwardSelected(fromIf: String, toIf: String) { + val configSelected = MulticastRoutingConfig.Builder( + MulticastRoutingConfig.FORWARD_SELECTED) + .addListeningAddress(mGroupAddressScope5).build() + + mService.applyMulticastRoutingConfig(fromIf, toIf, configSelected) + } + + @Test + fun testConstructor_multicastRoutingSocketIsCreated() { + prepareService() + verify(mDeps).createMulticastRoutingSocket() + } + + @Test + fun testMulticastRouting_applyForwardNone() { + prepareService() + + applyMulticastForwardNone(mIfName1, mIfName2) + mLooper.dispatchAll() + + // Both interfaces are not added as multicast routing interfaces + verify(mDeps, never()).setsockoptMrt6AddMif(eq(mFd), any()) + // No MFC should be added for FORWARD_NONE + verify(mDeps, never()).setsockoptMrt6AddMfc(eq(mFd), any()) + assertEquals(MulticastRoutingConfig.CONFIG_FORWARD_NONE, + mService.getMulticastRoutingConfig(mIfName1, mIfName2)); + } + + @Test + fun testMulticastRouting_applyForwardMinimumScope() { + prepareService() + + applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */) + mLooper.dispatchAll() + + // No MFC is added for FORWARD_WITH_MIN_SCOPE + verify(mDeps, never()).setsockoptMrt6AddMfc(eq(mFd), any()) + assertEquals(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, + mService.getMulticastRoutingConfig(mIfName1, mIfName2).getForwardingMode()) + assertEquals(4, mService.getMulticastRoutingConfig(mIfName1, mIfName2).getMinimumScope()) + } + + @Test + fun testMulticastRouting_addressScopelargerThanMinScope_allowMfcIsAdded() { + prepareService() + applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */) + mLooper.dispatchAll() + val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2)) + val mf6cctl = createStructMf6cctl(mSourceAddress, mGroupAddressScope5, + mService.getVirtualInterfaceIndex(mIfName1), oifs) + + // simulate a MRT6MSG_NOCACHE upcall for a packet sent to group address of scope 5 + sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5) + + // an MFC is added for the packet + verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctl)) + } + + @Test + fun testMulticastRouting_addressScopeSmallerThanMinScope_blockingMfcIsAdded() { + prepareService() + applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4) + val mf6cctl = createStructMf6cctl(mSourceAddress, mGroupAddressScope3, + mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs) + + // simulate a MRT6MSG_NOCACHE upcall when a packet should not be forwarded + sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope3) + + // a blocking MFC is added + verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctl)) + } + + @Test + fun testMulticastRouting_applyForwardSelected_joinsGroup() { + prepareService() + + applyMulticastForwardSelected(mIfName1, mIfName2) + mLooper.dispatchAll() + + verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1)) + assertEquals(MulticastRoutingConfig.FORWARD_SELECTED, + mService.getMulticastRoutingConfig(mIfName1, mIfName2).getForwardingMode()) + } + + @Test + fun testMulticastRouting_addListeningAddressInForwardSelected_joinsGroup() { + prepareService() + + val configSelectedNoAddress = MulticastRoutingConfig.Builder( + MulticastRoutingConfig.FORWARD_SELECTED).build() + mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedNoAddress) + mLooper.dispatchAll() + + val configSelectedWithAddresses = MulticastRoutingConfig.Builder( + MulticastRoutingConfig.FORWARD_SELECTED) + .addListeningAddress(mGroupAddressScope5) + .addListeningAddress(mGroupAddressScope4) + .build() + mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWithAddresses) + mLooper.dispatchAll() + + verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1)) + verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1)) + } + + @Test + fun testMulticastRouting_removeListeningAddressInForwardSelected_leavesGroup() { + prepareService() + val configSelectedWith2Addresses = MulticastRoutingConfig.Builder( + MulticastRoutingConfig.FORWARD_SELECTED) + .addListeningAddress(mGroupAddressScope5) + .addListeningAddress(mGroupAddressScope4) + .build() + mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWith2Addresses) + mLooper.dispatchAll() + + verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1)) + verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1)) + + // remove the scope4 address + val configSelectedWith1Address = MulticastRoutingConfig.Builder( + MulticastRoutingConfig.FORWARD_SELECTED) + .addListeningAddress(mGroupAddressScope5) + .build() + mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWith1Address) + mLooper.dispatchAll() + + verify(mMulticastSocket).leaveGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1)) + verify(mMulticastSocket, never()) + .leaveGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1)) + } + + @Test + fun testMulticastRouting_fromForwardSelectedToForwardNone_leavesGroup() { + prepareService() + applyMulticastForwardSelected(mIfName1, mIfName2) + mLooper.dispatchAll() + + verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1)) + + applyMulticastForwardNone(mIfName1, mIfName2) + mLooper.dispatchAll() + + verify(mMulticastSocket).leaveGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1)) + assertEquals(MulticastRoutingConfig.CONFIG_FORWARD_NONE, + mService.getMulticastRoutingConfig(mIfName1, mIfName2)); + } + + @Test + fun testMulticastRouting_fromFowardSelectedToForwardNone_removesMulticastInterfaces() { + prepareService() + + applyMulticastForwardSelected(mIfName1, mIfName2) + applyMulticastForwardSelected(mIfName1, mIfName3) + mLooper.dispatchAll() + + assertNotNull(mService.getVirtualInterfaceIndex(mIfName1)) + assertNotNull(mService.getVirtualInterfaceIndex(mIfName2)) + assertNotNull(mService.getVirtualInterfaceIndex(mIfName3)) + + applyMulticastForwardNone(mIfName1, mIfName2) + mLooper.dispatchAll() + + assertNotNull(mService.getVirtualInterfaceIndex(mIfName1)) + assertNull(mService.getVirtualInterfaceIndex(mIfName2)) + assertNotNull(mService.getVirtualInterfaceIndex(mIfName3)) + } + + @Test + fun testMulticastRouting_addMulticastRoutingInterfaces() { + prepareService() + + applyMulticastForwardSelected(mIfName1, mIfName2) + mLooper.dispatchAll() + + assertNotNull(mService.getVirtualInterfaceIndex(mIfName1)) + assertNotNull(mService.getVirtualInterfaceIndex(mIfName2)) + assertNotEquals(mService.getVirtualInterfaceIndex(mIfName1), + mService.getVirtualInterfaceIndex(mIfName2)) + } + + @Test + fun testMulticastRouting_removeMulticastRoutingInterfaces() { + prepareService() + + applyMulticastForwardSelected(mIfName1, mIfName2) + mService.removeInterfaceFromMulticastRouting(mIfName1) + mLooper.dispatchAll() + + assertNull(mService.getVirtualInterfaceIndex(mIfName1)) + assertNotNull(mService.getVirtualInterfaceIndex(mIfName2)) + } + + @Test + fun testMulticastRouting_applyConfigNone_removesMfc() { + prepareService() + + applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */) + applyMulticastForwardSelected(mIfName1, mIfName3) + + sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5) + val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2), + mService.getVirtualInterfaceIndex(mIfName3)) + val oifsUpdate = setOf(mService.getVirtualInterfaceIndex(mIfName3)) + val mf6cctlAdd = createStructMf6cctl(mSourceAddress, mGroupAddressScope5, + mService.getVirtualInterfaceIndex(mIfName1), oifs) + val mf6cctlUpdate = createStructMf6cctl(mSourceAddress, mGroupAddressScope5, + mService.getVirtualInterfaceIndex(mIfName1), oifsUpdate) + val mf6cctlDel = createStructMf6cctl(mSourceAddress, mGroupAddressScope5, + mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs) + + verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd)) + + applyMulticastForwardNone(mIfName1, mIfName2) + mLooper.dispatchAll() + + verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlUpdate)) + + applyMulticastForwardNone(mIfName1, mIfName3) + mLooper.dispatchAll() + + verify(mDeps, timeout(TIMEOUT_MS).times(1)).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel)) + } + + @Test + @LargeTest + fun testMulticastRouting_maxNumberOfMfcs() { + prepareService() + + // add MFC_MAX_NUMBER_OF_ENTRIES MFCs + applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */) + for (i in 1..MulticastRoutingCoordinatorService.MFC_MAX_NUMBER_OF_ENTRIES) { + val groupAddress = + Inet6Address.getByName("ff05::" + Integer.toHexString(i)) as Inet6Address + sendMrt6msgNocachePacket(0, mSourceAddress, groupAddress) + } + val mf6cctlDel = createStructMf6cctl(mSourceAddress, + Inet6Address.getByName("ff05::1" ) as Inet6Address, + mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs) + + verify(mDeps, times(MulticastRoutingCoordinatorService.MFC_MAX_NUMBER_OF_ENTRIES)). + setsockoptMrt6AddMfc(eq(mFd), any()) + // when number of mfcs reaches the max value, one mfc should be removed + verify(mDeps).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel)) + } + + @Test + fun testMulticastRouting_interfaceWithoutActiveConfig_isRemoved() { + prepareService() + applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */) + mLooper.dispatchAll() + val virtualIndexIf1 = mService.getVirtualInterfaceIndex(mIfName1) + val virtualIndexIf2 = mService.getVirtualInterfaceIndex(mIfName2) + + applyMulticastForwardNone(mIfName1, mIfName2) + mLooper.dispatchAll() + + verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf1)) + verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf2)) + } + + @Test + fun testMulticastRouting_interfaceWithActiveConfig_isNotRemoved() { + prepareService() + applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */) + applyMulticastForwardMinimumScope(mIfName2, mIfName3, 4 /* minScope */) + mLooper.dispatchAll() + val virtualIndexIf1 = mService.getVirtualInterfaceIndex(mIfName1) + val virtualIndexIf2 = mService.getVirtualInterfaceIndex(mIfName2) + val virtualIndexIf3 = mService.getVirtualInterfaceIndex(mIfName3) + + applyMulticastForwardNone(mIfName1, mIfName2) + mLooper.dispatchAll() + + verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf1)) + verify(mDeps, never()).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf2)) + verify(mDeps, never()).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf3)) + } + + @Test + fun testMulticastRouting_unusedMfc_isRemovedAfterTimeout() { + prepareService() + applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */) + sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5) + val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2)) + val mf6cctlAdd = createStructMf6cctl(mSourceAddress, mGroupAddressScope5, + mService.getVirtualInterfaceIndex(mIfName1), oifs) + val mf6cctlDel = createStructMf6cctl(mSourceAddress, mGroupAddressScope5, + mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs) + + // An MFC is added + verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd)) + + repeat(MulticastRoutingCoordinatorService.MFC_INACTIVE_TIMEOUT_MS / + MulticastRoutingCoordinatorService.MFC_INACTIVE_CHECK_INTERVAL_MS + 1) { + mClock.fastForward(MulticastRoutingCoordinatorService + .MFC_INACTIVE_CHECK_INTERVAL_MS.toLong()) + mLooper.moveTimeForward(MulticastRoutingCoordinatorService + .MFC_INACTIVE_CHECK_INTERVAL_MS.toLong()) + mLooper.dispatchAll(); + } + + verify(mDeps).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel)) + } +}