Parse NW filters XML structure

This commit is contained in:
Pierre HUBERT 2023-12-28 15:12:38 +01:00
parent b4f765d486
commit 3849b0d51d
9 changed files with 588 additions and 4 deletions

View File

@ -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<StopNetwork> for LibVirtActor {
Ok(())
}
}
#[derive(Message)]
#[rtype(result = "anyhow::Result<Vec<XMLUuid>>")]
pub struct GetNWFiltersListReq;
impl Handler<GetNWFiltersListReq> for LibVirtActor {
type Result = anyhow::Result<Vec<XMLUuid>>;
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<NetworkFilterXML>")]
pub struct GetNWFilterXMLReq(pub XMLUuid);
impl Handler<GetNWFilterXMLReq> for LibVirtActor {
type Result = anyhow::Result<NetworkFilterXML>;
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)
}
}

View File

@ -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",
];

View File

@ -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;

View File

@ -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::<Vec<_>>();*/
// 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<NetworkFilterID>) -> HttpResult {
let nwfilter = client.get_single_network_filter(req.uid).await?;
// TODO : turn into lib structure
Ok(HttpResponse::Ok().body(format!("{:#?}", nwfilter)))
}

View File

@ -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,

View File

@ -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<Vec<NetworkFilterXML>> {
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<NetworkFilterXML> {
self.0.send(libvirt_actor::GetNWFilterXMLReq(id)).await?
}
}

View File

