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