From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 033451FF146 for ; Tue, 23 Jun 2026 14:58:15 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A0F3534EDE; Tue, 23 Jun 2026 14:57:40 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-ve-rs v3 3/9] ve-config: add per-vnet IPv6 RA configuration Date: Tue, 23 Jun 2026 14:56:20 +0200 Message-ID: <20260623125626.1195681-4-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260623125626.1195681-1-h.laimer@proxmox.com> References: <20260623125626.1195681-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: 1782219403659 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.086 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: D732UBSBL4SHJCNINVXQ3VATTK7RQBA5 X-Message-ID-Hash: D732UBSBL4SHJCNINVXQ3VATTK7RQBA5 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 --- 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 2f30cf2..67b0316 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::{ + nd::{Ipv6RaConfig, NdPrefixConfig}, + SdnNameError, SubnetName, VnetName, ZoneName, + }, }; #[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 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..c1b7434 --- /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 0000000..6d52ebd --- /dev/null +++ b/proxmox-ve-config/tests/nd/main.rs @@ -0,0 +1,164 @@ +#![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)); +} + +#[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 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..bf50e30 --- /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 0000000..b8767eb --- /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 0000000..57b12f8 --- /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 0000000..a15f2ad --- /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 0000000..f7d34b9 --- /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 0000000..44b72b2 --- /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 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