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