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