Support multicast routing
Added support to apply multicast routing config between network
interfaces in MulticastRoutingCoordinatorService.
Bug: 281217735
Test: atest FrameworksNetTests:android.net.connectivity.com.android.server.connectivity.MulticastRoutingCoordinatorServiceTest
Change-Id: If608b79d4e62a20d2e01755a83a901f6e274d9a2
diff --git a/framework/Android.bp b/framework/Android.bp
index f3d8689..432cabf 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -95,6 +95,7 @@
],
impl_only_static_libs: [
"net-utils-device-common-bpf",
+ "net-utils-device-common-struct",
],
libs: [
"androidx.annotation_annotation",
@@ -126,6 +127,7 @@
// 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 0000000..4d5001b
--- /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.
+ *
+ * <p>Supports IPv6 multicast routing.
+ *
+ * <p>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<String> mVirtualInterfaces =
+ new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES);
+ /** Mapping from physical interface index to interface name */
+ private SparseArray<String> mInterfaces =
+ new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES);
+
+ /** Mapping of iif to PerInterfaceMulticastRoutingConfig */
+ private Map<String, PerInterfaceMulticastRoutingConfig> mMulticastRoutingConfigs =
+ new HashMap<String, PerInterfaceMulticastRoutingConfig>();
+
+ private static final class PerInterfaceMulticastRoutingConfig {
+ // mapping of oif name to MulticastRoutingConfig
+ public Map<String, MulticastRoutingConfig> oifConfigs =
+ new HashMap<String, MulticastRoutingConfig>();
+ }
+
+ /** 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<MfcKey, MfcValue> 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<Inet6Address> oldListeningAddresses =
+ (oldMode == FORWARD_SELECTED)
+ ? oldConfig.getListeningAddresses()
+ : new ArraySet<>();
+ final Set<Inet6Address> newListeningAddresses =
+ (newMode == FORWARD_SELECTED)
+ ? newConfig.getListeningAddresses()
+ : new ArraySet<>();
+ final CompareResult<Inet6Address> 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.
+ *
+ * <p>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).
+ *
+ * <p>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<Integer> 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<Integer> 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<Integer> 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<Integer> 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.
+ *
+ * <p>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<RtNetlinkRouteMessage> 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<Integer> 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<Map.Entry<MfcKey, MfcValue>> 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<Inet6Address> 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<Inet6Address> 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.
+ *
+ * <p>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 0000000..6c2c256
--- /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<Int>()
+ 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<Int>): 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))
+ }
+}