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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.