* [PATCH installer 0/8] add IPv6 SLAAC and v6-only support
@ 2026-05-08 18:44 Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 1/8] install: drop trivial fromjs() wrapper and use JSON::from_json() Christoph Heiss
` (7 more replies)
0 siblings, 8 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
Most glaringly, neither the GUI nor TUI handled IPv6-only setups
correctly, in that the pre-filled values for the networking setup simply
defaulted to the IPv4 defaults.
Patch #1-#4 rework the GUI side of things.
Patch #5 enables support for /127 and /128 IPv6 CIDRs, which we do
support elsewhere.
Finally, patch #8 enables sending router solicitations when the
installer starts, such than any IPv6 (SLAAC) configuration is retrieved.
Patches #5-#8 are independent and can be applied individually as such,
if desired.
Christoph Heiss (8):
install: drop trivial fromjs() wrapper and use JSON::from_json()
install: move network subroutines to Proxmox::Sys::Net
gui: use run_env->{network} instead of old run_env->{ipconf}
sys: net: drop the now-unused `ipconf` runtime environment
configuration
sys: net: allow up to /128 netmask for IPv6
sys: net: ignore ipv6 nameservers with zone identifiers
common: options: rework network address setup to handle ipv6-only
unconfigured: try to retrieve IPv6 SLAAC addresses on startup
Proxmox/Install/RunEnv.pm | 172 +----------------
Proxmox/Sys/Net.pm | 238 +++++++++++++++++-------
debian/control | 2 +
proxinstall | 66 ++++---
proxmox-installer-common/src/options.rs | 89 +++++----
unconfigured.sh | 24 +++
6 files changed, 288 insertions(+), 303 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH installer 1/8] install: drop trivial fromjs() wrapper and use JSON::from_json()
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
@ 2026-05-08 18:44 ` Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 2/8] install: move network subroutines to Proxmox::Sys::Net Christoph Heiss
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
.. in preparation for moving some subroutines.
No functional changes.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install/RunEnv.pm | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/Proxmox/Install/RunEnv.pm b/Proxmox/Install/RunEnv.pm
index a53fa8e..40041ad 100644
--- a/Proxmox/Install/RunEnv.pm
+++ b/Proxmox/Install/RunEnv.pm
@@ -14,10 +14,6 @@ use Proxmox::Sys::Net;
use Proxmox::Install::ISOEnv;
-my sub fromjs : prototype($) {
- return from_json($_[0], { utf8 => 1 });
-}
-
my $_cached_arch = undef;
sub query_arch : prototype() {
@@ -106,7 +102,7 @@ my sub query_netdevs : prototype() {
my $default;
# FIXME: not the same as the battle proven way we used in the installer for years?
- my $interfaces = fromjs(qx/ip --details --json address show/);
+ my $interfaces = from_json(qx/ip --details --json address show/, { utf8 => 1 });
my $pinned_counter = 0;
for my $if (@$interfaces) {
@@ -178,7 +174,7 @@ my sub query_routes : prototype() {
my ($gateway4, $gateway6);
log_info("query routes");
- my $route4 = fromjs(qx/ip -4 --json route show/);
+ my $route4 = from_json(qx/ip -4 --json route show/, { utf8 => 1 });
for my $route (@$route4) {
if ($route->{dst} eq 'default') {
$gateway4 = {
@@ -189,7 +185,7 @@ my sub query_routes : prototype() {
}
}
- my $route6 = fromjs(qx/ip -6 --json route show/);
+ my $route6 = from_json(qx/ip -6 --json route show/, { utf8 => 1 });
for my $route (@$route6) {
if ($route->{dst} eq 'default') {
$gateway6 = {
@@ -310,7 +306,7 @@ sub query_installation_environment : prototype() {
log_info("re-using cached runtime env from $run_env_file");
my $cached_env = eval {
my $run_env_raw = Proxmox::Sys::File::file_read_all($run_env_file);
- return fromjs($run_env_raw); # returns from eval
+ return from_json($run_env_raw, { utf8 => 1 }); # returns from eval
};
log_error("failed to parse cached runtime env - $@") if $@;
return $cached_env if defined($cached_env) && scalar keys $cached_env->%*;
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH installer 2/8] install: move network subroutines to Proxmox::Sys::Net
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 1/8] install: drop trivial fromjs() wrapper and use JSON::from_json() Christoph Heiss
@ 2026-05-08 18:44 ` Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 3/8] gui: use run_env->{network} instead of old run_env->{ipconf} Christoph Heiss
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
As they are dealing with network, they make more sense over there.
No functional changes.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install/RunEnv.pm | 160 ++------------------------------------
Proxmox/Sys/Net.pm | 152 +++++++++++++++++++++++++++++++++++-
2 files changed, 157 insertions(+), 155 deletions(-)
diff --git a/Proxmox/Install/RunEnv.pm b/Proxmox/Install/RunEnv.pm
index 40041ad..b83b9ff 100644
--- a/Proxmox/Install/RunEnv.pm
+++ b/Proxmox/Install/RunEnv.pm
@@ -80,154 +80,6 @@ sub query_cpu_info : prototype() {
return $cpu_info;
}
-# Returns a hash.
-#
-# {
-# <ifname> => {
-# mac => <mac address>,
-# index => <index>,
-# name => <ifname>,
-# state => <UP|DOWN|LOWERLAYERDOWN|DORMANT|TESTING|NOTPRESENT|UNKNOWN>,
-# pinned_id => <sequential numerical ID>,
-# driver => <driver name>,
-# addresses => [
-# family => <inet|inet6>,
-# address => <mac address>,
-# prefix => <length>,
-# ],
-# },
-# }
-my sub query_netdevs : prototype() {
- my $ifs = {};
- my $default;
-
- # FIXME: not the same as the battle proven way we used in the installer for years?
- my $interfaces = from_json(qx/ip --details --json address show/, { utf8 => 1 });
-
- my $pinned_counter = 0;
- for my $if (@$interfaces) {
- my ($index, $name, $state, $mac, $addresses) =
- $if->@{qw(ifindex ifname operstate address addr_info)};
-
- next if !$name || $name eq 'lo'; # could also check flags for LOOPBACK..
- if (!$mac) {
- log_info("skipped interface $name, no mac address detected");
- next;
- }
-
- my @valid_addrs;
- if (uc($state) eq 'UP') {
- for my $addr (@$addresses) {
- next if $addr->{scope} eq 'link';
-
- my ($family, $addr, $prefix) = $addr->@{qw(family local prefixlen)};
-
- push @valid_addrs,
- {
- family => $family,
- address => $addr,
- prefix => $prefix,
- };
- }
- }
-
- my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
- $driver =~ s!^.*/!!;
-
- $ifs->{$name} = {
- index => $index,
- name => $name,
- mac => $mac,
- state => uc($state),
- driver => $driver,
- };
- $ifs->{$name}->{addresses} = \@valid_addrs if @valid_addrs;
-
- # only set the `pinned_id` property if the interface actually can be pinned,
- # i.e. is a physical link
- my $is_pinnable =
- Proxmox::Sys::Net::ip_link_is_physical($if) && !Proxmox::Sys::Net::iface_is_vf($name);
- if ($is_pinnable) {
- $ifs->{$name}->{pinned_id} = "${pinned_counter}";
- $pinned_counter++;
- }
- }
-
- return $ifs;
-}
-
-# Returns a hash.
-#
-# {
-# gateway4 => {
-# dst => "default",
-# gateway => <ipv4>,
-# dev => <ifname>,
-# },
-# gateway6 => {
-# dst => "default",
-# gateway => <ipv6>,
-# dev => <ifname>,
-# },
-# }
-my sub query_routes : prototype() {
- my ($gateway4, $gateway6);
-
- log_info("query routes");
- my $route4 = from_json(qx/ip -4 --json route show/, { utf8 => 1 });
- for my $route (@$route4) {
- if ($route->{dst} eq 'default') {
- $gateway4 = {
- dev => $route->{dev},
- gateway => $route->{gateway},
- };
- last;
- }
- }
-
- my $route6 = from_json(qx/ip -6 --json route show/, { utf8 => 1 });
- for my $route (@$route6) {
- if ($route->{dst} eq 'default') {
- $gateway6 = {
- dev => $route->{dev},
- gateway => $route->{gateway},
- };
- last;
- }
- }
-
- my $routes;
- $routes->{gateway4} = $gateway4 if $gateway4;
- $routes->{gateway6} = $gateway6 if $gateway6;
-
- return $routes;
-}
-
-# If `/etc/resolv.conf` fails to open this returns nothing.
-# Otherwise it returns a hash:
-# {
-# dns => <first dns entry>,
-#
-my sub query_dns : prototype() {
- log_info("query DNS from resolv.conf (managed by DHCP client)");
- open my $fh, '<', '/etc/resolv.conf' or return;
-
- my @dns;
- my $domain;
- while (defined(my $line = <$fh>)) {
- if ($line =~ /^nameserver\s+(\S+)/) {
- push @dns, $1;
- } elsif (!defined($domain) && $line =~ /^domain\s+(\S+)/) {
- $domain = $1;
- }
- }
-
- my $output = {
- domain => $domain,
- @dns ? (dns => \@dns) : (),
- };
-}
-
# Uses `traceroute` and `geoiplookup`/`geoiplookup6` to figure out the current country.
# Has a 10s timeout and uses the stops at the first entry found in the geoip database.
my sub detect_country_tracing_to : prototype($$) {
@@ -294,9 +146,9 @@ my sub detect_country_tracing_to : prototype($$) {
# default_zfs_arc_max => <default upper limit for the ZFS ARC size in MiB>,
# disks => <see Proxmox::Sys::Block::hd_list()>,
# network => {
-# interfaces => <see query_netdevs()>,
-# routes => <see query_routes()>,
-# dns => <see query_dns()>,
+# interfaces => <see Proxmox::Sys::Net::query_netdevs()>,
+# routes => <see Proxmox::Sys::Net::query_routes()>,
+# dns => <see Proxmox::Sys::Net::query_dns()>,
# },
# }
sub query_installation_environment : prototype() {
@@ -315,14 +167,14 @@ sub query_installation_environment : prototype() {
# else re-query everything
my $output = {};
- my $routes = query_routes();
+ my $routes = Proxmox::Sys::Net::query_routes();
log_info("query block devices");
$output->{disks} = Proxmox::Sys::Block::get_cached_disks();
$output->{network} = {
- interfaces => query_netdevs(),
+ interfaces => Proxmox::Sys::Net::query_netdevs(),
routes => $routes,
- dns => query_dns(),
+ dns => Proxmox::Sys::Net::query_dns(),
};
# avoid serializing out null or an empty string, that can trip up the UIs
diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
index e3b3b56..9571236 100644
--- a/Proxmox/Sys/Net.pm
+++ b/Proxmox/Sys/Net.pm
@@ -3,9 +3,10 @@ package Proxmox::Sys::Net;
use strict;
use warnings;
+use Proxmox::Log;
use Proxmox::Sys::Command;
use Proxmox::Sys::Udev;
-use JSON qw();
+use JSON qw(from_json);
use base qw(Exporter);
our @EXPORT_OK = qw(
@@ -257,6 +258,155 @@ sub udevadm_netdev_details {
return $result;
}
+# Returns a hash.
+#
+# {
+# <ifname> => {
+# mac => <mac address>,
+# index => <index>,
+# name => <ifname>,
+# state => <UP|DOWN|LOWERLAYERDOWN|DORMANT|TESTING|NOTPRESENT|UNKNOWN>,
+# pinned_id => <sequential numerical ID>,
+# driver => <driver name>,
+# addresses => [
+# family => <inet|inet6>,
+# address => <mac address>,
+# prefix => <length>,
+# ],
+# },
+# }
+sub query_netdevs : prototype() {
+ my $ifs = {};
+ my $default;
+
+ # FIXME: not the same as the battle proven way we used in the installer for years?
+ my $interfaces = from_json(qx/ip --details --json address show/, { utf8 => 1 });
+
+ my $pinned_counter = 0;
+ for my $if (@$interfaces) {
+ my ($index, $name, $state, $mac, $addresses) =
+ $if->@{qw(ifindex ifname operstate address addr_info)};
+
+ next if !$name || $name eq 'lo'; # could also check flags for LOOPBACK..
+ if (!$mac) {
+ log_info("skipped interface $name, no mac address detected");
+ next;
+ }
+
+ my @valid_addrs;
+ if (uc($state) eq 'UP') {
+ for my $addr (@$addresses) {
+ next if $addr->{scope} eq 'link';
+
+ my ($family, $addr, $prefix) = $addr->@{qw(family local prefixlen)};
+
+ push @valid_addrs,
+ {
+ family => $family,
+ address => $addr,
+ prefix => $prefix,
+ };
+ }
+ }
+
+ my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
+ $driver =~ s!^.*/!!;
+
+ $ifs->{$name} = {
+ index => $index,
+ name => $name,
+ mac => $mac,
+ state => uc($state),
+ driver => $driver,
+ };
+ $ifs->{$name}->{addresses} = \@valid_addrs if @valid_addrs;
+
+ # only set the `pinned_id` property if the interface actually can be pinned,
+ # i.e. is a physical link
+ my $is_pinnable =
+ Proxmox::Sys::Net::ip_link_is_physical($if) && !Proxmox::Sys::Net::iface_is_vf($name);
+ if ($is_pinnable) {
+ $ifs->{$name}->{pinned_id} = "${pinned_counter}";
+ $pinned_counter++;
+ }
+ }
+
+ return $ifs;
+}
+
+# Returns a hash.
+#
+# {
+# gateway4 => {
+# dst => "default",
+# gateway => <ipv4>,
+# dev => <ifname>,
+# },
+# gateway6 => {
+# dst => "default",
+# gateway => <ipv6>,
+# dev => <ifname>,
+# },
+# }
+sub query_routes : prototype() {
+ my ($gateway4, $gateway6);
+
+ log_info("query routes");
+ my $route4 = from_json(qx/ip -4 --json route show/, { utf8 => 1 });
+ for my $route (@$route4) {
+ if ($route->{dst} eq 'default') {
+ $gateway4 = {
+ dev => $route->{dev},
+ gateway => $route->{gateway},
+ };
+ last;
+ }
+ }
+
+ my $route6 = from_json(qx/ip -6 --json route show/, { utf8 => 1 });
+ for my $route (@$route6) {
+ if ($route->{dst} eq 'default') {
+ $gateway6 = {
+ dev => $route->{dev},
+ gateway => $route->{gateway},
+ };
+ last;
+ }
+ }
+
+ my $routes;
+ $routes->{gateway4} = $gateway4 if $gateway4;
+ $routes->{gateway6} = $gateway6 if $gateway6;
+
+ return $routes;
+}
+
+# If `/etc/resolv.conf` fails to open this returns nothing.
+# Otherwise it returns a hash:
+# {
+# domain => <domain name or undefined, if not found>
+# dns => [<dns servers>..],
+# }
+sub query_dns : prototype() {
+ log_info("query DNS from resolv.conf (managed by DHCP client)");
+ open my $fh, '<', '/etc/resolv.conf' or return;
+
+ my @dns;
+ my $domain;
+ while (defined(my $line = <$fh>)) {
+ if ($line =~ /^nameserver\s+(\S+)/) {
+ push @dns, $1;
+ } elsif (!defined($domain) && $line =~ /^domain\s+(\S+)/) {
+ $domain = $1;
+ }
+ }
+
+ my $output = {
+ domain => $domain,
+ @dns ? (dns => \@dns) : (),
+ };
+}
+
# Tries to detect the hostname for this system given via DHCP, if available.
# The hostname _might_ also include the local domain name, depending on the
# concrete DHCP server implementation.
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH installer 3/8] gui: use run_env->{network} instead of old run_env->{ipconf}
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 1/8] install: drop trivial fromjs() wrapper and use JSON::from_json() Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 2/8] install: move network subroutines to Proxmox::Sys::Net Christoph Heiss
@ 2026-05-08 18:44 ` Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 4/8] sys: net: drop the now-unused `ipconf` runtime environment configuration Christoph Heiss
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
The TUI started out with using the newer network configuration. That
information is parsed from the iproute2 JSON output, instead of the
human-readable output - making it more robust.
Reworking the network address/gateway/DNS server selection also allows
for properly handling IPv4 and/or IPv6 setups, by always selecting the
correct IP address for the "active" network, based on the gateway.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Sys/Net.pm | 6 +++++
proxinstall | 66 ++++++++++++++++++++++++++++++----------------
2 files changed, 50 insertions(+), 22 deletions(-)
diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
index 9571236..10144e0 100644
--- a/Proxmox/Sys/Net.pm
+++ b/Proxmox/Sys/Net.pm
@@ -10,6 +10,7 @@ use JSON qw(from_json);
use base qw(Exporter);
our @EXPORT_OK = qw(
+ ip_get_version
parse_ip_address
parse_ip_mask
parse_fqdn
@@ -134,6 +135,11 @@ sub parse_ip_address {
return (undef, undef);
}
+sub ip_get_version {
+ my ($ip, $ver) = parse_ip_address($_[0]);
+ return $ver;
+}
+
sub parse_ip_mask {
my ($text, $ip_version) = @_;
$text =~ s/^\s+//;
diff --git a/proxinstall b/proxinstall
index f5e1555..8291a46 100755
--- a/proxinstall
+++ b/proxinstall
@@ -37,7 +37,9 @@ use Proxmox::Sys;
use Proxmox::Sys::Block qw(get_cached_disks);
use Proxmox::Sys::Command qw(syscmd);
use Proxmox::Sys::File qw(file_read_all file_write_all);
-use Proxmox::Sys::Net qw(parse_ip_address parse_ip_mask validate_link_pin_map DEFAULT_PIN_PREFIX);
+use Proxmox::Sys::Net qw(
+ parse_ip_address ip_get_version parse_ip_mask validate_link_pin_map DEFAULT_PIN_PREFIX
+);
use Proxmox::UI;
my $step_number = 0; # Init number for global function list
@@ -430,7 +432,21 @@ sub create_ipconf_view {
Proxmox::UI::display_html('ipconf.htm');
my $run_env = Proxmox::Install::RunEnv::get();
- my $ipconf = $run_env->{ipconf};
+ my $network = $run_env->{network};
+
+ # prefer ipv4 gateway and fallback to ipv6
+ my $default_gateway = $network->{routes}->{gateway4} // $network->{routes}->{gateway6};
+
+ my $default_gateway_ipversion = ip_get_version($default_gateway->{gateway})
+ if defined($default_gateway->{gateway});
+
+ my $default_dns = undef;
+ if (defined($default_gateway)) {
+ # try to retrieve the first dns server matching the IP version of the gateway
+ ($default_dns) = grep {
+ ip_get_version($_) == $default_gateway_ipversion
+ } @{ $network->{dns}->{dns} };
+ }
my $grid = &$create_basic_grid();
$grid->set_row_spacing(10);
@@ -463,20 +479,20 @@ sub create_ipconf_view {
my $mapping = Proxmox::Install::Config::get_network_interface_pin_map();
my $i = 0;
- for my $index (sort keys $ipconf->{ifaces}->%*) {
- my $iface = $ipconf->{ifaces}->{$index};
+ for my $name (sort keys $network->{interfaces}->%*) {
+ my $iface = $network->{interfaces}->{$name};
my $iter = $device_model->append();
my $symbol = "$iface->{state}" eq "UP" ? "\x{25CF}" : ' ';
- my $name =
+ my $label =
$gtk_state->{network_pinning_enabled} && defined($mapping->{ $iface->{mac} })
? $mapping->{ $iface->{mac} }
- : $iface->{name};
+ : $name;
$device_model->set(
$iter,
0 => $symbol,
- 1 => "$name - $iface->{mac} ($iface->{driver})",
+ 1 => "$label - $iface->{mac} ($iface->{driver})",
);
$i++;
}
@@ -513,7 +529,7 @@ sub create_ipconf_view {
return if $current->get_active() == -1;
my $new = $device_active_map->{ $current->get_active() };
- my $iface = $ipconf->{ifaces}->{$new};
+ my $iface = $network->{interfaces}->{$new};
my $selected = Proxmox::Install::Config::get_mngmt_nic();
return if defined($selected) && $iface->{name} eq $selected;
@@ -529,15 +545,22 @@ sub create_ipconf_view {
my ($initial_active_device_pos, $initial_addr, $initial_mask) = (0, undef, undef);
my $i = 0;
- for my $index (sort keys $ipconf->{ifaces}->%*) {
- my $iface = $ipconf->{ifaces}->{$index};
- $device_active_map->{$i} = $index;
- $device_active_reverse_map->{ $iface->{name} } = $i;
+ for my $name (sort keys $network->{interfaces}->%*) {
+ my $iface = $network->{interfaces}->{$name};
+ $device_active_map->{$i} = $name;
+ $device_active_reverse_map->{$name} = $i;
- if (defined($ipconf->{default}) && $index == $ipconf->{default}) {
+ if (defined($default_gateway) && $name eq $default_gateway->{dev}) {
+ # the chosen gateway is on this interface
$initial_active_device_pos = $i;
- $initial_addr = $iface->{inet}->{addr} || $iface->{inet6}->{addr};
- $initial_mask = $iface->{inet}->{prefix} || $iface->{inet6}->{prefix};
+
+ # now find an address that matches the IP version of the gateway
+ my ($addr) = grep {
+ ip_get_version($_->{address}) == $default_gateway_ipversion
+ } @{ $iface->{addresses} };
+
+ $initial_addr = $addr->{address};
+ $initial_mask = $addr->{prefix};
}
$i++;
}
@@ -548,9 +571,8 @@ sub create_ipconf_view {
if (my $nic = Proxmox::Install::Config::get_mngmt_nic()) {
$initial_active_device_pos = $device_active_reverse_map->{$nic};
} else {
- my $iface_id = $device_active_map->{$initial_active_device_pos};
- my $iface = $ipconf->{ifaces}->{$iface_id};
- Proxmox::Install::Config::set_mngmt_nic($iface->{name});
+ my $iface_name = $device_active_map->{$initial_active_device_pos};
+ Proxmox::Install::Config::set_mngmt_nic($iface_name);
}
if (my $cidr = Proxmox::Install::Config::get_cidr()) {
@@ -574,7 +596,7 @@ sub create_ipconf_view {
my $fqdn = Proxmox::Install::Config::get_fqdn();
my $hostname = $run_env->{network}->{hostname} || $iso_env->{product};
- my $domain = $ipconf->{domain} || "example.invalid";
+ my $domain = $network->{dns}->{domain} || "example.invalid";
$fqdn //= "$hostname.$domain";
my ($host_label, $hostentry) = create_text_input($fqdn, 'Hostname (FQDN)');
@@ -585,14 +607,14 @@ sub create_ipconf_view {
$grid->attach($cidr_box, 1, 2, 2, 1);
my $cfg_gateway = Proxmox::Install::Config::get_gateway();
- my $gateway = $cfg_gateway // $ipconf->{gateway} || '192.168.100.1';
+ my $gateway = $cfg_gateway // $default_gateway->{gateway} || '192.168.100.1';
my ($gw_label, $ipconf_entry_gw) = create_text_input($gateway, 'Gateway');
$grid->attach($gw_label, 0, 3, 1, 1);
$grid->attach($ipconf_entry_gw, 1, 3, 2, 1);
my $cfg_dns = Proxmox::Install::Config::get_dns();
- my $dnsserver = $cfg_dns // $ipconf->{dnsserver} || $gateway;
+ my $dnsserver = $cfg_dns // $default_dns || $gateway;
my ($dns_label, $ipconf_entry_dns) = create_text_input($dnsserver, 'DNS Server');
@@ -1915,7 +1937,7 @@ my $initial_error = 0;
}
my $run_env = Proxmox::Install::RunEnv::get();
-if (!$initial_error && (scalar keys $run_env->{ipconf}->{ifaces}->%* == 0)) {
+if (!$initial_error && (scalar keys $run_env->{network}->{interfaces}->%* == 0)) {
print STDERR "no network interfaces found\n";
$initial_error = 1;
Proxmox::UI::display_html("nonics.htm");
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH installer 4/8] sys: net: drop the now-unused `ipconf` runtime environment configuration
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
` (2 preceding siblings ...)
2026-05-08 18:44 ` [PATCH installer 3/8] gui: use run_env->{network} instead of old run_env->{ipconf} Christoph Heiss
@ 2026-05-08 18:44 ` Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 5/8] sys: net: allow up to /128 netmask for IPv6 Christoph Heiss
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
$run_env->{ipconf} is now completely unused, thus drop the dead code.
No functional changes.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install/RunEnv.pm | 6 ----
Proxmox/Sys/Net.pm | 71 ++-------------------------------------
2 files changed, 2 insertions(+), 75 deletions(-)
diff --git a/Proxmox/Install/RunEnv.pm b/Proxmox/Install/RunEnv.pm
index b83b9ff..d226ce1 100644
--- a/Proxmox/Install/RunEnv.pm
+++ b/Proxmox/Install/RunEnv.pm
@@ -136,7 +136,6 @@ my sub detect_country_tracing_to : prototype($$) {
# {
# arch => <dpkg architecture, e.g. 'amd64' or 'arm64'>,
# country => <short country>,
-# ipconf = <see Proxmox::Sys::Net::get_ip_config()>,
# kernel_cmdline = <contents of /proc/cmdline>,
# total_memory = <memory size in MiB>,
# hvm_supported = <1 if the CPU supports hardware-accelerated virtualization>,
@@ -187,11 +186,6 @@ sub query_installation_environment : prototype() {
$output->{network}->{hostname} = $fqdn;
}
- # FIXME: move whatever makes sense over to Proxmox::Sys::Net:: and keep that as single source,
- # it can then use some different structure just fine (after adapting the GTK GUI to that) but
- # **never** to (slightly different!) things for the same stuff...
- $output->{ipconf} = Proxmox::Sys::Net::get_ip_config();
-
$output->{kernel_cmdline} = file_read_firstline("/proc/cmdline");
$output->{total_memory} = query_total_memory();
diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
index 10144e0..52615ac 100644
--- a/Proxmox/Sys/Net.pm
+++ b/Proxmox/Sys/Net.pm
@@ -186,78 +186,11 @@ sub get_pin_link_file_content {
return sprintf($LINK_FILE_TEMPLATE, $mac, $pin_name);
}
-sub get_ip_config {
- my $ifaces = {};
- my $default;
- my $pinned_counter = 0;
-
- my $links = `ip -o l`;
- foreach my $l (split /\n/, $links) {
- my ($index, $name, $flags, $state, $mac) =
- $l =~ m/^(\d+):\s+(\S+):\s+<(\S+)>.*\s+state\s+(\S+)\s+.*\s+link\/ether\s+(\S+)\s+/;
- next if !$name || $name eq 'lo';
-
- my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
- $driver =~ s!^.*/!!;
-
- $ifaces->{"$index"} = {
- name => $name,
- pinned_id => "${pinned_counter}",
- driver => $driver,
- flags => $flags,
- state => $state,
- mac => $mac,
- };
- $pinned_counter++;
-
- my $addresses = `ip -o a s $name`;
- for my $addr_line (split /\n/, $addresses) {
- my ($family, $ip, $prefix) =
- $addr_line =~ m/^\Q$index\E:\s+\Q$name\E\s+(inet|inet6)\s+($IPRE)\/(\d+)\s+/;
- next if !$ip;
- next if $addr_line =~ /scope\s+link/; # ignore link local
-
- my $mask = $prefix;
-
- if ($family eq 'inet') {
- next if !$ip =~ /$IPV4RE/;
- next if $prefix < 8 || $prefix > 32;
- $mask = @$ipv4_reverse_mask[$prefix];
- } else {
- next if !$ip =~ /$IPV6RE/;
- }
-
- $default = $index if !$default;
-
- $ifaces->{"$index"}->{"$family"} = {
- prefix => $prefix,
- mask => $mask,
- addr => $ip,
- };
- }
- }
-
- my $route = `ip route`;
- my ($gateway) = $route =~ m/^default\s+via\s+(\S+)\s+/m;
-
- my $resolvconf = `cat /etc/resolv.conf`;
- my ($dnsserver) = $resolvconf =~ m/^nameserver\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/m;
- my ($domain) = $resolvconf =~ m/^domain\s+(\S+)$/m;
-
- return {
- default => $default,
- ifaces => $ifaces,
- gateway => $gateway,
- dnsserver => $dnsserver,
- domain => $domain,
- };
-}
-
sub udevadm_netdev_details {
- my $ip_config = get_ip_config();
+ my $ifaces = query_netdevs();
my $result = {};
- for my $dev (values $ip_config->{ifaces}->%*) {
+ for my $dev (values $ifaces->%*) {
my $name = $dev->{name};
$result->{$name} = Proxmox::Sys::Udev::get_udev_properties("/sys/class/net/$name");
}
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH installer 5/8] sys: net: allow up to /128 netmask for IPv6
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
` (3 preceding siblings ...)
2026-05-08 18:44 ` [PATCH installer 4/8] sys: net: drop the now-unused `ipconf` runtime environment configuration Christoph Heiss
@ 2026-05-08 18:44 ` Christoph Heiss
2026-05-08 18:44 ` [PATCH RFC installer 6/8] sys: net: ignore ipv6 nameservers with zone identifiers Christoph Heiss
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
This was previously capped to /126 for whether reason, everywhere else
in the stack we do allow /127 and /128, though.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Sys/Net.pm | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
index 52615ac..c6bd841 100644
--- a/Proxmox/Sys/Net.pm
+++ b/Proxmox/Sys/Net.pm
@@ -144,7 +144,7 @@ sub parse_ip_mask {
my ($text, $ip_version) = @_;
$text =~ s/^\s+//;
$text =~ s/\s+$//;
- if ($ip_version == 6 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 126) {
+ if ($ip_version == 6 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 128) {
return $text;
} elsif ($ip_version == 4 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 32) {
return $text;
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH RFC installer 6/8] sys: net: ignore ipv6 nameservers with zone identifiers
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
` (4 preceding siblings ...)
2026-05-08 18:44 ` [PATCH installer 5/8] sys: net: allow up to /128 netmask for IPv6 Christoph Heiss
@ 2026-05-08 18:44 ` Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 7/8] common: options: rework network address setup to handle ipv6-only Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 8/8] unconfigured: try to retrieve IPv6 SLAAC addresses on startup Christoph Heiss
7 siblings, 0 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
The stack does not handle IPv6 addresses with zone identifiers anywhere
(yet).
Just dropping the zone identifier would be another possibility for now,
but would probably break more things (as they are e.g. pretty much
mandantory for link-local addresses.)
IPv6 addresses with zone identifiers are not all that well supported and
are uncommon anyway it seems, see [0][1][2].
For now, work around that by ignoring all IPv6 nameservers with zone
identifiers.
[0] https://github.com/containers/common/pull/2233
[1] https://github.com/containers/aardvark-dns/issues/535
[2] https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=255316
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Sending as RFC as it is a rather ugly hack, but reworking the rest of
the stack is a bit more effort and wanted to send the series now.
Mainly fails due to parsing it as a `std::net::IpAddr` addresses in
Rust, which does not support zone identifiers, in addition to
Proxmox::Sys::Net::parse_ip_address() not recognizing them either.
Proxmox/Sys/Net.pm | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
index c6bd841..4781e51 100644
--- a/Proxmox/Sys/Net.pm
+++ b/Proxmox/Sys/Net.pm
@@ -334,7 +334,14 @@ sub query_dns : prototype() {
my $domain;
while (defined(my $line = <$fh>)) {
if ($line =~ /^nameserver\s+(\S+)/) {
- push @dns, $1;
+ # FIXME: handle IPv6 zone identifiers across the stack.
+ # For now, ignore all addresses containing them.
+ my $addr = $1;
+ if ($addr =~ /%\S+$/) {
+ log_warn("skipping nameserver $addr as being link-local");
+ } else {
+ push @dns, $addr;
+ }
} elsif (!defined($domain) && $line =~ /^domain\s+(\S+)/) {
$domain = $1;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH installer 7/8] common: options: rework network address setup to handle ipv6-only
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
` (5 preceding siblings ...)
2026-05-08 18:44 ` [PATCH RFC installer 6/8] sys: net: ignore ipv6 nameservers with zone identifiers Christoph Heiss
@ 2026-05-08 18:44 ` Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 8/8] unconfigured: try to retrieve IPv6 SLAAC addresses on startup Christoph Heiss
7 siblings, 0 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
With this, IPv6-only setups are now handled properly. Previously it
would fail to actually select IPv6 addresses for the interface and DNS
on such setups, instead falling back to the default IPv4 addresses set
above.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-installer-common/src/options.rs | 89 ++++++++++++-------------
1 file changed, 44 insertions(+), 45 deletions(-)
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index ed00b4b..8929bd4 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -425,59 +425,58 @@ impl NetworkOptions {
pinning_opts: pinning_opts.cloned(),
};
- if let Some(ip) = network.dns.dns.first() {
- this.dns_server = *ip;
- }
-
- if let Some(routes) = &network.routes
- && let Some(gw) = &routes.gateway4
- && let Some(iface) = network.interfaces.get(&gw.dev)
- {
- // we got some ipv4 connectivity, so use that
-
- if let Some(opts) = pinning_opts
- && let Some(pinned) = iface.to_pinned(opts)
- {
- this.ifname.clone_from(&pinned.name);
- } else {
- this.ifname.clone_from(&iface.name);
- }
-
- if let Some(addr) = iface.addresses.iter().find(|addr| addr.is_ipv4()) {
- this.gateway = gw.gateway;
- this.address = *addr;
- } else if let Some(gw) = &routes.gateway6
+ let iface = if let Some(routes) = &network.routes {
+ if let Some(gw) = &routes.gateway4
&& let Some(iface) = network.interfaces.get(&gw.dev)
- && let Some(addr) = iface.addresses.iter().find(|addr| addr.is_ipv6())
{
- // no ipv4, but ipv6 connectivity
- if let Some(opts) = pinning_opts
- && let Some(pinned) = iface.to_pinned(opts)
- {
- this.ifname.clone_from(&pinned.name);
- } else {
- this.ifname.clone_from(&iface.name);
+ this.gateway = gw.gateway;
+
+ if let Some(addr) = iface.addresses.iter().find(|addr| addr.is_ipv4()) {
+ this.address = *addr;
}
- this.gateway = gw.gateway;
- this.address = *addr;
- }
- }
+ if let Some(addr) = network.dns.dns.iter().find(|addr| addr.is_ipv4()) {
+ this.dns_server = *addr;
+ }
- // In case no there are no routes defined at all (e.g. no DHCP lease),
- // try to set the interface name to *some* valid values. At least one
- // NIC should always be present here, as the installation will abort
- // earlier in that case, so use the first one enumerated.
- if this.ifname.is_empty()
- && let Some(iface) = network.interfaces.values().min_by_key(|v| v.index)
- {
- if let Some(opts) = pinning_opts
- && let Some(pinned) = iface.to_pinned(opts)
+ Some(iface)
+ } else if let Some(gw) = &routes.gateway6
+ && let Some(iface) = network.interfaces.get(&gw.dev)
{
- this.ifname.clone_from(&pinned.name);
+ this.gateway = gw.gateway;
+
+ if let Some(addr) = iface.addresses.iter().find(|addr| addr.is_ipv6()) {
+ this.address = *addr;
+ }
+
+ if let Some(addr) = network.dns.dns.iter().find(|addr| addr.is_ipv6()) {
+ this.dns_server = *addr;
+ }
+
+ Some(iface)
} else {
- this.ifname.clone_from(&iface.name);
+ None
}
+ } else {
+ None
+ }
+ .unwrap_or_else(|| {
+ // Safety: In case no there are no routes defined at all (e.g. no DHCP lease), try to
+ // set the interface name to *some* valid values. At least one NIC must always be
+ // present here, as the installation will abort earlier otherwise, so use the first one
+ // enumerated.
+ network
+ .interfaces
+ .values()
+ .min_by_key(|v| v.index)
+ .expect("at least one NIC must be present")
+ });
+
+ // Use pinned network interface name, if enabled
+ if let Some(pinned) = pinning_opts.and_then(|opts| iface.to_pinned(opts)) {
+ this.ifname.clone_from(&pinned.name);
+ } else {
+ this.ifname.clone_from(&iface.name);
}
if let Some(ref mut opts) = this.pinning_opts {
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH installer 8/8] unconfigured: try to retrieve IPv6 SLAAC addresses on startup
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
` (6 preceding siblings ...)
2026-05-08 18:44 ` [PATCH installer 7/8] common: options: rework network address setup to handle ipv6-only Christoph Heiss
@ 2026-05-08 18:44 ` Christoph Heiss
7 siblings, 0 replies; 9+ messages in thread
From: Christoph Heiss @ 2026-05-08 18:44 UTC (permalink / raw)
To: pve-devel
Uses `rdisc6` to send IPv6 router solicitations, in order to get
potential router advertisements. The kernel then auto-configures any
retrieved IPv6 address on the corresponding interface.
`rdnssd` is used to to retrieve IPv6 (recursive) DNS servers from these
router advertisements.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
debian/control | 2 ++
unconfigured.sh | 24 ++++++++++++++++++++++++
2 files changed, 26 insertions(+)
diff --git a/debian/control b/debian/control
index 8049dd6..3c9de71 100644
--- a/debian/control
+++ b/debian/control
@@ -50,7 +50,9 @@ Depends: chrony,
libgtk3-perl,
libgtk3-webkit2-perl,
libjson-perl,
+ ndisc6,
proxmox-kernel-helper,
+ rdnssd,
squashfs-tools,
util-linux-extra,
${misc:Depends},
diff --git a/unconfigured.sh b/unconfigured.sh
index 7445ac4..02ace77 100755
--- a/unconfigured.sh
+++ b/unconfigured.sh
@@ -218,6 +218,16 @@ handle_wireless() {
fi
}
+# Iterates over all UP interfaces and sends a router solicitation on each.
+ipv6_send_router_solicitation() {
+ local devs
+ devs="$(ip -6 addr show scope link up | grep -oP '^\d+: \K\S+(?=:)')"
+
+ for dev in $devs; do
+ rdisc6 -1 --retry 1 "$dev" || true
+ done
+}
+
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin
echo "Starting Proxmox installation"
@@ -311,6 +321,20 @@ echo -n "Attempting to get DHCP leases... "
dhclient -v
echo "done"
+# start rdnssd in the background first
+# the default merge hook just appends it to /etc/resolv.conf, which is what we want
+# also need to create the run-directory for rdnssd, otherwise it will silently
+# fail to write out new entries
+echo "Starting rdnssd for retrieving IPv6 nameservers..."
+mkdir -p /run/rdnssd
+rdnssd --merge-hook /etc/rdnssd/merge-hook --user root || echo "starting rdnssd failed ($?)"
+
+# check for IPv6 SLAAC by sending a router solicitation
+# rdnssd will pick up any nameservers, and the kernel interface addresses
+echo "Sending IPv6 router solicitations..."
+ipv6_send_router_solicitation
+echo "Done sending IPv6 router solicitations."
+
echo "Starting chrony for opportunistic time-sync... "
chronyd || echo "starting chrony failed ($?)"
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-05-08 18:47 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-08 18:44 [PATCH installer 0/8] add IPv6 SLAAC and v6-only support Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 1/8] install: drop trivial fromjs() wrapper and use JSON::from_json() Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 2/8] install: move network subroutines to Proxmox::Sys::Net Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 3/8] gui: use run_env->{network} instead of old run_env->{ipconf} Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 4/8] sys: net: drop the now-unused `ipconf` runtime environment configuration Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 5/8] sys: net: allow up to /128 netmask for IPv6 Christoph Heiss
2026-05-08 18:44 ` [PATCH RFC installer 6/8] sys: net: ignore ipv6 nameservers with zone identifiers Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 7/8] common: options: rework network address setup to handle ipv6-only Christoph Heiss
2026-05-08 18:44 ` [PATCH installer 8/8] unconfigured: try to retrieve IPv6 SLAAC addresses on startup Christoph Heiss
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.