1mod ports;
13
14use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
15
16pub use self::ports::{DEFAULT_PORTS, RULE_PORT, host_port, port_policy};
17
18use openmoat_core::{
19 Action, AtomicAction, CompiledPolicy, Decision, Defaults, Kind, MapPathResolver, Policy, Taint,
20 Verdict,
21};
22
23pub const RULE_ADDRESS: &str = "proxy-address";
25pub const RULE_SNI: &str = "proxy-sni";
27pub const RULE_REQUEST: &str = "proxy-request";
29pub const RULE_UPSTREAM: &str = "proxy-upstream";
31pub const RULE_AUDIT: &str = "proxy-audit";
33
34const METADATA_NAMES: &[&str] = &["metadata.google.internal", "metadata.goog"];
36const ALIBABA_METADATA: Ipv4Addr = Ipv4Addr::new(100, 100, 100, 200);
37const AZURE_WIRESERVER: Ipv4Addr = Ipv4Addr::new(168, 63, 129, 16);
39const AWS_METADATA_V6: Ipv6Addr = Ipv6Addr::new(0xfd00, 0xec2, 0, 0, 0, 0, 0, 0x254);
40const NAT64: [u16; 6] = [0x64, 0xff9b, 0, 0, 0, 0];
42const NAT64_LOCAL: [u16; 3] = [0x64, 0xff9b, 1];
45const SIIT: [u16; 6] = [0, 0, 0, 0, 0xffff, 0];
47const PRIVATE_V4: [(Ipv4Addr, u8, &str); 6] = [
49 (
50 Ipv4Addr::new(10, 0, 0, 0),
51 8,
52 "a private address (10.0.0.0/8)",
53 ),
54 (
55 Ipv4Addr::new(172, 16, 0, 0),
56 12,
57 "a private address (172.16.0.0/12)",
58 ),
59 (
60 Ipv4Addr::new(192, 168, 0, 0),
61 16,
62 "a private address (192.168.0.0/16)",
63 ),
64 (
65 Ipv4Addr::new(100, 64, 0, 0),
66 10,
67 "a shared CGNAT address (100.64.0.0/10)",
68 ),
69 (
70 Ipv4Addr::new(198, 18, 0, 0),
71 15,
72 "a benchmarking address (198.18.0.0/15)",
73 ),
74 (
75 Ipv4Addr::new(192, 0, 0, 0),
76 24,
77 "an IETF protocol address (192.0.0.0/24)",
78 ),
79];
80
81#[must_use]
85pub fn host(policy: &CompiledPolicy<'_>, host: &str) -> Decision {
86 if let Some(refused) = metadata(host) {
87 return refused;
88 }
89 let atom = AtomicAction::Fetch {
90 host: host.to_owned(),
91 };
92 let mut decision = policy.evaluate_atomic(&atom).unwrap_or_else(|| {
93 Decision::single(
94 Verdict::Deny,
95 RULE_REQUEST,
96 format!("no rule decided {host}"),
97 )
98 });
99 if matches!(decision.verdict, Verdict::Ask | Verdict::Defer) {
102 decision.verdict = Verdict::Deny;
103 decision
104 .reasons
105 .push("the proxy cannot ask, so a host the hook would ask for or leave to the agent is refused".to_owned());
106 }
107 decision
108}
109
110fn metadata(host: &str) -> Option<Decision> {
112 METADATA_NAMES
113 .iter()
114 .any(|m| host == *m || host.strip_suffix(m).is_some_and(|p| p.ends_with('.')))
115 .then(|| {
116 Decision::single(
117 Verdict::Deny,
118 RULE_ADDRESS,
119 format!("{host} is a cloud metadata service"),
120 )
121 })
122}
123
124#[must_use]
130pub fn tainted(
131 policy: &CompiledPolicy<'_>,
132 host: &str,
133 port: u16,
134 taint: &Taint,
135) -> Option<Decision> {
136 let authority = if host.contains(':') {
137 format!("[{host}]:{port}")
138 } else {
139 format!("{host}:{port}")
140 };
141 let action = Action::Net {
142 url: format!("https://{authority}/"),
143 };
144 let mut decision = policy.with_taint(
145 Decision::new(Verdict::Allow),
146 &action,
147 &MapPathResolver::default(),
148 taint,
149 );
150 if decision.verdict == Verdict::Allow {
151 return None;
152 }
153 decision.verdict = Verdict::Deny;
154 decision.reasons.push(
155 "the proxy cannot ask, so a session that read secret material reaches only that \
156 secret's own host until it ends; `openmoat allow` does not answer a secret's taint, \
157 so start a new session"
158 .to_owned(),
159 );
160 Some(decision)
161}
162
163#[must_use]
167pub fn forbidden(ip: IpAddr) -> Option<Forbidden> {
168 match ip.to_canonical() {
169 IpAddr::V4(v4) => forbidden_v4(v4),
170 IpAddr::V6(v6) => forbidden_v6(v6),
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176#[non_exhaustive]
177pub enum Forbidden {
178 Loopback,
180 LinkLocal,
182 Metadata,
184 ThisMachine,
189 NotUnicast,
191 Private(&'static str),
195}
196
197impl Forbidden {
198 #[must_use]
200 pub fn describe(self) -> &'static str {
201 match self {
202 Self::Loopback => "a loopback address",
203 Self::LinkLocal => "a link-local address",
204 Self::Metadata => "a cloud metadata address",
205 Self::ThisMachine => "an address of this machine",
206 Self::NotUnicast => "not a unicast address",
207 Self::Private(range) => range,
208 }
209 }
210}
211
212fn forbidden_v4(ip: Ipv4Addr) -> Option<Forbidden> {
213 if ip.is_loopback() {
214 Some(Forbidden::Loopback)
215 } else if ip.is_link_local() {
216 Some(Forbidden::LinkLocal)
217 } else if ip == ALIBABA_METADATA || ip == AZURE_WIRESERVER {
218 Some(Forbidden::Metadata)
219 } else if ip.is_unspecified() || ip.is_broadcast() || ip.is_multicast() || ip.octets()[0] == 0 {
220 Some(Forbidden::NotUnicast)
221 } else {
222 PRIVATE_V4
223 .iter()
224 .find(|(net, len, _)| u32::from(ip) >> (32 - len) == u32::from(*net) >> (32 - len))
225 .map(|(_, _, range)| Forbidden::Private(range))
226 }
227}
228
229fn forbidden_v6(ip: Ipv6Addr) -> Option<Forbidden> {
230 let seg = ip.segments();
231 if ip.is_loopback() {
232 Some(Forbidden::Loopback)
233 } else if ip.is_unspecified() || ip.is_multicast() {
234 Some(Forbidden::NotUnicast)
235 } else if seg[0] & 0xffc0 == 0xfe80 {
236 Some(Forbidden::LinkLocal)
237 } else if ip == AWS_METADATA_V6 {
238 Some(Forbidden::Metadata)
239 } else if let Some(v4) = carried_v4(ip) {
240 forbidden_v4(v4)
241 } else if let Some(why) = tunnelled_v4(ip).find_map(forbidden_v4) {
242 Some(why)
243 } else if seg[..3] == NAT64_LOCAL {
244 let [a, b] = seg[6].to_be_bytes();
247 let [c, d] = seg[7].to_be_bytes();
248 forbidden_v4(Ipv4Addr::new(a, b, c, d)).or(Some(Forbidden::Private(
249 "a local-use NAT64 address (64:ff9b:1::/48)",
250 )))
251 } else if seg[0] & 0xfe00 == 0xfc00 {
252 Some(Forbidden::Private("a unique local address (fc00::/7)"))
253 } else {
254 None
255 }
256}
257
258fn carried_v4(ip: Ipv6Addr) -> Option<Ipv4Addr> {
262 let seg = ip.segments();
263 (seg[..6] == NAT64 || seg[..6] == [0; 6] || seg[..6] == SIIT).then(|| {
264 let [a, b] = seg[6].to_be_bytes();
265 let [c, d] = seg[7].to_be_bytes();
266 Ipv4Addr::new(a, b, c, d)
267 })
268}
269
270fn tunnelled_v4(ip: Ipv6Addr) -> impl Iterator<Item = Ipv4Addr> {
274 let o = ip.octets();
275 let six_to_four = (o[..2] == [0x20, 0x02]).then(|| Ipv4Addr::new(o[2], o[3], o[4], o[5]));
276 let teredo = o[..4] == [0x20, 0x01, 0, 0];
277 let client = teredo.then(|| Ipv4Addr::new(!o[12], !o[13], !o[14], !o[15]));
278 let server = teredo.then(|| Ipv4Addr::new(o[4], o[5], o[6], o[7]));
279 [six_to_four, client, server].into_iter().flatten()
280}
281
282#[must_use]
292pub fn address_policy(policy: &Policy) -> Policy {
293 let allow = policy
294 .allow
295 .iter()
296 .map(|g| {
297 let keep = |list: &[String]| {
299 let kept: Vec<String> = list
300 .iter()
301 .filter(|p| p.starts_with('!') || names_address(p))
302 .cloned()
303 .collect();
304 ports::with_ports(&kept, false)
305 };
306 openmoat_core::RuleGroup {
307 id: g.id.clone(),
308 reason: g.reason.clone(),
309 net: keep(g.patterns(Kind::Net)),
310 fetch: keep(g.patterns(Kind::Fetch)),
311 ..Default::default()
312 }
313 })
314 .collect();
315 Policy {
317 defaults: Defaults::All(Verdict::Ask),
318 deny: policy.deny.clone(),
319 allow,
320 ask: Vec::new(),
321 ..policy.clone()
322 }
323}
324
325fn names_address(pattern: &str) -> bool {
327 !pattern.starts_with(['*', '?', '[', '{'])
328 && (pattern.starts_with(|c: char| c.is_ascii_digit()) || pattern.contains(':'))
329 && pattern
330 .chars()
331 .all(|c| c.is_ascii_hexdigit() || ".:*?[]{},!-".contains(c))
332}
333
334#[must_use]
337pub fn named(addresses: &CompiledPolicy<'_>, ip: IpAddr) -> bool {
338 let atom = AtomicAction::Fetch {
339 host: ip.to_canonical().to_string(),
340 };
341 addresses
342 .evaluate_atomic(&atom)
343 .is_some_and(|d| d.verdict == Verdict::Allow)
344}
345
346#[must_use]
349pub fn named_on(addresses: &CompiledPolicy<'_>, addr: SocketAddr) -> bool {
350 if named(addresses, addr.ip()) {
351 return true;
352 }
353 let IpAddr::V4(v4) = addr.ip().to_canonical() else {
354 return false;
355 };
356 let atom = AtomicAction::Fetch {
357 host: format!("{v4}:{}", addr.port()),
358 };
359 addresses
360 .evaluate_atomic(&atom)
361 .is_some_and(|d| d.verdict == Verdict::Allow)
362}
363
364#[must_use]
371pub fn denied(addresses: &CompiledPolicy<'_>, ip: IpAddr) -> Option<Decision> {
372 let ip = ip.to_canonical();
373 let carried: Vec<IpAddr> = match ip {
374 IpAddr::V6(v6) => carried_v4(v6)
375 .into_iter()
376 .chain(tunnelled_v4(v6))
377 .map(IpAddr::V4)
378 .collect(),
379 IpAddr::V4(_) => Vec::new(),
380 };
381 std::iter::once(ip).chain(carried).find_map(|ip| {
382 let atom = AtomicAction::Fetch {
383 host: ip.to_string(),
384 };
385 addresses
386 .evaluate_atomic(&atom)
387 .filter(|d| d.verdict == Verdict::Deny)
388 })
389}
390
391#[cfg(test)]
392mod tests;