use openmoat_core::{
AtomicAction, CompiledPolicy, Decision, Defaults, Kind, Policy, RuleGroup, Verdict,
port_pattern,
};
pub const RULE_PORT: &str = "proxy-port";
pub const DEFAULT_PORTS: [u16; 2] = [443, 80];
#[must_use]
pub fn port_policy(policy: &Policy) -> Policy {
let lists = |groups: &[RuleGroup], ported_only: bool| -> Vec<RuleGroup> {
groups
.iter()
.map(|g| RuleGroup {
id: g.id.clone(),
reason: g.reason.clone(),
net: with_ports(g.patterns(Kind::Net), ported_only),
fetch: with_ports(g.patterns(Kind::Fetch), ported_only),
..Default::default()
})
.collect()
};
Policy {
defaults: Defaults::All(Verdict::Defer),
deny: lists(&policy.deny, false),
repo_ask: lists(&policy.repo_ask, false),
allow: lists(&policy.allow, true),
ask: Vec::new(),
defer: Vec::new(),
..policy.clone()
}
}
pub(super) fn with_ports(list: &[String], ported_only: bool) -> Vec<String> {
let kept = list
.iter()
.filter(|p| !ported_only || port_pattern(p.strip_prefix('!').unwrap_or(p)).is_some());
let excluded = list
.iter()
.filter_map(|p| p.strip_prefix('!'))
.filter(|host| !host.contains(':'))
.map(|host| format!("!{host}:*"));
kept.cloned().chain(excluded).collect()
}
fn ported(host: &str, port: u16) -> Option<String> {
(!host.contains(':')).then(|| format!("{host}:{port}"))
}
#[must_use]
pub fn host_port(
policy: &CompiledPolicy<'_>,
ports: &CompiledPolicy<'_>,
host: &str,
port: u16,
) -> Decision {
let decide = |subject: String| ports.evaluate_atomic(&AtomicAction::Fetch { host: subject });
let refused = |d: &Decision| matches!(d.verdict, Verdict::Deny | Verdict::Ask);
if super::metadata(host).is_some() || decide(host.to_owned()).is_some_and(|d| refused(&d)) {
return super::host(policy, host);
}
match ported(host, port).and_then(decide) {
Some(d) if d.verdict == Verdict::Allow => return d,
Some(mut d) if refused(&d) => {
if d.verdict == Verdict::Ask {
d.verdict = Verdict::Deny;
d.reasons.push(
"the proxy cannot ask, so a host and port it would ask for is refused"
.to_owned(),
);
}
return d;
}
_ => {}
}
let decision = super::host(policy, host);
if decision.verdict != Verdict::Allow || DEFAULT_PORTS.contains(&port) {
return decision;
}
let how = if host.contains(':') {
"an IPv6 address is reached on those ports only".to_owned()
} else {
format!("a `net` rule `{host}:{port}` allows this port")
};
Decision::single(
Verdict::Deny,
RULE_PORT,
format!("{host} is allowed on ports 443 and 80 only, not {port}; {how}"),
)
}
#[cfg(test)]
mod tests;