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 => {} }