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