1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2
3pub fn is_loopback_ip(ip: IpAddr) -> bool {
4 match ip {
5 IpAddr::V4(ip) => ip.is_loopback(),
6 IpAddr::V6(ip) => {
7 ip.is_loopback()
8 || ip
9 .to_ipv4_mapped()
10 .is_some_and(|mapped| mapped.is_loopback())
11 }
12 }
13}
14
15pub fn loopback_cidr(ip: IpAddr) -> &'static str {
16 match ip {
17 IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
18 IpAddr::V6(ip)
19 if ip
20 .to_ipv4_mapped()
21 .is_some_and(|mapped| mapped.is_loopback()) =>
22 {
23 "127.0.0.0/8"
24 }
25 IpAddr::V6(_) => "::1/128",
26 IpAddr::V4(_) => "127.0.0.0/8",
27 }
28}
29
30pub fn format_tcp_resource(host: &str, port: u16) -> String {
31 format!("tcp://{host}:{port}")
32}
33
34pub fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
35 match ip {
36 IpAddr::V4(ip) => {
37 if ip.is_unspecified() {
38 return Some(("0.0.0.0/32", "unspecified"));
39 }
40 let [first, second, ..] = ip.octets();
41 match (first, second) {
42 (10, _) => Some(("10.0.0.0/8", "private")),
43 (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
44 (172, 16..=31) => Some(("172.16.0.0/12", "private")),
45 (192, 168) => Some(("192.168.0.0/16", "private")),
46 (169, 254) => Some(("169.254.0.0/16", "link-local")),
47 (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
48 (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
49 _ => None,
50 }
51 }
52 IpAddr::V6(ip) => {
53 if let Some(mapped) = ip.to_ipv4_mapped() {
54 return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
55 }
56 if let Some(compat) = ipv4_compatible_embedded(ip) {
57 return restricted_non_loopback_ip_range(IpAddr::V4(compat));
58 }
59 if ip.is_unspecified() {
60 return Some(("::/128", "unspecified"));
61 }
62
63 let segments = ip.segments();
64 if (segments[0] & 0xfe00) == 0xfc00 {
65 return Some(("fc00::/7", "unique-local"));
66 }
67 if (segments[0] & 0xffc0) == 0xfe80 {
68 return Some(("fe80::/10", "link-local"));
69 }
70 None
71 }
72 }
73}
74
75fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
76 let segments = ip.segments();
77 if segments[0..6].iter().any(|&s| s != 0) {
78 return None;
79 }
80 let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
81 if embedded == 0 || embedded == 1 {
82 return None;
83 }
84 Some(Ipv4Addr::from(embedded))
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 fn assert_restricted(ip: IpAddr, expected_label: &str) {
92 let classification = restricted_non_loopback_ip_range(ip);
93 assert!(
94 classification.is_some(),
95 "{ip} must be classified as a restricted egress target"
96 );
97 let (_cidr, label) = classification.unwrap();
98 assert_eq!(
99 label, expected_label,
100 "{ip} should be labelled {expected_label}, got {label}"
101 );
102 }
103
104 #[test]
105 fn classifier_denies_unspecified_and_cgnat_targets() {
106 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
107 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
108 assert_restricted(
109 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
110 "carrier-grade-nat",
111 );
112 assert_restricted(
113 IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
114 "carrier-grade-nat",
115 );
116 assert!(
117 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255,)))
118 .is_none()
119 );
120 assert!(
121 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none()
122 );
123 }
124
125 #[test]
126 fn classifier_denies_ipv6_spelled_metadata_addresses() {
127 assert_restricted(
128 IpAddr::V6("::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap()),
129 "link-local",
130 );
131 assert_restricted(
132 IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
133 "link-local",
134 );
135 assert_restricted(
136 IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
137 "private",
138 );
139 assert_restricted(
140 IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
141 "carrier-grade-nat",
142 );
143 assert_eq!(
144 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
145 Some(("::/128", "unspecified"))
146 );
147 assert!(
148 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
149 || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST))
150 );
151 assert!(restricted_non_loopback_ip_range(IpAddr::V6(
152 "::8.8.8.8".parse::<Ipv6Addr>().unwrap()
153 ))
154 .is_none());
155 }
156
157 #[test]
158 fn classifier_denies_reserved_and_multicast_targets() {
159 assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
160 assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
161 assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
162 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
163 assert_restricted(
164 IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
165 "multicast",
166 );
167 assert_restricted(
168 IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
169 "reserved",
170 );
171 assert!(
172 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
173 .is_none()
174 );
175 }
176
177 #[test]
178 fn tcp_resource_format_matches_permission_resources() {
179 assert_eq!(
180 format_tcp_resource("127.0.0.1", 8080),
181 "tcp://127.0.0.1:8080"
182 );
183 assert_eq!(
184 format_tcp_resource("example.test", 443),
185 "tcp://example.test:443"
186 );
187 }
188}