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 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}