Port forwarding is working

This commit is contained in:
Pierre HUBERT 2024-01-10 21:59:41 +01:00
parent ed25eed31e
commit d6c8964380
4 changed files with 246 additions and 35 deletions

View File

@ -1,11 +1,18 @@
use crate::constants; use crate::constants;
use crate::libvirt_rest_structures::net::NetworkName; 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 crate::utils::net_utils;
use clap::Parser; use clap::Parser;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
use std::path::{Path, PathBuf}; 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 /// VirtWeb NAT configuration mode. This executable should never be executed manually
#[derive(Parser, Debug, Clone)] #[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_json = std::fs::read_to_string(args.network_file())?;
let conf: NetNat = serde_json::from_str(&conf_json)?; 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()) { match (args.operation.as_str(), args.sub_operation.as_str()) {
("started", "begin") => network_started_begin(&conf, &ips).await?, ("started", "begin") => {
("stopped", "end") => network_stopped_end(&conf, &ips).await?, 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!( _ => log::debug!(
"Operation {} - {} not supported", "Operation {} - {} not supported",
args.operation, args.operation,
@ -64,16 +77,156 @@ pub async fn sub_main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
pub async fn network_started_begin( async fn trigger_nat_forwarding(
enable: bool,
conf: &NetNat, conf: &NetNat,
ips: &HashMap<String, Vec<IpAddr>>, nic_ips: &HashMap<String, Vec<IpAddr>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
todo!() if let Some(ipv4) = &conf.ipv4 {
trigger_nat_forwarding_nat_ipv(
enable,
&conf.interface,
&ipv4.iter().map(|i| i.generalize()).collect::<Vec<_>>(),
nic_ips,
)
.await?;
} }
pub async fn network_stopped_end( if let Some(ipv6) = &conf.ipv6 {
conf: &NetNat, trigger_nat_forwarding_nat_ipv(
ips: &HashMap<String, Vec<IpAddr>>, enable,
) -> anyhow::Result<()> { &conf.interface,
todo!() &ipv6.iter().map(|i| i.generalize()).collect::<Vec<_>>(),
nic_ips,
)
.await?;
}
Ok(())
}
async fn trigger_nat_forwarding_nat_ipv(
enable: bool,
net_interface: &str,
rules: &[Nat<IpAddr>],
nic_ips: &HashMap<String, Vec<IpAddr>>,
) -> anyhow::Result<()> {
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(())
} }

View File

@ -1,6 +1,6 @@
use crate::constants; use crate::constants;
use crate::utils::net_utils; use crate::utils::net_utils;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
enum NatDefError { enum NatDefError {
@ -10,18 +10,28 @@ enum NatDefError {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")] #[serde(tag = "type", rename_all = "lowercase")]
pub enum NatSource<IPv> { pub enum NatSourceIP<IPv> {
Interface { name: String }, Interface { name: String },
Ip { ip: IPv }, Ip { ip: IPv },
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum NatProtocol { pub enum NatProtocol {
TCP, TCP,
UDP, UDP,
Both, 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)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")] #[serde(tag = "type", rename_all = "lowercase")]
pub enum NatHostPort { pub enum NatHostPort {
@ -29,19 +39,28 @@ pub enum NatHostPort {
Range { start: u16, end: u16 }, Range { start: u16, end: u16 },
} }
impl NatHostPort {
pub fn as_seq(&self) -> Vec<u16> {
match self {
NatHostPort::Single { port } => vec![*port],
NatHostPort::Range { start, end } => (*start..(*end + 1)).collect(),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Nat<IPv> { pub struct Nat<IPv> {
pub protocol: NatProtocol, pub protocol: NatProtocol,
pub host_addr: NatSource<IPv>, pub host_ip: NatSourceIP<IPv>,
pub host_port: NatHostPort, pub host_port: NatHostPort,
pub guest_addr: IPv, pub guest_ip: IPv,
pub guest_port: u16, pub guest_port: u16,
pub comment: Option<String>, pub comment: Option<String>,
} }
impl<IPv> Nat<IPv> { impl<IPv> Nat<IPv> {
pub fn check(&self) -> anyhow::Result<()> { 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) { if !net_utils::is_net_interface_name_valid(name) {
return Err(NatDefError::InvalidNatDef("Invalid nat interface name!").into()); return Err(NatDefError::InvalidNatDef("Invalid nat interface name!").into());
} }
@ -75,6 +94,46 @@ impl<IPv> Nat<IPv> {
} }
} }
impl Nat<Ipv4Addr> {
pub fn generalize(&self) -> Nat<IpAddr> {
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<Ipv6Addr> {
pub fn generalize(&self) -> Nat<IpAddr> {
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)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct NetNat { pub struct NetNat {
pub interface: String, pub interface: String,

View File

@ -23,9 +23,9 @@ export type NatHostPort =
export interface NatEntry { export interface NatEntry {
protocol: "TCP" | "UDP" | "Both"; protocol: "TCP" | "UDP" | "Both";
host_addr: NatSource; host_ip: NatSource;
host_port: NatHostPort; host_port: NatHostPort;
guest_addr: string; guest_ip: string;
guest_port: number; guest_port: number;
comment?: string; comment?: string;
} }

View File

@ -30,12 +30,12 @@ export function NetNatConfiguration(p: {
const addEntry = () => { const addEntry = () => {
p.nat.push({ p.nat.push({
host_addr: { host_ip: {
type: "ip", type: "ip",
ip: p.version === 4 ? "10.0.0.1" : "fd00::", ip: p.version === 4 ? "10.0.0.1" : "fd00::",
}, },
host_port: { type: "single", port: 80 }, 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, guest_port: 10,
protocol: "TCP", protocol: "TCP",
}); });
@ -122,7 +122,7 @@ function NatEntryForm(p: {
<NATEntryProp label="Host configuration"> <NATEntryProp label="Host configuration">
<SelectInput <SelectInput
{...p} {...p}
label="Host address specification" label="Host IP address specification"
options={[ options={[
{ {
label: "Specific IP", label: "Specific IP",
@ -136,39 +136,38 @@ function NatEntryForm(p: {
"Use active IP addresses on the selected network interface during network startup to determine host adddress", "Use active IP addresses on the selected network interface during network startup to determine host adddress",
}, },
]} ]}
value={p.entry.host_addr.type} value={p.entry.host_ip.type}
onValueChange={(v) => { onValueChange={(v) => {
p.entry.host_addr.type = v as any; p.entry.host_ip.type = v as any;
p.onChange?.(); p.onChange?.();
}} }}
/> />
{p.entry.host_addr.type === "ip" && ( {p.entry.host_ip.type === "ip" && (
<IPInput <IPInput
{...p} {...p}
label="Host IP address" label="Host IP address"
value={p.entry.host_addr.ip} value={p.entry.host_ip.ip}
onValueChange={(v) => { onValueChange={(v) => {
if (p.entry.host_addr.type === "ip") if (p.entry.host_ip.type === "ip") p.entry.host_ip.ip = v!;
p.entry.host_addr.ip = v!;
p.onChange?.(); p.onChange?.();
}} }}
/> />
)} )}
{p.entry.host_addr.type === "interface" && ( {p.entry.host_ip.type === "interface" && (
<SelectInput <SelectInput
{...p} {...p}
label="Network interface" label="Network interface"
value={p.entry.host_addr.name} value={p.entry.host_ip.name}
options={p.nicsList.map((n) => { options={p.nicsList.map((n) => {
return { return {
value: n, value: n,
}; };
})} })}
onValueChange={(v) => { onValueChange={(v) => {
if (p.entry.host_addr.type === "interface") if (p.entry.host_ip.type === "interface")
p.entry.host_addr.name = v!; p.entry.host_ip.name = v!;
p.onChange?.(); p.onChange?.();
}} }}
/> />
@ -178,10 +177,10 @@ function NatEntryForm(p: {
<NATEntryProp label="Target guest configuration"> <NATEntryProp label="Target guest configuration">
<IPInput <IPInput
{...p} {...p}
label="Guest address" label="Guest IP"
value={p.entry.guest_addr} value={p.entry.guest_ip}
onValueChange={(v) => { onValueChange={(v) => {
p.entry.guest_addr = v!; p.entry.guest_ip = v!;
p.onChange?.(); p.onChange?.();
}} }}
/> />