DnsSpeedTest.kt 9.63 KB
Newer Older
1
2
3
package com.frostnerd.smokescreen.util.speedtest

import androidx.annotation.IntRange
4
5
6
7
import cn.danielw.fop.ObjectFactory
import cn.danielw.fop.ObjectPool
import cn.danielw.fop.PoolConfig
import cn.danielw.fop.Poolable
8
9
import com.frostnerd.dnstunnelproxy.DnsServerInformation
import com.frostnerd.dnstunnelproxy.UpstreamAddress
Daniel Wolf's avatar
Daniel Wolf committed
10
11
import com.frostnerd.encrypteddnstunnelproxy.HttpsDnsServerInformation
import com.frostnerd.encrypteddnstunnelproxy.ServerConfiguration
12
import com.frostnerd.encrypteddnstunnelproxy.tls.TLSUpstreamAddress
13
14
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrl
Daniel Wolf's avatar
Daniel Wolf committed
15
import okhttp3.RequestBody.Companion.toRequestBody
16
import okhttp3.internal.closeQuietly
17
18
19
20
21
import org.minidns.dnsmessage.DnsMessage
import org.minidns.dnsmessage.Question
import org.minidns.record.Record
import java.io.DataInputStream
import java.io.DataOutputStream
22
import java.lang.IllegalArgumentException
23
import java.net.*
24
import java.util.concurrent.ConcurrentHashMap
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLSocketFactory
import kotlin.random.Random

/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * You can contact the developer at daniel.wolf@frostnerd.com.
 */

