steid

@jamesgill /

Rust5.4 KBCode·Blame·History·Raw
1//! Which addresses Steid is willing to connect to on a visitor's say-so.
2//!
3//! One function, used by both features that dial out: [`ImportSource`](super::import)
4//! and [`WebhookUrl`](super::webhook). They arrived on separate branches a day apart and
5//! each carried its own copy, which had already drifted — import knew about `0.0.0.0/8`
6//! and the benchmarking range, webhooks knew about IETF protocol assignments and the
7//! reserved top of IPv4 space. Neither was wrong; each was missing what the other had,
8//! and a *second* copy of a security boundary is the shape of the bug rather than the
9//! bug itself.
10//!
11//! The url-shape rules stay where they were. `WebhookUrl` refuses the packed spellings
12//! of an address (`127.1`, `2130706433`) by refusing a last label that begins with a
13//! digit, and `ImportSource` refuses anything but `https://`; both are rules about text,
14//! not about addresses, and only one of the two types wants each of them.
15
16use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
17
18/// Whether an address is one Steid will connect to.
19///
20/// The allowlist is "the public internet", expressed as a denylist of everything that is
21/// not, because the ranges that are *not* routable are the finite, enumerable half.
22/// Anything unrecognised is therefore allowed — which is the right way round only
23/// because the reserved ranges below are fixed by IANA and do not grow quietly.
24///
25/// Named as a positive so that a caller reads `if !is_public(address) { refuse }`: the
26/// negation is at the call site, where the refusal is.
27pub fn is_public(address: IpAddr) -> bool {
28 match address {
29 // An IPv4 address wearing an IPv6 costume reaches the same host as the v4 one,
30 // so it has to answer the same way: `::ffff:127.0.0.1` is loopback however it is
31 // spelled.
32 IpAddr::V6(address) => match address.to_ipv4_mapped() {
33 Some(mapped) => is_public_v4(mapped),
34 None => is_public_v6(address),
35 },
36 IpAddr::V4(address) => is_public_v4(address),
37 }
38}
39
40fn is_public_v4(address: Ipv4Addr) -> bool {
41 let [a, b, ..] = address.octets();
42
43 !(address.is_unspecified()
44 || address.is_loopback()
45 || address.is_private()
46 || address.is_link_local()
47 || address.is_multicast()
48 || address.is_broadcast()
49 || address.is_documentation()
50 // 0.0.0.0/8 — "this network". Reaches the local host on some stacks.
51 || a == 0
52 // 100.64.0.0/10, carrier-grade NAT: a provider's internal space, not the
53 // internet, and what container runtimes and Tailscale hand out. `Ipv4Addr` has
54 // no predicate for it.
55 || (a == 100 && (64..128).contains(&b))
56 // 192.0.0.0/24, IETF protocol assignments.
57 || address.octets()[..3] == [192, 0, 0]
58 // 198.18.0.0/15, benchmarking. Routed to lab equipment where it is used at all.
59 || (a == 198 && (b == 18 || b == 19))
60 // 240.0.0.0/4, reserved — and 255.255.255.255, which `is_broadcast` has already
61 // caught.
62 || a >= 240)
63}
64
65fn is_public_v6(address: Ipv6Addr) -> bool {
66 let first = address.segments()[0];
67
68 !(address.is_unspecified()
69 || address.is_loopback()
70 || address.is_multicast()
71 // fc00::/7, unique local — IPv6's answer to RFC 1918.
72 || (first & 0xfe00) == 0xfc00
73 // fe80::/10, link-local.
74 || (first & 0xffc0) == 0xfe80)
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn v4(value: &str) -> IpAddr {
82 IpAddr::V4(value.parse::<Ipv4Addr>().expect("a v4 address"))
83 }
84
85 fn v6(value: &str) -> IpAddr {
86 IpAddr::V6(value.parse::<Ipv6Addr>().expect("a v6 address"))
87 }
88
89 /// The server-side request forgery boundary, address by address. Each of these, if
90 /// allowed through, is a connection Steid makes from inside its own network because
91 /// somebody typed a URL into a form.
92 #[test]
93 fn nothing_on_this_machine_or_its_own_network_is_public() {
94 for address in [
95 "0.0.0.0",
96 "0.1.2.3",
97 "127.0.0.1",
98 "127.1.2.3",
99 "10.0.0.5",
100 "172.16.4.1",
101 "192.168.1.1",
102 // The AWS/GCP metadata endpoint, the canonical SSRF target.
103 "169.254.169.254",
104 "100.64.0.1",
105 "100.127.255.255",
106 "192.0.0.1",
107 "198.18.0.1",
108 "198.19.255.255",
109 "192.0.2.1",
110 "224.0.0.1",
111 "240.0.0.1",
112 "255.255.255.255",
113 ] {
114 assert!(
115 !is_public(v4(address)),
116 "{address} is not on the public internet"
117 );
118 }
119
120 for address in ["::", "::1", "fd00::1", "fe80::1", "ff02::1"] {
121 assert!(
122 !is_public(v6(address)),
123 "{address} is not on the public internet"
124 );
125 }
126 }
127
128 #[test]
129 fn an_ipv4_address_in_an_ipv6_costume_answers_the_same_way() {
130 assert!(!is_public(v6("::ffff:127.0.0.1")));
131 assert!(!is_public(v6("::ffff:10.0.0.1")));
132 assert!(is_public(v6("::ffff:1.1.1.1")));
133 }
134
135 #[test]
136 fn ordinary_public_addresses_are_public() {
137 for address in ["1.1.1.1", "8.8.8.8", "140.82.121.4", "100.63.255.255"] {
138 assert!(is_public(v4(address)), "{address} is on the internet");
139 }
140
141 for address in ["2606:4700::1111", "2001:4860:4860::8888"] {
142 assert!(is_public(v6(address)), "{address} is on the internet");
143 }
144 }
145}