use std::net::IpAddr;
use std::sync::Arc;
use axum::http::Method;
use ipnet::IpNet;
use regex::{Regex, RegexBuilder};
use crate::config::FilterConfig;
use crate::sqlite::order::Identifier;
pub mod build;
pub mod client_ip;
pub mod custom;
pub mod eab;
pub mod explain;
pub mod expr;
pub mod identifiers;
pub mod ip_allow;
pub mod ipam;
pub mod path;
pub mod policy;
pub mod reverse_dns;
pub use client_ip::{ClientIp, ProxyPolicy};
pub use eab::EabIdentity;
pub use policy::{
Check, CheckSummary, Effect, FilterPolicy, Mode, Outcome, Rule, RuleSummary, Stage, StageSet,
Verdict,
};
pub(crate) const SUBJECT_ONLY_TYPES: &[&str] = &["cn"];
#[derive(Debug)]
pub struct ConnectionContext<'a> {
pub client_ip: Option<IpAddr>,
pub method: &'a Method,
pub path: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentifierStage {
NewOrder,
Csr,
}
impl IdentifierStage {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::NewOrder => "newOrder",
Self::Csr => "CSR",
}
}
}
#[derive(Debug)]
pub struct IdentifierContext<'a> {
pub client_ip: Option<IpAddr>,
pub account_id: &'a str,
pub stage: IdentifierStage,
pub identifiers: &'a [Identifier],
pub eab: Option<EabIdentity>,
}
pub(crate) fn require_client_ip(client_ip: Option<IpAddr>) -> Result<IpAddr, Verdict> {
client_ip
.map(canonical)
.ok_or_else(|| Verdict::Fail("client address unavailable".to_string()))
}
pub fn from_config(
cfg: &FilterConfig,
dns: &crate::config::DnsConfig,
ipam: Option<Arc<crate::ipam::IpamRegistry>>,
eab_enabled: bool,
) -> anyhow::Result<Arc<FilterPolicy>> {
build::build(cfg, dns, ipam, eab_enabled).map(Arc::new)
}
pub(crate) fn parse_net(entry: &str) -> anyhow::Result<IpNet> {
if let Ok(net) = entry.parse::<IpNet>() {
return Ok(net);
}
match entry.parse::<IpAddr>() {
Ok(addr) => Ok(IpNet::from(addr)),
Err(_) => anyhow::bail!("invalid network or address: {entry}"),
}
}
pub(crate) fn parse_nets(entries: &[String], setting: &str) -> anyhow::Result<Vec<IpNet>> {
entries
.iter()
.map(|entry| parse_net(entry).map_err(|error| anyhow::anyhow!("{setting}: {error}")))
.collect()
}
pub(crate) fn canonical(ip: IpAddr) -> IpAddr {
ip.to_canonical()
}
pub(crate) fn nets_contain(nets: &[IpNet], ip: IpAddr) -> bool {
let ip = canonical(ip);
nets.iter().any(|net| net.contains(&ip))
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ListVerdict {
Permitted,
Denied,
NotAllowed,
}
pub(crate) fn check_lists<T>(allow: &[T], deny: &[T], matches: impl Fn(&T) -> bool) -> ListVerdict {
if deny.iter().any(&matches) {
return ListVerdict::Denied;
}
if !allow.is_empty() && !allow.iter().any(&matches) {
return ListVerdict::NotAllowed;
}
ListVerdict::Permitted
}
pub(crate) fn compile_anchored(patterns: &[String], setting: &str) -> anyhow::Result<Vec<Regex>> {
patterns
.iter()
.map(|pattern| {
RegexBuilder::new(&format!("^(?:{pattern})$"))
.case_insensitive(true)
.build()
.map_err(|error| anyhow::anyhow!("{setting}: invalid regex {pattern:?}: {error}"))
})
.collect()
}
pub(crate) fn default_identifier_types() -> Vec<String> {
vec!["dns".to_string(), "cn".to_string()]
}
pub(crate) fn glob_to_pattern(glob: &str) -> String {
glob.split('*')
.map(regex::escape)
.collect::<Vec<_>>()
.join("[^.]+")
}
pub(crate) fn compile_matchers(
globs: &[String],
regexes: &[String],
check: &str,
side: &str,
) -> anyhow::Result<Vec<Regex>> {
let from_globs: Vec<String> = globs.iter().map(|glob| glob_to_pattern(glob)).collect();
let mut compiled = compile_anchored(&from_globs, &format!("filter.check.{check}.{side}"))?;
compiled.extend(compile_anchored(
regexes,
&format!("filter.check.{check}.{side}_regex"),
)?);
Ok(compiled)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_net_accepts_cidr_and_bare_addresses() {
assert!(parse_net("192.168.1.0/24").is_ok());
assert!(parse_net("fd00::/8").is_ok());
let host = parse_net("203.0.113.7").unwrap();
assert_eq!(host.prefix_len(), 32);
assert!(host.contains(&"203.0.113.7".parse::<IpAddr>().unwrap()));
assert!(!host.contains(&"203.0.113.8".parse::<IpAddr>().unwrap()));
let host6 = parse_net("2001:db8::1").unwrap();
assert_eq!(host6.prefix_len(), 128);
assert!(parse_net("not-a-network").is_err());
assert!(parse_net("192.168.1.0/99").is_err());
}
#[test]
fn nets_contain_canonicalizes_ipv4_mapped_addresses() {
let nets = parse_nets(&["192.168.1.0/24".to_string()], "test").unwrap();
assert!(nets_contain(&nets, "192.168.1.5".parse().unwrap()));
assert!(nets_contain(&nets, "::ffff:192.168.1.5".parse().unwrap()));
assert!(!nets_contain(&nets, "10.0.0.1".parse().unwrap()));
}
#[test]
fn parse_nets_names_the_offending_setting() {
let error = parse_nets(&["garbage".to_string()], "filter.check.net.allow")
.unwrap_err()
.to_string();
assert!(error.contains("filter.check.net.allow"), "{error}");
assert!(error.contains("garbage"), "{error}");
}
#[test]
fn canonical_unmaps_ipv4_in_ipv6() {
assert_eq!(
canonical("::ffff:10.0.0.1".parse().unwrap()),
"10.0.0.1".parse::<IpAddr>().unwrap()
);
}
#[test]
fn compile_anchored_prevents_suffix_bypass() {
let patterns =
compile_anchored(&[r"example\.com".to_string()], "filter.check.x.allow").unwrap();
assert!(patterns[0].is_match("example.com"));
assert!(!patterns[0].is_match("example.com.evil.net"));
assert!(!patterns[0].is_match("notexample.com"));
}
#[test]
fn compile_anchored_is_case_insensitive() {
let patterns = compile_anchored(&[r"host\.example\.com".to_string()], "test").unwrap();
assert!(patterns[0].is_match("HOST.Example.COM"));
}
#[test]
fn compile_anchored_reports_a_bad_pattern() {
let error = compile_anchored(&["[unclosed".to_string()], "filter.check.x.deny")
.unwrap_err()
.to_string();
assert!(error.contains("filter.check.x.deny"), "{error}");
assert!(error.contains("[unclosed"), "{error}");
}
#[test]
fn a_glob_star_is_one_label_and_everything_else_is_literal() {
let cases: &[(&str, &str, bool)] = &[
("*.example.com", "a.example.com", true),
("*.example.com", "A.Example.COM", true),
("*.example.com", "a.b.example.com", false),
("*.example.com", "example.com", false),
("*.example.com", "aexample.com", false),
("example.com", "example.com", true),
("example.com", "exampleXcom", false),
("a+b.example.com", "a+b.example.com", true),
("a+b.example.com", "aab.example.com", false),
("*.example.com", "*.example.com", true),
("host-*.example.com", "host-1.example.com", true),
("host-*.example.com", "host-1.2.example.com", false),
];
for (glob, value, expected) in cases {
let compiled =
compile_anchored(&[glob_to_pattern(glob)], "test").expect("a glob always compiles");
assert_eq!(
compiled[0].is_match(value),
*expected,
"glob {glob:?} against {value:?}"
);
}
}
#[test]
fn compile_matchers_unions_globs_and_regexes_and_names_each_key() {
let compiled = compile_matchers(
&["*.example.com".to_string()],
&[r"host\d+\.internal".to_string()],
"names",
"allow",
)
.unwrap();
assert_eq!(compiled.len(), 2);
assert!(compiled[0].is_match("a.example.com"));
assert!(compiled[1].is_match("host12.internal"));
let error = compile_matchers(&[], &["[unclosed".to_string()], "names", "deny")
.unwrap_err()
.to_string();
assert!(error.contains("filter.check.names.deny_regex"), "{error}");
}
#[test]
fn identifier_stage_labels() {
assert_eq!(IdentifierStage::NewOrder.as_str(), "newOrder");
assert_eq!(IdentifierStage::Csr.as_str(), "CSR");
}
#[test]
fn check_lists_implements_the_shared_allow_deny_rule() {
fn matches(value: &'static str) -> impl Fn(&&str) -> bool {
move |entry: &&str| *entry == value
}
assert_eq!(
check_lists::<&str>(&[], &[], matches("a")),
ListVerdict::Permitted
);
assert_eq!(check_lists(&[], &["a"], matches("a")), ListVerdict::Denied);
assert_eq!(
check_lists(&["a"], &[], matches("a")),
ListVerdict::Permitted
);
assert_eq!(
check_lists(&["b"], &[], matches("a")),
ListVerdict::NotAllowed
);
assert_eq!(
check_lists(&["a"], &["a"], matches("a")),
ListVerdict::Denied
);
}
#[test]
fn require_client_ip_fails_closed_and_canonicalizes() {
assert_eq!(
require_client_ip(Some("::ffff:10.0.0.1".parse().unwrap())).unwrap(),
"10.0.0.1".parse::<IpAddr>().unwrap()
);
match require_client_ip(None) {
Err(Verdict::Fail(detail)) => assert!(detail.contains("unavailable"), "{detail}"),
other => panic!("expected Fail, got {other:?}"),
}
}
#[test]
fn the_default_identifier_types_are_dns_and_cn() {
assert_eq!(default_identifier_types(), vec!["dns", "cn"]);
}
}