From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-perl-rs 1/2] pve-rs: fabrics: wireguard: generate ifupdown2 configuration
Date: Thu, 19 Feb 2026 15:56:29 +0100 [thread overview]
Message-ID: <20260219145649.441418-13-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260219145649.441418-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. This also happens via respective post-up
/ down commands in the ifupdown2 configuration file. Currently, it is
not ECMP aware, so it is not possible to route the same subnet via two
different WireGuard interfaces. This is intended to be implemented in
a future patch series. For now, this function generates two entries in
the routing table and only the first inserted one works, analogous to
how ifupdown2 behaves when configuring the same subnet twice.
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/Cargo.toml | 1 +
pve-rs/src/bindings/sdn/fabrics.rs | 177 ++++++++++++++++++++++++-----
pve-rs/src/sdn/status.rs | 16 +++
3 files changed, 165 insertions(+), 29 deletions(-)
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 527fcae..3757b80 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -42,6 +42,7 @@ proxmox-notify = { version = "1", features = ["pve-context"] }
proxmox-oci = "0.2.1"
proxmox-openid = "1.0.2"
proxmox-resource-scheduling = "1.0.1"
+proxmox-schema = "5"
proxmox-section-config = "3"
proxmox-shared-cache = "1"
proxmox-subscription = "1"
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 54606e7..daec8a7 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -35,6 +35,11 @@ pub mod pve_rs_sdn_fabrics {
};
use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry};
use proxmox_ve_config::sdn::frr::FrrConfigBuilder;
+ 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};
@@ -364,6 +369,7 @@ pub mod pve_rs_sdn_fabrics {
}
}
}
+ ConfigNode::WireGuard(_) => {}
}
}
@@ -456,6 +462,7 @@ pub mod pve_rs_sdn_fabrics {
FabricEntry::Openfabric(_) => {
daemons.insert("fabricd");
}
+ FabricEntry::WireGuard(_) => {} // not a frr fabric
};
}
@@ -478,8 +485,72 @@ pub mod pve_rs_sdn_fabrics {
to_raw_config(&frr_config)
}
+ /// 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<HashMap<String, String>, 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>(
+ name: &str,
+ cidrs: (Option<&Ipv4Cidr>, Option<&Ipv6Cidr>),
+ allowed_ips: impl Iterator<Item = &'a Cidr>,
+ ) -> Result<String, Error> {
+ let mut interface = String::new();
+
+ let (ip, ip6) = cidrs;
+
+ writeln!(interface, "auto {name}")?;
+
+ if let Some(ip) = 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) = 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}")?;
@@ -488,14 +559,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(
@@ -513,37 +612,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}")?;
}
@@ -560,7 +642,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}")?;
}
@@ -568,10 +650,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(
@@ -579,9 +661,43 @@ 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| {
+ return if peer.iface() == interface.name() {
+ let ConfigNode::WireGuard(node) =
+ entry.get_node(peer.node()).ok()?
+ else {
+ return None;
+ };
+
+ Some(node.properties().allowed_ips())
+ } else {
+ None
+ };
+ })
+ .flatten();
+
+ let interface = render_wireguard_interface(
+ interface.name(),
+ (interface.ip(), interface.ip6()),
+ allowed_ips,
+ )?;
+
write!(interfaces, "{interface}")?;
}
}
@@ -678,6 +794,7 @@ pub mod pve_rs_sdn_fabrics {
status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename())
}
+ FabricEntry::WireGuard(_) => Ok(Vec::new()),
}
}
@@ -736,6 +853,7 @@ pub mod pve_rs_sdn_fabrics {
)
.map(|v| v.into())
}
+ FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())),
}
}
@@ -795,6 +913,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<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
--
2.47.3
next prev parent reply other threads:[~2026-02-19 14:56 UTC|newest]
Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox 1/2] wireguard: skip serializing preshared_key if unset Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox 2/2] wireguard: implement ApiType for endpoints and hostnames Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 1/9] debian: update control file Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 2/9] clippy: fix 'hiding a lifetime that's elided elsewhere is confusing' Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 3/9] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 4/9] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 5/9] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 6/9] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 7/9] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 8/9] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
2026-02-19 14:56 ` Stefan Hanreich [this message]
2026-02-19 14:56 ` [PATCH proxmox-perl-rs 2/2] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 1/3] sdn: add wireguard helper module Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 2/3] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 3/3] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 01/11] network: sdn: generate wireguard configuration on apply Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 02/11] ui: fix parsing of property-strings when values contain = Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 03/11] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 04/11] ui: fabrics: split node selector creation and config Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 05/11] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 06/11] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 07/11] ui: fabrics: interface: " Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 08/11] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 09/11] ui: fabrics: wireguard: add node " Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 10/11] ui: fabrics: wireguard: add fabric " Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 11/11] ui: fabrics: hook up wireguard components Stefan Hanreich
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=20260219145649.441418-13-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