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 5D0F71FF146 for ; Tue, 23 Jun 2026 14:58:25 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0812D351A9; Tue, 23 Jun 2026 14:58:10 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network v3 5/9] sdn: evpn: add IPv6 RA / SLAAC support Date: Tue, 23 Jun 2026 14:56:22 +0200 Message-ID: <20260623125626.1195681-6-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: 1782219403785 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: V7AFWEBRAVJDNYDHACSU7KUMBLDSM2HQ X-Message-ID-Hash: V7AFWEBRAVJDNYDHACSU7KUMBLDSM2HQ 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: Configure IPv6 Router Advertisements on EVPN vnets so guests can autoconfigure addresses via SLAAC and pick up DNS / DHCP hints. The configuration is split between the VNet and the subnet, matching the protocol's per-RA / per-prefix layers. Defaults are picked so the typical SLAAC case needs no per-subnet configuration. The API rejects each option where it does not apply: the RA toggle outside EVPN zones, per-prefix overrides on IPv4 subnets, and explicitly enabling the autonomous flag on non-/64 prefixes. Signed-off-by: Hannes Laimer --- Notes: v3: - accept valid IPv6 subnets again, non-/64 and SNAT no longer rejected - reject autonomous only when explicitly set on a non-/64 - validate preferred not above valid, cap both at u32 max - name the offending field in validation errors - gate RA via a supports_ipv6_ra predicate, breaks the load cycle - reject a router lifetime below the RA interval - read subnets once from the running snapshot, no pending leak - eval guard RA generation, gate it on zone node membership - register ipv6-ra-rdnss in encode_value src/PVE/Network/SDN.pm | 1 + src/PVE/Network/SDN/Controllers.pm | 21 ++++- src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 22 ++++- src/PVE/Network/SDN/Controllers/Plugin.pm | 2 +- src/PVE/Network/SDN/SubnetPlugin.pm | 68 +++++++++++++++ src/PVE/Network/SDN/VnetPlugin.pm | 85 +++++++++++++++++++ src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 14 ++- src/PVE/Network/SDN/Zones/Plugin.pm | 6 ++ .../evpn/slaac/expected_controller_config | 53 ++++++++++++ .../zones/evpn/slaac/expected_sdn_interfaces | 43 ++++++++++ src/test/zones/evpn/slaac/interfaces | 7 ++ src/test/zones/evpn/slaac/sdn_config | 41 +++++++++ 12 files changed, 354 insertions(+), 9 deletions(-) create mode 100644 src/test/zones/evpn/slaac/expected_controller_config create mode 100644 src/test/zones/evpn/slaac/expected_sdn_interfaces create mode 100644 src/test/zones/evpn/slaac/interfaces create mode 100644 src/test/zones/evpn/slaac/sdn_config diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm index 33a3cf3..8f30fe7 100644 --- a/src/PVE/Network/SDN.pm +++ b/src/PVE/Network/SDN.pm @@ -516,6 +516,7 @@ sub encode_value { || $key eq 'allowed_ips' || $key eq 'secondary-controllers' || $key eq 'redistribute' + || $key eq 'ipv6-ra-rdnss' ) { if (ref($value) eq 'HASH') { return join(',', sort keys(%$value)); diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm index 4336b86..5a4cb93 100644 --- a/src/PVE/Network/SDN/Controllers.pm +++ b/src/PVE/Network/SDN/Controllers.pm @@ -8,6 +8,7 @@ use JSON; use PVE::Tools qw(extract_param dir_glob_regex run_command); use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Network::SDN::Subnets; use PVE::Network::SDN::Vnets; use PVE::Network::SDN::Zones; @@ -153,6 +154,18 @@ sub generate_frr_config { } } + # Bucket subnets by vnet once, from the same config snapshot the rest of the FRR + # config is generated from: the committed running config on apply, the compiled + # pending config for the dry-run diff. This keeps frr.conf consistent with the + # generated /etc/network/interfaces.d/sdn. + my $subnets_by_vnet = {}; + my $subnet_cfg = $sdn_config->{subnets} // { ids => {} }; + for my $subnetid (sort keys %{ $subnet_cfg->{ids} }) { + my $subnet = PVE::Network::SDN::Subnets::sdn_subnets_config($subnet_cfg, $subnetid); + next if !$subnet->{vnet}; + $subnets_by_vnet->{ $subnet->{vnet} }->{$subnetid} = $subnet; + } + foreach my $id (sort keys %{ $vnet_cfg->{ids} }) { my $plugin_config = $vnet_cfg->{ids}->{$id}; my $zoneid = $plugin_config->{zone}; @@ -169,7 +182,13 @@ sub generate_frr_config { my $controller_plugin = PVE::Network::SDN::Controllers::Plugin->lookup($controller->{type}); $controller_plugin->generate_vnet_frr_config( - $plugin_config, $controller, $zone, $zoneid, $id, $frr_config, + $plugin_config, + $controller, + $zone, + $zoneid, + $id, + $frr_config, + $subnets_by_vnet->{$id} // {}, ); if (!$zone->{'rt-import'}) { diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm index 4220cb6..b9dcb9f 100644 --- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm +++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm @@ -11,6 +11,7 @@ use PVE::RESTEnvironment qw(log_warn); use PVE::Network::SDN::Controllers::Plugin; use PVE::Network::SDN::Zones::Plugin; use PVE::Network::SDN::Fabrics; +use PVE::RS::SDN::IPv6RA; use Net::IP; use base('PVE::Network::SDN::Controllers::Plugin'); @@ -584,19 +585,34 @@ sub generate_zone_frr_config { } sub generate_vnet_frr_config { - my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_; + my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config, $subnets) = @_; + + $subnets //= {}; + + my $local_node = PVE::INotify::nodename(); + + # Only emit ND/RA config on nodes that are part of the zone, mirroring the node + # filtering of the bridge generation in the zones plugin. + my $is_zone_member = !defined($zone->{nodes}) || $zone->{nodes}->{$local_node}; + + if ($plugin_config->{'ipv6-ra'} && $is_zone_member) { + my $nd_iface = + eval { PVE::RS::SDN::IPv6RA::build_interface($vnetid, $plugin_config, $subnets); }; + if (my $err = $@) { + log_warn("vnet $vnetid: could not generate IPv6 RA configuration: $err"); + } + $config->{frr}->{nd_interfaces}->{$vnetid} = $nd_iface if $nd_iface; + } my $exitnodes = $zone->{'exitnodes'}; my $exitnodes_local_routing = $zone->{'exitnodes-local-routing'}; return if !$exitnodes_local_routing; - my $local_node = PVE::INotify::nodename(); my $is_gateway = $exitnodes->{$local_node}; return if !$is_gateway; - my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1); $config->{frr}->{ip_routes} //= []; foreach my $subnetid (sort keys %{$subnets}) { my $subnet = $subnets->{$subnetid}; diff --git a/src/PVE/Network/SDN/Controllers/Plugin.pm b/src/PVE/Network/SDN/Controllers/Plugin.pm index 6da3eec..45870ee 100644 --- a/src/PVE/Network/SDN/Controllers/Plugin.pm +++ b/src/PVE/Network/SDN/Controllers/Plugin.pm @@ -93,7 +93,7 @@ sub generate_zone_frr_config { } sub generate_vnet_frr_config { - my ($class, $plugin_config, $controller, $zoneid, $vnetid, $config) = @_; + my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config, $subnets) = @_; } diff --git a/src/PVE/Network/SDN/SubnetPlugin.pm b/src/PVE/Network/SDN/SubnetPlugin.pm index e2a0e50..078fed7 100644 --- a/src/PVE/Network/SDN/SubnetPlugin.pm +++ b/src/PVE/Network/SDN/SubnetPlugin.pm @@ -177,6 +177,39 @@ sub properties { description => 'IP address for the DNS server', optional => 1, }, + # Per-prefix Router Advertisement overrides. Only meaningful for IPv6 subnets + # whose vnet has `ipv6-ra` enabled, silently ignored otherwise. + 'nd-prefix-autonomous' => { + type => 'boolean', + description => + "Set the autonomous (A) flag for this prefix in RAs, enabling SLAAC for" + . " hosts on this subnet. SLAAC requires a /64 prefix, so this defaults to" + . " enabled on /64 subnets and disabled (and not enablable) otherwise.", + optional => 1, + }, + 'nd-prefix-on-link' => { + type => 'boolean', + description => + "Set the on-link (L) flag for this prefix in RAs (default: on-link).", + optional => 1, + default => 1, + }, + 'nd-prefix-valid-lifetime' => { + type => 'integer', + description => "Valid lifetime for this prefix in RAs, in seconds.", + minimum => 0, + maximum => 4294967295, + default => 2592000, + optional => 1, + }, + 'nd-prefix-preferred-lifetime' => { + type => 'integer', + description => "Preferred lifetime for this prefix in RAs, in seconds.", + minimum => 0, + maximum => 4294967295, + default => 604800, + optional => 1, + }, }; } @@ -189,6 +222,10 @@ sub options { dnszoneprefix => { optional => 1 }, 'dhcp-range' => { optional => 1 }, 'dhcp-dns-server' => { optional => 1 }, + 'nd-prefix-autonomous' => { optional => 1 }, + 'nd-prefix-on-link' => { optional => 1 }, + 'nd-prefix-valid-lifetime' => { optional => 1 }, + 'nd-prefix-preferred-lifetime' => { optional => 1 }, }; } @@ -206,6 +243,37 @@ sub on_update_hook { my $dns = $zone->{dns}; my $dnszone = $zone->{dnszone}; my $reversedns = $zone->{reversedns}; + my $is_ipv6 = PVE::JSONSchema::pve_verify_cidrv6($cidr, 1); + + # Per-prefix RA overrides only apply on IPv6 subnets. Reject explicit overrides on + # IPv4 subnets so the user notices their config has no effect. + if (!$is_ipv6) { + for my $opt ( + qw(nd-prefix-autonomous nd-prefix-on-link nd-prefix-valid-lifetime nd-prefix-preferred-lifetime) + ) { + raise_param_exc({ $opt => "only valid on IPv6 subnets" }) + if defined($subnet->{$opt}); + } + } + + # SLAAC requires a /64 prefix (RFC 4862), so the autonomous flag defaults to enabled + # only on /64 subnets. Explicitly enabling it elsewhere can never work, reject it. + if ($is_ipv6 && $mask != 64 && $subnet->{'nd-prefix-autonomous'}) { + raise_param_exc( + { 'nd-prefix-autonomous' => "autonomous (SLAAC) flag requires a /64 prefix" }); + } + + # The preferred lifetime must not exceed the valid lifetime (RFC 4861), FRR rejects + # such prefix lines at config load. Mixed explicit/default cases are clamped when the + # FRR config is built. + my $nd_valid = $subnet->{'nd-prefix-valid-lifetime'}; + my $nd_preferred = $subnet->{'nd-prefix-preferred-lifetime'}; + if (defined($nd_valid) && defined($nd_preferred) && $nd_preferred > $nd_valid) { + raise_param_exc({ + 'nd-prefix-preferred-lifetime' => + "preferred lifetime must not be greater than the valid lifetime ($nd_valid)", + }); + } my $mac = undef; diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm index e041575..2479d7c 100644 --- a/src/PVE/Network/SDN/VnetPlugin.pm +++ b/src/PVE/Network/SDN/VnetPlugin.pm @@ -83,6 +83,57 @@ sub properties { "If true, sets the isolated property for all interfaces on the bridge of this VNet.", optional => 1, }, + # IPv6 Router Advertisement settings (EVPN zones only). The master toggle gates + # the rest. Per-prefix flags live on the subnet (nd-prefix-*). + 'ipv6-ra' => { + type => 'boolean', + description => + "Emit IPv6 Router Advertisements on this VNet's bridge. Requires the VNet to belong" + . " to an EVPN zone with at least one IPv6 subnet.", + optional => 1, + }, + 'ipv6-ra-managed' => { + type => 'boolean', + description => "Set the managed-address (M) flag in emitted RAs.", + optional => 1, + }, + 'ipv6-ra-other' => { + type => 'boolean', + description => "Set the other-configuration (O) flag in emitted RAs.", + optional => 1, + }, + 'ipv6-ra-rdnss' => { + type => 'array', + description => "RDNSS (Recursive DNS Server) addresses advertised in RAs.", + optional => 1, + items => { + type => 'string', + format => 'ipv6', + }, + }, + 'ipv6-ra-router-lifetime' => { + type => 'integer', + description => + "Router lifetime advertised in RAs (seconds). 0 tells hosts not to use this" + . " router as a default gateway.", + optional => 1, + minimum => 0, + maximum => 9000, + }, + 'ipv6-ra-interval' => { + type => 'integer', + description => "Maximum interval between unsolicited RAs (seconds).", + optional => 1, + minimum => 4, + maximum => 1800, + }, + 'ipv6-ra-mtu' => { + type => 'integer', + description => "MTU advertised in RAs.", + optional => 1, + minimum => 1280, + maximum => 65535, + }, }; } @@ -93,6 +144,13 @@ sub options { alias => { optional => 1 }, vlanaware => { optional => 1 }, 'isolate-ports' => { optional => 1 }, + 'ipv6-ra' => { optional => 1 }, + 'ipv6-ra-managed' => { optional => 1 }, + 'ipv6-ra-other' => { optional => 1 }, + 'ipv6-ra-rdnss' => { optional => 1 }, + 'ipv6-ra-router-lifetime' => { optional => 1 }, + 'ipv6-ra-interval' => { optional => 1 }, + 'ipv6-ra-mtu' => { optional => 1 }, }; } @@ -117,6 +175,33 @@ sub on_update_hook { raise_param_exc({ vlanaware => "vlanaware vnet is not compatible with subnets" }) if $subnets; } + + # RA settings are only consumed by zones whose plugin emits RAs (currently EVPN). + # Reject them elsewhere so the user notices their config has no effect. Loaded at + # runtime to avoid a compile-time module cycle (Zones -> Vnets -> VnetPlugin). + if (my @ra_opts = sort grep { /^ipv6-ra/ } keys %$vnet) { + require PVE::Network::SDN::Zones; + require PVE::Network::SDN::Zones::Plugin; + + my $zone_cfg = PVE::Network::SDN::Zones::config(); + my $zone = $zone_cfg->{ids}->{ $vnet->{zone} }; + my $zone_plugin = + $zone ? eval { PVE::Network::SDN::Zones::Plugin->lookup($zone->{type}) } : undef; + + raise_param_exc({ $ra_opts[0] => "IPv6 RA options are only supported on EVPN zones" }) + if !$zone_plugin || !$zone_plugin->supports_ipv6_ra(); + } + + # RFC 4861: a non-zero router lifetime must be at least the maximum RA interval + # (FRR default: 600s), FRR rejects the lifetime at config load otherwise. + my $ra_lifetime = $vnet->{'ipv6-ra-router-lifetime'}; + my $ra_interval = $vnet->{'ipv6-ra-interval'} // 600; + if (defined($ra_lifetime) && $ra_lifetime != 0 && $ra_lifetime < $ra_interval) { + raise_param_exc({ + 'ipv6-ra-router-lifetime' => + "router lifetime must be 0 or at least the RA interval ($ra_interval seconds)", + }); + } } 1; diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm index dfbd7e9..d8bfa80 100644 --- a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm +++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm @@ -266,12 +266,14 @@ sub generate_sdn_config { $enable_forward_v4 = 1 if $gateway; } - if ($subnet->{snat}) { + if ($subnet->{snat} && $is_evpn_gateway) { - #find outgoing interface + #find outgoing interface, but don't fail the whole vnet on a node that + #cannot route the check address (e.g. no IPv6 connectivity) my ($outip, $outiface) = - PVE::Network::SDN::Zones::Plugin::get_local_route_ip($checkrouteip); - if ($outip && $outiface && $is_evpn_gateway) { + eval { PVE::Network::SDN::Zones::Plugin::get_local_route_ip($checkrouteip) }; + warn "vnet $vnetid: not generating SNAT rules for $cidr: $@" if $@; + if ($outip && $outiface) { #use snat, faster than masquerade push @iface_config, "post-up $iptables -t nat -A POSTROUTING -s '$cidr' -o $outiface -j SNAT --to-source $outip"; @@ -431,5 +433,9 @@ sub vnet_update_hook { } } +sub supports_ipv6_ra { + return 1; +} + 1; diff --git a/src/PVE/Network/SDN/Zones/Plugin.pm b/src/PVE/Network/SDN/Zones/Plugin.pm index 74a3384..a0adfe9 100644 --- a/src/PVE/Network/SDN/Zones/Plugin.pm +++ b/src/PVE/Network/SDN/Zones/Plugin.pm @@ -163,6 +163,12 @@ sub vnet_update_hook { # do nothing by default } +# Whether vnets in this zone type can emit IPv6 Router Advertisements (ipv6-ra* vnet +# options). Zone plugins that generate RA configuration override this. +sub supports_ipv6_ra { + return 0; +} + #helpers sub parse_tag_number_or_range { my ($str, $max, $tag) = @_; diff --git a/src/test/zones/evpn/slaac/expected_controller_config b/src/test/zones/evpn/slaac/expected_controller_config new file mode 100644 index 0000000..7d59591 --- /dev/null +++ b/src/test/zones/evpn/slaac/expected_controller_config @@ -0,0 +1,53 @@ +frr version 10.6.1 +frr defaults datacenter +hostname localhost +log syslog informational +service integrated-vtysh-config +! +vrf vrf_myzone + vni 1000 +exit-vrf +! +router bgp 65000 + bgp router-id 192.168.0.1 + no bgp hard-administrative-reset + no bgp default ipv4-unicast + coalesce-time 1000 + no bgp graceful-restart notification + neighbor VTEP peer-group + neighbor VTEP remote-as 65000 + neighbor VTEP bfd + neighbor 192.168.0.2 peer-group VTEP + neighbor 192.168.0.3 peer-group VTEP + ! + address-family l2vpn evpn + neighbor VTEP activate + neighbor VTEP route-map MAP_VTEP_IN in + neighbor VTEP route-map MAP_VTEP_OUT out + advertise-all-vni + exit-address-family +exit +! +router bgp 65000 vrf vrf_myzone + bgp router-id 192.168.0.1 + no bgp hard-administrative-reset + no bgp graceful-restart notification +exit +! +route-map MAP_VTEP_IN permit 1 +exit +! +route-map MAP_VTEP_OUT permit 1 +exit +! +interface myvnet + no ipv6 nd suppress-ra + ipv6 nd managed-config-flag + ipv6 nd other-config-flag + ipv6 nd rdnss 2a08:2142:302:3::53 + ipv6 nd prefix 2a08:2142:302:3::/64 7200 3600 + ipv6 nd prefix fd00:1::/64 2592000 604800 no-autoconfig +exit +! +line vty +! diff --git a/src/test/zones/evpn/slaac/expected_sdn_interfaces b/src/test/zones/evpn/slaac/expected_sdn_interfaces new file mode 100644 index 0000000..2e479a3 --- /dev/null +++ b/src/test/zones/evpn/slaac/expected_sdn_interfaces @@ -0,0 +1,43 @@ +#version:1 + +auto myvnet +iface myvnet + address 2a08:2142:302:3::1/64 + address fd00:1::1/64 + hwaddress A2:1D:CB:1A:C0:8B + bridge_ports vxlan_myvnet + bridge_stp off + bridge_fd 0 + mtu 1450 + ip6-forward on + arp-accept on + vrf vrf_myzone + +auto vrf_myzone +iface vrf_myzone + vrf-table auto + post-up ip route add vrf vrf_myzone unreachable default metric 4278198272 + +auto vrfbr_myzone +iface vrfbr_myzone + bridge-ports vrfvx_myzone + bridge_stp off + bridge_fd 0 + mtu 1450 + vrf vrf_myzone + +auto vrfvx_myzone +iface vrfvx_myzone + vxlan-id 1000 + vxlan-local-tunnelip 192.168.0.1 + bridge-learning off + bridge-arp-nd-suppress on + mtu 1450 + +auto vxlan_myvnet +iface vxlan_myvnet + vxlan-id 100 + vxlan-local-tunnelip 192.168.0.1 + bridge-learning off + bridge-arp-nd-suppress on + mtu 1450 diff --git a/src/test/zones/evpn/slaac/interfaces b/src/test/zones/evpn/slaac/interfaces new file mode 100644 index 0000000..66bb826 --- /dev/null +++ b/src/test/zones/evpn/slaac/interfaces @@ -0,0 +1,7 @@ +auto vmbr0 +iface vmbr0 inet static + address 192.168.0.1/24 + gateway 192.168.0.254 + bridge-ports eth0 + bridge-stp off + bridge-fd 0 diff --git a/src/test/zones/evpn/slaac/sdn_config b/src/test/zones/evpn/slaac/sdn_config new file mode 100644 index 0000000..e6294e3 --- /dev/null +++ b/src/test/zones/evpn/slaac/sdn_config @@ -0,0 +1,41 @@ +{ + version => 1, + vnets => { + ids => { + myvnet => { + tag => "100", + type => "vnet", + zone => "myzone", + 'ipv6-ra' => 1, + 'ipv6-ra-managed' => 1, + 'ipv6-ra-other' => 1, + 'ipv6-ra-rdnss' => ['2a08:2142:302:3::53'], + }, + }, + }, + + zones => { + ids => { myzone => { ipam => "pve", type => "evpn", controller => "evpnctl", 'vrf-vxlan' => 1000, 'mac' => 'A2:1D:CB:1A:C0:8B' } }, + }, + controllers => { + ids => { evpnctl => { type => "evpn", 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3', asn => "65000" } }, + }, + + subnets => { + ids => { + 'myzone-2a08:2142:302:3::-64' => { + 'type' => 'subnet', + 'vnet' => 'myvnet', + 'gateway' => '2a08:2142:302:3::1', + 'nd-prefix-valid-lifetime' => 7200, + 'nd-prefix-preferred-lifetime' => 3600, + }, + 'myzone-fd00:1::-64' => { + 'type' => 'subnet', + 'vnet' => 'myvnet', + 'gateway' => 'fd00:1::1', + 'nd-prefix-autonomous' => 0, + }, + } + } +} -- 2.47.3