• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.tv.mdnsoffloadmanager;
2 
3 import android.os.IBinder;
4 import android.os.Process;
5 import android.os.UserHandle;
6 import android.util.Log;
7 
8 import androidx.annotation.NonNull;
9 import androidx.annotation.WorkerThread;
10 
11 import java.io.PrintWriter;
12 import java.nio.charset.StandardCharsets;
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.HashSet;
16 import java.util.List;
17 import java.util.Set;
18 import java.util.concurrent.ConcurrentHashMap;
19 import java.util.concurrent.ConcurrentMap;
20 import java.util.concurrent.atomic.AtomicInteger;
21 
22 import device.google.atv.mdns_offload.IMdnsOffload;
23 import device.google.atv.mdns_offload.IMdnsOffloadManager;
24 
25 /**
26  * Class to store OffloadIntents made by clients and assign record keys.
27  */
28 public class OffloadIntentStore {
29 
30     private static final String TAG = OffloadIntentStore.class.getSimpleName();
31 
32     private final AtomicInteger mNextKey = new AtomicInteger(1);
33     private final ConcurrentMap<Integer, OffloadIntent> mOffloadIntentsByRecordKey =
34             new ConcurrentHashMap<>();
35     // Note that we need to preserve the order of passthrough intents.
36     private final List<PassthroughIntent> mPassthroughIntents = new ArrayList<>();
37 
38     private final PriorityListManager mPriorityListManager;
39 
40     /**
41      * Only listed packages may offload data or manage the passthrough list, requests from any other
42      * packages are dropped.
43      */
44     private final Set<Integer> mAppIdAllowlist = new HashSet<>();
45 
OffloadIntentStore(@onNull PriorityListManager priorityListManager)46     OffloadIntentStore(@NonNull PriorityListManager priorityListManager) {
47         mPriorityListManager = priorityListManager;
48     }
49 
50     @WorkerThread
setAppIdAllowlist(Set<Integer> appIds)51     void setAppIdAllowlist(Set<Integer> appIds) {
52         mAppIdAllowlist.clear();
53         mAppIdAllowlist.addAll(appIds);
54         mAppIdAllowlist.add(UserHandle.getAppId(Process.SYSTEM_UID));
55     }
56 
57     /**
58      * Register the intention to offload an mDNS service. The system will do its best to offload it
59      * when possible (considering dependencies, network conditions etc.).
60      * <p>
61      * The offload intent will be associated with the caller via the clientToken, stored in the
62      * internal memory store, and be assigned a unique record key.
63      */
registerOffloadIntent( String networkInterface, IMdnsOffloadManager.OffloadServiceInfo serviceInfo, IBinder clientToken, int callerUid)64     OffloadIntent registerOffloadIntent(
65             String networkInterface,
66             IMdnsOffloadManager.OffloadServiceInfo serviceInfo,
67             IBinder clientToken,
68             int callerUid) {
69         int recordKey = mNextKey.getAndIncrement();
70         IMdnsOffload.MdnsProtocolData mdnsProtocolData = convertToMdnsProtocolData(serviceInfo);
71         int priority = mPriorityListManager.getPriority(mdnsProtocolData, recordKey);
72         int appId = UserHandle.getAppId(callerUid);
73         OffloadIntent offloadIntent = new OffloadIntent(
74                 networkInterface, recordKey, mdnsProtocolData, clientToken, priority, appId);
75         mOffloadIntentsByRecordKey.put(recordKey, offloadIntent);
76         return offloadIntent;
77     }
78 
79     /**
80      * Retrieve all offload intents for a given interface.
81      */
82     @WorkerThread
getOffloadIntentsForInterface(String networkInterface)83     Collection<OffloadIntent> getOffloadIntentsForInterface(String networkInterface) {
84         return mOffloadIntentsByRecordKey
85                 .values()
86                 .stream()
87                 .filter(intent -> intent.mNetworkInterface.equals(networkInterface)
88                         && mAppIdAllowlist.contains(intent.mOwnerAppId))
89                 .toList();
90     }
91 
92     /**
93      * Retrieve an offload intent by its record key and remove from internal database.
94      * <p>
95      * Only permitted if the offload intent was registered by the same caller.
96      */
97     @WorkerThread
getAndRemoveOffloadIntent(int recordKey, IBinder clientToken)98     OffloadIntent getAndRemoveOffloadIntent(int recordKey, IBinder clientToken) {
99         OffloadIntent offloadIntent = mOffloadIntentsByRecordKey.get(recordKey);
100         if (offloadIntent == null) {
101             Log.e(TAG, "Failed to remove protocol responses, bad record key {"
102                     + recordKey + "}.");
103             return null;
104         }
105         if (!offloadIntent.mClientToken.equals(clientToken)) {
106             Log.e(TAG, "Failed to remove protocol messages, bad client token {"
107                     + clientToken + "}.");
108             return null;
109         }
110         mOffloadIntentsByRecordKey.remove(recordKey);
111         return offloadIntent;
112     }
113 
114     @WorkerThread
getRecordKeys()115     Collection<Integer> getRecordKeys() {
116         return mOffloadIntentsByRecordKey.keySet();
117     }
118 
119     /**
120      * Create a passthrough intent, representing the intention to add a DNS query name to the
121      * passthrough list. The system will do its best to configure the passthrough when possible.
122      * <p>
123      * The passthrough intent will be associated with the caller via the clientToken, stored in the
124      * internal memory store, and identified by the passthrough QNAME.
125      */
126     @WorkerThread
registerPassthroughIntent( String networkInterface, String qname, IBinder clientToken, int callerUid)127     PassthroughIntent registerPassthroughIntent(
128             String networkInterface,
129             String qname,
130             IBinder clientToken,
131             int callerUid) {
132         String canonicalQName = mPriorityListManager.canonicalQName(qname);
133         int priority = mPriorityListManager.getPriority(canonicalQName, 0);
134         int appId = UserHandle.getAppId(callerUid);
135         PassthroughIntent passthroughIntent = new PassthroughIntent(
136                 networkInterface, qname, canonicalQName, clientToken, priority, appId);
137         mPassthroughIntents.add(passthroughIntent);
138         return passthroughIntent;
139     }
140 
141     /**
142      * Retrieve all passthrough intents for a given interface.
143      */
144     @WorkerThread
getPassthroughIntentsForInterface(String networkInterface)145     List<PassthroughIntent> getPassthroughIntentsForInterface(String networkInterface) {
146         return mPassthroughIntents
147                 .stream()
148                 .filter(intent -> intent.mNetworkInterface.equals(networkInterface)
149                         && mAppIdAllowlist.contains(intent.mOwnerAppId))
150                 .toList();
151     }
152 
153     /**
154      * Retrieve a passthrough intent by its QNAME remove from internal database.
155      * <p>
156      * Only permitted if the passthrough intent was registered by the same caller.
157      */
158     @WorkerThread
removePassthroughIntent(String qname, IBinder clientToken)159     boolean removePassthroughIntent(String qname, IBinder clientToken) {
160         String canonicalQName = mPriorityListManager.canonicalQName(qname);
161         boolean removed = mPassthroughIntents.removeIf(
162                 pt -> pt.mCanonicalQName.equals(canonicalQName)
163                         && pt.mClientToken.equals(clientToken));
164         if (!removed) {
165             Log.e(TAG, "Failed to remove passthrough intent, bad QNAME or client token.");
166             return false;
167         }
168         return true;
169     }
170 
convertToMdnsProtocolData( IMdnsOffloadManager.OffloadServiceInfo serviceData)171     private static IMdnsOffload.MdnsProtocolData convertToMdnsProtocolData(
172             IMdnsOffloadManager.OffloadServiceInfo serviceData) {
173         IMdnsOffload.MdnsProtocolData data = new IMdnsOffload.MdnsProtocolData();
174         data.rawOffloadPacket = serviceData.rawOffloadPacket;
175         data.matchCriteriaList = MdnsPacketParser.extractMatchCriteria(
176                 serviceData.rawOffloadPacket);
177         return data;
178     }
179 
180     @WorkerThread
dump(PrintWriter writer)181     void dump(PrintWriter writer) {
182         writer.println("OffloadIntentStore:");
183         writer.println("offload intents:");
184         mOffloadIntentsByRecordKey.values()
185                 .forEach(intent -> writer.println("* %s".formatted(intent)));
186         writer.println("passthrough intents:");
187         mPassthroughIntents.forEach(intent -> writer.println("* %s".formatted(intent)));
188         writer.println();
189     }
190 
191     /**
192      * Create a detailed dump of the OffloadIntents, including a hexdump of the raw packets.
193      */
194     @WorkerThread
dumpProtocolData(PrintWriter writer)195     void dumpProtocolData(PrintWriter writer) {
196         writer.println("Protocol data dump:");
197         mOffloadIntentsByRecordKey.values().forEach(intent -> {
198             writer.println("mRecordKey=%d".formatted(intent.mRecordKey));
199             IMdnsOffload.MdnsProtocolData data = intent.mProtocolData;
200             writer.println("match criteria:");
201             data.matchCriteriaList.forEach(criteria ->
202                     writer.println("* %s".formatted(formatMatchCriteria(criteria))));
203             writer.println("raw offload packet:");
204             hexDump(writer, data.rawOffloadPacket);
205         });
206         writer.println();
207     }
208 
209     /**
210      * Class representing the intention to offload mDNS protocol data.
211      */
212     static class OffloadIntent {
213         final String mNetworkInterface;
214         final int mRecordKey;
215         final IMdnsOffload.MdnsProtocolData mProtocolData;
216         final IBinder mClientToken;
217         final int mPriority; // Lower values take precedence.
218         final int mOwnerAppId;
219 
OffloadIntent( String networkInterface, int recordKey, IMdnsOffload.MdnsProtocolData protocolData, IBinder clientToken, int priority, int ownerAppId )220         private OffloadIntent(
221                 String networkInterface,
222                 int recordKey,
223                 IMdnsOffload.MdnsProtocolData protocolData,
224                 IBinder clientToken,
225                 int priority,
226                 int ownerAppId
227         ) {
228             mNetworkInterface = networkInterface;
229             mRecordKey = recordKey;
230             mProtocolData = protocolData;
231             mClientToken = clientToken;
232             mPriority = priority;
233             mOwnerAppId = ownerAppId;
234         }
235 
236         @Override
toString()237         public String toString() {
238             final StringBuilder sb = new StringBuilder("OffloadIntent{");
239             sb.append("mNetworkInterface='").append(mNetworkInterface).append('\'');
240             sb.append(", mRecordKey=").append(mRecordKey);
241             sb.append(", mPriority=").append(mPriority);
242             sb.append(", mOwnerAppId=").append(mOwnerAppId);
243             sb.append('}');
244             return sb.toString();
245         }
246     }
247 
248     /**
249      * Class representing the intention to configure mDNS passthrough for a given query name.
250      */
251     static class PassthroughIntent {
252         final String mNetworkInterface;
253         // Preserving the original upper/lowercase format.
254         final String mOriginalQName;
255         final String mCanonicalQName;
256         final IBinder mClientToken;
257         final int mPriority;
258         final int mOwnerAppId;
259 
PassthroughIntent( String networkInterface, String originalQName, String canonicalQName, IBinder clientToken, int priority, int ownerAppId)260         PassthroughIntent(
261                 String networkInterface,
262                 String originalQName,
263                 String canonicalQName,
264                 IBinder clientToken,
265                 int priority,
266                 int ownerAppId) {
267             mNetworkInterface = networkInterface;
268             mOriginalQName = originalQName;
269             mCanonicalQName = canonicalQName;
270             mClientToken = clientToken;
271             mPriority = priority;
272             mOwnerAppId = ownerAppId;
273         }
274 
275         @Override
toString()276         public String toString() {
277             final StringBuilder sb = new StringBuilder("PassthroughIntent{");
278             sb.append("mNetworkInterface='").append(mNetworkInterface).append('\'');
279             sb.append(", mOriginalQName='").append(mOriginalQName).append('\'');
280             sb.append(", mCanonicalQName='").append(mCanonicalQName).append('\'');
281             sb.append(", mPriority=").append(mPriority);
282             sb.append(", mOwnerAppId=").append(mOwnerAppId);
283             sb.append('}');
284             return sb.toString();
285         }
286     }
287 
formatMatchCriteria(IMdnsOffload.MdnsProtocolData.MatchCriteria matchCriteria)288     private String formatMatchCriteria(IMdnsOffload.MdnsProtocolData.MatchCriteria matchCriteria) {
289         return "MatchCriteria{type=%d, nameOffset=%d}"
290                 .formatted(matchCriteria.type, matchCriteria.nameOffset);
291     }
292 
hexDump(PrintWriter writer, byte[] data)293     private void hexDump(PrintWriter writer, byte[] data) {
294         final int width = 16;
295         for (int rowOffset = 0; rowOffset < data.length; rowOffset += width) {
296             writer.printf("%06d:  ", rowOffset);
297 
298             for (int index = 0; index < width; index++) {
299                 if (rowOffset + index < data.length) {
300                     writer.printf("%02x ", data[rowOffset + index]);
301                 } else {
302                     writer.print("   ");
303                 }
304             }
305 
306             int asciiWidth = Math.min(width, data.length - rowOffset);
307             writer.print("  |  ");
308             writer.println(new String(data, rowOffset, asciiWidth, StandardCharsets.US_ASCII)
309                     .replaceAll("[^\\x20-\\x7E]", "."));
310         }
311     }
312 }
313