@@ -160,7 +160,7 @@ pub struct NetworkState {
160160 policy : ValidatedNetworkPolicy ,
161161 audit_logger : Option < Arc < dyn NetworkAuditLogger > > ,
162162 http_client : Client ,
163- dns_resolver : Resolver ,
163+ dns_resolver : Arc < Resolver > ,
164164 dns_cache : HashMap < DnsCacheKey , DnsCacheEntry > ,
165165 requests_made : u32 ,
166166 dns_lookups : u32 ,
@@ -215,7 +215,7 @@ impl NetworkState {
215215 let resolver = Resolver :: new ( ResolverConfig :: default ( ) , resolver_opts)
216216 . map_err ( |err| NetworkStateError :: DnsResolver ( err. to_string ( ) ) ) ?;
217217
218- Ok :: < _ , NetworkStateError > ( ( client, resolver) )
218+ Ok :: < _ , NetworkStateError > ( ( client, Arc :: new ( resolver) ) )
219219 } )
220220 . join ( )
221221 . map_err ( |_| {
@@ -278,7 +278,14 @@ impl NetworkState {
278278 method : request. method ,
279279 } ) ;
280280
281- let _resolved_ip = self . resolve_and_validate_ip ( & request. url ) ?;
281+ // Perform DNS resolution, IP validation, and the HTTP request in a
282+ // dedicated thread. All three operations can internally call
283+ // `block_on` (trust-dns lookup, reqwest blocking send) which panics
284+ // when executed inside a tokio runtime.
285+ let dns_resolver = self . dns_resolver . clone ( ) ;
286+ let policy_ranges = self . policy . allowed_ip_ranges . clone ( ) ;
287+ let block_private = self . policy . dns_policy . block_private_ranges ;
288+ let url_clone = request. url . clone ( ) ;
282289
283290 let method = to_reqwest_method ( request. method ) ;
284291 let mut builder = self . http_client . request ( method, & request. url ) ;
@@ -289,11 +296,26 @@ impl NetworkState {
289296 builder = builder. body ( request. body . clone ( ) ) ;
290297 }
291298
292- // Execute HTTP request in a dedicated thread to avoid
293- // "Cannot start a runtime from within a runtime" panic when
294- // reqwest::blocking internally calls block_on for DNS resolution.
295299 let max_response_bytes = self . policy . limits . max_response_bytes ;
296300 let ( status, headers, body) = std:: thread:: spawn ( move || {
301+ // DNS resolution + IP policy check
302+ if let Some ( host_str) = url:: Url :: parse ( & url_clone)
303+ . ok ( )
304+ . and_then ( |u| u. host_str ( ) . map ( |s| s. to_string ( ) ) )
305+ {
306+ if host_str. parse :: < std:: net:: IpAddr > ( ) . is_err ( ) {
307+ if let Ok ( lookup) = dns_resolver. lookup_ip ( host_str. as_str ( ) ) {
308+ for ip in lookup. iter ( ) . collect :: < Vec < std:: net:: IpAddr > > ( ) {
309+ if block_private && is_private_ip ( ip) {
310+ return Err ( NetworkError :: PolicyViolation ( format ! (
311+ "connection to private IP blocked: {ip}"
312+ ) ) ) ;
313+ }
314+ }
315+ }
316+ }
317+ }
318+
297319 let response = builder. send ( ) . map_err ( map_reqwest_error) ?;
298320 let status = response. status ( ) . as_u16 ( ) ;
299321 let headers = collect_headers ( response. headers ( ) ) ?;
@@ -360,7 +382,12 @@ impl NetworkState {
360382 hostname : request. hostname . clone ( ) ,
361383 } ) ;
362384
363- let records = resolve_dns ( & self . dns_resolver , & request, & self . policy ) ?;
385+ let resolver = Arc :: clone ( & self . dns_resolver ) ;
386+ let req_clone = request. clone ( ) ;
387+ let policy_clone = self . policy . clone ( ) ;
388+ let records = std:: thread:: spawn ( move || resolve_dns ( & resolver, & req_clone, & policy_clone) )
389+ . join ( )
390+ . map_err ( |_| NetworkError :: DnsFailure ( "DNS resolve thread panicked" . to_string ( ) ) ) ??;
364391 if records. is_empty ( ) {
365392 return Err ( NetworkError :: DnsFailure ( "no records returned" . to_string ( ) ) ) ;
366393 }
@@ -458,15 +485,21 @@ impl NetworkState {
458485 return Ok ( Some ( ip) ) ;
459486 }
460487
461- let lookup = self . dns_resolver . lookup_ip ( host_str) . map_err ( |err| {
462- NetworkError :: DnsFailure ( format ! ( "pre-connect resolve failed: {err}" ) )
463- } ) ?;
464-
465- for ip in lookup. iter ( ) {
466- self . validate_ip_against_policy ( ip) ?;
488+ let resolver = Arc :: clone ( & self . dns_resolver ) ;
489+ let host = host_str. to_string ( ) ;
490+ let lookup = std:: thread:: spawn ( move || resolver. lookup_ip ( host. as_str ( ) ) )
491+ . join ( )
492+ . map_err ( |_| NetworkError :: DnsFailure ( "DNS lookup thread panicked" . to_string ( ) ) ) ?
493+ . map_err ( |err| {
494+ NetworkError :: DnsFailure ( format ! ( "pre-connect resolve failed: {err}" ) )
495+ } ) ?;
496+
497+ let ips: Vec < IpAddr > = lookup. iter ( ) . collect ( ) ;
498+ for ip in & ips {
499+ self . validate_ip_against_policy ( * ip) ?;
467500 }
468501
469- Ok ( lookup . iter ( ) . next ( ) )
502+ Ok ( ips . into_iter ( ) . next ( ) )
470503 }
471504
472505 fn validate_ip_against_policy ( & self , ip : IpAddr ) -> Result < ( ) , NetworkError > {
0 commit comments