Merge "Support multicast routing" into main am: bcc245d5bf
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2817454 Change-Id: I29d305ff556152543d878f5ec0fc782088a73ea1 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
commit
1a9cdebc72
3 changed files with 1321 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue