diff --git a/service/Android.bp b/service/Android.bp index 9371b028ad..98bbbacb3e 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -203,6 +203,7 @@ java_library { libs: [ "framework-annotations-lib", "framework-connectivity-pre-jarjar", + "framework-tethering", "framework-wifi", "service-connectivity-pre-jarjar", ], diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java index 2b99d0a924..1623669435 100644 --- a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java +++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java @@ -16,6 +16,8 @@ package com.android.server.connectivity.mdns; +import android.net.Network; + /** Interface for monitoring connectivity changes. */ public interface ConnectivityMonitor { /** @@ -29,6 +31,9 @@ public interface ConnectivityMonitor { void notifyConnectivityChange(); + /** Get available network which is received from connectivity change. */ + Network getAvailableNetwork(); + /** Listener interface for receiving connectivity changes. */ interface Listener { void onConnectivityChanged(); diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java index 3563d6118c..551e3db04c 100644 --- a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java +++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java @@ -16,6 +16,7 @@ package com.android.server.connectivity.mdns; +import android.annotation.Nullable; import android.annotation.TargetApi; import android.content.Context; import android.net.ConnectivityManager; @@ -37,6 +38,7 @@ public class ConnectivityMonitorWithConnectivityManager implements ConnectivityM // TODO(b/71901993): Ideally we shouldn't need this flag. However we still don't have clues why // the receiver is unregistered twice yet. private boolean isCallbackRegistered = false; + private Network lastAvailableNetwork = null; @SuppressWarnings({"nullness:assignment", "nullness:method.invocation"}) @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -50,6 +52,7 @@ public class ConnectivityMonitorWithConnectivityManager implements ConnectivityM @Override public void onAvailable(Network network) { LOGGER.log("network available."); + lastAvailableNetwork = network; notifyConnectivityChange(); } @@ -103,4 +106,10 @@ public class ConnectivityMonitorWithConnectivityManager implements ConnectivityM connectivityManager.unregisterNetworkCallback(networkCallback); isCallbackRegistered = false; } + + @Override + @Nullable + public Network getAvailableNetwork() { + return lastAvailableNetwork; + } } \ No newline at end of file diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java index 1faa6ceefd..0f3c23ad96 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.connectivity.mdns.util.MdnsLogger; @@ -34,7 +35,8 @@ import java.util.Map; * notify them when a mDNS service instance is found, updated, or removed? */ public class MdnsDiscoveryManager implements MdnsSocketClient.Callback { - + private static final String TAG = MdnsDiscoveryManager.class.getSimpleName(); + public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private static final MdnsLogger LOGGER = new MdnsLogger("MdnsDiscoveryManager"); private final ExecutorProvider executorProvider; diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java new file mode 100644 index 0000000000..6090415394 --- /dev/null +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2022 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.mdns; + +import static com.android.server.connectivity.mdns.MdnsSocket.MULTICAST_IPV4_ADDRESS; +import static com.android.server.connectivity.mdns.MdnsSocket.MULTICAST_IPV6_ADDRESS; + +import android.annotation.NonNull; +import android.net.LinkAddress; +import android.net.util.SocketUtils; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.util.Log; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.util.List; + +/** + * {@link MdnsInterfaceSocket} provides a similar interface to {@link MulticastSocket} and binds to + * an available multicast network interfaces. + * + *

This isn't thread safe and should be always called on the same thread unless specified + * otherwise. + * + * @see MulticastSocket for javadoc of each public method. + */ +public class MdnsInterfaceSocket { + private static final String TAG = MdnsInterfaceSocket.class.getSimpleName(); + @NonNull private final MulticastSocket mMulticastSocket; + @NonNull private final NetworkInterface mNetworkInterface; + private boolean mJoinedIpv4 = false; + private boolean mJoinedIpv6 = false; + + public MdnsInterfaceSocket(@NonNull NetworkInterface networkInterface, int port) + throws IOException { + mNetworkInterface = networkInterface; + mMulticastSocket = new MulticastSocket(port); + // RFC Spec: https://tools.ietf.org/html/rfc6762. Time to live is set 255 + mMulticastSocket.setTimeToLive(255); + mMulticastSocket.setNetworkInterface(networkInterface); + + // Bind socket to the interface for receiving from that interface only. + try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(mMulticastSocket)) { + SocketUtils.bindSocketToInterface(pfd.getFileDescriptor(), mNetworkInterface.getName()); + } catch (ErrnoException e) { + throw new IOException("Error setting socket options", e); + } + } + + /** + * Sends a datagram packet from this socket. + * + *

This method could be used on any thread. + */ + public void send(@NonNull DatagramPacket packet) throws IOException { + mMulticastSocket.send(packet); + } + + /** + * Receives a datagram packet from this socket. + * + *

This method could be used on any thread. + */ + public void receive(@NonNull DatagramPacket packet) throws IOException { + mMulticastSocket.receive(packet); + } + + private boolean hasIpv4Address(List addresses) { + for (LinkAddress address : addresses) { + if (address.isIpv4()) return true; + } + return false; + } + + private boolean hasIpv6Address(List addresses) { + for (LinkAddress address : addresses) { + if (address.isIpv6()) return true; + } + return false; + } + + /*** Joins both IPv4 and IPv6 multicast groups. */ + public void joinGroup(@NonNull List addresses) { + maybeJoinIpv4(addresses); + maybeJoinIpv6(addresses); + } + + private boolean joinGroup(InetSocketAddress multicastAddress) { + try { + mMulticastSocket.joinGroup(multicastAddress, mNetworkInterface); + return true; + } catch (IOException e) { + // The address may have just been removed + Log.e(TAG, "Error joining multicast group for " + mNetworkInterface, e); + return false; + } + } + + private void maybeJoinIpv4(List addresses) { + final boolean hasAddr = hasIpv4Address(addresses); + if (!mJoinedIpv4 && hasAddr) { + mJoinedIpv4 = joinGroup(MULTICAST_IPV4_ADDRESS); + } else if (!hasAddr) { + // Lost IPv4 address + mJoinedIpv4 = false; + } + } + + private void maybeJoinIpv6(List addresses) { + final boolean hasAddr = hasIpv6Address(addresses); + if (!mJoinedIpv6 && hasAddr) { + mJoinedIpv6 = joinGroup(MULTICAST_IPV6_ADDRESS); + } else if (!hasAddr) { + // Lost IPv6 address + mJoinedIpv6 = false; + } + } + + /*** Destroy this socket by leaving all joined multicast groups and closing this socket. */ + public void destroy() { + if (mJoinedIpv4) { + try { + mMulticastSocket.leaveGroup(MULTICAST_IPV4_ADDRESS, mNetworkInterface); + } catch (IOException e) { + Log.e(TAG, "Error leaving IPv4 group for " + mNetworkInterface, e); + } + } + if (mJoinedIpv6) { + try { + mMulticastSocket.leaveGroup(MULTICAST_IPV6_ADDRESS, mNetworkInterface); + } catch (IOException e) { + Log.e(TAG, "Error leaving IPv4 group for " + mNetworkInterface, e); + } + } + mMulticastSocket.close(); + } + + /** + * Returns the index of the network interface that this socket is bound to. If the interface + * cannot be determined, returns -1. + * + *

This method could be used on any thread. + */ + public int getInterfaceIndex() { + return mNetworkInterface.getIndex(); + } + + /*** Returns whether this socket has joined IPv4 group */ + public boolean hasJoinedIpv4() { + return mJoinedIpv4; + } + + /*** Returns whether this socket has joined IPv6 group */ + public boolean hasJoinedIpv6() { + return mJoinedIpv6; + } +} diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java index 623168c58a..3a4197867e 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java @@ -17,6 +17,7 @@ package com.android.server.connectivity.mdns; import android.annotation.Nullable; +import android.net.Network; import com.android.internal.annotations.VisibleForTesting; @@ -35,13 +36,16 @@ public class MdnsResponse { private MdnsInetAddressRecord inet4AddressRecord; private MdnsInetAddressRecord inet6AddressRecord; private long lastUpdateTime; - private int interfaceIndex = MdnsSocket.INTERFACE_INDEX_UNSPECIFIED; + private final int interfaceIndex; + @Nullable private final Network network; /** Constructs a new, empty response. */ - public MdnsResponse(long now) { + public MdnsResponse(long now, int interfaceIndex, @Nullable Network network) { lastUpdateTime = now; records = new LinkedList<>(); pointerRecords = new LinkedList<>(); + this.interfaceIndex = interfaceIndex; + this.network = network; } // This generic typed helper compares records for equality. @@ -207,22 +211,22 @@ public class MdnsResponse { return true; } - /** - * Updates the index of the network interface at which this response was received. Can be set to - * {@link MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if unset. - */ - public synchronized void setInterfaceIndex(int interfaceIndex) { - this.interfaceIndex = interfaceIndex; - } - /** * Returns the index of the network interface at which this response was received. Can be set to * {@link MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if unset. */ - public synchronized int getInterfaceIndex() { + public int getInterfaceIndex() { return interfaceIndex; } + /** + * Returns the network at which this response was received, or null if the network is unknown. + */ + @Nullable + public Network getNetwork() { + return network; + } + /** Gets the IPv6 address record. */ public synchronized MdnsInetAddressRecord getInet6AddressRecord() { return inet6AddressRecord; diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java index 6c2bc19545..7cf84f6dbf 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java @@ -18,6 +18,7 @@ package com.android.server.connectivity.mdns; import android.annotation.NonNull; import android.annotation.Nullable; +import android.net.Network; import android.os.SystemClock; import com.android.server.connectivity.mdns.util.MdnsLogger; @@ -95,10 +96,11 @@ public class MdnsResponseDecoder { * @param packet The packet to read from. * @param interfaceIndex the network interface index (or {@link * MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if not known) at which the packet was received + * @param network the network at which the packet was received, or null if it is unknown. * @return A list of mDNS responses, or null if the packet contained no appropriate responses. */ public int decode(@NonNull DatagramPacket packet, @NonNull List responses, - int interfaceIndex) { + int interfaceIndex, @Nullable Network network) { MdnsPacketReader reader = new MdnsPacketReader(packet); List records; @@ -253,12 +255,11 @@ public class MdnsResponseDecoder { MdnsResponse response = findResponseWithPointer(responses, pointerRecord.getPointer()); if (response == null) { - response = new MdnsResponse(now); + response = new MdnsResponse(now, interfaceIndex, network); responses.add(response); } // Set interface index earlier because some responses have PTR record only. // Need to know every response is getting from which interface. - response.setInterfaceIndex(interfaceIndex); response.addPointerRecord((MdnsPointerRecord) record); } } diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java index 195bc8e0b4..583c4a9861 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java @@ -17,6 +17,8 @@ package com.android.server.connectivity.mdns; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.Network; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -43,7 +45,8 @@ public class MdnsSearchOptions implements Parcelable { @Override public MdnsSearchOptions createFromParcel(Parcel source) { return new MdnsSearchOptions(source.createStringArrayList(), - source.readBoolean(), source.readBoolean()); + source.readBoolean(), source.readBoolean(), + source.readParcelable(null)); } @Override @@ -56,15 +59,19 @@ public class MdnsSearchOptions implements Parcelable { private final boolean isPassiveMode; private final boolean removeExpiredService; + // The target network for searching. Null network means search on all possible interfaces. + @Nullable private final Network mNetwork; - /** Parcelable constructs for a {@link MdnsServiceInfo}. */ - MdnsSearchOptions(List subtypes, boolean isPassiveMode, boolean removeExpiredService) { + /** Parcelable constructs for a {@link MdnsSearchOptions}. */ + MdnsSearchOptions(List subtypes, boolean isPassiveMode, boolean removeExpiredService, + @Nullable Network network) { this.subtypes = new ArrayList<>(); if (subtypes != null) { this.subtypes.addAll(subtypes); } this.isPassiveMode = isPassiveMode; this.removeExpiredService = removeExpiredService; + mNetwork = network; } /** Returns a {@link Builder} for {@link MdnsSearchOptions}. */ @@ -98,6 +105,16 @@ public class MdnsSearchOptions implements Parcelable { return removeExpiredService; } + /** + * Returns the network which the mdns query should target on. + * + * @return the target network or null if search on all possible interfaces. + */ + @Nullable + public Network getNetwork() { + return mNetwork; + } + @Override public int describeContents() { return 0; @@ -108,6 +125,7 @@ public class MdnsSearchOptions implements Parcelable { out.writeStringList(subtypes); out.writeBoolean(isPassiveMode); out.writeBoolean(removeExpiredService); + out.writeParcelable(mNetwork, 0); } /** A builder to create {@link MdnsSearchOptions}. */ @@ -115,6 +133,7 @@ public class MdnsSearchOptions implements Parcelable { private final Set subtypes; private boolean isPassiveMode = true; private boolean removeExpiredService; + private Network mNetwork; private Builder() { subtypes = new ArraySet<>(); @@ -165,10 +184,20 @@ public class MdnsSearchOptions implements Parcelable { return this; } + /** + * Sets if the mdns query should target on specific network. + * + * @param network the mdns query will target on given network. + */ + public Builder setNetwork(Network network) { + mNetwork = network; + return this; + } + /** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */ public MdnsSearchOptions build() { - return new MdnsSearchOptions( - new ArrayList<>(subtypes), isPassiveMode, removeExpiredService); + return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode, + removeExpiredService, mNetwork); } } } \ No newline at end of file diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java index 9683bc9af3..938fc3f204 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java @@ -16,8 +16,11 @@ package com.android.server.connectivity.mdns; +import static com.android.server.connectivity.mdns.MdnsSocket.INTERFACE_INDEX_UNSPECIFIED; + import android.annotation.NonNull; import android.annotation.Nullable; +import android.net.Network; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -58,7 +61,8 @@ public class MdnsServiceInfo implements Parcelable { source.readString(), source.createStringArrayList(), source.createTypedArrayList(TextEntry.CREATOR), - source.readInt()); + source.readInt(), + source.readParcelable(null)); } @Override @@ -82,6 +86,8 @@ public class MdnsServiceInfo implements Parcelable { private final int interfaceIndex; private final Map attributes; + @Nullable + private final Network network; /** Constructs a {@link MdnsServiceInfo} object with default values. */ public MdnsServiceInfo( @@ -103,7 +109,8 @@ public class MdnsServiceInfo implements Parcelable { ipv6Address, textStrings, /* textEntries= */ null, - /* interfaceIndex= */ -1); + /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED, + /* network= */ null); } /** Constructs a {@link MdnsServiceInfo} object with default values. */ @@ -127,7 +134,8 @@ public class MdnsServiceInfo implements Parcelable { ipv6Address, textStrings, textEntries, - /* interfaceIndex= */ -1); + /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED, + /* network= */ null); } /** @@ -146,6 +154,37 @@ public class MdnsServiceInfo implements Parcelable { @Nullable List textStrings, @Nullable List textEntries, int interfaceIndex) { + this( + serviceInstanceName, + serviceType, + subtypes, + hostName, + port, + ipv4Address, + ipv6Address, + textStrings, + textEntries, + interfaceIndex, + /* network= */ null); + } + + /** + * Constructs a {@link MdnsServiceInfo} object with default values. + * + * @hide + */ + public MdnsServiceInfo( + String serviceInstanceName, + String[] serviceType, + @Nullable List subtypes, + String[] hostName, + int port, + @Nullable String ipv4Address, + @Nullable String ipv6Address, + @Nullable List textStrings, + @Nullable List textEntries, + int interfaceIndex, + @Nullable Network network) { this.serviceInstanceName = serviceInstanceName; this.serviceType = serviceType; this.subtypes = new ArrayList<>(); @@ -180,6 +219,7 @@ public class MdnsServiceInfo implements Parcelable { } this.attributes = Collections.unmodifiableMap(attributes); this.interfaceIndex = interfaceIndex; + this.network = network; } private static List parseTextStrings(List textStrings) { @@ -243,6 +283,14 @@ public class MdnsServiceInfo implements Parcelable { return interfaceIndex; } + /** + * Returns the network at which this response was received, or null if the network is unknown. + */ + @Nullable + public Network getNetwork() { + return network; + } + /** * Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure * that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no @@ -293,6 +341,7 @@ public class MdnsServiceInfo implements Parcelable { out.writeStringList(textStrings); out.writeTypedList(textEntries); out.writeInt(interfaceIndex); + out.writeParcelable(network, 0); } @Override diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java index dd4ff9bc1d..538f37605b 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java @@ -130,7 +130,8 @@ public class MdnsServiceTypeClient { ipv6Address, textStrings, textEntries, - response.getInterfaceIndex()); + response.getInterfaceIndex(), + response.getNetwork()); } /** diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java index 0a9b2fc96f..64c4495b0c 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java @@ -17,6 +17,8 @@ package com.android.server.connectivity.mdns; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.Network; import com.android.internal.annotations.VisibleForTesting; import com.android.server.connectivity.mdns.util.MdnsLogger; @@ -38,9 +40,9 @@ public class MdnsSocket { private static final MdnsLogger LOGGER = new MdnsLogger("MdnsSocket"); static final int INTERFACE_INDEX_UNSPECIFIED = -1; - private static final InetSocketAddress MULTICAST_IPV4_ADDRESS = + protected static final InetSocketAddress MULTICAST_IPV4_ADDRESS = new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT); - private static final InetSocketAddress MULTICAST_IPV6_ADDRESS = + protected static final InetSocketAddress MULTICAST_IPV6_ADDRESS = new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT); private final MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider; private final MulticastSocket multicastSocket; @@ -125,6 +127,14 @@ public class MdnsSocket { } } + /** + * Returns the available network that this socket is used to, or null if the network is unknown. + */ + @Nullable + public Network getNetwork() { + return multicastNetworkInterfaceProvider.getAvailableNetwork(); + } + public boolean isOnIPv6OnlyNetwork() { return isOnIPv6OnlyNetwork; } diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java index 758221a8d1..6a321d1ff3 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.content.Context; +import android.net.Network; import android.net.wifi.WifiManager.MulticastLock; import android.os.SystemClock; import android.text.format.DateUtils; @@ -397,7 +398,8 @@ public class MdnsSocketClient { responseType, /* interfaceIndex= */ (socket == null || !propagateInterfaceIndex) ? MdnsSocket.INTERFACE_INDEX_UNSPECIFIED - : socket.getInterfaceIndex()); + : socket.getInterfaceIndex(), + /* network= */ socket.getNetwork()); } } catch (IOException e) { if (!shouldStopSocketLoop) { @@ -408,12 +410,12 @@ public class MdnsSocketClient { LOGGER.log("Receive thread stopped."); } - private int processResponsePacket( - @NonNull DatagramPacket packet, String responseType, int interfaceIndex) { + private int processResponsePacket(@NonNull DatagramPacket packet, String responseType, + int interfaceIndex, @Nullable Network network) { int packetNumber = ++receivedPacketNumber; List responses = new LinkedList<>(); - int errorCode = responseDecoder.decode(packet, responses, interfaceIndex); + int errorCode = responseDecoder.decode(packet, responses, interfaceIndex, network); if (errorCode == MdnsResponseDecoder.SUCCESS) { if (responseType.equals(MULTICAST_TYPE)) { receivedMulticastResponse = true; diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketProvider.java new file mode 100644 index 0000000000..b8c324ed21 --- /dev/null +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketProvider.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2022 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.mdns; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.INetd; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkRequest; +import android.net.TetheringManager; +import android.net.TetheringManager.TetheringEventCallback; +import android.os.Handler; +import android.os.Looper; +import android.system.OsConstants; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.LinkPropertiesUtils.CompareResult; +import com.android.net.module.util.ip.NetlinkMonitor; +import com.android.net.module.util.netlink.NetlinkConstants; +import com.android.net.module.util.netlink.NetlinkMessage; +import com.android.server.connectivity.mdns.util.MdnsLogger; + +import java.io.IOException; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link MdnsSocketProvider} manages the multiple sockets for mDns. + * + *

This class is not thread safe, it is intended to be used only from the looper thread. + * However, the constructor is an exception, as it is called on another thread; + * therefore for thread safety all members of this class MUST either be final or initialized + * to their default value (0, false or null). + * + */ +public class MdnsSocketProvider { + private static final String TAG = MdnsSocketProvider.class.getSimpleName(); + private static final boolean DBG = MdnsDiscoveryManager.DBG; + private static final MdnsLogger LOGGER = new MdnsLogger(TAG); + @NonNull private final Context mContext; + @NonNull private final Handler mHandler; + @NonNull private final Dependencies mDependencies; + @NonNull private final NetworkCallback mNetworkCallback; + @NonNull private final TetheringEventCallback mTetheringEventCallback; + @NonNull private final NetlinkMonitor mNetlinkMonitor; + private final ArrayMap mNetworkSockets = new ArrayMap<>(); + private final ArrayMap mTetherInterfaceSockets = new ArrayMap<>(); + private final ArrayMap mActiveNetworksLinkProperties = + new ArrayMap<>(); + private final ArrayMap mCallbacksToRequestedNetworks = + new ArrayMap<>(); + private final List mLocalOnlyInterfaces = new ArrayList<>(); + private final List mTetheredInterfaces = new ArrayList<>(); + private boolean mMonitoringSockets = false; + + public MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper) { + this(context, looper, new Dependencies()); + } + + MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper, + @NonNull Dependencies deps) { + mContext = context; + mHandler = new Handler(looper); + mDependencies = deps; + mNetworkCallback = new NetworkCallback() { + @Override + public void onLost(Network network) { + mActiveNetworksLinkProperties.remove(network); + removeSocket(network, null /* interfaceName */); + } + + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties lp) { + handleLinkPropertiesChanged(network, lp); + } + }; + mTetheringEventCallback = new TetheringEventCallback() { + @Override + public void onLocalOnlyInterfacesChanged(@NonNull List interfaces) { + handleTetherInterfacesChanged(mLocalOnlyInterfaces, interfaces); + } + + @Override + public void onTetheredInterfacesChanged(@NonNull List interfaces) { + handleTetherInterfacesChanged(mTetheredInterfaces, interfaces); + } + }; + + mNetlinkMonitor = new SocketNetlinkMonitor(mHandler); + } + + /** + * Dependencies of MdnsSocketProvider, for injection in tests. + */ + @VisibleForTesting + public static class Dependencies { + /*** Get network interface by given interface name */ + public NetworkInterfaceWrapper getNetworkInterfaceByName(String interfaceName) + throws SocketException { + final NetworkInterface ni = NetworkInterface.getByName(interfaceName); + return ni == null ? null : new NetworkInterfaceWrapper(ni); + } + + /*** Check whether given network interface can support mdns */ + public boolean canScanOnInterface(NetworkInterfaceWrapper networkInterface) { + return MulticastNetworkInterfaceProvider.canScanOnInterface(networkInterface); + } + + /*** Create a MdnsInterfaceSocket */ + public MdnsInterfaceSocket createMdnsInterfaceSocket(NetworkInterface networkInterface, + int port) throws IOException { + return new MdnsInterfaceSocket(networkInterface, port); + } + } + + /*** Data class for storing socket related info */ + private static class SocketInfo { + final MdnsInterfaceSocket mSocket; + final List mAddresses = new ArrayList<>(); + + SocketInfo(MdnsInterfaceSocket socket, List addresses) { + mSocket = socket; + mAddresses.addAll(addresses); + } + } + + private static class SocketNetlinkMonitor extends NetlinkMonitor { + SocketNetlinkMonitor(Handler handler) { + super(handler, LOGGER.mLog, TAG, OsConstants.NETLINK_ROUTE, + NetlinkConstants.RTMGRP_IPV4_IFADDR | NetlinkConstants.RTMGRP_IPV6_IFADDR); + } + + @Override + public void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) { + // TODO: Handle netlink message. + } + } + + private void ensureRunningOnHandlerThread() { + if (mHandler.getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException( + "Not running on Handler thread: " + Thread.currentThread().getName()); + } + } + + /*** Start monitoring sockets by listening callbacks for sockets creation or removal */ + public void startMonitoringSockets() { + ensureRunningOnHandlerThread(); + if (mMonitoringSockets) { + Log.d(TAG, "Already monitoring sockets."); + return; + } + if (DBG) Log.d(TAG, "Start monitoring sockets."); + mContext.getSystemService(ConnectivityManager.class).registerNetworkCallback( + new NetworkRequest.Builder().clearCapabilities().build(), + mNetworkCallback, mHandler); + + final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class); + tetheringManager.registerTetheringEventCallback(mHandler::post, mTetheringEventCallback); + + mHandler.post(mNetlinkMonitor::start); + mMonitoringSockets = true; + } + + /*** Stop monitoring sockets and unregister callbacks */ + public void stopMonitoringSockets() { + ensureRunningOnHandlerThread(); + if (!mMonitoringSockets) { + Log.d(TAG, "Monitoring sockets hasn't been started."); + return; + } + if (DBG) Log.d(TAG, "Stop monitoring sockets."); + mContext.getSystemService(ConnectivityManager.class) + .unregisterNetworkCallback(mNetworkCallback); + + final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class); + tetheringManager.unregisterTetheringEventCallback(mTetheringEventCallback); + + mHandler.post(mNetlinkMonitor::stop); + mMonitoringSockets = false; + } + + private static boolean isNetworkMatched(@Nullable Network targetNetwork, + @NonNull Network currentNetwork) { + return targetNetwork == null || targetNetwork.equals(currentNetwork); + } + + private boolean matchRequestedNetwork(Network network) { + for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) { + final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i); + if (isNetworkMatched(requestedNetwork, network)) { + return true; + } + } + return false; + } + + private boolean hasAllNetworksRequest() { + return mCallbacksToRequestedNetworks.containsValue(null); + } + + private void handleLinkPropertiesChanged(Network network, LinkProperties lp) { + mActiveNetworksLinkProperties.put(network, lp); + if (!matchRequestedNetwork(network)) { + if (DBG) { + Log.d(TAG, "Ignore LinkProperties change. There is no request for the" + + " Network:" + network); + } + return; + } + + final SocketInfo socketInfo = mNetworkSockets.get(network); + if (socketInfo == null) { + createSocket(network, lp); + } else { + // Update the addresses of this socket. + final List addresses = lp.getLinkAddresses(); + socketInfo.mAddresses.clear(); + socketInfo.mAddresses.addAll(addresses); + // Try to join the group again. + socketInfo.mSocket.joinGroup(addresses); + + notifyAddressesChanged(network, lp); + } + } + + private static LinkProperties createLPForTetheredInterface(String interfaceName) { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(interfaceName); + // TODO: Use NetlinkMonitor to update addresses for tethering interfaces. + return linkProperties; + } + + private void handleTetherInterfacesChanged(List current, List updated) { + if (!hasAllNetworksRequest()) { + // Currently, the network for tethering can not be requested, so the sockets for + // tethering are only created if there is a request for all networks (interfaces). + // Therefore, this change can skip if there is no such request. + if (DBG) { + Log.d(TAG, "Ignore tether interfaces change. There is no request for all" + + " networks."); + } + return; + } + + final CompareResult interfaceDiff = new CompareResult<>( + current, updated); + for (String name : interfaceDiff.added) { + createSocket(new Network(INetd.LOCAL_NET_ID), createLPForTetheredInterface(name)); + } + for (String name : interfaceDiff.removed) { + removeSocket(new Network(INetd.LOCAL_NET_ID), name); + } + current.clear(); + current.addAll(updated); + } + + private static List getLinkAddressFromNetworkInterface( + NetworkInterfaceWrapper networkInterface) { + List addresses = new ArrayList<>(); + for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) { + addresses.add(new LinkAddress(address)); + } + return addresses; + } + + private void createSocket(Network network, LinkProperties lp) { + final String interfaceName = lp.getInterfaceName(); + if (interfaceName == null) { + Log.e(TAG, "Can not create socket with null interface name."); + return; + } + + try { + final NetworkInterfaceWrapper networkInterface = + mDependencies.getNetworkInterfaceByName(interfaceName); + if (networkInterface == null || !mDependencies.canScanOnInterface(networkInterface)) { + return; + } + + if (DBG) { + Log.d(TAG, "Create a socket on network:" + network + + " with interfaceName:" + interfaceName); + } + final MdnsInterfaceSocket socket = mDependencies.createMdnsInterfaceSocket( + networkInterface.getNetworkInterface(), MdnsConstants.MDNS_PORT); + final List addresses; + if (network.netId == INetd.LOCAL_NET_ID) { + addresses = getLinkAddressFromNetworkInterface(networkInterface); + mTetherInterfaceSockets.put(interfaceName, new SocketInfo(socket, addresses)); + } else { + addresses = lp.getLinkAddresses(); + mNetworkSockets.put(network, new SocketInfo(socket, addresses)); + } + // Try to join IPv4/IPv6 group. + socket.joinGroup(addresses); + + // Notify the listeners which need this socket. + notifySocketCreated(network, socket, addresses); + } catch (IOException e) { + Log.e(TAG, "Create a socket failed with interface=" + interfaceName, e); + } + } + + private void removeSocket(Network network, String interfaceName) { + final SocketInfo socketInfo = network.netId == INetd.LOCAL_NET_ID + ? mTetherInterfaceSockets.remove(interfaceName) + : mNetworkSockets.remove(network); + if (socketInfo == null) return; + + socketInfo.mSocket.destroy(); + notifyInterfaceDestroyed(network, socketInfo.mSocket); + } + + private void notifySocketCreated(Network network, MdnsInterfaceSocket socket, + List addresses) { + for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) { + final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i); + if (isNetworkMatched(requestedNetwork, network)) { + mCallbacksToRequestedNetworks.keyAt(i).onSocketCreated(network, socket, addresses); + } + } + } + + private void notifyInterfaceDestroyed(Network network, MdnsInterfaceSocket socket) { + for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) { + final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i); + if (isNetworkMatched(requestedNetwork, network)) { + mCallbacksToRequestedNetworks.keyAt(i).onInterfaceDestroyed(network, socket); + } + } + } + + private void notifyAddressesChanged(Network network, LinkProperties lp) { + for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) { + final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i); + if (isNetworkMatched(requestedNetwork, network)) { + mCallbacksToRequestedNetworks.keyAt(i) + .onAddressesChanged(network, lp.getLinkAddresses()); + } + } + } + + private void retrieveAndNotifySocketFromNetwork(Network network, SocketCallback cb) { + final SocketInfo socketInfo = mNetworkSockets.get(network); + if (socketInfo == null) { + final LinkProperties lp = mActiveNetworksLinkProperties.get(network); + if (lp == null) { + // The requested network is not existed. Maybe wait for LinkProperties change later. + if (DBG) Log.d(TAG, "There is no LinkProperties for this network:" + network); + return; + } + createSocket(network, lp); + } else { + // Notify the socket for requested network. + cb.onSocketCreated(network, socketInfo.mSocket, socketInfo.mAddresses); + } + } + + private void retrieveAndNotifySocketFromInterface(String interfaceName, SocketCallback cb) { + final SocketInfo socketInfo = mTetherInterfaceSockets.get(interfaceName); + if (socketInfo == null) { + createSocket( + new Network(INetd.LOCAL_NET_ID), createLPForTetheredInterface(interfaceName)); + } else { + // Notify the socket for requested network. + cb.onSocketCreated( + new Network(INetd.LOCAL_NET_ID), socketInfo.mSocket, socketInfo.mAddresses); + } + } + + /** + * Request a socket for given network. + * + * @param network the required network for a socket. Null means create sockets on all possible + * networks (interfaces). + * @param cb the callback to listen the socket creation. + */ + public void requestSocket(@Nullable Network network, @NonNull SocketCallback cb) { + ensureRunningOnHandlerThread(); + mCallbacksToRequestedNetworks.put(cb, network); + if (network == null) { + // Does not specify a required network, create sockets for all possible + // networks (interfaces). + for (int i = 0; i < mActiveNetworksLinkProperties.size(); i++) { + retrieveAndNotifySocketFromNetwork(mActiveNetworksLinkProperties.keyAt(i), cb); + } + + for (String localInterface : mLocalOnlyInterfaces) { + retrieveAndNotifySocketFromInterface(localInterface, cb); + } + + for (String tetheredInterface : mTetheredInterfaces) { + retrieveAndNotifySocketFromInterface(tetheredInterface, cb); + } + } else { + retrieveAndNotifySocketFromNetwork(network, cb); + } + } + + /*** Unrequest the socket */ + public void unrequestSocket(@NonNull SocketCallback cb) { + ensureRunningOnHandlerThread(); + mCallbacksToRequestedNetworks.remove(cb); + if (hasAllNetworksRequest()) { + // Still has a request for all networks (interfaces). + return; + } + + // Check if remaining requests are matched any of sockets. + for (int i = mNetworkSockets.size() - 1; i >= 0; i--) { + if (matchRequestedNetwork(mNetworkSockets.keyAt(i))) continue; + mNetworkSockets.removeAt(i).mSocket.destroy(); + } + + // Remove all sockets for tethering interface because these sockets do not have associated + // networks, and they should invoke by a request for all networks (interfaces). If there is + // no such request, the sockets for tethering interface should be removed. + for (int i = mTetherInterfaceSockets.size() - 1; i >= 0; i--) { + mTetherInterfaceSockets.removeAt(i).mSocket.destroy(); + } + } + + /*** Callbacks for listening socket changes */ + public interface SocketCallback { + /*** Notify the socket is created */ + default void onSocketCreated(@NonNull Network network, @NonNull MdnsInterfaceSocket socket, + @NonNull List addresses) {} + /*** Notify the interface is destroyed */ + default void onInterfaceDestroyed(@NonNull Network network, + @NonNull MdnsInterfaceSocket socket) {} + /*** Notify the addresses is changed on the network */ + default void onAddressesChanged(@NonNull Network network, + @NonNull List addresses) {} + } +} diff --git a/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java index e0d8fa66c5..ade7b9594c 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java +++ b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java @@ -19,6 +19,7 @@ package com.android.server.connectivity.mdns; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.net.Network; import com.android.internal.annotations.VisibleForTesting; import com.android.server.connectivity.mdns.util.MdnsLogger; @@ -56,7 +57,7 @@ public class MulticastNetworkInterfaceProvider { context, this::onConnectivityChanged); } - private void onConnectivityChanged() { + private synchronized void onConnectivityChanged() { connectivityChanged = true; } @@ -141,7 +142,13 @@ public class MulticastNetworkInterfaceProvider { return networkInterfaceWrappers; } - private boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) { + @Nullable + public Network getAvailableNetwork() { + return connectivityMonitor.getAvailableNetwork(); + } + + /*** Check whether given network interface can support mdns */ + public static boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) { try { if ((networkInterface == null) || networkInterface.isLoopback() @@ -160,7 +167,7 @@ public class MulticastNetworkInterfaceProvider { return false; } - private boolean hasInet4Address(@NonNull NetworkInterfaceWrapper networkInterface) { + private static boolean hasInet4Address(@NonNull NetworkInterfaceWrapper networkInterface) { for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) { if (ifAddr.getAddress() instanceof Inet4Address) { return true; @@ -169,7 +176,7 @@ public class MulticastNetworkInterfaceProvider { return false; } - private boolean hasInet6Address(@NonNull NetworkInterfaceWrapper networkInterface) { + private static boolean hasInet6Address(@NonNull NetworkInterfaceWrapper networkInterface) { for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) { if (ifAddr.getAddress() instanceof Inet6Address) { return true; diff --git a/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java index f84e2d8d2f..8fb7be1eb0 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java +++ b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java @@ -21,6 +21,7 @@ import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -111,7 +112,7 @@ public class ConnectivityMonitorWithConnectivityManagerTests { any(NetworkRequest.class), callbackCaptor.capture()); final NetworkCallback callback = callbackCaptor.getValue(); - final Network testNetwork = new Network(1 /* netId */); + final Network testNetwork = mock(Network.class); // Simulate network available. callback.onAvailable(testNetwork); diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java index 02e00c2244..4cae4471ab 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import android.net.InetAddresses; +import android.net.Network; import com.android.net.module.util.HexDump; import com.android.testutils.DevSdkIgnoreRule; @@ -165,7 +166,8 @@ public class MdnsResponseDecoderTests { packet.setSocketAddress( new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT)); responses.clear(); - int errorCode = decoder.decode(packet, responses, MdnsSocket.INTERFACE_INDEX_UNSPECIFIED); + int errorCode = decoder.decode( + packet, responses, MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class)); assertEquals(MdnsResponseDecoder.SUCCESS, errorCode); assertEquals(1, responses.size()); } @@ -178,7 +180,8 @@ public class MdnsResponseDecoderTests { packet.setSocketAddress( new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT)); responses.clear(); - int errorCode = decoder.decode(packet, responses, MdnsSocket.INTERFACE_INDEX_UNSPECIFIED); + int errorCode = decoder.decode( + packet, responses, MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class)); assertEquals(MdnsResponseDecoder.SUCCESS, errorCode); assertEquals(2, responses.size()); } @@ -237,7 +240,8 @@ public class MdnsResponseDecoderTests { new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT)); responses.clear(); - int errorCode = decoder.decode(packet, responses, MdnsSocket.INTERFACE_INDEX_UNSPECIFIED); + int errorCode = decoder.decode( + packet, responses, MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class)); assertEquals(MdnsResponseDecoder.SUCCESS, errorCode); MdnsResponse response = responses.get(0); @@ -287,10 +291,13 @@ public class MdnsResponseDecoderTests { new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT)); responses.clear(); - int errorCode = decoder.decode(packet, responses, /* interfaceIndex= */ 10); + final Network network = mock(Network.class); + int errorCode = decoder.decode( + packet, responses, /* interfaceIndex= */ 10, network); assertEquals(errorCode, MdnsResponseDecoder.SUCCESS); assertEquals(responses.size(), 1); assertEquals(responses.get(0).getInterfaceIndex(), 10); + assertEquals(network, responses.get(0).getNetwork()); } @Test @@ -306,7 +313,8 @@ public class MdnsResponseDecoderTests { new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT)); responses.clear(); - int errorCode = decoder.decode(packet, responses, /* interfaceIndex= */ 0); + int errorCode = decoder.decode( + packet, responses, /* interfaceIndex= */ 0, mock(Network.class)); assertEquals(MdnsResponseDecoder.SUCCESS, errorCode); // This should emit two records: @@ -340,7 +348,8 @@ public class MdnsResponseDecoderTests { new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT)); responses.clear(); - int errorCode = decoder.decode(packet, responses, /* interfaceIndex= */ 0); + int errorCode = decoder.decode( + packet, responses, /* interfaceIndex= */ 0, mock(Network.class)); assertEquals(MdnsResponseDecoder.SUCCESS, errorCode); // This should emit only two records: diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java index 771e42c459..ec57dc87de 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java @@ -21,8 +21,12 @@ import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import android.net.Network; import com.android.net.module.util.HexDump; import com.android.testutils.DevSdkIgnoreRule; @@ -92,6 +96,9 @@ public class MdnsResponseTests { + "3839300878797A3D" + "21402324"); + private static final int INTERFACE_INDEX = 999; + private final Network mNetwork = mock(Network.class); + // The following helper classes act as wrappers so that IPv4 and IPv6 address records can // be explicitly created by type using same constructor signature as all other records. static class MdnsInet4AddressRecord extends MdnsInetAddressRecord { @@ -127,7 +134,7 @@ public class MdnsResponseTests { // Construct an MdnsResponse with the specified data packets applied. private MdnsResponse makeMdnsResponse(long time, List responseList) throws IOException { - MdnsResponse response = new MdnsResponse(time); + MdnsResponse response = new MdnsResponse(time, INTERFACE_INDEX, mNetwork); for (PacketAndRecordClass responseData : responseList) { DatagramPacket packet = new DatagramPacket(responseData.packetData, responseData.packetData.length); @@ -159,7 +166,7 @@ public class MdnsResponseTests { String[] name = reader.readLabels(); reader.skip(2); // skip record type indication. MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader); - MdnsResponse response = new MdnsResponse(0); + MdnsResponse response = new MdnsResponse(0, INTERFACE_INDEX, mNetwork); assertFalse(response.hasInet4AddressRecord()); assertTrue(response.setInet4AddressRecord(record)); assertEquals(response.getInet4AddressRecord(), record); @@ -173,7 +180,7 @@ public class MdnsResponseTests { reader.skip(2); // skip record type indication. MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader); - MdnsResponse response = new MdnsResponse(0); + MdnsResponse response = new MdnsResponse(0, INTERFACE_INDEX, mNetwork); assertFalse(response.hasInet6AddressRecord()); assertTrue(response.setInet6AddressRecord(record)); assertEquals(response.getInet6AddressRecord(), record); @@ -186,7 +193,7 @@ public class MdnsResponseTests { String[] name = reader.readLabels(); reader.skip(2); // skip record type indication. MdnsPointerRecord record = new MdnsPointerRecord(name, reader); - MdnsResponse response = new MdnsResponse(0); + MdnsResponse response = new MdnsResponse(0, INTERFACE_INDEX, mNetwork); assertFalse(response.hasPointerRecords()); assertTrue(response.addPointerRecord(record)); List recordList = response.getPointerRecords(); @@ -202,7 +209,7 @@ public class MdnsResponseTests { String[] name = reader.readLabels(); reader.skip(2); // skip record type indication. MdnsServiceRecord record = new MdnsServiceRecord(name, reader); - MdnsResponse response = new MdnsResponse(0); + MdnsResponse response = new MdnsResponse(0, INTERFACE_INDEX, mNetwork); assertFalse(response.hasServiceRecord()); assertTrue(response.setServiceRecord(record)); assertEquals(response.getServiceRecord(), record); @@ -215,23 +222,31 @@ public class MdnsResponseTests { String[] name = reader.readLabels(); reader.skip(2); // skip record type indication. MdnsTextRecord record = new MdnsTextRecord(name, reader); - MdnsResponse response = new MdnsResponse(0); + MdnsResponse response = new MdnsResponse(0, INTERFACE_INDEX, mNetwork); assertFalse(response.hasTextRecord()); assertTrue(response.setTextRecord(record)); assertEquals(response.getTextRecord(), record); } @Test - public void getInterfaceIndex_returnsDefaultValue() { - MdnsResponse response = new MdnsResponse(/* now= */ 0); - assertEquals(response.getInterfaceIndex(), -1); + public void getInterfaceIndex() { + final MdnsResponse response1 = new MdnsResponse(/* now= */ 0, INTERFACE_INDEX, mNetwork); + assertEquals(INTERFACE_INDEX, response1.getInterfaceIndex()); + + final MdnsResponse response2 = + new MdnsResponse(/* now= */ 0, 1234 /* interfaceIndex */, mNetwork); + assertEquals(1234, response2.getInterfaceIndex()); } @Test - public void getInterfaceIndex_afterSet_returnsValue() { - MdnsResponse response = new MdnsResponse(/* now= */ 0); - response.setInterfaceIndex(5); - assertEquals(response.getInterfaceIndex(), 5); + public void testGetNetwork() { + final MdnsResponse response1 = + new MdnsResponse(/* now= */ 0, INTERFACE_INDEX, null /* network */); + assertNull(response1.getNetwork()); + + final MdnsResponse response2 = + new MdnsResponse(/* now= */ 0, 1234 /* interfaceIndex */, mNetwork); + assertEquals(mNetwork, response2.getNetwork()); } @Test diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java index ebdb73fd48..76728cf419 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java @@ -16,13 +16,16 @@ package com.android.server.connectivity.mdns; +import static com.android.server.connectivity.mdns.MdnsSocket.INTERFACE_INDEX_UNSPECIFIED; import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import android.net.Network; import android.os.Parcel; import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry; @@ -128,7 +131,7 @@ public class MdnsServiceInfoTest { "2001::1", List.of()); - assertEquals(info.getInterfaceIndex(), -1); + assertEquals(info.getInterfaceIndex(), INTERFACE_INDEX_UNSPECIFIED); } @Test @@ -149,6 +152,41 @@ public class MdnsServiceInfoTest { assertEquals(info.getInterfaceIndex(), 20); } + @Test + public void testGetNetwork() { + final MdnsServiceInfo info1 = + new MdnsServiceInfo( + "my-mdns-service", + new String[] {"_googlecast", "_tcp"}, + List.of(), + new String[] {"my-host", "local"}, + 12345, + "192.168.1.1", + "2001::1", + List.of(), + /* textEntries= */ null, + /* interfaceIndex= */ 20); + + assertNull(info1.getNetwork()); + + final Network network = mock(Network.class); + final MdnsServiceInfo info2 = + new MdnsServiceInfo( + "my-mdns-service", + new String[] {"_googlecast", "_tcp"}, + List.of(), + new String[] {"my-host", "local"}, + 12345, + "192.168.1.1", + "2001::1", + List.of(), + /* textEntries= */ null, + /* interfaceIndex= */ 20, + network); + + assertEquals(network, info2.getNetwork()); + } + @Test public void parcelable_canBeParceledAndUnparceled() { Parcel parcel = Parcel.obtain(); @@ -165,7 +203,9 @@ public class MdnsServiceInfoTest { List.of( MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."), MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"), - MdnsServiceInfo.TextEntry.fromString("test="))); + MdnsServiceInfo.TextEntry.fromString("test=")), + 20 /* interfaceIndex */, + new Network(123)); beforeParcel.writeToParcel(parcel, 0); parcel.setDataPosition(0); @@ -179,6 +219,8 @@ public class MdnsServiceInfoTest { assertEquals(beforeParcel.getIpv4Address(), afterParcel.getIpv4Address()); assertEquals(beforeParcel.getIpv6Address(), afterParcel.getIpv6Address()); assertEquals(beforeParcel.getAttributes(), afterParcel.getAttributes()); + assertEquals(beforeParcel.getInterfaceIndex(), afterParcel.getInterfaceIndex()); + assertEquals(beforeParcel.getNetwork(), afterParcel.getNetwork()); } @Test diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java index 462685a7f7..697116ccad 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java @@ -39,6 +39,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import android.annotation.NonNull; import android.annotation.Nullable; import android.net.InetAddresses; +import android.net.Network; import android.text.TextUtils; import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry; @@ -79,6 +80,7 @@ public class MdnsServiceTypeClientTests { private static final int INTERFACE_INDEX = 999; private static final String SERVICE_TYPE = "_googlecast._tcp.local"; private static final String[] SERVICE_TYPE_LABELS = TextUtils.split(SERVICE_TYPE, "\\."); + private static final Network NETWORK = mock(Network.class); @Mock private MdnsServiceBrowserListener mockListenerOne; @@ -385,7 +387,8 @@ public class MdnsServiceTypeClientTests { private static void verifyServiceInfo(MdnsServiceInfo serviceInfo, String serviceName, String[] serviceType, String ipv4Address, String ipv6Address, int port, - List subTypes, Map attributes, int interfaceIndex) { + List subTypes, Map attributes, int interfaceIndex, + Network network) { assertEquals(serviceName, serviceInfo.getServiceInstanceName()); assertArrayEquals(serviceType, serviceInfo.getServiceType()); assertEquals(ipv4Address, serviceInfo.getIpv4Address()); @@ -396,6 +399,7 @@ public class MdnsServiceTypeClientTests { assertEquals(attributes.get(key), serviceInfo.getAttributeByKey(key)); } assertEquals(interfaceIndex, serviceInfo.getInterfaceIndex()); + assertEquals(network, serviceInfo.getNetwork()); } @Test @@ -405,6 +409,7 @@ public class MdnsServiceTypeClientTests { MdnsResponse response = mock(MdnsResponse.class); when(response.getServiceInstanceName()).thenReturn("service-instance-1"); doReturn(INTERFACE_INDEX).when(response).getInterfaceIndex(); + doReturn(NETWORK).when(response).getNetwork(); when(response.isComplete()).thenReturn(false); client.processResponse(response); @@ -417,7 +422,8 @@ public class MdnsServiceTypeClientTests { 0 /* port */, List.of() /* subTypes */, Collections.singletonMap("key", null) /* attributes */, - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class)); verify(mockListenerOne, never()).onServiceUpdated(any(MdnsServiceInfo.class)); @@ -436,7 +442,8 @@ public class MdnsServiceTypeClientTests { 5353, /* subtype= */ "ABCDE", Collections.emptyMap(), - /* interfaceIndex= */ 20); + /* interfaceIndex= */ 20, + NETWORK); client.processResponse(initialResponse); // Process a second response with a different port and updated text attributes. @@ -447,7 +454,8 @@ public class MdnsServiceTypeClientTests { 5354, /* subtype= */ "ABCDE", Collections.singletonMap("key", "value"), - /* interfaceIndex= */ 20); + /* interfaceIndex= */ 20, + NETWORK); client.processResponse(secondResponse); // Verify onServiceNameDiscovered was called once for the initial response. @@ -460,7 +468,8 @@ public class MdnsServiceTypeClientTests { 5353 /* port */, Collections.singletonList("ABCDE") /* subTypes */, Collections.singletonMap("key", null) /* attributes */, - 20 /* interfaceIndex */); + 20 /* interfaceIndex */, + NETWORK); // Verify onServiceFound was called once for the initial response. verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture()); @@ -471,6 +480,7 @@ public class MdnsServiceTypeClientTests { assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE")); assertNull(initialServiceInfo.getAttributeByKey("key")); assertEquals(initialServiceInfo.getInterfaceIndex(), 20); + assertEquals(NETWORK, initialServiceInfo.getNetwork()); // Verify onServiceUpdated was called once for the second response. verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture()); @@ -482,6 +492,7 @@ public class MdnsServiceTypeClientTests { assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE")); assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value"); assertEquals(updatedServiceInfo.getInterfaceIndex(), 20); + assertEquals(NETWORK, updatedServiceInfo.getNetwork()); } @Test @@ -497,7 +508,8 @@ public class MdnsServiceTypeClientTests { 5353, /* subtype= */ "ABCDE", Collections.emptyMap(), - /* interfaceIndex= */ 20); + /* interfaceIndex= */ 20, + NETWORK); client.processResponse(initialResponse); // Process a second response with a different port and updated text attributes. @@ -508,7 +520,8 @@ public class MdnsServiceTypeClientTests { 5354, /* subtype= */ "ABCDE", Collections.singletonMap("key", "value"), - /* interfaceIndex= */ 20); + /* interfaceIndex= */ 20, + NETWORK); client.processResponse(secondResponse); System.out.println("secondResponses ip" @@ -524,7 +537,8 @@ public class MdnsServiceTypeClientTests { 5353 /* port */, Collections.singletonList("ABCDE") /* subTypes */, Collections.singletonMap("key", null) /* attributes */, - 20 /* interfaceIndex */); + 20 /* interfaceIndex */, + NETWORK); // Verify onServiceFound was called once for the initial response. verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture()); @@ -535,6 +549,7 @@ public class MdnsServiceTypeClientTests { assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE")); assertNull(initialServiceInfo.getAttributeByKey("key")); assertEquals(initialServiceInfo.getInterfaceIndex(), 20); + assertEquals(NETWORK, initialServiceInfo.getNetwork()); // Verify onServiceUpdated was called once for the second response. verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture()); @@ -546,6 +561,7 @@ public class MdnsServiceTypeClientTests { assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE")); assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value"); assertEquals(updatedServiceInfo.getInterfaceIndex(), 20); + assertEquals(NETWORK, updatedServiceInfo.getNetwork()); } private void verifyServiceRemovedNoCallback(MdnsServiceBrowserListener listener) { @@ -554,15 +570,17 @@ public class MdnsServiceTypeClientTests { } private void verifyServiceRemovedCallback(MdnsServiceBrowserListener listener, - String serviceName, String[] serviceType, int interfaceIndex) { + String serviceName, String[] serviceType, int interfaceIndex, Network network) { verify(listener).onServiceRemoved(argThat( info -> serviceName.equals(info.getServiceInstanceName()) && Arrays.equals(serviceType, info.getServiceType()) - && info.getInterfaceIndex() == interfaceIndex)); + && info.getInterfaceIndex() == interfaceIndex + && network.equals(info.getNetwork()))); verify(listener).onServiceNameRemoved(argThat( info -> serviceName.equals(info.getServiceInstanceName()) && Arrays.equals(serviceType, info.getServiceType()) - && info.getInterfaceIndex() == interfaceIndex)); + && info.getInterfaceIndex() == interfaceIndex + && network.equals(info.getNetwork()))); } @Test @@ -580,11 +598,13 @@ public class MdnsServiceTypeClientTests { 5353 /* port */, /* subtype= */ "ABCDE", Collections.emptyMap(), - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); client.processResponse(initialResponse); MdnsResponse response = mock(MdnsResponse.class); doReturn("goodbye-service").when(response).getServiceInstanceName(); doReturn(INTERFACE_INDEX).when(response).getInterfaceIndex(); + doReturn(NETWORK).when(response).getNetwork(); doReturn(true).when(response).isGoodbye(); client.processResponse(response); // Verify removed callback won't be called if the service is not existed. @@ -595,9 +615,9 @@ public class MdnsServiceTypeClientTests { doReturn(serviceName).when(response).getServiceInstanceName(); client.processResponse(response); verifyServiceRemovedCallback( - mockListenerOne, serviceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX); + mockListenerOne, serviceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX, NETWORK); verifyServiceRemovedCallback( - mockListenerTwo, serviceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX); + mockListenerTwo, serviceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX, NETWORK); } @Test @@ -610,7 +630,8 @@ public class MdnsServiceTypeClientTests { 5353, /* subtype= */ "ABCDE", Collections.emptyMap(), - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); client.processResponse(initialResponse); client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions()); @@ -625,7 +646,8 @@ public class MdnsServiceTypeClientTests { 5353 /* port */, Collections.singletonList("ABCDE") /* subTypes */, Collections.singletonMap("key", null) /* attributes */, - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); // Verify onServiceFound was called once for the existing response. verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture()); @@ -662,7 +684,7 @@ public class MdnsServiceTypeClientTests { MdnsResponse initialResponse = createMockResponse( serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"), - Map.of(), INTERFACE_INDEX); + Map.of(), INTERFACE_INDEX, NETWORK); client.processResponse(initialResponse); // Clear the scheduled runnable. @@ -696,7 +718,7 @@ public class MdnsServiceTypeClientTests { MdnsResponse initialResponse = createMockResponse( serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"), - Map.of(), INTERFACE_INDEX); + Map.of(), INTERFACE_INDEX, NETWORK); client.processResponse(initialResponse); // Clear the scheduled runnable. @@ -714,8 +736,8 @@ public class MdnsServiceTypeClientTests { firstMdnsTask.run(); // Verify removed callback was called. - verifyServiceRemovedCallback( - mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX); + verifyServiceRemovedCallback(mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS, + INTERFACE_INDEX, NETWORK); } @Test @@ -736,7 +758,7 @@ public class MdnsServiceTypeClientTests { MdnsResponse initialResponse = createMockResponse( serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"), - Map.of(), INTERFACE_INDEX); + Map.of(), INTERFACE_INDEX, NETWORK); client.processResponse(initialResponse); // Clear the scheduled runnable. @@ -770,7 +792,7 @@ public class MdnsServiceTypeClientTests { MdnsResponse initialResponse = createMockResponse( serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"), - Map.of(), INTERFACE_INDEX); + Map.of(), INTERFACE_INDEX, NETWORK); client.processResponse(initialResponse); // Clear the scheduled runnable. @@ -781,8 +803,8 @@ public class MdnsServiceTypeClientTests { firstMdnsTask.run(); // Verify removed callback was called. - verifyServiceRemovedCallback( - mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX); + verifyServiceRemovedCallback(mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS, + INTERFACE_INDEX, NETWORK); } @Test @@ -801,7 +823,8 @@ public class MdnsServiceTypeClientTests { 5353, "ABCDE" /* subtype */, Collections.emptyMap(), - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); client.processResponse(initialResponse); // Process a second response which has ip address to make response become complete. @@ -812,7 +835,8 @@ public class MdnsServiceTypeClientTests { 5353, "ABCDE" /* subtype */, Collections.emptyMap(), - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); client.processResponse(secondResponse); // Process a third response with a different ip address, port and updated text attributes. @@ -823,7 +847,8 @@ public class MdnsServiceTypeClientTests { 5354, "ABCDE" /* subtype */, Collections.singletonMap("key", "value"), - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); client.processResponse(thirdResponse); // Process the last response which is goodbye message. @@ -842,7 +867,8 @@ public class MdnsServiceTypeClientTests { 5353 /* port */, Collections.singletonList("ABCDE") /* subTypes */, Collections.singletonMap("key", null) /* attributes */, - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); // Verify onServiceFound was second called for the second response. inOrder.verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture()); @@ -854,7 +880,8 @@ public class MdnsServiceTypeClientTests { 5353 /* port */, Collections.singletonList("ABCDE") /* subTypes */, Collections.singletonMap("key", null) /* attributes */, - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); // Verify onServiceUpdated was third called for the third response. inOrder.verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture()); @@ -866,7 +893,8 @@ public class MdnsServiceTypeClientTests { 5354 /* port */, Collections.singletonList("ABCDE") /* subTypes */, Collections.singletonMap("key", "value") /* attributes */, - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); // Verify onServiceRemoved was called for the last response. inOrder.verify(mockListenerOne).onServiceRemoved(serviceInfoCaptor.capture()); @@ -878,7 +906,8 @@ public class MdnsServiceTypeClientTests { 5354 /* port */, Collections.singletonList("ABCDE") /* subTypes */, Collections.singletonMap("key", "value") /* attributes */, - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); // Verify onServiceNameRemoved was called for the last response. inOrder.verify(mockListenerOne).onServiceNameRemoved(serviceInfoCaptor.capture()); @@ -890,7 +919,8 @@ public class MdnsServiceTypeClientTests { 5354 /* port */, Collections.singletonList("ABCDE") /* subTypes */, Collections.singletonMap("key", "value") /* attributes */, - INTERFACE_INDEX); + INTERFACE_INDEX, + NETWORK); } // verifies that the right query was enqueued with the right delay, and send query by executing @@ -962,26 +992,25 @@ public class MdnsServiceTypeClientTests { int port, @NonNull List subtypes, @NonNull Map textAttributes, - int interfaceIndex) + int interfaceIndex, + Network network) throws Exception { String[] hostName = new String[]{"hostname"}; MdnsServiceRecord serviceRecord = mock(MdnsServiceRecord.class); when(serviceRecord.getServiceHost()).thenReturn(hostName); when(serviceRecord.getServicePort()).thenReturn(port); - MdnsResponse response = spy(new MdnsResponse(0)); + MdnsResponse response = spy(new MdnsResponse(0, interfaceIndex, network)); MdnsInetAddressRecord inetAddressRecord = mock(MdnsInetAddressRecord.class); if (host.contains(":")) { when(inetAddressRecord.getInet6Address()) .thenReturn((Inet6Address) Inet6Address.getByName(host)); response.setInet6AddressRecord(inetAddressRecord); - response.setInterfaceIndex(interfaceIndex); } else { when(inetAddressRecord.getInet4Address()) .thenReturn((Inet4Address) Inet4Address.getByName(host)); response.setInet4AddressRecord(inetAddressRecord); - response.setInterfaceIndex(interfaceIndex); } MdnsTextRecord textRecord = mock(MdnsTextRecord.class); @@ -1011,10 +1040,10 @@ public class MdnsServiceTypeClientTests { int port, @NonNull String subtype, @NonNull Map textAttributes, - int interfaceIndex) + int interfaceIndex, + Network network) throws Exception { - MdnsResponse response = new MdnsResponse(0); - response.setInterfaceIndex(interfaceIndex); + MdnsResponse response = new MdnsResponse(0, interfaceIndex, network); // Set PTR record final MdnsPointerRecord pointerRecord = new MdnsPointerRecord( diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java index b4442a5a75..1d61cd36c5 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java @@ -501,8 +501,7 @@ public class MdnsSocketClientTests { //MdnsConfigsFlagsImpl.allowNetworkInterfaceIndexPropagation.override(true); when(mockMulticastSocket.getInterfaceIndex()).thenReturn(21); - mdnsClient = - new MdnsSocketClient(mContext, mockMulticastLock) { + mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) { @Override MdnsSocket createMdnsSocket(int port) { if (port == MdnsConstants.MDNS_PORT) { @@ -525,8 +524,7 @@ public class MdnsSocketClientTests { //MdnsConfigsFlagsImpl.allowNetworkInterfaceIndexPropagation.override(false); when(mockMulticastSocket.getInterfaceIndex()).thenReturn(21); - mdnsClient = - new MdnsSocketClient(mContext, mockMulticastLock) { + mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) { @Override MdnsSocket createMdnsSocket(int port) { if (port == MdnsConstants.MDNS_PORT) { diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java new file mode 100644 index 0000000000..2bb61a6a01 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2022 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.mdns; + +import static com.android.testutils.ContextUtils.mockService; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.INetd; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.TetheringManager; +import android.net.TetheringManager.TetheringEventCallback; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; + +import com.android.net.module.util.ArrayTrackRecord; +import com.android.server.connectivity.mdns.MdnsSocketProvider.Dependencies; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; +import com.android.testutils.HandlerUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2) +public class MdnsSocketProviderTest { + private static final String TEST_IFACE_NAME = "test"; + private static final String LOCAL_ONLY_IFACE_NAME = "local_only"; + private static final String TETHERED_IFACE_NAME = "tethered"; + private static final long DEFAULT_TIMEOUT = 2000L; + private static final long NO_CALLBACK_TIMEOUT = 200L; + private static final LinkAddress LINKADDRV4 = new LinkAddress("192.0.2.0/24"); + private static final LinkAddress LINKADDRV6 = + new LinkAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"); + private static final Network TEST_NETWORK = new Network(123); + private static final Network LOCAL_NETWORK = new Network(INetd.LOCAL_NET_ID); + + @Mock private Context mContext; + @Mock private Dependencies mDeps; + @Mock private ConnectivityManager mCm; + @Mock private TetheringManager mTm; + @Mock private NetworkInterfaceWrapper mTestNetworkIfaceWrapper; + @Mock private NetworkInterfaceWrapper mLocalOnlyIfaceWrapper; + @Mock private NetworkInterfaceWrapper mTetheredIfaceWrapper; + private Handler mHandler; + private MdnsSocketProvider mSocketProvider; + private NetworkCallback mNetworkCallback; + private TetheringEventCallback mTetheringEventCallback; + + @Before + public void setUp() throws IOException { + MockitoAnnotations.initMocks(this); + mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm); + mockService(mContext, TetheringManager.class, Context.TETHERING_SERVICE, mTm); + doReturn(true).when(mDeps).canScanOnInterface(any()); + doReturn(mTestNetworkIfaceWrapper).when(mDeps).getNetworkInterfaceByName(TEST_IFACE_NAME); + doReturn(mLocalOnlyIfaceWrapper).when(mDeps) + .getNetworkInterfaceByName(LOCAL_ONLY_IFACE_NAME); + doReturn(mTetheredIfaceWrapper).when(mDeps).getNetworkInterfaceByName(TETHERED_IFACE_NAME); + doReturn(mock(MdnsInterfaceSocket.class)) + .when(mDeps).createMdnsInterfaceSocket(any(), anyInt()); + final HandlerThread thread = new HandlerThread("MdnsSocketProviderTest"); + thread.start(); + mHandler = new Handler(thread.getLooper()); + + final ArgumentCaptor nwCallbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + final ArgumentCaptor teCallbackCaptor = + ArgumentCaptor.forClass(TetheringEventCallback.class); + mSocketProvider = new MdnsSocketProvider(mContext, thread.getLooper(), mDeps); + mHandler.post(mSocketProvider::startMonitoringSockets); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + verify(mCm).registerNetworkCallback(any(), nwCallbackCaptor.capture(), any()); + verify(mTm).registerTetheringEventCallback(any(), teCallbackCaptor.capture()); + + mNetworkCallback = nwCallbackCaptor.getValue(); + mTetheringEventCallback = teCallbackCaptor.getValue(); + } + + private class TestSocketCallback implements MdnsSocketProvider.SocketCallback { + private class SocketEvent { + public final Network mNetwork; + public final List mAddresses; + + SocketEvent(Network network, List addresses) { + mNetwork = network; + mAddresses = Collections.unmodifiableList(addresses); + } + } + + private class SocketCreatedEvent extends SocketEvent { + SocketCreatedEvent(Network nw, List addresses) { + super(nw, addresses); + } + } + + private class InterfaceDestroyedEvent extends SocketEvent { + InterfaceDestroyedEvent(Network nw, List addresses) { + super(nw, addresses); + } + } + + private class AddressesChangedEvent extends SocketEvent { + AddressesChangedEvent(Network nw, List addresses) { + super(nw, addresses); + } + } + + private final ArrayTrackRecord.ReadHead mHistory = + new ArrayTrackRecord().newReadHead(); + + @Override + public void onSocketCreated(Network network, MdnsInterfaceSocket socket, + List addresses) { + mHistory.add(new SocketCreatedEvent(network, addresses)); + } + + @Override + public void onInterfaceDestroyed(Network network, MdnsInterfaceSocket socket) { + mHistory.add(new InterfaceDestroyedEvent(network, List.of())); + } + + @Override + public void onAddressesChanged(Network network, List addresses) { + mHistory.add(new AddressesChangedEvent(network, addresses)); + } + + public void expectedSocketCreatedForNetwork(Network network, List addresses) { + final SocketEvent event = mHistory.poll(DEFAULT_TIMEOUT, c -> true); + assertNotNull(event); + assertTrue(event instanceof SocketCreatedEvent); + assertEquals(network, event.mNetwork); + assertEquals(addresses, event.mAddresses); + } + + public void expectedInterfaceDestroyedForNetwork(Network network) { + final SocketEvent event = mHistory.poll(DEFAULT_TIMEOUT, c -> true); + assertNotNull(event); + assertTrue(event instanceof InterfaceDestroyedEvent); + assertEquals(network, event.mNetwork); + } + + public void expectedAddressesChangedForNetwork(Network network, + List addresses) { + final SocketEvent event = mHistory.poll(DEFAULT_TIMEOUT, c -> true); + assertNotNull(event); + assertTrue(event instanceof AddressesChangedEvent); + assertEquals(network, event.mNetwork); + assertEquals(event.mAddresses, addresses); + } + + public void expectedNoCallback() { + final SocketEvent event = mHistory.poll(NO_CALLBACK_TIMEOUT, c -> true); + assertNull(event); + } + } + + @Test + public void testSocketRequestAndUnrequestSocket() { + final TestSocketCallback testCallback1 = new TestSocketCallback(); + mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback1)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + testCallback1.expectedNoCallback(); + + final LinkProperties testLp = new LinkProperties(); + testLp.setInterfaceName(TEST_IFACE_NAME); + testLp.setLinkAddresses(List.of(LINKADDRV4)); + mHandler.post(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, testLp)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + verify(mTestNetworkIfaceWrapper).getNetworkInterface(); + testCallback1.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4)); + + final TestSocketCallback testCallback2 = new TestSocketCallback(); + mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback2)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + testCallback1.expectedNoCallback(); + testCallback2.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4)); + + final TestSocketCallback testCallback3 = new TestSocketCallback(); + mHandler.post(() -> mSocketProvider.requestSocket(null /* network */, testCallback3)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + testCallback1.expectedNoCallback(); + testCallback2.expectedNoCallback(); + testCallback3.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4)); + + mHandler.post(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged( + List.of(LOCAL_ONLY_IFACE_NAME))); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + verify(mLocalOnlyIfaceWrapper).getNetworkInterface(); + testCallback1.expectedNoCallback(); + testCallback2.expectedNoCallback(); + testCallback3.expectedSocketCreatedForNetwork(LOCAL_NETWORK, List.of()); + + mHandler.post(() -> mTetheringEventCallback.onTetheredInterfacesChanged( + List.of(TETHERED_IFACE_NAME))); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + verify(mTetheredIfaceWrapper).getNetworkInterface(); + testCallback1.expectedNoCallback(); + testCallback2.expectedNoCallback(); + testCallback3.expectedSocketCreatedForNetwork(LOCAL_NETWORK, List.of()); + + mHandler.post(() -> mSocketProvider.unrequestSocket(testCallback1)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + testCallback1.expectedNoCallback(); + testCallback2.expectedNoCallback(); + testCallback3.expectedNoCallback(); + + mHandler.post(() -> mNetworkCallback.onLost(TEST_NETWORK)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + testCallback1.expectedNoCallback(); + testCallback2.expectedInterfaceDestroyedForNetwork(TEST_NETWORK); + testCallback3.expectedInterfaceDestroyedForNetwork(TEST_NETWORK); + + mHandler.post(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(List.of())); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + testCallback1.expectedNoCallback(); + testCallback2.expectedNoCallback(); + testCallback3.expectedInterfaceDestroyedForNetwork(LOCAL_NETWORK); + + mHandler.post(() -> mSocketProvider.unrequestSocket(testCallback3)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + testCallback1.expectedNoCallback(); + testCallback2.expectedNoCallback(); + testCallback3.expectedNoCallback(); + } + + @Test + public void testAddressesChanged() throws Exception { + final TestSocketCallback testCallback = new TestSocketCallback(); + mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + testCallback.expectedNoCallback(); + + final LinkProperties testLp = new LinkProperties(); + testLp.setInterfaceName(TEST_IFACE_NAME); + testLp.setLinkAddresses(List.of(LINKADDRV4)); + mHandler.post(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, testLp)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + verify(mTestNetworkIfaceWrapper, times(1)).getNetworkInterface(); + testCallback.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4)); + + final LinkProperties newTestLp = new LinkProperties(); + newTestLp.setInterfaceName(TEST_IFACE_NAME); + newTestLp.setLinkAddresses(List.of(LINKADDRV4, LINKADDRV6)); + mHandler.post(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, newTestLp)); + HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT); + verify(mTestNetworkIfaceWrapper, times(1)).getNetworkInterface(); + testCallback.expectedAddressesChangedForNetwork( + TEST_NETWORK, List.of(LINKADDRV4, LINKADDRV6)); + } +}