From d1a0401aa7d5a95a801f8daaa24d01bdd31a2456 Mon Sep 17 00:00:00 2001 From: Sanjay Ramadugu Date: Sat, 22 Aug 2026 01:27:51 -0700 Subject: [PATCH 1/5] feat: buzz #6500 CGNAT allowlist in buzz-core Co-authored-by: Sanjay Ramadugu Signed-off-by: Sanjay Ramadugu --- .env.example | 3 + crates/buzz-core/src/network.rs | 180 +++++++++++++++++++++++++++ crates/buzz-workflow/src/executor.rs | 7 +- 3 files changed, 188 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 0f7bbba6f13..0d5870315bc 100644 --- a/.env.example +++ b/.env.example @@ -249,4 +249,7 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz # attestation are optional; configuring any one enables policy acceptance. # BUZZ_TERMS_OF_SERVICE_MARKDOWN="# Terms of Service\n\nFull terms here." # BUZZ_PRIVACY_POLICY_MARKDOWN="# Privacy Policy\n\nFull policy here." +# Comma-separated CIDR allowlist for workflow webhook SSRF check. Empty (default) blocks all private/reserved ranges. +# BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS=100.64.0.0/10,fd00::/8 + # BUZZ_AGE_ATTESTATION_REQUIRED=true diff --git a/crates/buzz-core/src/network.rs b/crates/buzz-core/src/network.rs index fb3718d58c5..462ab5efab0 100644 --- a/crates/buzz-core/src/network.rs +++ b/crates/buzz-core/src/network.rs @@ -43,6 +43,8 @@ fn embedded_ipv4(v6: &std::net::Ipv6Addr, prefix: &[u8; 12]) -> Option bool { match ip { std::net::IpAddr::V4(v4) => { @@ -94,6 +96,108 @@ pub fn is_private_ip(ip: &std::net::IpAddr) -> bool { } } +/// CIDR range for allowlist matching (hand-rolled, no extra dependency). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IpCidr { + /// Base address of the CIDR range. + pub base: std::net::IpAddr, + /// Prefix length (0-32 for IPv4, 0-128 for IPv6). + pub prefix_len: u8, +} + +impl IpCidr { + /// Returns true if `ip` is within this CIDR range. Family mismatch returns false. + pub fn contains(&self, ip: &std::net::IpAddr) -> bool { + match (&self.base, ip) { + (std::net::IpAddr::V4(b), std::net::IpAddr::V4(i)) => { + if self.prefix_len == 0 { + return true; + } + if self.prefix_len > 32 { + return false; + } + let mask = if self.prefix_len == 32 { + u32::MAX + } else { + u32::MAX << (32 - self.prefix_len) + }; + (u32::from(*b) & mask) == (u32::from(*i) & mask) + } + (std::net::IpAddr::V6(b), std::net::IpAddr::V6(i)) => { + if self.prefix_len == 0 { + return true; + } + if self.prefix_len > 128 { + return false; + } + let b = b.octets(); + let i = i.octets(); + let full = (self.prefix_len / 8) as usize; + if b[..full] != i[..full] { + return false; + } + let rem = self.prefix_len % 8; + if rem == 0 { + return true; + } + let mask: u8 = 0xFF << (8 - rem); + (b[full] & mask) == (i[full] & mask) + } + _ => false, + } + } +} + +/// Parse comma-separated CIDR spec into valid entries. Malformed entries are dropped with a warning. +pub fn parse_allowed_cidrs(spec: &str) -> Vec { + let mut out = Vec::new(); + for part in spec.split(',') { + let s = part.trim(); + if s.is_empty() { + continue; + } + let Some((addr_str, prefix_str)) = s.split_once('/') else { + eprintln!( + "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entry '{}'", + s + ); + continue; + }; + let Ok(prefix_len) = prefix_str.trim().parse::() else { + eprintln!( + "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entry '{}'", + s + ); + continue; + }; + let Ok(base) = addr_str.trim().parse::() else { + eprintln!( + "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entry '{}'", + s + ); + continue; + }; + let max = match base { + std::net::IpAddr::V4(_) => 32, + std::net::IpAddr::V6(_) => 128, + }; + if prefix_len > max { + eprintln!( + "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entry '{}'", + s + ); + continue; + } + out.push(IpCidr { base, prefix_len }); + } + out +} + +/// Returns true if `ip` is private/reserved and not covered by `allowed`. +pub fn is_blocked_ip(ip: &std::net::IpAddr, allowed: &[IpCidr]) -> bool { + is_private_ip(ip) && !allowed.iter().any(|c| c.contains(ip)) +} + #[cfg(test)] mod tests { use super::*; @@ -359,4 +463,80 @@ mod tests { // fe00:: — just below ff00::/8 (not multicast, not link-local, not ULA) assert!(!is_private_ip(&"fe00::1".parse::().unwrap())); } + + #[test] + fn test_allowlist_cgnat() { + let ip: IpAddr = "100.64.1.5".parse().unwrap(); + assert!(is_blocked_ip(&ip, &[])); + let allowed = parse_allowed_cidrs("100.64.0.0/10"); + assert!(!is_blocked_ip(&ip, &allowed)); + let near = parse_allowed_cidrs("100.65.0.0/16"); + assert!(is_blocked_ip(&ip, &near)); + } + + #[test] + fn test_allowlist_public_never_blocked() { + let ip: IpAddr = "93.184.216.34".parse().unwrap(); + assert!(!is_blocked_ip(&ip, &[])); + assert!(!is_blocked_ip(&ip, &parse_allowed_cidrs("100.64.0.0/10"))); + } + + #[test] + fn test_allowlist_one_range_does_not_open_others() { + let ip: IpAddr = "10.0.0.1".parse().unwrap(); + let allowed = parse_allowed_cidrs("100.64.0.0/10"); + assert!(is_blocked_ip(&ip, &allowed)); + } + + #[test] + fn test_allowlist_ipv6_ula() { + let ip: IpAddr = "fd00::1".parse().unwrap(); + assert!(is_blocked_ip(&ip, &[])); + assert!(!is_blocked_ip(&ip, &parse_allowed_cidrs("fd00::/8"))); + } + + #[test] + fn test_allowlist_family_mismatch() { + let v4: IpAddr = "10.0.0.1".parse().unwrap(); + let v6: IpAddr = "fd00::1".parse().unwrap(); + assert!(is_blocked_ip(&v4, &parse_allowed_cidrs("fd00::/8"))); + assert!(is_blocked_ip(&v6, &parse_allowed_cidrs("10.0.0.0/8"))); + } + + #[test] + fn test_allowlist_single_host() { + let ip: IpAddr = "10.0.0.1".parse().unwrap(); + let neigh: IpAddr = "10.0.0.2".parse().unwrap(); + let allowed = parse_allowed_cidrs("10.0.0.1/32"); + assert!(!is_blocked_ip(&ip, &allowed)); + assert!(is_blocked_ip(&neigh, &allowed)); + let v6: IpAddr = "fd00::1".parse().unwrap(); + let v6n: IpAddr = "fd00::2".parse().unwrap(); + let allowed6 = parse_allowed_cidrs("fd00::1/128"); + assert!(!is_blocked_ip(&v6, &allowed6)); + assert!(is_blocked_ip(&v6n, &allowed6)); + } + + #[test] + fn test_parse_drops_malformed() { + let v = parse_allowed_cidrs("10.0.0.0/8, garbage, 100.64.0.0/99, 100.64.0.0/10"); + assert_eq!(v.len(), 2); + } + + #[test] + fn test_parse_empty() { + assert!(parse_allowed_cidrs("").is_empty()); + assert!(parse_allowed_cidrs(" ").is_empty()); + // empty allowlist behaves like is_private_ip + for s in [ + "10.0.0.1", + "8.8.8.8", + "100.64.1.5", + "fd00::1", + "2606:4700::1", + ] { + let ip: IpAddr = s.parse().unwrap(); + assert_eq!(is_blocked_ip(&ip, &[]), is_private_ip(&ip)); + } + } } diff --git a/crates/buzz-workflow/src/executor.rs b/crates/buzz-workflow/src/executor.rs index 5c712dcff7c..b390239dd68 100644 --- a/crates/buzz-workflow/src/executor.rs +++ b/crates/buzz-workflow/src/executor.rs @@ -843,10 +843,13 @@ async fn check_ssrf(host: &str, port: u16) -> Result Date: Sat, 22 Aug 2026 01:27:51 -0700 Subject: [PATCH 2/5] feat: buzz #6500 wire allowlist via ssrf_verdict Co-authored-by: Sanjay Ramadugu Signed-off-by: Sanjay Ramadugu --- crates/buzz-workflow/src/executor.rs | 47 +++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/crates/buzz-workflow/src/executor.rs b/crates/buzz-workflow/src/executor.rs index b390239dd68..9e9d18e2594 100644 --- a/crates/buzz-workflow/src/executor.rs +++ b/crates/buzz-workflow/src/executor.rs @@ -843,18 +843,30 @@ async fn check_ssrf(host: &str, port: u16) -> Result Result { + let allowed = buzz_core::network::parse_allowed_cidrs(allowed_spec); + for ip in addrs { if buzz_core::network::is_blocked_ip(ip, &allowed) { return Err(WorkflowError::WebhookError(format!( "SSRF blocked: '{host}' resolved to private/reserved address {ip} (allow it with BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS)" ))); } } - - Ok(addrs[0]) + addrs + .first() + .copied() + .ok_or_else(|| WorkflowError::WebhookError("DNS resolution returned no addresses".into())) } /// Maximum response body size for webhook calls (1 MiB). @@ -1961,6 +1973,31 @@ mod tests { ); } + #[test] + fn ssrf_verdict_empty_blocks_cgnat() { + let ip: std::net::IpAddr = "100.64.1.5".parse().unwrap(); + assert!(ssrf_verdict("example.com", &[ip], "").is_err()); + } + + #[test] + fn ssrf_verdict_allows_cgnat_with_spec() { + let ip: std::net::IpAddr = "100.64.1.5".parse().unwrap(); + assert!(ssrf_verdict("example.com", &[ip], "100.64.0.0/10").is_ok()); + } + + #[test] + fn ssrf_verdict_still_blocks_other_private() { + let ip: std::net::IpAddr = "10.0.0.1".parse().unwrap(); + assert!(ssrf_verdict("example.com", &[ip], "100.64.0.0/10").is_err()); + } + + #[test] + fn ssrf_verdict_public_passes() { + let ip: std::net::IpAddr = "93.184.216.34".parse().unwrap(); + assert!(ssrf_verdict("example.com", &[ip], "").is_ok()); + assert!(ssrf_verdict("example.com", &[ip], "100.64.0.0/10").is_ok()); + } + #[test] fn send_message_canonicalizes_valid_explicit_override_for_global_workflow() { let override_channel_id = Uuid::new_v4(); From 97a7d71e5b579b792b69f2a4f5e1a6a55469a5a4 Mon Sep 17 00:00:00 2001 From: Sanjay Ramadugu Date: Sat, 22 Aug 2026 01:42:25 -0700 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20buzz=20#6500=20R4/R5/N2=20=E2=80=94?= =?UTF-8?q?=20cfg=20guard,=20parse=20returns=20dropped,=20warn=20in=20work?= =?UTF-8?q?flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sanjay Ramadugu Signed-off-by: Sanjay Ramadugu --- crates/buzz-core/src/network.rs | 57 +++++++++++++--------------- crates/buzz-workflow/src/executor.rs | 19 +++++++++- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/crates/buzz-core/src/network.rs b/crates/buzz-core/src/network.rs index 462ab5efab0..c2ba509032f 100644 --- a/crates/buzz-core/src/network.rs +++ b/crates/buzz-core/src/network.rs @@ -148,33 +148,26 @@ impl IpCidr { } } -/// Parse comma-separated CIDR spec into valid entries. Malformed entries are dropped with a warning. -pub fn parse_allowed_cidrs(spec: &str) -> Vec { +/// Parse comma-separated CIDR spec into valid entries. Malformed entries are dropped. +/// Returns the valid ranges and the dropped entry strings. +pub fn parse_allowed_cidrs(spec: &str) -> (Vec, Vec) { let mut out = Vec::new(); + let mut dropped = Vec::new(); for part in spec.split(',') { let s = part.trim(); if s.is_empty() { continue; } let Some((addr_str, prefix_str)) = s.split_once('/') else { - eprintln!( - "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entry '{}'", - s - ); + dropped.push(s.to_string()); continue; }; let Ok(prefix_len) = prefix_str.trim().parse::() else { - eprintln!( - "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entry '{}'", - s - ); + dropped.push(s.to_string()); continue; }; let Ok(base) = addr_str.trim().parse::() else { - eprintln!( - "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entry '{}'", - s - ); + dropped.push(s.to_string()); continue; }; let max = match base { @@ -182,15 +175,12 @@ pub fn parse_allowed_cidrs(spec: &str) -> Vec { std::net::IpAddr::V6(_) => 128, }; if prefix_len > max { - eprintln!( - "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entry '{}'", - s - ); + dropped.push(s.to_string()); continue; } out.push(IpCidr { base, prefix_len }); } - out + (out, dropped) } /// Returns true if `ip` is private/reserved and not covered by `allowed`. @@ -468,9 +458,9 @@ mod tests { fn test_allowlist_cgnat() { let ip: IpAddr = "100.64.1.5".parse().unwrap(); assert!(is_blocked_ip(&ip, &[])); - let allowed = parse_allowed_cidrs("100.64.0.0/10"); + let (allowed, _) = parse_allowed_cidrs("100.64.0.0/10"); assert!(!is_blocked_ip(&ip, &allowed)); - let near = parse_allowed_cidrs("100.65.0.0/16"); + let (near, _) = parse_allowed_cidrs("100.65.0.0/16"); assert!(is_blocked_ip(&ip, &near)); } @@ -478,13 +468,14 @@ mod tests { fn test_allowlist_public_never_blocked() { let ip: IpAddr = "93.184.216.34".parse().unwrap(); assert!(!is_blocked_ip(&ip, &[])); - assert!(!is_blocked_ip(&ip, &parse_allowed_cidrs("100.64.0.0/10"))); + let (allowed, _) = parse_allowed_cidrs("100.64.0.0/10"); + assert!(!is_blocked_ip(&ip, &allowed)); } #[test] fn test_allowlist_one_range_does_not_open_others() { let ip: IpAddr = "10.0.0.1".parse().unwrap(); - let allowed = parse_allowed_cidrs("100.64.0.0/10"); + let (allowed, _) = parse_allowed_cidrs("100.64.0.0/10"); assert!(is_blocked_ip(&ip, &allowed)); } @@ -492,41 +483,45 @@ mod tests { fn test_allowlist_ipv6_ula() { let ip: IpAddr = "fd00::1".parse().unwrap(); assert!(is_blocked_ip(&ip, &[])); - assert!(!is_blocked_ip(&ip, &parse_allowed_cidrs("fd00::/8"))); + let (allowed, _) = parse_allowed_cidrs("fd00::/8"); + assert!(!is_blocked_ip(&ip, &allowed)); } #[test] fn test_allowlist_family_mismatch() { let v4: IpAddr = "10.0.0.1".parse().unwrap(); let v6: IpAddr = "fd00::1".parse().unwrap(); - assert!(is_blocked_ip(&v4, &parse_allowed_cidrs("fd00::/8"))); - assert!(is_blocked_ip(&v6, &parse_allowed_cidrs("10.0.0.0/8"))); + let (a1, _) = parse_allowed_cidrs("fd00::/8"); + assert!(is_blocked_ip(&v4, &a1)); + let (a2, _) = parse_allowed_cidrs("10.0.0.0/8"); + assert!(is_blocked_ip(&v6, &a2)); } #[test] fn test_allowlist_single_host() { let ip: IpAddr = "10.0.0.1".parse().unwrap(); let neigh: IpAddr = "10.0.0.2".parse().unwrap(); - let allowed = parse_allowed_cidrs("10.0.0.1/32"); + let (allowed, _) = parse_allowed_cidrs("10.0.0.1/32"); assert!(!is_blocked_ip(&ip, &allowed)); assert!(is_blocked_ip(&neigh, &allowed)); let v6: IpAddr = "fd00::1".parse().unwrap(); let v6n: IpAddr = "fd00::2".parse().unwrap(); - let allowed6 = parse_allowed_cidrs("fd00::1/128"); + let (allowed6, _) = parse_allowed_cidrs("fd00::1/128"); assert!(!is_blocked_ip(&v6, &allowed6)); assert!(is_blocked_ip(&v6n, &allowed6)); } #[test] fn test_parse_drops_malformed() { - let v = parse_allowed_cidrs("10.0.0.0/8, garbage, 100.64.0.0/99, 100.64.0.0/10"); + let (v, dropped) = parse_allowed_cidrs("10.0.0.0/8, garbage, 100.64.0.0/99, 100.64.0.0/10"); assert_eq!(v.len(), 2); + assert_eq!(dropped, vec!["garbage", "100.64.0.0/99"]); } #[test] fn test_parse_empty() { - assert!(parse_allowed_cidrs("").is_empty()); - assert!(parse_allowed_cidrs(" ").is_empty()); + assert!(parse_allowed_cidrs("").0.is_empty()); + assert!(parse_allowed_cidrs(" ").0.is_empty()); // empty allowlist behaves like is_private_ip for s in [ "10.0.0.1", diff --git a/crates/buzz-workflow/src/executor.rs b/crates/buzz-workflow/src/executor.rs index 9e9d18e2594..3da24573bbb 100644 --- a/crates/buzz-workflow/src/executor.rs +++ b/crates/buzz-workflow/src/executor.rs @@ -850,12 +850,19 @@ async fn check_ssrf(host: &str, port: u16) -> Result Result { - let allowed = buzz_core::network::parse_allowed_cidrs(allowed_spec); + let (allowed, dropped) = buzz_core::network::parse_allowed_cidrs(allowed_spec); + if !dropped.is_empty() { + tracing::warn!( + "BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS: dropping malformed entries {:?}", + dropped + ); + } for ip in addrs { if buzz_core::network::is_blocked_ip(ip, &allowed) { return Err(WorkflowError::WebhookError(format!( @@ -1974,30 +1981,40 @@ mod tests { } #[test] + #[cfg(feature = "reqwest")] fn ssrf_verdict_empty_blocks_cgnat() { let ip: std::net::IpAddr = "100.64.1.5".parse().unwrap(); assert!(ssrf_verdict("example.com", &[ip], "").is_err()); } #[test] + #[cfg(feature = "reqwest")] fn ssrf_verdict_allows_cgnat_with_spec() { let ip: std::net::IpAddr = "100.64.1.5".parse().unwrap(); assert!(ssrf_verdict("example.com", &[ip], "100.64.0.0/10").is_ok()); } #[test] + #[cfg(feature = "reqwest")] fn ssrf_verdict_still_blocks_other_private() { let ip: std::net::IpAddr = "10.0.0.1".parse().unwrap(); assert!(ssrf_verdict("example.com", &[ip], "100.64.0.0/10").is_err()); } #[test] + #[cfg(feature = "reqwest")] fn ssrf_verdict_public_passes() { let ip: std::net::IpAddr = "93.184.216.34".parse().unwrap(); assert!(ssrf_verdict("example.com", &[ip], "").is_ok()); assert!(ssrf_verdict("example.com", &[ip], "100.64.0.0/10").is_ok()); } + #[test] + #[cfg(feature = "reqwest")] + fn ssrf_verdict_empty_addrs_is_err() { + assert!(ssrf_verdict("example.com", &[], "").is_err()); + } + #[test] fn send_message_canonicalizes_valid_explicit_override_for_global_workflow() { let override_channel_id = Uuid::new_v4(); From 042efdeafb2005a41b8650a8db5e810e46ef79a8 Mon Sep 17 00:00:00 2001 From: Sanjay Ramadugu Date: Sat, 22 Aug 2026 01:51:58 -0700 Subject: [PATCH 4/5] fix: buzz #6500 R6 add wiring test for allowlist env var Co-authored-by: Sanjay Ramadugu Signed-off-by: Sanjay Ramadugu --- crates/buzz-workflow/src/executor.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/buzz-workflow/src/executor.rs b/crates/buzz-workflow/src/executor.rs index 3da24573bbb..bf1680e71be 100644 --- a/crates/buzz-workflow/src/executor.rs +++ b/crates/buzz-workflow/src/executor.rs @@ -2015,6 +2015,19 @@ mod tests { assert!(ssrf_verdict("example.com", &[], "").is_err()); } + #[cfg(feature = "reqwest")] + #[tokio::test] + #[allow(clippy::await_holding_lock)] + async fn check_ssrf_reads_allowlist_env_var() { + static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + std::env::remove_var("BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS"); + assert!(check_ssrf("127.0.0.1", 80).await.is_err()); + std::env::set_var("BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS", "127.0.0.0/8"); + assert!(check_ssrf("127.0.0.1", 80).await.is_ok()); + std::env::remove_var("BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS"); + } + #[test] fn send_message_canonicalizes_valid_explicit_override_for_global_workflow() { let override_channel_id = Uuid::new_v4(); From a61bef47fd1d3f0ed5e7c01ac9d8cd7948f015cf Mon Sep 17 00:00:00 2001 From: Sanjay Ramadugu Date: Sat, 22 Aug 2026 01:54:36 -0700 Subject: [PATCH 5/5] fix: buzz #6500 use sync test with block_on to avoid allow attr Co-authored-by: Sanjay Ramadugu Signed-off-by: Sanjay Ramadugu --- crates/buzz-workflow/src/executor.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/buzz-workflow/src/executor.rs b/crates/buzz-workflow/src/executor.rs index bf1680e71be..6f77f00c5c1 100644 --- a/crates/buzz-workflow/src/executor.rs +++ b/crates/buzz-workflow/src/executor.rs @@ -2016,15 +2016,18 @@ mod tests { } #[cfg(feature = "reqwest")] - #[tokio::test] - #[allow(clippy::await_holding_lock)] - async fn check_ssrf_reads_allowlist_env_var() { + #[test] + fn check_ssrf_reads_allowlist_env_var() { static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); std::env::remove_var("BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS"); - assert!(check_ssrf("127.0.0.1", 80).await.is_err()); + assert!(rt.block_on(check_ssrf("127.0.0.1", 80)).is_err()); std::env::set_var("BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS", "127.0.0.0/8"); - assert!(check_ssrf("127.0.0.1", 80).await.is_ok()); + assert!(rt.block_on(check_ssrf("127.0.0.1", 80)).is_ok()); std::env::remove_var("BUZZ_WORKFLOW_WEBHOOK_ALLOWED_CIDRS"); }