diff --git a/src/client_proxy_chain.rs b/src/client_proxy_chain.rs index b87abb7d..a88a16ed 100644 --- a/src/client_proxy_chain.rs +++ b/src/client_proxy_chain.rs @@ -708,8 +708,7 @@ impl ClientProxyChain { .await } - #[cfg(unix)] - pub(crate) async fn connect_rule_download( + pub(crate) async fn connect_tcp_pinned( &self, target: ResolvedLocation, resolver: &Arc, @@ -739,7 +738,7 @@ impl ClientProxyChain { .ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::NotFound, - "rule download proxy resolved to no addresses", + "proxy resolved to no addresses", ) })?; pinned.insert( @@ -1231,6 +1230,17 @@ impl ClientChainGroup { .await } + pub(crate) async fn connect_tcp_pinned( + &self, + remote_location: ResolvedLocation, + resolver: &Arc, + ) -> std::io::Result { + let index = self.next_tcp_index.fetch_add(1, Ordering::Relaxed) as usize; + self.chains[index % self.chains.len()] + .connect_tcp_pinned(remote_location, resolver) + .await + } + pub(crate) async fn connect_tcp_with_info( &self, remote_location: ResolvedLocation, diff --git a/src/dns/builder.rs b/src/dns/builder.rs index fb5c1055..57aa396f 100644 --- a/src/dns/builder.rs +++ b/src/dns/builder.rs @@ -375,23 +375,22 @@ fn group_uses_system( if spec.url == "system" { return Ok(true); } - let mut native_proxy = false; + let mut proxy_hostname = 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(); + proxy_hostname |= 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") - })) + if (proxy_hostname || 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); } diff --git a/src/dns/builder/tests.rs b/src/dns/builder/tests.rs index b6a8d198..913f7489 100644 --- a/src/dns/builder/tests.rs +++ b/src/dns/builder/tests.rs @@ -217,6 +217,30 @@ fn system_bootstrap_dependencies_preserve_named_group_precedence() { .unwrap(), ]; assert!(group_uses_system(group, &known).unwrap()); + group.specs[0].bootstrap_url = Some("udp://127.0.0.1:53".into()); + 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(), false); + assert!(!group_uses_system(group, &known).unwrap()); +} + +#[tokio::test] +async fn explicit_proxy_bootstrap_does_not_require_native_dns_preflight() { + let mut groups = fake_groups(false); + let spec = &mut groups[0].specs[0]; + spec.url = "tcp://127.0.0.1:53".into(); + spec.bootstrap_url = Some("udp://127.0.0.1:54".into()); + spec.client_chains = vec![ + serde_json::from_value(serde_json::json!({ + "address": "proxy.invalid:1080", "protocol": {"type": "socks"} + })) + .unwrap(), + ]; + let mut registry = build_dns_registry(groups).await.unwrap(); + registry.forbidden = Arc::from(["0.0.0.0/0".parse().unwrap(), "::/0".parse().unwrap()]); + assert!(registry.rule_bootstrap("fake").await.is_ok()); + assert!(registry.rule_bootstrap("system").await.is_err()); } #[tokio::test] diff --git a/src/dns/proxy_runtime.rs b/src/dns/proxy_runtime.rs index 3d2a3d15..3d28ecd4 100644 --- a/src/dns/proxy_runtime.rs +++ b/src/dns/proxy_runtime.rs @@ -24,7 +24,7 @@ const TEST_CONNECT_TIMEOUT: Duration = Duration::from_millis(100); pub struct ProxyRuntimeProvider { chain_group: Arc, /// Resolver for proxy server hostnames (not the DNS queries themselves). - /// Uses NativeResolver since we can't use the DNS server we're trying to reach. + /// Uses the explicit bootstrap route, or native DNS when none was configured. bootstrap_resolver: Arc, /// QUIC socket binder that selects from the same direct-only chain. quic_binder: ProxyQuicBinder, @@ -106,7 +106,7 @@ impl RuntimeProvider for ProxyRuntimeProvider { let target = NetLocation::new(address, server_addr.port()); let started = std::time::Instant::now(); - let connect_future = chain_group.connect_tcp(target.into(), &resolver); + let connect_future = chain_group.connect_tcp_pinned(target.into(), &resolver); match tokio::time::timeout(timeout, connect_future).await { Ok(Ok(result)) => { log::debug!( diff --git a/src/dns/proxy_runtime/tests.rs b/src/dns/proxy_runtime/tests.rs index 4b90ba4b..50949e56 100644 --- a/src/dns/proxy_runtime/tests.rs +++ b/src/dns/proxy_runtime/tests.rs @@ -6,6 +6,53 @@ use crate::tcp::chain_builder::build_direct_chain_group; use crate::tcp::socket_connector::SocketConnector; use async_trait::async_trait; +#[tokio::test] +async fn synthetic_intermediate_proxy_is_rejected_before_socks_connect() { + #[derive(Debug)] + struct Bootstrap; + impl Resolver for Bootstrap { + fn resolve_location(&self, location: &NetLocation) -> crate::resolver::ResolveFuture { + assert_eq!(location.address().to_string(), "second.invalid"); + let address = SocketAddr::new("198.18.0.53".parse().unwrap(), location.port()); + Box::pin(async move { Ok(vec![address]) }) + } + } + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let resolver = crate::dns::real_only( + Arc::new(Bootstrap), + Arc::from(["198.18.0.0/15".parse().unwrap()]), + ); + let chain: crate::config::ClientChain = serde_json::from_value(serde_json::json!([ + {"address": listener.local_addr().unwrap().to_string(), "protocol": {"type": "socks"}}, + {"address": "second.invalid:1080", "protocol": {"type": "socks"}} + ])) + .unwrap(); + let chains = crate::tcp::chain_builder::build_client_chain_group_with_limits( + crate::option_util::NoneOrSome::One(chain), + resolver.clone(), + Default::default(), + ) + .unwrap(); + let provider = ProxyRuntimeProvider::with_bootstrap( + Arc::new(chains), + resolver, + Duration::from_secs(1), + ); + for _ in 0..2 { + let error = provider + .connect_tcp("127.0.0.1:53".parse().unwrap(), None, None) + .await + .err() + .unwrap(); + assert!(error.to_string().contains("synthetic"), "{error}"); + assert!( + tokio::time::timeout(Duration::from_millis(50), listener.accept()) + .await + .is_err() + ); + } +} + #[derive(Debug)] struct PendingSocketConnector; diff --git a/src/rule_updater/http.rs b/src/rule_updater/http.rs index a780758f..8bcdb60e 100644 --- a/src/rule_updater/http.rs +++ b/src/rule_updater/http.rs @@ -110,7 +110,7 @@ async fn connect( let chain = &attempt.chain; let connected = crate::tcp::socket_connector_impl::connect_tcp_candidates(targets, |target| { let target = crate::address::NetLocation::from_ip_addr(target.ip(), target.port()); - Ok(chain.connect_rule_download(target.into(), &resolver)) + Ok(chain.connect_tcp_pinned(target.into(), &resolver)) }) .await .map_err(|_| invalid("configured rule download chain failed"))?;