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