Add mdns files and unit tests

- Put mdns code to service/mdns because we don't want to impact
  coverage for code that's not yet in use.
- Also fix some lint errors
  - Common typo errors
  - Lines longer than 100 characters
- Update IgnoreUpTo from S to SC_V2 because the tests should be
  ignored until T.

Bug: 232766079
Test: atest FrameworksNetTests
Change-Id: I19af3bf7ae004bd37960f2ad9014cf0c6a804c8d
This commit is contained in:
Paul Hu 2022-05-16 20:56:32 +08:00
commit 3e1f4489bf
38 changed files with 7249 additions and 0 deletions

View file

@ -181,6 +181,25 @@ java_library {
],
}
// TODO: Remove this temporary library and put code into module when test coverage is enough.
java_library {
name: "service-mdns",
sdk_version: "system_server_current",
min_sdk_version: "30",
srcs: [
"mdns/**/*.java",
],
libs: [
"framework-annotations-lib",
"framework-connectivity-pre-jarjar",
"framework-wifi.stubs.module_lib",
"service-connectivity-pre-jarjar",
],
visibility: [
"//packages/modules/Connectivity/tests:__subpackages__",
],
}
java_library {
name: "service-connectivity-protos",
sdk_version: "system_current",

View file

@ -0,0 +1,36 @@
/*
* Copyright (C) 2021 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;
/** Interface for monitoring connectivity changes. */
public interface ConnectivityMonitor {
/**
* Starts monitoring changes of connectivity of this device, which may indicate that the list of
* network interfaces available for multi-cast messaging has changed.
*/
void startWatchingConnectivityChanges();
/** Stops monitoring changes of connectivity. */
void stopWatchingConnectivityChanges();
void notifyConnectivityChange();
/** Listener interface for receiving connectivity changes. */
interface Listener {
void onConnectivityChanged();
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (C) 2021 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.TargetApi;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Build;
import com.android.server.connectivity.mdns.util.MdnsLogger;
/** Class for monitoring connectivity changes using {@link ConnectivityManager}. */
public class ConnectivityMonitorWithConnectivityManager implements ConnectivityMonitor {
private static final String TAG = "ConnMntrWConnMgr";
private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
private final Listener listener;
private final ConnectivityManager.NetworkCallback networkCallback;
private final ConnectivityManager connectivityManager;
// 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;
@SuppressWarnings({"nullness:assignment", "nullness:method.invocation"})
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener) {
this.listener = listener;
connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
networkCallback =
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
LOGGER.log("network available.");
notifyConnectivityChange();
}
@Override
public void onLost(Network network) {
LOGGER.log("network lost.");
notifyConnectivityChange();
}
@Override
public void onUnavailable() {
LOGGER.log("network unavailable.");
notifyConnectivityChange();
}
};
}
@Override
public void notifyConnectivityChange() {
listener.onConnectivityChanged();
}
/**
* Starts monitoring changes of connectivity of this device, which may indicate that the list of
* network interfaces available for multi-cast messaging has changed.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void startWatchingConnectivityChanges() {
LOGGER.log("Start watching connectivity changes");
if (isCallbackRegistered) {
return;
}
connectivityManager.registerNetworkCallback(
new NetworkRequest.Builder().addTransportType(
NetworkCapabilities.TRANSPORT_WIFI).build(),
networkCallback);
isCallbackRegistered = true;
}
/** Stops monitoring changes of connectivity. */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void stopWatchingConnectivityChanges() {
LOGGER.log("Stop watching connectivity changes");
if (!isCallbackRegistered) {
return;
}
connectivityManager.unregisterNetworkCallback(networkCallback);
isCallbackRegistered = false;
}
}

View file

