diff --git a/virtweb_backend/src/nat/nat_conf_mode.rs b/virtweb_backend/src/nat/nat_conf_mode.rs index 0e0d30c..e054d10 100644 --- a/virtweb_backend/src/nat/nat_conf_mode.rs +++ b/virtweb_backend/src/nat/nat_conf_mode.rs @@ -1,11 +1,18 @@ use crate::constants; use crate::libvirt_rest_structures::net::NetworkName; -use crate::nat::nat_definition::NetNat; +use crate::nat::nat_definition::{Nat, NatSourceIP, NetNat}; use crate::utils::net_utils; use clap::Parser; use std::collections::HashMap; use std::net::IpAddr; use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus}; + +#[derive(thiserror::Error, Debug)] +enum NatConfModeError { + #[error("UpdateFirewall failed!")] + UpdateFirewall, +} /// VirtWeb NAT configuration mode. This executable should never be executed manually #[derive(Parser, Debug, Clone)] @@ -49,11 +56,17 @@ pub async fn sub_main() -> anyhow::Result<()> { let conf_json = std::fs::read_to_string(args.network_file())?; let conf: NetNat = serde_json::from_str(&conf_json)?; - let ips = net_utils::net_list_and_ips()?; + let nic_ips = net_utils::net_list_and_ips()?; match (args.operation.as_str(), args.sub_operation.as_str()) { - ("started", "begin") => network_started_begin(&conf, &ips).await?, - ("stopped", "end") => network_stopped_end(&conf, &ips).await?, + ("started", "begin") => { + log::info!("Enable port forwarding for network"); + trigger_nat_forwarding(true, &conf, &nic_ips).await? + } + ("stopped", "end") => { + log::info!("Disable port forwarding for network"); + trigger_nat_forwarding(false, &conf, &nic_ips).await? + } _ => log::debug!( "Operation {} - {} not supported", args.operation, @@ -64,16 +77,156 @@ pub async fn sub_main() -> anyhow::Result<()> { Ok(()) } -pub async fn network_started_begin( +async fn trigger_nat_forwarding( + enable: bool, conf: &NetNat, - ips: &HashMap>, + nic_ips: &HashMap>, ) -> anyhow::Result<()> { - todo!() + if let Some(ipv4) = &conf.ipv4 { + trigger_nat_forwarding_nat_ipv( + enable, + &conf.interface, + &ipv4.iter().map(|i| i.generalize()).collect::>(), + nic_ips, + ) + .await?; + } + + if let Some(ipv6) = &conf.ipv6 { + trigger_nat_forwarding_nat_ipv( + enable, + &conf.interface, + &ipv6.iter().map(|i| i.generalize()).collect::>(), + nic_ips, + ) + .await?; + } + + Ok(()) } -pub async fn network_stopped_end( - conf: &NetNat, - ips: &HashMap>, +async fn trigger_nat_forwarding_nat_ipv( + enable: bool, + net_interface: &str, + rules: &[Nat], + nic_ips: &HashMap>, ) -> anyhow::Result<()> { - todo!() + for r in rules { + let host_ips = match &r.host_ip { + NatSourceIP::Interface { name } => nic_ips.get(name).cloned().unwrap_or_default(), + NatSourceIP::Ip { ip } => vec![*ip], + }; + + for host_ip in host_ips { + let mut guest_port = r.guest_port; + for host_port in r.host_port.as_seq() { + if r.protocol.has_tcp() { + toggle_port_forwarding( + enable, + false, + host_ip, + host_port, + net_interface, + r.guest_ip, + guest_port, + )? + } + + if r.protocol.has_udp() { + toggle_port_forwarding( + enable, + true, + host_ip, + host_port, + net_interface, + r.guest_ip, + guest_port, + )? + } + + guest_port += 1; + } + } + } + + Ok(()) +} + +fn check_cmd(s: ExitStatus) -> anyhow::Result<()> { + if !s.success() { + log::error!("Failed to update firewall rules!"); + return Err(NatConfModeError::UpdateFirewall.into()); + } + Ok(()) +} + +fn toggle_port_forwarding( + enable: bool, + is_udp: bool, + host_ip: IpAddr, + host_port: u16, + net_interface: &str, + guest_ip: IpAddr, + guest_port: u16, +) -> anyhow::Result<()> { + if host_ip.is_ipv4() != guest_ip.is_ipv4() { + log::trace!("Skipping invalid combination {host_ip} -> {guest_ip}"); + return Ok(()); + } + + let program = match host_ip.is_ipv4() { + true => "/sbin/iptables", + false => "/sbin/ip6tables", + }; + + let protocol = match is_udp { + true => "udp", + false => "tcp", + }; + + log::info!("Forward (add={enable}) incoming {protocol} connections for {host_ip}:{host_port} to {guest_ip}:{guest_port} int {net_interface}"); + + // Rule 1 + let cmd = Command::new(program) + .arg(match enable { + true => "-I", + false => "-D", + }) + .arg("FORWARD") + .arg("-o") + .arg(net_interface) + .arg("-p") + .arg(protocol) + .arg("-d") + .arg(guest_ip.to_string()) + .arg("--dport") + .arg(guest_port.to_string()) + .arg("-j") + .arg("ACCEPT") + .status()?; + check_cmd(cmd)?; + + // Rule 2 + let cmd = Command::new(program) + .arg("-t") + .arg("nat") + .arg(match enable { + true => "-I", + false => "-D", + }) + .arg("PREROUTING") + .arg("-p") + .arg(protocol) + .arg("-d") + .arg(host_ip.to_string()) + .arg("--dport") + .arg(host_port.to_string()) + .arg("-j") + .arg("DNAT") + .arg("--to") + .arg(format!("{guest_ip}:{guest_port}")) + .status()?; + check_cmd(cmd)?; + + Ok(()) } diff --git a/virtweb_backend/src/nat/nat_definition.rs b/virtweb_backend/src/nat/nat_definition.rs index 2952ed8..0e03e92 100644 --- a/virtweb_backend/src/nat/nat_definition.rs +++ b/virtweb_backend/src/nat/nat_definition.rs @@ -1,6 +1,6 @@ use crate::constants; use crate::utils::net_utils; -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; #[derive(thiserror::Error, Debug)] enum NatDefError { @@ -10,18 +10,28 @@ enum NatDefError { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] -pub enum NatSource { +pub enum NatSourceIP { Interface { name: String }, Ip { ip: IPv }, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] pub enum NatProtocol { TCP, UDP, Both, } +impl NatProtocol { + pub fn has_tcp(&self) -> bool { + !matches!(&self, NatProtocol::UDP) + } + + pub fn has_udp(&self) -> bool { + !matches!(&self, NatProtocol::TCP) + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum NatHostPort { @@ -29,19 +39,28 @@ pub enum NatHostPort { Range { start: u16, end: u16 }, } +impl NatHostPort { + pub fn as_seq(&self) -> Vec { + match self { + NatHostPort::Single { port } => vec![*port], + NatHostPort::Range { start, end } => (*start..(*end + 1)).collect(), + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Nat { pub protocol: NatProtocol, - pub host_addr: NatSource, + pub host_ip: NatSourceIP, pub host_port: NatHostPort, - pub guest_addr: IPv, + pub guest_ip: IPv, pub guest_port: u16, pub comment: Option, } impl Nat { pub fn check(&self) -> anyhow::Result<()> { - if let NatSource::Interface { name } = &self.host_addr { + if let NatSourceIP::Interface { name } = &self.host_ip { if !net_utils::is_net_interface_name_valid(name) { return Err(NatDefError::InvalidNatDef("Invalid nat interface name!").into()); } @@ -75,6 +94,46 @@ impl Nat { } } +impl Nat { + pub fn generalize(&self) -> Nat { + Nat { + protocol: self.protocol, + host_ip: match &self.host_ip { + NatSourceIP::Ip { ip } => NatSourceIP::Ip { + ip: IpAddr::V4(*ip), + }, + NatSourceIP::Interface { name } => NatSourceIP::Interface { + name: name.to_string(), + }, + }, + host_port: self.host_port.clone(), + guest_ip: IpAddr::V4(self.guest_ip), + guest_port: self.guest_port, + comment: self.comment.clone(), + } + } +} + +impl Nat { + pub fn generalize(&self) -> Nat { + Nat { + protocol: self.protocol, + host_ip: match &self.host_ip { + NatSourceIP::Ip { ip } => NatSourceIP::Ip { + ip: IpAddr::V6(*ip), + }, + NatSourceIP::Interface { name } => NatSourceIP::Interface { + name: name.to_string(), + }, + }, + host_port: self.host_port.clone(), + guest_ip: IpAddr::V6(self.guest_ip), + guest_port: self.guest_port, + comment: self.comment.clone(), + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] pub struct NetNat { pub interface: String, diff --git a/virtweb_frontend/src/api/NetworksApi.ts b/virtweb_frontend/src/api/NetworksApi.ts index 575cbd0..109e3ff 100644 --- a/virtweb_frontend/src/api/NetworksApi.ts +++ b/virtweb_frontend/src/api/NetworksApi.ts @@ -23,9 +23,9 @@ export type NatHostPort = export interface NatEntry { protocol: "TCP" | "UDP" | "Both"; - host_addr: NatSource; + host_ip: NatSource; host_port: NatHostPort; - guest_addr: string; + guest_ip: string; guest_port: number; comment?: string; } diff --git a/virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx b/virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx index c56cb20..a6aaa91 100644 --- a/virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx +++ b/virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx @@ -30,12 +30,12 @@ export function NetNatConfiguration(p: { const addEntry = () => { p.nat.push({ - host_addr: { + host_ip: { type: "ip", ip: p.version === 4 ? "10.0.0.1" : "fd00::", }, host_port: { type: "single", port: 80 }, - guest_addr: p.version === 4 ? "10.0.0.100" : "fd00::", + guest_ip: p.version === 4 ? "10.0.0.100" : "fd00::", guest_port: 10, protocol: "TCP", }); @@ -122,7 +122,7 @@ function NatEntryForm(p: { { - p.entry.host_addr.type = v as any; + p.entry.host_ip.type = v as any; p.onChange?.(); }} /> - {p.entry.host_addr.type === "ip" && ( + {p.entry.host_ip.type === "ip" && ( { - if (p.entry.host_addr.type === "ip") - p.entry.host_addr.ip = v!; + if (p.entry.host_ip.type === "ip") p.entry.host_ip.ip = v!; p.onChange?.(); }} /> )} - {p.entry.host_addr.type === "interface" && ( + {p.entry.host_ip.type === "interface" && ( { return { value: n, }; })} onValueChange={(v) => { - if (p.entry.host_addr.type === "interface") - p.entry.host_addr.name = v!; + if (p.entry.host_ip.type === "interface") + p.entry.host_ip.name = v!; p.onChange?.(); }} /> @@ -178,10 +177,10 @@ function NatEntryForm(p: { { - p.entry.guest_addr = v!; + p.entry.guest_ip = v!; p.onChange?.(); }} />