Expand description
SSRF classifier.
A pure address classifier plus a DNS-resolving host guard. The rule it
enforces is blunt: the HTTP client refuses RFC-1918 and link-local
destinations by default. This module is the mechanism; it is composed at
every call site that introduces a model/agent/peer-supplied URL (A2A push
targets, http workflow nodes), while the only operator-configured
outbound (intel/client.rs) is exempt — its address comes from the
operator, not from anything the model can steer.
§Resolve once, dial what you vetted
A guard that resolves a name, likes the answer, and then lets the
caller dial the name is decorative: the connect re-resolves, and an
attacker who controls the authoritative DNS answers the guard with a
public address and the connect with 169.254.169.254. That is DNS
rebinding, and it defeats an address check that does not carry its
result forward.
So the guard hands back the addresses it vetted
(resolve_guarded) and the dial takes addresses, never a name
(connect_vetted / connect_addrs, which re-assert is_global
on every address immediately before the syscall). TLS and the Host
header stay on the original hostname — connect by IP, verify by name —
so SNI and certificate validation are unaffected.
guard_host serves the yes/no admission check at registration
time, where there is no socket to dial yet; it is not sufficient on
its own at delivery time.
§What “non-global” means here
is_global returns false — i.e. the address is blocked — for
any address an attacker could pivot to from inside the appliance’s
network namespace:
- loopback (
127.0.0.0/8,::1) - RFC-1918 private (
10/8,172.16/12,192.168/16) - link-local (
169.254/16,fe80::/10) — this is the cloud metadata range (169.254.169.254) - IPv6 unique-local (
fc00::/7) - unspecified (
0.0.0.0,::) - multicast and the IPv4 limited broadcast (
255.255.255.255) - “this network”
0.0.0.0/8and the IETF/benchmark documentation ranges, which never route on the public Internet - any IPv4-mapped / IPv4-compatible IPv6 whose embedded v4
address is itself non-global —
::ffff:127.0.0.1and friends are a classic guard bypass, so we unwrap before classifying.
We deliberately do NOT lean on std’s unstable IpAddr::is_global
(feature ip, issue #27709) — it is not available on our MSRV and
its semantics are still in flux. Every range below is spelled out by
hand from primitives that are stable, in the same enumerate-the-bytes
spirit as the rest of the crate.
§Logging posture
Hosts and IPs are operational identifiers, not tool/instruction content, so the diagnostic carries the host and the offending class — never request bodies, headers, or secrets. The codebase is content-capture-off by default and this module keeps that contract.
Structs§
- Ssrf
Error - A host failed the SSRF guard, or could not be resolved at all.
Functions§
- connect_
addrs - Dial one of
addrs, re-asserting the classifier on every entry first. - connect_
vetted - Guard and dial in one step: resolve once, vet, connect to a vetted
address. This is what a model/peer-supplied URL must use instead of
http::connect_tcp, which resolves the name a second time. - connect_
vetted_ with connect_vettedagainst an injected resolver — the test seam.- guard_
host - Resolve
hostand reject if any resolved address is non-global. - is_
global trueiffipis a globally routable unicast address that is safe to dial from inside the appliance — i.e. not in any of the blocked ranges documented on this module.- resolve_
guarded - Resolve
host:portonce and return the addresses, having rejected the whole host if any of them is non-global. - resolve_
guarded_ with resolve_guardedagainst an injected resolver. Public so the rebinding regression test can drive both halves — guard and dial — through a resolver that changes its mind between them.- std_
resolve - The production resolver:
std’sToSocketAddrs, with the bracketed IPv6 literal form ([::1], as URLs write it) unwrapped first becauseToSocketAddrsdoes not accept the brackets on a bare host.
Type Aliases§
- Resolve
Fn - How a host is turned into addresses. A plain
fnpointer, not a trait object: the only production implementation isstd_resolve, and the seam exists so a test can install a hostile resolver that answers the guard and the dial differently — the rebinding shape this module has to survive.