//! 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 { 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")); } }