Skip to content

Commit 45b0a2e

Browse files
committed
feat(cli): surface deny-CIDR as DnsError and observe resolver decisions
When PolicyDnsResolver filters out every resolved address, reqwest wraps the resulting error through a connect-kind layer so `err.is_connect()` fires first and the guest sees ConnectionRefused. Walk the whole source() chain up front and map to DnsError when any hop mentions dns / failed to lookup / deny CIDR — policy denials are now attributable rather than looking like a refused socket. Mirror the fix on p3. Add a resolved/kept tracing::debug line to PolicyDnsResolver::resolve so the count is inspectable under RUST_LOG=act=debug. Drop default-features from wasmtime-wasi (explicit p2 + p3 only); carries over from removing wasmtime-wasi-http's default-send-request earlier in the reqwest switchover.
1 parent b668ef3 commit 45b0a2e

3 files changed

Lines changed: 46 additions & 84 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

act-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ url = "2"
4646
shellexpand = "3"
4747
wasmparser.workspace = true
4848
wasmtime = { version = "43", features = ["component-model", "component-model-async"] }
49-
wasmtime-wasi = { version = "43", features = ["p3"] }
49+
wasmtime-wasi = { version = "43", default-features = false, features = ["p2", "p3"] }
5050
wasmtime-wasi-http = { version = "43", default-features = false, features = ["p2", "p3"] }
5151
http = "1"
5252
http-body-util = "0.1"

act-cli/src/runtime/http_client.rs

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,22 @@ impl Resolve for PolicyDnsResolver {
4545
fn resolve(&self, name: Name) -> Resolving {
4646
let deny = self.deny_nets.clone();
4747
Box::pin(async move {
48-
// Do a real resolve via tokio (what reqwest's default resolver does).
49-
let addrs = tokio::net::lookup_host(format!("{}:0", name.as_str()))
48+
let host = name.as_str().to_string();
49+
let addrs = tokio::net::lookup_host(format!("{host}:0"))
5050
.await
5151
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
52-
let filtered: Vec<SocketAddr> = addrs
52+
let all: Vec<SocketAddr> = addrs.collect();
53+
let total = all.len();
54+
let filtered: Vec<SocketAddr> = all
55+
.into_iter()
5356
.filter(|addr| !network::any_deny_cidr_matches(&deny, addr.ip(), addr.port()))
5457
.collect();
58+
tracing::debug!(
59+
%host,
60+
resolved = total,
61+
kept = filtered.len(),
62+
"http policy dns resolve",
63+
);
5564
if filtered.is_empty() {
5665
return Err("all resolved addresses matched a deny CIDR".into());
5766
}
@@ -272,11 +281,35 @@ async fn reqwest_response_to_hyper(
272281
.map_err(|_| P2ErrorCode::HttpProtocolError)
273282
}
274283

284+
/// Walk the whole `source()` chain of a reqwest error, returning the first
285+
/// chain entry whose display string matches `needle`. reqwest wraps DNS
286+
/// resolver errors through multiple layers (reqwest → hyper-util → our
287+
/// `PolicyDnsResolver` error) so a single `.source()` hop isn't enough.
288+
fn error_chain_contains(err: &dyn Error, needles: &[&str]) -> bool {
289+
let mut current: Option<&dyn Error> = Some(err);
290+
while let Some(e) = current {
291+
let msg = e.to_string().to_ascii_lowercase();
292+
if needles.iter().any(|n| msg.contains(n)) {
293+
return true;
294+
}
295+
current = e.source();
296+
}
297+
false
298+
}
299+
275300
/// Translate a reqwest error to the closest wasi:http/types::ErrorCode.
276301
fn reqwest_to_p2_error(err: reqwest::Error) -> P2ErrorCode {
277302
if err.is_timeout() {
278303
return P2ErrorCode::ConnectionTimeout;
279304
}
305+
if error_chain_contains(&err, &["deny cidr", "failed to lookup", "dns"]) {
306+
return P2ErrorCode::DnsError(
307+
wasmtime_wasi_http::p2::bindings::http::types::DnsErrorPayload {
308+
rcode: Some(err.to_string()),
309+
info_code: None,
310+
},
311+
);
312+
}
280313
if err.is_connect() {
281314
return P2ErrorCode::ConnectionRefused;
282315
}
@@ -295,17 +328,6 @@ fn reqwest_to_p2_error(err: reqwest::Error) -> P2ErrorCode {
295328
if err.is_body() {
296329
return P2ErrorCode::HttpRequestBodySize(None);
297330
}
298-
if let Some(src) = err.source() {
299-
let msg = src.to_string();
300-
if msg.contains("dns") || msg.contains("failed to lookup") || msg.contains("deny CIDR") {
301-
return P2ErrorCode::DnsError(
302-
wasmtime_wasi_http::p2::bindings::http::types::DnsErrorPayload {
303-
rcode: Some(msg),
304-
info_code: None,
305-
},
306-
);
307-
}
308-
}
309331
P2ErrorCode::HttpProtocolError
310332
}
311333

@@ -363,6 +385,14 @@ fn reqwest_to_p3_error(err: reqwest::Error) -> P3ErrorCode {
363385
if err.is_timeout() {
364386
return P3ErrorCode::ConnectionTimeout;
365387
}
388+
if error_chain_contains(&err, &["deny cidr", "failed to lookup", "dns"]) {
389+
return P3ErrorCode::DnsError(
390+
wasmtime_wasi_http::p3::bindings::http::types::DnsErrorPayload {
391+
rcode: Some(err.to_string()),
392+
info_code: None,
393+
},
394+
);
395+
}
366396
if err.is_connect() {
367397
return P3ErrorCode::ConnectionRefused;
368398
}

0 commit comments

Comments
 (0)