public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-perl-rs v5 10/29] pve-rs: fabrics: wireguard: generate ifupdown2 configuration
Date: Tue, 12 May 2026 19:31:25 +0200	[thread overview]
Message-ID: <20260512173145.596958-11-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260512173145.596958-1-s.hanreich@proxmox.com>

From: Christoph Heiss <c.heiss@proxmox.com>

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 <s.hanreich@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 pve-rs/src/bindings/sdn/fabrics.rs | 179 ++++++++++++++++++++++++-----
 pve-rs/src/sdn/status.rs           |  29 ++++-
 2 files changed, 172 insertions(+), 36 deletions(-)

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 18848c4..8eb48a6 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -24,15 +24,21 @@ 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::bindings::sdn::wireguard::pve_rs_sdn_wireguard::PerlWireguardPrivateKeyConfig;
     use crate::sdn::status::{self, RunningConfig};
 
     /// A SDN Fabric config instance.
@@ -361,6 +367,7 @@ pub mod pve_rs_sdn_fabrics {
                         }
                     }
                 }
+                ConfigNode::WireGuard(_) => {}
             }
         }
 
@@ -453,14 +460,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,
+        #[try_from_ref] private_keys: &PerlWireguardPrivateKeyConfig,
+    ) -> Result<HashMap<String, String>, Error> {
+        let config = this.fabric_config.lock().unwrap().clone().into_valid()?;
+        let private_keys = private_keys.private_keys.lock().unwrap().clone();
+
+        let configs = WireGuardConfigBuilder::new(config, private_keys).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<Item = &'a Cidr>,
+    ) -> Result<String, Error> {
+        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<String, Error> {
+    fn render_interface(name: &str, cidr: Cidr, link_type: Option<&str>) -> Result<String, Error> {
         let mut interface = String::new();
 
         writeln!(interface, "auto {name}")?;
@@ -469,14 +540,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 +593,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 +623,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 +631,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 +642,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.skip_route_generation()
+                                        || 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 +773,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 +832,7 @@ pub mod pve_rs_sdn_fabrics {
                 )
                 .map(|v| v.into())
             }
+            FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())),
         }
     }
 
@@ -776,6 +892,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..a54668b 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<openfabric::NeighborStatus>),
     Ospf(Vec<ospf::NeighborStatus>),
+    WireGuard(Vec<wireguard::NeighborStatus>),
 }
 
 impl From<Vec<openfabric::NeighborStatus>> for NeighborStatus {
@@ -105,6 +115,7 @@ impl From<Vec<ospf::NeighborStatus>> for NeighborStatus {
 pub enum InterfaceStatus {
     Openfabric(Vec<openfabric::InterfaceStatus>),
     Ospf(Vec<ospf::InterfaceStatus>),
+    WireGuard(Vec<wireguard::InterfaceStatus>),
 }
 
 impl From<Vec<openfabric::InterfaceStatus>> 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
@@ -458,13 +474,16 @@ pub fn get_status(
             })
         });
 
+        let status = match current_protocol {
+            Protocol::Openfabric if has_routes => FabricStatus::Ok,
+            Protocol::Ospf if has_routes => FabricStatus::Ok,
+            Protocol::WireGuard => FabricStatus::Ok,
+            _ => FabricStatus::NotOk,
+        };
+
         let fabric = Status {
             ty: "network".to_owned(),
-            status: if has_routes {
-                FabricStatus::Ok
-            } else {
-                FabricStatus::NotOk
-            },
+            status,
             protocol: current_protocol,
             network: fabric_id.clone(),
             network_type: "fabric".to_string(),
-- 
2.47.3





  parent reply	other threads:[~2026-05-12 17:35 UTC|newest]

Thread overview: 35+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-cluster v5 01/29] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 02/29] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 03/29] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 04/29] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 05/29] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 06/29] ve-config: wireguard: add private keys section config Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 07/29] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 08/29] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 09/29] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
2026-05-12 17:31 ` Stefan Hanreich [this message]
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 11/29] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 12/29] pve-rs: sdn: wireguard: add private keys module Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 13/29] sdn: add wireguard helper module Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 14/29] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 15/29] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 16/29] network: sdn: generate wireguard configuration on apply Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 17/29] ui: fix parsing of property-strings when values contain = Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 18/29] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 19/29] sdn: fabrics view: handle case where interfaces are deleted Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 20/29] ui: fabrics: split node selector creation and config Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 21/29] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 22/29] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 23/29] ui: fabrics: interface: " Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 24/29] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
2026-05-12 17:41   ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 25/29] ui: fabrics: wireguard: add node " Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 26/29] ui: fabrics: wireguard: add fabric " Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 27/29] ui: fabrics: hook up wireguard components Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 28/29] fabrics: node edit: add option to include wireguard interfaces Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-docs v5 29/29] sdn: fabrics: add section about wireguard Stefan Hanreich
2026-05-12 17:38   ` Stefan Hanreich
2026-05-13  2:51 ` partially-applied: [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Thomas Lamprecht
2026-05-15  5:02 ` applied: " Thomas Lamprecht
2026-05-15  5:04 ` Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260512173145.596958-11-s.hanreich@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal