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