use std::collections::HashSet;
use super::composition::{VALID_PROTOCOLS, VALID_REJECT_REASONS};
use crate::error::ConfigError;
use crate::model::{LeafMatcher, MatchExprConfig};
pub(crate) fn rule_upstream_group_could_match_udp(rule: &crate::model::RuleConfig) -> bool {
if let Some(ref match_expr) = rule.match_expr {
return matcher_could_match_udp(match_expr);
}
if rule.host_exact.is_some()
|| rule.host_suffix.is_some()
|| rule.host_regex.is_some()
|| rule.destination_port.is_some()
{
return true;
}
if rule.any.unwrap_or(false) {
return true;
}
true
}
pub(crate) const MAX_MATCH_EXPR_DEPTH: usize = 10;
pub(crate) fn matcher_could_match_udp(matcher: &MatchExprConfig) -> bool {
matcher_could_match_udp_limited(matcher, 0)
}
pub(crate) fn matcher_could_match_udp_limited(matcher: &MatchExprConfig, depth: usize) -> bool {
if depth >= MAX_MATCH_EXPR_DEPTH {
return true;
}
match matcher {
MatchExprConfig::Leaf(leaf) => leaf_could_match_udp(leaf),
MatchExprConfig::Composite(composite) => {
if let Some(ref all) = composite.all {
return all
.iter()
.all(|child| matcher_could_match_udp_limited(child, depth + 1));
}
if let Some(ref any_of) = composite.any_of {
return any_of
.iter()
.any(|child| matcher_could_match_udp_limited(child, depth + 1));
}
if let Some(ref not) = composite.not {
return matcher_could_match_udp_limited(not, depth + 1);
}
true
}
}
}
pub(crate) fn leaf_could_match_udp(leaf: &LeafMatcher) -> bool {
if let Some(ref transport) = leaf.transport {
return transport == "udp";
}
true
}
pub(crate) fn validate_rules(
rules: &[crate::model::RuleConfig],
groups: Option<&[crate::model::UpstreamGroupConfig]>,
errors: &mut Vec<ConfigError>,
) {
let group_ids: HashSet<&str> = groups
.map(|g| g.iter().map(|g| g.id.as_str()).collect())
.unwrap_or_default();
for (i, rule) in rules.iter().enumerate() {
let path = format!("rules[{}]", i);
let matcher_count = [
rule.host_exact.is_some(),
rule.host_suffix.is_some(),
rule.host_regex.is_some(),
rule.destination_port.is_some(),
rule.destination_port_regex.is_some(),
rule.any.unwrap_or(false),
]
.iter()
.filter(|&&b| b)
.count();
if rule.match_expr.is_none() {
if matcher_count > 1 {
errors.push(ConfigError::validation(
&path,
"rule must have exactly one matcher field",
));
}
if let Some(ref host_regex) = rule.host_regex {
if regex::Regex::new(host_regex).is_err() {
errors.push(ConfigError::validation(
&path,
&format!("invalid host regex: {}", host_regex),
));
}
}
if let Some(ref port_regex) = rule.destination_port_regex {
if regex::Regex::new(port_regex).is_err() {
errors.push(ConfigError::validation(
&path,
&format!("invalid destination port regex: {}", port_regex),
));
}
}
} else if let Some(ref match_expr) = rule.match_expr {
if matcher_count > 0 {
errors.push(ConfigError::validation(
&path,
"rule must not combine match with legacy matcher fields",
));
}
validate_match_expr(match_expr, &path, errors, 0);
}
let action_count = [
rule.direct.is_some(),
rule.upstream_group.is_some(),
rule.reject.is_some(),
]
.iter()
.filter(|&&b| b)
.count();
if action_count != 1 {
errors.push(ConfigError::validation(
&path,
"rule must have exactly one action field",
));
}
if let Some(ref upstream_group) = rule.upstream_group {
if !group_ids.contains(upstream_group.as_str()) {
errors.push(ConfigError::validation(
&path,
&format!(
"action references unknown upstream group: {}",
upstream_group
),
));
}
}
if let Some(ref reject) = rule.reject {
if !VALID_REJECT_REASONS.contains(&reject.as_str()) {
errors.push(ConfigError::validation(
&path,
&format!("unknown reject reason: {}", reject),
));
}
}
}
}
pub(crate) fn validate_match_expr(
expr: &crate::model::MatchExprConfig,
path: &str,
errors: &mut Vec<ConfigError>,
depth: usize,
) {
if depth >= MAX_MATCH_EXPR_DEPTH {
errors.push(ConfigError::validation(
path,
&format!(
"expression exceeds maximum depth ({})",
MAX_MATCH_EXPR_DEPTH
),
));
return;
}
match expr {
crate::model::MatchExprConfig::Composite(composite) => {
let has_all = composite.all.is_some();
let has_any = composite.any_of.is_some();
let has_not = composite.not.is_some();
if !has_all && !has_any && !has_not {
errors.push(ConfigError::validation(
&format!("{}.match", path),
"composite must have exactly one of: all, any_of, not",
));
}
if let Some(ref all) = composite.all {
if all.is_empty() {
errors.push(ConfigError::validation(
&format!("{}.match.all", path),
"must not be empty",
));
}
for (j, item) in all.iter().enumerate() {
validate_match_expr(
item,
&format!("{}.match.all[{}]", path, j),
errors,
depth + 1,
);
}
}
if let Some(ref any_of) = composite.any_of {
if any_of.is_empty() {
errors.push(ConfigError::validation(
&format!("{}.match.any_of", path),
"must not be empty",
));
}
for (j, item) in any_of.iter().enumerate() {
validate_match_expr(
item,
&format!("{}.match.any_of[{}]", path, j),
errors,
depth + 1,
);
}
}
if let Some(ref not) = composite.not {
validate_match_expr(not, &format!("{}.match.not", path), errors, depth + 1);
}
}
crate::model::MatchExprConfig::Leaf(leaf) => {
if let Some(ref regex_str) = leaf.host_regex {
if regex::Regex::new(regex_str).is_err() {
errors.push(ConfigError::validation(
&format!("{}.host_regex", path),
&format!("invalid regex: {}", regex_str),
));
}
}
if let Some(ref regex_str) = leaf.destination_port_regex {
if regex::Regex::new(regex_str).is_err() {
errors.push(ConfigError::validation(
&format!("{}.destination_port_regex", path),
&format!("invalid regex: {}", regex_str),
));
}
}
if let Some(ref cidr) = leaf.destination_cidr {
if cidr.parse::<ipnet::IpNet>().is_err() {
errors.push(ConfigError::validation(
&format!("{}.destination_cidr", path),
&format!("invalid CIDR: {}", cidr),
));
}
}
if let Some(ref cidr) = leaf.source_cidr {
if cidr.parse::<ipnet::IpNet>().is_err() {
errors.push(ConfigError::validation(
&format!("{}.source_cidr", path),
&format!("invalid CIDR: {}", cidr),
));
}
}
if let Some(ref range) = leaf.destination_port_range {
if range.len() != 2 {
errors.push(ConfigError::validation(
&format!("{}.destination_port_range", path),
"must have exactly 2 elements [start, end]",
));
} else if range[0] > range[1] {
errors.push(ConfigError::validation(
&format!("{}.destination_port_range", path),
&format!("start ({}) must be <= end ({})", range[0], range[1]),
));
}
}
if let Some(ref ports) = leaf.destination_port_set {
if ports.is_empty() {
errors.push(ConfigError::validation(
&format!("{}.destination_port_set", path),
"must not be empty",
));
}
}
if let Some(ref proto) = leaf.protocol {
if !VALID_PROTOCOLS.contains(&proto.as_str()) && proto != "httponly" {
errors.push(ConfigError::validation(
&format!("{}.protocol", path),
&format!("unknown protocol: {}", proto),
));
}
}
}
}
}