From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs v2 02/11] ve-config: add per-vnet IPv6 RA configuration
Date: Thu, 30 Apr 2026 16:29:44 +0200 [thread overview]
Message-ID: <20260430142953.315412-3-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260430142953.315412-1-h.laimer@proxmox.com>
Wire IPv6 Router Advertisement and per-prefix settings into the
running-config types so the typed FRR pipeline can consume them.
Per-RA settings live on the vnet, per-prefix overrides on each
subnet, matching where each constraint applies in the protocol.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
proxmox-ve-config/src/sdn/config.rs | 122 +++++++++++++++-
proxmox-ve-config/src/sdn/mod.rs | 1 +
proxmox-ve-config/src/sdn/nd.rs | 129 +++++++++++++++++
proxmox-ve-config/tests/nd/main.rs | 137 ++++++++++++++++++
.../nd__explicit_lifetimes_are_preserved.snap | 9 ++
.../nd__mixed_subnets_under_one_vnet.snap | 14 ++
.../snapshots/nd__no_autoconfig_prefix.snap | 9 ++
.../nd__off_link_emits_off_link_modifier.snap | 10 ++
...vel_optional_knobs_are_passed_through.snap | 13 ++
.../nd__slaac_with_default_lifetimes.snap | 9 ++
10 files changed, 446 insertions(+), 7 deletions(-)
create mode 100644 proxmox-ve-config/src/sdn/nd.rs
create mode 100644 proxmox-ve-config/tests/nd/main.rs
create mode 100644 proxmox-ve-config/tests/nd/snapshots/nd__explicit_lifetimes_are_preserved.snap
create mode 100644 proxmox-ve-config/tests/nd/snapshots/nd__mixed_subnets_under_one_vnet.snap
create mode 100644 proxmox-ve-config/tests/nd/snapshots/nd__no_autoconfig_prefix.snap
create mode 100644 proxmox-ve-config/tests/nd/snapshots/nd__off_link_emits_off_link_modifier.snap
create mode 100644 proxmox-ve-config/tests/nd/snapshots/nd__ra_level_optional_knobs_are_passed_through.snap
create mode 100644 proxmox-ve-config/tests/nd/snapshots/nd__slaac_with_default_lifetimes.snap
diff --git a/proxmox-ve-config/src/sdn/config.rs b/proxmox-ve-config/src/sdn/config.rs
index afc5175..38e6e43 100644
--- a/proxmox-ve-config/src/sdn/config.rs
+++ b/proxmox-ve-config/src/sdn/config.rs
@@ -2,7 +2,7 @@ use std::{
collections::{BTreeMap, HashMap},
error::Error,
fmt::Display,
- net::IpAddr,
+ net::{IpAddr, Ipv6Addr},
str::FromStr,
};
@@ -16,7 +16,10 @@ use crate::{
ipset::{IpsetEntry, IpsetName, IpsetScope},
Ipset,
},
- sdn::{SdnNameError, SubnetName, VnetName, ZoneName},
+ sdn::{
+ nd::{Ipv6RaConfig, NdPrefixConfig},
+ SdnNameError, SubnetName, VnetName, ZoneName,
+ },
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -185,6 +188,25 @@ pub struct SubnetRunningConfig {
snat: Option<u8>,
#[serde(rename = "dhcp-range")]
dhcp_range: Option<Vec<PropertyString<DhcpRange>>>,
+
+ // Per-prefix RA / SLAAC overrides. Only meaningful for IPv6 subnets whose vnet has
+ // RA enabled, silently ignored otherwise.
+ #[serde(rename = "nd-prefix-autonomous")]
+ nd_prefix_autonomous: Option<u8>,
+ #[serde(rename = "nd-prefix-on-link")]
+ nd_prefix_on_link: Option<u8>,
+ #[serde(
+ default,
+ rename = "nd-prefix-valid-lifetime",
+ deserialize_with = "proxmox_serde::perl::deserialize_u32"
+ )]
+ nd_prefix_valid_lifetime: Option<u32>,
+ #[serde(
+ default,
+ rename = "nd-prefix-preferred-lifetime",
+ deserialize_with = "proxmox_serde::perl::deserialize_u32"
+ )]
+ nd_prefix_preferred_lifetime: Option<u32>,
}
/// Struct for deserializing the subnets of the SDN running config
@@ -196,8 +218,55 @@ pub struct SubnetsRunningConfig {
/// Struct for deserializing a vnet entry of the SDN running config
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VnetRunningConfig {
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_u32")]
tag: Option<u32>,
zone: ZoneName,
+
+ // Per-vnet IPv6 RA settings. `ipv6-ra` is the master toggle, the rest are only
+ // meaningful when it is enabled.
+ #[serde(rename = "ipv6-ra")]
+ ipv6_ra: Option<u8>,
+ #[serde(rename = "ipv6-ra-managed")]
+ ipv6_ra_managed: Option<u8>,
+ #[serde(rename = "ipv6-ra-other")]
+ ipv6_ra_other: Option<u8>,
+ #[serde(rename = "ipv6-ra-rdnss")]
+ ipv6_ra_rdnss: Option<Vec<Ipv6Addr>>,
+ #[serde(
+ default,
+ rename = "ipv6-ra-router-lifetime",
+ deserialize_with = "proxmox_serde::perl::deserialize_u32"
+ )]
+ ipv6_ra_router_lifetime: Option<u32>,
+ #[serde(
+ default,
+ rename = "ipv6-ra-interval",
+ deserialize_with = "proxmox_serde::perl::deserialize_u32"
+ )]
+ ipv6_ra_interval: Option<u32>,
+ #[serde(
+ default,
+ rename = "ipv6-ra-mtu",
+ deserialize_with = "proxmox_serde::perl::deserialize_u32"
+ )]
+ ipv6_ra_mtu: Option<u32>,
+}
+
+impl VnetRunningConfig {
+ /// Materialize the IPv6 RA settings if enabled on this vnet, otherwise return `None`.
+ pub fn ipv6_ra_config(&self) -> Option<Ipv6RaConfig> {
+ if self.ipv6_ra.unwrap_or(0) == 0 {
+ return None;
+ }
+ Some(Ipv6RaConfig {
+ managed: self.ipv6_ra_managed.unwrap_or(0) != 0,
+ other: self.ipv6_ra_other.unwrap_or(0) != 0,
+ rdnss: self.ipv6_ra_rdnss.clone().unwrap_or_default(),
+ router_lifetime: self.ipv6_ra_router_lifetime,
+ interval: self.ipv6_ra_interval,
+ mtu: self.ipv6_ra_mtu,
+ })
+ }
}
/// struct for deserializing the vnets of the SDN running config
@@ -223,6 +292,7 @@ pub struct SubnetConfig {
gateway: Option<IpAddr>,
snat: bool,
dhcp_range: Vec<IpRange>,
+ nd_prefix: NdPrefixConfig,
}
impl SubnetConfig {
@@ -247,6 +317,7 @@ impl SubnetConfig {
gateway,
snat,
dhcp_range: dhcp_range.into_iter().collect(),
+ nd_prefix: NdPrefixConfig::default(),
})
}
@@ -269,7 +340,23 @@ impl SubnetConfig {
None => Vec::new(),
};
- Self::new(name, running_config.gateway, snat, dhcp_range)
+ let nd_default = NdPrefixConfig::default();
+ let nd_prefix = NdPrefixConfig {
+ autonomous: running_config
+ .nd_prefix_autonomous
+ .map(|v| v != 0)
+ .unwrap_or(nd_default.autonomous),
+ on_link: running_config
+ .nd_prefix_on_link
+ .map(|v| v != 0)
+ .unwrap_or(nd_default.on_link),
+ valid_lifetime: running_config.nd_prefix_valid_lifetime,
+ preferred_lifetime: running_config.nd_prefix_preferred_lifetime,
+ };
+
+ let mut config = Self::new(name, running_config.gateway, snat, dhcp_range)?;
+ config.nd_prefix = nd_prefix;
+ Ok(config)
}
pub fn name(&self) -> &SubnetName {
@@ -291,6 +378,16 @@ impl SubnetConfig {
pub fn dhcp_ranges(&self) -> impl Iterator<Item = &IpRange> + '_ {
self.dhcp_range.iter()
}
+
+ /// Per-prefix Router Advertisement overrides. Defaults to "include the prefix in the RA
+ /// with autonomous + on-link flags set" if no explicit overrides were configured.
+ pub fn nd_prefix(&self) -> &NdPrefixConfig {
+ &self.nd_prefix
+ }
+
+ pub fn set_nd_prefix(&mut self, nd_prefix: NdPrefixConfig) {
+ self.nd_prefix = nd_prefix;
+ }
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -298,6 +395,7 @@ pub struct VnetConfig {
name: VnetName,
tag: Option<u32>,
subnets: BTreeMap<Cidr, SubnetConfig>,
+ ipv6_ra: Option<Ipv6RaConfig>,
}
impl VnetConfig {
@@ -306,6 +404,7 @@ impl VnetConfig {
name,
subnets: BTreeMap::default(),
tag,
+ ipv6_ra: None,
}
}
@@ -360,6 +459,15 @@ impl VnetConfig {
pub fn tag(&self) -> &Option<u32> {
&self.tag
}
+
+ /// Per-vnet IPv6 RA settings. `None` means RAs are not emitted on this vnet's bridge.
+ pub fn ipv6_ra(&self) -> &Option<Ipv6RaConfig> {
+ &self.ipv6_ra
+ }
+
+ pub fn set_ipv6_ra(&mut self, ipv6_ra: Option<Ipv6RaConfig>) {
+ self.ipv6_ra = ipv6_ra;
+ }
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -635,10 +743,10 @@ impl TryFrom<RunningConfig> for SdnConfig {
if let Some(running_vnets) = value.vnets.take() {
for (name, running_config) in running_vnets.ids {
- config.add_vnet(
- &running_config.zone,
- VnetConfig::new(name, running_config.tag),
- )?;
+ let ipv6_ra = running_config.ipv6_ra_config();
+ let mut vnet = VnetConfig::new(name, running_config.tag);
+ vnet.set_ipv6_ra(ipv6_ra);
+ config.add_vnet(&running_config.zone, vnet)?;
}
}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 2133396..457a9ec 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,6 +1,7 @@
pub mod config;
pub mod fabric;
pub mod ipam;
+pub mod nd;
pub mod prefix_list;
pub mod route_map;
pub mod wireguard;
diff --git a/proxmox-ve-config/src/sdn/nd.rs b/proxmox-ve-config/src/sdn/nd.rs
new file mode 100644
index 0000000..25985d2
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/nd.rs
@@ -0,0 +1,129 @@
+//! IPv6 Router Advertisement / Neighbor Discovery configuration.
+//!
+//! Hosts on an EVPN vnet can autoconfigure addresses via SLAAC when the per-node anycast
+//! gateway emits Router Advertisements. RA configuration is split across two PVE objects
+//! to match the protocol layers:
+//!
+//! * Per-vnet ([`Ipv6RaConfig`]) holds the RA-level settings (M and O flags, RDNSS list,
+//! optional router lifetime, RA interval, advertised MTU). One per vnet.
+//! * Per-subnet ([`NdPrefixConfig`]) holds the per-prefix overrides (autonomous and
+//! on-link flags, valid and preferred lifetimes).
+//!
+//! These are exposed as fields on [`VnetConfig`](crate::sdn::config::VnetConfig) and
+//! [`SubnetConfig`](crate::sdn::config::SubnetConfig). The FRR conversion lives in the
+//! [`frr`] submodule.
+
+use std::net::Ipv6Addr;
+
+use serde::{Deserialize, Serialize};
+
+/// Default valid lifetime (30 days) for advertised prefixes when no override is set.
+pub const DEFAULT_PREFIX_VALID_LIFETIME: u32 = 2_592_000;
+/// Default preferred lifetime (7 days) for advertised prefixes when no override is set.
+pub const DEFAULT_PREFIX_PREFERRED_LIFETIME: u32 = 604_800;
+
+/// Per-vnet IPv6 Router Advertisement configuration.
+///
+/// Presence of this struct on a [`VnetConfig`](crate::sdn::config::VnetConfig) implies the
+/// vnet has RAs enabled. Absence means RAs are suppressed for the vnet.
+#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Ipv6RaConfig {
+ pub managed: bool,
+ pub other: bool,
+ pub rdnss: Vec<Ipv6Addr>,
+ pub router_lifetime: Option<u32>,
+ pub interval: Option<u32>,
+ pub mtu: Option<u32>,
+}
+
+/// Per-subnet (per-prefix) overrides for Router Advertisements.
+///
+/// Defaults match the typical SLAAC use case: autonomous and on-link flags set, FRR's
+/// default lifetimes.
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct NdPrefixConfig {
+ pub autonomous: bool,
+ pub on_link: bool,
+ pub valid_lifetime: Option<u32>,
+ pub preferred_lifetime: Option<u32>,
+}
+
+impl Default for NdPrefixConfig {
+ fn default() -> Self {
+ Self {
+ autonomous: true,
+ on_link: true,
+ valid_lifetime: None,
+ preferred_lifetime: None,
+ }
+ }
+}
+
+#[cfg(feature = "frr")]
+pub mod frr {
+ //! FRR conversion for IPv6 RA / ND configuration.
+ //!
+ //! Folds the per-vnet [`Ipv6RaConfig`] and the per-subnet [`NdPrefixConfig`] of every
+ //! IPv6 subnet under the vnet into a single
+ //! [`NdInterface`](proxmox_frr::ser::nd::NdInterface) keyed by the vnet name in
+ //! [`FrrConfig::nd_interfaces`](proxmox_frr::ser::FrrConfig::nd_interfaces).
+
+ use super::*;
+
+ use proxmox_frr::ser::nd::{NdInterface, NdPrefix, NdPrefixMode};
+ use proxmox_network_types::ip_address::Cidr;
+
+ use crate::sdn::config::{SubnetConfig, VnetConfig};
+
+ /// Build an [`NdInterface`] for the given vnet from its RA settings and per-subnet
+ /// prefix overrides.
+ ///
+ /// Returns `None` when:
+ /// * the vnet has no [`Ipv6RaConfig`] (RAs are disabled), or
+ /// * the vnet has no IPv6 subnet (no prefix to advertise).
+ pub fn build_nd_interface<'a>(
+ vnet: &'a VnetConfig,
+ subnets: impl IntoIterator<Item = &'a SubnetConfig>,
+ ) -> Option<NdInterface> {
+ let ra = vnet.ipv6_ra().as_ref()?;
+
+ let mut prefixes = Vec::new();
+ for subnet in subnets {
+ let &Cidr::Ipv6(cidr) = subnet.cidr() else {
+ continue;
+ };
+ let nd = subnet.nd_prefix();
+
+ let mode = if nd.autonomous {
+ NdPrefixMode::Autoconfig {
+ valid: nd.valid_lifetime.unwrap_or(DEFAULT_PREFIX_VALID_LIFETIME),
+ preferred: nd
+ .preferred_lifetime
+ .unwrap_or(DEFAULT_PREFIX_PREFERRED_LIFETIME),
+ }
+ } else {
+ NdPrefixMode::NoAutoconfig
+ };
+
+ prefixes.push(NdPrefix {
+ cidr,
+ on_link: nd.on_link,
+ mode,
+ });
+ }
+
+ if prefixes.is_empty() {
+ return None;
+ }
+
+ Some(NdInterface {
+ managed_config_flag: ra.managed,
+ other_config_flag: ra.other,
+ rdnss: ra.rdnss.clone(),
+ router_lifetime: ra.router_lifetime,
+ interval: ra.interval,
+ mtu: ra.mtu,
+ prefixes,
+ })
+ }
+}
diff --git a/proxmox-ve-config/tests/nd/main.rs b/proxmox-ve-config/tests/nd/main.rs
new file mode 100644
index 0000000..9eeedf8
--- /dev/null
+++ b/proxmox-ve-config/tests/nd/main.rs
@@ -0,0 +1,137 @@
+#![cfg(feature = "frr")]
+
+use proxmox_frr::ser::{serializer::dump, FrrConfig, InterfaceName};
+use proxmox_network_types::ip_address::Cidr;
+use proxmox_ve_config::sdn::{
+ config::{SubnetConfig, VnetConfig},
+ nd::{frr::build_nd_interface, Ipv6RaConfig, NdPrefixConfig},
+ SubnetName, VnetName, ZoneName,
+};
+
+fn vnet(name: &str, ra: Option<Ipv6RaConfig>) -> VnetConfig {
+ let mut v = VnetConfig::new(VnetName::new(name.to_owned()).unwrap(), Some(100));
+ v.set_ipv6_ra(ra);
+ v
+}
+
+fn subnet(zone: &str, cidr: &str, nd: NdPrefixConfig) -> SubnetConfig {
+ let cidr: Cidr = cidr.parse().unwrap();
+ let zone = ZoneName::new(zone.to_owned()).unwrap();
+ let name = SubnetName::new(zone, cidr);
+ let mut s = SubnetConfig::new(name, None, false, std::iter::empty()).unwrap();
+ s.set_nd_prefix(nd);
+ s
+}
+
+fn render(vname: &str, iface: proxmox_frr::ser::nd::NdInterface) -> String {
+ let mut config = FrrConfig::default();
+ let name: InterfaceName = vname.to_owned().try_into().unwrap();
+ config.nd_interfaces.insert(name, iface);
+ dump(&config).expect("renders")
+}
+
+#[test]
+fn slaac_with_default_lifetimes() {
+ let v = vnet("vrnet100", Some(Ipv6RaConfig::default()));
+ let s = [subnet("zone", "fd00:1::/64", NdPrefixConfig::default())];
+ let iface = build_nd_interface(&v, &s).expect("ra enabled");
+ insta::assert_snapshot!(render("vrnet100", iface));
+}
+
+#[test]
+fn no_autoconfig_prefix() {
+ let v = vnet("vrnet200", Some(Ipv6RaConfig::default()));
+ let s = [subnet(
+ "zone",
+ "fd00:2::/64",
+ NdPrefixConfig {
+ autonomous: false,
+ ..Default::default()
+ },
+ )];
+ let iface = build_nd_interface(&v, &s).expect("ra enabled");
+ insta::assert_snapshot!(render("vrnet200", iface));
+}
+
+#[test]
+fn mixed_subnets_under_one_vnet() {
+ let ra = Ipv6RaConfig {
+ managed: true,
+ other: true,
+ rdnss: vec![
+ "2001:db8::1".parse().unwrap(),
+ "2001:db8::2".parse().unwrap(),
+ ],
+ ..Default::default()
+ };
+ let v = vnet("vrnet300", Some(ra));
+ let s = [
+ // SLAAC-eligible /64
+ subnet("zone", "fd00:1::/64", NdPrefixConfig::default()),
+ // /96 announced but not autoconfig
+ subnet(
+ "zone",
+ "fd00:2::/96",
+ NdPrefixConfig {
+ autonomous: false,
+ ..Default::default()
+ },
+ ),
+ // IPv4 subnet must be skipped silently
+ subnet("zone", "10.0.0.0/24", NdPrefixConfig::default()),
+ ];
+ let iface = build_nd_interface(&v, &s).expect("ra enabled");
+ insta::assert_snapshot!(render("vrnet300", iface));
+}
+
+#[test]
+fn explicit_lifetimes_are_preserved() {
+ let v = vnet("vrnet400", Some(Ipv6RaConfig::default()));
+ let s = [subnet(
+ "zone",
+ "fd00:1::/64",
+ NdPrefixConfig {
+ valid_lifetime: Some(3600),
+ preferred_lifetime: Some(1800),
+ ..Default::default()
+ },
+ )];
+ let iface = build_nd_interface(&v, &s).expect("ra enabled");
+ insta::assert_snapshot!(render("vrnet400", iface));
+}
+
+#[test]
+fn ra_disabled_returns_none() {
+ let v = vnet("vrnet500", None);
+ let s = [subnet("zone", "fd00:1::/64", NdPrefixConfig::default())];
+ assert!(build_nd_interface(&v, &s).is_none());
+}
+
+#[test]
+fn off_link_emits_off_link_modifier() {
+ let v = vnet("vrnet600", Some(Ipv6RaConfig::default()));
+ let s = [subnet(
+ "zone",
+ "fd00:1::/64",
+ NdPrefixConfig {
+ on_link: false,
+ ..Default::default()
+ },
+ )];
+ let iface = build_nd_interface(&v, &s).expect("ra enabled");
+ insta::assert_snapshot!(render("vrnet600", iface));
+}
+
+#[test]
+fn ra_level_optional_knobs_are_passed_through() {
+ let ra = Ipv6RaConfig {
+ router_lifetime: Some(0),
+ interval: Some(60),
+ mtu: Some(1450),
+ ..Default::default()
+ };
+ let v = vnet("vrnet700", Some(ra));
+ let s = [subnet("zone", "fd00:1::/64", NdPrefixConfig::default())];
+ let iface = build_nd_interface(&v, &s).expect("ra enabled");
+ insta::assert_snapshot!(render("vrnet700", iface));
+}
diff --git a/proxmox-ve-config/tests/nd/snapshots/nd__explicit_lifetimes_are_preserved.snap b/proxmox-ve-config/tests/nd/snapshots/nd__explicit_lifetimes_are_preserved.snap
new file mode 100644
index 0000000..8abcd07
--- /dev/null
+++ b/proxmox-ve-config/tests/nd/snapshots/nd__explicit_lifetimes_are_preserved.snap
@@ -0,0 +1,9 @@
+---
+source: proxmox-ve-config/tests/nd/main.rs
+expression: "render(\"vrnet400\", iface)"
+---
+!
+interface vrnet400
+ no ipv6 nd suppress-ra
+ ipv6 nd prefix fd00:1::/64 3600 1800
+exit
diff --git a/proxmox-ve-config/tests/nd/snapshots/nd__mixed_subnets_under_one_vnet.snap b/proxmox-ve-config/tests/nd/snapshots/nd__mixed_subnets_under_one_vnet.snap
new file mode 100644
index 0000000..2e86e71
--- /dev/null
+++ b/proxmox-ve-config/tests/nd/snapshots/nd__mixed_subnets_under_one_vnet.snap
@@ -0,0 +1,14 @@
+---
+source: proxmox-ve-config/tests/nd/main.rs
+expression: "render(\"vrnet300\", iface)"
+---
+!
+interface vrnet300
+ no ipv6 nd suppress-ra
+ ipv6 nd managed-config-flag
+ ipv6 nd other-config-flag
+ ipv6 nd rdnss 2001:db8::1
+ ipv6 nd rdnss 2001:db8::2
+ ipv6 nd prefix fd00:1::/64 2592000 604800
+ ipv6 nd prefix fd00:2::/96 no-autoconfig
+exit
diff --git a/proxmox-ve-config/tests/nd/snapshots/nd__no_autoconfig_prefix.snap b/proxmox-ve-config/tests/nd/snapshots/nd__no_autoconfig_prefix.snap
new file mode 100644
index 0000000..43668db
--- /dev/null
+++ b/proxmox-ve-config/tests/nd/snapshots/nd__no_autoconfig_prefix.snap
@@ -0,0 +1,9 @@
+---
+source: proxmox-ve-config/tests/nd/main.rs
+expression: "render(\"vrnet200\", iface)"
+---
+!
+interface vrnet200
+ no ipv6 nd suppress-ra
+ ipv6 nd prefix fd00:2::/64 no-autoconfig
+exit
diff --git a/proxmox-ve-config/tests/nd/snapshots/nd__off_link_emits_off_link_modifier.snap b/proxmox-ve-config/tests/nd/snapshots/nd__off_link_emits_off_link_modifier.snap
new file mode 100644
index 0000000..86d2f09
--- /dev/null
+++ b/proxmox-ve-config/tests/nd/snapshots/nd__off_link_emits_off_link_modifier.snap
@@ -0,0 +1,10 @@
+---
+source: proxmox-ve-config/tests/nd/main.rs
+assertion_line: 131
+expression: "render(\"vrnet600\", iface)"
+---
+!
+interface vrnet600
+ no ipv6 nd suppress-ra
+ ipv6 nd prefix fd00:1::/64 2592000 604800 off-link
+exit
diff --git a/proxmox-ve-config/tests/nd/snapshots/nd__ra_level_optional_knobs_are_passed_through.snap b/proxmox-ve-config/tests/nd/snapshots/nd__ra_level_optional_knobs_are_passed_through.snap
new file mode 100644
index 0000000..cc0a689
--- /dev/null
+++ b/proxmox-ve-config/tests/nd/snapshots/nd__ra_level_optional_knobs_are_passed_through.snap
@@ -0,0 +1,13 @@
+---
+source: proxmox-ve-config/tests/nd/main.rs
+assertion_line: 145
+expression: "render(\"vrnet700\", iface)"
+---
+!
+interface vrnet700
+ no ipv6 nd suppress-ra
+ ipv6 nd ra-lifetime 0
+ ipv6 nd ra-interval 60
+ ipv6 nd mtu 1450
+ ipv6 nd prefix fd00:1::/64 2592000 604800
+exit
diff --git a/proxmox-ve-config/tests/nd/snapshots/nd__slaac_with_default_lifetimes.snap b/proxmox-ve-config/tests/nd/snapshots/nd__slaac_with_default_lifetimes.snap
new file mode 100644
index 0000000..52b2e5b
--- /dev/null
+++ b/proxmox-ve-config/tests/nd/snapshots/nd__slaac_with_default_lifetimes.snap
@@ -0,0 +1,9 @@
+---
+source: proxmox-ve-config/tests/nd/main.rs
+expression: "render(\"vrnet100\", iface)"
+---
+!
+interface vrnet100
+ no ipv6 nd suppress-ra
+ ipv6 nd prefix fd00:1::/64 2592000 604800
+exit
--
2.47.3
next prev parent reply other threads:[~2026-04-30 14:31 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-30 14:29 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v2 00/11] sdn: evpn: add IPv6 RA / SLAAC support Hannes Laimer
2026-04-30 14:29 ` [PATCH proxmox-ve-rs v2 01/11] frr: add IPv6 router advertisement support Hannes Laimer
2026-04-30 14:29 ` Hannes Laimer [this message]
2026-04-30 14:29 ` [PATCH proxmox-perl-rs v2 03/11] pve-rs: sdn: add IPv6 RA builder binding Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-network v2 04/11] sdn: evpn: add IPv6 RA / SLAAC support Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-network v2 05/11] sdn: evpn: derive IP version from CIDR for gateway-less subnets Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-network v2 06/11] sdn: evpn: accept untracked IPv6 NA on EVPN vnet bridges Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-network v2 07/11] api: vnet: include zone-type in vnet list Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-manager v2 08/11] ui: sdn: disable SNAT for IPv6 subnets Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-manager v2 09/11] ui: sdn: add IPv6 RA / SLAAC support Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-docs v2 10/11] sdn: document IPv6 RA / SLAAC configuration Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-docs v2 11/11] sdn: add example for IPv6 in an EVPN zone Hannes Laimer
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=20260430142953.315412-3-h.laimer@proxmox.com \
--to=h.laimer@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