@ -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<String>,
#[serde(
rename(serialize = "@srcmacmask"),
skip_serializing_if = "Option::is_none"
)]
srcmacmask: Option<String>,
#[serde(
rename(serialize = "@dstmacaddr"),
skip_serializing_if = "Option::is_none"
)]
dstmacaddr: Option<String>,
#[serde(
rename(serialize = "@dstmacmask"),
skip_serializing_if = "Option::is_none"
)]
dstmacmask: Option<String>,
#[serde(
rename(serialize = "@comment"),
skip_serializing_if = "Option::is_none"
)]
comment: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
#[serde(rename = "arp")]
pub struct NetworkFilterRuleProtocolArp {
#[serde(
rename(serialize = "@srcmacaddr"),
skip_serializing_if = "Option::is_none"
)]
srcmacaddr: Option<String>,
#[serde(
rename(serialize = "@srcmacmask"),
skip_serializing_if = "Option::is_none"
)]
srcmacmask: Option<String>,
#[serde(
rename(serialize = "@dstmacaddr"),
skip_serializing_if = "Option::is_none"
)]
dstmacaddr: Option<String>,
#[serde(
rename(serialize = "@dstmacmask"),
skip_serializing_if = "Option::is_none"
)]
dstmacmask: Option<String>,
#[serde(
rename(serialize = "@arpsrcipaddr"),
skip_serializing_if = "Option::is_none"
)]
arpsrcipaddr: Option<String>,
#[serde(
rename(serialize = "@arpsrcipmask"),
skip_serializing_if = "Option::is_none"
)]
arpsrcipmask: Option<u8>,
#[serde(
rename(serialize = "@arpdstipaddr"),
skip_serializing_if = "Option::is_none"
)]
arpdstipaddr: Option<String>,
#[serde(
rename(serialize = "@arpdstipmask"),
skip_serializing_if = "Option::is_none"
)]
arpdstipmask: Option<u8>,
#[serde(
rename(serialize = "@comment"),
skip_serializing_if = "Option::is_none"
)]
comment: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
#[serde(rename = "ipvx")]
pub struct NetworkFilterRuleProtocolIpvx {
#[serde(
rename(serialize = "@srcmacaddr"),
skip_serializing_if = "Option::is_none"
)]
srcmacaddr: Option<String>,
#[serde(
rename(serialize = "@srcmacmask"),
skip_serializing_if = "Option::is_none"
)]
srcmacmask: Option<String>,
#[serde(
rename(serialize = "@dstmacaddr"),
skip_serializing_if = "Option::is_none"
)]
dstmacaddr: Option<String>,
#[serde(
rename(serialize = "@dstmacmask"),
skip_serializing_if = "Option::is_none"
)]
dstmacmask: Option<String>,
#[serde(
rename(serialize = "@srcipaddr"),
skip_serializing_if = "Option::is_none"
)]
srcipaddr: Option<String>,
#[serde(
rename(serialize = "@srcipmask"),
skip_serializing_if = "Option::is_none"
)]
srcipmask: Option<u8>,
#[serde(
rename(serialize = "@dstipaddr"),
skip_serializing_if = "Option::is_none"
)]
dstipaddr: Option<String>,
#[serde(
rename(serialize = "@dstipmask"),
skip_serializing_if = "Option::is_none"
)]
dstipmask: Option<u8>,
#[serde(
rename(serialize = "@comment"),
skip_serializing_if = "Option::is_none"
)]
comment: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
#[serde(rename = "layer4")]
pub struct NetworkFilterRuleProtocolLayer4 {
#[serde(
rename(serialize = "@srcmacaddr"),
skip_serializing_if = "Option::is_none"
)]
srcmacaddr: Option<String>,
#[serde(
rename(serialize = "@srcipaddr"),
skip_serializing_if = "Option::is_none"
)]
srcipaddr: Option<IpAddr>,
#[serde(
rename(serialize = "@srcipmask"),
skip_serializing_if = "Option::is_none"
)]
srcipmask: Option<u8>,
#[serde(
rename(serialize = "@dstipaddr"),
skip_serializing_if = "Option::is_none"
)]
dstipaddr: Option<IpAddr>,
#[serde(
rename(serialize = "@dstipmask"),
skip_serializing_if = "Option::is_none"
)]
dstipmask: Option<u8>,
/// Start of range of source IP address
#[serde(
rename(serialize = "@srcipfrom"),
skip_serializing_if = "Option::is_none"
)]
srcipfrom: Option<IpAddr>,
/// End of range of source IP address
#[serde(
rename(serialize = "@srcipto"),
skip_serializing_if = "Option::is_none"
)]
srcipto: Option<IpAddr>,
/// Start of range of destination IP address
#[serde(
rename(serialize = "@dstipfrom"),
skip_serializing_if = "Option::is_none"
)]
dstipfrom: Option<IpAddr>,
/// End of range of destination IP address
#[serde(
rename(serialize = "@dstipto"),
skip_serializing_if = "Option::is_none"
)]
dstipto: Option<IpAddr>,
#[serde(
rename(serialize = "@srcportstart"),
skip_serializing_if = "Option::is_none"
)]
srcportstart: Option<u16>,
#[serde(
rename(serialize = "@srcportend"),
skip_serializing_if = "Option::is_none"
)]
srcportend: Option<u16>,
#[serde(
rename(serialize = "@dstportstart"),
skip_serializing_if = "Option::is_none"
)]
dstportstart: Option<u16>,
#[serde(
rename(serialize = "@dstportend"),
skip_serializing_if = "Option::is_none"
)]
dstportend: Option<u16>,
#[serde(rename(serialize = "@state"), skip_serializing_if = "Option::is_none")]
state: Option<String>,
#[serde(
rename(serialize = "@comment"),
skip_serializing_if = "Option::is_none"
)]
comment: Option<String>,
}
#[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<i32>,
/// Match all protocols
#[serde(skip_serializing_if = "Option::is_none")]
pub all: Option<NetworkFilterRuleProtocolAll>,
/// Match mac protocol
#[serde(default, rename = "mac", skip_serializing_if = "Vec::is_empty")]
pub mac_rules: Vec<NetworkFilterRuleProtocolMac>,
/// Match arp protocol
#[serde(default, rename = "arp", skip_serializing_if = "Vec::is_empty")]
pub arp_rules: Vec<NetworkFilterRuleProtocolArp>,
/// Match IPv4 protocol
#[serde(default, rename = "ip", skip_serializing_if = "Vec::is_empty")]
pub ipv4_rules: Vec<NetworkFilterRuleProtocolIpvx>,
/// Match IPv6 protocol
#[serde(default, rename = "ipv6", skip_serializing_if = "Vec::is_empty")]
pub ipv6_rules: Vec<NetworkFilterRuleProtocolIpvx>,
/// Match TCP protocol
#[serde(default, rename = "tcp", skip_serializing_if = "Vec::is_empty")]
pub tcp_rules: Vec<NetworkFilterRuleProtocolLayer4>,
/// Match UDP protocol
#[serde(default, rename = "udp", skip_serializing_if = "Vec::is_empty")]
pub udp_rules: Vec<NetworkFilterRuleProtocolLayer4>,
/// Match SCTP protocol
#[serde(default, rename = "sctp", skip_serializing_if = "Vec::is_empty")]
pub sctp_rules: Vec<NetworkFilterRuleProtocolLayer4>,
}
#[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<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<XMLUuid>,
#[serde(default, rename = "filterref", skip_serializing_if = "Vec::is_empty")]
pub filterrefs: Vec<NetworkFilterRefXML>,
#[serde(default, rename = "rule", skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<NetworkFilterRuleXML>,
}
impl NetworkFilterXML {
pub fn parse_xml<D: Display>(xml: D) -> anyhow::Result<Self> {
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#"<filterref.*/>"#, &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 <filterref /> object!");
}
""
});
let filter_refs = filter_refs.join("\n");
let xml = xml.replace("</filter>", &format!("{filter_refs}</filter>"));
log::debug!("Effective NW filter rule parsed: {xml}");
Ok(serde_xml_rs::from_str(&xml)?)
}
}

