1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! # bogon — canonical SSRF-policy IP classification
//!
//! Single source of truth for the question *"is this address safe to
//! fetch from over the internet, or does it belong to a private /
//! reserved / metadata-leaking range that an SSRF guard must refuse?"*
//! across the Santh scanner fleet.
//!
//! Before this crate existed, four independent implementations of the
//! same predicate lived in the tree — scanclient, wafrift-types,
//! netshift's DNS pool, netshift's DNS cache, and golemn's URL
//! guard. Three of the four had coverage gaps (no CGN, no IETF
//! protocol-assignment range, no benchmark range, no Teredo, no
//! ORCHIDv2, no discard prefix). One had a `::1` IPv6 loopback
//! escape bug. Re-export shims couldn't fix it because scanclient is
//! a heavy reqwest/tokio/rustls/hickory consumer and netshift sits
//! *below* scanclient in the dependency graph — depending on
//! scanclient from netshift would have created a cycle.
//!
//! This crate exists to break that cycle. Pure std-only, zero
//! transitive dep cost, depended on by every consumer that needs an
//! SSRF guard. A future RFC update (a new IETF-reserved range, a
//! new IPv6 documentation prefix) lands here once and propagates
//! everywhere.
//!
//! ## What counts as a bogon
//!
//! "Bogon" here means *not safe to fetch from over the internet
//! unless the operator explicitly opted into private/lab access*.
//! Covers:
//!
//! **IPv4:** RFC 1918 private, loopback, link-local, broadcast,
//! documentation (TEST-NET-1/2/3), unspecified, Carrier-Grade NAT
//! (100.64.0.0/10), IETF protocol assignment (192.0.0.0/24),
//! benchmark (198.18.0.0/15), AWS/GCP/Azure IMDS metadata
//! (169.254.169.254 specifically — but covered by the broader
//! 169.254.0.0/16 link-local rule).
//!
//! **IPv6:** loopback (`::1`), unspecified (`::`), unique-local
//! (`fc00::/7`), link-local (`fe80::/10`), multicast, documentation
//! (`2001:db8::/32`), Teredo (`2001::/32`), ORCHIDv2
//! (`2001:20::/28`), discard prefix (`100::/64`), 6to4 wrapping a
//! bogon IPv4 (`2002::/16`), IPv4-mapped (`::ffff:0:0/96`) and
//! IPv4-compatible (`::a.b.c.d`) wrappings of bogon IPv4.
//!
//! ## What this is *not*
//!
//! Not a public-routing classifier. Multicast IPv4, anycast, and
//! some reserved-but-routable ranges are intentionally allowed
//! because legitimate scanner workloads need them. The function
//! answers exactly *"should SSRF policy refuse this address?"*, not
//! *"is this address globally routable?"*. Consumers that need
//! stricter rules (e.g. keyhog's verifier, which also blocks
//! multicast and broadcast IPv4) should layer their additional
//! checks on top of [`ip_addr_is_bogon`], not fork it.
//!
//! ## The `::1` regression
//!
//! Pre-2026-05-23 the original wafrift donor copy let `::1` past
//! the SSRF guard. The cause: `Ipv6Addr::to_ipv4()` decomposes
//! `::1` to `0.0.0.1`, which is *not* in the IPv4 loopback range
//! (`127.0.0.0/8`). The donor fell through to the v4 fallback and
//! returned `false`. The fix — check `is_loopback()` /
//! `is_unspecified()` before any v4 mapping — is now load-bearing
//! and pinned by [`tests::rejects_ipv6_loopback`].
// This module is `no_std`-clean by construction — it imports only
// `core::net` and forbids unsafe — but `#![no_std]` is a *crate*-level
// attribute and is silently ignored (with a warning) inside a submodule, so
// it is intentionally not declared here. The `core::` imports below are the
// real enforcement.
use IpAddr;
/// True if this IP should be blocked when private/upstream lab
/// access is disallowed.
///
/// Covers the union of IPv4 + IPv6 bogon ranges every shipping
/// scanner in the Santh fleet has independently needed to refuse.
/// See [crate-level docs](crate) for the exact coverage list and the
/// non-goals.
///
/// # Examples
///
/// ```
/// use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
/// use keyhog_verifier::bogon::ip_addr_is_bogon;
///
/// assert!(ip_addr_is_bogon(IpAddr::V4(Ipv4Addr::LOCALHOST)));
/// assert!(ip_addr_is_bogon(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
/// assert!(ip_addr_is_bogon(IpAddr::V4(Ipv4Addr::new(169, 254, 169, 254))));
/// assert!(ip_addr_is_bogon(IpAddr::V6(Ipv6Addr::LOCALHOST)));
/// assert!(!ip_addr_is_bogon(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));
/// ```