diff --git a/src/net.rs b/src/net.rs index 89e6f0bd2..d76f0ff7e 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,22 +1,22 @@ //! # Common network utilities. -use std::net::{IpAddr, SocketAddr}; -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::net::SocketAddr; use std::pin::Pin; -use std::str::FromStr; use std::time::Duration; use anyhow::{format_err, Context as _, Result}; -use tokio::net::{lookup_host, TcpStream}; +use tokio::net::TcpStream; use tokio::time::timeout; use tokio_io_timeout::TimeoutStream; use crate::context::Context; use crate::tools::time; +pub(crate) mod dns; pub(crate) mod http; pub(crate) mod session; pub(crate) mod tls; +use dns::lookup_host_with_cache; pub use http::{read_url, read_url_blob, Response as HttpResponse}; async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result { @@ -27,182 +27,6 @@ async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result Result> { - let res = timeout(timeout_val, lookup_host((hostname, port))) - .await - .context("DNS lookup timeout")? - .context("DNS lookup failure")?; - Ok(res.collect()) -} - -/// Looks up hostname and port using DNS and updates the address resolution cache. -/// -/// 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. -pub(crate) async fn lookup_host_with_cache( - context: &Context, - hostname: &str, - port: u16, - timeout_val: Duration, - load_cache: bool, -) -> Result> { - let now = time(); - let mut resolved_addrs = match lookup_host_with_timeout(hostname, port, timeout_val).await { - Ok(res) => res, - Err(err) => { - warn!( - context, - "DNS resolution for {}:{} failed: {:#}.", hostname, port, err - ); - Vec::new() - } - }; - - for addr in &resolved_addrs { - let ip_string = addr.ip().to_string(); - if ip_string == hostname { - // IP address resolved into itself, not interesting to cache. - continue; - } - - info!(context, "Resolved {}:{} into {}.", hostname, port, &addr); - - // Update the cache. - context - .sql - .execute( - "INSERT INTO dns_cache - (hostname, address, timestamp) - VALUES (?, ?, ?) - ON CONFLICT (hostname, address) - DO UPDATE SET timestamp=excluded.timestamp", - (hostname, ip_string, now), - ) - .await?; - } - - if load_cache { - for cached_address in context - .sql - .query_map( - "SELECT address - FROM dns_cache - WHERE hostname = ? - AND ? < timestamp + 30 * 24 * 3600 - ORDER BY timestamp DESC", - (hostname, now), - |row| { - let address: String = row.get(0)?; - Ok(address) - }, - |rows| { - rows.collect::, _>>() - .map_err(Into::into) - }, - ) - .await? - { - match IpAddr::from_str(&cached_address) { - Ok(ip_addr) => { - let addr = SocketAddr::new(ip_addr, port); - if !resolved_addrs.contains(&addr) { - resolved_addrs.push(addr); - } - } - Err(err) => { - warn!( - context, - "Failed to parse cached address {:?}: {:#}.", cached_address, err - ); - } - } - } - - if resolved_addrs.is_empty() { - // Load hardcoded cache if everything else fails. - // - // See and - // for reasons. - // - // In the future we may pre-resolve all provider database addresses - // and build them in. - match hostname { - "mail.sangham.net" => { - resolved_addrs.push(SocketAddr::new( - IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)), - port, - )); - resolved_addrs.push(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)), - port, - )); - } - "nine.testrun.org" => { - resolved_addrs.push(SocketAddr::new( - IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)), - port, - )); - resolved_addrs.push(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)), - port, - )); - } - "disroot.org" => { - resolved_addrs.push(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139)), - port, - )); - } - "mail.riseup.net" => { - resolved_addrs.push(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)), - port, - )); - resolved_addrs.push(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)), - port, - )); - } - "imap.gmail.com" => { - resolved_addrs.push(SocketAddr::new( - IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)), - port, - )); - resolved_addrs.push(SocketAddr::new( - IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)), - port, - )); - resolved_addrs.push(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)), - port, - )); - resolved_addrs.push(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)), - port, - )); - } - "smtp.gmail.com" => { - resolved_addrs.push(SocketAddr::new( - IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)), - port, - )); - resolved_addrs.push(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)), - port, - )); - } - _ => {} - } - } - } - - Ok(resolved_addrs) -} - /// Returns a TCP connection stream with read/write timeouts set /// and Nagle's algorithm disabled with `TCP_NODELAY`. /// diff --git a/src/net/dns.rs b/src/net/dns.rs new file mode 100644 index 000000000..4fe75489e --- /dev/null +++ b/src/net/dns.rs @@ -0,0 +1,187 @@ +//! DNS resolution and cache. + +use anyhow::{Context as _, Result}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::str::FromStr; +use std::time::Duration; +use tokio::net::lookup_host; +use tokio::time::timeout; + +use crate::context::Context; +use crate::tools::time; + +async fn lookup_host_with_timeout( + hostname: &str, + port: u16, + timeout_val: Duration, +) -> Result> { + let res = timeout(timeout_val, lookup_host((hostname, port))) + .await + .context("DNS lookup timeout")? + .context("DNS lookup failure")?; + Ok(res.collect()) +} + +/// Looks up hostname and port using DNS and updates the address resolution cache. +/// +/// 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. +pub(crate) async fn lookup_host_with_cache( + context: &Context, + hostname: &str, + port: u16, + timeout_val: Duration, + load_cache: bool, +) -> Result> { + let now = time(); + let mut resolved_addrs = match lookup_host_with_timeout(hostname, port, timeout_val).await { + Ok(res) => res, + Err(err) => { + warn!( + context, + "DNS resolution for {}:{} failed: {:#}.", hostname, port, err + ); + Vec::new() + } + }; + + for addr in &resolved_addrs { + let ip_string = addr.ip().to_string(); + if ip_string == hostname { + // IP address resolved into itself, not interesting to cache. + continue; + } + + info!(context, "Resolved {}:{} into {}.", hostname, port, &addr); + + // Update the cache. + context + .sql + .execute( + "INSERT INTO dns_cache + (hostname, address, timestamp) + VALUES (?, ?, ?) + ON CONFLICT (hostname, address) + DO UPDATE SET timestamp=excluded.timestamp", + (hostname, ip_string, now), + ) + .await?; + } + + if load_cache { + for cached_address in context + .sql + .query_map( + "SELECT address + FROM dns_cache + WHERE hostname = ? + AND ? < timestamp + 30 * 24 * 3600 + ORDER BY timestamp DESC", + (hostname, now), + |row| { + let address: String = row.get(0)?; + Ok(address) + }, + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, + ) + .await? + { + match IpAddr::from_str(&cached_address) { + Ok(ip_addr) => { + let addr = SocketAddr::new(ip_addr, port); + if !resolved_addrs.contains(&addr) { + resolved_addrs.push(addr); + } + } + Err(err) => { + warn!( + context, + "Failed to parse cached address {:?}: {:#}.", cached_address, err + ); + } + } + } + + if resolved_addrs.is_empty() { + // Load hardcoded cache if everything else fails. + // + // See and + // for reasons. + // + // In the future we may pre-resolve all provider database addresses + // and build them in. + match hostname { + "mail.sangham.net" => { + resolved_addrs.push(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)), + port, + )); + resolved_addrs.push(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)), + port, + )); + } + "nine.testrun.org" => { + resolved_addrs.push(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)), + port, + )); + resolved_addrs.push(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)), + port, + )); + } + "disroot.org" => { + resolved_addrs.push(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139)), + port, + )); + } + "mail.riseup.net" => { + resolved_addrs.push(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)), + port, + )); + resolved_addrs.push(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)), + port, + )); + } + "imap.gmail.com" => { + resolved_addrs.push(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)), + port, + )); + resolved_addrs.push(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)), + port, + )); + resolved_addrs.push(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)), + port, + )); + resolved_addrs.push(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)), + port, + )); + } + "smtp.gmail.com" => { + resolved_addrs.push(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)), + port, + )); + resolved_addrs.push(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)), + port, + )); + } + _ => {} + } + } + } + + Ok(resolved_addrs) +}