1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
11
12use openmoat_core::{AtomicAction, CompiledPolicy, Decision, Defaults, Kind, Policy, Verdict};
13
14pub const RULE_ADDRESS: &str = "proxy-address";
16pub const RULE_SNI: &str = "proxy-sni";
18pub const RULE_REQUEST: &str = "proxy-request";
20pub const RULE_UPSTREAM: &str = "proxy-upstream";
22pub const RULE_AUDIT: &str = "proxy-audit";
24
25const METADATA_NAMES: &[&str] = &["metadata.google.internal", "metadata.goog"];
27const ALIBABA_METADATA: Ipv4Addr = Ipv4Addr::new(100, 100, 100, 200);
28const AWS_METADATA_V6: Ipv6Addr = Ipv6Addr::new(0xfd00, 0xec2, 0, 0, 0, 0, 0, 0x254);
29const NAT64: [u16; 6] = [0x64, 0xff9b, 0, 0, 0, 0];
31const PRIVATE_V4: [(Ipv4Addr, u8, &str); 5] = [
33 (
34 Ipv4Addr::new(10, 0, 0, 0),
35 8,
36 "a private address (10.0.0.0/8)",
37 ),
38 (
39 Ipv4Addr::new(172, 16, 0, 0),
40 12,
41 "a private address (172.16.0.0/12)",
42 ),
43 (
44 Ipv4Addr::new(192, 168, 0, 0),
45 16,
46 "a private address (192.168.0.0/16)",
47 ),
48 (
49 Ipv4Addr::new(100, 64, 0, 0),
50 10,
51 "a shared CGNAT address (100.64.0.0/10)",
52 ),
53 (
54 Ipv4Addr::new(198, 18, 0, 0),
55 15,
56 "a benchmarking address (198.18.0.0/15)",
57 ),
58];
59
60#[must_use]
63pub fn host(policy: &CompiledPolicy<'_>, host: &str) -> Decision {
64 if METADATA_NAMES
65 .iter()
66 .any(|m| host == *m || host.strip_suffix(m).is_some_and(|p| p.ends_with('.')))
67 {
68 return Decision::single(
69 Verdict::Deny,
70 RULE_ADDRESS,
71 format!("{host} is a cloud metadata service"),
72 );
73 }
74 let atom = AtomicAction::Fetch {
75 host: host.to_owned(),
76 };
77 let mut decision = policy.evaluate_atomic(&atom).unwrap_or_else(|| {
78 Decision::single(
79 Verdict::Deny,
80 RULE_REQUEST,
81 format!("no rule decided {host}"),
82 )
83 });
84 if decision.verdict == Verdict::Ask {
85 decision.verdict = Verdict::Deny;
86 decision
87 .reasons
88 .push("the proxy cannot ask, so a host that would ask is refused".to_owned());
89 }
90 decision
91}
92
93#[must_use]
97pub fn forbidden(ip: IpAddr) -> Option<Forbidden> {
98 match ip.to_canonical() {
99 IpAddr::V4(v4) => forbidden_v4(v4),
100 IpAddr::V6(v6) => forbidden_v6(v6),
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum Forbidden {
108 Loopback,
110 LinkLocal,
112 Metadata,
114 NotUnicast,
116 Private(&'static str),
120}
121
122impl Forbidden {
123 #[must_use]
125 pub fn describe(self) -> &'static str {
126 match self {
127 Self::Loopback => "a loopback address",
128 Self::LinkLocal => "a link-local address",
129 Self::Metadata => "a cloud metadata address",
130 Self::NotUnicast => "not a unicast address",
131 Self::Private(range) => range,
132 }
133 }
134}
135
136fn forbidden_v4(ip: Ipv4Addr) -> Option<Forbidden> {
137 if ip.is_loopback() {
138 Some(Forbidden::Loopback)
139 } else if ip.is_link_local() {
140 Some(Forbidden::LinkLocal)
141 } else if ip == ALIBABA_METADATA {
142 Some(Forbidden::Metadata)
143 } else if ip.is_unspecified() || ip.is_broadcast() || ip.is_multicast() || ip.octets()[0] == 0 {
144 Some(Forbidden::NotUnicast)
145 } else {
146 PRIVATE_V4
147 .iter()
148 .find(|(net, len, _)| u32::from(ip) >> (32 - len) == u32::from(*net) >> (32 - len))
149 .map(|(_, _, range)| Forbidden::Private(range))
150 }
151}
152
153fn forbidden_v6(ip: Ipv6Addr) -> Option<Forbidden> {
154 let seg = ip.segments();
155 if ip.is_loopback() {
156 Some(Forbidden::Loopback)
157 } else if ip.is_unspecified() || ip.is_multicast() {
158 Some(Forbidden::NotUnicast)
159 } else if seg[0] & 0xffc0 == 0xfe80 {
160 Some(Forbidden::LinkLocal)
161 } else if ip == AWS_METADATA_V6 {
162 Some(Forbidden::Metadata)
163 } else if seg[..6] == NAT64 || seg[..6] == [0; 6] {
164 let [a, b] = seg[6].to_be_bytes();
166 let [c, d] = seg[7].to_be_bytes();
167 forbidden_v4(Ipv4Addr::new(a, b, c, d))
168 } else if seg[0] & 0xfe00 == 0xfc00 {
169 Some(Forbidden::Private("a unique local address (fc00::/7)"))
170 } else {
171 None
172 }
173}
174
175#[must_use]
183pub fn address_policy(policy: &Policy) -> Policy {
184 let allow = policy
185 .allow
186 .iter()
187 .map(|g| {
188 let keep = |list: &[String]| {
189 list.iter()
190 .filter(|p| names_address(p))
191 .cloned()
192 .collect::<Vec<_>>()
193 };
194 openmoat_core::RuleGroup {
195 id: g.id.clone(),
196 reason: g.reason.clone(),
197 net: keep(g.patterns(Kind::Net)),
198 fetch: keep(g.patterns(Kind::Fetch)),
199 ..Default::default()
200 }
201 })
202 .collect();
203 Policy {
204 defaults: Defaults::All(Verdict::Deny),
205 deny: policy.deny.clone(),
206 allow,
207 ask: Vec::new(),
208 ..policy.clone()
209 }
210}
211
212fn names_address(pattern: &str) -> bool {
214 !pattern.starts_with(['*', '?', '[', '{'])
215 && (pattern.starts_with(|c: char| c.is_ascii_digit()) || pattern.contains(':'))
216 && pattern
217 .chars()
218 .all(|c| c.is_ascii_hexdigit() || ".:*?[]{},!-".contains(c))
219}
220
221#[must_use]
224pub fn named(addresses: &CompiledPolicy<'_>, ip: IpAddr) -> bool {
225 let atom = AtomicAction::Fetch {
226 host: ip.to_canonical().to_string(),
227 };
228 addresses
229 .evaluate_atomic(&atom)
230 .is_some_and(|d| d.verdict == Verdict::Allow)
231}
232
233#[cfg(test)]
234mod tests;