Daniel Wolf's avatar
Daniel Wolf committed
48
class DnsSpeedTest(val server: DnsServerInformation<*>,
Daniel Wolf's avatar
Daniel Wolf committed
49
50
                   private val connectTimeout: Int = 2500,
                   private val readTimeout:Int = 1500,
Daniel Wolf's avatar
Daniel Wolf committed
51
                   val log:(line:String) -> Unit) {
52
53
54
55
    private val httpClient by lazy {
        OkHttpClient.Builder()
            .dns(httpsDnsClient)
            .connectTimeout(3, TimeUnit.SECONDS)
56
            .readTimeout(readTimeout.toLong(), TimeUnit.MILLISECONDS)
57
58
59
60
61
62
63
            .build()
    }
    private val httpsDnsClient by lazy {
        PinnedDns((server as HttpsDnsServerInformation).serverConfigurations.values.map {
            it.urlCreator.address
        })
    }
64
    private val connectionPool = ConcurrentHashMap<TLSUpstreamAddress, ObjectPool<Socket>>()
Daniel Wolf's avatar
Daniel Wolf committed
65
66
67
68
69
70
    private var poolConfig:PoolConfig = PoolConfig().apply {
        this.maxSize = 1
        this.minSize = 1
        this.partitionSize = 1
        this.maxIdleMilliseconds = 60*1000*5
    }
71
72
73
74
    private val poolFactory = object: ObjectFactory<Socket> {
        private val sslSocketFactory = SSLSocketFactory.getDefault()

        override fun validate(t: Socket): Boolean {
75
            return t.isClosed || !t.isConnected
76
77
78
79
80
81
82
83
84
85
86
        }

        override fun destroy(t: Socket) {
            t.closeQuietly()
        }

        override fun create(): Socket {
            return sslSocketFactory.createSocket()
        }
    }

87
88
89
90
91
92
93
94
95
96
97
    companion object {
        val testDomains = listOf("google.com", "frostnerd.com", "amazon.com", "youtube.com", "github.com",
            "stackoverflow.com", "stackexchange.com", "spotify.com", "material.io", "reddit.com", "android.com")
    }

    /**
     * @param passes The amount of requests to make
     * @return The average response time (in ms)
     */
    fun runTest(@IntRange(from = 1) passes: Int): Int? {
        var ttl = 0
98

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
        for (i in 0 until passes) {
            if (server is HttpsDnsServerInformation) {
                server.serverConfigurations.values.forEach {
                    ttl += testHttps(it) ?: 0
                }
            } else {
                (server as DnsServerInformation<TLSUpstreamAddress>).servers.forEach {
                    ttl += testTls(it.address) ?: 0
                }
            }
        }
        return (ttl / passes).let {
            if (it <= 0) null else it
        }
    }

    private fun testHttps(config: ServerConfiguration): Int? {
        val msg = createTestDnsPacket()
        val url: URL = config.urlCreator.createUrl(msg, config.urlCreator.address)
118
119
120
121
122
123
124
125
        try {
            url.toString().toHttpUrl()
            log("Using URL: $url")
        } catch (ignored:IllegalArgumentException) {
            log("Invalid URL: $url")
            return null
        }

126
127
128
129
130
        val requestBuilder = Request.Builder().url(url)
        if (config.requestHasBody) {
            val body = config.bodyCreator!!.createBody(msg, config.urlCreator.address)
            if (body != null) {
                requestBuilder.header("Content-Type", config.contentType)
Daniel Wolf's avatar
Daniel Wolf committed
131
                requestBuilder.post(body.rawBody.toRequestBody(body.mediaType, 0, body.rawBody.size))
132
            } else {
Daniel Wolf's avatar
Daniel Wolf committed
133
                log("DoH test failed once for ${server.name}: BodyCreator didn't create a body")
134
135
136
                return null
            }
        }
137
        var response:Response? = null
138
139
        try {
            val start = System.currentTimeMillis()
140
            response = httpClient.newCall(requestBuilder.build()).execute()
Daniel Wolf's avatar
Daniel Wolf committed
141
            if(!response.isSuccessful) {
Daniel Wolf's avatar
Daniel Wolf committed
142
                log("DoH test failed once for ${server.name}: Request not successful (${response.code})")
Daniel Wolf's avatar
Daniel Wolf committed
143
144
145
146
147
148
                return null
            }
            val body = response.body ?: run{
                log("DoH test failed once for ${server.name}: No response body")
                return null
            }
149
150
151
152
            val bytes = body.bytes()
            val time = (System.currentTimeMillis() - start).toInt()

            if (bytes.size < 17) {
Daniel Wolf's avatar
Daniel Wolf committed
153
                log("DoH test failed once for ${server.name}: Returned less than 17 bytes")
154
155
                return null
            } else if(!testResponse(DnsMessage(bytes))) {
Daniel Wolf's avatar
Daniel Wolf committed
156
                log("DoH test failed once for ${server.name}: Testing the response for valid dns message failed")
157
158
159
160
                return null
            }
            return time
        } catch (ex: Exception) {
161
            log("DoH test failed with exception once for ${server.name}: ${ex.message}")
162
            return null
163
        } finally {
Daniel Wolf's avatar
Daniel Wolf committed
164
            if(response?.body != null) response.close()
165
166
167
        }
    }

168
169
170
171
172
173
174
175
176
177
    private fun obtainTlsSocket(address: TLSUpstreamAddress): Poolable<Socket>? {
        return try {
            connectionPool.getOrPut(address) {
                ObjectPool(poolConfig, poolFactory)
            }.borrowObject()
        } catch (e: RuntimeException) {
            null
        }
    }

178
179
    private fun testTls(address: TLSUpstreamAddress): Int? {
        val addr =
Daniel Wolf's avatar
Daniel Wolf committed
180
            address.addressCreator.resolveOrGetResultOrNull(retryIfError = true, runResolveNow = true) ?: run {
Daniel Wolf's avatar
Daniel Wolf committed
181
182
183
                log("DoT test failed once for ${server.name}: Address failed to resolve ($address)")
                return null
            }
184
185
        var socketPooled: Poolable<Socket>? = null
        var socket:Socket? = null
186
        try {
187
188
            socketPooled = obtainTlsSocket(address)
            socket = socketPooled?.`object` ?: SSLSocketFactory.getDefault().createSocket()
189
190
            val msg = createTestDnsPacket()
            val start = System.currentTimeMillis()
191
            socket!!.connect(InetSocketAddress(addr[0], address.port), connectTimeout)
192
            socket.soTimeout = readTimeout
193
194
195
196
197
198
199
200
201
202
203
204
205
206
            val data: ByteArray = msg.toArray()
            val outputStream = DataOutputStream(socket.getOutputStream())
            val size = data.size
            val arr: ByteArray = byteArrayOf(((size shr 8) and 0xFF).toByte(), (size and 0xFF).toByte())
            outputStream.write(arr)
            outputStream.write(data)
            outputStream.flush()

            val inStream = DataInputStream(socket.getInputStream())
            val readData = ByteArray(inStream.readUnsignedShort())
            inStream.read(readData)
            val time = (System.currentTimeMillis() - start).toInt()

            socket = null
Daniel Wolf's avatar
Daniel Wolf committed
207
208
209
210
            if(!testResponse(DnsMessage(readData))) {
                log("DoT test failed once for ${server.name}: Testing the response for valid dns message failed")
                return null
            }
211
212
            return time
        } catch (ex: Exception) {
Daniel Wolf's avatar
Daniel Wolf committed
213
            log("DoT test failed with exception once for ${server.name}: $ex")
214
215
216
            return null
        } finally {
            socket?.close()
217
            socketPooled?.returnObject()
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
        }
    }

    private fun createTestDnsPacket(): DnsMessage {
        val msg = DnsMessage.builder().setQrFlag(false)
            .addQuestion(Question(testDomains.random(), Record.TYPE.A, Record.CLASS.IN))
            .setId(Random.nextInt(1, 999999))
            .setRecursionDesired(true)
            .setAuthenticData(true)
            .setRecursionAvailable(true)
        return msg.build()
    }

    private fun testResponse(message:DnsMessage):Boolean {
        return message.answerSection.size > 0
    }

    private inner class PinnedDns(private val upstreamServers: List<UpstreamAddress>) : Dns {

        override fun lookup(hostname: String): MutableList<InetAddress> {
            val res = mutableListOf<InetAddress>()
            for (server in upstreamServers) {
                if (server.host.equals(hostname, true)) {
                    res.addAll(server.addressCreator.resolveOrGetResultOrNull(true) ?: emptyArray())
                    break
                }
            }
            if (res.isEmpty()) {
                res.addAll(Dns.SYSTEM.lookup(hostname))
            }
            if (res.isEmpty()) {
                throw UnknownHostException("Could not resolve $hostname")
            }
            return res
        }
    }
}