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 final DNSMessage response = this.networkDataSource.query(question, dnsServer);
74 if (response == null) {
75 continue;
76 }
77 switch (response.responseCode) {
78 case NO_ERROR:
79 case NX_DOMAIN:
80 break;
81 default:
82 continue;
83 }
84 cacheQuery(cacheKey, response);
85 return response;
86 }
87 return null;
88 }
89
90 public boolean isAskForDnssec() {
91 return askForDnssec;
92 }
93
94 public void setAskForDnssec(boolean askForDnssec) {
95 this.askForDnssec = askForDnssec;
96 }
97
98 private List<DNSServer> getDNSServers() {
99 final ImmutableList.Builder<DNSServer> dnsServerBuilder = new ImmutableList.Builder<>();
100 final ConnectivityManager connectivityManager =
101 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
102 final Network[] networks = getActiveNetworks(connectivityManager);
103 for (final Network network : networks) {
104 final LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
105 if (linkProperties == null) {
106 continue;
107 }
108 final String privateDnsServerName = getPrivateDnsServerName(linkProperties);
109 if (Strings.isNullOrEmpty(privateDnsServerName)) {
110 final boolean isPrivateDns = isPrivateDnsActive(linkProperties);
111 for (final InetAddress dnsServer : linkProperties.getDnsServers()) {
112 if (isPrivateDns) {
113 dnsServerBuilder.add(new DNSServer(dnsServer, Transport.TLS));
114 } else {
115 dnsServerBuilder.add(new DNSServer(dnsServer));
116 }
117 }
118 } else {
119 dnsServerBuilder.add(new DNSServer(privateDnsServerName, Transport.TLS));
120 }
121 }
122 return dnsServerBuilder.build();
123 }
124
125 private Network[] getActiveNetworks(final ConnectivityManager connectivityManager) {
126 if (connectivityManager == null) {
127 return new Network[0];
128 }
129 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
130 final Network activeNetwork = connectivityManager.getActiveNetwork();
131 if (activeNetwork != null) {
132 return new Network[] {activeNetwork};
133 }
134 }
135 return connectivityManager.getAllNetworks();
136 }
137
138 private DNSMessage queryCache(final QuestionServerTuple key) {
139 final DNSMessage cachedResponse;
140 synchronized (QUERY_CACHE) {
141 cachedResponse = QUERY_CACHE.get(key);
142 if (cachedResponse == null) {
143 return null;
144 }
145 final long expiresIn = expiresIn(cachedResponse);
146 if (expiresIn < 0) {
147 QUERY_CACHE.remove(key);
148 return null;
149 }
150 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
151 Log.d(
152 Config.LOGTAG,
153 "DNS query came from cache. expires in " + Duration.ofMillis(expiresIn));
154 }
155 }
156 return cachedResponse;
157 }
158
159 private void cacheQuery(final QuestionServerTuple key, final DNSMessage response) {
160 if (response.receiveTimestamp <= 0) {
161 return;
162 }
163 synchronized (QUERY_CACHE) {
164 QUERY_CACHE.put(key, response);
165 }
166 }
167
168 private static long expiresAt(final DNSMessage dnsMessage) {
169 return dnsMessage.receiveTimestamp
170 + (Collections.min(Collections2.transform(dnsMessage.answerSection, d -> d.ttl))
171 * 1000L);
172 }
173
174 private static long expiresIn(final DNSMessage dnsMessage) {
175 return expiresAt(dnsMessage) - System.currentTimeMillis();
176 }
177
178 private static class QuestionServerTuple {
179 private final DNSServer dnsServer;
180 private final DNSMessage question;
181
182 private QuestionServerTuple(final DNSServer dnsServer, final DNSMessage question) {
183 this.dnsServer = dnsServer;
184 this.question = question.asNormalizedVersion();
185 }
186
187 @Override
188 public boolean equals(Object o) {
189 if (this == o) return true;
190 if (o == null || getClass() != o.getClass()) return false;
191 QuestionServerTuple that = (QuestionServerTuple) o;
192 return Objects.equal(dnsServer, that.dnsServer)
193 && Objects.equal(question, that.question);
194 }
195
196 @Override
197 public int hashCode() {
198 return Objects.hashCode(dnsServer, question);
199 }
200 }
201}