@@ -13,8 +13,10 @@ import java.net.InetAddress
1313import java.net.URL
1414import java.util.Base64
1515import java.util.Locale
16+ import java.util.concurrent.ConcurrentHashMap
1617import java.util.concurrent.TimeUnit
1718import android.util.Log
19+ import android.os.SystemClock
1820
1921data class ParsedDnsQuery (
2022 val id : Int ,
@@ -35,6 +37,16 @@ data class DnsResolution(
3537 val responseCode : Int = 0 ,
3638)
3739
40+ data class CachedDnsResolution (
41+ val resolution : DnsResolution ,
42+ val expiresAtMs : Long ,
43+ )
44+
45+ data class DnsCacheKey (
46+ val host : String ,
47+ val type : Int ,
48+ )
49+
3850data class PreparedDohEndpoint (
3951 val rawUrl : String ,
4052 val host : String ,
@@ -47,6 +59,7 @@ class LinuxdoDnsResolver(
4759 endpoints : List <PreparedDohEndpoint >,
4860) {
4961 private val tag = " LinuxdoDnsResolver"
62+ private val cache = ConcurrentHashMap <DnsCacheKey , CachedDnsResolution >()
5063 private val client = OkHttpClient .Builder ()
5164 .connectTimeout(10 , TimeUnit .SECONDS )
5265 .readTimeout(10 , TimeUnit .SECONDS )
@@ -57,25 +70,34 @@ class LinuxdoDnsResolver(
5770
5871 fun resolveManagedPayload (requestPayload : ByteArray , query : ParsedDnsQuery ): ByteArray {
5972 val host = query.name.lowercase(Locale .US )
73+ readCached(host, query.type)?.let { cached ->
74+ Log .d(tag, " cache hit for $host type=${query.type} answers=${cached.answers.size} " )
75+ return DnsPacketCodec .buildResponse(query, cached)
76+ }
77+
6078 resolveLocal(host, query.type)?.let { answers ->
79+ val resolution = DnsResolution (answers)
80+ writeCache(host, query.type, resolution)
6181 Log .d(tag, " resolved local override for $host type=${query.type} answers=${answers.size} " )
62- return DnsPacketCodec .buildResponse(query, DnsResolution (answers) )
82+ return DnsPacketCodec .buildResponse(query, resolution )
6383 }
6484
6585 var lastError: Exception ? = null
6686 for (endpoint in preparedEndpoints) {
6787 try {
6888 if (supportsDnsMessage(endpoint.url)) {
69- val payload = queryDohDnsMessageRaw(endpoint, requestPayload)
89+ val resolution = queryDohDnsMessageRaw(endpoint, requestPayload, query)
90+ writeCache(host, query.type, resolution)
7091 Log .d(tag, " resolved $host type=${query.type} via ${endpoint.rawUrl} raw dns-message" )
71- return payload
92+ return DnsPacketCodec .buildResponse(query, resolution)
7293 }
7394
7495 if (isJsonFriendlyType(query.type)) {
75- val answers = queryDohJson(endpoint, host, query.type)
76- if (answers.isNotEmpty()) {
77- Log .d(tag, " resolved $host type=${query.type} via ${endpoint.rawUrl} answers=${answers.size} " )
78- return DnsPacketCodec .buildResponse(query, DnsResolution (answers))
96+ val resolution = DnsResolution (queryDohJson(endpoint, host, query.type))
97+ if (resolution.answers.isNotEmpty()) {
98+ writeCache(host, query.type, resolution)
99+ Log .d(tag, " resolved $host type=${query.type} via ${endpoint.rawUrl} answers=${resolution.answers.size} " )
100+ return DnsPacketCodec .buildResponse(query, resolution)
79101 }
80102 lastError = IOException (" empty answer from ${endpoint.rawUrl} " )
81103 continue
@@ -97,8 +119,17 @@ class LinuxdoDnsResolver(
97119 }
98120
99121 val host = query.name.lowercase(Locale .US )
122+ readCached(host, query.type)?.let {
123+ Log .d(tag, " cache hit for $host type=${query.type} answers=${it.answers.size} " )
124+ return it
125+ }
126+
100127 val managedHost = config.isManagedHost(host)
101- resolveLocal(host, query.type)?.let { return DnsResolution (it) }
128+ resolveLocal(host, query.type)?.let {
129+ val resolution = DnsResolution (it)
130+ writeCache(host, query.type, resolution)
131+ return resolution
132+ }
102133
103134 val endpoints = if (managedHost) {
104135 preparedEndpoints
@@ -109,10 +140,11 @@ class LinuxdoDnsResolver(
109140 var lastError: Exception ? = null
110141 for (endpoint in endpoints) {
111142 try {
112- val answers = queryDoh(endpoint, host, query.type)
113- if (answers.isNotEmpty()) {
114- Log .d(tag, " resolved $host type=${query.type} via ${endpoint.rawUrl} answers=${answers.size} " )
115- return DnsResolution (answers)
143+ val resolution = DnsResolution (queryDoh(endpoint, host, query.type))
144+ if (resolution.answers.isNotEmpty()) {
145+ writeCache(host, query.type, resolution)
146+ Log .d(tag, " resolved $host type=${query.type} via ${endpoint.rawUrl} answers=${resolution.answers.size} " )
147+ return resolution
116148 }
117149 lastError = IOException (" empty answer from ${endpoint.rawUrl} " )
118150 } catch (error: Exception ) {
@@ -123,6 +155,7 @@ class LinuxdoDnsResolver(
123155
124156 throw IOException (" DoH lookup failed for $host : ${lastError?.message ? : " no endpoint available" } " )
125157 }
158+
126159 private fun resolveLocal (host : String , type : Int ): List <DnsAnswerRecord >? {
127160 config.findDnsHostOverride(host)?.let { override ->
128161 return resolveOverride(override , type)
@@ -256,7 +289,8 @@ class LinuxdoDnsResolver(
256289 private fun queryDohDnsMessageRaw (
257290 endpoint : PreparedDohEndpoint ,
258291 payload : ByteArray ,
259- ): ByteArray {
292+ query : ParsedDnsQuery ,
293+ ): DnsResolution {
260294 val encoded = Base64 .getUrlEncoder()
261295 .withoutPadding()
262296 .encodeToString(payload)
@@ -272,8 +306,32 @@ class LinuxdoDnsResolver(
272306 if (! response.isSuccessful) {
273307 throw IOException (" DoH server ${endpoint.rawUrl} returned ${response.code} " )
274308 }
275- return response.body?.bytes() ? : throw IOException (" empty DoH response from ${endpoint.rawUrl} " )
309+ val body = response.body?.bytes() ? : throw IOException (" empty DoH response from ${endpoint.rawUrl} " )
310+ return DnsResolution (DnsPacketCodec .parseWireResponse(body, query.type))
311+ }
312+ }
313+
314+ private fun readCached (host : String , type : Int ): DnsResolution ? {
315+ val key = DnsCacheKey (host, type)
316+ val entry = cache[key] ? : return null
317+ if (SystemClock .elapsedRealtime() >= entry.expiresAtMs) {
318+ cache.remove(key)
319+ return null
276320 }
321+ return entry.resolution
322+ }
323+
324+ private fun writeCache (host : String , type : Int , resolution : DnsResolution ) {
325+ val minTtl = resolution.answers.minOfOrNull { it.ttl } ? : return
326+ if (minTtl <= 0 ) {
327+ cache.remove(DnsCacheKey (host, type))
328+ return
329+ }
330+ cache.entries.removeIf { (_, entry) -> SystemClock .elapsedRealtime() >= entry.expiresAtMs }
331+ cache[DnsCacheKey (host, type)] = CachedDnsResolution (
332+ resolution = resolution,
333+ expiresAtMs = SystemClock .elapsedRealtime() + (minTtl.toLong() * 1000L ),
334+ )
277335 }
278336
279337 private fun supportsDnsMessage (url : URL ): Boolean {
0 commit comments