@ -0,0 +1,155 @@
/*
* Copyright (C) 2021 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.text.TextUtils;
import android.util.Pair;
import com.android.server.connectivity.mdns.util.MdnsLogger;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
/**
* A {@link Callable} that builds and enqueues a mDNS query to send over the multicast socket. If a
* query is built and enqueued successfully, then call to {@link #call()} returns the transaction ID
* and the list of the subtypes in the query as a {@link Pair}. If a query is failed to build, or if
* it can not be enqueued, then call to {@link #call()} returns {@code null}.
*/
// TODO(b/177655645): Resolve nullness suppression.
@SuppressWarnings("nullness")
public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<String>>> {
private static final String TAG = "MdnsQueryCallable";
private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
private static final List<Integer> castShellEmulatorMdnsPorts;
static {
castShellEmulatorMdnsPorts = new ArrayList<>();
String[] stringPorts = MdnsConfigs.castShellEmulatorMdnsPorts();
for (String port : stringPorts) {
try {
castShellEmulatorMdnsPorts.add(Integer.parseInt(port));
} catch (NumberFormatException e) {
// Ignore.
}
}
}
private final WeakReference<MdnsSocketClient> weakRequestSender;
private final MdnsPacketWriter packetWriter;
private final String[] serviceTypeLabels;
private final List<String> subtypes;
private final boolean expectUnicastResponse;
private final int transactionId;
EnqueueMdnsQueryCallable(
@NonNull MdnsSocketClient requestSender,
@NonNull MdnsPacketWriter packetWriter,
@NonNull String serviceType,
@NonNull Collection<String> subtypes,
boolean expectUnicastResponse,
int transactionId) {
weakRequestSender = new WeakReference<>(requestSender);
this.packetWriter = packetWriter;
serviceTypeLabels = TextUtils.split(serviceType, "\\.");
this.subtypes = new ArrayList<>(subtypes);
this.expectUnicastResponse = expectUnicastResponse;
this.transactionId = transactionId;
}
@Override
public Pair<Integer, List<String>> call() {
try {
MdnsSocketClient requestSender = weakRequestSender.get();
if (requestSender == null) {
return null;
}
int numQuestions = 1;
if (!subtypes.isEmpty()) {
numQuestions += subtypes.size();
}
// Header.
packetWriter.writeUInt16(transactionId); // transaction ID
packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
packetWriter.writeUInt16(numQuestions); // number of questions
packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
packetWriter.writeUInt16(0); // number of authority entries
packetWriter.writeUInt16(0); // number of additional records
// Question(s). There will be one question for each (fqdn+subtype, recordType)
// combination,
// as well as one for each (fqdn, recordType) combination.
for (String subtype : subtypes) {
String[] labels = new String[serviceTypeLabels.length + 2];
labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
labels[1] = MdnsConstants.SUBTYPE_LABEL;
System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
packetWriter.writeLabels(labels);
packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
packetWriter.writeUInt16(
MdnsConstants.QCLASS_INTERNET
| (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
}
packetWriter.writeLabels(serviceTypeLabels);
packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
packetWriter.writeUInt16(
MdnsConstants.QCLASS_INTERNET
| (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
InetAddress mdnsAddress = MdnsConstants.getMdnsIPv4Address();
if (requestSender.isOnIPv6OnlyNetwork()) {
mdnsAddress = MdnsConstants.getMdnsIPv6Address();
}
sendPacketTo(requestSender,
new InetSocketAddress(mdnsAddress, MdnsConstants.MDNS_PORT));
for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
sendPacketTo(requestSender, new InetSocketAddress(mdnsAddress, emulatorPort));
}
return Pair.create(transactionId, subtypes);
} catch (IOException e) {
LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
TextUtils.join(",", subtypes)), e);
return null;
}
}
private void sendPacketTo(MdnsSocketClient requestSender, InetSocketAddress address)
throws IOException {
DatagramPacket packet = packetWriter.getPacket(address);
if (expectUnicastResponse) {
requestSender.sendUnicastPacket(packet);
} else {
requestSender.sendMulticastPacket(packet);
}
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 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.util.ArraySet;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
/**
* This class provides {@link ScheduledExecutorService} instances to {@link MdnsServiceTypeClient}
* instances, and provides method to shutdown all the created executors.
*/
public class ExecutorProvider {
private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
new ArraySet<>();
/** Returns a new {@link ScheduledExecutorService} instance. */
public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
// TODO: actually use a pool ?
ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1);
serviceTypeClientSchedulerExecutors.add(executor);
return executor;
}
/** Shuts down all the created {@link ScheduledExecutorService} instances. */
public void shutdownAll() {
for (ScheduledExecutorService executor : serviceTypeClientSchedulerExecutors) {
executor.shutdownNow();
}
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (C) 2021 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;
/**
* mDNS configuration values.
*
* TODO: consider making some of these adjustable via flags.
*/
public class MdnsConfigs {
public static String[] castShellEmulatorMdnsPorts() {
return new String[0];
}
public static long initialTimeBetweenBurstsMs() {
return 5000L;
}
public static long timeBetweenBurstsMs() {
return 20_000L;
}
public static int queriesPerBurst() {
return 3;
}
public static long timeBetweenQueriesInBurstMs() {
return 1000L;
}
public static int queriesPerBurstPassive() {
return 1;
}
public static boolean alwaysAskForUnicastResponseInEachBurst() {
return false;
}
public static boolean useSessionIdToScheduleMdnsTask() {
return false;
}
public static boolean shouldCancelScanTaskWhenFutureIsNull() {
return false;
}
public static long sleepTimeForSocketThreadMs() {
return 20_000L;
}
public static boolean checkMulticastResponse() {
return false;
}
public static boolean useSeparateSocketToSendUnicastQuery() {
return false;
}
public static long checkMulticastResponseIntervalMs() {
return 10_000L;
}
public static boolean clearMdnsPacketQueueAfterDiscoveryStops() {
return true;
}
public static boolean allowAddMdnsPacketAfterDiscoveryStops() {
return false;
}
public static int mdnsPacketQueueMaxSize() {
return Integer.MAX_VALUE;
}
public static boolean preferIpv6() {
return false;
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (C) 2021 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.TargetApi;
import android.os.Build;
import com.android.internal.annotations.VisibleForTesting;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/** mDNS-related constants. */
// TODO(b/177655645): Resolve nullness suppression.
@SuppressWarnings("nullness")
@VisibleForTesting
public final class MdnsConstants {
public static final int MDNS_PORT = 5353;
// Flags word format is:
// 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
// QR [ Opcode ] AA TC RD RA Z AD CD [ Rcode ]
// See http://www.networksorcery.com/enp/protocol/dns.htm
// For responses, QR bit should be 1, AA - CD bits should be ignored, and all other bits
// should be 0.
public static final int FLAGS_QUERY = 0x0000;
public static final int FLAGS_RESPONSE_MASK = 0xF80F;
public static final int FLAGS_RESPONSE = 0x8000;
public static final int QCLASS_INTERNET = 0x0001;
public static final int QCLASS_UNICAST = 0x8000;
public static final String SUBTYPE_LABEL = "_sub";
public static final String SUBTYPE_PREFIX = "_";
private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
private static InetAddress mdnsAddress;
private static Charset utf8Charset;
private MdnsConstants() {
}
public static InetAddress getMdnsIPv4Address() {
synchronized (MdnsConstants.class) {
InetAddress addr = null;
try {
addr = InetAddress.getByName(MDNS_IPV4_HOST_ADDRESS);
} catch (UnknownHostException e) {
/* won't happen */
}
mdnsAddress = addr;
return mdnsAddress;
}
}
public static InetAddress getMdnsIPv6Address() {
synchronized (MdnsConstants.class) {
InetAddress addr = null;
try {
addr = InetAddress.getByName(MDNS_IPV6_HOST_ADDRESS);
} catch (UnknownHostException e) {
/* won't happen */
}
mdnsAddress = addr;
return mdnsAddress;
}
}
public static Charset getUtf8Charset() {
synchronized (MdnsConstants.class) {
if (utf8Charset == null) {
utf8Charset = getUtf8CharsetOnKitKat();
}
return utf8Charset;
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private static Charset getUtf8CharsetOnKitKat() {
return StandardCharsets.UTF_8;
}
}

View file

@ -0,0 +1,146 @@
/*
* Copyright (C) 2021 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.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.text.TextUtils;
import android.util.ArrayMap;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.connectivity.mdns.util.MdnsLogger;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
/**
* This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
* notify them when a mDNS service instance is found, updated, or removed?
*/
public class MdnsDiscoveryManager implements MdnsSocketClient.Callback {
private static final MdnsLogger LOGGER = new MdnsLogger("MdnsDiscoveryManager");
private final ExecutorProvider executorProvider;
private final MdnsSocketClient socketClient;
private final Map<String, MdnsServiceTypeClient> serviceTypeClients = new ArrayMap<>();
public MdnsDiscoveryManager(
@NonNull ExecutorProvider executorProvider, @NonNull MdnsSocketClient socketClient) {
this.executorProvider = executorProvider;
this.socketClient = socketClient;
}
/**
* Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
* {@code listener} for receiving mDNS service discovery responses.
*
* @param serviceType The type of the service to discover.
* @param listener The {@link MdnsServiceBrowserListener} listener.
* @param searchOptions The {@link MdnsSearchOptions} to be used for discovering {@code
* serviceType}.
*/
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
public synchronized void registerListener(
@NonNull String serviceType,
@NonNull MdnsServiceBrowserListener listener,
@NonNull MdnsSearchOptions searchOptions) {
LOGGER.log(
"Registering listener for subtypes: %s",
TextUtils.join(",", searchOptions.getSubtypes()));
if (serviceTypeClients.isEmpty()) {
// First listener. Starts the socket client.
try {
socketClient.startDiscovery();
} catch (IOException e) {
LOGGER.e("Failed to start discover.", e);
return;
}
}
// All listeners of the same service types shares the same MdnsServiceTypeClient.
MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
if (serviceTypeClient == null) {
serviceTypeClient = createServiceTypeClient(serviceType);
serviceTypeClients.put(serviceType, serviceTypeClient);
}
serviceTypeClient.startSendAndReceive(listener, searchOptions);
}
/**
* Unregister {@code listener} for receiving mDNS service discovery responses. IF no listener is
* registered for the given service type, stops discovery for the service type.
*
* @param serviceType The type of the service to discover.
* @param listener The {@link MdnsServiceBrowserListener} listener.
*/
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
public synchronized void unregisterListener(
@NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
LOGGER.log("Unregistering listener for service type: %s", serviceType);
MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
if (serviceTypeClient == null) {
return;
}
if (serviceTypeClient.stopSendAndReceive(listener)) {
// No listener is registered for the service type anymore, remove it from the list of
// the
// service type clients.
serviceTypeClients.remove(serviceType);
if (serviceTypeClients.isEmpty()) {
// No discovery request. Stops the socket client.
socketClient.stopDiscovery();
}
}
}
@Override
public synchronized void onResponseReceived(@NonNull MdnsResponse response) {
String[] name =
response.getPointerRecords().isEmpty()
? null
: response.getPointerRecords().get(0).getName();
if (name != null) {
for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
String[] serviceType = serviceTypeClient.getServiceTypeLabels();
if ((Arrays.equals(name, serviceType)
|| ((name.length == (serviceType.length + 2))
&& name[1].equals(MdnsConstants.SUBTYPE_LABEL)
&& MdnsRecord.labelsAreSuffix(serviceType, name)))) {
serviceTypeClient.processResponse(response);
return;
}
}
}
}
@Override
public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
}
}
@VisibleForTesting
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
return new MdnsServiceTypeClient(
serviceType, socketClient,
executorProvider.newServiceTypeClientSchedulerExecutor());
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import java.util.Objects;
/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
// TODO(b/177655645): Resolve nullness suppression.
@SuppressWarnings("nullness")
@VisibleForTesting
public class MdnsInetAddressRecord extends MdnsRecord {
private Inet6Address inet6Address;
private Inet4Address inet4Address;
/**
* Constructs the {@link MdnsRecord}
*
* @param name the service host name
* @param type the type of record (either Type 'AAAA' or Type 'A')
* @param reader the reader to read the record from.
*/
public MdnsInetAddressRecord(String[] name, int type, MdnsPacketReader reader)
throws IOException {
super(name, type, reader);
}
/** Returns the IPv6 address. */
public Inet6Address getInet6Address() {
return inet6Address;
}
/** Returns the IPv4 address. */
public Inet4Address getInet4Address() {
return inet4Address;
}
@Override
protected void readData(MdnsPacketReader reader) throws IOException {
int size = 4;
if (super.getType() == MdnsRecord.TYPE_AAAA) {
size = 16;
}
byte[] buf = new byte[size];
reader.readBytes(buf);
try {
InetAddress address = InetAddress.getByAddress(buf);
if (address instanceof Inet4Address) {
inet4Address = (Inet4Address) address;
inet6Address = null;
} else if (address instanceof Inet6Address) {
inet4Address = null;
inet6Address = (Inet6Address) address;
} else {
inet4Address = null;
inet6Address = null;
}
} catch (UnknownHostException e) {
// Ignore exception
}
}
@Override
protected void writeData(MdnsPacketWriter writer) throws IOException {
byte[] buf = null;
if (inet4Address != null) {
buf = inet4Address.getAddress();
} else if (inet6Address != null) {
buf = inet6Address.getAddress();
}
if (buf != null) {
writer.writeBytes(buf);
}
}
@Override
public String toString() {
String type = "AAAA";
if (super.getType() == MdnsRecord.TYPE_A) {
type = "A";
}
return String.format(
Locale.ROOT, "%s: Inet4Address: %s Inet6Address: %s", type, inet4Address,
inet6Address);
}
@Override
public int hashCode() {
return (super.hashCode() * 31)
+ Objects.hashCode(inet4Address)
+ Objects.hashCode(inet6Address);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof MdnsInetAddressRecord)) {
return false;
}
return super.equals(other)
&& Objects.equals(inet4Address, ((MdnsInetAddressRecord) other).inet4Address)
&& Objects.equals(inet6Address, ((MdnsInetAddressRecord) other).inet6Address);
}
}

View file

@ -0,0 +1,254 @@
/*
* Copyright (C) 2021 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.util.SparseArray;
import java.io.EOFException;
import java.io.IOException;
import java.net.DatagramPacket;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/** Simple decoder for mDNS packets. */
public class MdnsPacketReader {
private final byte[] buf;
private final int count;
private final SparseArray<LabelEntry> labelDictionary;
private int pos;
private int limit;
/** Constructs a reader for the given packet. */
public MdnsPacketReader(DatagramPacket packet) {
buf = packet.getData();
count = packet.getLength();
pos = 0;
limit = -1;
labelDictionary = new SparseArray<>(16);
}
/**
* Sets a temporary limit (from the current read position) for subsequent reads. Any attempt to
* read past this limit will result in an EOFException.
*
* @param limit The new limit.
* @throws IOException If there is insufficient data for the new limit.
*/
public void setLimit(int limit) throws IOException {
if (limit >= 0) {
if (pos + limit <= count) {
this.limit = pos + limit;
} else {
throw new IOException(
String.format(
Locale.ROOT,
"attempt to set limit beyond available data: %d exceeds %d",
pos + limit,
count));
}
}
}
/** Clears the limit set by {@link #setLimit}. */
public void clearLimit() {
limit = -1;
}
/**
* Returns the number of bytes left to read, between the current read position and either the
* limit (if set) or the end of the packet.
*/
public int getRemaining() {
return (limit >= 0 ? limit : count) - pos;
}
/**
* Reads an unsigned 8-bit integer.
*
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
* read.
*/
public int readUInt8() throws EOFException {
checkRemaining(1);
byte val = buf[pos++];
return val & 0xFF;
}
/**
* Reads an unsigned 16-bit integer.
*
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
* read.
*/
public int readUInt16() throws EOFException {
checkRemaining(2);
int val = (buf[pos++] & 0xFF) << 8;
val |= (buf[pos++]) & 0xFF;
return val;
}
/**
* Reads an unsigned 32-bit integer.
*
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
* read.
*/
public long readUInt32() throws EOFException {
checkRemaining(4);
long val = (long) (buf[pos++] & 0xFF) << 24;
val |= (long) (buf[pos++] & 0xFF) << 16;
val |= (long) (buf[pos++] & 0xFF) << 8;
val |= buf[pos++] & 0xFF;
return val;
}
/**
* Reads a sequence of labels and returns them as an array of strings. A sequence of labels is
* either a sequence of strings terminated by a NUL byte, a sequence of strings terminated by a
* pointer, or a pointer.
*
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
* read.
* @throws IOException If invalid data is read.
*/
public String[] readLabels() throws IOException {
List<String> result = new ArrayList<>(5);
LabelEntry previousEntry = null;
while (getRemaining() > 0) {
byte nextByte = peekByte();
if (nextByte == 0) {
// A NUL byte terminates a sequence of labels.
skip(1);
break;
}
int currentOffset = pos;
boolean isLabelPointer = (nextByte & 0xC0) == 0xC0;
if (isLabelPointer) {
// A pointer terminates a sequence of labels. Store the pointer value in the
// previous label entry.
int labelOffset = ((readUInt8() & 0x3F) << 8) | (readUInt8() & 0xFF);
if (previousEntry != null) {
previousEntry.nextOffset = labelOffset;
}
// Follow the chain of labels starting at this pointer, adding all of them onto the
// result.
while (labelOffset != 0) {
LabelEntry entry = labelDictionary.get(labelOffset);
if (entry == null) {
throw new IOException(
String.format(Locale.ROOT, "Invalid label pointer: %04X",
labelOffset));
}
result.add(entry.label);
labelOffset = entry.nextOffset;
}
break;
} else {
// It's an ordinary label. Chain it onto the previous label entry (if any), and add
// it onto the result.
String val = readString();
LabelEntry newEntry = new LabelEntry(val);
labelDictionary.put(currentOffset, newEntry);
if (previousEntry != null) {
previousEntry.nextOffset = currentOffset;
}
previousEntry = newEntry;
result.add(val);
}
}
return result.toArray(new String[result.size()]);
}
/**
* Reads a length-prefixed string.
*
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
* read.
*/
public String readString() throws EOFException {
int len = readUInt8();
checkRemaining(len);
String val = new String(buf, pos, len, MdnsConstants.getUtf8Charset());
pos += len;
return val;
}
/**
* Reads a specific number of bytes.
*
* @param bytes The array to fill.
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
* read.
*/
public void readBytes(byte[] bytes) throws EOFException {
checkRemaining(bytes.length);
System.arraycopy(buf, pos, bytes, 0, bytes.length);
pos += bytes.length;
}
/**
* Skips over the given number of bytes.
*
* @param count The number of bytes to read and discard.
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
* read.
*/
public void skip(int count) throws EOFException {
checkRemaining(count);
pos += count;
}
/**
* Peeks at and returns the next byte in the packet, without advancing the read position.
*
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
* read.
*/
public byte peekByte() throws EOFException {
checkRemaining(1);
return buf[pos];
}
/** Returns the current byte position of the reader for the data packet. */
public int getPosition() {
return pos;
}
// Checks if the number of remaining bytes to be read in the packet is at least |count|.
private void checkRemaining(int count) throws EOFException {
if (getRemaining() < count) {
throw new EOFException();
}
}
private static class LabelEntry {
public final String label;
public int nextOffset = 0;
public LabelEntry(String label) {
this.label = label;
}
}
}

View file

@ -0,0 +1,223 @@
/*
* Copyright (C) 2021 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 java.io.IOException;
import java.net.DatagramPacket;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/** Simple encoder for mDNS packets. */
public class MdnsPacketWriter {
private static final int MDNS_POINTER_MASK = 0xC000;
private final byte[] data;
private final Map<Integer, String[]> labelDictionary;
private int pos = 0;
private int savedWritePos = -1;
/**
* Constructs a writer for a new packet.
*
* @param maxSize The maximum size of a packet.
*/
public MdnsPacketWriter(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("invalid size");
}
data = new byte[maxSize];
labelDictionary = new HashMap<>();
}
/** Returns the current write position. */
public int getWritePosition() {
return pos;
}
/**
* Saves the current write position and then rewinds the write position by the given number of
* bytes. This is useful for updating length fields earlier in the packet. Rewinds cannot be
* nested.
*
* @param position The position to rewind to.
* @throws IOException If the count would go beyond the beginning of the packet, or if there is
* already a rewind in effect.
*/
public void rewind(int position) throws IOException {
if ((savedWritePos != -1) || (position > pos) || (position < 0)) {
throw new IOException("invalid rewind");
}
savedWritePos = pos;
pos = position;
}
/**
* Sets the current write position to what it was prior to the last rewind.
*
* @throws IOException If there was no rewind in effect.
*/
public void unrewind() throws IOException {
if (savedWritePos == -1) {
throw new IOException("no rewind is in effect");
}
pos = savedWritePos;
savedWritePos = -1;
}
/** Clears any rewind state. */
public void clearRewind() {
savedWritePos = -1;
}
/**
* Writes an unsigned 8-bit integer.
*
* @param value The value to write.
* @throws IOException If there is not enough space remaining in the packet.
*/
public void writeUInt8(int value) throws IOException {
checkRemaining(1);
data[pos++] = (byte) (value & 0xFF);
}
/**
* Writes an unsigned 16-bit integer.
*
* @param value The value to write.
* @throws IOException If there is not enough space remaining in the packet.
*/
public void writeUInt16(int value) throws IOException {
checkRemaining(2);
data[pos++] = (byte) ((value >>> 8) & 0xFF);
data[pos++] = (byte) (value & 0xFF);
}
/**
* Writes an unsigned 32-bit integer.
*
* @param value The value to write.
* @throws IOException If there is not enough space remaining in the packet.
*/
public void writeUInt32(long value) throws IOException {
checkRemaining(4);
data[pos++] = (byte) ((value >>> 24) & 0xFF);
data[pos++] = (byte) ((value >>> 16) & 0xFF);
data[pos++] = (byte) ((value >>> 8) & 0xFF);
data[pos++] = (byte) (value & 0xFF);
}
/**
* Writes a specific number of bytes.
*
* @param data The array to write.
* @throws IOException If there is not enough space remaining in the packet.
*/
public void writeBytes(byte[] data) throws IOException {
checkRemaining(data.length);
System.arraycopy(data, 0, this.data, pos, data.length);
pos += data.length;
}
/**
* Writes a string.
*
* @param value The string to write.
* @throws IOException If there is not enough space remaining in the packet.
*/
public void writeString(String value) throws IOException {
byte[] utf8 = value.getBytes(MdnsConstants.getUtf8Charset());
writeUInt8(utf8.length);
writeBytes(utf8);
}
/**
* Writes a series of labels. Uses name compression.
*
* @param labels The labels to write.
* @throws IOException If there is not enough space remaining in the packet.
*/
public void writeLabels(String[] labels) throws IOException {
// See section 4.1.4 of RFC 1035 (http://tools.ietf.org/html/rfc1035) for a description
// of the name compression method used here.
int suffixLength = 0;
int suffixPointer = 0;
for (Map.Entry<Integer, String[]> entry : labelDictionary.entrySet()) {
int existingOffset = entry.getKey();
String[] existingLabels = entry.getValue();
if (Arrays.equals(existingLabels, labels)) {
writePointer(existingOffset);
return;
} else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
// Keep track of the longest matching suffix so far.
if (existingLabels.length > suffixLength) {
suffixLength = existingLabels.length;
suffixPointer = existingOffset;
}
}
}
if (suffixLength > 0) {
for (int i = 0; i < (labels.length - suffixLength); ++i) {
writeString(labels[i]);
}
writePointer(suffixPointer);
} else {
int[] offsets = new int[labels.length];
for (int i = 0; i < labels.length; ++i) {
offsets[i] = getWritePosition();
writeString(labels[i]);
}
writeUInt8(0); // NUL terminator
// Add entries to the label dictionary for each suffix of the label list, including
// the whole list itself.
for (int i = 0, len = labels.length; i < labels.length; ++i, --len) {
String[] value = new String[len];
System.arraycopy(labels, i, value, 0, len);
labelDictionary.put(offsets[i], value);
}
}
}
/** Returns the number of bytes that can still be written. */
public int getRemaining() {
return data.length - pos;
}
// Writes a pointer to a label.
private void writePointer(int offset) throws IOException {
writeUInt16(MDNS_POINTER_MASK | offset);
}
// Checks if the remaining space in the packet is at least |count|.
private void checkRemaining(int count) throws IOException {
if (getRemaining() < count) {
throw new IOException();
}
}
/** Builds and returns the packet. */
public DatagramPacket getPacket(SocketAddress destAddress) throws IOException {
return new DatagramPacket(data, pos, destAddress);
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.Arrays;
/** An mDNS "PTR" record, which holds a name (the "pointer"). */
// TODO(b/177655645): Resolve nullness suppression.
@SuppressWarnings("nullness")
@VisibleForTesting
public class MdnsPointerRecord extends MdnsRecord {
private String[] pointer;
public MdnsPointerRecord(String[] name, MdnsPacketReader reader) throws IOException {
super(name, TYPE_PTR, reader);
}
/** Returns the pointer as an array of labels. */
public String[] getPointer() {
return pointer;
}
@Override
protected void readData(MdnsPacketReader reader) throws IOException {
pointer = reader.readLabels();
}
@Override
protected void writeData(MdnsPacketWriter writer) throws IOException {
writer.writeLabels(pointer);
}
public boolean hasSubtype() {
return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
}
public String getSubtype() {
return hasSubtype() ? name[0] : null;
}
@Override
public String toString() {
return "PTR: " + labelsToString(name) + " -> " + labelsToString(pointer);
}
@Override
public int hashCode() {
return (super.hashCode() * 31) + Arrays.hashCode(pointer);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof MdnsPointerRecord)) {
return false;
}
return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
}
}

View file

@ -0,0 +1,253 @@
/*
* Copyright (C) 2021 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.os.SystemClock;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Abstract base class for mDNS records. Stores the header fields and provides methods for reading
* the record from and writing it to a packet.
*/
// TODO(b/177655645): Resolve nullness suppression.
@SuppressWarnings("nullness")
public abstract class MdnsRecord {
public static final int TYPE_A = 0x0001;
public static final int TYPE_AAAA = 0x001C;
public static final int TYPE_PTR = 0x000C;
public static final int TYPE_SRV = 0x0021;
public static final int TYPE_TXT = 0x0010;
/** Status indicating that the record is current. */
public static final int STATUS_OK = 0;
/** Status indicating that the record has expired (TTL reached 0). */
public static final int STATUS_EXPIRED = 1;
/** Status indicating that the record should be refreshed (Less than half of TTL remains.) */
public static final int STATUS_NEEDS_REFRESH = 2;
protected final String[] name;
private final int type;
private final int cls;
private final long receiptTimeMillis;
private final long ttlMillis;
private Object key;
/**
* Constructs a new record with the given name and type.
*
* @param reader The reader to read the record from.
* @throws IOException If an error occurs while reading the packet.
*/
protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException {
this.name = name;
this.type = type;
cls = reader.readUInt16();
ttlMillis = TimeUnit.SECONDS.toMillis(reader.readUInt32());
int dataLength = reader.readUInt16();
receiptTimeMillis = SystemClock.elapsedRealtime();
reader.setLimit(dataLength);
readData(reader);
reader.clearLimit();
}
/**
* Converts an array of labels into their dot-separated string representation. This method
* should
* be used for logging purposes only.
*/
public static String labelsToString(String[] labels) {
if (labels == null) {
return null;
}
return TextUtils.join(".", labels);
}
/** Tests if |list1| is a suffix of |list2|. */
public static boolean labelsAreSuffix(String[] list1, String[] list2) {
int offset = list2.length - list1.length;
if (offset < 1) {
return false;
}
for (int i = 0; i < list1.length; ++i) {
if (!list1[i].equals(list2[i + offset])) {
return false;
}
}
return true;
}
/** Returns the record's receipt (creation) time. */
public final long getReceiptTime() {
return receiptTimeMillis;
}
/** Returns the record's name. */
public String[] getName() {
return name;
}
/** Returns the record's original TTL, in milliseconds. */
public final long getTtl() {
return ttlMillis;
}
/** Returns the record's type. */
public final int getType() {
return type;
}
/**
* Returns the record's remaining TTL.
*
* @param now The current system time.
* @return The remaning TTL, in milliseconds.
*/
public long getRemainingTTL(final long now) {
long age = now - receiptTimeMillis;
if (age > ttlMillis) {
return 0;
}
return ttlMillis - age;
}
/**
* Reads the record's payload from a packet.
*
* @param reader The reader to use.
* @throws IOException If an I/O error occurs.
*/
protected abstract void readData(MdnsPacketReader reader) throws IOException;
/**
* Writes the record to a packet.
*
* @param writer The writer to use.
* @param now The current system time. This is used when writing the updated TTL.
*/
@VisibleForTesting
public final void write(MdnsPacketWriter writer, long now) throws IOException {
writer.writeLabels(name);
writer.writeUInt16(type);
writer.writeUInt16(cls);
writer.writeUInt32(TimeUnit.MILLISECONDS.toSeconds(getRemainingTTL(now)));
int dataLengthPos = writer.getWritePosition();
writer.writeUInt16(0); // data length
int dataPos = writer.getWritePosition();
writeData(writer);
// Calculate amount of data written, and overwrite the data field earlier in the packet.
int endPos = writer.getWritePosition();
int dataLength = endPos - dataPos;
writer.rewind(dataLengthPos);
writer.writeUInt16(dataLength);
writer.unrewind();
}
/**
* Writes the record's payload to a packet.
*
* @param writer The writer to use.
* @throws IOException If an I/O error occurs.
*/
protected abstract void writeData(MdnsPacketWriter writer) throws IOException;
/** Gets the status of the record. */
public int getStatus(final long now) {
final long age = now - receiptTimeMillis;
if (age > ttlMillis) {
return STATUS_EXPIRED;
}
if (age > (ttlMillis / 2)) {
return STATUS_NEEDS_REFRESH;
}
return STATUS_OK;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof MdnsRecord)) {
return false;
}
MdnsRecord otherRecord = (MdnsRecord) other;
return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(name), type);
}
/**
* Returns an opaque object that uniquely identifies this record through a combination of its
* type
* and name. Suitable for use as a key in caches.
*/
public final Object getKey() {
if (key == null) {
key = new Key(type, name);
}
return key;
}
private static final class Key {
private final int recordType;
private final String[] recordName;
public Key(int recordType, String[] recordName) {
this.recordType = recordType;
this.recordName = recordName;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Key)) {
return false;
}
Key otherKey = (Key) other;
return (recordType == otherKey.recordType) && Arrays.equals(recordName,
otherKey.recordName);
}
@Override
public int hashCode() {
return (recordType * 31) + Arrays.hashCode(recordName);
}
}
}

