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