Connlib/forward dns (#2325)

With this we implement DNS forwarding that's specified in  #2043 

This also solve the DNS story in Android.

For the headless client in Linux we still need to implement split dns,
but we can make do with this, specially, we can read from resolvconf and
use the forward DNS (not ideal but can work if we want a beta headless
client).

For the resolver I used `trusted-proto-resolver`.

The other options were:

* Using `domain`'s resolver but while it could work for now, it's no
ideal for this since it doesn't support DoH or DoT and doesn't provide
us with a DNS cache.
* Using `trusted-proto-client`, it doesn't provide us with a DNS cache,
though we could eventually replace it since it provides a way to access
the underlying buffer which could make our code a bit simpler.
* Writing our own. While we could make the API ideal, this is too much
work for beta.


@pratikvelani I did some refactor in the kotlin side so we can return an
array of bytearrays so that we don't require parsing on connlib side, I
also tried to make the dns server detector a bit simpler please take a
look it's my first time doing kotlin

@thomaseizinger please take a look specially at the first commit, I
tried to integrate with the `poll_events` and the `ClientState`.
This commit is contained in:
Gabi
2023-10-18 17:39:20 -03:00
committed by GitHub
parent 44e05c8a3d
commit d626f6dbf6
20 changed files with 722 additions and 236 deletions

View File

@@ -124,8 +124,8 @@ class TunnelService : VpnService() {
return fd
}
override fun getSystemDefaultResolvers(): String {
return moshi.adapter<Array<String>>().toJson(DnsServersDetector(this@TunnelService).servers)
override fun getSystemDefaultResolvers(): Array<ByteArray> {
return DnsServersDetector(this@TunnelService).servers.map { it.address }.toTypedArray()
}
override fun onDisconnect(error: String?): Boolean {

View File

@@ -27,5 +27,5 @@ interface ConnlibCallback {
fun onError(error: String): Boolean
fun getSystemDefaultResolvers(): String
fun getSystemDefaultResolvers(): Array<ByteArray>
}

View File

@@ -40,36 +40,23 @@ class DnsServersDetector(
) {
//region - public //////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////
val servers: Array<String>
val servers: Set<InetAddress>
/**
* Returns android DNS servers used for current connected network
* @return Dns servers array
*/
get() {
// use connectivity manager
serversMethodConnectivityManager?.run {
if (isNotEmpty()) {
return this
}
}
// detect android DNS servers by executing getprop string command in a separate process
// This method fortunately works in Oreo too but many people may want to avoid exec
// so it's used only as a failsafe scenario
serversMethodExec?.run {
if (isNotEmpty()) {
return this
}
}
// Fall back on factory DNS servers
return FACTORY_DNS_SERVERS
return serversMethodConnectivityManager
?.takeIf { it.isNotEmpty() }
?: serversMethodExec
?.takeIf { it.isNotEmpty() }
?: FACTORY_DNS_SERVERS
}
//endregion
//region - private /////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////
private val serversMethodConnectivityManager: Array<String>?
private val serversMethodConnectivityManager: Set<InetAddress>?
/**
* Detect android DNS servers by using connectivity manager
*
@@ -81,8 +68,8 @@ class DnsServersDetector(
// This code only works on LOLLIPOP and higher
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try {
val priorityServersArrayList = ArrayList<String>()
val serversArrayList = ArrayList<String>()
val priorityServers: MutableSet<InetAddress> = HashSet(10)
val servers: MutableSet<InetAddress> = HashSet(10)
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
if (connectivityManager != null) {
@@ -93,37 +80,20 @@ class DnsServersDetector(
val networkInfo = connectivityManager.getNetworkInfo(network)
if (networkInfo!!.isConnected) {
val linkProperties = connectivityManager.getLinkProperties(network)
val dnsServersList = linkProperties!!.dnsServers
val dnsServersList = linkProperties!!.dnsServers.toSet()
// Prioritize the DNS servers for link which have a default route
if (linkPropertiesHasDefaultRoute(linkProperties)) {
for (element in dnsServersList) {
val dnsHost = element.hostAddress
dnsHost?.let {
priorityServersArrayList.add(it)
}
}
priorityServers += dnsServersList
} else {
for (element in dnsServersList) {
val dnsHost = element.hostAddress
dnsHost?.let {
serversArrayList.add(it)
}
}
servers += dnsServersList
}
}
}
}
// Append secondary arrays only if priority is empty
if (priorityServersArrayList.isEmpty()) {
priorityServersArrayList.addAll(serversArrayList)
}
// Stop here if we have at least one DNS server
if (priorityServersArrayList.size > 0) {
return priorityServersArrayList.toTypedArray()
}
return priorityServers.takeIf { it.isNotEmpty() } ?: servers
} catch (ex: Exception) {
Log.d(
TAG,
@@ -133,10 +103,9 @@ class DnsServersDetector(
}
}
// Failure
return null
}
private val serversMethodExec: Array<String>?
private val serversMethodExec: Set<InetAddress>?
/**
* Detect android DNS servers by executing getprop string command in a separate process
*
@@ -152,15 +121,11 @@ class DnsServersDetector(
val process = Runtime.getRuntime().exec("getprop")
val inputStream = process.inputStream
val lineNumberReader = LineNumberReader(InputStreamReader(inputStream))
val serversSet = methodExecParseProps(lineNumberReader)
if (serversSet.isNotEmpty()) {
return serversSet.toTypedArray()
}
return methodExecParseProps(lineNumberReader)
} catch (ex: Exception) {
Log.d(TAG, "Exception in getServersMethodExec", ex)
}
// Failed
return null
}
@@ -171,9 +136,9 @@ class DnsServersDetector(
* @throws Exception
*/
@Throws(Exception::class)
private fun methodExecParseProps(lineNumberReader: BufferedReader): Set<String> {
private fun methodExecParseProps(lineNumberReader: BufferedReader): Set<InetAddress> {
var line: String
val serversSet: MutableSet<String> = HashSet(10)
val serversSet: MutableSet<InetAddress> = HashSet(10)
while (lineNumberReader.readLine().also { line = it } != null) {
val split = line.indexOf(METHOD_EXEC_PROP_DELIM)
if (split == -1) {
@@ -196,11 +161,10 @@ class DnsServersDetector(
property.endsWith(".dns2") || property.endsWith(".dns3") ||
property.endsWith(".dns4")
) {
InetAddress.getByName(value).hostAddress?.takeIf { it.isNotEmpty() }?.let {
serversSet.add(it)
}
serversSet.add(InetAddress.getByName(value))
}
}
return serversSet
}
@@ -226,9 +190,9 @@ class DnsServersDetector(
* Can be set to null if you want caller to fail in this situation.
*/
private val FACTORY_DNS_SERVERS =
arrayOf(
"8.8.8.8",
"8.8.4.4",
setOf(
InetAddress.getByName("8.8.8.8"),
InetAddress.getByName("8.8.4.4"),
)
/**

313
rust/Cargo.lock generated
View File

@@ -126,9 +126,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
@@ -170,15 +170,15 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
[[package]]
name = "anstyle-parse"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
dependencies = [
"utf8parse",
]
@@ -302,14 +302,14 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
name = "atomic-waker"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
@@ -470,9 +470,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "blake2"
@@ -550,9 +550,9 @@ dependencies = [
[[package]]
name = "byteorder"
version = "1.4.3"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
@@ -735,7 +735,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -811,6 +811,8 @@ dependencies = [
"chrono",
"connlib-shared",
"firezone-tunnel",
"hickory-resolver",
"parking_lot",
"reqwest",
"secrecy",
"serde",
@@ -837,6 +839,7 @@ dependencies = [
"chrono",
"futures",
"futures-util",
"hickory-resolver",
"ip_network",
"log",
"os_info",
@@ -1033,7 +1036,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -1128,7 +1131,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -1191,6 +1194,18 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "enum-as-inner"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "env_logger"
version = "0.10.0"
@@ -1206,30 +1221,19 @@ dependencies = [
[[package]]
name = "errno"
version = "0.3.3"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "fastrand"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "ff"
@@ -1300,6 +1304,7 @@ dependencies = [
"futures",
"futures-bounded 0.2.0",
"futures-util",
"hickory-resolver",
"ip_network",
"ip_network_table",
"itertools 0.11.0",
@@ -1374,7 +1379,7 @@ dependencies = [
[[package]]
name = "futures-bounded"
version = "0.2.0"
source = "git+https://github.com/libp2p/rust-libp2p?branch=feat/stream-map#0c0349221f3daa697ae871ab6dba5c1f84e84b10"
source = "git+https://github.com/libp2p/rust-libp2p?branch=feat/stream-map#07ff4c28f6d7d878641fe9bf464b27f84191dc77"
dependencies = [
"futures-timer",
"futures-util",
@@ -1421,7 +1426,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -1584,6 +1589,51 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "hickory-proto"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf"
dependencies = [
"async-trait",
"cfg-if 1.0.0",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"once_cell",
"rand",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35b8f021164e6a984c9030023544c57789c51760065cd510572fedcfb04164e8"
dependencies = [
"cfg-if 1.0.0",
"futures-util",
"hickory-proto",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "hkdf"
version = "0.12.3"
@@ -1620,6 +1670,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]]
name = "http"
version = "0.2.9"
@@ -1712,16 +1773,16 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.57"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
"android_system_properties",
"core-foundation-sys 0.8.4",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows 0.48.0",
"windows-core",
]
[[package]]
@@ -1824,6 +1885,18 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d"
[[package]]
name = "ipconfig"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2 0.5.4",
"widestring",
"windows-sys 0.48.0",
"winreg",
]
[[package]]
name = "ipnet"
version = "2.8.0"
@@ -1938,9 +2011,9 @@ dependencies = [
[[package]]
name = "libloading"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161"
dependencies = [
"cfg-if 1.0.0",
"windows-sys 0.48.0",
@@ -1948,9 +2021,15 @@ dependencies = [
[[package]]
name = "libm"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
@@ -1960,9 +2039,9 @@ checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
[[package]]
name = "lock_api"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
dependencies = [
"autocfg",
"scopeguard",
@@ -1974,6 +2053,15 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "mach"
version = "0.2.3"
@@ -1992,6 +2080,12 @@ dependencies = [
"libc",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matchers"
version = "0.1.0"
@@ -2003,16 +2097,17 @@ dependencies = [
[[package]]
name = "matchit"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "md-5"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if 1.0.0",
"digest",
]
@@ -2024,9 +2119,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.6.3"
version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "memoffset"
@@ -2165,7 +2260,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.0",
"bitflags 2.4.1",
"cfg-if 1.0.0",
"libc",
]
@@ -2219,9 +2314,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
"autocfg",
"libm",
@@ -2379,9 +2474,9 @@ dependencies = [
[[package]]
name = "ordered-float"
version = "3.9.1"
version = "3.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06"
checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc"
dependencies = [
"num-traits",
]
@@ -2436,13 +2531,13 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.8"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"redox_syscall 0.4.1",
"smallvec",
"windows-targets 0.48.5",
]
@@ -2517,7 +2612,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -2566,7 +2661,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -2627,9 +2722,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.67"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
@@ -2642,7 +2737,7 @@ checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e"
dependencies = [
"bit-set",
"bit-vec",
"bitflags 2.4.0",
"bitflags 2.4.1",
"lazy_static",
"num-traits",
"rand",
@@ -2773,15 +2868,24 @@ dependencies = [
]
[[package]]
name = "regex"
version = "1.9.5"
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.8",
"regex-syntax 0.7.5",
"regex-automata 0.4.3",
"regex-syntax 0.8.2",
]
[[package]]
@@ -2795,13 +2899,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.8"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.5",
"regex-syntax 0.8.2",
]
[[package]]
@@ -2816,6 +2920,12 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "relay"
version = "1.20231001.0"
@@ -2900,6 +3010,16 @@ dependencies = [
"winreg",
]
[[package]]
name = "resolv-conf"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
dependencies = [
"hostname",
"quick-error",
]
[[package]]
name = "rfc6979"
version = "0.3.1"
@@ -3016,7 +3136,7 @@ version = "0.38.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed"
dependencies = [
"bitflags 2.4.0",
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
@@ -3208,9 +3328,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.18"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
@@ -3229,7 +3349,7 @@ checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -3285,9 +3405,9 @@ dependencies = [
[[package]]
name = "sharded-slab"
version = "0.1.4"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
@@ -3414,7 +3534,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -3425,7 +3545,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -3533,9 +3653,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.37"
version = "2.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
dependencies = [
"proc-macro2",
"quote",
@@ -3589,7 +3709,7 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if 1.0.0",
"fastrand",
"redox_syscall",
"redox_syscall 0.3.5",
"rustix",
"windows-sys 0.48.0",
]
@@ -3612,7 +3732,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -3632,7 +3752,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -3726,7 +3846,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -3882,7 +4002,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -4256,7 +4376,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
"wasm-bindgen-shared",
]
@@ -4290,7 +4410,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4548,6 +4668,12 @@ dependencies = [
"rustix",
]
[[package]]
name = "widestring"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -4579,15 +4705,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.51.1"
@@ -4756,10 +4873,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b83b0eca06dd125dbcd48a45327c708a6da8aada3d95a3f06db0ce4b17e0d4"
dependencies = [
"c2rust-bitfields",
"libloading 0.8.0",
"libloading 0.8.1",
"log",
"thiserror",
"windows 0.51.1",
"windows",
]
[[package]]
@@ -4837,5 +4954,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]

View File

@@ -22,6 +22,7 @@ backoff = { version = "0.4", features = ["tokio"] }
tracing = { version = "0.1.39" }
tracing-subscriber = { version = "0.3.17", features = ["parking_lot"] }
secrecy = "0.8"
hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
connlib-client-android = { path = "connlib/clients/android"}
connlib-client-apple = { path = "connlib/clients/apple"}

View File

@@ -6,13 +6,13 @@
use connlib_client_shared::{file_logger, Callbacks, Error, ResourceDescription, Session};
use ip_network::IpNetwork;
use jni::{
objects::{GlobalRef, JClass, JObject, JString, JValue},
objects::{GlobalRef, JByteArray, JClass, JObject, JObjectArray, JString, JValue, JValueGen},
strings::JNIString,
JNIEnv, JavaVM,
};
use secrecy::SecretString;
use std::path::Path;
use std::sync::OnceLock;
use std::{net::IpAddr, path::Path};
use std::{
net::{Ipv4Addr, Ipv6Addr},
os::fd::RawFd,
@@ -305,6 +305,42 @@ impl Callbacks for CallbackHandler {
None
})
}
fn get_system_default_resolvers(&self) -> Result<Option<Vec<IpAddr>>, Self::Error> {
self.env(|mut env| {
let name = "getSystemDefaultResolvers";
let addrs = env
.call_method(&self.callback_handler, name, "()[[B", &[])
.and_then(JValueGen::l)
.and_then(|arr| convert_byte_array_array(&mut env, arr.into()))
.map_err(|source| CallbackError::CallMethodFailed { name, source })?;
Ok(Some(addrs.iter().filter_map(|v| to_ip(v)).collect()))
})
}
}
fn to_ip(val: &[u8]) -> Option<IpAddr> {
let addr: Option<[u8; 4]> = val.try_into().ok();
if let Some(addr) = addr {
return Some(addr.into());
}
let addr: [u8; 16] = val.try_into().ok()?;
Some(addr.into())
}
fn convert_byte_array_array(
env: &mut JNIEnv,
array: JObjectArray,
) -> jni::errors::Result<Vec<Vec<u8>>> {
let len = env.get_array_length(&array)?;
let mut result = Vec::with_capacity(len as usize);
for i in 0..len {
let arr: JByteArray<'_> = env.get_object_array_element(&array, i)?.into();
result.push(env.convert_byte_array(arr)?);
}
Ok(result)
}
fn throw(env: &mut JNIEnv, class: &str, msg: impl Into<JNIString>) {

View File

@@ -27,6 +27,8 @@ time = { version = "0.3.30", features = ["formatting"] }
reqwest = { version = "0.11.22", default-features = false, features = ["stream", "rustls-tls"] }
tokio-tungstenite = { version = "0.20", default-features = false, features = ["connect", "handshake", "rustls-tls-webpki-roots"] }
async-compression = { version = "0.4.3", features = ["tokio", "gzip"] }
hickory-resolver = { workspace = true, features = ["tokio-runtime"] }
parking_lot = "0.12"
[target.'cfg(target_os = "android")'.dependencies]
tracing = { workspace = true, features = ["std", "attributes"] }

View File

@@ -1,4 +1,5 @@
use async_compression::tokio::bufread::GzipEncoder;
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::{io, sync::Arc};
@@ -15,16 +16,52 @@ use connlib_shared::{
};
use firezone_tunnel::{ClientState, Request, Tunnel};
use hickory_resolver::config::{NameServerConfig, Protocol, ResolverConfig};
use hickory_resolver::TokioAsyncResolver;
use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE};
use tokio::io::BufReader;
use tokio::sync::Mutex;
use tokio_util::codec::{BytesCodec, FramedRead};
use url::Url;
const DNS_PORT: u16 = 53;
pub struct ControlPlane<CB: Callbacks> {
pub tunnel: Arc<Tunnel<CB, ClientState>>,
pub phoenix_channel: PhoenixSenderWithTopic,
pub tunnel_init: Mutex<bool>,
// It's a Mutex<Option<_>> because we need the init message to initialize the resolver
// also, in platforms with split DNS and no configured upstream dns this will be None.
//
// We could still initialize the resolver with no nameservers in those platforms...
pub fallback_resolver: parking_lot::Mutex<Option<TokioAsyncResolver>>,
}
fn create_resolver(
upstream_dns: Vec<IpAddr>,
callbacks: &impl Callbacks,
) -> Option<TokioAsyncResolver> {
let dns_servers = if upstream_dns.is_empty() {
let Ok(Some(dns_servers)) = callbacks.get_system_default_resolvers() else {
return None;
};
if dns_servers.is_empty() {
return None;
}
dns_servers
} else {
upstream_dns
};
let mut resolver_config = ResolverConfig::new();
for ip in dns_servers.iter() {
let name_server = NameServerConfig::new(SocketAddr::new(*ip, DNS_PORT), Protocol::Udp);
resolver_config.add_name_server(name_server);
}
Some(TokioAsyncResolver::tokio(
resolver_config,
Default::default(),
))
}
impl<CB: Callbacks + 'static> ControlPlane<CB> {
@@ -44,6 +81,8 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
return Err(e);
} else {
*init = true;
*self.fallback_resolver.lock() =
create_resolver(interface.upstream_dns, self.tunnel.callbacks());
tracing::info!("Firezoned Started!");
}
} else {
@@ -285,6 +324,22 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
// TODO: Clean up connection in `ClientState` here?
}
}
firezone_tunnel::Event::DnsQuery(query) => {
// Until we handle it better on a gateway-like eventloop, making sure not to block the loop
let Some(resolver) = self.fallback_resolver.lock().clone() else {
return;
};
let tunnel = self.tunnel.clone();
tokio::spawn(async move {
let response = resolver.lookup(query.name, query.record_type).await;
if let Err(err) = tunnel
.write_dns_lookup_response(response, query.query)
.await
{
tracing::error!(err = ?err, "DNS lookup failed: {err:#}");
}
});
}
}
}
}

View File

@@ -158,12 +158,12 @@ where
tunnel: Arc::new(tunnel),
phoenix_channel: connection.sender_with_topic("client".to_owned()),
tunnel_init: Mutex::new(false),
fallback_resolver: parking_lot::Mutex::new(None),
};
tokio::spawn(async move {
let mut log_stats_interval = tokio::time::interval(Duration::from_secs(10));
let mut upload_logs_interval = upload_interval();
loop {
tokio::select! {
Some((msg, reference)) = control_plane_receiver.recv() => {

View File

@@ -32,6 +32,7 @@ url = { version = "2.4.1", default-features = false }
uuid = { version = "1.5", default-features = false, features = ["std", "v4", "serde"] }
webrtc = { version = "0.8" }
ring = "0.17"
hickory-resolver = { workspace = true }
# Needed for Android logging until tracing is working
log = "0.4"

View File

@@ -2,7 +2,7 @@ use crate::messages::ResourceDescription;
use ip_network::IpNetwork;
use std::error::Error;
use std::fmt::{Debug, Display};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::PathBuf;
// Avoids having to map types for Windows
@@ -71,6 +71,11 @@ pub trait Callbacks: Clone + Send + Sync {
Ok(())
}
/// Returns the system's default resolver iff split dns isn't available for platform
fn get_system_default_resolvers(&self) -> Result<Option<Vec<IpAddr>>, Self::Error> {
Ok(None)
}
fn roll_log_file(&self) -> Option<PathBuf> {
None
}

View File

@@ -1,7 +1,7 @@
use crate::messages::ResourceDescription;
use crate::{Callbacks, Error, Result};
use ip_network::IpNetwork;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
// Avoids having to map types for Windows
type RawFd = i32;
@@ -93,4 +93,12 @@ impl<CB: Callbacks> Callbacks for CallbackErrorFacade<CB> {
// There's nothing we really want to do if `on_error` fails.
Ok(())
}
fn get_system_default_resolvers(
&self,
) -> std::result::Result<Option<Vec<IpAddr>>, Self::Error> {
self.0
.get_system_default_resolvers()
.map_err(|err| Error::GetSystemDefaultResolverFailed(err.to_string()))
}
}

View File

@@ -79,6 +79,8 @@ pub enum ConnlibError {
OnRemoveRouteFailed(String),
#[error("`on_update_resources` failed: {0}")]
OnUpdateResourcesFailed(String),
#[error("`get_system_default_resolvers` failed: {0}")]
GetSystemDefaultResolverFailed(String),
/// Glob for errors without a type.
#[error("Other error: {0}")]
Other(&'static str),
@@ -126,6 +128,13 @@ pub enum ConnlibError {
/// Any parse error
#[error("parse error")]
ParseError,
/// DNS lookup error
#[error("Error with the DNS fallback lookup")]
DNSFallback(#[from] hickory_resolver::error::ResolveError),
#[error("Error with the DNS fallback lookup")]
DNSFallbackKind(#[from] hickory_resolver::error::ResolveErrorKind),
#[error("DNS proto error")]
DnsProtoError(#[from] hickory_resolver::proto::error::ProtoError),
}
impl ConnlibError {

View File

@@ -26,6 +26,7 @@ boringtun = { workspace = true }
chrono = { workspace = true }
pnet_packet = { version = "0.34" }
futures-bounded = { git = "https://github.com/libp2p/rust-libp2p", branch = "feat/stream-map" }
hickory-resolver = { workspace = true }
# TODO: research replacing for https://github.com/algesten/str0m
webrtc = { version = "0.8" }

View File

@@ -0,0 +1,59 @@
use core::fmt;
use std::{
collections::VecDeque,
task::{Context, Poll, Waker},
};
// Simple bounded queue for one-time events
#[derive(Debug, Clone)]
pub(crate) struct BoundedQueue<T> {
queue: VecDeque<T>,
limit: usize,
waker: Option<Waker>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Full;
impl fmt::Display for Full {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Queue is full")
}
}
impl<T> BoundedQueue<T> {
pub(crate) fn with_capacity(cap: usize) -> BoundedQueue<T> {
BoundedQueue {
queue: VecDeque::with_capacity(cap),
limit: cap,
waker: None,
}
}
pub(crate) fn poll(&mut self, cx: &Context) -> Poll<T> {
if let Some(front) = self.queue.pop_front() {
return Poll::Ready(front);
}
self.waker = Some(cx.waker().clone());
Poll::Pending
}
fn at_capacity(&self) -> bool {
self.queue.len() == self.limit
}
pub(crate) fn push_back(&mut self, x: T) -> Result<(), Full> {
if self.at_capacity() {
return Err(Full);
}
self.queue.push_back(x);
if let Some(ref waker) = self.waker {
waker.wake_by_ref();
}
Ok(())
}
}

View File

@@ -1,9 +1,12 @@
use crate::bounded_queue::BoundedQueue;
use crate::device_channel::{create_iface, DeviceIo};
use crate::ip_packet::IpPacket;
use crate::peer::Peer;
use crate::resource_table::ResourceTable;
use crate::{
dns, peer_by_ip, tokio_util, Device, Event, PeerConfig, RoleState, Tunnel,
ICE_GATHERING_TIMEOUT_SECONDS, MAX_CONCURRENT_ICE_GATHERING, MAX_UDP_SIZE,
dns, peer_by_ip, tokio_util, Device, DnsQuery, Event, PeerConfig, RoleState, Tunnel,
DNS_QUERIES_QUEUE_SIZE, ICE_GATHERING_TIMEOUT_SECONDS, MAX_CONCURRENT_ICE_GATHERING,
MAX_UDP_SIZE,
};
use boringtun::x25519::{PublicKey, StaticSecret};
use connlib_shared::error::{ConnlibError as Error, ConnlibError};
@@ -15,6 +18,7 @@ use connlib_shared::{Callbacks, DNS_SENTINEL};
use futures::channel::mpsc::Receiver;
use futures::stream;
use futures_bounded::{PushError, StreamMap};
use hickory_resolver::lookup::Lookup;
use ip_network::IpNetwork;
use ip_network_table::IpNetworkTable;
use std::collections::hash_map::Entry;
@@ -65,6 +69,46 @@ where
Ok(())
}
/// Writes the response to a DNS lookup
#[tracing::instrument(level = "trace", skip(self))]
pub async fn write_dns_lookup_response(
self: &Arc<Self>,
response: hickory_resolver::error::ResolveResult<Lookup>,
query: IpPacket<'static>,
) -> connlib_shared::Result<()> {
let Some(mut message) = dns::as_dns_message(&query) else {
debug_assert!(false, "The original message should be a DNS query for us to ever call write_dns_lookup_response");
return Ok(());
};
let response = match response.map_err(|err| err.kind().clone()) {
Ok(response) => message.add_answers(response.records().to_vec()),
Err(hickory_resolver::error::ResolveErrorKind::NoRecordsFound {
soa,
response_code,
..
}) => {
if let Some(soa) = soa {
message.add_name_server(soa.clone().into_record_of_rdata());
}
message.set_response_code(response_code)
}
Err(e) => {
return Err(e.into());
}
};
if let Some(pkt) = dns::build_response(query, response.to_vec()?) {
let Some(ref device) = *self.device.read().await else {
return Ok(());
};
send_dns_packet(&device.io, pkt)?;
}
Ok(())
}
/// Sets the interface configuration and starts background tasks.
#[tracing::instrument(level = "trace", skip(self))]
pub async fn set_interface(
@@ -133,15 +177,20 @@ where
return Ok(());
};
if let Some(dns_packet) =
dns::parse(&tunnel.role_state.lock().resources, packet.as_immutable())
{
if let Err(e) = send_dns_packet(&device_writer, dns_packet) {
tracing::error!(err = %e, "failed to send DNS packet");
let _ = tunnel.callbacks.on_error(&e.into());
}
match dns::parse(&tunnel.role_state.lock().resources, packet.as_immutable()) {
Some(dns::ResolveStrategy::LocalResponse(pkt)) => {
if let Err(e) = send_dns_packet(&device_writer, pkt) {
tracing::error!(err = %e, "failed to send DNS packet");
let _ = tunnel.callbacks.on_error(&e.into());
}
continue;
continue;
}
Some(dns::ResolveStrategy::ForwardQuery(query)) => {
tunnel.role_state.lock().dns_query(query);
continue;
}
None => {}
}
let dest = packet.destination();
@@ -166,10 +215,13 @@ where
fn send_dns_packet(device_writer: &DeviceIo, packet: dns::Packet) -> io::Result<()> {
match packet {
dns::Packet::Ipv4(r) => device_writer.write4(&r[..])?,
dns::Packet::Ipv6(r) => device_writer.write6(&r[..])?,
};
dns::Packet::Ipv4(r) => {
device_writer.write4(&r[..])?;
}
dns::Packet::Ipv6(r) => {
device_writer.write6(&r[..])?;
}
}
Ok(())
}
@@ -188,6 +240,7 @@ pub struct ClientState {
pub gateway_public_keys: HashMap<GatewayId, PublicKey>,
resources_gateways: HashMap<ResourceId, GatewayId>,
resources: ResourceTable<ResourceDescription>,
dns_queries: BoundedQueue<DnsQuery<'static>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -416,6 +469,12 @@ impl ClientState {
IpAddr::V6(ipv6) => self.resources.get_by_ip(ipv6),
}
}
pub fn dns_query(&mut self, query: DnsQuery) {
if self.dns_queries.push_back(query.into_owned()).is_err() {
tracing::warn!("Too many DNS queries, dropping new ones");
}
}
}
impl Default for ClientState {
@@ -432,6 +491,7 @@ impl Default for ClientState {
gateway_public_keys: Default::default(),
resources_gateways: Default::default(),
resources: Default::default(),
dns_queries: BoundedQueue::with_capacity(DNS_QUERIES_QUEUE_SIZE),
}
}
}
@@ -449,7 +509,8 @@ impl RoleState for ClientState {
})
}
Poll::Ready((id, Some(Err(e)))) => {
tracing::warn!(gateway_id = %id, "ICE gathering timed out: {e}")
tracing::warn!(gateway_id = %id, "ICE gathering timed out: {e}");
continue;
}
Poll::Ready((_, None)) => continue,
Poll::Pending => {}
@@ -494,7 +555,7 @@ impl RoleState for ClientState {
Poll::Pending => {}
}
return Poll::Pending;
return self.dns_queries.poll(cx).map(Event::DnsQuery);
}
}
}

View File

@@ -1,12 +1,16 @@
use crate::ip_packet::{to_dns, IpPacket, MutableIpPacket, Version};
use crate::resource_table::ResourceTable;
use crate::DnsQuery;
use connlib_shared::{messages::ResourceDescription, DNS_SENTINEL};
use domain::base::{
iana::{Class, Rcode, Rtype},
Dname, Message, MessageBuilder, ParsedDname, ToDname,
Dname, Message, MessageBuilder, ParsedDname, Question, ToDname,
};
use hickory_resolver::proto::op::Message as TrustDnsMessage;
use hickory_resolver::proto::rr::RecordType;
use itertools::Itertools;
use pnet_packet::{udp::MutableUdpPacket, MutablePacket, Packet as UdpPacket, PacketSize};
use std::net::IpAddr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
const DNS_TTL: u32 = 300;
const UDP_HEADER_SIZE: usize = 8;
@@ -20,16 +24,45 @@ pub(crate) enum Packet {
Ipv6(Vec<u8>),
}
#[derive(Debug)]
pub(crate) enum ResolveStrategy<T, U> {
LocalResponse(T),
ForwardQuery(U),
}
struct DnsQueryParams {
name: String,
record_type: RecordType,
}
impl DnsQueryParams {
fn into_query(self, query: IpPacket) -> DnsQuery {
DnsQuery {
name: self.name,
record_type: self.record_type,
query,
}
}
}
impl<T> ResolveStrategy<T, DnsQueryParams> {
fn new(name: String, record_type: Rtype) -> ResolveStrategy<T, DnsQueryParams> {
ResolveStrategy::ForwardQuery(DnsQueryParams {
name,
record_type: u16::from(record_type).into(),
})
}
}
// We don't need to support multiple questions/qname in a single query because
// nobody does it and since this run with each packet we want to squeeze as much optimization
// as we can therefore we won't do it.
//
// See: https://stackoverflow.com/a/55093896
pub(crate) fn parse(
pub(crate) fn parse<'a>(
resources: &ResourceTable<ResourceDescription>,
packet: IpPacket<'_>,
) -> Option<Packet> {
let version = packet.version();
packet: IpPacket<'a>,
) -> Option<ResolveStrategy<Packet, DnsQuery<'a>>> {
if packet.destination() != IpAddr::from(DNS_SENTINEL) {
return None;
}
@@ -39,58 +72,23 @@ pub(crate) fn parse(
return None;
}
let question = message.first_question()?;
let resource = match question.qtype() {
Rtype::A | Rtype::Aaaa => resources
.get_by_name(&ToDname::to_cow(question.qname()).to_string())
.cloned(),
Rtype::Ptr => {
let dns_parts = ToDname::to_cow(question.qname()).to_string();
let mut dns_parts = dns_parts.split('.').rev();
if !dns_parts
.next()
.is_some_and(|d| d == REVERSE_DNS_ADDRESS_END)
{
return None;
}
let ip: IpAddr = match dns_parts.next() {
Some(REVERSE_DNS_ADDRESS_V4) => {
let mut ip = [0u8; 4];
for i in ip.iter_mut() {
*i = dns_parts.next()?.parse().ok()?;
}
ip.into()
}
Some(REVERSE_DNS_ADDRESS_V6) => {
let mut ip = [0u8; 16];
for i in ip.iter_mut() {
*i = u8::from_str_radix(
&format!("{}{}", dns_parts.next()?, dns_parts.next()?),
16,
)
.ok()?;
}
ip.into()
}
_ => return None,
};
if dns_parts.next().is_some() {
return None;
}
resources.get_by_ip(ip).cloned()
let resource = match resource_from_question(resources, &question)? {
ResolveStrategy::LocalResponse(resource) => resource,
ResolveStrategy::ForwardQuery(params) => {
return Some(ResolveStrategy::ForwardQuery(params.into_query(packet)))
}
_ => return None,
};
let response = build_dns_with_answer(message, question.qname(), question.qtype(), &resource?)?;
let response = build_response(packet, response);
response.map(|pkt| match version {
Version::Ipv4 => Packet::Ipv4(pkt),
Version::Ipv6 => Packet::Ipv6(pkt),
})
let response = build_dns_with_answer(message, question.qname(), question.qtype(), &resource)?;
Some(ResolveStrategy::LocalResponse(build_response(
packet, response,
)?))
}
fn build_response(original_pkt: IpPacket<'_>, mut dns_answer: Vec<u8>) -> Option<Vec<u8>> {
pub(crate) fn build_response(
original_pkt: IpPacket<'_>,
mut dns_answer: Vec<u8>,
) -> Option<Packet> {
let version = original_pkt.version();
let response_len = dns_answer.len();
let original_dgm = original_pkt.as_udp()?;
let hdr_len = original_pkt.packet_size() - original_dgm.payload().len();
@@ -113,7 +111,10 @@ fn build_response(original_pkt: IpPacket<'_>, mut dns_answer: Vec<u8>) -> Option
let udp_checksum = pkt.to_immutable().udp_checksum(&pkt.as_immutable_udp()?);
pkt.as_udp()?.set_checksum(udp_checksum);
pkt.set_ipv4_checksum();
Some(res_buf)
match version {
Version::Ipv4 => Some(Packet::Ipv4(res_buf)),
Version::Ipv6 => Some(Packet::Ipv6(res_buf)),
}
}
fn build_dns_with_answer<N>(
@@ -161,3 +162,123 @@ where
}
Some(answer_builder.finish())
}
fn resource_from_question<N: ToDname>(
resources: &ResourceTable<ResourceDescription>,
question: &Question<N>,
) -> Option<ResolveStrategy<ResourceDescription, DnsQueryParams>> {
let name = ToDname::to_cow(question.qname()).to_string();
let qtype = question.qtype();
let resource = match qtype {
Rtype::A | Rtype::Aaaa => resources.get_by_name(&name),
Rtype::Ptr => {
let ip = reverse_dns_addr(&name)?;
resources.get_by_ip(ip)
}
_ => return None,
};
resource
.cloned()
.map(ResolveStrategy::LocalResponse)
.unwrap_or(ResolveStrategy::new(name, qtype))
.into()
}
pub(crate) fn as_dns_message(pkt: &IpPacket) -> Option<TrustDnsMessage> {
let datagram = pkt.as_udp()?;
TrustDnsMessage::from_vec(datagram.payload()).ok()
}
fn reverse_dns_addr(name: &str) -> Option<IpAddr> {
let mut dns_parts = name.split('.').rev();
if dns_parts.next()? != REVERSE_DNS_ADDRESS_END {
return None;
}
let ip: IpAddr = match dns_parts.next()? {
REVERSE_DNS_ADDRESS_V4 => reverse_dns_addr_v4(&mut dns_parts)?.into(),
REVERSE_DNS_ADDRESS_V6 => reverse_dns_addr_v6(&mut dns_parts)?.into(),
_ => return None,
};
if dns_parts.next().is_some() {
return None;
}
Some(ip)
}
fn reverse_dns_addr_v4<'a>(dns_parts: &mut impl Iterator<Item = &'a str>) -> Option<Ipv4Addr> {
dns_parts.join(".").parse().ok()
}
fn reverse_dns_addr_v6<'a>(dns_parts: &mut impl Iterator<Item = &'a str>) -> Option<Ipv6Addr> {
dns_parts
.chunks(4)
.into_iter()
.map(|mut s| s.join(""))
.join(":")
.parse()
.ok()
}
#[cfg(test)]
mod test {
use super::reverse_dns_addr;
use std::net::Ipv4Addr;
#[test]
fn reverse_dns_addr_works_v4() {
assert_eq!(
reverse_dns_addr("1.2.3.4.in-addr.arpa"),
Some(Ipv4Addr::new(4, 3, 2, 1).into())
);
}
#[test]
fn reverse_dns_v4_addr_extra_number() {
assert_eq!(reverse_dns_addr("0.1.2.3.4.in-addr.arpa"), None);
}
#[test]
fn reverse_dns_addr_wrong_ending() {
assert_eq!(reverse_dns_addr("1.2.3.4.in-addr.carpa"), None);
}
#[test]
fn reverse_dns_v4_addr_with_ip6_ending() {
assert_eq!(reverse_dns_addr("1.2.3.4.ip6.arpa"), None);
}
#[test]
fn reverse_dns_addr_v6() {
assert_eq!(
reverse_dns_addr(
"b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"
),
Some("2001:db8::567:89ab".parse().unwrap())
);
}
#[test]
fn reverse_dns_addr_v6_extra_number() {
assert_eq!(
reverse_dns_addr(
"0.b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"
),
None
);
}
#[test]
fn reverse_dns_addr_v6_ipv4_ending() {
assert_eq!(
reverse_dns_addr(
"b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.in-addr.arpa"
),
None
);
}
}

View File

@@ -185,12 +185,22 @@ pub(crate) enum Version {
}
#[derive(Debug, PartialEq)]
pub(crate) enum IpPacket<'a> {
pub enum IpPacket<'a> {
Ipv4Packet(Ipv4Packet<'a>),
Ipv6Packet(Ipv6Packet<'a>),
}
impl<'a> IpPacket<'a> {
pub(crate) fn owned(data: Vec<u8>) -> Option<IpPacket<'static>> {
let packet = match data[0] >> 4 {
4 => Ipv4Packet::owned(data)?.into(),
6 => Ipv6Packet::owned(data)?.into(),
_ => return None,
};
Some(packet)
}
pub(crate) fn version(&self) -> Version {
match self {
IpPacket::Ipv4Packet(_) => Version::Ipv4,

View File

@@ -11,8 +11,11 @@ use bytes::Bytes;
use connlib_shared::{messages::Key, CallbackErrorFacade, Callbacks, Error};
use ip_network::IpNetwork;
use ip_network_table::IpNetworkTable;
use ip_packet::IpPacket;
use pnet_packet::Packet;
use serde::{Deserialize, Serialize};
use hickory_resolver::proto::rr::RecordType;
use itertools::Itertools;
use parking_lot::{Mutex, RwLock};
use peer::{Peer, PeerStats};
@@ -47,6 +50,7 @@ use crate::ip_packet::MutableIpPacket;
use connlib_shared::messages::SecretKey;
use index::IndexLfsr;
mod bounded_queue;
mod client;
mod control_protocol;
mod device_channel;
@@ -65,6 +69,8 @@ const MAX_UDP_SIZE: usize = (1 << 16) - 1;
const RESET_PACKET_COUNT_INTERVAL: Duration = Duration::from_secs(1);
const REFRESH_PEERS_TIMERS_INTERVAL: Duration = Duration::from_secs(1);
const REFRESH_MTU_INTERVAL: Duration = Duration::from_secs(30);
const DNS_QUERIES_QUEUE_SIZE: usize = 100;
/// For how long we will attempt to gather ICE candidates before aborting.
///
/// Chosen arbitrarily.
@@ -226,6 +232,34 @@ pub(crate) fn peer_by_ip(peers_by_ip: &IpNetworkTable<Arc<Peer>>, ip: IpAddr) ->
peers_by_ip.longest_match(ip).map(|(_, peer)| peer).cloned()
}
#[derive(Debug)]
pub struct DnsQuery<'a> {
pub name: String,
pub record_type: RecordType,
// We could be much more efficient with this field,
// we only need the header to create the response.
pub query: IpPacket<'a>,
}
impl<'a> DnsQuery<'a> {
pub(crate) fn into_owned(self) -> DnsQuery<'static> {
let Self {
name,
record_type,
query,
} = self;
let buf = query.packet().to_vec();
let query =
IpPacket::owned(buf).expect("We are constructing the ip packet from an ip packet");
DnsQuery {
name,
record_type,
query,
}
}
}
pub enum Event<TId> {
SignalIceCandidate {
conn_id: TId,
@@ -236,6 +270,7 @@ pub enum Event<TId> {
connected_gateway_ids: Vec<GatewayId>,
reference: usize,
},
DnsQuery(DnsQuery<'static>),
}
impl<CB, TRoleState> Tunnel<CB, TRoleState>

View File

@@ -192,6 +192,7 @@ impl Eventloop {
Poll::Ready(Event::ConnectionIntent { .. }) => {
unreachable!("Not used on the gateway, split the events!")
}
Poll::Ready(_) => continue,
Poll::Pending => {}
}