1 /* 2 * Copyright (C) 2024 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 package android.net.apf; 17 18 import static com.android.net.module.util.NetworkStackConstants.TYPE_A; 19 import static com.android.net.module.util.NetworkStackConstants.TYPE_AAAA; 20 import static com.android.net.module.util.NetworkStackConstants.TYPE_PTR; 21 import static com.android.net.module.util.NetworkStackConstants.TYPE_SRV; 22 import static com.android.net.module.util.NetworkStackConstants.TYPE_TXT; 23 24 import android.annotation.NonNull; 25 import android.annotation.RequiresApi; 26 import android.net.nsd.OffloadServiceInfo; 27 import android.os.Build; 28 import android.util.ArraySet; 29 30 import com.android.net.module.util.CollectionUtils; 31 import com.android.net.module.util.DnsUtils; 32 33 import java.io.ByteArrayOutputStream; 34 import java.io.IOException; 35 import java.nio.charset.StandardCharsets; 36 import java.util.ArrayList; 37 import java.util.List; 38 import java.util.Set; 39 40 /** 41 * Collection of utilities for APF mDNS functionalities. 42 * 43 * @hide 44 */ 45 public class ApfMdnsUtils { 46 47 private static final int MAX_SUPPORTED_SUBTYPES = 3; ApfMdnsUtils()48 private ApfMdnsUtils() {} 49 addMatcherIfNotExist(@onNull Set<MdnsOffloadRule.Matcher> allMatchers, @NonNull List<MdnsOffloadRule.Matcher> matcherGroup, @NonNull MdnsOffloadRule.Matcher matcher)50 private static void addMatcherIfNotExist(@NonNull Set<MdnsOffloadRule.Matcher> allMatchers, 51 @NonNull List<MdnsOffloadRule.Matcher> matcherGroup, 52 @NonNull MdnsOffloadRule.Matcher matcher) { 53 if (allMatchers.contains(matcher)) { 54 return; 55 } 56 matcherGroup.add(matcher); 57 allMatchers.add(matcher); 58 } 59 60 /** 61 * Extract the offload rules from the list of offloadServiceInfos. The rules are returned in 62 * priority order (most important first). If there are too many rules, APF could decide only 63 * offload the rules with the higher priority. 64 */ 65 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) 66 @NonNull extractOffloadReplyRule( @onNull List<OffloadServiceInfo> offloadServiceInfos)67 public static List<MdnsOffloadRule> extractOffloadReplyRule( 68 @NonNull List<OffloadServiceInfo> offloadServiceInfos) throws IOException { 69 final List<OffloadServiceInfo> sortedOffloadServiceInfos = new ArrayList<>( 70 offloadServiceInfos); 71 sortedOffloadServiceInfos.sort((a, b) -> { 72 int priorityA = a.getPriority(); 73 int priorityB = b.getPriority(); 74 return Integer.compare(priorityA, priorityB); 75 }); 76 final List<MdnsOffloadRule> rules = new ArrayList<>(); 77 final Set<MdnsOffloadRule.Matcher> allMatchers = new ArraySet<>(); 78 for (OffloadServiceInfo info : sortedOffloadServiceInfos) { 79 List<MdnsOffloadRule.Matcher> matcherGroup = new ArrayList<>(); 80 final OffloadServiceInfo.Key key = info.getKey(); 81 final String[] serviceTypeLabels = CollectionUtils.appendArray(String.class, 82 key.getServiceType().split("\\.", 0), "local"); 83 final String[] fullQualifiedName = CollectionUtils.prependArray(String.class, 84 serviceTypeLabels, key.getServiceName()); 85 final byte[] replyPayload = info.getOffloadPayload(); 86 final byte[] encodedServiceType = encodeQname(serviceTypeLabels); 87 // If (QTYPE == PTR) and (QNAME == mServiceName + mServiceType), then reply. 88 MdnsOffloadRule.Matcher ptrMatcher = new MdnsOffloadRule.Matcher( 89 encodedServiceType, 90 new int[] { TYPE_PTR } 91 ); 92 addMatcherIfNotExist(allMatchers, matcherGroup, ptrMatcher); 93 final List<String> subTypes = info.getSubtypes(); 94 // If subtype list is less than MAX_SUPPORTED_SUBTYPES, then matching each subtype. 95 // Otherwise, use wildcard matching and fail open. 96 boolean tooManySubtypes = subTypes.size() > MAX_SUPPORTED_SUBTYPES; 97 if (tooManySubtypes) { 98 // If (QTYPE == PTR) and (QNAME == wildcard + _sub + mServiceType), then fail open. 99 final String[] serviceTypeSuffix = CollectionUtils.prependArray(String.class, 100 serviceTypeLabels, "_sub"); 101 final ByteArrayOutputStream buf = new ByteArrayOutputStream(); 102 // byte = 0xff is used as a wildcard. 103 buf.write(-1); 104 final byte[] encodedFullServiceType = encodeQname(buf, serviceTypeSuffix); 105 final MdnsOffloadRule.Matcher subtypePtrMatcher = new MdnsOffloadRule.Matcher( 106 encodedFullServiceType, new int[] { TYPE_PTR }); 107 addMatcherIfNotExist(allMatchers, matcherGroup, subtypePtrMatcher); 108 } else { 109 // If (QTYPE == PTR) and (QNAME == subType + _sub + mServiceType), then reply. 110 for (String subType : subTypes) { 111 final String[] fullServiceType = CollectionUtils.prependArray(String.class, 112 serviceTypeLabels, subType, "_sub"); 113 final byte[] encodedFullServiceType = encodeQname(fullServiceType); 114 // If (QTYPE == PTR) and (QNAME == subType + "_sub" + mServiceType), then reply. 115 final MdnsOffloadRule.Matcher subtypePtrMatcher = new MdnsOffloadRule.Matcher( 116 encodedFullServiceType, new int[] { TYPE_PTR }); 117 addMatcherIfNotExist(allMatchers, matcherGroup, subtypePtrMatcher); 118 } 119 } 120 final byte[] encodedFullQualifiedNameQname = encodeQname(fullQualifiedName); 121 // If (QTYPE == SRV) and (QNAME == mServiceName + mServiceType), then reply. 122 // If (QTYPE == TXT) and (QNAME == mServiceName + mServiceType), then reply. 123 addMatcherIfNotExist(allMatchers, matcherGroup, 124 new MdnsOffloadRule.Matcher(encodedFullQualifiedNameQname, 125 new int[] { TYPE_SRV, TYPE_TXT })); 126 // If (QTYPE == A or AAAA) and (QNAME == mDeviceHostName), then reply. 127 final String[] hostNameLabels = info.getHostname().split("\\.", 0); 128 final byte[] encodedHostName = encodeQname(hostNameLabels); 129 addMatcherIfNotExist(allMatchers, matcherGroup, 130 new MdnsOffloadRule.Matcher(encodedHostName, 131 new int[] { TYPE_A, TYPE_AAAA })); 132 if (!matcherGroup.isEmpty()) { 133 rules.add(new MdnsOffloadRule( 134 key.getServiceName() + "." + key.getServiceType(), 135 matcherGroup, tooManySubtypes ? null : replyPayload)); 136 } 137 } 138 return rules; 139 } 140 encodeQname(@onNull ByteArrayOutputStream buf, @NonNull String[] labels)141 private static byte[] encodeQname(@NonNull ByteArrayOutputStream buf, @NonNull String[] labels) 142 throws IOException { 143 final String[] upperCaseLabel = DnsUtils.toDnsLabelsUpperCase(labels); 144 for (final String label : upperCaseLabel) { 145 int labelLength = label.length(); 146 if (labelLength < 1 || labelLength > 63) { 147 throw new IOException("Label is too long: " + label); 148 } 149 buf.write(labelLength); 150 buf.write(label.getBytes(StandardCharsets.UTF_8)); 151 } 152 // APF take array of qnames as input, each qname is terminated by a 0 byte. 153 // A 0 byte is required to mark the end of the list. 154 // This method always writes 1-item lists, as there isn't currently a use-case for 155 // multiple qnames of the same type using the same offload packet. 156 buf.write(0); 157 buf.write(0); 158 return buf.toByteArray(); 159 } 160 encodeQname(@onNull String[] labels)161 private static byte[] encodeQname(@NonNull String[] labels) throws IOException { 162 final ByteArrayOutputStream buf = new ByteArrayOutputStream(); 163 return encodeQname(buf, labels); 164 } 165 } 166