fix(dns): preflight native rule bootstrap endpoints

This commit is contained in:
cfal
2026-10-31 11:00:53 +00:00
parent 994503cae0
commit a466981fbb
5 changed files with 148 additions and 7 deletions
+56 -3
View File
@@ -89,6 +89,7 @@ impl PoolState {
pub struct DnsRegistry {
limits: GlobalLimits,
groups: FxHashMap<String, Arc<dyn Resolver>>,
system_groups: std::collections::HashMap<String, bool>,
/// Default resolver, created lazily only if needed.
default_resolver: Option<Arc<dyn Resolver>>,
forbidden: Arc<[IpNetwork]>,
@@ -96,8 +97,11 @@ pub struct DnsRegistry {
}
impl DnsRegistry {
pub(crate) fn rule_bootstrap(&self, url: &str) -> std::io::Result<Arc<dyn Resolver>> {
pub(crate) async fn rule_bootstrap(&self, url: &str) -> std::io::Result<Arc<dyn Resolver>> {
if let Some(resolver) = self.get_by_name(url) {
if self.system_groups.get(url) == Some(&true) {
SystemResolver::check_native_configuration(self.forbidden.clone()).await?;
}
return Ok(real_only(resolver, self.forbidden.clone()));
}
let parsed = ParsedDnsUrl::parse(url)
@@ -111,7 +115,10 @@ impl DnsRegistry {
.to_parsed_server(None)
.map_err(std::io::Error::other)?;
let endpoint = match &server {
ParsedDnsServer::System => None,
ParsedDnsServer::System => {
SystemResolver::check_native_configuration(self.forbidden.clone()).await?;
None
}
ParsedDnsServer::Udp { addr }
| ParsedDnsServer::Tcp { addr }
| ParsedDnsServer::Tls { addr, .. }
@@ -199,6 +206,14 @@ impl DnsRegistry {
}
}
groups.retain(|group| required.contains(&group.name));
let mut system_groups = std::collections::HashMap::new();
for group in &mut groups {
let system = group_uses_system(group, &system_groups)?;
system_groups.insert(group.name.clone(), system);
}
if system_groups.values().any(|system| *system) {
SystemResolver::check_native_configuration(guard.forbidden.clone()).await?;
}
for group in &mut groups {
group.fake_ip = None;
for spec in &mut group.specs {
@@ -238,6 +253,7 @@ impl DnsRegistry {
Self {
limits: crate::resources::limits(),
groups: FxHashMap::default(),
system_groups: Default::default(),
default_resolver: None,
forbidden: Arc::from([]),
pools: FxHashMap::default(),
@@ -341,6 +357,41 @@ impl DnsRegistry {
}
}
fn group_uses_system(
group: &mut ExpandedDnsGroup,
system_groups: &std::collections::HashMap<String, bool>,
) -> std::io::Result<bool> {
if group.use_default_resolver {
return Ok(true);
}
for spec in &mut group.specs {
if spec.url == "system" {
return Ok(true);
}
let mut native_proxy = false;
for chain in &mut spec.client_chains {
for hop in chain.hops.iter_mut() {
crate::config::client_visit::visit_hop(hop, &mut |client| {
native_proxy |= client.address.address().hostname().is_some();
Ok(())
})?;
}
}
if native_proxy
|| (ParsedDnsUrl::parse(&spec.url).is_ok_and(|url| url.has_hostname())
&& spec.bootstrap_url.as_ref().is_none_or(|bootstrap| {
system_groups
.get(bootstrap)
.copied()
.unwrap_or(bootstrap == "system")
}))
{
return Ok(true);
}
}
Ok(false)
}
fn checkpoint_pools(pools: Vec<Arc<FakeIpPool>>) -> std::io::Result<()> {
let mut error = None;
for pool in pools {
@@ -881,7 +932,9 @@ pub async fn build_dns_registry_for_runtime(
};
let mut bootstraps = FxHashMap::default();
for group in groups {
for mut group in groups {
let system = group_uses_system(&mut group, &registry.system_groups)?;
registry.system_groups.insert(group.name.clone(), system);
for spec in &group.specs {
for endpoint in std::iter::once(spec.url.as_str()).chain(spec.bootstrap_url.as_deref())
{
+65
View File
@@ -154,6 +154,71 @@ async fn native_cache_policy_distinguishes_implicit_and_explicit_system() {
}
}
#[tokio::test]
async fn rule_system_bootstrap_checks_current_and_retained_forbidden_upstreams() {
let dir = tempfile::tempdir().unwrap();
let state = dir.path().join("fake.json");
let forbidden: Arc<[IpNetwork]> =
Arc::from(["0.0.0.0/0".parse().unwrap(), "::/0".parse().unwrap()]);
let guard = DnsRegistry {
forbidden: forbidden.clone(),
..DnsRegistry::new()
};
let source: crate::rule_set::RuleSource = serde_json::from_value(serde_json::json!({
"url": "https://remote.invalid/rules.json", "path": dir.path().join("cache.json"),
"download": {"client_chain": "direct", "bootstrap_url": "fake"}
}))
.unwrap();
for url in ["system", "https://upstream.invalid/dns-query"] {
let mut groups = fake_groups(true);
groups[0].specs = vec![make_spec(url)];
groups[0].fake_ip.as_mut().unwrap().state_file = Some(state.clone());
let error = DnsRegistry::for_rule_downloads(
groups,
GlobalLimits::default(),
std::slice::from_ref(&source),
&[],
Some(&guard),
)
.await
.err()
.unwrap();
assert!(error.to_string().contains("system DNS"), "{url}: {error}");
assert!(!state.exists());
}
let mut cached = build_dns_registry(fake_groups(false)).await.unwrap();
cached.forbidden = forbidden;
for bootstrap in ["system", "fake"] {
let error = cached.rule_bootstrap(bootstrap).await.err().unwrap();
assert!(
error.to_string().contains("system DNS"),
"{bootstrap}: {error}"
);
}
}
#[test]
fn system_bootstrap_dependencies_preserve_named_group_precedence() {
let mut groups = fake_groups(false);
let group = &mut groups[0];
let mut known = std::collections::HashMap::from([("system".into(), false)]);
group.specs[0] = make_spec("https://upstream.invalid/dns-query");
assert!(group_uses_system(group, &known).unwrap());
group.specs[0].bootstrap_url = Some("system".into());
assert!(!group_uses_system(group, &known).unwrap());
known.insert("system".into(), true);
assert!(group_uses_system(group, &known).unwrap());
group.specs[0] = make_spec("udp://127.0.0.1:53");
assert!(!group_uses_system(group, &known).unwrap());
group.specs[0].client_chains = vec![
serde_json::from_value(serde_json::json!({
"address": "proxy.invalid:1080", "protocol": {"type": "socks"}
}))
.unwrap(),
];
assert!(group_uses_system(group, &known).unwrap());
}
#[tokio::test]
async fn omitted_servers_keeps_default_cache_and_retired_pools_freeze_on_shutdown() {
let configs = crate::config::create_server_configs(
+23
View File
@@ -27,6 +27,29 @@ pub(super) struct SystemResolver {
}
impl SystemResolver {
pub(super) async fn check_native_configuration(forbidden: Arc<[IpNetwork]>) -> io::Result<()> {
if forbidden.is_empty() {
return Ok(());
}
tokio::task::spawn_blocking(move || {
let (mut config, _) = discover()?;
// Native lookup uses the OS configuration, not our filtered copy.
if config.name_servers.iter().any(|server| {
forbidden
.iter()
.any(|range| range.contains(server.ip.to_canonical()))
}) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"system DNS endpoint overlaps a synthetic endpoint",
));
}
filter_endpoints(&mut config, &forbidden)
})
.await
.map_err(io::Error::other)?
}
pub(super) async fn check_configuration(forbidden: Arc<[IpNetwork]>) -> io::Result<()> {
if crate::socket_policy::bootstrap().is_some() {
return Ok(());
+1 -1
View File
@@ -60,7 +60,7 @@ async fn connect(
url::Host::Ipv4(ip) => ip.to_string(),
url::Host::Ipv6(ip) => ip.to_string(),
};
let resolver = dns.rule_bootstrap(&download.bootstrap_url)?;
let resolver = dns.rule_bootstrap(&download.bootstrap_url).await?;
let target = target(url)?;
let targets = crate::resolver::resolve_addresses(&resolver, &target)
.await
+3 -3
View File
@@ -1679,8 +1679,8 @@ async fn cold_rule_bootstrap_rejects_synthetic_upstreams_before_lookup() {
.unwrap();
let error = dns
.rule_bootstrap(&bootstrap)
.err()
.expect("cold synthetic bootstrap endpoint accepted");
.await
.expect_err("cold synthetic bootstrap endpoint accepted");
assert!(
error.to_string().contains("synthetic endpoint"),
"{bootstrap}: {error}"
@@ -1754,7 +1754,7 @@ async fn remote_bootstrap_group_names_take_precedence_over_url_syntax() {
)
.await
.unwrap();
dns.rule_bootstrap(name).unwrap();
dns.rule_bootstrap(name).await.unwrap();
}
}