From 3849b0d51de61b0cba0d3bebf57ac60b03459de6 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 28 Dec 2023 15:12:38 +0100 Subject: [PATCH] Parse NW filters XML structure --- virtweb_backend/src/actors/libvirt_actor.rs | 41 ++- virtweb_backend/src/constants.rs | 28 ++ virtweb_backend/src/controllers/mod.rs | 1 + .../src/controllers/nwfilter_controller.rs | 36 ++ .../src/controllers/server_controller.rs | 2 + virtweb_backend/src/libvirt_client.rs | 19 +- virtweb_backend/src/libvirt_lib_structures.rs | 314 ++++++++++++++++++ .../src/libvirt_rest_structures.rs | 138 ++++++++ virtweb_backend/src/main.rs | 13 +- 9 files changed, 588 insertions(+), 4 deletions(-) create mode 100644 virtweb_backend/src/controllers/nwfilter_controller.rs diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs index 55e548b..05127cf 100644 --- a/virtweb_backend/src/actors/libvirt_actor.rs +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -1,5 +1,7 @@ use crate::app_config::AppConfig; -use crate::libvirt_lib_structures::{DomainState, DomainXML, NetworkXML, XMLUuid}; +use crate::libvirt_lib_structures::{ + DomainState, DomainXML, NetworkFilterXML, NetworkXML, XMLUuid, +}; use crate::libvirt_rest_structures::*; use actix::{Actor, Context, Handler, Message}; use image::ImageOutputFormat; @@ -7,6 +9,7 @@ use std::io::Cursor; use virt::connect::Connect; use virt::domain::Domain; use virt::network::Network; +use virt::nwfilter::NWFilter; use virt::stream::Stream; use virt::sys; use virt::sys::VIR_DOMAIN_XML_SECURE; @@ -549,3 +552,39 @@ impl Handler for LibVirtActor { Ok(()) } } + +#[derive(Message)] +#[rtype(result = "anyhow::Result>")] +pub struct GetNWFiltersListReq; + +impl Handler for LibVirtActor { + type Result = anyhow::Result>; + + fn handle(&mut self, _msg: GetNWFiltersListReq, _ctx: &mut Self::Context) -> Self::Result { + log::debug!("Get full list of network filters"); + let networks = self.m.list_all_nw_filters(0)?; + let mut ids = Vec::with_capacity(networks.len()); + + for d in networks { + ids.push(XMLUuid::parse_from_str(&d.get_uuid_string()?)?); + } + + Ok(ids) + } +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result")] +pub struct GetNWFilterXMLReq(pub XMLUuid); + +impl Handler for LibVirtActor { + type Result = anyhow::Result; + + fn handle(&mut self, msg: GetNWFilterXMLReq, _ctx: &mut Self::Context) -> Self::Result { + log::debug!("Get network filter XML:\n{}", msg.0.as_string()); + let filter = NWFilter::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; + let xml = filter.get_xml_desc(0)?; + log::debug!("XML = {}", xml); + NetworkFilterXML::parse_xml(xml) + } +} diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index 060982c..924db1a 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -46,3 +46,31 @@ pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2; /// Network mac address default prefix pub const NET_MAC_ADDR_PREFIX: &str = "52:54:00"; + +/// Built-in network filter rules +pub const BUILTIN_NETWORK_FILTER_RULES: [&str; 24] = [ + "allow-arp", + "allow-dhcp", + "allow-dhcp-server", + "allow-dhcpv6", + "allow-dhcpv6-server", + "allow-incoming-ipv4", + "allow-incoming-ipv6", + "allow-ipv4", + "allow-ipv6", + "clean-traffic", + "clean-traffic-gateway", + "no-arp-ip-spoofing", + "no-arp-mac-spoofing", + "no-arp-spoofing", + "no-ip-multicast", + "no-ip-spoofing", + "no-ipv6-multicast", + "no-ipv6-spoofing", + "no-mac-broadcast", + "no-mac-spoofing", + "no-other-l2-traffic", + "no-other-rarp-traffic", + "qemu-announce-self", + "qemu-announce-self-rarp", +]; diff --git a/virtweb_backend/src/controllers/mod.rs b/virtweb_backend/src/controllers/mod.rs index d5dbcc9..08cf587 100644 --- a/virtweb_backend/src/controllers/mod.rs +++ b/virtweb_backend/src/controllers/mod.rs @@ -8,6 +8,7 @@ use std::io::ErrorKind; pub mod auth_controller; pub mod iso_controller; pub mod network_controller; +pub mod nwfilter_controller; pub mod server_controller; pub mod static_controller; pub mod vm_controller; diff --git a/virtweb_backend/src/controllers/nwfilter_controller.rs b/virtweb_backend/src/controllers/nwfilter_controller.rs new file mode 100644 index 0000000..9135538 --- /dev/null +++ b/virtweb_backend/src/controllers/nwfilter_controller.rs @@ -0,0 +1,36 @@ +use crate::controllers::{HttpResult, LibVirtReq}; +use crate::libvirt_lib_structures::XMLUuid; +use actix_web::{web, HttpResponse}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct NetworkFilterID { + uid: XMLUuid, +} + +/// Get the list of network filters +pub async fn list(client: LibVirtReq) -> HttpResult { + let networks = match client.get_full_network_filters_list().await { + Err(e) => { + log::error!("Failed to get the list of network filters! {e}"); + return Ok(HttpResponse::InternalServerError() + .json(format!("Failed to get the list of networks! {e}"))); + } + Ok(l) => l, + }; + + /*let networks = networks + .into_iter() + .map(|n| NetworkInfo::from_xml(n).unwrap()) + .collect::>();*/ + // TODO : turn into lib structure + println!("{:#?}", networks); + + Ok(HttpResponse::Ok().body(format!("{:#?}", networks))) +} + +/// Get the information about a single network filter +pub async fn get_single(client: LibVirtReq, req: web::Path) -> HttpResult { + let nwfilter = client.get_single_network_filter(req.uid).await?; + // TODO : turn into lib structure + Ok(HttpResponse::Ok().body(format!("{:#?}", nwfilter))) +} diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 46db904..b61dd2a 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -16,6 +16,7 @@ struct StaticConfig { iso_mimetypes: &'static [&'static str], net_mac_prefix: &'static str, constraints: ServerConstraints, + builtin_network_rules: &'static [&'static str], } #[derive(serde::Serialize)] @@ -45,6 +46,7 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { oidc_auth_enabled: !AppConfig::get().disable_oidc, iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES, net_mac_prefix: constants::NET_MAC_ADDR_PREFIX, + builtin_network_rules: &constants::BUILTIN_NETWORK_FILTER_RULES, constraints: ServerConstraints { iso_max_size: constants::ISO_MAX_SIZE, diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs index 9f7443a..9b74f75 100644 --- a/virtweb_backend/src/libvirt_client.rs +++ b/virtweb_backend/src/libvirt_client.rs @@ -1,6 +1,8 @@ use crate::actors::libvirt_actor; use crate::actors::libvirt_actor::LibVirtActor; -use crate::libvirt_lib_structures::{DomainState, DomainXML, NetworkXML, XMLUuid}; +use crate::libvirt_lib_structures::{ + DomainState, DomainXML, NetworkFilterXML, NetworkXML, XMLUuid, +}; use crate::libvirt_rest_structures::{HypervisorInfo, NetworkInfo, VMInfo}; use actix::Addr; @@ -165,4 +167,19 @@ impl LibVirtClient { pub async fn stop_network(&self, id: XMLUuid) -> anyhow::Result<()> { self.0.send(libvirt_actor::StopNetwork(id)).await? } + + /// Get the full list of network filters + pub async fn get_full_network_filters_list(&self) -> anyhow::Result> { + let ids = self.0.send(libvirt_actor::GetNWFiltersListReq).await??; + let mut info = Vec::with_capacity(ids.len()); + for id in ids { + info.push(self.get_single_network_filter(id).await?) + } + Ok(info) + } + + /// Get the information about a single domain + pub async fn get_single_network_filter(&self, id: XMLUuid) -> anyhow::Result { + self.0.send(libvirt_actor::GetNWFilterXMLReq(id)).await? + } } diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs index 8671b91..51d25fe 100644 --- a/virtweb_backend/src/libvirt_lib_structures.rs +++ b/virtweb_backend/src/libvirt_lib_structures.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::net::{IpAddr, Ipv4Addr}; #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] @@ -548,3 +549,316 @@ impl NetworkXML { Ok(network_xml) } } + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "filterref")] +pub struct NetworkFilterRefXML { + #[serde(rename(serialize = "@filter"))] + pub filter: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "all")] +pub struct NetworkFilterRuleProtocolAll {} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "mac")] +pub struct NetworkFilterRuleProtocolMac { + #[serde( + rename(serialize = "@srcmacaddr"), + skip_serializing_if = "Option::is_none" + )] + srcmacaddr: Option, + #[serde( + rename(serialize = "@srcmacmask"), + skip_serializing_if = "Option::is_none" + )] + srcmacmask: Option, + #[serde( + rename(serialize = "@dstmacaddr"), + skip_serializing_if = "Option::is_none" + )] + dstmacaddr: Option, + #[serde( + rename(serialize = "@dstmacmask"), + skip_serializing_if = "Option::is_none" + )] + dstmacmask: Option, + #[serde( + rename(serialize = "@comment"), + skip_serializing_if = "Option::is_none" + )] + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "arp")] +pub struct NetworkFilterRuleProtocolArp { + #[serde( + rename(serialize = "@srcmacaddr"), + skip_serializing_if = "Option::is_none" + )] + srcmacaddr: Option, + #[serde( + rename(serialize = "@srcmacmask"), + skip_serializing_if = "Option::is_none" + )] + srcmacmask: Option, + #[serde( + rename(serialize = "@dstmacaddr"), + skip_serializing_if = "Option::is_none" + )] + dstmacaddr: Option, + #[serde( + rename(serialize = "@dstmacmask"), + skip_serializing_if = "Option::is_none" + )] + dstmacmask: Option, + #[serde( + rename(serialize = "@arpsrcipaddr"), + skip_serializing_if = "Option::is_none" + )] + arpsrcipaddr: Option, + #[serde( + rename(serialize = "@arpsrcipmask"), + skip_serializing_if = "Option::is_none" + )] + arpsrcipmask: Option, + #[serde( + rename(serialize = "@arpdstipaddr"), + skip_serializing_if = "Option::is_none" + )] + arpdstipaddr: Option, + #[serde( + rename(serialize = "@arpdstipmask"), + skip_serializing_if = "Option::is_none" + )] + arpdstipmask: Option, + + #[serde( + rename(serialize = "@comment"), + skip_serializing_if = "Option::is_none" + )] + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "ipvx")] +pub struct NetworkFilterRuleProtocolIpvx { + #[serde( + rename(serialize = "@srcmacaddr"), + skip_serializing_if = "Option::is_none" + )] + srcmacaddr: Option, + #[serde( + rename(serialize = "@srcmacmask"), + skip_serializing_if = "Option::is_none" + )] + srcmacmask: Option, + #[serde( + rename(serialize = "@dstmacaddr"), + skip_serializing_if = "Option::is_none" + )] + dstmacaddr: Option, + #[serde( + rename(serialize = "@dstmacmask"), + skip_serializing_if = "Option::is_none" + )] + dstmacmask: Option, + #[serde( + rename(serialize = "@srcipaddr"), + skip_serializing_if = "Option::is_none" + )] + srcipaddr: Option, + #[serde( + rename(serialize = "@srcipmask"), + skip_serializing_if = "Option::is_none" + )] + srcipmask: Option, + #[serde( + rename(serialize = "@dstipaddr"), + skip_serializing_if = "Option::is_none" + )] + dstipaddr: Option, + #[serde( + rename(serialize = "@dstipmask"), + skip_serializing_if = "Option::is_none" + )] + dstipmask: Option, + + #[serde( + rename(serialize = "@comment"), + skip_serializing_if = "Option::is_none" + )] + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "layer4")] +pub struct NetworkFilterRuleProtocolLayer4 { + #[serde( + rename(serialize = "@srcmacaddr"), + skip_serializing_if = "Option::is_none" + )] + srcmacaddr: Option, + #[serde( + rename(serialize = "@srcipaddr"), + skip_serializing_if = "Option::is_none" + )] + srcipaddr: Option, + #[serde( + rename(serialize = "@srcipmask"), + skip_serializing_if = "Option::is_none" + )] + srcipmask: Option, + #[serde( + rename(serialize = "@dstipaddr"), + skip_serializing_if = "Option::is_none" + )] + dstipaddr: Option, + #[serde( + rename(serialize = "@dstipmask"), + skip_serializing_if = "Option::is_none" + )] + dstipmask: Option, + /// Start of range of source IP address + #[serde( + rename(serialize = "@srcipfrom"), + skip_serializing_if = "Option::is_none" + )] + srcipfrom: Option, + /// End of range of source IP address + #[serde( + rename(serialize = "@srcipto"), + skip_serializing_if = "Option::is_none" + )] + srcipto: Option, + /// Start of range of destination IP address + #[serde( + rename(serialize = "@dstipfrom"), + skip_serializing_if = "Option::is_none" + )] + dstipfrom: Option, + /// End of range of destination IP address + #[serde( + rename(serialize = "@dstipto"), + skip_serializing_if = "Option::is_none" + )] + dstipto: Option, + #[serde( + rename(serialize = "@srcportstart"), + skip_serializing_if = "Option::is_none" + )] + srcportstart: Option, + #[serde( + rename(serialize = "@srcportend"), + skip_serializing_if = "Option::is_none" + )] + srcportend: Option, + #[serde( + rename(serialize = "@dstportstart"), + skip_serializing_if = "Option::is_none" + )] + dstportstart: Option, + #[serde( + rename(serialize = "@dstportend"), + skip_serializing_if = "Option::is_none" + )] + dstportend: Option, + #[serde(rename(serialize = "@state"), skip_serializing_if = "Option::is_none")] + state: Option, + + #[serde( + rename(serialize = "@comment"), + skip_serializing_if = "Option::is_none" + )] + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "rule")] +pub struct NetworkFilterRuleXML { + #[serde(rename(serialize = "@action"))] + pub action: String, + #[serde(rename(serialize = "@direction"))] + pub direction: String, + #[serde(rename(serialize = "@priority"))] + pub priority: Option, + + /// Match all protocols + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + + /// Match mac protocol + #[serde(default, rename = "mac", skip_serializing_if = "Vec::is_empty")] + pub mac_rules: Vec, + + /// Match arp protocol + #[serde(default, rename = "arp", skip_serializing_if = "Vec::is_empty")] + pub arp_rules: Vec, + + /// Match IPv4 protocol + #[serde(default, rename = "ip", skip_serializing_if = "Vec::is_empty")] + pub ipv4_rules: Vec, + + /// Match IPv6 protocol + #[serde(default, rename = "ipv6", skip_serializing_if = "Vec::is_empty")] + pub ipv6_rules: Vec, + + /// Match TCP protocol + #[serde(default, rename = "tcp", skip_serializing_if = "Vec::is_empty")] + pub tcp_rules: Vec, + + /// Match UDP protocol + #[serde(default, rename = "udp", skip_serializing_if = "Vec::is_empty")] + pub udp_rules: Vec, + + /// Match SCTP protocol + #[serde(default, rename = "sctp", skip_serializing_if = "Vec::is_empty")] + pub sctp_rules: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "filter")] +pub struct NetworkFilterXML { + #[serde(rename(serialize = "@name"))] + pub name: String, + #[serde(rename(serialize = "@chain"), default)] + pub chain: String, + #[serde( + skip_serializing_if = "Option::is_none", + rename(serialize = "@priority"), + default + )] + pub priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, + #[serde(default, rename = "filterref", skip_serializing_if = "Vec::is_empty")] + pub filterrefs: Vec, + #[serde(default, rename = "rule", skip_serializing_if = "Vec::is_empty")] + pub rules: Vec, +} + +impl NetworkFilterXML { + pub fn parse_xml(xml: D) -> anyhow::Result { + let xml = xml.to_string(); + + // We need to put all filter refs at the same location + let mut filter_refs = Vec::new(); + let xml = lazy_regex::regex_replace_all!(r#""#, &xml, |r: &str| { + filter_refs.push(r.to_string()); + + if r.contains('\n') { + log::warn!("A filterref contain a new line. This is a symptom of a new unsupported child attribute of object!"); + } + + "" + }); + + let filter_refs = filter_refs.join("\n"); + let xml = xml.replace("", &format!("{filter_refs}")); + log::debug!("Effective NW filter rule parsed: {xml}"); + + Ok(serde_xml_rs::from_str(&xml)?) + } +} diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs index c9438b4..73af939 100644 --- a/virtweb_backend/src/libvirt_rest_structures.rs +++ b/virtweb_backend/src/libvirt_rest_structures.rs @@ -700,6 +700,144 @@ impl NetworkInfo { } } +pub enum NetworkFilterChain { + Root, + Mac, + STP, + VLAN, + ARP, + RARP, + IPv4, + IPv6, +} + +/// Network filter definition +pub struct NetworkFilter { + name: String, + chain: Option, + priority: Option, + uuid: Option, + /// Referenced filters rules + join_rules: Vec, + rules: Vec, +} + +pub enum NetworkFilterAction { + /// matching the rule silently discards the packet with no further analysis + Drop, + /// matching the rule generates an ICMP reject message with no further analysis + Reject, + /// matching the rule accepts the packet with no further analysis + Accept, + /// matching the rule passes this filter, but returns control to the calling filter for further + /// analysis + Return, + /// matching the rule goes on to the next rule for further analysis + Continue, +} + +pub enum NetworkFilterDirection { + In, + Out, + InOut, +} + +pub enum Layer4State { + NEW, + ESTABLISHED, + RELATED, + INVALID, + NONE, +} + +pub enum Layer4Type { + TCP, + UDP, + SCTP, + ICMP, + TCPipv6, + UDPipv6, + SCTPipv6, + ICMPipv6, +} + +pub enum NetworkFilterSelector { + All, + Mac { + src_mac_addr: Option, + src_mac_mask: Option, + dst_mac_addr: Option, + dst_mac_mask: Option, + comment: Option, + }, + Arp { + srcmacaddr: Option, + srcmacmask: Option, + dstmacaddr: Option, + dstmacmask: Option, + arpsrcipaddr: Option, + arpsrcipmask: Option, + arpdstipaddr: Option, + arpdstipmask: Option, + comment: Option, + }, + IPv4 { + srcmacaddr: Option, + srcmacmask: Option, + dstmacaddr: Option, + dstmacmask: Option, + srcipaddr: Option, + srcipmask: Option, + dstipaddr: Option, + dstipmask: Option, + comment: Option, + }, + IPv6 { + srcmacaddr: Option, + srcmacmask: Option, + dstmacaddr: Option, + dstmacmask: Option, + srcipaddr: Option, + srcipmask: Option, + dstipaddr: Option, + dstipmask: Option, + comment: Option, + }, + Layer4 { + r#type: Layer4Type, + srcmacaddr: Option, + srcipaddr: Option, + srcipmask: Option, + dstipaddr: Option, + dstipmask: Option, + /// Start of range of source IP address + srcipfrom: Option, + /// End of range of source IP address + srcipto: Option, + /// Start of range of destination IP address + dstipfrom: Option, + /// End of range of destination IP address + dstipto: Option, + srcportstart: Option, + srcportend: Option, + dstportstart: Option, + dstportend: Option, + state: Option, + comment: Option, + }, +} + +pub struct NetworkFilterRule { + action: NetworkFilterAction, + direction: NetworkFilterDirection, + /// optional; the priority of the rule controls the order in which the rule will be instantiated + /// relative to other rules + /// + /// Valid values are in the range of -1000 to 1000. + priority: Option, + selectors: Vec, +} + fn extract_ipv4(ip: IpAddr) -> Ipv4Addr { match ip { IpAddr::V4(i) => i, diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index c728db0..0b0c261 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -22,8 +22,8 @@ use virtweb_backend::constants::{ MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, }; use virtweb_backend::controllers::{ - auth_controller, iso_controller, network_controller, server_controller, static_controller, - vm_controller, + auth_controller, iso_controller, network_controller, nwfilter_controller, server_controller, + static_controller, vm_controller, }; use virtweb_backend::libvirt_client::LibVirtClient; use virtweb_backend::middlewares::auth_middleware::AuthChecker; @@ -239,6 +239,15 @@ async fn main() -> std::io::Result<()> { "/api/network/{uid}/stop", web::get().to(network_controller::stop), ) + // Network filters controller + .route( + "/api/nwfilter/list", + web::get().to(nwfilter_controller::list), + ) + .route( + "/api/nwfilter/{uid}", + web::get().to(nwfilter_controller::get_single), + ) // Static assets .route("/", web::get().to(static_controller::root_index)) .route(