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 2D1C91FF13F for ; Thu, 07 May 2026 14:43:56 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id AD76F1A2B9; Thu, 7 May 2026 14:41:15 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network v4 19/31] fabrics: wireguard: implement wireguard key auto-generation Date: Thu, 7 May 2026 14:39:54 +0200 Message-ID: <20260507124008.417223-20-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260507124008.417223-1-s.hanreich@proxmox.com> References: <20260507124008.417223-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: 1778157509894 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.640 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: VLEJSBMXBH4HTWGSSCBJ3SMJPX5ARZAU X-Message-ID-Hash: VLEJSBMXBH4HTWGSSCBJ3SMJPX5ARZAU 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: Add additional logic to the existing fabrics API endpoints that automatically create / delete keypairs for wireguard interfaces in /etc/wireguard/proxmox. Keys are generated when new interfaces are added - while they are deleted when applying the SDN configuration. After generating the key, it is stored alongside the user-defined configuration in the section config. This allows for easy access to the public key of other nodes during config generation and when returning them from the API. Signed-off-by: Stefan Hanreich --- src/PVE/API2/Network/SDN.pm | 2 + .../API2/Network/SDN/Fabrics/FabricNode.pm | 105 +++++++++++++++++- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm index 6b73af2..e3c8d9d 100644 --- a/src/PVE/API2/Network/SDN.pm +++ b/src/PVE/API2/Network/SDN.pm @@ -325,6 +325,8 @@ __PACKAGE__->register_method({ PVE::Network::SDN::commit_config(); $new_config_has_frr = PVE::Network::SDN::running_config_has_frr(); + PVE::Network::SDN::WireGuard::cleanup_private_keys(); + PVE::Network::SDN::delete_global_lock() if $lock_token && $release_lock; }, "could not commit SDN config", diff --git a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm index 000e4c3..2df52c1 100644 --- a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm +++ b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm @@ -3,11 +3,13 @@ package PVE::API2::Network::SDN::Fabrics::FabricNode; use strict; use warnings; -use PVE::JSONSchema qw(get_standard_option); -use PVE::Tools qw(extract_param); +use PVE::JSONSchema qw(get_standard_option parse_property_string); +use PVE::Tools qw(extract_param run_command); use PVE::Network::SDN; use PVE::Network::SDN::Fabrics; +use PVE::Network::SDN::WireGuard; +use PVE::RS::SDN::Fabrics; use PVE::RESTHandler; use base qw(PVE::RESTHandler); @@ -131,6 +133,11 @@ __PACKAGE__->register_method({ }, }); +my sub is_internal_wireguard_node { + my ($node) = @_; + return $node->{protocol} eq 'wireguard' && $node->{role} eq 'internal'; +} + __PACKAGE__->register_method({ name => 'add_node', path => '', @@ -162,8 +169,42 @@ __PACKAGE__->register_method({ my $digest = extract_param($param, 'digest'); PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest; - $config->add_node($param); - PVE::Network::SDN::Fabrics::write_config($config); + if (is_internal_wireguard_node($param) && $param->{interfaces}) { + my $private_keys = PVE::Network::SDN::WireGuard::private_keys(); + + my @parsed_interfaces = map { + PVE::RS::SDN::Fabrics::parse_wireguard_create_interface($_) + } $param->{interfaces}->@*; + + my @interfaces; + for my $interface (@parsed_interfaces) { + $interface->{public_key} = + $private_keys->upsert($param->{node_id}, $interface->{name}); + push @interfaces, + PVE::RS::SDN::Fabrics::print_wireguard_interface($interface); + } + + $param->{interfaces} = \@interfaces; + $config->add_node($param); + + eval { PVE::Network::SDN::WireGuard::write_private_keys($private_keys); }; + die "could not save private key config: $@\n" if $@; + + eval { PVE::Network::SDN::Fabrics::write_config($config); }; + if (my $err = $@) { + for my $interface (@parsed_interfaces) { + $private_keys->delete($param->{node_id}, $interface->{name}); + } + + eval { PVE::Network::SDN::WireGuard::write_private_keys($private_keys) }; + warn "could not roll back private key config: $@\n" if $@; + + die $err; + } + } else { + $config->add_node($param); + PVE::Network::SDN::Fabrics::write_config($config); + } }, "adding node failed", $lock_token, @@ -205,8 +246,58 @@ __PACKAGE__->register_method({ my $digest = extract_param($param, 'digest'); PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest; - $config->update_node($fabric_id, $node_id, $param); - PVE::Network::SDN::Fabrics::write_config($config); + my $old_node = $config->get_node($fabric_id, $node_id); + + # required so rust can parse the proper wireguard node + # variant + $param->{role} = $old_node->{role} if $old_node->{protocol} eq 'wireguard'; + + if (is_internal_wireguard_node($param)) { + my $private_keys = PVE::Network::SDN::WireGuard::private_keys(); + + my %new_interfaces = map { + my $interface = + PVE::RS::SDN::Fabrics::parse_wireguard_create_interface($_); + $interface->{name} => $interface + } $param->{interfaces}->@*; + + my %old_interfaces = map { + my $interface = PVE::RS::SDN::Fabrics::parse_wireguard_interface($_); + $interface->{name} => $interface + } $old_node->{interfaces}->@*; + + my @interfaces; + for my $interface_name (keys %new_interfaces) { + my $interface = $new_interfaces{$interface_name}; + $interface->{public_key} = + $private_keys->upsert($node_id, $interface_name) + if !exists($old_interfaces{$interface_name}); + push @interfaces, + PVE::RS::SDN::Fabrics::print_wireguard_interface($interface); + } + $param->{interfaces} = \@interfaces; + + $config->update_node($fabric_id, $node_id, $param); + + eval { PVE::Network::SDN::WireGuard::write_private_keys($private_keys); }; + die "could not save private key config: $@\n" if $@; + + eval { PVE::Network::SDN::Fabrics::write_config($config); }; + + if (my $err = $@) { + for my $interface (values %new_interfaces) { + $private_keys->delete($node_id, $interface->{name}); + } + + eval { PVE::Network::SDN::WireGuard::write_private_keys($private_keys) }; + warn "could not roll back private key config: $@\n" if $@; + + die $err; + } + } else { + $config->update_node($fabric_id, $node_id, $param); + PVE::Network::SDN::Fabrics::write_config($config); + } }, "updating node failed", $lock_token, @@ -251,6 +342,8 @@ __PACKAGE__->register_method({ my $digest = extract_param($param, 'digest'); PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest; + my $old_node = $config->get_node($fabric_id, $node_id); + $config->delete_node($fabric_id, $node_id); PVE::Network::SDN::Fabrics::write_config($config); }, -- 2.47.3