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 AZURE_WIRESERVER: Ipv4Addr = Ipv4Addr::new(168, 63, 129, 16);
30const AWS_METADATA_V6: Ipv6Addr = Ipv6Addr::new(0xfd00, 0xec2, 0, 0, 0, 0, 0, 0x254);
31const NAT64: [u16; 6] = [0x64, 0xff9b, 0, 0, 0, 0];
33const NAT64_LOCAL: [u16; 3] = [0x64, 0xff9b, 1];
36const PRIVATE_V4: [(Ipv4Addr, u8, &str); 6] = [
38 (
39 Ipv4Addr::new(10, 0, 0, 0),
40 8,
41 "a private address (10.0.0.0/8)",
42 ),
43 (
44 Ipv4Addr::new(172, 16, 0, 0),
45 12,
46 "a private address (172.16.0.0/12)",
47 ),
48 (
49 Ipv4Addr::new(192, 168, 0, 0),
50 16,
51 "a private address (192.168.0.0/16)",
52 ),
53 (
54 Ipv4Addr::new(100, 64, 0, 0),
55 10,
56 "a shared CGNAT address (100.64.0.0/10)",
57 ),
58 (
59 Ipv4Addr::new(198, 18, 0, 0),
60 15,
61 "a benchmarking address (198.18.0.0/15)",
62 ),
63 (
64 Ipv4Addr::new(192, 0, 0, 0),
65 24,
66 "an IETF protocol address (192.0.0.0/24)",
67 ),
68];
69
70#[must_use]
73pub fn host(policy: &CompiledPolicy<'_>, host: &str) -> Decision {
74 if METADATA_NAMES
75 .iter()
76 .any(|m| host == *m || host.strip_suffix(m).is_some_and(|p| p.ends_with('.')))
77 {
78 return Decision::single(
79 Verdict::Deny,
80 RULE_ADDRESS,
81 format!("{host} is a cloud metadata service"),
82 );
83 }
84 let atom = AtomicAction::Fetch {
85 host: host.to_owned(),
86 };
87 let mut decision = policy.evaluate_atomic(&atom).unwrap_or_else(|| {
88 Decision::single(
89 Verdict::Deny,
90 RULE_REQUEST,
91 format!("no rule decided {host}"),
92 )
93 });
94 if matches!(decision.verdict, Verdict::Ask | Verdict::Defer) {
97 decision.verdict = Verdict::Deny;
98 decision
99 .reasons
100 .push("the proxy cannot ask, so a host the hook would ask for or leave to the agent is refused".to_owned());
101 }
102 decision
103}
104
105#[must_use]
109pub fn forbidden(ip: IpAddr) -> Option<Forbidden> {
110 match ip.to_canonical() {
111 IpAddr::V4(v4) => forbidden_v4(v4),
112 IpAddr::V6(v6) => forbidden_v6(v6),
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum Forbidden {
120 Loopback,
122 LinkLocal,
124 Metadata,
126 NotUnicast,
128 Private(&'static str),
132}
133
134impl Forbidden {
135 #[must_use]
137 pub fn describe(self) -> &'static str {
138 match self {
139 Self::Loopback => "a loopback address",
140 Self::LinkLocal => "a link-local address",
141 Self::Metadata => "a cloud metadata address",
142 Self::NotUnicast => "not a unicast address",
143 Self::Private(range) => range,
144 }
145 }
146}
147
148fn forbidden_v4(ip: Ipv4Addr) -> Option<Forbidden> {
149 if ip.is_loopback() {
150 Some(Forbidden::Loopback)
151 } else if ip.is_link_local() {
152 Some(Forbidden::LinkLocal)
153 } else if ip == ALIBABA_METADATA || ip == AZURE_WIRESERVER {
154 Some(Forbidden::Metadata)
155 } else if ip.is_unspecified() || ip.is_broadcast() || ip.is_multicast() || ip.octets()[0] == 0 {
156 Some(Forbidden::NotUnicast)
157 } else {
158 PRIVATE_V4
159 .iter()
160 .find(|(net, len, _)| u32::from(ip) >> (32 - len) == u32::from(*net) >> (32 - len))
161 .map(|(_, _, range)| Forbidden::Private(range))
162 }
163}
164
165fn forbidden_v6(ip: Ipv6Addr) -> Option<Forbidden> {
166 let seg = ip.segments();
167 if ip.is_loopback() {
168 Some(Forbidden::Loopback)
169 } else if ip.is_unspecified() || ip.is_multicast() {
170 Some(Forbidden::NotUnicast)
171 } else if seg[0] & 0xffc0 == 0xfe80 {
172 Some(Forbidden::LinkLocal)
173 } else if ip == AWS_METADATA_V6 {
174 Some(Forbidden::Metadata)
175 } else if seg[..6] == NAT64 || seg[..6] == [0; 6] {
176 let [a, b] = seg[6].to_be_bytes();
178 let [c, d] = seg[7].to_be_bytes();
179 forbidden_v4(Ipv4Addr::new(a, b, c, d))
180 } else if seg[..3] == NAT64_LOCAL {
181 let [a, b] = seg[6].to_be_bytes();
184 let [c, d] = seg[7].to_be_bytes();
185 forbidden_v4(Ipv4Addr::new(a, b, c, d)).or(Some(Forbidden::Private(
186 "a local-use NAT64 address (64:ff9b:1::/48)",
187 )))
188 } else if seg[0] & 0xfe00 == 0xfc00 {
189 Some(Forbidden::Private("a unique local address (fc00::/7)"))
190 } else {
191 None
192 }
193}
194
195#[must_use]
203pub fn address_policy(policy: &Policy) -> Policy {
204 let allow = policy
205 .allow
206 .iter()
207 .map(|g| {
208 let keep = |list: &[String]| {
209 list.iter()
210 .filter(|p| names_address(p))
211 .cloned()
212 .collect::<Vec<_>>()
213 };
214 openmoat_core::RuleGroup {
215 id: g.id.clone(),
216 reason: g.reason.clone(),
217 net: keep(g.patterns(Kind::Net)),
218 fetch: keep(g.patterns(Kind::Fetch)),
219 ..Default::default()
220 }
221 })
222 .collect();
223 Policy {
224 defaults: Defaults::All(Verdict::Deny),
225 deny: policy.deny.clone(),
226 allow,
227 ask: Vec::new(),
228 ..policy.clone()
229 }
230}
231
232fn names_address(pattern: &str) -> bool {
234 !pattern.starts_with(['*', '?', '[', '{'])
235 && (pattern.starts_with(|c: char| c.is_ascii_digit()) || pattern.contains(':'))
236 && pattern
237 .chars()
238 .all(|c| c.is_ascii_hexdigit() || ".:*?[]{},!-".contains(c))
239}
240
241#[must_use]
244pub fn named(addresses: &CompiledPolicy<'_>, ip: IpAddr) -> bool {
245 let atom = AtomicAction::Fetch {
246 host: ip.to_canonical().to_string(),
247 };
248 addresses
249 .evaluate_atomic(&atom)
250 .is_some_and(|d| d.verdict == Verdict::Allow)
251}
252
253#[cfg(test)]
254mod tests;