diff --git a/Cargo.toml b/Cargo.toml index 09e2f93cb..f89a5e010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -560,6 +560,9 @@ features = ["std"] [workspace.dependencies.nonzero_ext] version = "0.3.0" +[workspace.dependencies.resolvematrix] +version = "0.1.0" + [workspace.dependencies.serde_urlencoded] version = "0.7.1" diff --git a/src/admin/Cargo.toml b/src/admin/Cargo.toml index 4afed12b4..1f82978a4 100644 --- a/src/admin/Cargo.toml +++ b/src/admin/Cargo.toml @@ -92,6 +92,7 @@ serde-saphyr.workspace = true tokio.workspace = true tracing-subscriber.workspace = true tracing.workspace = true +resolvematrix.workspace = true [lints] workspace = true diff --git a/src/admin/debug/commands.rs b/src/admin/debug/commands.rs index 7d792a428..0ab5a0522 100644 --- a/src/admin/debug/commands.rs +++ b/src/admin/debug/commands.rs @@ -20,6 +20,7 @@ use conduwuit::{ }; use futures::{FutureExt, StreamExt, TryStreamExt}; use lettre::message::Mailbox; +use resolvematrix::server::{MatrixResolver, ResolvedDestination}; use ruma::{ CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId, UInt, @@ -1007,15 +1008,22 @@ impl crate::Context<'_> { ); } - let actual = self - .services - .resolver - .resolve_actual_dest(&server_name, !no_cache) - .await?; + let resolver: &MatrixResolver = if no_cache { + &MatrixResolver::new()? + } else { + &self.services.resolver.resolver + }; - let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host); - self.write_str(&msg).await - } + let actual = resolver.resolve_server(&server_name.as_str()).await?; + + let destination = match actual.destination { + | ResolvedDestination::Literal(addr) => addr.to_string(), + | ResolvedDestination::Named(host, port) => format!("{host}:{port}"), + }; + + let msg = format!("Destination: {}\nHostname URI (SNI): {}", destination, actual.host); + self.write_str(&msg).await +} pub(super) async fn memory_stats(&self, opts: Option) -> Result { const OPTS: &str = "abcdefghijklmnopqrstuvwxyz"; diff --git a/src/admin/query/resolver.rs b/src/admin/query/resolver.rs index 7e5a4b40d..4a88650f8 100644 --- a/src/admin/query/resolver.rs +++ b/src/admin/query/resolver.rs @@ -46,7 +46,7 @@ impl crate::Context<'_> { writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?; writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?; - let mut destinations = self.services.resolver.cache.destinations().boxed(); + let mut destinations = self.services.resolver.dns.cache.destinations().boxed(); while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await { if let Some(server_name) = server_name.as_ref() { @@ -69,7 +69,7 @@ impl crate::Context<'_> { writeln!(self, "| Server Name | IP | Port | Expires | Overriding |").await?; writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?; - let mut overrides = self.services.resolver.cache.overrides().boxed(); + let mut overrides = self.services.resolver.dns.cache.overrides().boxed(); while let Some((name, CachedOverride { ips, port, expire, overriding })) = overrides.next().await @@ -92,11 +92,11 @@ impl crate::Context<'_> { async fn flush_cache(&self, name: Option, all: bool) -> Result { if all { - self.services.resolver.cache.clear().await; + self.services.resolver.dns.cache.clear().await; writeln!(self, "Resolver caches cleared!").await } else if let Some(name) = name { - self.services.resolver.cache.del_destination(&name); - self.services.resolver.cache.del_override(&name); + self.services.resolver.dns.cache.del_destination(&name); + self.services.resolver.dns.cache.del_override(&name); self.write_str(&format!("Cleared {name} from resolver caches!")) .await } else { diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index 39eded4cf..e918285c1 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -118,6 +118,7 @@ url.workspace = true parking_lot.workspace = true lock_api.workspace = true hyper-util.workspace = true +resolvematrix.workspace = true [target.'cfg(unix)'.dependencies] nix.workspace = true diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index d61867b01..813324912 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -87,6 +87,8 @@ pub enum Error { YamlDe(#[from] serde_saphyr::Error), #[error(transparent)] YamlSer(#[from] serde_saphyr::ser_error::Error), + #[error(transparent)] + ResolveServer(#[from] resolvematrix::server::ResolveServerError), // ruma/conduwuit #[error("Arithmetic operation failed: {0}")] diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index e302ff6c9..923fef2cb 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -119,6 +119,7 @@ recaptcha-verify = { version = "0.2.0", default-features = false } reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"] } # As long as recaptcha-verify's reqwest is outdated yansi.workspace = true lettre.workspace = true +resolvematrix.workspace = true serde_urlencoded.workspace = true [target.'cfg(all(unix, target_os = "linux"))'.dependencies] diff --git a/src/service/client/mod.rs b/src/service/client/mod.rs index b4c645ba2..4a299fee3 100644 --- a/src/service/client/mod.rs +++ b/src/service/client/mod.rs @@ -43,7 +43,7 @@ impl crate::Service for Service { Ok(Arc::new(Self { default: base(config)? - .dns_resolver(resolver.resolver.clone()) + .dns_resolver(resolver.dns.resolver.clone()) .build()?, url_preview: base(config) @@ -51,19 +51,19 @@ impl crate::Service for Service { builder_interface(builder, url_preview_bind_iface.as_deref()) })? .local_address(url_preview_bind_addr) - .dns_resolver(resolver.resolver.clone()) + .dns_resolver(resolver.dns.resolver.clone()) .timeout(Duration::from_secs(config.url_preview_timeout)) .redirect(redirect::Policy::limited(3)) .user_agent(url_preview_user_agent) .build()?, extern_media: base(config)? - .dns_resolver(resolver.resolver.clone()) + .dns_resolver(resolver.dns.resolver.clone()) .redirect(redirect::Policy::limited(3)) .build()?, well_known: base(config)? - .dns_resolver(resolver.resolver.clone()) + .dns_resolver(resolver.dns.resolver.clone()) .connect_timeout(Duration::from_secs(config.well_known_conn_timeout)) .read_timeout(Duration::from_secs(config.well_known_timeout)) .timeout(Duration::from_secs(config.well_known_timeout)) @@ -72,7 +72,7 @@ impl crate::Service for Service { .build()?, federation: base(config)? - .dns_resolver(resolver.resolver.hooked.clone()) + .dns_resolver(resolver.dns.resolver.hooked.clone()) .connect_timeout(Duration::from_secs(config.federation_conn_timeout)) .read_timeout(Duration::from_secs(config.federation_timeout)) .timeout(Duration::from_secs( @@ -86,7 +86,7 @@ impl crate::Service for Service { .build()?, federation_slow: base(config)? - .dns_resolver(resolver.resolver.hooked.clone()) + .dns_resolver(resolver.dns.resolver.hooked.clone()) .connect_timeout(Duration::from_secs(config.federation_conn_timeout)) .read_timeout(Duration::from_secs(config.federation_timeout.saturating_mul(6))) .timeout(Duration::from_secs( @@ -100,7 +100,7 @@ impl crate::Service for Service { .build()?, sender: base(config)? - .dns_resolver(resolver.resolver.hooked.clone()) + .dns_resolver(resolver.dns.resolver.hooked.clone()) .connect_timeout(Duration::from_secs(config.federation_conn_timeout)) .read_timeout(Duration::from_secs(config.sender_timeout)) .timeout(Duration::from_secs(config.sender_timeout)) @@ -110,7 +110,7 @@ impl crate::Service for Service { .build()?, appservice: base(config)? - .dns_resolver(resolver.resolver.clone()) + .dns_resolver(resolver.dns.resolver.clone()) .connect_timeout(Duration::from_secs(5)) .read_timeout(Duration::from_secs(config.appservice_timeout)) .timeout(Duration::from_secs(config.appservice_timeout)) @@ -120,7 +120,7 @@ impl crate::Service for Service { .build()?, pusher: base(config)? - .dns_resolver(resolver.resolver.clone()) + .dns_resolver(resolver.dns.resolver.clone()) .connect_timeout(Duration::from_secs(config.pusher_conn_timeout)) .timeout(Duration::from_secs(config.pusher_timeout)) .pool_max_idle_per_host(1) diff --git a/src/service/federation/execute.rs b/src/service/federation/execute.rs index 70bef2f0e..d843f9ad4 100644 --- a/src/service/federation/execute.rs +++ b/src/service/federation/execute.rs @@ -7,6 +7,7 @@ use conduwuit::{ }; use ipaddress::IPAddress; use reqwest::{Client, Method, Request, Response, Url}; +use resolvematrix::resolution::Resolution; use ruma::{ ServerName, api::{ @@ -150,7 +151,7 @@ impl super::Service { async fn perform( &self, dest: &ServerName, - actual: &ActualDest, + actual: &Resolution, request: Request, client: &Client, ) -> Result @@ -198,7 +199,7 @@ impl super::Service { async fn handle_response( &self, dest: &ServerName, - actual: &ActualDest, + actual: &Resolution, method: &Method, url: &Url, response: Response, @@ -231,7 +232,7 @@ impl super::Service { async fn into_http_response( dest: &ServerName, - actual: &ActualDest, + actual: &Resolution, method: &Method, url: &Url, mut response: Response, @@ -243,7 +244,7 @@ async fn into_http_response( request_url = %url, response_url = %response.url(), "Received response from {}", - actual.string(), + actual.base_url(), ); let mut http_response_builder = http::Response::builder() @@ -280,7 +281,7 @@ async fn into_http_response( } fn handle_error( - actual: &ActualDest, + actual: &Resolution, method: &Method, url: &Url, mut e: reqwest::Error, diff --git a/src/service/resolver/actual.rs b/src/service/resolver/actual.rs index 1ca3b595c..e69de29bb 100644 --- a/src/service/resolver/actual.rs +++ b/src/service/resolver/actual.rs @@ -1,441 +0,0 @@ -use std::fmt::Debug; - -use conduwuit::{Err, Result, debug, debug_info, err, error, trace}; -use futures::{FutureExt, TryFutureExt}; -use hickory_resolver::{ - net::{DnsError, NetError}, - proto::rr::rdata::SRV, -}; -use ipaddress::IPAddress; -use ruma::ServerName; - -use super::{ - cache::{CachedDest, CachedOverride, MAX_IPS}, - fed::{FedDest, PortString, add_port_to_hostname, ensure_host_has_port, get_ip_with_port}, -}; - -const DEFAULT_PORT: u16 = 8448; - -#[derive(Clone, Debug)] -pub(crate) struct ActualDest { - pub(crate) dest: FedDest, - pub(crate) host: String, -} - -impl ActualDest { - #[inline] - pub(crate) fn string(&self) -> String { self.dest.https_string() } -} - -impl super::Service { - #[tracing::instrument(skip_all, level = "debug", name = "resolve")] - pub(crate) async fn get_actual_dest(&self, server_name: &ServerName) -> Result { - let (CachedDest { dest, host, .. }, _cached) = - self.lookup_actual_dest(server_name).await?; - - Ok(ActualDest { dest, host }) - } - - pub(crate) async fn lookup_actual_dest( - &self, - server_name: &ServerName, - ) -> Result<(CachedDest, bool)> { - if let Ok(result) = self.cache.get_destination(server_name).await { - return Ok((result, true)); - } - - let _dedup = self.resolving.lock(server_name.as_str()); - if let Ok(result) = self.cache.get_destination(server_name).await { - return Ok((result, true)); - } - - self.resolve_actual_dest(server_name, true) - .inspect_ok(|result| self.cache.set_destination(server_name, result)) - .map_ok(|result| (result, false)) - .boxed() - .await - } - - /// Returns: `actual_destination` + `Host` http header - #[tracing::instrument(name = "actual", level = "debug", skip(self, cache))] - pub async fn resolve_actual_dest( - &self, - dest: &ServerName, - cache: bool, - ) -> Result { - debug!( - dest = %dest, - cache = %cache, - "Resolving server name and port" - ); - // Ensure dest is a valid connection endpoint - self.validate_dest(dest)?; - - // Clippy believes this can be a clone, however we are actually converting - // ServerName to String - #[allow(clippy::implicit_clone)] - let mut host_header = dest.to_string().to_owned(); - let actual_dest = self - .resolve_server_name(dest, cache, &mut host_header) - .await?; - - host_header = ensure_host_has_port(&host_header).to_string(); - - debug!( - dest = %dest, - actual_dest = %actual_dest, - host = %host_header, - "Finished resolving server name" - ); - Ok(CachedDest { - dest: actual_dest, - host: host_header, - expire: CachedDest::default_expire(), - }) - } - - /// Performs the server resolution steps as per the specification: - /// - async fn resolve_server_name( - &self, - dest: &ServerName, - cache: bool, - host: &mut String, - ) -> Result { - // 1. If `dest` is an IP, use it directly. If a port is provided as well - // (IP:port socket pair) use that, otherwise default to port 8448 - if let Some(fed_dest) = get_ip_with_port(dest.as_str()) { - debug!("1: IP literal with provided or default port"); - return Ok(fed_dest); - } - - // 2. If `dest` is a hostname and has a provided port (format of `host:port`), - // resolve the hostname to an IP address and connect it and the provided port - if dest.as_str().contains(':') { - return self.resolve_2_host_port(dest, cache).await; - } - - // Pre-resolve IP? Unsure what overrides exactly do, system is due to be removed - // either way https://matrix.to/#/!da26JtAjE6APGLnX8ncWsvc-skF2KQZ9Nw_MbNpYD2k/%24_hq6JP0JXANbMTMPdV64iZbgbsZdhy92M5ndDYGy6No - self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, true) - .await?; - - // Ensure server is running (not shutting down) before continuing resolution - self.services.server.check_running()?; - - // 3. If `dest` is a hostname with no port, send GET to `https:///.well-known/matrix/server`. - // If invalid JSON (throws error), skip to step 4. Otherwise, parse - // `delegated` as `[:]` and... - if let Some(delegated) = self.request_well_known(dest.as_str()).await? { - return self.resolve_3_well_known(host, cache, delegated).await; - } - - // 4. if .well-known errored, perform SRV (see 3.3) - if let Some(overrider) = self.query_srv_record(dest.as_str()).await? { - return self.resolve_4_srv_lookup(host, cache, overrider).await; - } - - // 5. if .well-known errored and no SRV exists, resolve IP and connect on - // default port (8448) - self.resolve_5_direct(dest, cache).await - } - - /// Parse a host:port socket pair into separate parts, and resolve the - /// hostname into an IP address - async fn resolve_2_host_port(&self, dest: &ServerName, cache: bool) -> Result { - debug!("2: Hostname with included port"); - let (host, port) = dest.as_str().split_once(':').unwrap(); - - self.conditional_query_and_cache( - host, - port.parse::().unwrap_or(DEFAULT_PORT), - cache, - ) - .await?; - - Ok(FedDest::Named( - host.to_owned(), - port.try_into().unwrap_or_else(|_| FedDest::default_port()), - )) - } - - async fn resolve_3_well_known( - &self, - host: &mut String, - cache: bool, - delegated: String, - ) -> Result { - debug!("3: A .well-known file is available"); - *host = add_port_to_hostname(&delegated).uri_string(); - - // 3.1 - If is IP:port, connect to that, - // or IP with default port if no port provided (8448) - if let Some(host_and_port) = get_ip_with_port(&delegated) { - debug!("3.1: IP with port in .well-known file"); - return Ok(host_and_port); - } - - // 3.2 - If is hostname:port, lookup IP for hostname and connect - if delegated.contains(':') { - return self.resolve_3_2_hostname_port(cache, &delegated).await; - } - - // 3.3 - If is not an IP and there is no port, lookup SRV - // `_matrix._tcp.` (which may provide a new hostname + port to use, - // see steps 3.1 and 3.2) - trace!("Delegated hostname has no port, querying SRV"); - if let Some(overrider) = self.query_srv_record(&delegated).await? { - return self.resolve_3_3_use_srv(cache, &delegated, overrider).await; - } - - self.resolve_3_4_use_default_port(cache, delegated).await - } - - async fn resolve_3_2_hostname_port(&self, cache: bool, delegated: &str) -> Result { - debug!("3.2: Hostname with port in .well-known file"); - let (host, port) = &delegated.split_once(':').unwrap(); - self.conditional_query_and_cache( - host, - port.parse::().unwrap_or(DEFAULT_PORT), - cache, - ) - .await?; - - trace!("Successfully resolved IP for {delegated}"); - Ok(FedDest::Named( - host.to_owned().to_owned(), - port.to_owned() - .try_into() - .unwrap_or_else(|_| FedDest::default_port()), - )) - } - - async fn resolve_3_3_use_srv( - &self, - cache: bool, - delegated: &String, - overrider: FedDest, - ) -> Result { - debug!("3.3: SRV lookup successful"); - - let force_port = overrider.port(); - self.conditional_query_and_cache_override( - delegated, - &overrider.hostname(), - force_port.unwrap_or(DEFAULT_PORT), - cache, - ) - .await?; - - if let Some(port) = force_port { - return Ok(FedDest::Named( - delegated.to_owned(), - format!(":{port}") - .as_str() - .try_into() - .unwrap_or_else(|_| FedDest::default_port()), - )); - } - - Ok(add_port_to_hostname(delegated)) - } - - async fn resolve_3_4_use_default_port( - &self, - cache: bool, - delegated: String, - ) -> Result { - debug!("3.4: No SRV records found, use the hostname from .well-known with default port"); - self.conditional_query_and_cache(&delegated, DEFAULT_PORT, cache) - .await?; - Ok(add_port_to_hostname(&delegated)) - } - - async fn resolve_4_srv_lookup( - &self, - host: &str, - cache: bool, - overrider: FedDest, - ) -> Result { - debug!("4: No .well-known; SRV record found"); - let force_port = overrider.port(); - self.conditional_query_and_cache_override( - host, - &overrider.hostname(), - force_port.unwrap_or(DEFAULT_PORT), - cache, - ) - .await?; - - if let Some(port) = force_port { - let port = format!(":{port}"); - - return Ok(FedDest::Named( - host.to_owned(), - PortString::from(port.as_str()).unwrap_or_else(|_| FedDest::default_port()), - )); - } - - Ok(add_port_to_hostname(host)) - } - - async fn resolve_5_direct(&self, dest: &ServerName, cache: bool) -> Result { - debug!("5: No port provided and no SRV record found"); - self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, cache) - .await?; - - Ok(add_port_to_hostname(dest.as_str())) - } - - #[inline] - async fn conditional_query_and_cache( - &self, - hostname: &str, - port: u16, - cache: bool, - ) -> Result { - self.conditional_query_and_cache_override(hostname, hostname, port, cache) - .await - } - - #[inline] - async fn conditional_query_and_cache_override( - &self, - untername: &str, - hostname: &str, - port: u16, - cache: bool, - ) -> Result { - if !cache { - return Ok(()); - } - - if self.cache.has_override(untername).await { - return Ok(()); - } - - self.query_and_cache_override(untername, hostname, port) - .await - } - - #[tracing::instrument(name = "ip", level = "debug", skip(self))] - async fn query_and_cache_override( - &self, - untername: &'_ str, - hostname: &'_ str, - port: u16, - ) -> Result { - self.services.server.check_running()?; - - debug!("querying IP for {untername:?} ({hostname:?}:{port})"); - match self.resolver.resolver.lookup_ip(hostname.to_owned()).await { - | Err(e) => Self::handle_resolve_error(&e, hostname), - | Ok(override_ip) => { - self.cache.set_override(untername, &CachedOverride { - ips: override_ip.into_iter().take(MAX_IPS).collect(), - port, - expire: CachedOverride::default_expire(), - overriding: (hostname != untername) - .then_some(hostname.into()) - .inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")), - }); - - Ok(()) - }, - } - } - - #[tracing::instrument(name = "srv", level = "debug", skip(self))] - async fn query_srv_record(&self, hostname: &'_ str) -> Result> { - let hostnames = - [format!("_matrix-fed._tcp.{hostname}."), format!("_matrix._tcp.{hostname}.")]; - - for hostname in hostnames { - self.services.server.check_running()?; - - debug!("querying SRV for {hostname:?}"); - let hostname = hostname.trim_end_matches('.'); - match self.resolver.resolver.srv_lookup(hostname).await { - | Err(e) => Self::handle_resolve_error(&e, hostname)?, - | Ok(result) => { - return Ok(result.answers().iter().next().map(|result| { - let data = result.try_borrow::().expect("should be SRV response"); - - FedDest::Named( - data.data() - .target - .to_string() - .trim_end_matches('.') - .to_owned(), - format!(":{}", data.data().port) - .as_str() - .try_into() - .unwrap_or_else(|_| FedDest::default_port()), - ) - })); - }, - } - } - - Ok(None) - } - - fn handle_resolve_error(err: &NetError, host: &'_ str) -> Result<()> { - match err { - | NetError::NoConnections => { - error!( - "Your DNS server is overloaded and has ran out of connections. It is \ - strongly recommended you remediate this issue to ensure proper federation \ - connectivity." - ); - - Err!(error!(%host, "DNS error: {err}")) - }, - | NetError::Timeout => Err!(error!(%host, "DNS query timed out")), - | NetError::Dns(DnsError::NoRecordsFound(..)) => { - // Raise to debug_warn if we can find out the result wasn't from cache - debug!(%host, "No DNS records found: {err}"); - Ok(()) - }, - | _ => Err!(error!(%host, "DNS error: {err}")), - } - } - - /// Ensure `dest` is a valid destination (valid ip if it is an IP), and not - /// ourselves (unless in config) - fn validate_dest(&self, dest: &ServerName) -> Result<()> { - if dest == self.services.server.name && !self.services.server.config.federation_loopback { - return Err!("Won't send federation request to ourselves"); - } - - if dest.is_ip_literal() || IPAddress::is_valid(dest.host()) { - self.validate_dest_ip_literal(dest)?; - } - - debug!(dest = %dest, "Valid destination for resolution"); - Ok(()) - } - - fn validate_dest_ip_literal(&self, dest: &ServerName) -> Result<()> { - trace!("Destination is an IP literal, checking against IP range denylist.",); - debug_assert!( - dest.is_ip_literal() || !IPAddress::is_valid(dest.host()), - "Destination is not an IP literal." - ); - let ip = IPAddress::parse(dest.host()).map_err(|e| { - err!(BadServerResponse(debug_error!("Failed to parse IP literal from string: {e}"))) - })?; - - self.validate_ip(&ip)?; - - Ok(()) - } - - pub(crate) fn validate_ip(&self, ip: &IPAddress) -> Result<()> { - if !self.services.client.valid_cidr_range(ip) { - return Err!(BadServerResponse("Not allowed to send requests to this IP")); - } - - Ok(()) - } -} diff --git a/src/service/resolver/cache.rs b/src/service/resolver/cache.rs index b0268a3cd..6ec0b5b4d 100644 --- a/src/service/resolver/cache.rs +++ b/src/service/resolver/cache.rs @@ -4,7 +4,7 @@ use conduwuit::{ Result, arrayvec::ArrayVec, at, err, - utils::{math::Expected, rand, stream::TryIgnore}, + utils::{math::Expected, stream::TryIgnore}, }; use database::{Cbor, Deserialized, Map}; use futures::{Stream, StreamExt, future::join}; @@ -114,11 +114,6 @@ impl CachedDest { #[must_use] pub fn valid(&self) -> bool { self.expire > SystemTime::now() } - #[must_use] - pub(crate) fn default_expire() -> SystemTime { - rand::time_from_now_secs(60 * 60 * 18..60 * 60 * 36) - } - #[inline] #[must_use] pub fn size(&self) -> usize { @@ -134,11 +129,6 @@ impl CachedOverride { #[must_use] pub fn valid(&self) -> bool { self.expire > SystemTime::now() } - #[must_use] - pub(crate) fn default_expire() -> SystemTime { - rand::time_from_now_secs(60 * 60 * 6..60 * 60 * 12) - } - #[inline] #[must_use] pub fn size(&self) -> usize { size_of_val(self) } diff --git a/src/service/resolver/fed.rs b/src/service/resolver/fed.rs index 83601a98a..f8068f596 100644 --- a/src/service/resolver/fed.rs +++ b/src/service/resolver/fed.rs @@ -1,8 +1,4 @@ -use std::{ - borrow::Cow, - fmt, - net::{IpAddr, SocketAddr}, -}; +use std::{fmt, net::SocketAddr}; use conduwuit::{arrayvec::ArrayString, utils::math::Expected}; use serde::{Deserialize, Serialize}; @@ -18,50 +14,7 @@ pub type PortString = ArrayString<16>; const DEFAULT_PORT: &str = ":8448"; -/// Attempt to parse `dest_str` as either an IP:port socket pair or as a plain -/// IP (adding the default port), returning `None` if dest_str is neither a -/// socket pair nor a plain IP. -pub(crate) fn get_ip_with_port(dest_str: &str) -> Option { - if let Ok(dest) = dest_str.parse::() { - Some(FedDest::Literal(dest)) - } else if let Ok(ip_addr) = dest_str.parse::() { - Some(FedDest::Literal(SocketAddr::new(ip_addr, 8448))) - } else { - None - } -} - -/// Convert a `dest` string with or without port into a FedDest with either -/// the provided port (if host:port format) or the default port (8448) -pub(crate) fn add_port_to_hostname(dest: &str) -> FedDest { - let (host, port) = match dest.find(':') { - | None => (dest, DEFAULT_PORT), - | Some(pos) => dest.split_at(pos), - }; - - FedDest::Named( - host.to_owned(), - PortString::from(port).unwrap_or_else(|_| FedDest::default_port()), - ) -} - -/// Ensure `host` always has a port -/// -/// `get_ip_with_port` returns `None` if `host` isn't an IP:port string or plain -/// IP, in which case `add_port_to_hostname` adds it instead -#[inline] -pub(crate) fn ensure_host_has_port(host: &str) -> FedDest { - get_ip_with_port(host).unwrap_or_else(|| add_port_to_hostname(host)) -} - impl FedDest { - pub(crate) fn https_string(&self) -> String { - match self { - | Self::Literal(addr) => format!("https://{addr}"), - | Self::Named(host, port) => format!("https://{host}{port}"), - } - } - pub(crate) fn uri_string(&self) -> String { match self { | Self::Literal(addr) => addr.to_string(), @@ -69,23 +22,6 @@ impl FedDest { } } - #[inline] - pub(crate) fn hostname(&self) -> Cow<'_, str> { - match &self { - | Self::Literal(addr) => addr.ip().to_string().into(), - | Self::Named(host, _) => host.into(), - } - } - - #[inline] - #[allow(clippy::string_slice)] - pub(crate) fn port(&self) -> Option { - match &self { - | Self::Literal(addr) => Some(addr.port()), - | Self::Named(_, port) => port[1..].parse().ok(), - } - } - #[inline] #[must_use] pub fn default_port() -> PortString { diff --git a/src/service/resolver/mod.rs b/src/service/resolver/mod.rs index c513cec9a..c24b8ba60 100644 --- a/src/service/resolver/mod.rs +++ b/src/service/resolver/mod.rs @@ -1,33 +1,31 @@ -pub mod actual; pub mod cache; mod dns; pub mod fed; -#[cfg(test)] -mod tests; -mod well_known; use std::sync::Arc; use async_trait::async_trait; -use conduwuit::{Result, Server, arrayvec::ArrayString, utils::MutexMap}; +use conduwuit::{Err, Result, implement}; +use ipaddress::IPAddress; +use resolvematrix::server::MatrixResolver; use self::{cache::Cache, dns::Resolver}; use crate::{Dep, client}; pub struct Service { - pub cache: Arc, - pub resolver: Arc, - resolving: Resolving, + pub resolver: MatrixResolver, + pub dns: Dns, services: Services, } struct Services { - server: Arc, client: Dep, } -type Resolving = MutexMap; -type NameBuf = ArrayString<256>; +pub struct Dns { + pub cache: Arc, + pub resolver: Arc, +} #[async_trait] impl crate::Service for Service { @@ -35,20 +33,31 @@ impl crate::Service for Service { fn build(args: crate::Args<'_>) -> Result> { let cache = Cache::new(&args); Ok(Arc::new(Self { - cache: cache.clone(), - resolver: Resolver::build(args.server, cache)?, - resolving: MutexMap::new(), + resolver: MatrixResolver::new()?, + dns: Dns { + cache: cache.clone(), + resolver: Resolver::build(args.server, cache)?, + }, services: Services { - server: args.server.clone(), client: args.depend::("client"), }, })) } async fn clear_cache(&self) { - self.resolver.clear_cache(); - self.cache.clear().await; + // No ability to clean resolvematrix cache at the moment + self.dns.resolver.clear_cache(); + self.dns.cache.clear().await; } - fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } + fn name(&self) -> &str { crate::service::make_name(module_path!()) } +} + +#[implement(Service)] +pub fn validate_ip(&self, ip: &IPAddress) -> Result<()> { + if !self.services.client.valid_cidr_range(ip) { + return Err!(BadServerResponse("Not allowed to send requests to this IP")); + } + + Ok(()) } diff --git a/src/service/resolver/tests.rs b/src/service/resolver/tests.rs deleted file mode 100644 index 068e08bd0..000000000 --- a/src/service/resolver/tests.rs +++ /dev/null @@ -1,41 +0,0 @@ -use super::fed::{FedDest, add_port_to_hostname, get_ip_with_port}; - -#[test] -fn ips_get_default_ports() { - assert_eq!( - get_ip_with_port("1.1.1.1"), - Some(FedDest::Literal("1.1.1.1:8448".parse().unwrap())) - ); - assert_eq!( - get_ip_with_port("dead:beef::"), - Some(FedDest::Literal("[dead:beef::]:8448".parse().unwrap())) - ); -} - -#[test] -fn ips_keep_custom_ports() { - assert_eq!( - get_ip_with_port("1.1.1.1:1234"), - Some(FedDest::Literal("1.1.1.1:1234".parse().unwrap())) - ); - assert_eq!( - get_ip_with_port("[dead::beef]:8933"), - Some(FedDest::Literal("[dead::beef]:8933".parse().unwrap())) - ); -} - -#[test] -fn hostnames_get_default_ports() { - assert_eq!( - add_port_to_hostname("example.com"), - FedDest::Named(String::from("example.com"), ":8448".try_into().unwrap()) - ); -} - -#[test] -fn hostnames_keep_custom_ports() { - assert_eq!( - add_port_to_hostname("example.com:1337"), - FedDest::Named(String::from("example.com"), ":1337".try_into().unwrap()) - ); -} diff --git a/src/service/resolver/well_known.rs b/src/service/resolver/well_known.rs deleted file mode 100644 index 2a4ecf222..000000000 --- a/src/service/resolver/well_known.rs +++ /dev/null @@ -1,50 +0,0 @@ -use conduwuit::{Result, debug, debug_error, debug_info, trace, utils::response::LimitReadExt}; -use ruma::ServerName; - -impl super::Service { - #[tracing::instrument(name = "well-known", level = "debug", skip(self, dest))] - pub(super) async fn request_well_known(&self, dest: &str) -> Result> { - trace!("Requesting well known for {dest}"); - let response = self - .services - .client - .well_known - .get(format!("https://{dest}/.well-known/matrix/server")) - .send() - .await; - - trace!("response: {response:?}"); - if let Err(e) = &response { - debug!("error: {e:?}"); - return Ok(None); - } - - let response = response?; - if !response.status().is_success() { - debug!("response not 2XX"); - return Ok(None); - } - - let Ok(text) = response.limit_read_text(8192).await else { - debug!("failed to read well-known response (too large or non-text content)"); - return Ok(None); - }; - trace!("response text: {text:?}"); - - let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_default(); - - let m_server = body - .get("m.server") - .unwrap_or(&serde_json::Value::Null) - .as_str() - .unwrap_or_default(); - - if ServerName::parse(m_server).is_err() { - debug_error!("response content missing or invalid"); - return Ok(None); - } - - debug_info!("{dest:?} found at {m_server:?}"); - Ok(Some(m_server.to_owned())) - } -}