From 41a9ae79ad1699acf0b23d4bac7bef01d9f7f57e Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 13 Jul 2026 18:26:40 -0400 Subject: [PATCH 1/4] Write our own DNS message reader --- .../okhttp3/dnsoverhttps/-DnsMessageReader.kt | 205 ++++++++++++++++++ .../okhttp3/dnsoverhttps/DnsOverHttps.kt | 42 ++-- .../dnsoverhttps/DnsMessageReaderTest.kt | 183 ++++++++++++++++ .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 6 +- 4 files changed, 414 insertions(+), 22 deletions(-) create mode 100644 okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt create mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderTest.kt diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt new file mode 100644 index 000000000000..5cd61cd7731f --- /dev/null +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("ktlint:standard:filename") + +package okhttp3.dnsoverhttps + +import java.io.IOException +import okio.BufferedSource +import okio.ByteString +import okio.ProtocolException + +/** + * https://datatracker.ietf.org/doc/html/rfc1035 + */ +internal class DnsMessageReader( + source: BufferedSource, +) { + /** + * Keep a reference to the source at its initial offset, so we can implement name compression. + * That mechanism requires us to backtrack in the message. + */ + private val sourceOffsetZero = source + private val source = source.peek() + + fun read(): DnsMessage { + val id = source.readShort() + val flags = source.readShort().toInt() + + val questionCount = source.readShort().toUShort().toInt() + val answerCount = source.readShort().toUShort().toInt() + val nameServerCount = source.readShort().toUShort().toInt() + val additionalRecordCount = source.readShort().toUShort().toInt() + + val questions = mutableListOf() + for (i in 0 until questionCount) { + questions += readQuestion() + } + + val answers = mutableListOf() + for (i in 0 until answerCount) { + answers += readResourceRecord() ?: continue + } + + val authorityRecords = mutableListOf() + for (i in 0 until nameServerCount) { + authorityRecords += readResourceRecord() ?: continue + } + + val additionalRecords = mutableListOf() + for (i in 0 until additionalRecordCount) { + additionalRecords += readResourceRecord() ?: continue + } + + if (!source.exhausted()) { + throw ProtocolException("unexpected trailing data in message") + } + + sourceOffsetZero.skipAll() + + return DnsMessage( + id = id, + flags = flags, + questions = questions, + answers = answers, + authorityRecords = authorityRecords, + additionalRecords = additionalRecords, + ) + } + + private fun readQuestion(): Question { + val name = readName() + val type = source.readShort() + val `class` = source.readShort() + return Question( + name = name, + type = type, + `class` = `class`, + ) + } + + private fun readName(): String { + return buildString { + readName(source, this) + } + } + + private tailrec fun readName(source: BufferedSource, builder: StringBuilder) { + while (true) { + val labelTypeAndLength = source.readByte().toUByte().toInt() + val labelType = labelTypeAndLength and 0b11000000 + val labelLength = labelTypeAndLength and 0b00111111 + when (labelType) { + // Inline value. + 0b00_000000 -> { + if (labelLength == 0) return + if (builder.isNotEmpty()) builder.append('.') + builder.append(source.readUtf8(labelLength.toLong())) + } + + // Compressed suffix. + 0b11_000000 -> { + val offsetLength = (labelLength shl 8) or source.readByte().toUByte().toInt() + val offsetSource = sourceOffsetZero.peek() + offsetSource.skip(offsetLength.toLong()) + return readName(offsetSource, builder) + } + + 0b01_000000, 0b10_000000 -> throw ProtocolException("unsupported label type") + } + } + } + + /** Returns null if this record type is unsupported. */ + fun readResourceRecord(): ResourceRecord? { + val name = readName() + val type = source.readShort().toInt() + val `class` = source.readShort().toInt() + val timeToLive = source.readInt() + val recordDataLength = source.readShort().toLong() + + when { + `class` == CLASS_IN && type == TYPE_A -> { + if (recordDataLength != 4L) throw ProtocolException("unexpected record length") + val address = source.readByteString(4L) + return ResourceRecord.IpAddress( + name = name, + timeToLive = timeToLive, + address = address, + ) + } + + `class` == CLASS_IN && type == TYPE_AAAA -> { + if (recordDataLength != 16L) throw ProtocolException("unexpected record length") + val address = source.readByteString(16L) + return ResourceRecord.IpAddress( + name = name, + timeToLive = timeToLive, + address = address, + ) + } + + else -> { + source.skip(recordDataLength) + return null + } + } + } + + data class DnsMessage( + val id: Short, + val flags: Int, + val questions: List, + val answers: List, + val authorityRecords: List = listOf(), + val additionalRecords: List = listOf(), + ) { + val responseCode: Int + get() = (flags and 0b0000_0000_0000_1111) + } + + data class Question( + val name: String, + val type: Short, + val `class`: Short, + ) + + sealed interface ResourceRecord { + val name: String + val timeToLive: Int + + data class IpAddress( + override val name: String, + override val timeToLive: Int, + val address: ByteString, + ) : ResourceRecord + } +} + +internal val TYPE_A = 1 +internal val TYPE_AAAA = 28 + +internal val CLASS_IN = 1 + +internal val RESPONSE_CODE_SUCCESS = 0 +internal val RESPONSE_CODE_SERVER_FAILURE = 2 + +@Throws(IOException::class) +internal fun BufferedSource.skipAll() { + while (!exhausted()) { + skip(buffer.size) + } +} diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt index aaacf45ff4fd..66df7161b9f3 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -17,6 +17,7 @@ package okhttp3.dnsoverhttps import java.io.IOException import java.net.InetAddress +import java.net.ProtocolException import java.net.UnknownHostException import java.util.concurrent.CountDownLatch import okhttp3.Call @@ -81,7 +82,7 @@ class DnsOverHttps internal constructor( val failures = ArrayList(2) val results = ArrayList(5) - executeRequests(hostname, networkRequests, results, failures) + executeRequests(networkRequests, results, failures) return results.ifEmpty { throwBestFailure(hostname, failures) @@ -89,7 +90,6 @@ class DnsOverHttps internal constructor( } private fun executeRequests( - hostname: String, networkRequests: List, responses: MutableList, failures: MutableList, @@ -113,7 +113,7 @@ class DnsOverHttps internal constructor( call: Call, response: Response, ) { - processResponse(response, hostname, responses, failures) + processResponse(response, responses, failures) latch.countDown() } }, @@ -129,16 +129,15 @@ class DnsOverHttps internal constructor( private fun processResponse( response: Response, - hostname: String, results: MutableList, failures: MutableList, ) { try { - val addresses = readResponse(hostname, response) + val addresses = readResponse(response) synchronized(results) { results.addAll(addresses) } - } catch (e: Exception) { + } catch (e: IOException) { synchronized(failures) { failures.add(e) } @@ -170,31 +169,38 @@ class DnsOverHttps internal constructor( throw unknownHostException } - @Throws(Exception::class) - private fun readResponse( - hostname: String, - response: Response, - ): List { - if (response.cacheResponse == null && response.protocol !== Protocol.HTTP_2 && response.protocol !== Protocol.QUIC) { + @Throws(IOException::class) + private fun readResponse(response: Response): List { + if ( + response.cacheResponse == null && + response.protocol !== Protocol.HTTP_2 && + response.protocol !== Protocol.QUIC + ) { Platform.get().log("Incorrect protocol: ${response.protocol}", Platform.WARN) } response.use { if (!response.isSuccessful) { - throw IOException("response: " + response.code + " " + response.message) + throw IOException("response: ${response.code} ${response.message}") } val body = response.body - if (body.contentLength() > MAX_RESPONSE_SIZE) { - throw IOException( + throw ProtocolException( "response size exceeds limit ($MAX_RESPONSE_SIZE bytes): ${body.contentLength()} bytes", ) } - val responseBytes = body.source().readByteString() - - return DnsRecordCodec.decodeAnswers(hostname, responseBytes) + val dnsResponse = DnsMessageReader(body.source()).read() + when (dnsResponse.responseCode) { + RESPONSE_CODE_SUCCESS -> { + return dnsResponse.answers + .filterIsInstance() + .map { InetAddress.getByAddress(it.address.toByteArray()) } + } + RESPONSE_CODE_SERVER_FAILURE -> throw UnknownHostException("DNS server failure") + else -> throw UnknownHostException() + } } } diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderTest.kt new file mode 100644 index 000000000000..c8925001f3e2 --- /dev/null +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.dnsoverhttps + +import assertk.assertThat +import assertk.assertions.isEqualTo +import java.net.InetAddress +import kotlin.test.Test +import okio.Buffer +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.toByteString + +class DnsMessageReaderTest { + @Test + fun `resource record with compressed suffix`() { + val reader = DnsMessageReader( + "00008180000100040000000006676f6f676c6503636f6d00001c0001c00c001c00010000003c00102607f8b040" + + "2318070000000000000071c00c001c00010000003c00102607f8b0402318070000000000000065c00c001c00" + + "010000003c00102607f8b040231807000000000000008ac00c001c00010000003c00102607f8b04023180700" + + "0000000000008b" + ) + assertThat(reader.read()).isEqualTo( + DnsMessageReader.DnsMessage( + id = 0, + flags = -32384, + questions = listOf( + DnsMessageReader.Question( + name = "google.com", + type = 28, + `class` = 1, + ) + ), + answers = listOf( + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:71").address.toByteString() + ), + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:65").address.toByteString() + ), + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:8a").address.toByteString() + ), + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:8b").address.toByteString() + ), + ) + ) + ) + } + + @Test + fun `resource record with not compressed suffix`() { + val reader = DnsMessageReader( + "00008180000100010000000106676f6f676c6503636f6d0000010001c00c000100010000028e0004acd917ee00" + + "002904d000000000000d000c0009585858585858585858" + ) + assertThat(reader.read()).isEqualTo( + DnsMessageReader.DnsMessage( + id = 0, + flags = -32384, + questions = listOf( + DnsMessageReader.Question( + name = "google.com", + type = 1, + `class` = 1, + ) + ), + answers = listOf( + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 654, + address = InetAddress.getByName("172.217.23.238").address.toByteString() + ) + ) + ) + ) + } + + @Test + fun `ignore cname`() { + val reader = DnsMessageReader( + "0000818000010002000000000567726170680866616365626f6f6b03636f6d0000010001c00c0005000100000a" + + "0f000c04737461720463313072c012c030000100010000002e00041f0d5008" + ) + assertThat(reader.read()).isEqualTo( + DnsMessageReader.DnsMessage( + id = 0, + flags = -32384, + questions = listOf( + DnsMessageReader.Question( + name = "graph.facebook.com", + type = 1, + `class` = 1, + ) + ), + answers = listOf( + DnsMessageReader.ResourceRecord.IpAddress( + name = "star.c10r.facebook.com", + timeToLive = 46, + address = InetAddress.getByName("31.13.80.8").address.toByteString() + ) + ) + ) + ) + } + + @Test + fun `ignore soa`() { + val reader = DnsMessageReader( + "0000818300010000000100000e7364666c6b686673646c6b6a646602656500001c0001c01b00060001000004ff" + + "0038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b6a554d2d0000" + + "0e10000003840012750000000e10" + ) + assertThat(reader.read()).isEqualTo( + DnsMessageReader.DnsMessage( + id = 0, + flags = -32381, + questions = listOf( + DnsMessageReader.Question( + name = "sdflkhfsdlkjdf.ee", + type = 28, + `class` = 1, + ) + ), + answers = listOf( + ) + ) + ) + } + + /** https://www.rfc-editor.org/info/rfc2671/ */ + @Test + fun `ignore opt`() { + val reader = DnsMessageReader( + "00008180000100010000000106676f6f676c6503636f6d0000010001c00c000100010000028e0004acd917ee00" + + "002904d000000000000d000c0009585858585858585858" + ) + assertThat(reader.read()).isEqualTo( + DnsMessageReader.DnsMessage( + id = 0, + flags = -32384, + questions = listOf( + DnsMessageReader.Question( + name = "google.com", + type = 1, + `class` = 1, + ) + ), + answers = listOf( + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 654, + address = InetAddress.getByName("172.217.23.238").address.toByteString(), + ) + ) + ) + ) + } + + private fun DnsMessageReader(hex: String) = + DnsMessageReader(Buffer().write(hex.decodeHex())) +} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index 6d6f9aa5dbe3..a304a6a7813f 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -30,6 +30,7 @@ import java.net.InetAddress import java.net.UnknownHostException import java.util.concurrent.TimeUnit import kotlin.reflect.KClass +import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop @@ -134,11 +135,8 @@ class DnsOverHttpsTest { "2c100000e10000003840012750000000e10", ), ) - try { + assertFailsWith { dns.lookup("google.com") - fail() - } catch (uhe: UnknownHostException) { - assertThat(uhe.message).isEqualTo("google.com: NXDOMAIN") } val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("GET") From aa5fd0cb5edb500be5942ff8adb5f216cde95cec Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 13 Jul 2026 19:23:15 -0400 Subject: [PATCH 2/4] Spotless --- .../okhttp3/dnsoverhttps/-DnsMessageReader.kt | 14 +- .../okhttp3/dnsoverhttps/DnsOverHttps.kt | 10 +- .../dnsoverhttps/DnsMessageReaderTest.kt | 229 +++++++++--------- 3 files changed, 138 insertions(+), 115 deletions(-) diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt index 5cd61cd7731f..f2067f549ff5 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt @@ -91,13 +91,15 @@ internal class DnsMessageReader( ) } - private fun readName(): String { - return buildString { + private fun readName(): String = + buildString { readName(source, this) } - } - private tailrec fun readName(source: BufferedSource, builder: StringBuilder) { + private tailrec fun readName( + source: BufferedSource, + builder: StringBuilder, + ) { while (true) { val labelTypeAndLength = source.readByte().toUByte().toInt() val labelType = labelTypeAndLength and 0b11000000 @@ -118,7 +120,9 @@ internal class DnsMessageReader( return readName(offsetSource, builder) } - 0b01_000000, 0b10_000000 -> throw ProtocolException("unsupported label type") + 0b01_000000, 0b10_000000 -> { + throw ProtocolException("unsupported label type") + } } } } diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt index 66df7161b9f3..766c034f53e4 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -198,8 +198,14 @@ class DnsOverHttps internal constructor( .filterIsInstance() .map { InetAddress.getByAddress(it.address.toByteArray()) } } - RESPONSE_CODE_SERVER_FAILURE -> throw UnknownHostException("DNS server failure") - else -> throw UnknownHostException() + + RESPONSE_CODE_SERVER_FAILURE -> { + throw UnknownHostException("DNS server failure") + } + + else -> { + throw UnknownHostException() + } } } } diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderTest.kt index c8925001f3e2..cf6376f57e38 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderTest.kt @@ -26,158 +26,171 @@ import okio.ByteString.Companion.toByteString class DnsMessageReaderTest { @Test fun `resource record with compressed suffix`() { - val reader = DnsMessageReader( - "00008180000100040000000006676f6f676c6503636f6d00001c0001c00c001c00010000003c00102607f8b040" + - "2318070000000000000071c00c001c00010000003c00102607f8b0402318070000000000000065c00c001c00" + - "010000003c00102607f8b040231807000000000000008ac00c001c00010000003c00102607f8b04023180700" + - "0000000000008b" - ) + val reader = + DnsMessageReader( + "00008180000100040000000006676f6f676c6503636f6d00001c0001c00c001c00010000003c00102607f8b040" + + "2318070000000000000071c00c001c00010000003c00102607f8b0402318070000000000000065c00c001c00" + + "010000003c00102607f8b040231807000000000000008ac00c001c00010000003c00102607f8b04023180700" + + "0000000000008b", + ) assertThat(reader.read()).isEqualTo( DnsMessageReader.DnsMessage( id = 0, flags = -32384, - questions = listOf( - DnsMessageReader.Question( - name = "google.com", - type = 28, - `class` = 1, - ) - ), - answers = listOf( - DnsMessageReader.ResourceRecord.IpAddress( - name = "google.com", - timeToLive = 60, - address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:71").address.toByteString() - ), - DnsMessageReader.ResourceRecord.IpAddress( - name = "google.com", - timeToLive = 60, - address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:65").address.toByteString() + questions = + listOf( + DnsMessageReader.Question( + name = "google.com", + type = 28, + `class` = 1, + ), ), - DnsMessageReader.ResourceRecord.IpAddress( - name = "google.com", - timeToLive = 60, - address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:8a").address.toByteString() + answers = + listOf( + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:71").address.toByteString(), + ), + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:65").address.toByteString(), + ), + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:8a").address.toByteString(), + ), + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:8b").address.toByteString(), + ), ), - DnsMessageReader.ResourceRecord.IpAddress( - name = "google.com", - timeToLive = 60, - address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:8b").address.toByteString() - ), - ) - ) + ), ) } @Test fun `resource record with not compressed suffix`() { - val reader = DnsMessageReader( - "00008180000100010000000106676f6f676c6503636f6d0000010001c00c000100010000028e0004acd917ee00" + - "002904d000000000000d000c0009585858585858585858" - ) + val reader = + DnsMessageReader( + "00008180000100010000000106676f6f676c6503636f6d0000010001c00c000100010000028e0004acd917ee00" + + "002904d000000000000d000c0009585858585858585858", + ) assertThat(reader.read()).isEqualTo( DnsMessageReader.DnsMessage( id = 0, flags = -32384, - questions = listOf( - DnsMessageReader.Question( - name = "google.com", - type = 1, - `class` = 1, - ) - ), - answers = listOf( - DnsMessageReader.ResourceRecord.IpAddress( - name = "google.com", - timeToLive = 654, - address = InetAddress.getByName("172.217.23.238").address.toByteString() - ) - ) - ) + questions = + listOf( + DnsMessageReader.Question( + name = "google.com", + type = 1, + `class` = 1, + ), + ), + answers = + listOf( + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 654, + address = InetAddress.getByName("172.217.23.238").address.toByteString(), + ), + ), + ), ) } @Test fun `ignore cname`() { - val reader = DnsMessageReader( - "0000818000010002000000000567726170680866616365626f6f6b03636f6d0000010001c00c0005000100000a" + - "0f000c04737461720463313072c012c030000100010000002e00041f0d5008" - ) + val reader = + DnsMessageReader( + "0000818000010002000000000567726170680866616365626f6f6b03636f6d0000010001c00c0005000100000a" + + "0f000c04737461720463313072c012c030000100010000002e00041f0d5008", + ) assertThat(reader.read()).isEqualTo( DnsMessageReader.DnsMessage( id = 0, flags = -32384, - questions = listOf( - DnsMessageReader.Question( - name = "graph.facebook.com", - type = 1, - `class` = 1, - ) - ), - answers = listOf( - DnsMessageReader.ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 46, - address = InetAddress.getByName("31.13.80.8").address.toByteString() - ) - ) - ) + questions = + listOf( + DnsMessageReader.Question( + name = "graph.facebook.com", + type = 1, + `class` = 1, + ), + ), + answers = + listOf( + DnsMessageReader.ResourceRecord.IpAddress( + name = "star.c10r.facebook.com", + timeToLive = 46, + address = InetAddress.getByName("31.13.80.8").address.toByteString(), + ), + ), + ), ) } @Test fun `ignore soa`() { - val reader = DnsMessageReader( - "0000818300010000000100000e7364666c6b686673646c6b6a646602656500001c0001c01b00060001000004ff" + - "0038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b6a554d2d0000" + - "0e10000003840012750000000e10" - ) + val reader = + DnsMessageReader( + "0000818300010000000100000e7364666c6b686673646c6b6a646602656500001c0001c01b00060001000004ff" + + "0038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b6a554d2d0000" + + "0e10000003840012750000000e10", + ) assertThat(reader.read()).isEqualTo( DnsMessageReader.DnsMessage( id = 0, flags = -32381, - questions = listOf( - DnsMessageReader.Question( - name = "sdflkhfsdlkjdf.ee", - type = 28, - `class` = 1, - ) - ), - answers = listOf( - ) - ) + questions = + listOf( + DnsMessageReader.Question( + name = "sdflkhfsdlkjdf.ee", + type = 28, + `class` = 1, + ), + ), + answers = + listOf(), + ), ) } /** https://www.rfc-editor.org/info/rfc2671/ */ @Test fun `ignore opt`() { - val reader = DnsMessageReader( - "00008180000100010000000106676f6f676c6503636f6d0000010001c00c000100010000028e0004acd917ee00" + - "002904d000000000000d000c0009585858585858585858" - ) + val reader = + DnsMessageReader( + "00008180000100010000000106676f6f676c6503636f6d0000010001c00c000100010000028e0004acd917ee00" + + "002904d000000000000d000c0009585858585858585858", + ) assertThat(reader.read()).isEqualTo( DnsMessageReader.DnsMessage( id = 0, flags = -32384, - questions = listOf( - DnsMessageReader.Question( - name = "google.com", - type = 1, - `class` = 1, - ) - ), - answers = listOf( - DnsMessageReader.ResourceRecord.IpAddress( - name = "google.com", - timeToLive = 654, - address = InetAddress.getByName("172.217.23.238").address.toByteString(), - ) - ) - ) + questions = + listOf( + DnsMessageReader.Question( + name = "google.com", + type = 1, + `class` = 1, + ), + ), + answers = + listOf( + DnsMessageReader.ResourceRecord.IpAddress( + name = "google.com", + timeToLive = 654, + address = InetAddress.getByName("172.217.23.238").address.toByteString(), + ), + ), + ), ) } - private fun DnsMessageReader(hex: String) = - DnsMessageReader(Buffer().write(hex.decodeHex())) + private fun DnsMessageReader(hex: String) = DnsMessageReader(Buffer().write(hex.decodeHex())) } From bb0fc43acaf0cbd04e86d89db42954aaf5b13bc6 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 13 Jul 2026 21:47:27 -0400 Subject: [PATCH 3/4] Avoid hashCode accelerators --- .../okhttp3/dnsoverhttps/-DnsMessageReader.kt | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt index f2067f549ff5..e513026e6fc6 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt @@ -173,13 +173,34 @@ internal class DnsMessageReader( ) { val responseCode: Int get() = (flags and 0b0000_0000_0000_1111) + + // Avoid Short.hashCode(short) which isn't available on Android 5. + override fun hashCode(): Int { + var result = 0 + result = 31 * result + id + result = 31 * result + flags + result = 31 * result + questions.hashCode() + result = 31 * result + answers.hashCode() + result = 31 * result + authorityRecords.hashCode() + result = 31 * result + additionalRecords.hashCode() + return result + } } data class Question( val name: String, val type: Short, val `class`: Short, - ) + ) { + // Avoid Short.hashCode(short) which isn't available on Android 5. + override fun hashCode(): Int { + var result = 0 + result = 31 * result + name.hashCode() + result = 31 * result + type + result = 31 * result + `class` + return result + } + } sealed interface ResourceRecord { val name: String @@ -189,7 +210,16 @@ internal class DnsMessageReader( override val name: String, override val timeToLive: Int, val address: ByteString, - ) : ResourceRecord + ) : ResourceRecord { + // Avoid Int.hashCode(int) which isn't available on Android 5. + override fun hashCode(): Int { + var result = 0 + result = 31 * result + name.hashCode() + result = 31 * result + timeToLive + result = 31 * result + address.hashCode() + return result + } + } } } From 102c6e0f53dcc4afcafdb8c29dc358b3cc202656 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Tue, 14 Jul 2026 09:02:24 -0400 Subject: [PATCH 4/4] PR feedback --- .../okhttp3/dnsoverhttps/-DnsMessageReader.kt | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt index e513026e6fc6..efacd7ccc36b 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt @@ -18,6 +18,7 @@ package okhttp3.dnsoverhttps import java.io.IOException +import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.ProtocolException @@ -41,25 +42,25 @@ internal class DnsMessageReader( val questionCount = source.readShort().toUShort().toInt() val answerCount = source.readShort().toUShort().toInt() - val nameServerCount = source.readShort().toUShort().toInt() + val authorityRecordCount = source.readShort().toUShort().toInt() val additionalRecordCount = source.readShort().toUShort().toInt() - val questions = mutableListOf() + val questions = ArrayList(questionCount) for (i in 0 until questionCount) { questions += readQuestion() } - val answers = mutableListOf() + val answers = ArrayList(answerCount) for (i in 0 until answerCount) { answers += readResourceRecord() ?: continue } - val authorityRecords = mutableListOf() - for (i in 0 until nameServerCount) { + val authorityRecords = ArrayList(authorityRecordCount) + for (i in 0 until authorityRecordCount) { authorityRecords += readResourceRecord() ?: continue } - val additionalRecords = mutableListOf() + val additionalRecords = ArrayList(additionalRecordCount) for (i in 0 until additionalRecordCount) { additionalRecords += readResourceRecord() ?: continue } @@ -91,14 +92,15 @@ internal class DnsMessageReader( ) } - private fun readName(): String = - buildString { - readName(source, this) - } + private fun readName(): String { + val result = Buffer() + readName(source, result) + return result.readUtf8() + } private tailrec fun readName( source: BufferedSource, - builder: StringBuilder, + sink: Buffer, ) { while (true) { val labelTypeAndLength = source.readByte().toUByte().toInt() @@ -108,8 +110,8 @@ internal class DnsMessageReader( // Inline value. 0b00_000000 -> { if (labelLength == 0) return - if (builder.isNotEmpty()) builder.append('.') - builder.append(source.readUtf8(labelLength.toLong())) + if (sink.size > 0L) sink.writeByte('.'.code) + sink.write(source, labelLength.toLong()) } // Compressed suffix. @@ -117,7 +119,7 @@ internal class DnsMessageReader( val offsetLength = (labelLength shl 8) or source.readByte().toUByte().toInt() val offsetSource = sourceOffsetZero.peek() offsetSource.skip(offsetLength.toLong()) - return readName(offsetSource, builder) + return readName(offsetSource, sink) } 0b01_000000, 0b10_000000 -> {