1package de.gultsch.minidns;
2
3import android.content.Context;
4import android.net.ConnectivityManager;
5import android.net.LinkProperties;
6import android.net.Network;
7import android.os.Build;
8import android.util.Log;
9
10import androidx.collection.LruCache;
11
12import com.google.common.base.Objects;
13import com.google.common.base.Strings;
14import com.google.common.collect.Collections2;
15import com.google.common.collect.ImmutableList;
16
17import de.measite.minidns.AbstractDNSClient;
18import de.measite.minidns.DNSMessage;
19import de.measite.minidns.Record;
20import de.measite.minidns.record.Data;
21
22import eu.siacs.conversations.Config;
23
24import java.io.IOException;
25import java.net.InetAddress;
26import java.time.Duration;
27import java.util.Collections;
28import java.util.List;
29
30public class AndroidDNSClient extends AbstractDNSClient {
31
32 private static final long DNS_MAX_TTL = 86_400L;
33
34 private static final LruCache<QuestionServerTuple, DNSMessage> QUERY_CACHE =
35 new LruCache<>(1024);
36 private final Context context;
37 private final NetworkDataSource networkDataSource = new NetworkDataSource();
38 private boolean askForDnssec = false;
39
40 public AndroidDNSClient(final Context context) {
41 super();
42 this.setDataSource(networkDataSource);
43 this.context = context;
44 }
45
46 private static String getPrivateDnsServerName(final LinkProperties linkProperties) {
47 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
48 return linkProperties.getPrivateDnsServerName();
49 } else {
50 return null;
51 }
52 }
53
54 private static boolean isPrivateDnsActive(final LinkProperties linkProperties) {
55 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
56 return linkProperties.isPrivateDnsActive();
57 } else {
58 return false;
59 }
60 }
61
62 @Override
63 protected DNSMessage.Builder newQuestion(final DNSMessage.Builder message) {
64 message.setRecursionDesired(true);
65 message.getEdnsBuilder()
66 .setUdpPayloadSize(networkDataSource.getUdpPayloadSize())
67 .setDnssecOk(askForDnssec);
68 return message;
69 }
70
71 @Override
72 protected DNSMessage query(final DNSMessage.Builder queryBuilder) throws IOException {
73 final DNSMessage question = newQuestion(queryBuilder).build();
74 for (final DNSServer dnsServer : getDNSServers()) {
75 final QuestionServerTuple cacheKey = new QuestionServerTuple(dnsServer, question);
76 final DNSMessage cachedResponse = queryCache(cacheKey);
77 if (cachedResponse != null) {
78 return cachedResponse;
79 }
80 final DNSMessage response = this.networkDataSource.query(question, dnsServer);
81 if (response == null) {
82 continue;
83 }
84 switch (response.responseCode) {
85 case NO_ERROR:
86 case NX_DOMAIN:
87 break;
88 default:
89 continue;
90 }
91 cacheQuery(cacheKey, response);
92 return response;
93 }
94 return null;
95 }
96
97 public boolean isAskForDnssec() {
98 return askForDnssec;
99 }
100
101 public void setAskForDnssec(boolean askForDnssec) {
102 this.askForDnssec = askForDnssec;
103 }
104
105 private List<DNSServer> getDNSServers() {
106 final ImmutableList.Builder<DNSServer> dnsServerBuilder = new ImmutableList.Builder<>();
107 final ConnectivityManager connectivityManager =
108 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
109 final Network[] networks = getActiveNetworks(connectivityManager);
110 for (final Network network : networks) {
111 final LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
112 if (linkProperties == null) {
113 continue;
114 }
115 final String privateDnsServerName = getPrivateDnsServerName(linkProperties);
116 if (Strings.isNullOrEmpty(privateDnsServerName)) {
117 final boolean isPrivateDns = isPrivateDnsActive(linkProperties);
118 for (final InetAddress dnsServer : linkProperties.getDnsServers()) {
119 if (isPrivateDns) {
120 dnsServerBuilder.add(new DNSServer(dnsServer, Transport.TLS));
121 } else {
122 dnsServerBuilder.add(new DNSServer(dnsServer));
123 }
124 }
125 } else {
126 dnsServerBuilder.add(new DNSServer(privateDnsServerName, Transport.TLS));
127 }
128 }
129 return dnsServerBuilder.build();
130 }
131
132 private Network[] getActiveNetworks(final ConnectivityManager connectivityManager) {
133 if (connectivityManager == null) {
134 return new Network[0];
135 }
136 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
137 final Network activeNetwork = connectivityManager.getActiveNetwork();
138 if (activeNetwork != null) {
139 return new Network[] {activeNetwork};
140 }
141 }
142 return connectivityManager.getAllNetworks();
143 }
144
145 private DNSMessage queryCache(final QuestionServerTuple key) {
146 final DNSMessage cachedResponse;
147 synchronized (QUERY_CACHE) {
148 cachedResponse = QUERY_CACHE.get(key);
149 if (cachedResponse == null) {
150 return null;
151 }
152 final long expiresIn = expiresIn(cachedResponse);
153 if (expiresIn < 0) {
154 QUERY_CACHE.remove(key);
155 return null;
156 }
157 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
158 Log.d(
159 Config.LOGTAG,
160 "DNS query came from cache. expires in " + Duration.ofMillis(expiresIn));
161 }
162 }
163 return cachedResponse;
164 }
165
166 private void cacheQuery(final QuestionServerTuple key, final DNSMessage response) {
167 if (response.receiveTimestamp <= 0) {
168 return;
169 }
170 synchronized (QUERY_CACHE) {
171 QUERY_CACHE.put(key, response);
172 }
173 }
174
175 private static long ttl(final DNSMessage dnsMessage) {
176 final List<Record<? extends Data>> answerSection = dnsMessage.answerSection;
177 if (answerSection == null || answerSection.isEmpty()) {
178 final List<Record<? extends Data>> authoritySection = dnsMessage.authoritySection;
179 if (authoritySection == null || authoritySection.isEmpty()) {
180 return 0;
181 } else {
182 return Collections.min(Collections2.transform(authoritySection, d -> d.ttl));
183 }
184
185 } else {
186 return Collections.min(Collections2.transform(answerSection, d -> d.ttl));
187 }
188 }
189
190 private static long expiresAt(final DNSMessage dnsMessage) {
191 return dnsMessage.receiveTimestamp + (Math.min(DNS_MAX_TTL, ttl(dnsMessage)) * 1000L);
192 }
193
194 private static long expiresIn(final DNSMessage dnsMessage) {
195 return expiresAt(dnsMessage) - System.currentTimeMillis();
196 }
197
198 private static class QuestionServerTuple {
199 private final DNSServer dnsServer;
200 private final DNSMessage question;
201
202 private QuestionServerTuple(final DNSServer dnsServer, final DNSMessage question) {
203 this.dnsServer = dnsServer;
204 this.question = question.asNormalizedVersion();
205 }
206
207 @Override
208 public boolean equals(Object o) {
209 if (this == o) return true;
210 if (o == null || getClass() != o.getClass()) return false;
211 QuestionServerTuple that = (QuestionServerTuple) o;
212 return Objects.equal(dnsServer, that.dnsServer)
213 && Objects.equal(question, that.question);
214 }
215
216 @Override
217 public int hashCode() {
218 return Objects.hashCode(dnsServer, question);
219 }
220 }
221}