Skip to content

Commit 37eca4f

Browse files
committed
feat: add custom DNS resolver support to ProxyInterceptor
1 parent 4c6f8a9 commit 37eca4f

File tree

3 files changed

+143
-23
lines changed

3 files changed

+143
-23
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ dependencies {
204204
implementation(libs.quickjs)
205205
implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance
206206
implementation(libs.safefile) // To Prevent the URI File Fu*kery
207+
implementation(libs.dnsjava)
207208
coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor
208209
implementation(libs.conscrypt.android) {
209210
version {

app/src/main/java/com/lagradost/cloudstream3/network/ProxyInterceptor.kt

Lines changed: 139 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,59 @@ package com.lagradost.cloudstream3.network
22

33
import android.util.Log
44
import okhttp3.Credentials
5+
import okhttp3.Dns
6+
import okhttp3.HttpUrl.Companion.toHttpUrl
57
import okhttp3.Interceptor
68
import okhttp3.OkHttpClient
79
import okhttp3.Response
10+
import okhttp3.dnsoverhttps.DnsOverHttps
11+
import org.xbill.DNS.*
812
import java.io.IOException
913
import java.net.ConnectException
14+
import java.net.InetAddress
1015
import java.net.InetSocketAddress
1116
import java.net.Proxy
1217
import java.net.SocketTimeoutException
18+
import java.net.UnknownHostException
1319
import java.util.concurrent.TimeUnit
1420

21+
/**
22+
* An OkHttp Interceptor that routes requests through a proxy with custom DNS resolution.
23+
*
24+
* @param host The proxy server hostname or IP address.
25+
* @param port The proxy server port.
26+
* @param proxyType The type of proxy (e.g., HTTP, SOCKS). Defaults to HTTP.
27+
* @param username Optional proxy username for authentication.
28+
* @param password Optional proxy password for authentication.
29+
* @param allowFallback Whether to fall back to a direct connection if the proxy fails. Defaults to false.
30+
* @param connectTimeoutMillis Connection timeout in seconds. Defaults to 15.
31+
* @param readTimeoutMillis Read timeout in seconds. Defaults to 15.
32+
* @param dnsServer Optional custom DNS server (e.g., "8.8.8.8" or "cloudflare" for DoH).
33+
*/
1534
class ProxyInterceptor(
1635
private val host: String,
1736
private val port: Int,
1837
private val proxyType: Proxy.Type = Proxy.Type.HTTP,
1938
private val username: String? = null,
2039
private val password: String? = null,
2140
private val allowFallback: Boolean = false,
22-
private val connectTimeoutSeconds: Long = 15L,
23-
private val readTimeoutSeconds: Long = 15L
41+
private val connectTimeoutMillis: Long = 15_000L,
42+
private val readTimeoutMillis: Long = 15_000L,
43+
private val dnsServer: String? = null
2444
) : Interceptor {
2545

2646
companion object {
2747
private const val TAG = "ProxyDebug"
48+
private val DNS_OVER_HTTPS_URLS = mapOf(
49+
"cloudflare" to "https://cloudflare-dns.com/dns-query",
50+
"google" to "https://dns.google/dns-query",
51+
"quad9" to "https://dns.quad9.net/dns-query",
52+
"adguard" to "https://dns.adguard.com/dns-query"
53+
)
2854
}
2955

30-
init {
31-
Log.d(
32-
TAG,
33-
"proxy setup: " + listOf(
34-
"host=$host",
35-
"port=$port",
36-
"type=${proxyType.name}",
37-
"timeouts=${connectTimeoutSeconds}s/${readTimeoutSeconds}s",
38-
"auth=${if (username != null) "enabled" else "None"}",
39-
"fallback=${if (allowFallback) "Allowed" else "Disabled"}"
40-
).joinToString(separator = ", ") // Join only the parameters
41-
)
56+
private val internalDns by lazy {
57+
dnsServer?.let { createDnsResolver(it) } ?: Dns.SYSTEM
4258
}
4359

4460
private val proxyClient by lazy {
@@ -47,8 +63,9 @@ class ProxyInterceptor(
4763
val proxy = Proxy(proxyType, InetSocketAddress(host, port))
4864
OkHttpClient.Builder()
4965
.proxy(proxy)
50-
.connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS)
51-
.readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
66+
.dns(internalDns)
67+
.connectTimeout(connectTimeoutMillis, TimeUnit.MILLISECONDS)
68+
.readTimeout(readTimeoutMillis, TimeUnit.MILLISECONDS)
5269
.apply {
5370
if (username != null && password != null) {
5471
Log.d(TAG, "Configuring proxy credentials")
@@ -63,6 +80,79 @@ class ProxyInterceptor(
6380
.build()
6481
}
6582

83+
/**
84+
* Creates a custom DNS resolver based on the provided server.
85+
*
86+
* @param server The DNS server (e.g., DoH keyword, DoH URL, or IP address).
87+
* @return A configured Dns instance.
88+
*/
89+
private fun createDnsResolver(server: String): Dns {
90+
return when {
91+
server in DNS_OVER_HTTPS_URLS -> {
92+
val url = DNS_OVER_HTTPS_URLS.getValue(server)
93+
DnsOverHttps.Builder()
94+
.client(OkHttpClient())
95+
.url(url.toHttpUrl())
96+
.build()
97+
}
98+
99+
server.startsWith("https://") -> {
100+
try {
101+
DnsOverHttps.Builder()
102+
.client(OkHttpClient())
103+
.url(server.toHttpUrl())
104+
.build()
105+
} catch (e: IllegalArgumentException) {
106+
Log.e(TAG, "Invalid DoH URL: $server")
107+
Dns.SYSTEM
108+
}
109+
}
110+
111+
else -> {
112+
Log.d(TAG, "Using dnsjava for custom DNS server: $server")
113+
val resolver = SimpleResolver(server)
114+
val cacheA = Lookup.getDefaultCache(Type.A)
115+
val cacheAAAA = Lookup.getDefaultCache(Type.AAAA)
116+
117+
Dns { hostname ->
118+
try {
119+
val lookupA = Lookup(hostname, Type.A)
120+
lookupA.setResolver(resolver)
121+
lookupA.setCache(cacheA)
122+
val aRecords =
123+
lookupA.run()?.map { InetAddress.getByName(hostname) } ?: emptyList()
124+
125+
val lookupAAAA = Lookup(hostname, Type.AAAA)
126+
lookupAAAA.setResolver(resolver)
127+
lookupAAAA.setCache(cacheAAAA)
128+
val aaaaRecords =
129+
lookupAAAA.run()?.map { InetAddress.getByName(hostname) }
130+
?: emptyList()
131+
132+
(aRecords + aaaaRecords).ifEmpty {
133+
throw IOException("No DNS records found for $hostname")
134+
}
135+
} catch (e: UnknownHostException) {
136+
Log.w(TAG, "DNS lookup failed for $hostname: ${e.message}")
137+
Dns.SYSTEM.lookup(hostname)
138+
} catch (e: IOException) {
139+
Log.w(TAG, "IO error during DNS lookup for $hostname: ${e.message}")
140+
Dns.SYSTEM.lookup(hostname)
141+
} catch (e: Exception) {
142+
Log.e(TAG, "Unexpected error during DNS lookup for $hostname", e)
143+
throw e // Rethrow unexpected errors
144+
}
145+
}
146+
}
147+
}
148+
}
149+
150+
/**
151+
* Intercepts the request and routes it through the proxy.
152+
*
153+
* @param chain The interceptor chain.
154+
* @return The response from the proxy or fallback.
155+
*/
66156
override fun intercept(chain: Interceptor.Chain): Response {
67157
Log.d(TAG, "Intercepting request to ${chain.request().url.host}")
68158

@@ -71,11 +161,11 @@ class ProxyInterceptor(
71161

72162
Log.d(
73163
TAG,
74-
"proxy response:" + listOf(
164+
"Proxy response:" + listOf(
75165
"url=${response.request.url}",
76166
"status=${response.code}",
77167
"headers=${response.headers.size}",
78-
"body=${response.body?.contentLength() ?: 0} bytes"
168+
"body=${response.body.contentLength()} bytes"
79169
).joinToString(separator = " , ")
80170
)
81171

@@ -87,8 +177,8 @@ class ProxyInterceptor(
87177
} catch (e: IOException) {
88178
Log.d(
89179
TAG,
90-
"proxy error:" + listOf(
91-
"type=${e.javaClass.simpleName}",
180+
"Proxy error:" + listOf(
181+
"type=${e.javaClass}",
92182
"message=${e.message}",
93183
"request=${chain.request().url}"
94184
).joinToString(separator = " , ")
@@ -119,12 +209,12 @@ class ProxyInterceptor(
119209
}
120210

121211
is SocketTimeoutException -> {
122-
Log.d(TAG, "Timeout connecting to proxy (${connectTimeoutSeconds}s)")
212+
Log.d(TAG, "Timeout connecting to proxy (${connectTimeoutMillis}s)")
123213
if (allowFallback) fallback(chain) else throw e
124214
}
125215

126216
else -> {
127-
Log.d(TAG, "Unexpected proxy error: ${e.javaClass.simpleName}")
217+
Log.d(TAG, "Unexpected proxy error: ${e.javaClass}")
128218
throw e
129219
}
130220
}
@@ -135,12 +225,38 @@ class ProxyInterceptor(
135225
return chain.proceed(chain.request()).also { response ->
136226
Log.d(
137227
TAG,
138-
"direct connection: " + listOf(
228+
"Direct connection: " + listOf(
139229
"status=${response.code}",
140230
"via=${response.handshake?.tlsVersion ?: "Plaintext"}",
141231
"server=${response.header("Server") ?: "Unknown"}"
142232
).joinToString(separator = " , ")
143233
)
144234
}
145235
}
236+
237+
/**
238+
* Tests the DNS configuration by resolving "example.com".
239+
*
240+
* @return True if DNS resolution succeeds, false otherwise.
241+
*/
242+
fun testDnsConfiguration(): Boolean {
243+
val testDomain = "example.com"
244+
Log.d(TAG, "Testing DNS resolution for $testDomain")
245+
return try {
246+
val addresses = internalDns.lookup(testDomain)
247+
if (addresses.isNotEmpty()) {
248+
Log.d(
249+
TAG,
250+
"DNS resolution successful: ${addresses.joinToString { it.hostAddress ?: "unknown" }}"
251+
)
252+
true
253+
} else {
254+
Log.w(TAG, "DNS resolution returned no addresses for $testDomain")
255+
false
256+
}
257+
} catch (e: Exception) {
258+
Log.e(TAG, "DNS test failed: ${e.javaClass} - ${e.message}")
259+
false
260+
}
261+
}
146262
}

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ video = "1.0.0"
4545
workRuntime = "2.10.0"
4646
workRuntimeKtx = "2.10.0"
4747

48+
dnsjava = "3.6.3"
49+
4850
jvmTarget = "1.8"
4951
minSdk = "21"
5052
compileSdk = "35"
@@ -109,6 +111,7 @@ tvprovider = { module = "androidx.tvprovider:tvprovider", version.ref = "tvprovi
109111
video = { module = "com.google.android.mediahome:video", version.ref = "video" }
110112
work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }
111113
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
114+
dnsjava = { module = "dnsjava:dnsjava", version.ref = "dnsjava" }
112115

113116
[plugins]
114117

0 commit comments

Comments
 (0)