From d626f6dbf696eb23f081dd085b41ae23b56cc9be Mon Sep 17 00:00:00 2001 From: Gabi Date: Wed, 18 Oct 2023 17:39:20 -0300 Subject: [PATCH] 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`. --- .../firezone/android/tunnel/TunnelService.kt | 4 +- .../tunnel/callback/ConnlibCallback.kt | 2 +- .../android/tunnel/util/DnsServersDetector.kt | 80 ++--- rust/Cargo.lock | 313 ++++++++++++------ rust/Cargo.toml | 1 + rust/connlib/clients/android/src/lib.rs | 40 ++- rust/connlib/clients/shared/Cargo.toml | 2 + rust/connlib/clients/shared/src/control.rs | 55 +++ rust/connlib/clients/shared/src/lib.rs | 2 +- rust/connlib/shared/Cargo.toml | 1 + rust/connlib/shared/src/callbacks.rs | 7 +- .../shared/src/callbacks_error_facade.rs | 10 +- rust/connlib/shared/src/error.rs | 9 + rust/connlib/tunnel/Cargo.toml | 1 + rust/connlib/tunnel/src/bounded_queue.rs | 59 ++++ rust/connlib/tunnel/src/client.rs | 93 +++++- rust/connlib/tunnel/src/dns.rs | 231 ++++++++++--- rust/connlib/tunnel/src/ip_packet.rs | 12 +- rust/connlib/tunnel/src/lib.rs | 35 ++ rust/gateway/src/eventloop.rs | 1 + 20 files changed, 722 insertions(+), 236 deletions(-) create mode 100644 rust/connlib/tunnel/src/bounded_queue.rs diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt index f30819acb..8c95c549a 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt @@ -124,8 +124,8 @@ class TunnelService : VpnService() { return fd } - override fun getSystemDefaultResolvers(): String { - return moshi.adapter>().toJson(DnsServersDetector(this@TunnelService).servers) + override fun getSystemDefaultResolvers(): Array { + return DnsServersDetector(this@TunnelService).servers.map { it.address }.toTypedArray() } override fun onDisconnect(error: String?): Boolean { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/callback/ConnlibCallback.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/callback/ConnlibCallback.kt index e6e9d250a..f6561e910 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/callback/ConnlibCallback.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/callback/ConnlibCallback.kt @@ -27,5 +27,5 @@ interface ConnlibCallback { fun onError(error: String): Boolean - fun getSystemDefaultResolvers(): String + fun getSystemDefaultResolvers(): Array } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/util/DnsServersDetector.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/util/DnsServersDetector.kt index 775e8d3d6..c08dba0ea 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/util/DnsServersDetector.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/util/DnsServersDetector.kt @@ -40,36 +40,23 @@ class DnsServersDetector( ) { //region - public ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////////////////////// - val servers: Array + val servers: Set /** * 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? + private val serversMethodConnectivityManager: Set? /** * 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() - val serversArrayList = ArrayList() + val priorityServers: MutableSet = HashSet(10) + val servers: MutableSet = 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? + private val serversMethodExec: Set? /** * 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 { + private fun methodExecParseProps(lineNumberReader: BufferedReader): Set { var line: String - val serversSet: MutableSet = HashSet(10) + val serversSet: MutableSet = 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"), ) /** diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 174f9af8d..8c1ab4fc2 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -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", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index bed47c39e..2cc39344f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -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"} diff --git a/rust/connlib/clients/android/src/lib.rs b/rust/connlib/clients/android/src/lib.rs index 0bfea0816..23a1447ba 100644 --- a/rust/connlib/clients/android/src/lib.rs +++ b/rust/connlib/clients/android/src/lib.rs @@ -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>, 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 { + 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>> { + 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) { diff --git a/rust/connlib/clients/shared/Cargo.toml b/rust/connlib/clients/shared/Cargo.toml index 2c7a162fb..790afaae5 100644 --- a/rust/connlib/clients/shared/Cargo.toml +++ b/rust/connlib/clients/shared/Cargo.toml @@ -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"] } diff --git a/rust/connlib/clients/shared/src/control.rs b/rust/connlib/clients/shared/src/control.rs index 04c6ca81c..daafcc47b 100644 --- a/rust/connlib/clients/shared/src/control.rs +++ b/rust/connlib/clients/shared/src/control.rs @@ -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 { pub tunnel: Arc>, pub phoenix_channel: PhoenixSenderWithTopic, pub tunnel_init: Mutex, + // It's a Mutex> 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>, +} + +fn create_resolver( + upstream_dns: Vec, + callbacks: &impl Callbacks, +) -> Option { + 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 ControlPlane { @@ -44,6 +81,8 @@ impl ControlPlane { 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 ControlPlane { // 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:#}"); + } + }); + } } } } diff --git a/rust/connlib/clients/shared/src/lib.rs b/rust/connlib/clients/shared/src/lib.rs index eda2ff2b7..9c93a096b 100644 --- a/rust/connlib/clients/shared/src/lib.rs +++ b/rust/connlib/clients/shared/src/lib.rs @@ -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() => { diff --git a/rust/connlib/shared/Cargo.toml b/rust/connlib/shared/Cargo.toml index 25f3e2c56..d9f74b203 100644 --- a/rust/connlib/shared/Cargo.toml +++ b/rust/connlib/shared/Cargo.toml @@ -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" diff --git a/rust/connlib/shared/src/callbacks.rs b/rust/connlib/shared/src/callbacks.rs index dafda6c5c..adbbc7747 100644 --- a/rust/connlib/shared/src/callbacks.rs +++ b/rust/connlib/shared/src/callbacks.rs @@ -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>, Self::Error> { + Ok(None) + } + fn roll_log_file(&self) -> Option { None } diff --git a/rust/connlib/shared/src/callbacks_error_facade.rs b/rust/connlib/shared/src/callbacks_error_facade.rs index 45cddc4ee..75a94163c 100644 --- a/rust/connlib/shared/src/callbacks_error_facade.rs +++ b/rust/connlib/shared/src/callbacks_error_facade.rs @@ -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 Callbacks for CallbackErrorFacade { // There's nothing we really want to do if `on_error` fails. Ok(()) } + + fn get_system_default_resolvers( + &self, + ) -> std::result::Result>, Self::Error> { + self.0 + .get_system_default_resolvers() + .map_err(|err| Error::GetSystemDefaultResolverFailed(err.to_string())) + } } diff --git a/rust/connlib/shared/src/error.rs b/rust/connlib/shared/src/error.rs index d0a8ab9ec..78e4956f8 100644 --- a/rust/connlib/shared/src/error.rs +++ b/rust/connlib/shared/src/error.rs @@ -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 { diff --git a/rust/connlib/tunnel/Cargo.toml b/rust/connlib/tunnel/Cargo.toml index c034eafc8..12ea59d29 100644 --- a/rust/connlib/tunnel/Cargo.toml +++ b/rust/connlib/tunnel/Cargo.toml @@ -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" } diff --git a/rust/connlib/tunnel/src/bounded_queue.rs b/rust/connlib/tunnel/src/bounded_queue.rs new file mode 100644 index 000000000..898440fd3 --- /dev/null +++ b/rust/connlib/tunnel/src/bounded_queue.rs @@ -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 { + queue: VecDeque, + limit: usize, + waker: Option, +} + +#[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 BoundedQueue { + pub(crate) fn with_capacity(cap: usize) -> BoundedQueue { + BoundedQueue { + queue: VecDeque::with_capacity(cap), + limit: cap, + waker: None, + } + } + + pub(crate) fn poll(&mut self, cx: &Context) -> Poll { + 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(()) + } +} diff --git a/rust/connlib/tunnel/src/client.rs b/rust/connlib/tunnel/src/client.rs index 5e2b69352..cab9b6b4c 100644 --- a/rust/connlib/tunnel/src/client.rs +++ b/rust/connlib/tunnel/src/client.rs @@ -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, + response: hickory_resolver::error::ResolveResult, + 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, resources_gateways: HashMap, resources: ResourceTable, + dns_queries: BoundedQueue>, } #[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); } } } diff --git a/rust/connlib/tunnel/src/dns.rs b/rust/connlib/tunnel/src/dns.rs index 41783c564..d94c18d68 100644 --- a/rust/connlib/tunnel/src/dns.rs +++ b/rust/connlib/tunnel/src/dns.rs @@ -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), } +#[derive(Debug)] +pub(crate) enum ResolveStrategy { + 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 ResolveStrategy { + fn new(name: String, record_type: Rtype) -> ResolveStrategy { + 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, - packet: IpPacket<'_>, -) -> Option { - let version = packet.version(); + packet: IpPacket<'a>, +) -> Option>> { 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) -> Option> { +pub(crate) fn build_response( + original_pkt: IpPacket<'_>, + mut dns_answer: Vec, +) -> Option { + 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) -> 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( @@ -161,3 +162,123 @@ where } Some(answer_builder.finish()) } + +fn resource_from_question( + resources: &ResourceTable, + question: &Question, +) -> Option> { + 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 { + let datagram = pkt.as_udp()?; + TrustDnsMessage::from_vec(datagram.payload()).ok() +} + +fn reverse_dns_addr(name: &str) -> Option { + 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) -> Option { + dns_parts.join(".").parse().ok() +} + +fn reverse_dns_addr_v6<'a>(dns_parts: &mut impl Iterator) -> Option { + 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 + ); + } +} diff --git a/rust/connlib/tunnel/src/ip_packet.rs b/rust/connlib/tunnel/src/ip_packet.rs index 74c552238..48f1cced5 100644 --- a/rust/connlib/tunnel/src/ip_packet.rs +++ b/rust/connlib/tunnel/src/ip_packet.rs @@ -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) -> Option> { + 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, diff --git a/rust/connlib/tunnel/src/lib.rs b/rust/connlib/tunnel/src/lib.rs index 7c5bcb443..646e7845a 100644 --- a/rust/connlib/tunnel/src/lib.rs +++ b/rust/connlib/tunnel/src/lib.rs @@ -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>, 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 { SignalIceCandidate { conn_id: TId, @@ -236,6 +270,7 @@ pub enum Event { connected_gateway_ids: Vec, reference: usize, }, + DnsQuery(DnsQuery<'static>), } impl Tunnel diff --git a/rust/gateway/src/eventloop.rs b/rust/gateway/src/eventloop.rs index abccc1677..307140b01 100644 --- a/rust/gateway/src/eventloop.rs +++ b/rust/gateway/src/eventloop.rs @@ -192,6 +192,7 @@ impl Eventloop { Poll::Ready(Event::ConnectionIntent { .. }) => { unreachable!("Not used on the gateway, split the events!") } + Poll::Ready(_) => continue, Poll::Pending => {} }