From 9e53b630c52eae92ee761a76d7fef3db825db1fa Mon Sep 17 00:00:00 2001 From: Paul Hu Date: Mon, 14 Nov 2022 06:12:00 +0000 Subject: [PATCH 1/2] Put the Network info in MdnsServiceInfo In Nsd, every request has Network info to assign the specific network to do the mdns query. But the response MdnsServiceInfo only has interface index which is not very useful and need to transfer to a Network every time when using it because most APIs for apps to use the network are based on Network object. Thus, put the Network info into MdnsServiceInfo. Bug: 254166302 Test: atest FrameworksNetTests Change-Id: I2206a84636981fc7d9aa9deda0f18f60642bc7d7 --- .../mdns/ConnectivityMonitor.java | 5 + ...ctivityMonitorWithConnectivityManager.java | 9 ++ .../connectivity/mdns/MdnsResponse.java | 26 +++-- .../mdns/MdnsResponseDecoder.java | 7 +- .../connectivity/mdns/MdnsServiceInfo.java | 55 ++++++++- .../mdns/MdnsServiceTypeClient.java | 3 +- .../server/connectivity/mdns/MdnsSocket.java | 10 ++ .../connectivity/mdns/MdnsSocketClient.java | 10 +- .../MulticastNetworkInterfaceProvider.java | 8 +- ...tyMonitorWithConnectivityManagerTests.java | 3 +- .../mdns/MdnsResponseDecoderTests.java | 21 +++- .../connectivity/mdns/MdnsResponseTests.java | 41 ++++--- .../mdns/MdnsServiceInfoTest.java | 46 +++++++- .../mdns/MdnsServiceTypeClientTests.java | 107 +++++++++++------- 14 files changed, 267 insertions(+), 84 deletions(-) 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/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/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..f8452c93f0 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; @@ -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/MulticastNetworkInterfaceProvider.java b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java index e0d8fa66c5..644460dc4a 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,6 +142,11 @@ public class MulticastNetworkInterfaceProvider { return networkInterfaceWrappers; } + @Nullable + public Network getAvailableNetwork() { + return connectivityMonitor.getAvailableNetwork(); + } + private boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) { try { if ((networkInterface == null) 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( From 2f1de803154296fff7d26fb9d994f35964d1e2ad Mon Sep 17 00:00:00 2001 From: Paul Hu Date: Thu, 27 Oct 2022 03:27:19 +0000 Subject: [PATCH 2/2] Add MdnsInterfaceSocket and MdnsSocketProvider MdnsInterfaceSocket: using for listening the multicast socket on specific interface. MdnsSocketProvider: using for creating and managing the multiple multicast sockets. - Listen NetworkCallback and TetheringEventCallback to know the network connected / disconnected changes. And use the callbacks to create / remove the sockets for mdns discovery Bug: 254166302 Test: atest FrameworksNetTests Change-Id: Icd8424792e89e71198fa31f9ec92e1ec8172128d --- service/Android.bp | 1 + .../mdns/MdnsDiscoveryManager.java | 4 +- .../mdns/MdnsInterfaceSocket.java | 175 +++++++ .../connectivity/mdns/MdnsSearchOptions.java | 39 +- .../server/connectivity/mdns/MdnsSocket.java | 4 +- .../connectivity/mdns/MdnsSocketProvider.java | 460 ++++++++++++++++++ .../MulticastNetworkInterfaceProvider.java | 7 +- .../mdns/MdnsSocketClientTests.java | 6 +- .../mdns/MdnsSocketProviderTest.java | 289 +++++++++++ 9 files changed, 970 insertions(+), 15 deletions(-) create mode 100644 service/mdns/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java create mode 100644 service/mdns/com/android/server/connectivity/mdns/MdnsSocketProvider.java create mode 100644 tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java 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/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/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/MdnsSocket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java index f8452c93f0..64c4495b0c 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java @@ -40,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; 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 644460dc4a..ade7b9594c 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java +++ b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java @@ -147,7 +147,8 @@ public class MulticastNetworkInterfaceProvider { return connectivityMonitor.getAvailableNetwork(); } - private boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) { + /*** Check whether given network interface can support mdns */ + public static boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) { try { if ((networkInterface == null) || networkInterface.isLoopback() @@ -166,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; @@ -175,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/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)); + } +}