View File

@ -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<NetworkFilterChain>,
priority: Option<i32>,
uuid: Option<XMLUuid>,
/// Referenced filters rules
join_rules: Vec<String>,
rules: Vec<NetworkFilterRule>,
}
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<String>,
src_mac_mask: Option<String>,
dst_mac_addr: Option<String>,
dst_mac_mask: Option<String>,
comment: Option<String>,
},
Arp {
srcmacaddr: Option<String>,
srcmacmask: Option<String>,
dstmacaddr: Option<String>,
dstmacmask: Option<String>,
arpsrcipaddr: Option<IpAddr>,
arpsrcipmask: Option<u8>,
arpdstipaddr: Option<IpAddr>,
arpdstipmask: Option<u8>,
comment: Option<String>,
},
IPv4 {
srcmacaddr: Option<String>,
srcmacmask: Option<String>,
dstmacaddr: Option<String>,
dstmacmask: Option<String>,
srcipaddr: Option<Ipv4Addr>,
srcipmask: Option<u8>,
dstipaddr: Option<Ipv4Addr>,
dstipmask: Option<u8>,
comment: Option<String>,
},
IPv6 {
srcmacaddr: Option<String>,
srcmacmask: Option<String>,
dstmacaddr: Option<String>,
dstmacmask: Option<String>,
srcipaddr: Option<Ipv6Addr>,
srcipmask: Option<u8>,
dstipaddr: Option<Ipv6Addr>,
dstipmask: Option<u8>,
comment: Option<String>,
},
Layer4 {
r#type: Layer4Type,
srcmacaddr: Option<String>,
srcipaddr: Option<IpAddr>,
srcipmask: Option<u8>,
dstipaddr: Option<IpAddr>,
dstipmask: Option<u8>,
/// Start of range of source IP address
srcipfrom: Option<IpAddr>,
/// End of range of source IP address
srcipto: Option<IpAddr>,
/// Start of range of destination IP address
dstipfrom: Option<IpAddr>,
/// End of range of destination IP address
dstipto: Option<IpAddr>,
srcportstart: Option<u16>,
srcportend: Option<u16>,
dstportstart: Option<u16>,
dstportend: Option<u16>,
state: Option<Layer4State>,
comment: Option<String>,
},
}
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<i32>,
selectors: Vec<NetworkFilterSelector>,
}
fn extract_ipv4(ip: IpAddr) -> Ipv4Addr {
match ip {
IpAddr::V4(i) => i,

View File

@ -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(