/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/fetch/dns.rs
401 строка
12 KB
Bartek Iwańczuk
refactor(ext/fetch): check net deny list against connected peer addr (#34418)
17 июн 2026, 13:28
Не верифицирован
17 июн 2026, 13:28
e4444d5
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::future::Future; use std::io; use std::net::IpAddr; use std::net::SocketAddr; use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; use std::task::Poll; use std::task::{self}; use std::vec; use deno_permissions::PermissionsContainer; use hickory_resolver::name_server::TokioConnectionProvider; use http::Uri; use http::uri::Scheme; use hyper_util::client::legacy::connect::HttpConnector; use hyper_util::client::legacy::connect::dns::GaiResolver; use hyper_util::client::legacy::connect::dns::Name; use hyper_util::rt::TokioIo; use tokio::net::TcpStream; use tokio::task::JoinHandle; use tower::Service; #[derive(Clone, Debug)] pub struct Resolver { kind: ResolverKind, } #[allow(clippy::large_enum_variant, reason = "TODO: investigate")] #[derive(Clone, Debug)] enum ResolverKind { /// A resolver using blocking `getaddrinfo` calls in a threadpool. Gai(GaiResolver), /// hickory-resolver's userspace resolver. Hickory(hickory_resolver::Resolver<TokioConnectionProvider>), /// A custom resolver that implements `Resolve`. Custom(Arc<dyn Resolve>), } /// Alias for the `Future` type returned by a custom DNS resolver. // The future has to be `Send` as `tokio::spawn` is used to execute the future. pub type Resolving = Pin<Box<dyn Future<Output = Result<SocketAddrs, io::Error>> + Send>>; /// A trait for customizing DNS resolution in ext/fetch. // The resolver needs to be `Send` and `Sync` for two reasons. One is it is // wrapped inside an `Arc` and will be cloned and moved to an async block to // perfrom DNS resolution. That async block will be executed by `tokio::spawn`, // so to make that async block `Send`, `Arc<dyn Resolve>` needs to be // `Send`. The other is `Resolver` needs to be `Send` to make the wrapping // `HttpConnector` `Send`. pub trait Resolve: Send + Sync + std::fmt::Debug { fn resolve(&self, name: Name) -> Resolving; } impl Default for Resolver { fn default() -> Self { Self::gai() } } impl Resolver { pub fn gai() -> Self { Self { kind: ResolverKind::Gai(GaiResolver::new()), } } /// Create a [`AsyncResolver`] from system conf. pub fn hickory() -> Result<Self, hickory_resolver::ResolveError> { Ok(Self { kind: ResolverKind::Hickory( hickory_resolver::Resolver::builder_tokio()?.build(), ), }) } pub fn hickory_from_resolver( resolver: hickory_resolver::Resolver<TokioConnectionProvider>, ) -> Self { Self { kind: ResolverKind::Hickory(resolver), } } pub fn custom(resolver: Arc<dyn Resolve>) -> Self { Self { kind: ResolverKind::Custom(resolver), } } } type SocketAddrs = vec::IntoIter<SocketAddr>; pub struct ResolveFut { inner: JoinHandle<Result<SocketAddrs, io::Error>>, } impl Future for ResolveFut { type Output = Result<SocketAddrs, io::Error>; fn poll( mut self: Pin<&mut Self>, cx: &mut task::Context<'_>, ) -> Poll<Self::Output> { Pin::new(&mut self.inner).poll(cx).map(|res| match res { Ok(Ok(addrs)) => Ok(addrs), Ok(Err(e)) => Err(e), Err(join_err) => { if join_err.is_cancelled() { Err(io::Error::new(io::ErrorKind::Interrupted, join_err)) } else { Err(io::Error::other(join_err)) } } }) } } impl Service<Name> for Resolver { type Response = SocketAddrs; type Error = io::Error; type Future = ResolveFut; fn poll_ready( &mut self, _cx: &mut task::Context<'_>, ) -> Poll<Result<(), io::Error>> { Poll::Ready(Ok(())) } fn call(&mut self, name: Name) -> Self::Future { let task = match &mut self.kind { ResolverKind::Gai(gai_resolver) => { let mut resolver = gai_resolver.clone(); tokio::spawn(async move { let result = resolver.call(name).await?; Ok(result.collect::<Vec<_>>().into_iter()) }) } ResolverKind::Hickory(async_resolver) => { let resolver = async_resolver.clone(); tokio::spawn(async move { let result = resolver.lookup_ip(name.as_str()).await?; let addrs: Vec<_> = result.into_iter().map(|x| SocketAddr::new(x, 0)).collect(); Ok(addrs.into_iter()) }) } ResolverKind::Custom(resolver) => { let resolver = resolver.clone(); tokio::spawn(async move { resolver.resolve(name).await }) } }; ResolveFut { inner: task } } } /// `Service<Uri>` adapter that runs the net-deny permission check on every /// resolved address *before* a socket is opened. /// /// hyper-util's `HttpConnector` only hands the hostname to its DNS resolver /// and applies the destination port after resolution, so the resolver alone /// can't run the port-aware deny check. This wrapper sees the full `Uri` /// (and thus the real port) and owns the resolver: it resolves the hostname /// itself, checks every resolved address (denying before any connection is /// made, like `Deno.connect` in `ext/net/ops.rs`), and then hands the vetted /// addresses to the inner connector through a pre-resolved resolver, so the /// connection goes to exactly the addresses that were checked and no second /// DNS query can race with a record change. #[derive(Clone, Debug)] pub struct PermissionedHttpConnector { resolver: Resolver, local_address: Option<IpAddr>, permissions: Option<PermissionsContainer>, } impl PermissionedHttpConnector { pub fn new( resolver: Resolver, local_address: Option<IpAddr>, permissions: Option<PermissionsContainer>, ) -> Self { Self { resolver, local_address, permissions, } } fn http_connector(&self, resolver: Resolver) -> HttpConnector<Resolver> { let mut connector = HttpConnector::new_with_resolver(resolver); connector.enforce_http(false); connector.set_local_address(self.local_address); connector } } /// Resolver handing out a fixed, already-permission-checked set of addresses. #[derive(Debug)] struct PreResolved(Vec<SocketAddr>); impl Resolve for PreResolved { fn resolve(&self, _name: Name) -> Resolving { let addrs = self.0.clone(); Box::pin(async move { Ok(addrs.into_iter()) }) } } type BoxError = Box<dyn std::error::Error + Send + Sync>; /// Mirrors the `ConnectError` shape hyper-util's `HttpConnector` uses for /// resolution failures, so error messages keep the same /// `client error (Connect): dns error: ...` format now that resolution /// happens in [`PermissionedHttpConnector`] instead. #[derive(Debug)] struct DnsError(io::Error); impl std::fmt::Display for DnsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("dns error") } } impl std::error::Error for DnsError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) } } fn check_resolved( permissions: &PermissionsContainer, ip: &IpAddr, port: u16, ) -> Result<(), BoxError> { permissions .clone() .check_net_resolved(ip, port, "fetch()") .map_err(|e| { io::Error::new(io::ErrorKind::PermissionDenied, e.to_string()).into() }) } /// Extracts the connection host (with IPv6 brackets stripped) and the /// effective destination port from a `Uri`, defaulting the port from the /// scheme. Returns `None` if the `Uri` has no host. fn bare_host_and_port(uri: &Uri) -> Option<(&str, u16)> { let host = uri.host()?; let port = uri.port_u16().unwrap_or_else(|| { if uri.scheme() == Some(&Scheme::HTTPS) { 443 } else { 80 } }); let bare_host = host .strip_prefix('[') .and_then(|h| h.strip_suffix(']')) .unwrap_or(host); Some((bare_host, port)) } impl Service<Uri> for PermissionedHttpConnector { type Response = TokioIo<TcpStream>; type Error = BoxError; type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; fn poll_ready( &mut self, _cx: &mut task::Context<'_>, ) -> Poll<Result<(), Self::Error>> { Poll::Ready(Ok(())) } fn call(&mut self, uri: Uri) -> Self::Future { let this = self.clone(); Box::pin(async move { let Some(permissions) = &this.permissions else { let mut connector = this.http_connector(this.resolver.clone()); return connector.call(uri).await.map_err(Into::into); }; let Some((bare_host, port)) = bare_host_and_port(&uri) else { return Err( io::Error::new(io::ErrorKind::InvalidInput, "missing host in URI") .into(), ); }; if let Ok(ip) = bare_host.parse::<IpAddr>() { // IP literal: `HttpConnector` connects to it directly without // consulting the resolver. check_resolved(permissions, &ip, port)?; let mut connector = this.http_connector(this.resolver.clone()); return connector.call(uri).await.map_err(Into::into); } let name = Name::from_str(bare_host).map_err(|e| -> BoxError { io::Error::new(io::ErrorKind::InvalidInput, e.to_string()).into() })?; let addrs: Vec<SocketAddr> = this .resolver .clone() .call(name) .await .map_err(|e| -> BoxError { DnsError(e).into() })? .collect(); for addr in &addrs { check_resolved(permissions, &addr.ip(), port)?; } let mut connector = this.http_connector(Resolver::custom(Arc::new(PreResolved(addrs)))); connector.call(uri).await.map_err(Into::into) }) } } /// Lets a connector run a best-effort net-deny check against a destination /// `Uri` without opening a connection to it. /// /// Used by the proxy connector: when a request is routed through a proxy, the /// deny check the connector would otherwise run sees the connection to the /// *proxy*, not the destination, so an IP-level `--deny-net` rule would only be /// enforced against the destination's pre-resolution hostname. This resolves /// the destination and checks every resolved address too. /// /// It is best-effort: a proxy may be able to reach a host this process cannot /// resolve locally, so a resolution failure here is not fatal. The connection /// to the proxy itself is still checked separately. pub trait CheckDst { fn check_dst( &self, uri: Uri, ) -> Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>; } impl CheckDst for PermissionedHttpConnector { fn check_dst( &self, uri: Uri, ) -> Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>> { let this = self.clone(); Box::pin(async move { let Some(permissions) = &this.permissions else { return Ok(()); }; let Some((bare_host, port)) = bare_host_and_port(&uri) else { return Ok(()); }; if let Ok(ip) = bare_host.parse::<IpAddr>() { return check_resolved(permissions, &ip, port); } let Ok(name) = Name::from_str(bare_host) else { return Ok(()); }; // Best-effort: if the destination can't be resolved locally, let the // proxy handle it rather than failing the request. let Ok(addrs) = this.resolver.clone().call(name).await else { return Ok(()); }; for addr in addrs { check_resolved(permissions, &addr.ip(), port)?; } Ok(()) }) } } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; // A resolver that resolves any name into the same address. #[derive(Debug)] struct DebugResolver(SocketAddr); impl Resolve for DebugResolver { fn resolve(&self, _name: Name) -> Resolving { let addr = self.0; Box::pin(async move { Ok(vec![addr].into_iter()) }) } } #[tokio::test] async fn custom_dns_resolver() { let mut resolver = Resolver::custom(Arc::new(DebugResolver( "127.0.0.1:8080".parse().unwrap(), ))); let mut addr = resolver .call(Name::from_str("foo.com").unwrap()) .await .unwrap(); let addr = addr.next().unwrap(); assert_eq!(addr, "127.0.0.1:8080".parse().unwrap()); } }