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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
//! # 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`), benchmark
//! (`2001:2::/48`), deprecated site-local (`fec0::/10`), NAT64
//! well-known prefix wrapping a bogon IPv4 (`64:ff9b::/96`, RFC 6052)
//! AND the NAT64 Local-Use prefix wrapping a bogon IPv4
//! (`64:ff9b:1::/48`, RFC 8215), 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::testing::{TestApi, VerifierTestApi};
///
/// assert!(TestApi.ip_addr_is_bogon(IpAddr::V4(Ipv4Addr::LOCALHOST)));
/// assert!(TestApi.ip_addr_is_bogon(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
/// assert!(TestApi.ip_addr_is_bogon(IpAddr::V4(Ipv4Addr::new(169, 254, 169, 254))));
/// assert!(TestApi.ip_addr_is_bogon(IpAddr::V6(Ipv6Addr::LOCALHOST)));
/// assert!(!TestApi.ip_addr_is_bogon(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));
/// ```
pub
/// Extract the IPv4 address a NAT64 prefix embeds, if `segs` sits in one of the
/// two standardized NAT64 prefixes. Single owner for the NAT64 → IPv4
/// decomposition (RFC 6052 §2.2 embedding format).
///
/// - **Well-Known Prefix** `64:ff9b::/96` (RFC 6052 §2.1): /96 embedding only
/// the IPv4 occupies the low 32 bits (`segs[6..8]`).
/// - **Local-Use Prefix** `64:ff9b:1::/48` (RFC 8215): /48 embedding, the IPv4
/// occupies bits 48-63 (`segs[3]`) and 72-87 (`segs[4]` low byte, `segs[5]`
/// high byte), skipping the reserved u-octet at bits 64-71. The u-octet is
/// NOT required to be zero here: the whole /48 is dedicated to NAT64, so an
/// attacker setting it must not smuggle an embedded internal IPv4 past the
/// screen (fail closed (extract and check regardless)).
///
/// Network-Specific Prefixes (operator-chosen, any of the six RFC 6052 lengths)
/// are intentionally not enumerated: they are unknowable without operator
/// config, and a self-hosted NAT64 gateway is already inside the trust boundary.