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