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 1E07E1FF13C for ; Thu, 30 Apr 2026 16:31:45 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 2970DCBBE; Thu, 30 Apr 2026 16:31:01 +0200 (CEST) From: Hannes Laimer 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 Message-ID: <20260430142953.315412-3-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260430142953.315412-1-h.laimer@proxmox.com> References: <20260430142953.315412-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1777559296714 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.080 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: S3PO3GVFF7HRN7LTCZNXXBHRM4NXBWX3 X-Message-ID-Hash: S3PO3GVFF7HRN7LTCZNXXBHRM4NXBWX3 X-MailFrom: h.laimer@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: 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 --- 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, #[serde(rename = "dhcp-range")] dhcp_range: Option>>, + + // 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, + #[serde(rename = "nd-prefix-on-link")] + nd_prefix_on_link: Option, + #[serde( + default, + rename = "nd-prefix-valid-lifetime", + deserialize_with = "proxmox_serde::perl::deserialize_u32" + )] + nd_prefix_valid_lifetime: Option, + #[serde( + default, + rename = "nd-prefix-preferred-lifetime", + deserialize_with = "proxmox_serde::perl::deserialize_u32" + )] + nd_prefix_preferred_lifetime: Option, } /// 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, 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, + #[serde(rename = "ipv6-ra-managed")] + ipv6_ra_managed: Option, + #[serde(rename = "ipv6-ra-other")] + ipv6_ra_other: Option, + #[serde(rename = "ipv6-ra-rdnss")] + ipv6_ra_rdnss: Option>, + #[serde( + default, + rename = "ipv6-ra-router-lifetime", + deserialize_with = "proxmox_serde::perl::deserialize_u32" + )] + ipv6_ra_router_lifetime: Option, + #[serde( + default, + rename = "ipv6-ra-interval", + deserialize_with = "proxmox_serde::perl::deserialize_u32" + )] + ipv6_ra_interval: Option, + #[serde( + default, + rename = "ipv6-ra-mtu", + deserialize_with = "proxmox_serde::perl::deserialize_u32" + )] + ipv6_ra_mtu: Option, +} + +impl VnetRunningConfig { + /// Materialize the IPv6 RA settings if enabled on this vnet, otherwise return `None`. + pub fn ipv6_ra_config(&self) -> Option { + 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, snat: bool, dhcp_range: Vec, + 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 + '_ { 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, subnets: BTreeMap, + ipv6_ra: Option, } 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 { &self.tag } + + /// Per-vnet IPv6 RA settings. `None` means RAs are not emitted on this vnet's bridge. + pub fn ipv6_ra(&self) -> &Option { + &self.ipv6_ra + } + + pub fn set_ipv6_ra(&mut self, ipv6_ra: Option) { + self.ipv6_ra = ipv6_ra; + } } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -635,10 +743,10 @@ impl TryFrom 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, + pub router_lifetime: Option, + pub interval: Option, + pub mtu: Option, +} + +/// 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, + pub preferred_lifetime: Option, +} + +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, + ) -> Option { + 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) -> 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