From 8f400dda8507d8233fb4fa81663517123e3b5367 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 20 Jul 2024 12:59:51 +0000 Subject: [PATCH] feat: use custom DNS resolver for HTTP(S) --- Cargo.lock | 153 ++++++++++++++++++++++++++++++++++-------------- Cargo.toml | 2 +- deny.toml | 2 +- src/net.rs | 2 +- src/net/http.rs | 62 ++++++++++++++++++-- src/oauth2.rs | 17 ++++-- src/qr.rs | 13 +++- 7 files changed, 193 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8096ed2eb..db91b3f59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -430,6 +430,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attohttpc" version = "0.24.1" @@ -1430,12 +1436,12 @@ dependencies = [ "proptest", "qrcodegen", "quick-xml", - "quinn", + "quinn 0.10.2", "quoted_printable", "rand 0.8.5", "ratelimit", "regex", - "reqwest 0.11.27", + "reqwest 0.12.5", "rusqlite", "rust-hsluv", "sanitize-filename", @@ -2762,6 +2768,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.4.0" @@ -3020,7 +3045,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -3043,6 +3068,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -3070,32 +3096,36 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.26.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", "hyper 1.2.0", "hyper-util", - "rustls 0.22.4", + "rustls 0.23.10", "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tower-service", + "webpki-roots 0.26.1", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.28", + "http-body-util", + "hyper 1.2.0", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] @@ -3299,7 +3329,7 @@ dependencies = [ "portable-atomic", "postcard", "quic-rpc", - "quinn", + "quinn 0.10.2", "rand 0.7.3", "rcgen 0.10.0", "ring 0.16.20", @@ -3403,7 +3433,7 @@ dependencies = [ "hyper-util", "once_cell", "prometheus-client", - "reqwest 0.12.4", + "reqwest 0.12.5", "serde", "struct_iterable", "time 0.3.34", @@ -3461,7 +3491,7 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rcgen 0.12.1", - "reqwest 0.12.4", + "reqwest 0.12.5", "ring 0.17.8", "rtnetlink", "rustls 0.21.11", @@ -4970,7 +5000,7 @@ dependencies = [ "flume", "futures", "pin-project", - "quinn", + "quinn 0.10.2", "serde", "tokio", "tokio-serde", @@ -5001,8 +5031,8 @@ checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" dependencies = [ "bytes", "pin-project-lite", - "quinn-proto", - "quinn-udp", + "quinn-proto 0.10.6", + "quinn-udp 0.4.1", "rustc-hash", "rustls 0.21.11", "thiserror", @@ -5010,6 +5040,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "quinn" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto 0.11.3", + "quinn-udp 0.5.2", + "rustc-hash", + "rustls 0.23.10", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "quinn-proto" version = "0.10.6" @@ -5028,6 +5075,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand 0.8.5", + "ring 0.17.8", + "rustc-hash", + "rustls 0.23.10", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + [[package]] name = "quinn-udp" version = "0.4.1" @@ -5041,6 +5105,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -5309,17 +5386,15 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", "hyper-rustls 0.24.2", - "hyper-tls", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -5331,7 +5406,6 @@ dependencies = [ "sync_wrapper 0.1.2", "system-configuration 0.5.1", "tokio", - "tokio-native-tls", "tokio-rustls 0.24.1", "tower-service", "url", @@ -5344,36 +5418,43 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "http-body-util", "hyper 1.2.0", - "hyper-rustls 0.26.0", + "hyper-rustls 0.27.2", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.22.4", + "quinn 0.11.2", + "rustls 0.23.10", "rustls-pemfile 2.1.2", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.0", + "system-configuration 0.5.1", "tokio", - "tokio-rustls 0.25.0", + "tokio-native-tls", + "tokio-rustls 0.26.0", "tower-service", "url", "wasm-bindgen", @@ -5588,20 +5669,6 @@ dependencies = [ "sct", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring 0.17.8", - "rustls-pki-types", - "rustls-webpki 0.102.4", - "subtle", - "zeroize", -] - [[package]] name = "rustls" version = "0.23.10" @@ -6670,11 +6737,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.22.4", + "rustls 0.23.10", "rustls-pki-types", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 8f1b1f168..18e52ac25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ quick-xml = "0.35" quoted_printable = "0.5" rand = { workspace = true } regex = { workspace = true } -reqwest = { version = "0.11.27", features = ["json"] } +reqwest = { version = "0.12.5", features = ["json"] } rusqlite = { workspace = true, features = ["sqlcipher"] } rust-hsluv = "0.1" sanitize-filename = { workspace = true } diff --git a/deny.toml b/deny.toml index 51bade197..524437253 100644 --- a/deny.toml +++ b/deny.toml @@ -54,6 +54,7 @@ skip = [ { name = "fastrand", version = "1.9.0" }, { name = "futures-lite", version = "1.13.0" }, { name = "getrandom", version = "<0.2" }, + { name = "h2", version = "0.3.26" }, { name = "http-body", version = "0.4.6" }, { name = "http", version = "0.2.12" }, { name = "hyper-rustls", version = "0.24.2" }, @@ -78,7 +79,6 @@ skip = [ { name = "ring", version = "0.16.20" }, { name = "rustls-pemfile", version = "1.0.4" }, { name = "rustls", version = "0.21.11" }, - { name = "rustls", version = "0.22.4" }, { name = "rustls-webpki", version = "0.101.7" }, { name = "sec1", version = "0.3.0" }, { name = "sha2", version = "<0.10" }, diff --git a/src/net.rs b/src/net.rs index 7a9a8465b..cbdb60b9c 100644 --- a/src/net.rs +++ b/src/net.rs @@ -43,7 +43,7 @@ async fn lookup_host_with_timeout( /// /// If `load_cache` is true, appends cached results not older than 30 days to the end /// or entries from fallback cache if there are no cached addresses. -async fn lookup_host_with_cache( +pub(crate) async fn lookup_host_with_cache( context: &Context, hostname: &str, port: u16, diff --git a/src/net/http.rs b/src/net/http.rs index 8a3a906f9..ea334a7fb 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -1,5 +1,6 @@ //! # HTTP module. +use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Result}; @@ -7,6 +8,7 @@ use mime::Mime; use once_cell::sync::Lazy; use crate::context::Context; +use crate::net::lookup_host_with_cache; use crate::socks::Socks5Config; const HTTP_TIMEOUT: Duration = Duration::from_secs(30); @@ -60,8 +62,13 @@ pub async fn read_url_blob(context: &Context, url: &str) -> Result { } async fn read_url_inner(context: &Context, url: &str) -> Result { - let socks5_config = Socks5Config::from_database(&context.sql).await?; - let client = get_client(socks5_config)?; + // It is safe to use cached IP addresses + // for HTTPS URLs, but for HTTP URLs + // better resolve from scratch each time to prevent + // cache poisoning attacks from having lasting effects. + let load_cache = url.starts_with("https://"); + + let client = get_client(context, load_cache).await?; let mut url = url.to_string(); // Follow up to 10 http-redirects @@ -86,10 +93,57 @@ async fn read_url_inner(context: &Context, url: &str) -> Result) -> Result { +struct CustomResolver { + context: Context, + + /// Whether to return cached results or not. + /// If resolver can be used for URLs + /// without TLS, e.g. HTTP URLs from HTML email, + /// this must be false. If TLS is used + /// and certificate hostnames are checked, + /// it is safe to load cache. + load_cache: bool, +} + +impl CustomResolver { + fn new(context: Context, load_cache: bool) -> Self { + Self { + context, + load_cache, + } + } +} + +impl reqwest::dns::Resolve for CustomResolver { + fn resolve(&self, hostname: reqwest::dns::Name) -> reqwest::dns::Resolving { + let context = self.context.clone(); + let load_cache = self.load_cache; + Box::pin(async move { + let port = 443; // Actual port does not matter. + + let socket_addrs = + lookup_host_with_cache(&context, hostname.as_str(), port, HTTP_TIMEOUT, load_cache) + .await; + match socket_addrs { + Ok(socket_addrs) => { + let addrs: reqwest::dns::Addrs = Box::new(socket_addrs.into_iter()); + + Ok(addrs) + } + Err(err) => Err(err.into()), + } + }) + } +} + +pub(crate) async fn get_client(context: &Context, load_cache: bool) -> Result { + let socks5_config = Socks5Config::from_database(&context.sql).await?; + let resolver = Arc::new(CustomResolver::new(context.clone(), load_cache)); + let builder = reqwest::ClientBuilder::new() .timeout(HTTP_TIMEOUT) - .add_root_certificate(LETSENCRYPT_ROOT.clone()); + .add_root_certificate(LETSENCRYPT_ROOT.clone()) + .dns_resolver(resolver); let builder = if let Some(socks5_config) = socks5_config { let proxy = reqwest::Proxy::all(socks5_config.to_url())?; diff --git a/src/oauth2.rs b/src/oauth2.rs index f3e5dfe5e..26936088a 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -10,7 +10,6 @@ use crate::config::Config; use crate::context::Context; use crate::provider; use crate::provider::Oauth2Authorizer; -use crate::socks::Socks5Config; use crate::tools::time; const OAUTH2_GMAIL: Oauth2 = Oauth2 { @@ -159,8 +158,12 @@ pub(crate) async fn get_oauth2_access_token( } // ... and POST - let socks5_config = Socks5Config::from_database(&context.sql).await?; - let client = crate::net::http::get_client(socks5_config)?; + + // All OAuth URLs are hardcoded HTTPS URLs, + // so it is safe to load DNS cache. + let load_cache = true; + + let client = crate::net::http::get_client(context, load_cache).await?; let response: Response = match client.post(post_url).form(&post_param).send().await { Ok(resp) => match resp.json().await { @@ -290,8 +293,12 @@ impl Oauth2 { // "verified_email": true, // "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg" // } - let socks5_config = Socks5Config::from_database(&context.sql).await.ok()?; - let client = match crate::net::http::get_client(socks5_config) { + + // All OAuth URLs are hardcoded HTTPS URLs, + // so it is safe to load DNS cache. + let load_cache = true; + + let client = match crate::net::http::get_client(context, load_cache).await { Ok(cl) => cl, Err(err) => { warn!(context, "failed to get HTTP client: {}", err); diff --git a/src/qr.rs b/src/qr.rs index edce65b96..539b3af04 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -20,7 +20,6 @@ use crate::events::EventType; use crate::key::Fingerprint; use crate::message::Message; use crate::peerstate::Peerstate; -use crate::socks::Socks5Config; use crate::token; use crate::tools::validate_id; use iroh_old as iroh; @@ -590,8 +589,16 @@ struct CreateAccountErrorResponse { #[allow(clippy::indexing_slicing)] async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { let url_str = &qr[DCACCOUNT_SCHEME.len()..]; - let socks5_config = Socks5Config::from_database(&context.sql).await?; - let response = crate::net::http::get_client(socks5_config)? + + if !url_str.starts_with(HTTPS_SCHEME) { + bail!("DCACCOUNT QR codes must use HTTPS scheme"); + } + + // As only HTTPS is used, it is safe to load DNS cache. + let load_cache = true; + + let response = crate::net::http::get_client(context, load_cache) + .await? .post(url_str) .send() .await?;