From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 04D651FF136 for ; Mon, 04 May 2026 18:12:36 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3BA4359DF; Mon, 4 May 2026 18:12:05 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-perl-rs v3 10/26] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Date: Mon, 4 May 2026 18:10:53 +0200 Message-ID: <20260504161115.408970-11-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260504161115.408970-1-s.hanreich@proxmox.com> References: <20260504161115.408970-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1777910981471 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.665 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: ALTMUO5RJ7RBOGUJBTYRWYN4N3IHQYTW X-Message-ID-Hash: ALTMUO5RJ7RBOGUJBTYRWYN4N3IHQYTW X-MailFrom: s.hanreich@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Christoph Heiss Add support for generating the respective sections in /etc/network/interfaces.d/sdn for WireGuard interfaces. The method automatically generates the ifupdown2 configuration for creating the WireGuard interfaces on the host. The WireGuard specific configuration is implemented via a post-up command that syncs the generated configuration via wg(8). Additionally, routes are created for every configured AdditionalIp in the WireGuard configuration, when auto_generate_routes is set to 1 (default) in the peer configuration. This happens via respective post-up / down commands in the ifupdown2 configuration file. This does not work with duplicate Allowed IPs, two routes are created instead where the first one 'wins'. This is analogous to the behavior when configuring the same subnet multiple times in the ifupdown2 configuration. In the future it would be possible to implement ECMP for this case, by auto-generating next-hop groups for duplicated allowed IPs. If ECMP via WireGuard is desired, it is currently only possible via utilizing another routing protocol on top of WireGuard (BGP or OSPF). For the new WireGuard variants, stubs are implemented for the status reporting. This makes all status calls return empty values for everything. Status reporting will be implemented in a future patch series. Co-authored-by: Stefan Hanreich Signed-off-by: Christoph Heiss --- pve-rs/src/bindings/sdn/fabrics.rs | 178 ++++++++++++++++++++++++----- pve-rs/src/sdn/status.rs | 16 +++ 2 files changed, 163 insertions(+), 31 deletions(-) diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs index 18848c4..56d1f0e 100644 --- a/pve-rs/src/bindings/sdn/fabrics.rs +++ b/pve-rs/src/bindings/sdn/fabrics.rs @@ -24,14 +24,19 @@ pub mod pve_rs_sdn_fabrics { use proxmox_ve_config::common::valid::{Valid, Validatable}; use proxmox_ve_config::sdn::config::{SdnConfig, ZoneConfig}; - use proxmox_ve_config::sdn::fabric::section_config::Section; use proxmox_ve_config::sdn::fabric::section_config::fabric::{ - Fabric as ConfigFabric, FabricId, api::{Fabric, FabricUpdater}, + Fabric as ConfigFabric, FabricId, }; use proxmox_ve_config::sdn::fabric::section_config::interface::InterfaceName; use proxmox_ve_config::sdn::fabric::section_config::node::{Node as ConfigNode, NodeId}; + use proxmox_ve_config::sdn::fabric::section_config::Section; use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry}; + use proxmox_ve_config::sdn::wireguard::WireGuardConfigBuilder; + + use proxmox_ve_config::sdn::fabric::section_config::protocol::wireguard::{ + WireGuardInterfaceCreateProperties, WireGuardInterfaceProperties, WireGuardNode, + }; use crate::sdn::status::{self, RunningConfig}; @@ -361,6 +366,7 @@ pub mod pve_rs_sdn_fabrics { } } } + ConfigNode::WireGuard(_) => {} } } @@ -453,14 +459,78 @@ pub mod pve_rs_sdn_fabrics { FabricEntry::Openfabric(_) => { daemons.insert("fabricd"); } + FabricEntry::WireGuard(_) => {} // not a frr fabric }; } daemons.into_iter().map(String::from).collect() } + /// Returns the WireGuard configuration for the interfaces of the given node, + /// + /// It is a hash with the interface name as key and the configuration for that interface + /// as a string in INI-style as accepted by wg(8). + #[export] + pub fn get_wireguard_raw_config( + #[try_from_ref] this: &PerlFabricConfig, + node_id: NodeId, + ) -> Result, Error> { + let config = this.fabric_config.lock().unwrap(); + + let configs = WireGuardConfigBuilder::default() + .add_fabrics(config.clone().into_valid()?) + .build(node_id)?; + + let mut result = HashMap::new(); + + for (id, config) in configs { + result.insert(id.clone(), config.to_raw_config()?); + } + + Ok(result) + } + + /// Helper function to generate the section for a WireGuard interface in `/etc/network/interfaces.d/sdn`. + fn render_wireguard_interface<'a>( + wireguard_interface: &WireGuardInterfaceProperties, + allowed_ips: impl Iterator, + ) -> Result { + let mut interface = String::new(); + let name = wireguard_interface.name(); + + writeln!(interface)?; + writeln!(interface, "auto {name}")?; + + if let Some(ip) = wireguard_interface.ip() { + writeln!(interface, "iface {name} inet static")?; + writeln!(interface, "\taddress {ip}")?; + } else { + writeln!(interface, "iface {name} inet manual")?; + } + + writeln!(interface, "\tlink-type wireguard")?; + writeln!(interface, "\tip-forward 1")?; + writeln!( + interface, + "\tpost-up wg syncconf {name} /etc/wireguard/proxmox/{name}.conf" + )?; + + for ip in allowed_ips { + writeln!(interface, "\tpost-up ip route add {ip} dev {name}")?; + writeln!(interface, "\tdown ip route del {ip} dev {name}")?; + } + + if let Some(ip) = wireguard_interface.ip6() { + writeln!(interface)?; + writeln!(interface, "iface {name} inet6 static")?; + writeln!(interface, "\taddress {ip}")?; + } + + Ok(interface) + } + /// Helper function to generate the default `/etc/network/interfaces` config for a given CIDR. - fn render_interface(name: &str, cidr: Cidr, is_dummy: bool) -> Result { + fn render_interface(name: &str, cidr: Cidr, link_type: Option<&str>) -> Result { let mut interface = String::new(); writeln!(interface, "auto {name}")?; @@ -469,14 +539,42 @@ pub mod pve_rs_sdn_fabrics { Cidr::Ipv6(_) => writeln!(interface, "iface {name} inet6 static")?, } writeln!(interface, "\taddress {cidr}")?; - if is_dummy { - writeln!(interface, "\tlink-type dummy")?; + if let Some(link_type) = link_type { + writeln!(interface, "\tlink-type {link_type}")?; } writeln!(interface, "\tip-forward 1")?; Ok(interface) } + fn render_dummy_interfaces( + interfaces: &mut String, + fabric: &Fabric, + node: &ConfigNode, + ) -> Result<(), Error> { + if let Some(ip) = node.ip() { + let interface = render_interface( + &format!("dummy_{}", fabric.id()), + Cidr::new_v4(ip, 32)?, + Some("dummy"), + )?; + writeln!(interfaces)?; + write!(interfaces, "{interface}")?; + } + + if let Some(ip6) = node.ip6() { + let interface = render_interface( + &format!("dummy_{}", fabric.id()), + Cidr::new_v6(ip6, 128)?, + Some("dummy"), + )?; + writeln!(interfaces)?; + write!(interfaces, "{interface}")?; + } + + Ok(()) + } + /// Method: Generate the ifupdown2 configuration for a given node. #[export] pub fn get_interfaces_etc_network_config( @@ -494,37 +592,20 @@ pub mod pve_rs_sdn_fabrics { }); for (fabric, node) in node_fabrics { - // dummy interface - if let Some(ip) = node.ip() { - let interface = render_interface( - &format!("dummy_{}", fabric.id()), - Cidr::new_v4(ip, 32)?, - true, - )?; - writeln!(interfaces)?; - write!(interfaces, "{interface}")?; - } - if let Some(ip6) = node.ip6() { - let interface = render_interface( - &format!("dummy_{}", fabric.id()), - Cidr::new_v6(ip6, 128)?, - true, - )?; - writeln!(interfaces)?; - write!(interfaces, "{interface}")?; - } + render_dummy_interfaces(&mut interfaces, fabric, node)?; + match node { ConfigNode::Openfabric(node_section) => { for interface in node_section.properties().interfaces() { if let Some(ip) = interface.ip() { let interface = - render_interface(interface.name(), Cidr::from(ip), false)?; + render_interface(interface.name(), Cidr::from(ip), None)?; writeln!(interfaces)?; write!(interfaces, "{interface}")?; } if let Some(ip) = interface.ip6() { let interface = - render_interface(interface.name(), Cidr::from(ip), false)?; + render_interface(interface.name(), Cidr::from(ip), None)?; writeln!(interfaces)?; write!(interfaces, "{interface}")?; } @@ -541,7 +622,7 @@ pub mod pve_rs_sdn_fabrics { } else { anyhow::bail!("there has to be a ipv4 or ipv6 node address"); }); - let interface = render_interface(interface.name(), cidr, false)?; + let interface = render_interface(interface.name(), cidr, None)?; writeln!(interfaces)?; write!(interfaces, "{interface}")?; } @@ -549,10 +630,10 @@ pub mod pve_rs_sdn_fabrics { } ConfigNode::Ospf(node_section) => { for interface in node_section.properties().interfaces() { + writeln!(interfaces)?; if let Some(ip) = interface.ip() { let interface = - render_interface(interface.name(), Cidr::from(ip), false)?; - writeln!(interfaces)?; + render_interface(interface.name(), Cidr::from(ip), None)?; write!(interfaces, "{interface}")?; } else { let interface = render_interface( @@ -560,9 +641,41 @@ pub mod pve_rs_sdn_fabrics { Cidr::from(IpAddr::from(node.ip().ok_or_else(|| { anyhow::anyhow!("there has to be a ipv4 address") })?)), - false, + None, )?; - writeln!(interfaces)?; + write!(interfaces, "{interface}")?; + } + } + } + ConfigNode::WireGuard(node_section) => { + if let WireGuardNode::Internal(node_properties) = node_section.properties() { + for interface in node_properties.interfaces() { + let entry = config + .get_fabric(fabric.id()) + // safe because we use the fabric we obtained earlier from the same config + .expect("entry for fabric exists in fabric config"); + + let allowed_ips = node_properties + .peers() + .filter_map(|peer| { + if !peer.auto_generate_routes() + || peer.iface() != interface.name() + { + return None; + } + + let ConfigNode::WireGuard(node) = + entry.get_node(peer.node()).ok()? + else { + return None; + }; + + Some(node.properties().allowed_ips()) + }) + .flatten(); + + let interface = render_wireguard_interface(interface, allowed_ips)?; + write!(interfaces, "{interface}")?; } } @@ -659,6 +772,7 @@ pub mod pve_rs_sdn_fabrics { status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename()) } + FabricEntry::WireGuard(_) => Ok(Vec::new()), } } @@ -717,6 +831,7 @@ pub mod pve_rs_sdn_fabrics { ) .map(|v| v.into()) } + FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())), } } @@ -776,6 +891,7 @@ pub mod pve_rs_sdn_fabrics { ) .map(|v| v.into()) } + FabricEntry::WireGuard(_) => Ok(status::InterfaceStatus::WireGuard(Vec::new())), } } diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs index e1e3362..132a0f4 100644 --- a/pve-rs/src/sdn/status.rs +++ b/pve-rs/src/sdn/status.rs @@ -80,12 +80,22 @@ mod openfabric { } } +mod wireguard { + use serde::Serialize; + + #[derive(Debug, Serialize)] + pub struct NeighborStatus; + #[derive(Debug, Serialize)] + pub struct InterfaceStatus; +} + /// Common NeighborStatus that contains either OSPF or Openfabric neighbors #[derive(Debug, Serialize)] #[serde(untagged)] pub enum NeighborStatus { Openfabric(Vec), Ospf(Vec), + WireGuard(Vec), } impl From> for NeighborStatus { @@ -105,6 +115,7 @@ impl From> for NeighborStatus { pub enum InterfaceStatus { Openfabric(Vec), Ospf(Vec), + WireGuard(Vec), } impl From> for InterfaceStatus { @@ -135,6 +146,8 @@ pub enum Protocol { Openfabric, /// OSPF Ospf, + /// WireGuard + WireGuard, } /// The status of a fabric. @@ -217,6 +230,7 @@ pub fn get_routes( .interfaces() .map(|i| i.name().as_str()) .collect(), + ConfigNode::WireGuard(_) => HashSet::new(), }; let dummy_interface = format!("dummy_{}", fabric_id.as_str()); @@ -429,6 +443,7 @@ pub fn get_status( let (current_protocol, all_routes) = match &node { ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0), ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0), + ConfigNode::WireGuard(_) => (Protocol::WireGuard, &BTreeMap::new()), }; // get interfaces @@ -443,6 +458,7 @@ pub fn get_status( .interfaces() .map(|i| i.name().as_str()) .collect(), + ConfigNode::WireGuard(_n) => HashSet::new(), }; // determine status by checking if any routes exist for our interfaces -- 2.47.3