From 3ca69e722df77654b61d40df491586a0f227d8ff Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Tue, 28 Apr 2026 23:23:12 +0200 Subject: [PATCH 1/2] fix(net): catch IPv4-mapped blocked ranges in is_always_blocked_net The IPv6 branch only checked whether the network address itself mapped to a blocked IPv4 address. A broader prefix like ::ffff:168.0.0.0/103 has a public network address but spans ::ffff:169.254.0.0, so the old code accepted it at policy load time while is_always_blocked_ip silently rejected every connection at runtime. Add three containment checks for the IPv4-mapped loopback, link-local, and unspecified representatives. The existing network-address check is kept because it handles single-host entries (/128) whose network address is already in a blocked range. Five new tests cover: single-host loopback and link-local mapped addresses, broad prefixes that span each blocked range without starting there, and a public single-host address that must not be blocked. --- crates/openshell-core/src/net.rs | 49 +++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/openshell-core/src/net.rs b/crates/openshell-core/src/net.rs index 3cd00d7b8..97b35abe5 100644 --- a/crates/openshell-core/src/net.rs +++ b/crates/openshell-core/src/net.rs @@ -97,12 +97,23 @@ pub fn is_always_blocked_net(net: ipnet::IpNet) -> bool { return true; } - // Check IPv4-mapped IPv6 (::ffff:127.0.0.1, ::ffff:169.254.x.x, etc.) + // Check IPv4-mapped IPv6 addresses. The network-address check covers + // ranges whose first address is already in a blocked range + // (e.g. ::ffff:127.0.0.1/128, ::ffff:169.254.0.1/128). The + // containment checks below catch broader prefixes that only reach + // into a blocked range further in — e.g. ::ffff:168.0.0.0/103 + // has a public network address but spans ::ffff:169.254.0.0. if let Some(v4) = network.to_ipv4_mapped() { if v4.is_loopback() || v4.is_link_local() || v4.is_unspecified() { return true; } } + if v6net.contains(&Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped()) + || v6net.contains(&Ipv4Addr::new(169, 254, 0, 0).to_ipv6_mapped()) + || v6net.contains(&Ipv4Addr::UNSPECIFIED.to_ipv6_mapped()) + { + return true; + } false } @@ -332,6 +343,42 @@ mod tests { assert!(is_always_blocked_net(net)); } + #[test] + fn test_always_blocked_net_v6_ipv4_mapped_loopback_single() { + let net: ipnet::IpNet = "::ffff:127.0.0.1/128".parse().unwrap(); + assert!(is_always_blocked_net(net)); + } + + #[test] + fn test_always_blocked_net_v6_ipv4_mapped_link_local_single() { + let net: ipnet::IpNet = "::ffff:169.254.0.1/128".parse().unwrap(); + assert!(is_always_blocked_net(net)); + } + + #[test] + fn test_always_blocked_net_v6_ipv4_mapped_broad_spans_link_local() { + // ::ffff:168.0.0.0/103 has a public network address (168.0.0.0) but + // the range covers 168.0.0.0–169.255.255.255, which includes the + // link-local block 169.254.0.0/16. + let net: ipnet::IpNet = "::ffff:168.0.0.0/103".parse().unwrap(); + assert!(is_always_blocked_net(net)); + } + + #[test] + fn test_always_blocked_net_v6_ipv4_mapped_broad_spans_loopback() { + // ::ffff:64.0.0.0/98 has a public network address (64.0.0.0) but the + // range covers 64.0.0.0–127.255.255.255, which includes loopback. + let net: ipnet::IpNet = "::ffff:64.0.0.0/98".parse().unwrap(); + assert!(is_always_blocked_net(net)); + } + + #[test] + fn test_always_blocked_net_v6_ipv4_mapped_allows_public() { + // ::ffff:8.8.8.8/128 is a public address — should not be blocked. + let net: ipnet::IpNet = "::ffff:8.8.8.8/128".parse().unwrap(); + assert!(!is_always_blocked_net(net)); + } + // -- is_internal_ip -- #[test] From 2e174ee68cadf93add1e3a5fef27c519778111f6 Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Tue, 28 Apr 2026 23:55:51 +0200 Subject: [PATCH 2/2] fix(net): address clippy warnings in is_always_blocked_net Use Ipv4Addr::LOCALHOST instead of Ipv4Addr::new(127, 0, 0, 1) and collapse the nested if let / if into is_some_and. --- crates/openshell-core/src/net.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/openshell-core/src/net.rs b/crates/openshell-core/src/net.rs index 97b35abe5..a73cb38dc 100644 --- a/crates/openshell-core/src/net.rs +++ b/crates/openshell-core/src/net.rs @@ -103,12 +103,13 @@ pub fn is_always_blocked_net(net: ipnet::IpNet) -> bool { // containment checks below catch broader prefixes that only reach // into a blocked range further in — e.g. ::ffff:168.0.0.0/103 // has a public network address but spans ::ffff:169.254.0.0. - if let Some(v4) = network.to_ipv4_mapped() { - if v4.is_loopback() || v4.is_link_local() || v4.is_unspecified() { - return true; - } + if network + .to_ipv4_mapped() + .is_some_and(|v4| v4.is_loopback() || v4.is_link_local() || v4.is_unspecified()) + { + return true; } - if v6net.contains(&Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped()) + if v6net.contains(&Ipv4Addr::LOCALHOST.to_ipv6_mapped()) || v6net.contains(&Ipv4Addr::new(169, 254, 0, 0).to_ipv6_mapped()) || v6net.contains(&Ipv4Addr::UNSPECIFIED.to_ipv6_mapped()) {