mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
Depends on #6994 (merged). Stack: PR 2 #6994 (merged) → this PR (#7109) → PR 4 #7148 → PR 5 ## What Production assertion runtime for NIP-FI Phase A: JWKS caching layer, SSRF-hardened HTTP fetcher, startup validation gate, NIP-11 discovery serialization, federated assertion verifier with sealed key-source authority, and supporting invariant tests. ## Changes ### `crates/buzz-auth/src/nip_fi/jwks/` `ProductionJwksSource<F>` implements the sealed `IssuerKeySource` trait: - `HttpJwksFetcher`: reqwest-backed fetch with SSRF protection — URI validation (HTTPS, no credentials, no fragment, no host matched by the shared enumerated deny policy), per-fetch DNS resolution rejecting any resolved address matched by the shared enumerated deny policy, address pinning to prevent DNS rebinding TOCTOU, redirect denial, incremental body streaming capped at 512 KiB before any parse - IPv6 host extraction via typed `Url::host()` accessor (strips brackets before SSRF check and provides the correct bare input form for reqwest `resolve()` pinning; the bracketed form from `host_str()` fails `IpAddr::parse` and does not match the URL authority key) - Complete-operation deadline via `tokio::time::timeout` covering DNS resolution through body streaming - Bounded periodic refresh with configurable interval and hard snapshot deadline - Cancellation-safe RAII refresh permit: dropped on future cancellation so the next caller can re-fetch - Content-digest-gated generation counter: identical re-fetches preserve generation; key rotations advance it - Injectable clock (`now_fn: Arc<dyn Fn() -> DateTime<Utc> + Send + Sync>`): production uses `Arc::new(Utc::now)`; all four deadline creation and expiry checks use `(self.now_fn)()`, enabling controlled-time testing without wall-clock sleep `JwksSourceContract`: a closed value type that is the single source of truth for the three deployment fields whose change alters which keys the runtime trusts and how long it trusts them: - `jwks_uri` — selects the authenticated key source; validated at construction (HTTPS, no credentials/fragment, no bare private-IP host); stored as the `Url`-normalized form so that equivalent spellings (uppercase host, explicit default port `:443`, dot-segment paths like `/.well-known/./jwks.json`) converge to the same `AssertionPolicyId` - `refresh_interval_seconds` — defines bounded refresh behavior; positive, ≤ 1 year, strictly < `key_snapshot_hard_deadline_seconds` - `key_snapshot_hard_deadline_seconds` — defines the source's accepted time rule; every `VerifiedAssertion.revalidation_dependencies` deadline derives from this `JwksSourceContract` is a required `IssuerPolicy` input and is included in `derive_assertion_policy_id` after a domain separator. `IssuerJwksConfig` embeds the contract instead of independently restating these fields — startup validation rejects any contract mismatch (`NipFiStartupError::JwksContractMismatch`). ### `crates/buzz-auth/src/nip_fi/verifier.rs` - `FederatedAssertionVerifier<S>`: provider-neutral verifier over a closed multi-issuer registry and sealed `IssuerKeySource` - `Arc<S>: IssuerKeySource` forwarding impl (blanket seal for `Arc<S>` in the sealed module) — one `Arc<ProductionJwksSource>` can be shared across multiple verifiers; all observe JWKS refreshes through the shared cache without rebuilding the verifier - `AssertionKeySet`: crate-private constructor seals issuer binding — no external crate can relabel issuer B's JWKS as issuer A - Sealed `IssuerKeySource` trait closes the authority-construction seam at both ends ### `crates/buzz-core/src/network.rs` Renamed `is_private_ip` to `is_not_global_unicast` (compat alias retained) and restored the complete IANA deny/exception table from this branch's own history (`272dacadb`). The predicate is an enumerated deny/explicit exception policy: addresses covered by a named deny rule are rejected; addresses not covered by any explicit deny rule (e.g. `fe00::1`) pass through. Deny rules are derived from the IANA Special-Purpose Address Space registries (last updated 2025-10-09), with globally-reachable exceptions carved out explicitly (e.g. PCP/TURN anycast inside 2001::/23). Blocked IPv4 classes: loopback (127/8), private RFC 1918 (10/8, 172.16/12, 192.168/16), link-local (169.254/16), unspecified (0/8), broadcast, CGNAT/RFC 6598 (100.64/10), benchmarking/RFC 2544 (198.18/15), IETF Protocol Assignments (192.0.0.0/24, globally reachable exceptions: 192.0.0.9 PCP anycast RFC 7723 and 192.0.0.10 TURN anycast RFC 8155), documentation/RFC 5737 (192.0.2/24, 198.51.100/24, 203.0.113/24), deprecated 6to4 relay anycast (192.88.99.0/24, RFC 7526, global=None → conservative deny), multicast/RFC 5771 (224/4), reserved class-E (240/4). Blocked IPv6 classes: loopback (::1), unspecified (::), ULA (fc00::/7), link-local (fe80::/10), deprecated site-local (fec0::/10, RFC 3879), multicast (ff00::/8), IETF Protocol Assignments envelope (2001::/23, globally reachable exceptions: 2001:1::1–::3 PCP/TURN/DNS-SD anycast, 2001:3::/32 AMT RFC 7450, 2001:4:112::/48 AS112-v6 RFC 7535, 2001:20::/28 ORCHIDv2 RFC 7343, 2001:30::/28 DETs RFC 9374), documentation (2001:db8::/32 RFC 3849, 3fff::/20 RFC 9637), 6to4 (2002::/16, RFC 3056), Discard-Only (100::/64, RFC 6666), Dummy IPv6 Prefix (100:0:0:1::/64, RFC 9780), SRv6 SIDs (5f00::/16, RFC 9252), NAT64 local-use (64:ff9b:1::/48, RFC 8215). IPv4 embedded in mapped, compatible, NAT64 well-known (64:ff9b::/96), and SIIT IPv4-translated (::ffff:0:0:0/96) forms is checked recursively. All three callers (JWKS boundary, webhook SSRF, link-preview SSRF) inherit the complete predicate through the inline `is_private_ip` compatibility alias. ### Invariant coverage **Canonical URI convergence.** `jwks_contract_uri_canonicalization_convergence_and_divergence` asserts that uppercase host, explicit `:443`, and dot-segment path (`/.well-known/./jwks.json`) each produce the same `AssertionPolicyId` as the canonical form; a genuinely different host or path diverges. Mutation: storing raw input bytes instead of `parsed.to_string()` turns the three convergence assertions red. **Resolved-target and pin-input seam.** `resolved_target_and_pin_key_seam_public_ipv6_and_fec0_rejection` carries a public `2606:4700::1` URI through all three stages of `fetch_jwks_inner`: `extract_url_host_and_port` yields the bare host (no brackets), `resolve_and_check_ssrf` takes the IP-literal fast path and returns the accepted `IpAddr`, and the extracted host string equals the URL authority form (verifying the correct bare input to reqwest's `resolve()` pin call). `fec0::1` traverses the same extraction and SSRF stages and is rejected as `InvalidUri`. Network-free: both addresses are IP literals with no DNS lookup. Mutation: restoring `host_str()` brackets the address, `IpAddr::parse` fails, the SSRF fast path is unreachable, and all three assertions flip red. **Controlled original-deadline rotation.** `shared_arc_source_verifier_rejects_expired_a1_accepts_a2` uses an `AtomicI64`-backed injectable clock to advance past A1's original absolute deadline without wall-clock sleep. A1's deadline is computed at T0 and never mutated. The clock advances to T0 + HARD_DEADLINE_SECS + 1; `get_snapshot` fires a re-fetch and installs A2. One unchanged `FederatedAssertionVerifier` then rejects A1-signed tokens (deadline enforced by the `key_set` read path) and accepts A2-signed tokens, proves A2's generation is strictly greater, and confirms A2's deadline is later than A1's original. Mutation oracle: replace the shared `Arc` with an independently constructed source built from the same configs and sharing the same controlled clock, warmed with a separate A1 fetch before advancement. Post-advancement, `key_set()` on the verifier's independent source filters the expired A1 snapshot (`filter(|c| now < c.hard_deadline)`) and returns no keys — the verifier never re-fetches and never observes A2. A1-reject stays green (the independent cache is also expired, so no A1 keys are served), but A2-accept flips red, because the verifier never observes A2. A2 acceptance is the reliable shared-source oracle. **Complete SSRF classifier boundary.** JWKS-boundary tests cover every newly restored class through `validate_jwks_uri` (URI-validation path): `192.0.0.1` (IETF Protocol Assignments interior), `192.0.0.9`/`.10` (PCP/TURN anycast global exceptions), `192.88.99.1` (deprecated 6to4 anycast), `2001:2::1` (2001::/23 interior), `2001:1::1` (2001::/23 global exception), `100::1` (Discard-Only), `3fff::1` (documentation), and `5f00::1` (SRv6 SIDs). URI validation and resolved-target enforcement share the same `is_not_global_unicast` predicate, so these URI-path tests exercise the complete classifier table. All pass mutation: removing any deny branch makes the rejection assertion red; removing any exception branch makes the acceptance assertion red. The resolved-target enforcement path is covered separately by `resolved_target_and_pin_key_seam_public_ipv6_and_fec0_rejection` for `::1` (loopback), public `2606:4700::1`, and `fec0::1`. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
409 lines
16 KiB
Rust
409 lines
16 KiB
Rust
//! Network utility functions for Buzz.
|
|
//!
|
|
//! Provides shared helpers used across crates for SSRF protection and
|
|
//! IP address classification.
|
|
|
|
// RFC 6052 well-known NAT64 prefix (64:ff9b::/96).
|
|
const NAT64_WELL_KNOWN_PREFIX: [u8; 12] = [0x00, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
|
|
// Legacy SIIT IPv4-translated prefix (::ffff:0:0:0/96).
|
|
const IPV4_TRANSLATED_PREFIX: [u8; 12] = [0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0];
|
|
|
|
/// Extract an IPv4 address stored in the final four octets under a `/96` prefix.
|
|
///
|
|
/// Using network-order octets directly avoids error-prone segment shifting.
|
|
fn embedded_ipv4(v6: &std::net::Ipv6Addr, prefix: &[u8; 12]) -> Option<std::net::Ipv4Addr> {
|
|
let octets = v6.octets();
|
|
octets
|
|
.starts_with(prefix)
|
|
.then(|| std::net::Ipv4Addr::new(octets[12], octets[13], octets[14], octets[15]))
|
|
}
|
|
|
|
/// Enumerated-deny SSRF predicate: returns `true` when the address falls within
|
|
/// a blocked class, and `false` (accepted) for everything else, including
|
|
/// addresses not covered by any explicit deny rule (e.g. `fe00::1`).
|
|
///
|
|
/// Blocked classes are drawn from the IANA IPv4 and IPv6 Special-Purpose
|
|
/// Address Space registries (last updated 2025-10-09): ranges whose
|
|
/// `Globally Reachable` column is `False`, `None`, or absent, plus multicast
|
|
/// space. Within otherwise-denied envelopes, explicitly global entries are
|
|
/// carved out as exceptions (e.g., PCP/TURN/DNS-SD anycast inside 2001::/23).
|
|
/// IPv4 embedded in IPv4-mapped, IPv4-compatible, and NAT64 well-known
|
|
/// (64:ff9b::/96) space is evaluated recursively against the IPv4 table —
|
|
/// registry global=True for the IPv6 wrapper does not bypass the
|
|
/// embedded-address check. SIIT IPv4-translated (::ffff:0:0:0/96) follows the
|
|
/// same recursive path. The local-use NAT64 prefix (64:ff9b:1::/48) is blocked
|
|
/// wholesale as a non-global range; its embedded IPv4 payload is not decoded.
|
|
///
|
|
/// Used for SSRF protection: rejects outbound targets in known non-public
|
|
/// address classes; addresses not covered by an explicit deny rule pass through.
|
|
/// Conservative posture: `None`/blank registry entries are treated as non-global.
|
|
///
|
|
/// Registries retrieved 2026-08-31; registries last updated 2025-10-09:
|
|
/// https://www.iana.org/assignments/iana-ipv4-special-registry/
|
|
/// https://www.iana.org/assignments/iana-ipv6-special-registry/
|
|
///
|
|
/// Compatibility alias: `is_private_ip` (see below).
|
|
///
|
|
/// Callers: `buzz-auth` JWKS boundary, `buzz-workflow` webhook SSRF check,
|
|
/// desktop `link_preview` SSRF check.
|
|
pub fn is_not_global_unicast(ip: &std::net::IpAddr) -> bool {
|
|
match ip {
|
|
std::net::IpAddr::V4(v4) => {
|
|
let o = v4.octets();
|
|
v4.is_loopback() // 127.0.0.0/8
|
|
|| v4.is_private() // 10/8, 172.16/12, 192.168/16
|
|
|| v4.is_link_local() // 169.254.0.0/16
|
|
|| o[0] == 0 // 0.0.0.0/8 "This network"
|
|
|| v4.is_broadcast() // 255.255.255.255
|
|
|| (o[0] == 100 && (o[1] & 0xC0) == 64) // 100.64.0.0/10 Shared/CGNAT
|
|
|| (o[0] == 198 && (o[1] & 0xFE) == 18) // 198.18.0.0/15 Benchmarking
|
|
|| (o[0] & 0xF0) == 0xE0 // 224.0.0.0/4 Multicast
|
|
|| (o[0] & 0xF0) == 0xF0 // 240.0.0.0/4 Reserved
|
|
// 192.0.0.0/24 IETF Protocol Assignments.
|
|
// Globally reachable exceptions: 192.0.0.9 (PCP anycast, RFC 7723)
|
|
// and 192.0.0.10 (TURN anycast, RFC 8155).
|
|
|| (o[0] == 192 && o[1] == 0 && o[2] == 0
|
|
&& o[3] != 9 && o[3] != 10)
|
|
|| (o[0] == 192 && o[1] == 0 && o[2] == 2) // 192.0.2.0/24 TEST-NET-1
|
|
// 192.88.99.0/24 deprecated 6to4 relay anycast (RFC 7526).
|
|
// Registry global field is None/blank — conservative posture: block.
|
|
|| (o[0] == 192 && o[1] == 88 && o[2] == 99)
|
|
|| (o[0] == 198 && o[1] == 51 && o[2] == 100) // 198.51.100.0/24 TEST-NET-2
|
|
|| (o[0] == 203 && o[1] == 0 && o[2] == 113) // 203.0.113.0/24 TEST-NET-3
|
|
}
|
|
std::net::IpAddr::V6(v6) => {
|
|
// IPv4-compatible and IPv4-mapped addresses are checked against IPv4 rules.
|
|
if let Some(v4) = v6.to_ipv4() {
|
|
return is_not_global_unicast(&std::net::IpAddr::V4(v4));
|
|
}
|
|
|
|
let s = v6.segments();
|
|
|
|
// NAT64 well-known prefix (RFC 6052): reachability follows the embedded
|
|
// IPv4 address (registry global=True, but SSRF policy checks payload).
|
|
if let Some(v4) = embedded_ipv4(v6, &NAT64_WELL_KNOWN_PREFIX) {
|
|
return is_not_global_unicast(&std::net::IpAddr::V4(v4));
|
|
}
|
|
|
|
// SIIT IPv4-translated addresses (::ffff:0:0:0/96) route to the embedded
|
|
// IPv4 value and are not recognised by `to_ipv4()`.
|
|
if let Some(v4) = embedded_ipv4(v6, &IPV4_TRANSLATED_PREFIX) {
|
|
return is_not_global_unicast(&std::net::IpAddr::V4(v4));
|
|
}
|
|
|
|
if v6.is_loopback() || v6.is_unspecified() {
|
|
return true;
|
|
}
|
|
|
|
// 2001::/23 IETF Protocol Assignments envelope (registry global=False).
|
|
// All addresses within the /23 are non-global by default, with explicit
|
|
// globally-reachable exceptions carved out below.
|
|
//
|
|
// /23 check: segments[0]==0x2001 and top 7 bits of segments[1] are zero
|
|
// (i.e., segments[1] in [0x0000..0x01ff]).
|
|
if s[0] == 0x2001 && (s[1] >> 9) == 0 {
|
|
// Globally reachable exceptions inside 2001::/23 (registry global=True):
|
|
// 2001:1::1 PCP Anycast RFC 7723
|
|
// 2001:1::2 TURN Anycast RFC 8155
|
|
// 2001:1::3 DNS-SD SRP Anycast RFC 9665
|
|
// 2001:3::/32 AMT RFC 7450
|
|
// 2001:4:112::/48 AS112-v6 RFC 7535
|
|
// 2001:20::/28 ORCHIDv2 RFC 7343 (segments[1] in 0x0020..0x002f)
|
|
// 2001:30::/28 DETs Prefix RFC 9374 (segments[1] in 0x0030..0x003f)
|
|
let is_global_exception = (s[1] == 1
|
|
&& s[2] == 0
|
|
&& s[3] == 0
|
|
&& s[4] == 0
|
|
&& s[5] == 0
|
|
&& s[6] == 0
|
|
&& matches!(s[7], 1..=3))
|
|
|| s[1] == 3 // 2001:3::/32 AMT
|
|
|| (s[1] == 4 && s[2] == 0x0112) // 2001:4:112::/48 AS112-v6
|
|
|| (s[1] >> 4) == 0x0002 // 2001:20::/28 ORCHIDv2
|
|
|| (s[1] >> 4) == 0x0003; // 2001:30::/28 DETs
|
|
|
|
if !is_global_exception {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
s[0] & 0xfe00 == 0xfc00 // fc00::/7 ULA
|
|
|| s[0] & 0xffc0 == 0xfe80 // fe80::/10 link-local
|
|
|| s[0] & 0xffc0 == 0xfec0 // fec0::/10 deprecated site-local (RFC 3879)
|
|
|| s[0] & 0xff00 == 0xff00 // ff00::/8 multicast
|
|
// 64:ff9b:1::/48 local-use NAT64 (RFC 8215)
|
|
|| (s[0] == 0x0064 && s[1] == 0xff9b && s[2] == 1)
|
|
// 100::/64 Discard-Only (RFC 6666)
|
|
|| (s[0] == 0x0100 && s[1] == 0 && s[2] == 0 && s[3] == 0)
|
|
// 100:0:0:1::/64 Dummy IPv6 Prefix (RFC 9780)
|
|
|| (s[0] == 0x0100 && s[1] == 0 && s[2] == 0 && s[3] == 1)
|
|
// 2001:db8::/32 Documentation (RFC 3849) — outside 2001::/23
|
|
|| (s[0] == 0x2001 && s[1] == 0x0db8)
|
|
|| s[0] == 0x2002 // 2002::/16 6to4 (RFC 3056)
|
|
// 3fff::/20 Documentation (RFC 9637)
|
|
|| (s[0] == 0x3fff && (s[1] >> 12) == 0)
|
|
|| s[0] == 0x5f00 // 5f00::/16 SRv6 SIDs (RFC 9252)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Compatibility alias; prefer [`is_not_global_unicast`].
|
|
#[inline]
|
|
pub fn is_private_ip(ip: &std::net::IpAddr) -> bool {
|
|
is_not_global_unicast(ip)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::net::IpAddr;
|
|
|
|
fn blocked(s: &str) -> bool {
|
|
is_not_global_unicast(&s.parse::<IpAddr>().unwrap())
|
|
}
|
|
|
|
#[test]
|
|
fn public_v4() {
|
|
assert!(!blocked("1.1.1.1"));
|
|
assert!(!blocked("8.8.8.8"));
|
|
}
|
|
|
|
#[test]
|
|
fn public_v6_cloudflare() {
|
|
assert!(!blocked("2606:4700::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_and_unspecified() {
|
|
assert!(blocked("127.0.0.1"));
|
|
assert!(blocked("0.0.0.0"));
|
|
assert!(blocked("::1"));
|
|
assert!(blocked("::"));
|
|
}
|
|
|
|
#[test]
|
|
fn private_rfc1918() {
|
|
assert!(blocked("10.0.0.1"));
|
|
assert!(blocked("172.16.0.1"));
|
|
assert!(blocked("192.168.1.1"));
|
|
}
|
|
|
|
#[test]
|
|
fn link_local() {
|
|
assert!(blocked("169.254.1.1"));
|
|
assert!(blocked("fe80::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn broadcast() {
|
|
assert!(blocked("255.255.255.255"));
|
|
}
|
|
|
|
#[test]
|
|
fn cgnat() {
|
|
assert!(blocked("100.64.0.1"));
|
|
assert!(blocked("100.127.255.254"));
|
|
assert!(!blocked("100.63.255.255"));
|
|
assert!(!blocked("100.128.0.0"));
|
|
}
|
|
|
|
#[test]
|
|
fn benchmarking_v4() {
|
|
assert!(blocked("198.18.0.1"));
|
|
assert!(blocked("198.19.255.254"));
|
|
assert!(!blocked("198.17.255.255"));
|
|
assert!(!blocked("198.20.0.0"));
|
|
}
|
|
|
|
#[test]
|
|
fn multicast_and_reserved_v4() {
|
|
assert!(blocked("224.0.0.0"));
|
|
assert!(blocked("239.255.255.255"));
|
|
assert!(blocked("240.0.0.0"));
|
|
assert!(blocked("254.255.255.255"));
|
|
assert!(!blocked("223.255.255.255"));
|
|
}
|
|
|
|
// Most of 192.0.0.0/24 is non-global; 192.0.0.9 (PCP, RFC 7723) and
|
|
// 192.0.0.10 (TURN, RFC 8155) are the only globally-reachable exceptions.
|
|
#[test]
|
|
fn ietf_protocol_assignments() {
|
|
assert!(blocked("192.0.0.0"));
|
|
assert!(blocked("192.0.0.1"));
|
|
assert!(blocked("192.0.0.170")); // NAT64/DNS64 discovery — non-global
|
|
assert!(blocked("192.0.0.255"));
|
|
assert!(!blocked("192.0.0.9")); // PCP Anycast (RFC 7723) — global
|
|
assert!(!blocked("192.0.0.10")); // TURN Anycast (RFC 8155) — global
|
|
}
|
|
|
|
#[test]
|
|
fn documentation_v4() {
|
|
assert!(blocked("192.0.2.0"));
|
|
assert!(blocked("192.0.2.255"));
|
|
assert!(blocked("198.51.100.0"));
|
|
assert!(blocked("198.51.100.255"));
|
|
assert!(blocked("203.0.113.0"));
|
|
assert!(blocked("203.0.113.255"));
|
|
assert!(!blocked("192.0.1.255"));
|
|
assert!(!blocked("192.0.3.0"));
|
|
assert!(!blocked("198.51.99.255"));
|
|
assert!(!blocked("198.51.101.0"));
|
|
assert!(!blocked("203.0.112.255"));
|
|
assert!(!blocked("203.0.114.0"));
|
|
}
|
|
|
|
// Registry global field is None/blank; conservative posture: block.
|
|
#[test]
|
|
fn deprecated_6to4_anycast_v4() {
|
|
assert!(blocked("192.88.99.0"));
|
|
assert!(blocked("192.88.99.1"));
|
|
assert!(blocked("192.88.99.255"));
|
|
assert!(!blocked("192.88.98.255"));
|
|
assert!(!blocked("192.88.100.0"));
|
|
}
|
|
|
|
#[test]
|
|
fn ula_v6() {
|
|
assert!(blocked("fd00::1"));
|
|
assert!(blocked("fc00::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn multicast_v6() {
|
|
assert!(blocked("ff02::1"));
|
|
assert!(blocked("ff02::2"));
|
|
assert!(blocked("ffff::1"));
|
|
assert!(!blocked("fe00::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn ietf_protocol_assignments_v6_interior() {
|
|
assert!(blocked("2001::"));
|
|
assert!(blocked("2001:2::1"));
|
|
assert!(blocked("2001:10::1"));
|
|
assert!(blocked("2001:db8::1")); // Documentation — outside /23 but blocked separately
|
|
assert!(blocked("2001:1ff:ffff::1"));
|
|
assert!(!blocked("2001:200::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn ietf_protocol_assignments_v6_global_exceptions() {
|
|
// PCP/TURN/DNS-SD anycast /128s — registry global=True
|
|
assert!(!blocked("2001:1::1")); // PCP Anycast (RFC 7723)
|
|
assert!(!blocked("2001:1::2")); // TURN Anycast (RFC 8155)
|
|
assert!(!blocked("2001:1::3")); // DNS-SD SRP Anycast (RFC 9665)
|
|
assert!(blocked("2001:1::4")); // not an exception
|
|
assert!(blocked("2001:1:1::1")); // not an exception
|
|
|
|
// 2001:3::/32 AMT — registry global=True
|
|
assert!(!blocked("2001:3::1"));
|
|
assert!(!blocked("2001:3:ffff::1"));
|
|
assert!(blocked("2001:4::1"));
|
|
|
|
// 2001:4:112::/48 AS112-v6 — registry global=True
|
|
assert!(!blocked("2001:4:112::1"));
|
|
assert!(!blocked("2001:4:112:ffff::1"));
|
|
assert!(blocked("2001:4:113::1"));
|
|
|
|
// 2001:20::/28 ORCHIDv2 — registry global=True
|
|
assert!(!blocked("2001:20::1"));
|
|
assert!(!blocked("2001:2f::1"));
|
|
assert!(blocked("2001:10::1"));
|
|
|
|
// 2001:30::/28 DETs — registry global=True
|
|
assert!(!blocked("2001:30::1"));
|
|
assert!(!blocked("2001:3f::1"));
|
|
assert!(!blocked("2001:3::1")); // AMT exception — distinct check
|
|
}
|
|
|
|
#[test]
|
|
fn documentation_v6() {
|
|
assert!(blocked("2001:db8::1"));
|
|
assert!(blocked("2001:db8:ffff::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn six_to_four_v6() {
|
|
assert!(blocked("2002::"));
|
|
assert!(blocked("2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
|
|
assert!(!blocked("2003::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn discard_only_v6() {
|
|
assert!(blocked("100::1"));
|
|
assert!(blocked("100::ffff:ffff:ffff:ffff"));
|
|
assert!(!blocked("100:0:1::1")); // outside both discard and dummy ranges
|
|
}
|
|
|
|
#[test]
|
|
fn dummy_prefix_v6() {
|
|
assert!(blocked("100:0:0:1::"));
|
|
assert!(blocked("100:0:0:1:ffff:ffff:ffff:ffff"));
|
|
assert!(!blocked("100:0:0:2::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn nat64_local_use_v6() {
|
|
assert!(blocked("64:ff9b:1::"));
|
|
assert!(blocked("64:ff9b:1:ffff:ffff:ffff:ffff:ffff"));
|
|
assert!(!blocked("64:ff9b:2::"));
|
|
}
|
|
|
|
#[test]
|
|
fn documentation_3fff_v6() {
|
|
assert!(blocked("3fff::1"));
|
|
assert!(blocked("3fff:0fff::1"));
|
|
assert!(!blocked("3fff:1000::1"));
|
|
assert!(!blocked("3ffe::1"));
|
|
}
|
|
|
|
#[test]
|
|
fn srv6_sids_v6() {
|
|
assert!(blocked("5f00::1"));
|
|
assert!(blocked("5f00:ffff::1"));
|
|
assert!(!blocked("5e00::1"));
|
|
assert!(!blocked("5fff::1")); // 5fff ≠ 5f00 — outside /16
|
|
}
|
|
|
|
#[test]
|
|
fn nat64_well_known_v6() {
|
|
assert!(blocked("64:ff9b::10.0.0.1")); // private embedded
|
|
assert!(blocked("64:ff9b::127.0.0.1")); // loopback embedded
|
|
assert!(blocked("64:ff9b::169.254.169.254")); // link-local embedded
|
|
assert!(!blocked("64:ff9b::8.8.8.8")); // public embedded — policy follows payload
|
|
assert!(!blocked("64:ff9a:ffff:ffff:ffff:ffff:ffff:ffff")); // different prefix
|
|
assert!(!blocked("64:ff9b::1:0:0")); // outside /96
|
|
}
|
|
|
|
#[test]
|
|
fn ipv4_translated_v6() {
|
|
assert!(blocked("::ffff:0:10.0.0.1"));
|
|
assert!(blocked("::ffff:0:127.0.0.1"));
|
|
assert!(!blocked("::ffff:0:8.8.8.8"));
|
|
assert!(!blocked("0:0:0:0:fffe:ffff:ffff:ffff")); // outside prefix
|
|
}
|
|
|
|
#[test]
|
|
fn ipv4_mapped_v6() {
|
|
assert!(blocked("::ffff:10.0.0.1"));
|
|
assert!(blocked("::ffff:127.0.0.1"));
|
|
assert!(!blocked("::ffff:8.8.8.8"));
|
|
}
|
|
|
|
#[test]
|
|
fn ipv4_compatible_v6() {
|
|
assert!(blocked("::10.0.0.1"));
|
|
assert!(blocked("::127.0.0.1"));
|
|
assert!(!blocked("::8.8.8.8"));
|
|
}
|
|
|
|
#[test]
|
|
fn deprecated_site_local_fec0() {
|
|
// fec0::/10 — deprecated IPv6 site-local (RFC 3879); blocked as non-global.
|
|
assert!(blocked("fec0::1"));
|
|
assert!(blocked("feff::1")); // fec0::/10 boundary
|
|
}
|
|
}
|