diff --git a/virtweb_backend/src/controllers/nwfilter_controller.rs b/virtweb_backend/src/controllers/nwfilter_controller.rs index e8eb53d..57d0b6a 100644 --- a/virtweb_backend/src/controllers/nwfilter_controller.rs +++ b/virtweb_backend/src/controllers/nwfilter_controller.rs @@ -1,3 +1,4 @@ +use crate::constants; use crate::controllers::{HttpResult, LibVirtReq}; use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_rest_structures::nw_filter::NetworkFilter; @@ -8,6 +9,37 @@ pub struct NetworkFilterID { uid: XMLUuid, } +/// Create a new nw filter +pub async fn create(client: LibVirtReq, req: web::Json) -> HttpResult { + let network = match req.0.rest2lib() { + Ok(d) => d, + Err(e) => { + log::error!("Failed to extract network filter info! {e}"); + return Ok(HttpResponse::BadRequest() + .json(format!("Failed to extract network filter info! {e}"))); + } + }; + + if constants::BUILTIN_NETWORK_FILTER_RULES.contains(&network.name.as_str()) { + return Ok(HttpResponse::ExpectationFailed() + .json("Builtin network filter rules shall not be modified!")); + } + + // TODO : remove + return Ok(HttpResponse::Ok().json(network)); + + let uid = match client.update_network_filter(req.0, network).await { + Ok(u) => u, + Err(e) => { + log::error!("Failed to update network filter! {e}"); + return Ok(HttpResponse::InternalServerError() + .json(format!("Failed to update network filter! {e}"))); + } + }; + + Ok(HttpResponse::Ok().json(NetworkFilterID { uid })) +} + /// Get the list of network filters pub async fn list(client: LibVirtReq) -> HttpResult { let networks = match client.get_full_network_filters_list().await { @@ -21,7 +53,7 @@ pub async fn list(client: LibVirtReq) -> HttpResult { let networks = networks .into_iter() - .map(|n| NetworkFilter::from_xml(n).unwrap()) + .map(|n| NetworkFilter::lib2rest(n).unwrap()) .collect::>(); Ok(HttpResponse::Ok().json(networks)) @@ -29,6 +61,36 @@ pub async fn list(client: LibVirtReq) -> HttpResult { /// Get the information about a single network filter pub async fn get_single(client: LibVirtReq, req: web::Path) -> HttpResult { - let nwfilter = NetworkFilter::from_xml(client.get_single_network_filter(req.uid).await?)?; + let nwfilter = NetworkFilter::lib2rest(client.get_single_network_filter(req.uid).await?)?; Ok(HttpResponse::Ok().json(nwfilter)) } + +/// Update the information about a single network filter +pub async fn update( + client: LibVirtReq, + path: web::Path, + body: web::Json, +) -> HttpResult { + let mut network = match body.0.rest2lib() { + Ok(n) => n, + Err(e) => { + log::error!("Failed to extract network filter info! {e}"); + return Ok(HttpResponse::BadRequest() + .json(format!("Failed to extract network filter info!\n${e}"))); + } + }; + network.uuid = Some(path.uid); + + if constants::BUILTIN_NETWORK_FILTER_RULES.contains(&network.name.as_str()) { + return Ok(HttpResponse::ExpectationFailed() + .json("Builtin network filter rules shall not be modified!")); + } + + if let Err(e) = client.update_network_filter(body.0, network).await { + log::error!("Failed to update network filter! {e}"); + return Ok(HttpResponse::InternalServerError() + .json(format!("Failed to update network filter!\n${e}"))); + } + + Ok(HttpResponse::Ok().json("Network filter updated")) +} diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs index 51f4660..a94acd4 100644 --- a/virtweb_backend/src/libvirt_client.rs +++ b/virtweb_backend/src/libvirt_client.rs @@ -6,6 +6,7 @@ use crate::libvirt_lib_structures::nwfilter::NetworkFilterXML; use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; use crate::libvirt_rest_structures::net::NetworkInfo; +use crate::libvirt_rest_structures::nw_filter::NetworkFilter; use crate::libvirt_rest_structures::vm::VMInfo; use actix::Addr; @@ -185,4 +186,14 @@ impl LibVirtClient { pub async fn get_single_network_filter(&self, id: XMLUuid) -> anyhow::Result { self.0.send(libvirt_actor::GetNWFilterXMLReq(id)).await? } + + /// Update the information about a single domain + pub async fn update_network_filter( + &self, + _vm_def: NetworkFilter, + xml: NetworkFilterXML, + ) -> anyhow::Result { + println!("nwfilter xml to update: {:#?}", xml); + todo!() + } } diff --git a/virtweb_backend/src/libvirt_lib_structures/nwfilter.rs b/virtweb_backend/src/libvirt_lib_structures/nwfilter.rs index 2359272..a7cd628 100644 --- a/virtweb_backend/src/libvirt_lib_structures/nwfilter.rs +++ b/virtweb_backend/src/libvirt_lib_structures/nwfilter.rs @@ -238,60 +238,60 @@ pub struct NetworkFilterRuleXML { pub priority: Option, /// Match all protocols - #[serde(skip_serializing_if = "Option::is_none")] - pub all: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub all: Vec, /// Match mac protocol #[serde(default, rename = "mac", skip_serializing_if = "Vec::is_empty")] - pub mac_rules: Vec, + pub mac_selectors: Vec, /// Match arp protocol #[serde(default, rename = "arp", skip_serializing_if = "Vec::is_empty")] - pub arp_rules: Vec, + pub arp_selectors: Vec, /// Match rarp protocol #[serde(default, rename = "arp", skip_serializing_if = "Vec::is_empty")] - pub rarp_rules: Vec, + pub rarp_selectors: Vec, /// Match IPv4 protocol #[serde(default, rename = "ip", skip_serializing_if = "Vec::is_empty")] - pub ipv4_rules: Vec, + pub ipv4_selectors: Vec, /// Match IPv6 protocol #[serde(default, rename = "ipv6", skip_serializing_if = "Vec::is_empty")] - pub ipv6_rules: Vec, + pub ipv6_selectors: Vec, /// Match TCP protocol #[serde(default, rename = "tcp", skip_serializing_if = "Vec::is_empty")] - pub tcp_rules: Vec>, + pub tcp_selectors: Vec>, /// Match UDP protocol #[serde(default, rename = "udp", skip_serializing_if = "Vec::is_empty")] - pub udp_rules: Vec>, + pub udp_selectors: Vec>, /// Match SCTP protocol #[serde(default, rename = "sctp", skip_serializing_if = "Vec::is_empty")] - pub sctp_rules: Vec>, + pub sctp_selectors: Vec>, /// Match ICMP protocol #[serde(default, rename = "icmp", skip_serializing_if = "Vec::is_empty")] - pub imcp_rules: Vec>, + pub imcp_selectors: Vec>, /// Match TCP IPv6 protocol #[serde(default, rename = "tcp-ipv6", skip_serializing_if = "Vec::is_empty")] - pub tcp_ipv6_rules: Vec>, + pub tcp_ipv6_selectors: Vec>, /// Match UDP IPv6 protocol #[serde(default, rename = "udp-ipv6", skip_serializing_if = "Vec::is_empty")] - pub udp_ipv6_rules: Vec>, + pub udp_ipv6_selectors: Vec>, /// Match SCTP IPv6 protocol #[serde(default, rename = "sctp-ipv6", skip_serializing_if = "Vec::is_empty")] - pub sctp_ipv6_rules: Vec>, + pub sctp_ipv6_selectors: Vec>, /// Match ICMP IPv6 protocol #[serde(default, rename = "icmpv6", skip_serializing_if = "Vec::is_empty")] - pub imcp_ipv6_rules: Vec>, + pub imcp_ipv6_selectors: Vec>, } #[derive(serde::Serialize, serde::Deserialize, Debug)] @@ -299,8 +299,12 @@ pub struct NetworkFilterRuleXML { pub struct NetworkFilterXML { #[serde(rename(serialize = "@name"))] pub name: String, - #[serde(rename(serialize = "@chain"), default)] - pub chain: String, + #[serde( + rename(serialize = "@chain"), + skip_serializing_if = "Option::is_none", + default + )] + pub chain: Option, #[serde( skip_serializing_if = "Option::is_none", rename(serialize = "@priority"), diff --git a/virtweb_backend/src/libvirt_rest_structures/nw_filter.rs b/virtweb_backend/src/libvirt_rest_structures/nw_filter.rs index 5642ef8..ce5e6d7 100644 --- a/virtweb_backend/src/libvirt_rest_structures/nw_filter.rs +++ b/virtweb_backend/src/libvirt_rest_structures/nw_filter.rs @@ -1,12 +1,25 @@ use crate::libvirt_lib_structures::nwfilter::{ - NetworkFilterRuleProtocolAll, NetworkFilterRuleProtocolArp, NetworkFilterRuleProtocolIpvx, - NetworkFilterRuleProtocolLayer4, NetworkFilterRuleProtocolMac, NetworkFilterXML, + NetworkFilterRefXML, NetworkFilterRuleProtocolAll, NetworkFilterRuleProtocolArp, + NetworkFilterRuleProtocolIpvx, NetworkFilterRuleProtocolLayer4, NetworkFilterRuleProtocolMac, + NetworkFilterRuleXML, NetworkFilterXML, }; use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_rest_structures::LibVirtStructError; +use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; +use lazy_regex::regex; use std::net::{Ipv4Addr, Ipv6Addr}; +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct NetworkFilterName(pub String); + +impl NetworkFilterName { + pub fn is_valid(&self) -> bool { + regex!("^[a-zA-Z0-9-_]+$").is_match(&self.0) + } +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] +#[serde(rename_all = "lowercase")] pub enum NetworkFilterChainProtocol { Root, Mac, @@ -60,12 +73,8 @@ pub struct NetworkFilterChain { } impl NetworkFilterChain { - pub fn from_xml(xml: &str) -> anyhow::Result> { - if xml.is_empty() { - return Ok(None); - } - - Ok(Some(match xml.split_once('-') { + pub fn from_xml(xml: &str) -> anyhow::Result { + Ok(match xml.split_once('-') { None => Self { protocol: NetworkFilterChainProtocol::from_xml(xml)?, suffix: None, @@ -74,7 +83,7 @@ impl NetworkFilterChain { protocol: NetworkFilterChainProtocol::from_xml(prefix)?, suffix: Some(suffix.to_string()), }, - })) + }) } pub fn to_xml(&self) -> String { @@ -88,31 +97,31 @@ impl NetworkFilterChain { /// Network filter definition #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct NetworkFilter { - name: String, + name: NetworkFilterName, chain: Option, priority: Option, uuid: Option, /// Referenced filters rules - join_rules: Vec, + join_filters: Vec, rules: Vec, } impl NetworkFilter { - fn process_all_rule(_n: &NetworkFilterRuleProtocolAll) -> NetworkFilterSelector { + fn lib2rest_process_all_rule(_n: &NetworkFilterRuleProtocolAll) -> NetworkFilterSelector { NetworkFilterSelector::All } - fn process_mac_rule(n: &NetworkFilterRuleProtocolMac) -> NetworkFilterSelector { - NetworkFilterSelector::Mac { + fn lib2rest_process_mac_rule(n: &NetworkFilterRuleProtocolMac) -> NetworkFilterSelector { + NetworkFilterSelector::Mac(NetworkSelectorMac { src_mac_addr: n.srcmacaddr.clone(), src_mac_mask: n.srcmacmask.clone(), dst_mac_addr: n.dstmacaddr.clone(), dst_mac_mask: n.dstmacmask.clone(), comment: n.comment.clone(), - } + }) } - fn process_arp_rule(n: &NetworkFilterRuleProtocolArp) -> NetworkSelectorARP { + fn lib2rest_process_arp_rule(n: &NetworkFilterRuleProtocolArp) -> NetworkSelectorARP { NetworkSelectorARP { srcmacaddr: n.srcmacaddr.clone(), srcmacmask: n.srcmacmask.clone(), @@ -126,7 +135,7 @@ impl NetworkFilter { } } - fn process_ip_rule(n: &NetworkFilterRuleProtocolIpvx) -> NetworkFilterSelectorIP { + fn lib2rest_process_ip_rule(n: &NetworkFilterRuleProtocolIpvx) -> NetworkFilterSelectorIP { NetworkFilterSelectorIP { srcmacaddr: n.srcmacaddr.clone(), srcmacmask: n.srcmacmask.clone(), @@ -140,7 +149,7 @@ impl NetworkFilter { } } - fn process_layer4_rule( + fn lib2rest_process_layer4_rule( n: &NetworkFilterRuleProtocolLayer4, ) -> anyhow::Result> { Ok(NetworkSelectorLayer4 { @@ -162,120 +171,148 @@ impl NetworkFilter { }) } - pub fn from_xml(xml: NetworkFilterXML) -> anyhow::Result { + pub fn lib2rest(xml: NetworkFilterXML) -> anyhow::Result { let mut rules = Vec::with_capacity(xml.rules.len()); for rule in &xml.rules { let mut selectors = Vec::new(); // All selector - selectors.append(&mut rule.all.iter().map(Self::process_all_rule).collect()); + selectors.append( + &mut rule + .all + .iter() + .map(Self::lib2rest_process_all_rule) + .collect(), + ); // Mac rules - selectors.append(&mut rule.mac_rules.iter().map(Self::process_mac_rule).collect()); + selectors.append( + &mut rule + .mac_selectors + .iter() + .map(Self::lib2rest_process_mac_rule) + .collect(), + ); // ARP - RARP rules selectors.append( &mut rule - .arp_rules + .arp_selectors .iter() - .map(|r| NetworkFilterSelector::Arp(Self::process_arp_rule(r))) + .map(|r| NetworkFilterSelector::Arp(Self::lib2rest_process_arp_rule(r))) .collect(), ); selectors.append( &mut rule - .rarp_rules + .rarp_selectors .iter() - .map(|r| NetworkFilterSelector::Rarp(Self::process_arp_rule(r))) + .map(|r| NetworkFilterSelector::Rarp(Self::lib2rest_process_arp_rule(r))) .collect(), ); // IPv4 - IPv6 rules selectors.append( &mut rule - .ipv4_rules + .ipv4_selectors .iter() - .map(|r| NetworkFilterSelector::IPv4(Self::process_ip_rule(r))) + .map(|r| NetworkFilterSelector::IPv4(Self::lib2rest_process_ip_rule(r))) .collect(), ); selectors.append( &mut rule - .ipv6_rules + .ipv6_selectors .iter() - .map(|r| NetworkFilterSelector::IPv6(Self::process_ip_rule(r))) + .map(|r| NetworkFilterSelector::IPv6(Self::lib2rest_process_ip_rule(r))) .collect(), ); // Layer 4 protocols selectors.append( &mut rule - .tcp_rules + .tcp_selectors .iter() - .map(|r| Ok(NetworkFilterSelector::TCP(Self::process_layer4_rule(r)?))) + .map(|r| { + Ok(NetworkFilterSelector::TCP( + Self::lib2rest_process_layer4_rule(r)?, + )) + }) .collect::, anyhow::Error>>()?, ); selectors.append( &mut rule - .udp_rules + .udp_selectors .iter() - .map(|r| Ok(NetworkFilterSelector::UDP(Self::process_layer4_rule(r)?))) + .map(|r| { + Ok(NetworkFilterSelector::UDP( + Self::lib2rest_process_layer4_rule(r)?, + )) + }) .collect::, anyhow::Error>>()?, ); selectors.append( &mut rule - .sctp_rules + .sctp_selectors .iter() - .map(|r| Ok(NetworkFilterSelector::SCTP(Self::process_layer4_rule(r)?))) + .map(|r| { + Ok(NetworkFilterSelector::SCTP( + Self::lib2rest_process_layer4_rule(r)?, + )) + }) .collect::, anyhow::Error>>()?, ); selectors.append( &mut rule - .imcp_rules + .imcp_selectors .iter() - .map(|r| Ok(NetworkFilterSelector::ICMP(Self::process_layer4_rule(r)?))) + .map(|r| { + Ok(NetworkFilterSelector::ICMP( + Self::lib2rest_process_layer4_rule(r)?, + )) + }) .collect::, anyhow::Error>>()?, ); selectors.append( &mut rule - .tcp_ipv6_rules + .tcp_ipv6_selectors .iter() .map(|r| { - Ok(NetworkFilterSelector::TCPipv6(Self::process_layer4_rule( - r, - )?)) + Ok(NetworkFilterSelector::TCPipv6( + Self::lib2rest_process_layer4_rule(r)?, + )) }) .collect::, anyhow::Error>>()?, ); selectors.append( &mut rule - .udp_ipv6_rules + .udp_ipv6_selectors .iter() .map(|r| { - Ok(NetworkFilterSelector::UDPipv6(Self::process_layer4_rule( - r, - )?)) + Ok(NetworkFilterSelector::UDPipv6( + Self::lib2rest_process_layer4_rule(r)?, + )) }) .collect::, anyhow::Error>>()?, ); selectors.append( &mut rule - .sctp_ipv6_rules + .sctp_ipv6_selectors .iter() .map(|r| { - Ok(NetworkFilterSelector::SCTPipv6(Self::process_layer4_rule( - r, - )?)) + Ok(NetworkFilterSelector::SCTPipv6( + Self::lib2rest_process_layer4_rule(r)?, + )) }) .collect::, anyhow::Error>>()?, ); selectors.append( &mut rule - .imcp_ipv6_rules + .imcp_ipv6_selectors .iter() .map(|r| { - Ok(NetworkFilterSelector::ICMPipv6(Self::process_layer4_rule( - r, - )?)) + Ok(NetworkFilterSelector::ICMPipv6( + Self::lib2rest_process_layer4_rule(r)?, + )) }) .collect::, anyhow::Error>>()?, ); @@ -289,21 +326,123 @@ impl NetworkFilter { } Ok(Self { - name: xml.name, + name: NetworkFilterName(xml.name), uuid: xml.uuid, - chain: NetworkFilterChain::from_xml(&xml.chain)?, + chain: xml + .chain + .as_deref() + .map(NetworkFilterChain::from_xml) + .transpose()?, priority: xml.priority, - join_rules: xml + join_filters: xml .filterrefs .iter() - .map(|i| i.filter.to_string()) + .map(|i| NetworkFilterName(i.filter.to_string())) .collect(), rules, }) } + + fn rest2lib_process_rule(rule: &NetworkFilterRule) -> anyhow::Result { + let mut rule_xml = NetworkFilterRuleXML { + action: rule.action.to_xml(), + direction: rule.direction.to_xml(), + priority: rule.priority, + all: vec![], + mac_selectors: vec![], + arp_selectors: vec![], + rarp_selectors: vec![], + ipv4_selectors: vec![], + ipv6_selectors: vec![], + tcp_selectors: vec![], + udp_selectors: vec![], + sctp_selectors: vec![], + imcp_selectors: vec![], + tcp_ipv6_selectors: vec![], + udp_ipv6_selectors: vec![], + sctp_ipv6_selectors: vec![], + imcp_ipv6_selectors: vec![], + }; + + for sel in &rule.selectors { + match sel { + NetworkFilterSelector::All => { + rule_xml.all.push(NetworkFilterRuleProtocolAll {}); + } + + NetworkFilterSelector::Mac(mac) => { + todo!() + /*rule_xml.mac_selectors.push(NetworkFilterRuleProtocolMac { + srcmacaddr: mac.src_mac_addr, + srcmacmask: mac.src_mac_mask, + dstmacaddr: mac., + dstmacmask: None, + comment: None, + })*/ + } + + NetworkFilterSelector::Arp(_) => todo!(), + NetworkFilterSelector::Rarp(_) => todo!(), + NetworkFilterSelector::IPv4(_) => todo!(), + NetworkFilterSelector::IPv6(_) => todo!(), + NetworkFilterSelector::TCP(_) => todo!(), + NetworkFilterSelector::UDP(_) => todo!(), + NetworkFilterSelector::SCTP(_) => todo!(), + NetworkFilterSelector::ICMP(_) => todo!(), + NetworkFilterSelector::TCPipv6(_) => todo!(), + NetworkFilterSelector::UDPipv6(_) => todo!(), + NetworkFilterSelector::SCTPipv6(_) => todo!(), + NetworkFilterSelector::ICMPipv6(_) => todo!(), + } + } + + Ok(rule_xml) + } + + pub fn rest2lib(&self) -> anyhow::Result { + if !self.name.is_valid() { + return Err(StructureExtraction("Network filter name is invalid!").into()); + } + + if let Some(priority) = self.priority { + if !(-1000..=1000).contains(&priority) { + return Err(StructureExtraction("Network priority is invalid!").into()); + } + } + + for fref in &self.join_filters { + if !fref.is_valid() { + return Err( + StructureExtraction("Referenced network filter name is invalid!").into(), + ); + } + } + + let mut rules = Vec::with_capacity(self.rules.len()); + + for rule in &self.rules { + rules.push(Self::rest2lib_process_rule(rule)?); + } + + Ok(NetworkFilterXML { + name: self.name.0.to_string(), + uuid: self.uuid, + chain: self.chain.as_ref().map(|c| c.to_xml()), + priority: self.priority, + filterrefs: self + .join_filters + .iter() + .map(|jf| NetworkFilterRefXML { + filter: jf.0.to_string(), + }) + .collect::>(), + rules, + }) + } } #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] +#[serde(rename_all = "lowercase")] pub enum NetworkFilterAction { /// matching the rule silently discards the packet with no further analysis Drop, @@ -348,6 +487,7 @@ impl NetworkFilterAction { } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] pub enum NetworkFilterDirection { In, Out, @@ -430,6 +570,15 @@ pub struct NetworkFilterSelectorIP { comment: Option, } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct NetworkSelectorMac { + src_mac_addr: Option, + src_mac_mask: Option, + dst_mac_addr: Option, + dst_mac_mask: Option, + comment: Option, +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct NetworkSelectorARP { srcmacaddr: Option, @@ -467,15 +616,10 @@ pub struct NetworkSelectorLayer4 { } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", rename_all = "lowercase")] pub enum NetworkFilterSelector { All, - Mac { - src_mac_addr: Option, - src_mac_mask: Option, - dst_mac_addr: Option, - dst_mac_mask: Option, - comment: Option, - }, + Mac(NetworkSelectorMac), Arp(NetworkSelectorARP), Rarp(NetworkSelectorARP), IPv4(NetworkFilterSelectorIP), diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 0b0c261..e73ed34 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -240,6 +240,10 @@ async fn main() -> std::io::Result<()> { web::get().to(network_controller::stop), ) // Network filters controller + .route( + "/api/nwfilter/create", + web::post().to(nwfilter_controller::create), + ) .route( "/api/nwfilter/list", web::get().to(nwfilter_controller::list), @@ -248,6 +252,10 @@ async fn main() -> std::io::Result<()> { "/api/nwfilter/{uid}", web::get().to(nwfilter_controller::get_single), ) + .route( + "/api/nwfilter/{uid}", + web::put().to(nwfilter_controller::update), + ) // Static assets .route("/", web::get().to(static_controller::root_index)) .route( diff --git a/virtweb_backend/src/utils/net_utils.rs b/virtweb_backend/src/utils/net_utils.rs index b0bd80e..d01d261 100644 --- a/virtweb_backend/src/utils/net_utils.rs +++ b/virtweb_backend/src/utils/net_utils.rs @@ -17,3 +17,22 @@ pub fn extract_ipv6(ip: IpAddr) -> Ipv6Addr { IpAddr::V6(i) => i, } } + +pub fn is_mac_address_valid>(mac: D) -> bool { + lazy_regex::regex!("^([a-fA-F0-9]{2}[:-]){5}[a-fA-F0-9]{2}$").is_match(mac.as_ref()) +} + +#[cfg(test)] +mod tests { + use crate::utils::net_utils::is_mac_address_valid; + + #[test] + fn mac_addresses() { + assert!(is_mac_address_valid("FF:FF:FF:FF:FF:FF")); + assert!(is_mac_address_valid("02:42:a4:6e:f2:be")); + + assert!(!is_mac_address_valid("tata")); + assert!(!is_mac_address_valid("FF:FF:FF:FF:FF:FZ")); + assert!(!is_mac_address_valid("FF:FF:FF:FF:FF:FF:FF")); + } +}