diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service/mdns/com/android/server/connectivity/mdns/MdnsAdvertiser.java new file mode 100644 index 0000000000..dee78fdc2e --- /dev/null +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsAdvertiser.java @@ -0,0 +1,29 @@ +/* + * 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.util.Log; + +/** + * MdnsAdvertiser manages advertising services per {@link com.android.server.NsdService} requests. + * + * TODO: implement + */ +public class MdnsAdvertiser { + private static final String TAG = MdnsAdvertiser.class.getSimpleName(); + public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); +} diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacket.java new file mode 100644 index 0000000000..eae084aca7 --- /dev/null +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacket.java @@ -0,0 +1,43 @@ +/* + * 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 java.util.Collections; +import java.util.List; + +/** + * A class holding data that can be included in a mDNS packet. + */ +public class MdnsPacket { + public final int flags; + public final List questions; + public final List answers; + public final List authorityRecords; + public final List additionalRecords; + + MdnsPacket(int flags, + List questions, + List answers, + List authorityRecords, + List additionalRecords) { + this.flags = flags; + this.questions = Collections.unmodifiableList(questions); + this.answers = Collections.unmodifiableList(answers); + this.authorityRecords = Collections.unmodifiableList(authorityRecords); + this.additionalRecords = Collections.unmodifiableList(additionalRecords); + } +} diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketRepeater.java new file mode 100644 index 0000000000..015dbd824e --- /dev/null +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketRepeater.java @@ -0,0 +1,179 @@ +/* + * 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.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.net.SocketAddress; + +/** + * A class used to send several packets at given time intervals. + * @param The type of the request providing packet repeating parameters. + */ +public abstract class MdnsPacketRepeater { + private static final boolean DBG = MdnsAdvertiser.DBG; + @NonNull + private final MdnsReplySender mReplySender; + @NonNull + protected final Handler mHandler; + @Nullable + private final PacketRepeaterCallback mCb; + + /** + * Status callback from {@link MdnsPacketRepeater}. + * + * Callbacks are called on the {@link MdnsPacketRepeater} handler thread. + * @param The type of the request providing packet repeating parameters. + */ + public interface PacketRepeaterCallback { + /** + * Called when a packet was sent. + */ + default void onSent(int index, @NonNull T info) {} + + /** + * Called when the {@link MdnsPacketRepeater} is done sending packets. + */ + default void onFinished(@NonNull T info) {} + } + + /** + * A request to repeat packets. + * + * All methods are called in the looper thread. + */ + public interface Request { + /** + * Get a packet to send for one iteration. + */ + @NonNull + MdnsPacket getPacket(int index); + + /** + * Get a set of destinations for the packet for one iteration. + */ + @NonNull + Iterable getDestinations(int index); + + /** + * Get the delay in milliseconds until the next packet transmission. + */ + long getDelayMs(int nextIndex); + + /** + * Get the number of packets that should be sent. + */ + int getNumSends(); + } + + /** + * Get the logging tag to use. + */ + @NonNull + protected abstract String getTag(); + + private final class ProbeHandler extends Handler { + ProbeHandler(@NonNull Looper looper) { + super(looper); + } + + @Override + public void handleMessage(@NonNull Message msg) { + final int index = msg.arg1; + final T request = (T) msg.obj; + + if (index >= request.getNumSends()) { + if (mCb != null) { + mCb.onFinished(request); + } + return; + } + + final MdnsPacket packet = request.getPacket(index); + final Iterable destinations = request.getDestinations(index); + if (DBG) { + Log.v(getTag(), "Sending packets to " + destinations + " for iteration " + + index + " out of " + request.getNumSends()); + } + for (SocketAddress destination : destinations) { + try { + mReplySender.sendNow(packet, destination); + } catch (IOException e) { + Log.e(getTag(), "Error sending packet to " + destination, e); + } + } + + int nextIndex = index + 1; + // No need to go through the last handler loop if there's no callback to call + if (nextIndex < request.getNumSends() || mCb != null) { + // TODO: consider using AlarmManager / WakeupMessage to avoid missing sending during + // deep sleep; but this would affect battery life, and discovered services are + // likely not to be available since the device is in deep sleep anyway. + final long delay = request.getDelayMs(nextIndex); + sendMessageDelayed(obtainMessage(msg.what, nextIndex, 0, request), delay); + if (DBG) Log.v(getTag(), "Scheduled next packet in " + delay + "ms"); + } + + // Call onSent after scheduling the next run, to allow the callback to cancel it + if (mCb != null) { + mCb.onSent(index, request); + } + } + } + + protected MdnsPacketRepeater(@NonNull Looper looper, @NonNull MdnsReplySender replySender, + @Nullable PacketRepeaterCallback cb) { + mHandler = new ProbeHandler(looper); + mReplySender = replySender; + mCb = cb; + } + + protected void startSending(int id, @NonNull T request, long initialDelayMs) { + if (DBG) { + Log.v(getTag(), "Starting send with id " + id + ", request " + + request.getClass().getSimpleName() + ", delay " + initialDelayMs); + } + mHandler.sendMessageDelayed(mHandler.obtainMessage(id, 0, 0, request), initialDelayMs); + } + + /** + * Stop sending the packets for the specified ID + * @return true if probing was in progress, false if this was a no-op + */ + public boolean stop(int id) { + if (mHandler.getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException("stop can only be called from the looper thread"); + } + // Since this is run on the looper thread, messages cannot be currently processing and are + // all in the handler queue; unless this method is called from a message, but the current + // message cannot be cancelled. + if (mHandler.hasMessages(id)) { + if (DBG) { + Log.v(getTag(), "Stopping send on id " + id); + } + mHandler.removeMessages(id); + return true; + } + return false; + } +} diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java index 611787f057..1f22fa9d9c 100644 --- a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java @@ -29,7 +29,7 @@ import java.util.Map; public class MdnsPacketWriter { private static final int MDNS_POINTER_MASK = 0xC000; private final byte[] data; - private final Map labelDictionary; + private final Map labelDictionary = new HashMap<>(); private int pos = 0; private int savedWritePos = -1; @@ -44,7 +44,15 @@ public class MdnsPacketWriter { } data = new byte[maxSize]; - labelDictionary = new HashMap<>(); + } + + /** + * Constructs a writer for a new packet. + * + * @param buffer The buffer to write to. + */ + public MdnsPacketWriter(byte[] buffer) { + data = buffer; } /** Returns the current write position. */ diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsReplySender.java b/service/mdns/com/android/server/connectivity/mdns/MdnsReplySender.java new file mode 100644 index 0000000000..2acd7898cb --- /dev/null +++ b/service/mdns/com/android/server/connectivity/mdns/MdnsReplySender.java @@ -0,0 +1,88 @@ +/* + * 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.os.Looper; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.MulticastSocket; +import java.net.SocketAddress; + +/** + * A class that handles sending mDNS replies to a {@link MulticastSocket}, possibly queueing them + * to be sent after some delay. + * + * TODO: implement sending after a delay, combining queued replies and duplicate answer suppression + */ +public class MdnsReplySender { + @NonNull + private final MulticastSocket mSocket; + @NonNull + private final Looper mLooper; + @NonNull + private final byte[] mPacketCreationBuffer; + + public MdnsReplySender(@NonNull Looper looper, + @NonNull MulticastSocket socket, @NonNull byte[] packetCreationBuffer) { + mLooper = looper; + mSocket = socket; + mPacketCreationBuffer = packetCreationBuffer; + } + + /** + * Send a packet immediately. + * + * Must be called on the looper thread used by the {@link MdnsReplySender}. + */ + public void sendNow(@NonNull MdnsPacket packet, @NonNull SocketAddress destination) + throws IOException { + if (Thread.currentThread() != mLooper.getThread()) { + throw new IllegalStateException("sendNow must be called in the handler thread"); + } + + // TODO: support packets over size (send in multiple packets with TC bit set) + final MdnsPacketWriter writer = new MdnsPacketWriter(mPacketCreationBuffer); + + writer.writeUInt16(0); // Transaction ID (advertisement: 0) + writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4) + writer.writeUInt16(packet.questions.size()); // questions count + writer.writeUInt16(packet.answers.size()); // answers count + writer.writeUInt16(packet.authorityRecords.size()); // authority entries count + writer.writeUInt16(packet.additionalRecords.size()); // additional records count + + for (MdnsRecord record : packet.questions) { + record.write(writer, 0L); + } + for (MdnsRecord record : packet.answers) { + record.write(writer, 0L); + } + for (MdnsRecord record : packet.authorityRecords) { + record.write(writer, 0L); + } + for (MdnsRecord record : packet.additionalRecords) { + record.write(writer, 0L); + } + + final int len = writer.getWritePosition(); + final byte[] outBuffer = new byte[len]; + System.arraycopy(mPacketCreationBuffer, 0, outBuffer, 0, len); + + mSocket.send(new DatagramPacket(outBuffer, 0, len, destination)); + } +}