mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 09:27:46 +08:00
fix(dns): unify provider resolution and bound SMTP and tunnel egress
Share provider DNS policy across WebSocket and connection probes, handle bracketed IPv6 literals, and preserve bounded address sets for outbound clients. Bound SMTP DNS and TCP setup with multi-address fallback. Add opt-in trusted proxy DNS for tunnel upstreams while retaining default IP ACLs and origin isolation. Document DNS policy boundaries and verify 809 gateway, tunnel, and HTTP regression tests.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Maximum number of addresses accepted from one hostname lookup.
|
||||
@@ -13,6 +13,16 @@ pub const MAX_DNS_RESOLVED_ADDRESSES: usize = 32;
|
||||
/// Upper bound used by callers that do not have a tighter request deadline.
|
||||
pub const DEFAULT_DNS_LOOKUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub fn parse_ip_literal_host(host: &str) -> Option<IpAddr> {
|
||||
host.parse().ok().or_else(|| {
|
||||
host.strip_prefix('[')?
|
||||
.strip_suffix(']')?
|
||||
.parse::<Ipv6Addr>()
|
||||
.ok()
|
||||
.map(IpAddr::V6)
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve a host while bounding both resolver wait time and answer count.
|
||||
///
|
||||
/// The iterator is consumed one item past the allowed count so an answer set
|
||||
@@ -30,6 +40,9 @@ pub async fn lookup_host_with_limits(
|
||||
"DNS lookup timeout must be non-zero",
|
||||
));
|
||||
}
|
||||
if let Some(ip) = parse_ip_literal_host(host) {
|
||||
return Ok(vec![SocketAddr::new(ip, port)]);
|
||||
}
|
||||
|
||||
let mut resolved = tokio::time::timeout(timeout, tokio::net::lookup_host((host, port)))
|
||||
.await
|
||||
@@ -59,10 +72,32 @@ mod tests {
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use super::{
|
||||
collect_resolved_addresses_with_limit, lookup_host_with_limits, DEFAULT_DNS_LOOKUP_TIMEOUT,
|
||||
MAX_DNS_RESOLVED_ADDRESSES,
|
||||
collect_resolved_addresses_with_limit, lookup_host_with_limits, parse_ip_literal_host,
|
||||
DEFAULT_DNS_LOOKUP_TIMEOUT, MAX_DNS_RESOLVED_ADDRESSES,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ip_literal_parser_does_not_turn_bracketed_names_into_hosts() {
|
||||
for host in [
|
||||
"localhost",
|
||||
"[localhost]",
|
||||
"[127.0.0.1]",
|
||||
"[::1",
|
||||
"::1]",
|
||||
"[[::1]]",
|
||||
] {
|
||||
assert_eq!(parse_ip_literal_host(host), None, "{host}");
|
||||
}
|
||||
for (host, expected) in [
|
||||
("198.18.78.41", "198.18.78.41"),
|
||||
("::1", "::1"),
|
||||
("[::1]", "::1"),
|
||||
("[::ffff:127.0.0.1]", "::ffff:127.0.0.1"),
|
||||
] {
|
||||
assert_eq!(parse_ip_literal_host(host), Some(expected.parse().unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_zero_dns_timeout_before_resolving() {
|
||||
let error = lookup_host_with_limits("localhost", 80, std::time::Duration::ZERO)
|
||||
@@ -71,6 +106,16 @@ mod tests {
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolves_bracketed_ipv6_without_a_hostname_lookup() {
|
||||
for host in ["[::1]", "[2606:4700:4700::1111]", "[fd00::1]"] {
|
||||
let addresses = lookup_host_with_limits(host, 8443, DEFAULT_DNS_LOOKUP_TIMEOUT)
|
||||
.await
|
||||
.expect("URL-form IPv6 literals must not be sent to DNS");
|
||||
assert_eq!(addresses, vec![format!("{host}:8443").parse().unwrap()]);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolves_within_shared_address_bound() {
|
||||
let addresses = lookup_host_with_limits("localhost", 80, DEFAULT_DNS_LOOKUP_TIMEOUT)
|
||||
@@ -80,6 +125,18 @@ mod tests {
|
||||
assert!(addresses.len() <= MAX_DNS_RESOLVED_ADDRESSES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_every_answer_at_the_shared_bound() {
|
||||
let expected = (0..MAX_DNS_RESOLVED_ADDRESSES)
|
||||
.map(|index| SocketAddr::from(([198, 18, 0, index as u8], 443)))
|
||||
.collect::<Vec<_>>();
|
||||
let mut resolved = expected.clone().into_iter();
|
||||
assert_eq!(
|
||||
collect_resolved_addresses_with_limit(&mut resolved).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_answer_set_larger_than_the_shared_bound() {
|
||||
let mut resolved = (0..=MAX_DNS_RESOLVED_ADDRESSES)
|
||||
|
||||
@@ -7,7 +7,10 @@ mod retry;
|
||||
|
||||
pub use client::{apply_http_client_config, build_http_client, build_http_client_with_headers};
|
||||
pub use config::{HttpClientConfig, HttpRetryConfig};
|
||||
pub use dns::{lookup_host_with_limits, DEFAULT_DNS_LOOKUP_TIMEOUT, MAX_DNS_RESOLVED_ADDRESSES};
|
||||
pub use dns::{
|
||||
lookup_host_with_limits, parse_ip_literal_host, DEFAULT_DNS_LOOKUP_TIMEOUT,
|
||||
MAX_DNS_RESOLVED_ADDRESSES,
|
||||
};
|
||||
pub use header_security::{
|
||||
connection_declared_header_names, is_https_or_loopback_http_url, is_ipv4_benchmarking_fake_ip,
|
||||
is_private_or_reserved_ip, url_has_literal_loopback_host,
|
||||
|
||||
Reference in New Issue
Block a user