AndroidDNSClient.java

  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}