1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.connectivity.mdns;
18 
19 import android.annotation.NonNull;
20 import android.annotation.RequiresApi;
21 import android.os.Build;
22 import android.os.Looper;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 import com.android.net.module.util.CollectionUtils;
26 import com.android.net.module.util.SharedLog;
27 import com.android.server.connectivity.mdns.util.MdnsUtils;
28 
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.List;
32 
33 /**
34  * Sends mDns probe requests to verify service records are unique on the network.
35  *
36  * TODO: implement receiving replies and handling conflicts.
37  */
38 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
39 public class MdnsProber extends MdnsPacketRepeater<MdnsProber.ProbingInfo> {
40     private static final long CONFLICT_RETRY_DELAY_MS = 5_000L;
41 
MdnsProber(@onNull Looper looper, @NonNull MdnsReplySender replySender, @NonNull PacketRepeaterCallback<ProbingInfo> cb, @NonNull SharedLog sharedLog)42     public MdnsProber(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
43             @NonNull PacketRepeaterCallback<ProbingInfo> cb, @NonNull SharedLog sharedLog) {
44         super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
45     }
46 
47     /** Probing request to send with {@link MdnsProber}. */
48     public static class ProbingInfo implements Request {
49 
50         private final int mServiceId;
51         @NonNull
52         private final MdnsPacket mPacket;
53 
54         /**
55          * Create a new ProbingInfo
56          * @param serviceId Service to probe for.
57          * @param probeRecords Records to be probed for uniqueness.
58          */
ProbingInfo(int serviceId, @NonNull List<MdnsRecord> probeRecords)59         ProbingInfo(int serviceId, @NonNull List<MdnsRecord> probeRecords) {
60             mServiceId = serviceId;
61             mPacket = makePacket(probeRecords);
62         }
63 
getServiceId()64         public int getServiceId() {
65             return mServiceId;
66         }
67 
68         @NonNull
69         @Override
getPacket(int index)70         public MdnsPacket getPacket(int index) {
71             return mPacket;
72         }
73 
74         @Override
getDelayMs(int nextIndex)75         public long getDelayMs(int nextIndex) {
76             // As per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
77             return 250L;
78         }
79 
80         @Override
getNumSends()81         public int getNumSends() {
82             // 3 packets as per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
83             return 3;
84         }
85 
makePacket(@onNull List<MdnsRecord> records)86         private static MdnsPacket makePacket(@NonNull List<MdnsRecord> records) {
87             final ArrayList<MdnsRecord> questions = new ArrayList<>(records.size());
88             for (final MdnsRecord record : records) {
89                 if (containsName(questions, record.getName())) {
90                     // Already added this name
91                     continue;
92                 }
93 
94                 // TODO: legacy Android mDNS used to send the first probe (only) as unicast, even
95                 //  though https://datatracker.ietf.org/doc/html/rfc6762#section-8.1 says they
96                 // SHOULD all be. rfc6762 15.1 says that if the port is shared with another
97                 // responder unicast questions should not be used, and the legacy mdnsresponder may
98                 // be running, so not using unicast at all may be better. Consider using legacy
99                 // behavior if this causes problems.
100                 questions.add(new MdnsAnyRecord(record.getName(), false /* unicast */));
101             }
102 
103             return new MdnsPacket(
104                     MdnsConstants.FLAGS_QUERY,
105                     questions,
106                     Collections.emptyList() /* answers */,
107                     records /* authorityRecords */,
108                     Collections.emptyList() /* additionalRecords */);
109         }
110 
111         /**
112          * Return whether the specified name is present in the list of records.
113          */
containsName(@onNull List<MdnsRecord> records, @NonNull String[] name)114         private static boolean containsName(@NonNull List<MdnsRecord> records,
115                 @NonNull String[] name) {
116             return CollectionUtils.any(records,
117                     r -> MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
118         }
119     }
120 
121 
122     @VisibleForTesting
getInitialDelay()123     protected long getInitialDelay() {
124         // First wait for a random time in 0-250ms
125         // as per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
126         return (long) (Math.random() * 250);
127     }
128 
129     /**
130      * Start sending packets for probing.
131      */
startProbing(@onNull ProbingInfo info)132     public void startProbing(@NonNull ProbingInfo info) {
133         startProbing(info, getInitialDelay());
134     }
135 
startProbing(@onNull ProbingInfo info, long delay)136     private void startProbing(@NonNull ProbingInfo info, long delay) {
137         startSending(info.getServiceId(), info, delay);
138     }
139 
140     /**
141      * Restart probing with new service info as a conflict was found.
142      */
restartForConflict(@onNull ProbingInfo newInfo)143     public void restartForConflict(@NonNull ProbingInfo newInfo) {
144         stop(newInfo.getServiceId());
145 
146         /* RFC 6762 8.1: "If fifteen conflicts occur within any ten-second period, then the host
147         MUST wait at least five seconds before each successive additional probe attempt. [...]
148         For very simple devices, a valid way to comply with this requirement is to always wait
149         five seconds after any failed probe attempt before trying again. */
150         // TODO: count 15 conflicts in 10s instead of waiting for 5s every time
151         startProbing(newInfo, CONFLICT_RETRY_DELAY_MS);
152     }
153 }
154