- Created T026-practical-test task.yaml for MVP smoke testing - Added k8shost-server to flake.nix (packages, apps, overlays) - Staged all workspace directories for nix flake build - Updated flake.nix shellHook to include k8shost Resolves: T026.S1 blocker (R8 - nix submodule visibility)
428 lines
13 KiB
Rust
428 lines
13 KiB
Rust
//! ACL Rule Translation for OVN
|
|
//!
|
|
//! This module translates SecurityGroupRule objects into OVN ACL match expressions.
|
|
//! It supports TCP, UDP, ICMP, and wildcard protocols with port ranges and CIDR matching.
|
|
|
|
use novanet_types::{IpProtocol, RuleDirection, SecurityGroupRule};
|
|
|
|
/// Build OVN ACL match expression from a SecurityGroupRule
|
|
///
|
|
/// # Arguments
|
|
/// * `rule` - The security group rule to translate
|
|
/// * `port_name` - Optional logical port name to include in match (e.g., "port-123")
|
|
///
|
|
/// # Returns
|
|
/// A complete OVN match expression string
|
|
///
|
|
/// # Examples
|
|
/// ```
|
|
/// use novanet_types::{SecurityGroupRule, SecurityGroupId, RuleDirection, IpProtocol};
|
|
/// use novanet_server::ovn::acl::build_acl_match;
|
|
///
|
|
/// let mut rule = SecurityGroupRule::new(
|
|
/// SecurityGroupId::new(),
|
|
/// RuleDirection::Ingress,
|
|
/// IpProtocol::Tcp,
|
|
/// );
|
|
/// rule.port_range_min = Some(80);
|
|
/// rule.port_range_max = Some(80);
|
|
/// rule.remote_cidr = Some("10.0.0.0/8".to_string());
|
|
///
|
|
/// let match_expr = build_acl_match(&rule, Some("port-123"));
|
|
/// // Result: "inport == \"port-123\" && ip4 && tcp && tcp.dst == 80 && ip4.src == 10.0.0.0/8"
|
|
/// ```
|
|
pub fn build_acl_match(rule: &SecurityGroupRule, port_name: Option<&str>) -> String {
|
|
let mut parts = Vec::new();
|
|
|
|
// Add port constraint if provided
|
|
if let Some(port) = port_name {
|
|
match rule.direction {
|
|
RuleDirection::Ingress => {
|
|
parts.push(format!("inport == \"{}\"", port));
|
|
}
|
|
RuleDirection::Egress => {
|
|
parts.push(format!("outport == \"{}\"", port));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add IP version (we only support IPv4 for now)
|
|
parts.push("ip4".to_string());
|
|
|
|
// Add protocol-specific matching
|
|
match rule.protocol {
|
|
IpProtocol::Tcp => {
|
|
parts.push("tcp".to_string());
|
|
if let Some(port_match) = build_port_match("tcp", rule) {
|
|
parts.push(port_match);
|
|
}
|
|
}
|
|
IpProtocol::Udp => {
|
|
parts.push("udp".to_string());
|
|
if let Some(port_match) = build_port_match("udp", rule) {
|
|
parts.push(port_match);
|
|
}
|
|
}
|
|
IpProtocol::Icmp => {
|
|
parts.push("icmp4".to_string());
|
|
}
|
|
IpProtocol::Icmpv6 => {
|
|
parts.push("icmp6".to_string());
|
|
}
|
|
IpProtocol::Any => {
|
|
// No additional protocol constraint, just ip4
|
|
}
|
|
}
|
|
|
|
// Add CIDR matching based on direction
|
|
if let Some(cidr) = &rule.remote_cidr {
|
|
let cidr_match = match rule.direction {
|
|
RuleDirection::Ingress => format!("ip4.src == {}", cidr),
|
|
RuleDirection::Egress => format!("ip4.dst == {}", cidr),
|
|
};
|
|
parts.push(cidr_match);
|
|
}
|
|
|
|
parts.join(" && ")
|
|
}
|
|
|
|
/// Build port matching expression for TCP/UDP
|
|
fn build_port_match(protocol: &str, rule: &SecurityGroupRule) -> Option<String> {
|
|
match (rule.port_range_min, rule.port_range_max) {
|
|
(Some(min), Some(max)) if min == max => {
|
|
// Single port
|
|
Some(format!("{}.dst == {}", protocol, min))
|
|
}
|
|
(Some(min), Some(max)) if min < max => {
|
|
// Port range
|
|
Some(format!(
|
|
"{}.dst >= {} && {}.dst <= {}",
|
|
protocol, min, protocol, max
|
|
))
|
|
}
|
|
(Some(min), None) => {
|
|
// Only min specified, treat as single port
|
|
Some(format!("{}.dst == {}", protocol, min))
|
|
}
|
|
(None, Some(max)) => {
|
|
// Only max specified, treat as upper bound
|
|
Some(format!("{}.dst <= {}", protocol, max))
|
|
}
|
|
(None, None) => {
|
|
// No port constraint, match any port
|
|
None
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Get OVN direction string from rule direction
|
|
///
|
|
/// OVN uses "to-lport" for ingress (traffic TO the port) and
|
|
/// "from-lport" for egress (traffic FROM the port)
|
|
pub fn rule_direction_to_ovn(direction: &RuleDirection) -> &'static str {
|
|
match direction {
|
|
RuleDirection::Ingress => "to-lport",
|
|
RuleDirection::Egress => "from-lport",
|
|
}
|
|
}
|
|
|
|
/// Calculate ACL priority based on rule specificity
|
|
///
|
|
/// More specific rules get higher priority:
|
|
/// - Protocol + CIDR + port range: 1000
|
|
/// - Protocol + CIDR or port: 800-900
|
|
/// - Protocol only: 700
|
|
/// - Any protocol: 600
|
|
pub fn calculate_priority(rule: &SecurityGroupRule) -> u16 {
|
|
let mut priority = 600; // Base priority for "any" protocol
|
|
|
|
let has_port = rule.port_range_min.is_some() || rule.port_range_max.is_some();
|
|
let has_cidr = rule.remote_cidr.is_some();
|
|
|
|
// Protocol specificity
|
|
match rule.protocol {
|
|
IpProtocol::Any => {}
|
|
IpProtocol::Tcp | IpProtocol::Udp | IpProtocol::Icmp | IpProtocol::Icmpv6 => {
|
|
priority += 100; // Protocol only: 700
|
|
}
|
|
}
|
|
|
|
// Port and/or CIDR add specificity
|
|
if has_port && has_cidr {
|
|
// Both port and CIDR: most specific
|
|
priority += 300; // Total: 1000
|
|
} else if has_port || has_cidr {
|
|
// Either port or CIDR
|
|
priority += 100; // Total: 800
|
|
}
|
|
|
|
priority
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use novanet_types::{SecurityGroupId, SecurityGroupRule};
|
|
|
|
#[test]
|
|
fn test_tcp_single_port() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(80);
|
|
rule.port_range_max = Some(80);
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("tcp"));
|
|
assert!(match_expr.contains("tcp.dst == 80"));
|
|
assert!(match_expr.contains("ip4"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_tcp_port_range() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(1024);
|
|
rule.port_range_max = Some(65535);
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("tcp.dst >= 1024"));
|
|
assert!(match_expr.contains("tcp.dst <= 65535"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_udp_single_port() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Udp,
|
|
);
|
|
rule.port_range_min = Some(53);
|
|
rule.port_range_max = Some(53);
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("udp"));
|
|
assert!(match_expr.contains("udp.dst == 53"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_udp_port_range() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Egress,
|
|
IpProtocol::Udp,
|
|
);
|
|
rule.port_range_min = Some(5000);
|
|
rule.port_range_max = Some(6000);
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("udp.dst >= 5000"));
|
|
assert!(match_expr.contains("udp.dst <= 6000"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_icmp_protocol() {
|
|
let rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Icmp,
|
|
);
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("icmp4"));
|
|
assert!(match_expr.contains("ip4"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_any_protocol() {
|
|
let rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Any,
|
|
);
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("ip4"));
|
|
assert!(!match_expr.contains("tcp"));
|
|
assert!(!match_expr.contains("udp"));
|
|
assert!(!match_expr.contains("icmp"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_cidr_ingress() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Any,
|
|
);
|
|
rule.remote_cidr = Some("10.0.0.0/8".to_string());
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("ip4.src == 10.0.0.0/8"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_cidr_egress() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Egress,
|
|
IpProtocol::Any,
|
|
);
|
|
rule.remote_cidr = Some("192.168.0.0/16".to_string());
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("ip4.dst == 192.168.0.0/16"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_complete_rule_with_port() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(443);
|
|
rule.port_range_max = Some(443);
|
|
rule.remote_cidr = Some("0.0.0.0/0".to_string());
|
|
|
|
let match_expr = build_acl_match(&rule, Some("port-123"));
|
|
assert!(match_expr.contains("inport == \"port-123\""));
|
|
assert!(match_expr.contains("ip4"));
|
|
assert!(match_expr.contains("tcp"));
|
|
assert!(match_expr.contains("tcp.dst == 443"));
|
|
assert!(match_expr.contains("ip4.src == 0.0.0.0/0"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_egress_with_port_name() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Egress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(22);
|
|
rule.port_range_max = Some(22);
|
|
|
|
let match_expr = build_acl_match(&rule, Some("port-456"));
|
|
assert!(match_expr.contains("outport == \"port-456\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_direction_to_ovn() {
|
|
assert_eq!(
|
|
rule_direction_to_ovn(&RuleDirection::Ingress),
|
|
"to-lport"
|
|
);
|
|
assert_eq!(
|
|
rule_direction_to_ovn(&RuleDirection::Egress),
|
|
"from-lport"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_any_protocol() {
|
|
let rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Any,
|
|
);
|
|
assert_eq!(calculate_priority(&rule), 600);
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_with_protocol() {
|
|
let rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
assert_eq!(calculate_priority(&rule), 700);
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_with_port() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(80);
|
|
rule.port_range_max = Some(80);
|
|
assert_eq!(calculate_priority(&rule), 800);
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_with_cidr() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.remote_cidr = Some("10.0.0.0/8".to_string());
|
|
assert_eq!(calculate_priority(&rule), 800);
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_full_specificity() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(443);
|
|
rule.port_range_max = Some(443);
|
|
rule.remote_cidr = Some("10.0.0.0/8".to_string());
|
|
assert_eq!(calculate_priority(&rule), 1000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_port_only_min() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(8080);
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("tcp.dst == 8080"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_ssh_rule_example() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(22);
|
|
rule.port_range_max = Some(22);
|
|
rule.remote_cidr = Some("203.0.113.0/24".to_string());
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("tcp"));
|
|
assert!(match_expr.contains("tcp.dst == 22"));
|
|
assert!(match_expr.contains("ip4.src == 203.0.113.0/24"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_http_https_range() {
|
|
let mut rule = SecurityGroupRule::new(
|
|
SecurityGroupId::new(),
|
|
RuleDirection::Ingress,
|
|
IpProtocol::Tcp,
|
|
);
|
|
rule.port_range_min = Some(80);
|
|
rule.port_range_max = Some(443);
|
|
|
|
let match_expr = build_acl_match(&rule, None);
|
|
assert!(match_expr.contains("tcp.dst >= 80"));
|
|
assert!(match_expr.contains("tcp.dst <= 443"));
|
|
}
|
|
}
|