From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 0654C1FF0BC for ; Thu, 29 Oct 2026 16:42:47 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 6B2962165B; Thu, 29 Oct 2026 16:42:25 +0100 (CET) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-ve-rs v5 3/9] ve-config: sdn: add IPv6 router advertisement settings Date: Thu, 29 Oct 2026 16:41:42 +0100 Message-ID: <20261029154148.2882594-4-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261029154148.2882594-1-h.laimer@proxmox.com> References: <20261029154148.2882594-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: 1793288518406 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.368 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust 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: HQ2OR3V52OAUEWFCVZHRVPIL2Q4UTKCY X-Message-ID-Hash: HQ2OR3V52OAUEWFCVZHRVPIL2Q4UTKCY 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: Guests can only configure IPv6 addresses with SLAAC when their gateway sends router advertisements. Parsing the running config now picks up the RA settings of a vnet and the prefix settings of its subnets. The FRR ND config of a vnet's interface is built from the vnet and its subnets, with one prefix per IPv6 subnet. A vnet without RAs enabled or without an IPv6 subnet gets none. An RA goes out on the vnet's interface and carries one prefix option per subnet. So the vnet holds the RA settings and each subnet those of its prefix. Prefixes are on-link by default. They are autonomous by default only as a /64, since SLAAC needs one. Signed-off-by: Hannes Laimer --- Notes: v3: - clamp the default preferred lifetime to a shorter valid lifetime - boolean fields use Option with deserialize_bool - autonomous defaults by mask, on for /64 - add a non-/64 default test proxmox-ve-config/src/sdn/config.rs | 146 +++++++++++++++- proxmox-ve-config/src/sdn/mod.rs | 1 + proxmox-ve-config/src/sdn/nd.rs | 131 ++++++++++++++ proxmox-ve-config/tests/nd/main.rs | 164 ++++++++++++++++++ .../nd__explicit_lifetimes_are_preserved.snap | 9 + .../nd__mixed_subnets_under_one_vnet.snap | 14 ++ .../snapshots/nd__no_autoconfig_prefix.snap | 9 + ...sh64_prefix_defaults_to_no_autoconfig.snap | 9 + .../nd__off_link_emits_off_link_modifier.snap | 9 + ...__preferred_lifetime_clamped_to_valid.snap | 9 + ...vel_optional_knobs_are_passed_through.snap | 12 ++ .../nd__slaac_with_default_lifetimes.snap | 9 + 12 files changed, 515 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__non_slash64_prefix_defaults_to_no_autoconfig.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__preferred_lifetime_clamped_to_valid.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 2f30cf2bd77c..83a5fc42051c 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, ipset::{IpsetEntry, IpsetName, IpsetScope}, }, - sdn::{SdnNameError, SubnetName, VnetName, ZoneName}, + sdn::{ + SdnNameError, SubnetName, VnetName, ZoneName, + nd::{Ipv6RaConfig, NdPrefixConfig}, + }, }; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -185,6 +188,33 @@ 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( + default, + rename = "nd-prefix-autonomous", + deserialize_with = "proxmox_serde::perl::deserialize_bool" + )] + nd_prefix_autonomous: Option, + #[serde( + default, + rename = "nd-prefix-on-link", + deserialize_with = "proxmox_serde::perl::deserialize_bool" + )] + 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 +226,67 @@ 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( + default, + rename = "ipv6-ra", + deserialize_with = "proxmox_serde::perl::deserialize_bool" + )] + ipv6_ra: Option, + #[serde( + default, + rename = "ipv6-ra-managed", + deserialize_with = "proxmox_serde::perl::deserialize_bool" + )] + ipv6_ra_managed: Option, + #[serde( + default, + rename = "ipv6-ra-other", + deserialize_with = "proxmox_serde::perl::deserialize_bool" + )] + 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(false) { + return None; + } + Some(Ipv6RaConfig { + managed: self.ipv6_ra_managed.unwrap_or(false), + other: self.ipv6_ra_other.unwrap_or(false), + 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 +312,7 @@ pub struct SubnetConfig { gateway: Option, snat: bool, dhcp_range: Vec, + nd_prefix: NdPrefixConfig, } impl SubnetConfig { @@ -242,11 +332,19 @@ impl SubnetConfig { } } + // SLAAC requires a /64 prefix, so the autonomous flag only defaults to set there. + // The subnet schema rejects explicitly enabling it on other prefix lengths. + let nd_prefix = NdPrefixConfig { + autonomous: matches!(name.cidr(), Cidr::Ipv6(cidr) if cidr.mask() == 64), + ..NdPrefixConfig::default() + }; + Ok(Self { name, gateway, snat, dhcp_range: dhcp_range.into_iter().collect(), + nd_prefix, }) } @@ -269,7 +367,19 @@ impl SubnetConfig { None => Vec::new(), }; - Self::new(name, running_config.gateway, snat, dhcp_range) + let mut config = Self::new(name, running_config.gateway, snat, dhcp_range)?; + let nd_default = config.nd_prefix().clone(); + config.set_nd_prefix(NdPrefixConfig { + autonomous: running_config + .nd_prefix_autonomous + .unwrap_or(nd_default.autonomous), + on_link: running_config + .nd_prefix_on_link + .unwrap_or(nd_default.on_link), + valid_lifetime: running_config.nd_prefix_valid_lifetime, + preferred_lifetime: running_config.nd_prefix_preferred_lifetime, + }); + Ok(config) } pub fn name(&self) -> &SubnetName { @@ -291,6 +401,17 @@ impl SubnetConfig { pub fn dhcp_ranges(&self) -> impl Iterator + '_ { self.dhcp_range.iter() } + + /// Per-prefix Router Advertisement overrides. If no explicit overrides were + /// configured, this defaults to "include the prefix in the RA with the on-link flag + /// set, and the autonomous flag set iff the prefix is a /64" (SLAAC requires /64). + 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 +419,7 @@ pub struct VnetConfig { name: VnetName, tag: Option, subnets: BTreeMap, + ipv6_ra: Option, } impl VnetConfig { @@ -306,6 +428,7 @@ impl VnetConfig { name, subnets: BTreeMap::default(), tag, + ipv6_ra: None, } } @@ -360,6 +483,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 +767,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 2133396145bd..457a9ec3af5f 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 000000000000..c1b743428979 --- /dev/null +++ b/proxmox-ve-config/src/sdn/nd.rs @@ -0,0 +1,131 @@ +//! 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. +/// +/// [`Default`] matches the typical SLAAC use case: autonomous and on-link flags set, +/// FRR's default lifetimes. Note that +/// [`SubnetConfig::new`](crate::sdn::config::SubnetConfig::new) only applies the +/// autonomous default to /64 prefixes, since SLAAC requires a /64. +#[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}; + 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 valid = nd.valid_lifetime.unwrap_or(DEFAULT_PREFIX_VALID_LIFETIME); + // RFC 4861 requires preferred <= valid (FRR rejects the prefix otherwise), so + // clamp the default when only a shorter valid lifetime is configured. Explicit + // invalid combinations are already rejected by the subnet schema hook. + let preferred = nd + .preferred_lifetime + .unwrap_or(DEFAULT_PREFIX_PREFERRED_LIFETIME) + .min(valid); + + prefixes.push(NdPrefix { + cidr, + autonomous: nd.autonomous, + on_link: nd.on_link, + valid, + preferred, + }); + } + + 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 000000000000..b875e10a9b3b --- /dev/null +++ b/proxmox-ve-config/tests/nd/main.rs @@ -0,0 +1,164 @@ +#![cfg(feature = "frr")] + +use proxmox_frr::ser::{FrrConfig, InterfaceName, serializer::dump}; +use proxmox_network_types::ip_address::Cidr; +use proxmox_ve_config::sdn::{ + SubnetName, VnetName, ZoneName, + config::{SubnetConfig, VnetConfig}, + nd::{Ipv6RaConfig, NdPrefixConfig, frr::build_nd_interface}, +}; + +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)); +} + +#[test] +fn preferred_lifetime_clamped_to_valid() { + let v = vnet("vrnet800", Some(Ipv6RaConfig::default())); + let s = [subnet( + "zone", + "fd00:1::/64", + NdPrefixConfig { + valid_lifetime: Some(3600), + ..Default::default() + }, + )]; + let iface = build_nd_interface(&v, &s).expect("ra enabled"); + insta::assert_snapshot!(render("vrnet800", iface)); +} + +#[test] +fn non_slash64_prefix_defaults_to_no_autoconfig() { + let v = vnet("vrnet900", Some(Ipv6RaConfig::default())); + let cidr: Cidr = "fd00:1::/96".parse().unwrap(); + let zone = ZoneName::new("zone".to_owned()).unwrap(); + let name = SubnetName::new(zone, cidr); + // no explicit NdPrefixConfig: the constructor default applies (autonomous iff /64) + let s = [SubnetConfig::new(name, None, false, std::iter::empty()).unwrap()]; + let iface = build_nd_interface(&v, &s).expect("ra enabled"); + insta::assert_snapshot!(render("vrnet900", 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 000000000000..8abcd0760bae --- /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 000000000000..bf50e301f353 --- /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 2592000 604800 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 000000000000..b8767ebfd671 --- /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 2592000 604800 no-autoconfig +exit diff --git a/proxmox-ve-config/tests/nd/snapshots/nd__non_slash64_prefix_defaults_to_no_autoconfig.snap b/proxmox-ve-config/tests/nd/snapshots/nd__non_slash64_prefix_defaults_to_no_autoconfig.snap new file mode 100644 index 000000000000..57b12f88311d --- /dev/null +++ b/proxmox-ve-config/tests/nd/snapshots/nd__non_slash64_prefix_defaults_to_no_autoconfig.snap @@ -0,0 +1,9 @@ +--- +source: proxmox-ve-config/tests/nd/main.rs +expression: "render(\"vrnet900\", iface)" +--- +! +interface vrnet900 + no ipv6 nd suppress-ra + ipv6 nd prefix fd00:1::/96 2592000 604800 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 000000000000..a15f2adb1f94 --- /dev/null +++ b/proxmox-ve-config/tests/nd/snapshots/nd__off_link_emits_off_link_modifier.snap @@ -0,0 +1,9 @@ +--- +source: proxmox-ve-config/tests/nd/main.rs +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__preferred_lifetime_clamped_to_valid.snap b/proxmox-ve-config/tests/nd/snapshots/nd__preferred_lifetime_clamped_to_valid.snap new file mode 100644 index 000000000000..f7d34b9457ce --- /dev/null +++ b/proxmox-ve-config/tests/nd/snapshots/nd__preferred_lifetime_clamped_to_valid.snap @@ -0,0 +1,9 @@ +--- +source: proxmox-ve-config/tests/nd/main.rs +expression: "render(\"vrnet800\", iface)" +--- +! +interface vrnet800 + no ipv6 nd suppress-ra + ipv6 nd prefix fd00:1::/64 3600 3600 +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 000000000000..44b72b283944 --- /dev/null +++ b/proxmox-ve-config/tests/nd/snapshots/nd__ra_level_optional_knobs_are_passed_through.snap @@ -0,0 +1,12 @@ +--- +source: proxmox-ve-config/tests/nd/main.rs +expression: "render(\"vrnet700\", iface)" +--- +! +interface vrnet700 + no ipv6 nd suppress-ra + ipv6 nd ra-interval 60 + ipv6 nd ra-lifetime 0 + 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 000000000000..52b2e5b5fb46 --- /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