View file

@ -0,0 +1,380 @@
/*
* Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/** An mDNS response. */
// TODO(b/177655645): Resolve nullness suppression.
@SuppressWarnings("nullness")
public class MdnsResponse {
private final List<MdnsRecord> records;
private final List<MdnsPointerRecord> pointerRecords;
private MdnsServiceRecord serviceRecord;
private MdnsTextRecord textRecord;
private MdnsInetAddressRecord inet4AddressRecord;
private MdnsInetAddressRecord inet6AddressRecord;
private long lastUpdateTime;
/** Constructs a new, empty response. */
public MdnsResponse(long now) {
lastUpdateTime = now;
records = new LinkedList<>();
pointerRecords = new LinkedList<>();
}
// This generic typed helper compares records for equality.
// Returns True if records are the same.
private <T> boolean recordsAreSame(T a, T b) {
return ((a == null) && (b == null)) || ((a != null) && (b != null) && a.equals(b));
}
/**
* Adds a pointer record.
*
* @return <code>true</code> if the record was added, or <code>false</code> if a matching
* pointer
* record is already present in the response.
*/
public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
if (!pointerRecords.contains(pointerRecord)) {
pointerRecords.add(pointerRecord);
records.add(pointerRecord);
return true;
}
return false;
}
/** Gets the pointer records. */
public synchronized List<MdnsPointerRecord> getPointerRecords() {
// Returns a shallow copy.
return new LinkedList<>(pointerRecords);
}
public synchronized boolean hasPointerRecords() {
return !pointerRecords.isEmpty();
}
@VisibleForTesting
/* package */ synchronized void clearPointerRecords() {
pointerRecords.clear();
}
public synchronized boolean hasSubtypes() {
for (MdnsPointerRecord pointerRecord : pointerRecords) {
if (pointerRecord.hasSubtype()) {
return true;
}
}
return false;
}
public synchronized List<String> getSubtypes() {
List<String> subtypes = null;
for (MdnsPointerRecord pointerRecord : pointerRecords) {
if (pointerRecord.hasSubtype()) {
if (subtypes == null) {
subtypes = new LinkedList<>();
}
subtypes.add(pointerRecord.getSubtype());
}
}
return subtypes;
}
@VisibleForTesting
public synchronized void removeSubtypes() {
Iterator<MdnsPointerRecord> iter = pointerRecords.iterator();
while (iter.hasNext()) {
MdnsPointerRecord pointerRecord = iter.next();
if (pointerRecord.hasSubtype()) {
iter.remove();
}
}
}
/** Sets the service record. */
public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) {
if (recordsAreSame(this.serviceRecord, serviceRecord)) {
return false;
}
if (this.serviceRecord != null) {
records.remove(this.serviceRecord);
}
this.serviceRecord = serviceRecord;
if (this.serviceRecord != null) {
records.add(this.serviceRecord);
}
return true;
}
/** Gets the service record. */
public synchronized MdnsServiceRecord getServiceRecord() {
return serviceRecord;
}
public synchronized boolean hasServiceRecord() {
return serviceRecord != null;
}
/** Sets the text record. */
public synchronized boolean setTextRecord(MdnsTextRecord textRecord) {
if (recordsAreSame(this.textRecord, textRecord)) {
return false;
}
if (this.textRecord != null) {
records.remove(this.textRecord);
}
this.textRecord = textRecord;
if (this.textRecord != null) {
records.add(this.textRecord);
}
return true;
}
/** Gets the text record. */
public synchronized MdnsTextRecord getTextRecord() {
return textRecord;
}
public synchronized boolean hasTextRecord() {
return textRecord != null;
}
/** Sets the IPv4 address record. */
public synchronized boolean setInet4AddressRecord(MdnsInetAddressRecord newInet4AddressRecord) {
if (recordsAreSame(this.inet4AddressRecord, newInet4AddressRecord)) {
return false;
}
if (this.inet4AddressRecord != null) {
records.remove(this.inet4AddressRecord);
}
if (newInet4AddressRecord != null && newInet4AddressRecord.getInet4Address() != null) {
this.inet4AddressRecord = newInet4AddressRecord;
records.add(this.inet4AddressRecord);
}
return true;
}
/** Gets the IPv4 address record. */
public synchronized MdnsInetAddressRecord getInet4AddressRecord() {
return inet4AddressRecord;
}
public synchronized boolean hasInet4AddressRecord() {
return inet4AddressRecord != null;
}
/** Sets the IPv6 address record. */
public synchronized boolean setInet6AddressRecord(MdnsInetAddressRecord newInet6AddressRecord) {
if (recordsAreSame(this.inet6AddressRecord, newInet6AddressRecord)) {
return false;
}
if (this.inet6AddressRecord != null) {
records.remove(this.inet6AddressRecord);
}
if (newInet6AddressRecord != null && newInet6AddressRecord.getInet6Address() != null) {
this.inet6AddressRecord = newInet6AddressRecord;
records.add(this.inet6AddressRecord);
}
return true;
}
/** Gets the IPv6 address record. */
public synchronized MdnsInetAddressRecord getInet6AddressRecord() {
return inet6AddressRecord;
}
public synchronized boolean hasInet6AddressRecord() {
return inet6AddressRecord != null;
}
/** Gets all of the records. */
public synchronized List<MdnsRecord> getRecords() {
return new LinkedList<>(records);
}
/**
* Merges any records that are present in another response into this one.
*
* @return <code>true</code> if any records were added or updated.
*/
public synchronized boolean mergeRecordsFrom(MdnsResponse other) {
lastUpdateTime = other.lastUpdateTime;
boolean updated = false;
List<MdnsPointerRecord> pointerRecords = other.getPointerRecords();
if (pointerRecords != null) {
for (MdnsPointerRecord pointerRecord : pointerRecords) {
if (addPointerRecord(pointerRecord)) {
updated = true;
}
}
}
MdnsServiceRecord serviceRecord = other.getServiceRecord();
if (serviceRecord != null) {
if (setServiceRecord(serviceRecord)) {
updated = true;
}
}
MdnsTextRecord textRecord = other.getTextRecord();
if (textRecord != null) {
if (setTextRecord(textRecord)) {
updated = true;
}
}
MdnsInetAddressRecord otherInet4AddressRecord = other.getInet4AddressRecord();
if (otherInet4AddressRecord != null && otherInet4AddressRecord.getInet4Address() != null) {
if (setInet4AddressRecord(otherInet4AddressRecord)) {
updated = true;
}
}
MdnsInetAddressRecord otherInet6AddressRecord = other.getInet6AddressRecord();
if (otherInet6AddressRecord != null && otherInet6AddressRecord.getInet6Address() != null) {
if (setInet6AddressRecord(otherInet6AddressRecord)) {
updated = true;
}
}
// If the hostname in the service record no longer matches the hostname in either of the
// address records, then drop the address records.
if (this.serviceRecord != null) {
boolean dropAddressRecords = false;
if (this.inet4AddressRecord != null) {
if (!Arrays.equals(
this.serviceRecord.getServiceHost(), this.inet4AddressRecord.getName())) {
dropAddressRecords = true;
}
}
if (this.inet6AddressRecord != null) {
if (!Arrays.equals(
this.serviceRecord.getServiceHost(), this.inet6AddressRecord.getName())) {
dropAddressRecords = true;
}
}
if (dropAddressRecords) {
setInet4AddressRecord(null);
setInet6AddressRecord(null);
updated = true;
}
}
return updated;
}
/**
* Tests if the response is complete. A response is considered complete if it contains PTR, SRV,
* TXT, and A (for IPv4) or AAAA (for IPv6) records.
*/
public synchronized boolean isComplete() {
return !pointerRecords.isEmpty()
&& (serviceRecord != null)
&& (textRecord != null)
&& (inet4AddressRecord != null || inet6AddressRecord != null);
}
/**
* Returns the key for this response. The key uniquely identifies the response by its service
* name.
*/
public synchronized String getServiceInstanceName() {
if (pointerRecords.isEmpty()) {
return null;
}
String[] pointers = pointerRecords.get(0).getPointer();
return ((pointers != null) && (pointers.length > 0)) ? pointers[0] : null;
}
/**
* Tests if this response is a goodbye message. This will be true if a service record is present
* and any of the records have a TTL of 0.
*/
public synchronized boolean isGoodbye() {
if (getServiceInstanceName() != null) {
for (MdnsRecord record : records) {
// Expiring PTR records with subtypes just signal a change in known supported
// criteria, not the device itself going offline, so ignore those.
if ((record instanceof MdnsPointerRecord)
&& ((MdnsPointerRecord) record).hasSubtype()) {
continue;
}
if (record.getTtl() == 0) {
return true;
}
}
}
return false;
}
/**
* Writes the response to a packet.
*
* @param writer The writer to use.
* @param now The current time. This is used to write updated TTLs that reflect the remaining
* TTL
* since the response was received.
* @return The number of records that were written.
* @throws IOException If an error occurred while writing (typically indicating overflow).
*/
public synchronized int write(MdnsPacketWriter writer, long now) throws IOException {
int count = 0;
for (MdnsPointerRecord pointerRecord : pointerRecords) {
pointerRecord.write(writer, now);
++count;
}
if (serviceRecord != null) {
serviceRecord.write(writer, now);
++count;
}
if (textRecord != null) {
textRecord.write(writer, now);
++count;
}
if (inet4AddressRecord != null) {
inet4AddressRecord.write(writer, now);
++count;
}
if (inet6AddressRecord != null) {
inet6AddressRecord.write(writer, now);
++count;
}
return count;
}
}

