photoncloud-monorepo/novanet/crates/novanet-server/src/ovn/acl.rs
centra a7ec7e2158 Add T026 practical test + k8shost to flake + workspace files
- 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)
2025-12-09 06:07:50 +09:00

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