Skip to content

Commit 94841e8

Browse files
committed
test(supervisor): IPv6 fail-closed cases and allowed_ips for NAT64 answers
allowed_ips entries now match a NAT64 address through the IPv4 address it embeds, so 10.0.0.0/8 covers 64:ff9b::a00:5 the same way it covers 10.0.0.5. Loopback, link-local and metadata stay blocked first. The mediated IPv6 test now also checks that these are denied: the real upstream IPv6, an unallocated synthetic address, the right address on the wrong port, an open without a binary identity, and the old mapping after a policy reload. A second test covers an unreachable trusted resolver. The store gets the IPv6 version of the wrong port / stale generation / expiry test. Signed-off-by: Joffref <mjoffre@blaxel.ai>
1 parent 6f71df4 commit 94841e8

4 files changed

Lines changed: 317 additions & 50 deletions

File tree

‎crates/openshell-core/src/net.rs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,21 @@ pub fn is_internal_ip(ip: IpAddr) -> bool {
269269
}
270270
}
271271

272+
/// Check whether an `allowed_ips` entry covers `ip`.
273+
///
274+
/// A NAT64 address also matches through the IPv4 address it embeds, so
275+
/// `10.0.0.0/8` covers the DNS64 answer `64:ff9b::a00:5` exactly as it covers
276+
/// `10.0.0.5`. Callers apply the always-blocked check first.
277+
pub fn allowed_net_contains(net: &IpNet, ip: IpAddr) -> bool {
278+
if net.contains(&ip) {
279+
return true;
280+
}
281+
match ip {
282+
IpAddr::V6(v6) => nat64::embedded_ipv4(v6).is_some_and(|v4| net.contains(&IpAddr::V4(v4))),
283+
IpAddr::V4(_) => false,
284+
}
285+
}
286+
272287
/// Check if a CIDR network intersects any address range classified by
273288
/// [`is_internal_ip`].
274289
pub fn is_internal_net(net: IpNet) -> bool {
@@ -863,4 +878,24 @@ mod tests {
863878
assert!(is_internal_net(net("64:ff9b:1::/64")));
864879
assert!(!is_internal_net(net("64:ff9b::8c52:7000/120")));
865880
}
881+
882+
#[test]
883+
fn nat64_answers_match_allowed_ipv4_networks() {
884+
let net: IpNet = "10.0.0.0/8".parse().unwrap();
885+
assert!(allowed_net_contains(&net, "10.0.0.5".parse().unwrap()));
886+
assert!(allowed_net_contains(
887+
&net,
888+
"64:ff9b::a00:5".parse().unwrap()
889+
));
890+
assert!(!allowed_net_contains(
891+
&net,
892+
"64:ff9b::b00:5".parse().unwrap()
893+
));
894+
assert!(!allowed_net_contains(
895+
&net,
896+
"2001:db8::a00:5".parse().unwrap()
897+
));
898+
let v6: IpNet = "2001:db8::/32".parse().unwrap();
899+
assert!(allowed_net_contains(&v6, "2001:db8::1".parse().unwrap()));
900+
}
866901
}

‎crates/openshell-supervisor-network/src/policy_dns/store.rs‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,57 @@ mod tests {
816816
));
817817
}
818818

819+
#[test]
820+
fn ipv6_wrong_port_stale_generation_and_expiry_fail_closed() {
821+
let store = store(2);
822+
let now = Instant::now();
823+
let mut ipv6 = request("db.example", 4, Duration::from_secs(2));
824+
ipv6.family = AddressFamily::Ipv6;
825+
ipv6.contracts[0].pinned_addresses = vec!["2001:db8::8".parse().unwrap()];
826+
let record = store.publish(ipv6, 4, now).unwrap();
827+
assert!(record.synthetic_address.is_ipv6());
828+
assert_eq!(
829+
store
830+
.lookup(record.synthetic_address, 5432, 4, now)
831+
.unwrap()
832+
.pinned_addresses(),
833+
["2001:db8::8".parse::<IpAddr>().unwrap()]
834+
);
835+
assert!(matches!(
836+
store.lookup(record.synthetic_address, 3306, 4, now),
837+
Err(MappingLookupError::PortMismatch)
838+
));
839+
assert!(matches!(
840+
store.lookup(record.synthetic_address, 5432, 5, now),
841+
Err(MappingLookupError::StalePolicy)
842+
));
843+
assert!(matches!(
844+
store.lookup(
845+
record.synthetic_address,
846+
5432,
847+
4,
848+
now + Duration::from_secs(2)
849+
),
850+
Err(MappingLookupError::Expired)
851+
));
852+
// An unallocated address in the IPv6 pool, and the real upstream
853+
// address, have no mapping.
854+
for unmapped in ["fd00:1::2", "2001:db8::8"] {
855+
assert!(matches!(
856+
store.lookup(unmapped.parse().unwrap(), 5432, 4, now),
857+
Err(MappingLookupError::Missing)
858+
));
859+
}
860+
}
861+
862+
#[test]
863+
fn ipv6_answers_cannot_publish_ipv4_pins() {
864+
let store = store(2);
865+
let mut mixed = request("db.example", 1, Duration::from_secs(2));
866+
mixed.family = AddressFamily::Ipv6;
867+
assert!(store.publish(mixed, 1, Instant::now()).is_err());
868+
}
869+
819870
#[test]
820871
fn expiry_never_reassigns_synthetic_address_to_another_name() {
821872
let store = store(2);

0 commit comments

Comments
 (0)