View file

@ -0,0 +1,298 @@
/*
* Copyright (C) 2021 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.os.SystemClock;
import com.android.server.connectivity.mdns.util.MdnsLogger;
import java.io.EOFException;
import java.io.IOException;
import java.net.DatagramPacket;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/** A class that decodes mDNS responses from UDP packets. */
// TODO(b/177655645): Resolve nullness suppression.
@SuppressWarnings("nullness")
public class MdnsResponseDecoder {
public static final int SUCCESS = 0;
private static final String TAG = "MdnsResponseDecoder";
private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
private final String[] serviceType;
private final Clock clock;
/** Constructs a new decoder that will extract responses for the given service type. */
public MdnsResponseDecoder(@NonNull Clock clock, @Nullable String[] serviceType) {
this.clock = clock;
this.serviceType = serviceType;
}
private static void skipMdnsRecord(MdnsPacketReader reader) throws IOException {
reader.skip(2 + 4); // skip the class and TTL
int dataLength = reader.readUInt16();
reader.skip(dataLength);
}
private static MdnsResponse findResponseWithPointer(
List<MdnsResponse> responses, String[] pointer) {
if (responses != null) {
for (MdnsResponse response : responses) {
List<MdnsPointerRecord> pointerRecords = response.getPointerRecords();
if (pointerRecords == null) {
continue;
}
for (MdnsPointerRecord pointerRecord : pointerRecords) {
if (Arrays.equals(pointerRecord.getPointer(), pointer)) {
return response;
}
}
}
}
return null;
}
private static MdnsResponse findResponseWithHostName(
List<MdnsResponse> responses, String[] hostName) {
if (responses != null) {
for (MdnsResponse response : responses) {
MdnsServiceRecord serviceRecord = response.getServiceRecord();
if (serviceRecord == null) {
continue;
}
if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
return response;
}
}
}
return null;
}
/**
* Decodes all mDNS responses for the desired service type from a packet. The class does not
* check
* the responses for completeness; the caller should do that.
*
* @param packet The packet to read from.
* @return A list of mDNS responses, or null if the packet contained no appropriate responses.
*/
public int decode(@NonNull DatagramPacket packet, @NonNull List<MdnsResponse> responses) {
MdnsPacketReader reader = new MdnsPacketReader(packet);
List<MdnsRecord> records;
try {
reader.readUInt16(); // transaction ID (not used)
int flags = reader.readUInt16();
if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
return MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE;
}
int numQuestions = reader.readUInt16();
int numAnswers = reader.readUInt16();
int numAuthority = reader.readUInt16();
int numRecords = reader.readUInt16();
LOGGER.log(String.format(
"num questions: %d, num answers: %d, num authority: %d, num records: %d",
numQuestions, numAnswers, numAuthority, numRecords));
if (numAnswers < 1) {
return MdnsResponseErrorCode.ERROR_NO_ANSWERS;
}
records = new LinkedList<>();
for (int i = 0; i < (numAnswers + numAuthority + numRecords); ++i) {
String[] name;
try {
name = reader.readLabels();
} catch (IOException e) {
LOGGER.e("Failed to read labels from mDNS response.", e);
return MdnsResponseErrorCode.ERROR_READING_RECORD_NAME;
}
int type = reader.readUInt16();
switch (type) {
case MdnsRecord.TYPE_A: {
try {
records.add(new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader));
} catch (IOException e) {
LOGGER.e("Failed to read A record from mDNS response.", e);
return MdnsResponseErrorCode.ERROR_READING_A_RDATA;
}
break;
}
case MdnsRecord.TYPE_AAAA: {
try {
// AAAA should only contain the IPv6 address.
MdnsInetAddressRecord record =
new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
if (record.getInet6Address() != null) {
records.add(record);
}
} catch (IOException e) {
LOGGER.e("Failed to read AAAA record from mDNS response.", e);
return MdnsResponseErrorCode.ERROR_READING_AAAA_RDATA;
}
break;
}
case MdnsRecord.TYPE_PTR: {
try {
records.add(new MdnsPointerRecord(name, reader));
} catch (IOException e) {
LOGGER.e("Failed to read PTR record from mDNS response.", e);
return MdnsResponseErrorCode.ERROR_READING_PTR_RDATA;
}
break;
}
case MdnsRecord.TYPE_SRV: {
if (name.length == 4) {
try {
records.add(new MdnsServiceRecord(name, reader));
} catch (IOException e) {
LOGGER.e("Failed to read SRV record from mDNS response.", e);
return MdnsResponseErrorCode.ERROR_READING_SRV_RDATA;
}
} else {
try {
skipMdnsRecord(reader);
} catch (IOException e) {
LOGGER.e("Failed to skip SVR record from mDNS response.", e);
return MdnsResponseErrorCode.ERROR_SKIPPING_SRV_RDATA;
}
}
break;
}
case MdnsRecord.TYPE_TXT: {
try {
records.add(new MdnsTextRecord(name, reader));
} catch (IOException e) {
LOGGER.e("Failed to read TXT record from mDNS response.", e);
return MdnsResponseErrorCode.ERROR_READING_TXT_RDATA;
}
break;
}
default: {
try {
skipMdnsRecord(reader);
} catch (IOException e) {
LOGGER.e("Failed to skip mDNS record.", e);
return MdnsResponseErrorCode.ERROR_SKIPPING_UNKNOWN_RECORD;
}
}
}
}
} catch (EOFException e) {
LOGGER.e("Reached the end of the mDNS response unexpectedly.", e);
return MdnsResponseErrorCode.ERROR_END_OF_FILE;
}
// The response records are structured in a hierarchy, where some records reference
// others, as follows:
//
// PTR
// / \
// / \
// TXT SRV
// / \
// / \
// A AAAA
//
// But the order in which these records appear in the response packet is completely
// arbitrary. This means that we need to rescan the record list to construct each level of
// this hierarchy.
//
// PTR: service type -> service instance name
//
// SRV: service instance name -> host name (priority, weight)
//
// TXT: service instance name -> machine readable txt entries.
//
// A: host name -> IP address
// Loop 1: find PTR records, which identify distinct service instances.
long now = SystemClock.elapsedRealtime();
for (MdnsRecord record : records) {
if (record instanceof MdnsPointerRecord) {
String[] name = record.getName();
if ((serviceType == null)
|| Arrays.equals(name, serviceType)
|| ((name.length == (serviceType.length + 2))
&& name[1].equals(MdnsConstants.SUBTYPE_LABEL)
&& MdnsRecord.labelsAreSuffix(serviceType, name))) {
MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
// Group PTR records that refer to the same service instance name into a single
// response.
MdnsResponse response = findResponseWithPointer(responses,
pointerRecord.getPointer());
if (response == null) {
response = new MdnsResponse(now);
responses.add(response);
}
response.addPointerRecord((MdnsPointerRecord) record);
}
}
}
// Loop 2: find SRV and TXT records, which reference the pointer in the PTR record.
for (MdnsRecord record : records) {
if (record instanceof MdnsServiceRecord) {
MdnsServiceRecord serviceRecord = (MdnsServiceRecord) record;
MdnsResponse response = findResponseWithPointer(responses, serviceRecord.getName());
if (response != null) {
response.setServiceRecord(serviceRecord);
}
} else if (record instanceof MdnsTextRecord) {
MdnsTextRecord textRecord = (MdnsTextRecord) record;
MdnsResponse response = findResponseWithPointer(responses, textRecord.getName());
if (response != null) {
response.setTextRecord(textRecord);
}
}
}
// Loop 3: find A and AAAA records, which reference the host name in the SRV record.
for (MdnsRecord record : records) {
if (record instanceof MdnsInetAddressRecord) {
MdnsInetAddressRecord inetRecord = (MdnsInetAddressRecord) record;
MdnsResponse response = findResponseWithHostName(responses, inetRecord.getName());
if (inetRecord.getInet4Address() != null && response != null) {
response.setInet4AddressRecord(inetRecord);
} else if (inetRecord.getInet6Address() != null && response != null) {
response.setInet6AddressRecord(inetRecord);
}
}
}
return SUCCESS;
}
public static class Clock {
public long elapsedRealtime() {
return SystemClock.elapsedRealtime();
}
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (C) 2021 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;
/**
* The list of error code for parsing mDNS response.
*
* @hide
*/
public class MdnsResponseErrorCode {
public static final int SUCCESS = 0;
public static final int ERROR_NOT_RESPONSE_MESSAGE = 1;
public static final int ERROR_NO_ANSWERS = 2;
public static final int ERROR_READING_RECORD_NAME = 3;
public static final int ERROR_READING_A_RDATA = 4;
public static final int ERROR_READING_AAAA_RDATA = 5;
public static final int ERROR_READING_PTR_RDATA = 6;
public static final int ERROR_SKIPPING_PTR_RDATA = 7;
public static final int ERROR_READING_SRV_RDATA = 8;
public static final int ERROR_SKIPPING_SRV_RDATA = 9;
public static final int ERROR_READING_TXT_RDATA = 10;
public static final int ERROR_SKIPPING_UNKNOWN_RECORD = 11;
public static final int ERROR_END_OF_FILE = 12;
}

View file

@ -0,0 +1,155 @@
/*
* Copyright (C) 2021 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.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.ArraySet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* API configuration parameters for searching the mDNS service.
*
* <p>Use {@link MdnsSearchOptions.Builder} to create {@link MdnsSearchOptions}.
*
* @hide
*/
public class MdnsSearchOptions implements Parcelable {
/** @hide */
public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
new Parcelable.Creator<MdnsSearchOptions>() {
@Override
public MdnsSearchOptions createFromParcel(Parcel source) {
return new MdnsSearchOptions(source.createStringArrayList(),
source.readBoolean());
}
@Override
public MdnsSearchOptions[] newArray(int size) {
return new MdnsSearchOptions[size];
}
};
private static MdnsSearchOptions defaultOptions;
private final List<String> subtypes;
private final boolean isPassiveMode;
/** Parcelable constructs for a {@link MdnsServiceInfo}. */
MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode) {
this.subtypes = new ArrayList<>();
if (subtypes != null) {
this.subtypes.addAll(subtypes);
}
this.isPassiveMode = isPassiveMode;
}
/** Returns a {@link Builder} for {@link MdnsSearchOptions}. */
public static Builder newBuilder() {
return new Builder();
}
/** Returns a default search options. */
public static synchronized MdnsSearchOptions getDefaultOptions() {
if (defaultOptions == null) {
defaultOptions = newBuilder().build();
}
return defaultOptions;
}
/** @return the list of subtypes to search. */
public List<String> getSubtypes() {
return subtypes;
}
/**
* @return {@code true} if the passive mode is used. The passive mode scans less frequently in
* order to conserve battery and produce less network traffic.
*/
public boolean isPassiveMode() {
return isPassiveMode;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeStringList(subtypes);
out.writeBoolean(isPassiveMode);
}
/** A builder to create {@link MdnsSearchOptions}. */
public static final class Builder {
private final Set<String> subtypes;
private boolean isPassiveMode = true;
private Builder() {
subtypes = new ArraySet<>();
}
/**
* Adds a subtype to search.
*
* @param subtype the subtype to add.
*/
public Builder addSubtype(@NonNull String subtype) {
if (TextUtils.isEmpty(subtype)) {
throw new IllegalArgumentException("Empty subtype");
}
subtypes.add(subtype);
return this;
}
/**
* Adds a set of subtypes to search.
*
* @param subtypes The list of subtypes to add.
*/
public Builder addSubtypes(@NonNull Collection<String> subtypes) {
this.subtypes.addAll(Objects.requireNonNull(subtypes));
return this;
}
/**
* Sets if the passive mode scan should be used. The passive mode scans less frequently in
* order
* to conserve battery and produce less network traffic.
*
* @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
* false}, active mode will be used.
*/
public Builder setIsPassiveMode(boolean isPassiveMode) {
this.isPassiveMode = isPassiveMode;
return this;
}
/** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
public MdnsSearchOptions build() {
return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode);
}
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (C) 2021 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 java.util.List;
/**
* Listener interface for mDNS service instance discovery events.
*
* @hide
*/
public interface MdnsServiceBrowserListener {
/**
* Called when an mDNS service instance is found.
*
* @param serviceInfo The found mDNS service instance.
*/
void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
/**
* Called when an mDNS service instance is updated.
*
* @param serviceInfo The updated mDNS service instance.
*/
void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo);
/**
* Called when an mDNS service instance is no longer valid and removed.
*
* @param serviceInstanceName The service instance name of the removed mDNS service.
*/
void onServiceRemoved(@NonNull String serviceInstanceName);
/**
* Called when searching for mDNS service has stopped because of an error.
*
* TODO (changed when importing code): define error constants
*
* @param error The error code of the stop reason.
*/
void onSearchStoppedWithError(int error);
/** Called when it failed to start an mDNS service discovery process. */
void onSearchFailedToStart();
/**
* Called when a mDNS service discovery query has been sent.
*
* @param subtypes The list of subtypes in the discovery query.
* @param transactionId The transaction ID of the query.
*/
void onDiscoveryQuerySent(@NonNull List<String> subtypes, int transactionId);
/**
* Called when an error has happened when parsing a received mDNS response packet.
*
* @param receivedPacketNumber The packet sequence number of the received packet.
* @param errorCode The error code, defined in {@link MdnsResponseErrorCode}.
*/
void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
}

View file

@ -0,0 +1,198 @@
/*
* Copyright (C) 2021 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.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* A class representing a discovered mDNS service instance.
*
* @hide
*/
public class MdnsServiceInfo implements Parcelable {
/** @hide */
public static final Parcelable.Creator<MdnsServiceInfo> CREATOR =
new Parcelable.Creator<MdnsServiceInfo>() {
@Override
public MdnsServiceInfo createFromParcel(Parcel source) {
return new MdnsServiceInfo(
source.readString(),
source.createStringArray(),
source.createStringArrayList(),
source.createStringArray(),
source.readInt(),
source.readString(),
source.readString(),
source.createStringArrayList());
}
@Override
public MdnsServiceInfo[] newArray(int size) {
return new MdnsServiceInfo[size];
}
};
private final String serviceInstanceName;
private final String[] serviceType;
private final List<String> subtypes;
private final String[] hostName;
private final int port;
private final String ipv4Address;
private final String ipv6Address;
private final Map<String, String> attributes = new HashMap<>();
List<String> textStrings;
/**
* Constructs a {@link MdnsServiceInfo} object with default values.
*
* @hide
*/
public MdnsServiceInfo(
String serviceInstanceName,
String[] serviceType,
List<String> subtypes,
String[] hostName,
int port,
String ipv4Address,
String ipv6Address,
List<String> textStrings) {
this.serviceInstanceName = serviceInstanceName;
this.serviceType = serviceType;
this.subtypes = new ArrayList<>();
if (subtypes != null) {
this.subtypes.addAll(subtypes);
}
this.hostName = hostName;
this.port = port;
this.ipv4Address = ipv4Address;
this.ipv6Address = ipv6Address;
if (textStrings != null) {
for (String text : textStrings) {
int pos = text.indexOf('=');
if (pos < 1) {
continue;
}
attributes.put(text.substring(0, pos).toLowerCase(Locale.ENGLISH),
text.substring(++pos));
}
}
}
/** @return the name of this service instance. */
public String getServiceInstanceName() {
return serviceInstanceName;
}
/** @return the type of this service instance. */
public String[] getServiceType() {
return serviceType;
}
/** @return the list of subtypes supported by this service instance. */
public List<String> getSubtypes() {
return new ArrayList<>(subtypes);
}
/**
* @return {@code true} if this service instance supports any subtypes.
* @return {@code false} if this service instance does not support any subtypes.
*/
public boolean hasSubtypes() {
return !subtypes.isEmpty();
}
/** @return the host name of this service instance. */
public String[] getHostName() {
return hostName;
}
/** @return the port number of this service instance. */
public int getPort() {
return port;
}
/** @return the IPV4 address of this service instance. */
public String getIpv4Address() {
return ipv4Address;
}
/** @return the IPV6 address of this service instance. */
public String getIpv6Address() {
return ipv6Address;
}
/**
* @return the attribute value for {@code key}.
* @return {@code null} if no attribute value exists for {@code key}.
*/
public String getAttributeByKey(@NonNull String key) {
return attributes.get(key.toLowerCase(Locale.ENGLISH));
}
/** @return an immutable map of all attributes. */
public Map<String, String> getAttributes() {
return Collections.unmodifiableMap(attributes);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
if (textStrings == null) {
// Lazily initialize the parcelable field mTextStrings.
textStrings = new ArrayList<>(attributes.size());
for (Map.Entry<String, String> kv : attributes.entrySet()) {
textStrings.add(String.format(Locale.ROOT, "%s=%s", kv.getKey(), kv.getValue()));
}
}
out.writeString(serviceInstanceName);
out.writeStringArray(serviceType);
out.writeStringList(subtypes);
out.writeStringArray(hostName);
out.writeInt(port);
out.writeString(ipv4Address);
out.writeString(ipv6Address);
out.writeStringList(textStrings);
}
@Override
public String toString() {
return String.format(
Locale.ROOT,
"Name: %s, subtypes: %s, ip: %s, port: %d",
serviceInstanceName,
TextUtils.join(",", subtypes),
ipv4Address,
port);
}
}

View file

@ -0,0 +1,149 @@
/*
* Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
/** An mDNS "SRV" record, which contains service information. */
// TODO(b/177655645): Resolve nullness suppression.
@SuppressWarnings("nullness")
@VisibleForTesting
public class MdnsServiceRecord extends MdnsRecord {
public static final int PROTO_NONE = 0;
public static final int PROTO_TCP = 1;
public static final int PROTO_UDP = 2;
private static final String PROTO_TOKEN_TCP = "_tcp";
private static final String PROTO_TOKEN_UDP = "_udp";
private int servicePriority;
private int serviceWeight;
private int servicePort;
private String[] serviceHost;
public MdnsServiceRecord(String[] name, MdnsPacketReader reader) throws IOException {
super(name, TYPE_SRV, reader);
}
/** Returns the service's port number. */
public int getServicePort() {
return servicePort;
}
/** Returns the service's host name. */
public String[] getServiceHost() {
return serviceHost;
}
/** Returns the service's priority. */
public int getServicePriority() {
return servicePriority;
}
/** Returns the service's weight. */
public int getServiceWeight() {
return serviceWeight;
}
// Format of name is <instance-name>.<service-name>.<protocol>.<domain>
/** Returns the service's instance name, which uniquely identifies the service instance. */
public String getServiceInstanceName() {
if (name.length < 1) {
return null;
}
return name[0];
}
/** Returns the service's name. */
public String getServiceName() {
if (name.length < 2) {
return null;
}
return name[1];
}
/** Returns the service's protocol. */
public int getServiceProtocol() {
if (name.length < 3) {
return PROTO_NONE;
}
String protocol = name[2];
if (protocol.equals(PROTO_TOKEN_TCP)) {
return PROTO_TCP;
}
if (protocol.equals(PROTO_TOKEN_UDP)) {
return PROTO_UDP;
}
return PROTO_NONE;
}
@Override
protected void readData(MdnsPacketReader reader) throws IOException {
servicePriority = reader.readUInt16();
serviceWeight = reader.readUInt16();
servicePort = reader.readUInt16();
serviceHost = reader.readLabels();
}
@Override
protected void writeData(MdnsPacketWriter writer) throws IOException {
writer.writeUInt16(servicePriority);
writer.writeUInt16(serviceWeight);
writer.writeUInt16(servicePort);
writer.writeLabels(serviceHost);
}
@Override
public String toString() {
return String.format(
Locale.ROOT,
"SRV: %s:%d (prio=%d, weight=%d)",
labelsToString(serviceHost),
servicePort,
servicePriority,
serviceWeight);
}
@Override
public int hashCode() {
return (super.hashCode() * 31)
+ Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
servicePort);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof MdnsServiceRecord)) {
return false;
}
MdnsServiceRecord otherRecord = (MdnsServiceRecord) other;
return super.equals(other)
&& (servicePriority == otherRecord.servicePriority)
&& (serviceWeight == otherRecord.serviceWeight)
&& Objects.equals(serviceHost, otherRecord.serviceHost)
&& (servicePort == otherRecord.servicePort);
}
}

Some files were not shown because too many files have changed in this diff Show more