Browse Source

http: make DNS setup bootstrap-safe

Remote configuration can require an HTTP client while CONFIG is still
initializing. Building the DNS resolver currently reads
CONFIG.dns_prefer_ipv6(), so loading an S3-backed config can deadlock.

Build one resolver without consulting CONFIG. Order addresses for each
lookup using the merged setting when available, falling back to the
environment and then IPv4-first during bootstrap.
pull/7639/head
Chase Douglas 2 weeks ago
parent
commit
f0d4ef2deb
  1. 55
      src/http_client.rs

55
src/http_client.rs

@ -14,7 +14,10 @@ use reqwest::{
}; };
use url::Host; use url::Host;
use crate::{CONFIG, util::is_global}; use crate::{
CONFIG,
util::{get_env_bool, is_global},
};
pub fn make_http_request(method: reqwest::Method, url: &str) -> Result<reqwest::RequestBuilder, crate::Error> { pub fn make_http_request(method: reqwest::Method, url: &str) -> Result<reqwest::RequestBuilder, crate::Error> {
static INSTANCE: LazyLock<Client> = static INSTANCE: LazyLock<Client> =
@ -59,6 +62,14 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder {
.timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(10))
} }
fn dns_prefer_ipv6() -> bool {
// CONFIG may require DNS to initialize, so avoid forcing it during bootstrap.
match LazyLock::get(&CONFIG) {
Some(config) => config.dns_prefer_ipv6(),
None => get_env_bool("DNS_PREFER_IPV6").unwrap_or(false),
}
}
fn should_block_ip(ip: IpAddr) -> bool { fn should_block_ip(ip: IpAddr) -> bool {
if !CONFIG.http_request_block_non_global_ips() { if !CONFIG.http_request_block_non_global_ips() {
return false; return false;
@ -258,12 +269,8 @@ impl CustomDnsResolver {
fn new() -> Arc<Self> { fn new() -> Arc<Self> {
TokioResolver::builder(TokioRuntimeProvider::default()) TokioResolver::builder(TokioRuntimeProvider::default())
.and_then(|mut builder| { .and_then(|mut builder| {
// Hickory's default since v0.26 is `Ipv6AndIpv4`, which sorts IPv6 first // Query both families; the preferred order is applied per lookup below.
// This might cause issues on IPv4 only systems or containers builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6;
// Unless someone enabled DNS_PREFER_IPV6, use Ipv4AndIpv6, which returns IPv4 first which was our previous default
if !CONFIG.dns_prefer_ipv6() {
builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6;
}
builder.build() builder.build()
}) })
.inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}")) .inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}"))
@ -289,6 +296,14 @@ impl CustomDnsResolver {
} }
} }
fn sort_addresses(addresses: &mut [SocketAddr], prefer_ipv6: bool) {
if prefer_ipv6 {
addresses.sort_by_key(SocketAddr::is_ipv4);
} else {
addresses.sort_by_key(SocketAddr::is_ipv6);
}
}
fn pre_resolve(name: &str, enforce_block: bool) -> Result<(), CustomHttpClientError> { fn pre_resolve(name: &str, enforce_block: bool) -> Result<(), CustomHttpClientError> {
let Ok(host) = get_valid_host(name) else { let Ok(host) = get_valid_host(name) else {
return Err(CustomHttpClientError::Invalid { return Err(CustomHttpClientError::Invalid {
@ -320,7 +335,9 @@ impl Resolve for CustomDns {
let this = Arc::clone(&self.resolver); let this = Arc::clone(&self.resolver);
Box::pin(async move { Box::pin(async move {
let name = name.as_str(); let name = name.as_str();
let results = this.resolve_domain(name, enforce_block).await?; let mut results = this.resolve_domain(name, enforce_block).await?;
// Recheck after bootstrap so long-lived clients adopt the loaded config.
sort_addresses(&mut results, dns_prefer_ipv6());
if results.is_empty() { if results.is_empty() {
warn!("Unable to resolve {name} to any valid IP address"); warn!("Unable to resolve {name} to any valid IP address");
} }
@ -391,7 +408,7 @@ pub(crate) mod aws {
mod tests { mod tests {
use super::*; use super::*;
use crate::util::is_global_hardcoded; use crate::util::is_global_hardcoded;
use std::net::Ipv4Addr; use std::net::{Ipv4Addr, Ipv6Addr};
use url::Host; use url::Host;
// === // ===
@ -404,6 +421,26 @@ mod tests {
} }
} }
#[test]
fn dns_setup_does_not_initialize_config() {
assert!(LazyLock::get(&CONFIG).is_none());
drop(CustomDns::instance(false));
assert!(LazyLock::get(&CONFIG).is_none());
}
#[test]
fn dns_preference_orders_addresses() {
let ipv4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0);
let mut addresses = [ipv6, ipv4];
sort_addresses(&mut addresses, false);
assert_eq!(addresses, [ipv4, ipv6]);
sort_addresses(&mut addresses, true);
assert_eq!(addresses, [ipv6, ipv4]);
}
#[test] #[test]
fn dotted_decimal_loopback_normalizes() { fn dotted_decimal_loopback_normalizes() {
let ip = parse_to_ip("127.0.0.1").unwrap(); let ip = parse_to_ip("127.0.0.1").unwrap();

Loading…
Cancel
Save