Skip to content

Commit 53a1f2e

Browse files
committed
refactor(cli): extract network primitives to runtime/network.rs
Moves cidr_contains and host_matches out of http_policy.rs into a dedicated runtime/network.rs module. Both are generic network-policy helpers that won't stay HTTP-specific: when raw TCP/UDP policy lands for wasi:sockets, it'll call the same primitives. Keeping them in a neutral module avoids circular or upward dependencies between http_policy and a future socket_policy. host_matches also picks up case-insensitive suffix comparison (the old version only case-folded the apex); 7 unit tests cover exact match, apex wildcard, subdomain wildcard, family-mismatch CIDRs, malformed CIDRs, and IPv6. Drops the duplicated host_matches_exact_and_wildcard and cidr_contains_basic tests from http_policy.rs — they're more thorough in network.rs now.
1 parent 8a11495 commit 53a1f2e

3 files changed

Lines changed: 84 additions & 30 deletions

File tree

act-cli/src/runtime/http_policy.rs

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use http::Uri;
3333
use wasmtime_wasi::TrappableError;
3434

3535
use crate::config::{HttpConfig, HttpRule, PolicyMode};
36+
use crate::runtime::network::{cidr_contains, host_matches};
3637

3738
type P2ErrorCode = wasmtime_wasi_http::p2::bindings::http::types::ErrorCode;
3839
type P3ErrorCode = wasmtime_wasi_http::p3::bindings::http::types::ErrorCode;
@@ -132,19 +133,6 @@ fn rule_matches(rule: &HttpRule, method: Option<&str>, uri: &Uri) -> bool {
132133
true
133134
}
134135

135-
fn host_matches(pattern: &str, host: &str) -> bool {
136-
if let Some(suffix) = pattern.strip_prefix("*.") {
137-
return host == suffix || host.ends_with(&format!(".{}", suffix));
138-
}
139-
host.eq_ignore_ascii_case(pattern)
140-
}
141-
142-
fn cidr_contains(spec: &str, ip: IpAddr) -> bool {
143-
spec.parse::<cidr::IpCidr>()
144-
.map(|c| c.contains(&ip))
145-
.unwrap_or(false)
146-
}
147-
148136
fn deny_reason(method: Option<&str>, uri: &Uri) -> String {
149137
format!("blocked by ACT policy: {} {}", method.unwrap_or("?"), uri)
150138
}
@@ -318,23 +306,6 @@ mod tests {
318306
PolicyHttpHooks::new(cfg)
319307
}
320308

