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