321-
#[test]
322-
fn host_matches_exact_and_wildcard() {
323-
assert!(host_matches("api.example.com", "api.example.com"));
324-
assert!(!host_matches("api.example.com", "api2.example.com"));
325-
assert!(host_matches("*.example.com", "api.example.com"));
326-
assert!(host_matches("*.example.com", "example.com"));
327-
assert!(!host_matches("*.example.com", "api.other.com"));
328-
}
329-
330-
#[test]
331-
fn cidr_contains_basic() {
332-
assert!(cidr_contains("10.0.0.0/8", "10.1.2.3".parse().unwrap()));
333-
assert!(!cidr_contains("10.0.0.0/8", "11.0.0.0".parse().unwrap()));
334-
assert!(cidr_contains("127.0.0.0/8", "127.1.2.3".parse().unwrap()));
335-
assert!(cidr_contains("0.0.0.0/0", "8.8.8.8".parse().unwrap()));
336-
}
337-
338309
#[test]
339310
fn mode_deny_blocks_everything() {
340311
let h = hooks(HttpConfig {

act-cli/src/runtime/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use wasmtime_wasi_http::p3::WasiHttpCtxView;
1313
pub mod fs_matcher;
1414
pub mod fs_policy;
1515
pub mod http_policy;
16+
pub mod network;
1617

1718
// Generated bindings from WIT — fully auto-generated, no manual patching.
1819
#[allow(unused_mut, unused_variables, dead_code)]

act-cli/src/runtime/network.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! Network-policy primitives shared between HTTP and (upcoming) raw TCP/UDP
2+
//! policy paths. Keep this module free of HTTP- or socket-specific config
3+
//! types so it can be consumed from any direction — the `HttpConfig` matcher
4+
//! and a future `SocketConfig` matcher both end up calling the same
5+
//! `cidr_contains` / `host_matches` logic.
6+
7+
use std::net::IpAddr;
8+
9+
/// Does `cidr` (e.g. `"10.0.0.0/8"` or `"fc00::/7"`) contain `ip`? Parses via
10+
/// the `cidr` crate; returns `false` for malformed specs rather than
11+
/// panicking. Family mismatch (v4 rule vs v6 ip) is also `false`.
12+
pub fn cidr_contains(cidr: &str, ip: IpAddr) -> bool {
13+
cidr.parse::<cidr::IpCidr>()
14+
.map(|c| c.contains(&ip))
15+
.unwrap_or(false)
16+
}
17+
18+
/// Host-pattern match. Supports exact match (case-insensitive) and
19+
/// `*.suffix` wildcards. `*.example.com` matches both `example.com` and any
20+
/// subdomain `foo.example.com` / `a.b.example.com`.
21+
pub fn host_matches(pattern: &str, host: &str) -> bool {
22+
if let Some(suffix) = pattern.strip_prefix("*.") {
23+
return host.eq_ignore_ascii_case(suffix)
24+
|| host
25+
.to_ascii_lowercase()
26+
.ends_with(&format!(".{}", suffix.to_ascii_lowercase()));
27+
}
28+
host.eq_ignore_ascii_case(pattern)
29+
}
30+
31+
#[cfg(test)]
32+
mod tests {
33+
use super::*;
34+
35+
#[test]
36+
fn cidr_basic_ipv4() {
37+
assert!(cidr_contains("10.0.0.0/8", "10.1.2.3".parse().unwrap()));
38+
assert!(!cidr_contains("10.0.0.0/8", "11.0.0.0".parse().unwrap()));
39+
assert!(cidr_contains("0.0.0.0/0", "8.8.8.8".parse().unwrap()));
40+
}
41+
42+
#[test]
43+
fn cidr_basic_ipv6() {
44+
assert!(cidr_contains("fc00::/7", "fc00::1".parse().unwrap()));
45+
assert!(!cidr_contains("fc00::/7", "2001:db8::1".parse().unwrap()));
46+
}
47+
48+
#[test]
49+
fn cidr_malformed_is_no_match() {
50+
assert!(!cidr_contains("not-a-cidr", "10.0.0.1".parse().unwrap()));
51+
assert!(!cidr_contains("10.0.0.0/99", "10.0.0.1".parse().unwrap()));
52+
}
53+
54+
#[test]
55+
fn cidr_family_mismatch_is_no_match() {
56+
assert!(!cidr_contains("10.0.0.0/8", "::1".parse().unwrap()));
57+
assert!(!cidr_contains("fc00::/7", "10.0.0.1".parse().unwrap()));
58+
}
59+
60+
#[test]
61+
fn host_exact_case_insensitive() {
62+
assert!(host_matches("api.example.com", "api.example.com"));
63+
assert!(host_matches("api.example.com", "API.EXAMPLE.COM"));
64+
assert!(!host_matches("api.example.com", "api2.example.com"));
65+
}
66+
67+
#[test]
68+
fn host_wildcard_matches_apex_and_subdomains() {
69+
assert!(host_matches("*.example.com", "example.com"));
70+
assert!(host_matches("*.example.com", "api.example.com"));
71+
assert!(host_matches("*.example.com", "a.b.example.com"));
72+
}
73+
74+
#[test]
75+
fn host_wildcard_rejects_unrelated_and_confusable_suffixes() {
76+
assert!(!host_matches("*.example.com", "api.other.com"));
77+
// Confusable: "evil.com" ending literally with ".example.com" shouldn't
78+
// match if it's "notexample.com", but "evil.example.com" should.
79+
assert!(!host_matches("*.example.com", "notexample.com"));
80+
assert!(!host_matches("*.example.com", "example.com.evil.com"));
81+
}
82+
}

0 commit comments

Comments
 (0)