Fix live activation of Linux VLAN interfaces
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled

ⓘ Create and detect VLAN devices before applying live configuration, parse live VLANs with their physical parent instead of a virtual alias, normalize persisted dotted VLAN names when the parent device exists, consistently target the real VLAN device across the ip, ifup and vconfig paths, skip recreating existing devices or activating absent disabled ones, and add regression coverage.

https://github.com/webmin/webmin/pull/2785
This commit is contained in:
Ilia Ross
2026-07-11 13:33:58 +02:00
parent 2fb4eb1721
commit 77610666cc
2 changed files with 377 additions and 35 deletions

View File

@@ -26,13 +26,15 @@ if (&has_command("ip")) {
my %ifc;
my $iface_name = $ifc{'name'};
my $iface_auto_ip;
if ($l =~ /^\d+:\s+([^ \t\r\n\@]+\d+\.(\d+))@([^ \t\r\n\@]+\d+):/) {
if ($l =~ /^\d+:\s+([^ \t\r\n\@]+\.(\d+))@([^ \t\r\n\@]+):/) {
# Line like :
# 3: eth0.99@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
$ifc{'fullname'} = $1;
$ifc{'name'} = $ifc{'fullname'};
$ifc{'vlanid'} = $2;
$ifc{'virtual'} = $3;
$ifc{'physical'} = $3;
$ifc{'vlan'} = 1;
$ifc{'virtual'} = '';
}
elsif ($l =~ /^\d+:\s+([^ \t\r\n\@]+):/) {
# Line like :
@@ -240,16 +242,48 @@ return @rv;
sub activate_interface
{
my ($a) = @_;
my ($old) = grep { $_->{'fullname'} eq $a->{'fullname'} } &active_interfaces();
# For Debian 5.0+ the "vconfig add" command is deprecated, this is handled
# by ifup.
if(($a->{'vlan'} == 1) && !(($gconfig{'os_type'} eq 'debian-linux') && ($gconfig{'os_version'} >= 5))) {
local $vconfigCMD = "vconfig add " .
quotemeta($a->{'physical'})." ".
quotemeta($a->{'vlanid'});
local $vconfigout = &backquote_logged("$vconfigCMD 2>&1");
if ($?) { &error($vonconfigout); }
my @active = &active_interfaces(1);
# Some boot-time backends preserve only the conventional dotted VLAN name.
# Only infer a VLAN when the parent device really exists, as a dotted name
# alone can belong to an interface of any type.
if (!$a->{'vlan'} &&
(!defined($a->{'virtual'}) || $a->{'virtual'} eq '') &&
$a->{'fullname'} =~ /^(.+)\.(\d+)$/) {
my ($physical, $vlanid) = ($1, $2);
if (grep { $_->{'fullname'} eq $physical } @active) {
$a = { %$a,
'vlan' => 1,
'physical' => $physical,
'vlanid' => $vlanid,
'virtual' => '' };
}
}
my ($old) = grep { $_->{'fullname'} eq $a->{'fullname'} } @active;
my $devname = $a->{'vlan'} ? $a->{'physical'}.".".$a->{'vlanid'}
: $a->{'name'};
# A disabled VLAN that does not exist has no live state to update.
return if ($a->{'vlan'} && !$a->{'up'} && !$old);
# Create a missing VLAN device before assigning addresses to it.
if ($a->{'vlan'} && $a->{'up'} && !$old) {
if (&has_command("ip")) {
my $cmd = "ip link add link ".quotemeta($a->{'physical'}).
" name ".quotemeta($devname).
" type vlan id ".quotemeta($a->{'vlanid'});
my $out = &backquote_logged("$cmd 2>&1");
&error("Failed to create VLAN device : $out") if ($?);
}
# For Debian 5.0+ VLAN creation is handled by ifup.
elsif (!(($gconfig{'os_type'} eq 'debian-linux') &&
($gconfig{'os_version'} >= 5))) {
my $cmd = "vconfig add ".quotemeta($a->{'physical'})." ".
quotemeta($a->{'vlanid'});
my $out = &backquote_logged("$cmd 2>&1");
&error($out) if ($?);
}
}
if (&has_command("ip") && $a->{'virtual'} ne '' && !$a->{'up'}) {
@@ -295,17 +329,18 @@ if (&has_command("ip") && $a->{'bond'} && $a->{'up'} && !$old) {
}
}
if (($a->{'bond'} || !&has_command("ifconfig")) && &has_command("ip")) {
if (($a->{'bond'} || $a->{'vlan'} || !&has_command("ifconfig")) &&
&has_command("ip")) {
# For a real interface, activate or de-activate the link
if ($a->{'virtual'} eq '' && $a->{'up'} && (!$old || !$old->{'up'})) {
# Bring up
my $cmd = "ip link set dev ".quotemeta($a->{'name'})." up";
my $cmd = "ip link set dev ".quotemeta($devname)." up";
my $out = &backquote_logged("$cmd 2>&1");
&error("Failed to bring up link : $out") if ($?);
}
elsif ($a->{'virtual'} eq '' && !$a->{'up'} && $old && $old->{'up'}) {
# Take down
my $cmd = "ip link set dev ".quotemeta($a->{'name'})." down";
my $cmd = "ip link set dev ".quotemeta($devname)." down";
my $out = &backquote_logged("$cmd 2>&1");
&error("Failed to bring down link : $out") if ($?);
}
@@ -320,7 +355,7 @@ if (&has_command("ip")) {
$old->{'netmask'} ne $a->{'netmask'}) {
my $rcmd = "ip addr del ".quotemeta($old->{'address'}).
"/".&mask_to_prefix($old->{'netmask'}).
" dev ".quotemeta($a->{'name'});
" dev ".quotemeta($devname);
&system_logged("$rcmd >/dev/null 2>&1");
$readd = 1;
}
@@ -338,13 +373,7 @@ if (&has_command("ip")) {
if ($a->{'broadcast'}) {
$cmd .= " broadcast ".quotemeta($a->{'broadcast'});
}
if($a->{'vlan'} == 1) {
$cmd .= " dev ".quotemeta($a->{'physical'}).".".
quotemeta($a->{'vlanid'});
}
else {
$cmd .= " dev ".quotemeta($a->{'name'});
}
$cmd .= " dev ".quotemeta($devname);
}
}
elsif (&use_ifup_command($a)) {
@@ -406,37 +435,46 @@ elsif (&has_command("ifconfig")) {
else {
&error("Both the ifconfig and ip commands are missing");
}
my $out = &backquote_logged("cd / ; $cmd 2>&1");
if ($?) { &error("$cmd failed : $out"); }
my $out;
if ($cmd) {
$out = &backquote_logged("cd / ; $cmd 2>&1");
if ($?) { &error("$cmd failed : $out"); }
}
# Apply settings directly when a VLAN was activated using ip, even on
# systems where its boot-time configuration would normally use ifup.
my $apply_link_settings = !&use_ifup_command($a) ||
($a->{'vlan'} && &has_command("ip") &&
($a->{'up'} || $old));
# Apply ethernet address
if ($a->{'ether'} && !&use_ifup_command($a) && &has_command("ifconfig")) {
if ($a->{'ether'} && $apply_link_settings && &has_command("ifconfig")) {
# With ifconfig command
$out = &backquote_logged("ifconfig ".quotemeta($a->{'name'}).
$out = &backquote_logged("ifconfig ".quotemeta($devname).
" hw ether ".quotemeta($a->{'ether'})." 2>&1");
if ($?) { &error($out); }
}
elsif ($a->{'ether'} && !&use_ifup_command($a) && &has_command("ip")) {
elsif ($a->{'ether'} && $apply_link_settings && &has_command("ip")) {
# With ip link command
$out = &backquote_logged("ip link set dev ".quotemeta($a->{'name'}).
$out = &backquote_logged("ip link set dev ".quotemeta($devname).
" address ".quotemeta($a->{'ether'})." 2>&1");
if ($?) { &error($out); }
}
# Apply MTU
if ($a->{'mtu'} && !&use_ifup_command($a) && &has_command("ip")) {
if ($a->{'mtu'} && $apply_link_settings && &has_command("ip")) {
$out = &backquote_logged(
"ip link set dev ".quotemeta($a->{'name'})." mtu ".
"ip link set dev ".quotemeta($devname)." mtu ".
quotemeta($a->{'mtu'})." 2>&1");
if ($?) { &error($out); }
}
if ($a->{'virtual'} eq '' && &has_command("ifconfig")) {
# Remove old IPv6 addresses
local $l = &backquote_command("ifconfig ".quotemeta($a->{'name'}));
local $l = &backquote_command("ifconfig ".quotemeta($devname));
while($l =~ s/inet6 addr:\s*(\S+)\/(\d+)\s+Scope:(\S+)// ||
$l =~ s/inet6\s+(\S+)\s+prefixlen\s+(\d+)\s+scopeid\s+\S+//) {
my $cmd = "ifconfig ".quotemeta($a->{'name'})." inet6 del ".
my $cmd = "ifconfig ".quotemeta($devname)." inet6 del ".
quotemeta("$1/$2")." 2>&1";
$out = &backquote_logged($cmd);
&error("Failed to remove old IPv6 address : $out") if ($?);
@@ -444,7 +482,7 @@ if ($a->{'virtual'} eq '' && &has_command("ifconfig")) {
# Add IPv6 addresses
for(my $i=0; $i<@{$a->{'address6'}}; $i++) {
my $cmd = "ifconfig ".quotemeta($a->{'name'})." inet6 add ".
my $cmd = "ifconfig ".quotemeta($devname)." inet6 add ".
quotemeta($a->{'address6'}->[$i])."/".
quotemeta($a->{'netmask6'}->[$i])." 2>&1";
$out = &backquote_logged($cmd);
@@ -458,7 +496,7 @@ elsif ($a->{'virtual'} eq '' && &has_command("ip")) {
my $cmd = "ip -6 addr del ".
quotemeta($old->{'address6'}->[$i])."/".
quotemeta($old->{'netmask6'}->[$i])." dev ".
quotemeta($a->{'name'});
quotemeta($devname);
$out = &backquote_logged("$cmd 2>&1");
&error("Failed to remove old IPv6 address : $out") if ($?);
}
@@ -469,7 +507,7 @@ elsif ($a->{'virtual'} eq '' && &has_command("ip")) {
my $cmd = "ip -6 addr add ".
quotemeta($a->{'address6'}->[$i])."/".
quotemeta($a->{'netmask6'}->[$i])." dev ".
quotemeta($a->{'name'});
quotemeta($devname);
$out = &backquote_logged("$cmd 2>&1");
&error("Failed to add IPv6 address with $cmd : $out") if ($?);
}

View File

@@ -567,6 +567,48 @@ is_deeply(\@commands,
"dhcpcd active delete drops only the selected live virtual alias");
do "$root/net/linux-lib.pl" || die "linux-lib.pl: $@ $!";
my $ip_vlan_output = <<'IPADDR';
5: br-lan.10@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 02:00:00:00:00:10 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.2/24 brd 10.0.0.255 scope global br-lan.10
6: eth0.10.30@eth0.10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
inet 10.0.30.1/24 brd 10.0.30.255 scope global eth0.10.30
IPADDR
my @parsed_vlan;
{
no warnings 'redefine';
no warnings 'once';
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" : undef;
};
local *main::clean_language = sub { };
local *main::reset_environment = sub { };
local *main::open_execute_command = sub {
open(main::IFC, "<", \$ip_vlan_output) || die "open scalar: $!";
return 1;
};
@parsed_vlan = main::active_interfaces(1);
}
is_deeply([ map { [ $_->{'fullname'}, $_->{'name'},
$_->{'vlanid'}, $_->{'physical'},
$_->{'vlan'}, $_->{'virtual'} ] } @parsed_vlan ],
[ [ 'br-lan.10', 'br-lan.10', '10', 'br-lan', 1, '' ],
[ 'eth0.10.30', 'eth0.10.30', '30', 'eth0.10', 1, '' ] ],
"Linux active VLAN parsing supports parent names without digits");
@commands = ( );
{
no warnings 'redefine';
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" : undef;
};
main::deactivate_interface($parsed_vlan[0]);
}
is_deeply(\@commands,
[ 'ip addr del 10\.0\.0\.2\/24 dev br\-lan\.10 2>&1',
"ip link set dev br\\-lan\\.10 down 2>&1" ],
"Linux active VLAN is treated as a real link when deactivated");
@commands = ( );
{
no warnings 'redefine';
@@ -660,4 +702,266 @@ is_deeply(\@commands,
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev bond0 2>&1" ],
"Linux active bond interface is created before assigning an address");
my $vlan = { 'name' => 'auto',
'fullname' => 'eth0.10',
'virtual' => '',
'vlan' => 1,
'physical' => 'eth0',
'vlanid' => '10',
'address' => '10.0.0.2',
'netmask' => '255.255.255.0',
'address6' => [ ],
'netmask6' => [ ],
'up' => 1 };
@commands = ( );
{
no warnings 'redefine';
no warnings 'once';
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" :
$_[0] eq "ifconfig" ? "/sbin/ifconfig" : undef;
};
local *main::active_interfaces = sub {
return ( );
};
local *main::backquote_command = sub {
return "";
};
main::activate_interface({ %$vlan });
}
is_deeply(\@commands,
[ "ip link add link eth0 name eth0\\.10 type vlan id 10 2>&1",
"ip link set dev eth0\\.10 up 2>&1",
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev eth0\\.10 2>&1" ],
"Linux active VLAN interface is created before assigning an address");
my $persisted_vlan = { 'name' => 'br-lan.10',
'fullname' => 'br-lan.10',
'address' => '10.0.0.2',
'netmask' => '255.255.255.0',
'address6' => [ ],
'netmask6' => [ ],
'up' => 1 };
@commands = ( );
{
no warnings 'redefine';
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" :
$_[0] eq "ifconfig" ? "/sbin/ifconfig" : undef;
};
local *main::active_interfaces = sub {
return ({ 'name' => 'br-lan',
'fullname' => 'br-lan',
'virtual' => '',
'address6' => [ ],
'netmask6' => [ ],
'up' => 1 });
};
local *main::backquote_command = sub {
return "";
};
main::activate_interface({ %$persisted_vlan });
}
is_deeply(\@commands,
[ "ip link add link br\\-lan name br\\-lan\\.10 type vlan id 10 2>&1",
"ip link set dev br\\-lan\\.10 up 2>&1",
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev br\\-lan\\.10 2>&1" ],
"Linux persisted dotted VLAN is normalized before activation");
@commands = ( );
{
no warnings 'redefine';
no warnings 'once';
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" : undef;
};
local *main::active_interfaces = sub {
return ( );
};
main::activate_interface({ 'name' => 'lan.10',
'fullname' => 'lan.10',
'virtual' => '',
'address' => '10.0.0.2',
'netmask' => '255.255.255.0',
'address6' => [ ],
'netmask6' => [ ],
'up' => 1 });
}
is_deeply(\@commands,
[ "ip link set dev lan\\.10 up 2>&1",
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev lan\\.10 2>&1" ],
"Linux dotted interface without a live parent is not made a VLAN");
@commands = ( );
{
no warnings 'redefine';
no warnings 'once';
local $main::gconfig{'os_type'} = 'redhat-linux';
local $main::gconfig{'os_version'} = 13;
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" :
$_[0] eq "ifconfig" ? "/sbin/ifconfig" : undef;
};
local *main::active_interfaces = sub {
return ({ 'name' => 'eth0.10',
'fullname' => 'eth0.10',
'virtual' => '',
'vlan' => 1,
'physical' => 'eth0',
'vlanid' => '10',
'address' => '10.0.0.2',
'netmask' => '255.255.255.0',
'address6' => [ ],
'netmask6' => [ ],
'up' => 1 });
};
local *main::system_logged = sub {
push(@commands, $_[0]);
$? = 0;
return 0;
};
local *main::backquote_command = sub {
return "";
};
main::activate_interface({ %$vlan, 'address' => '10.0.0.3' });
}
is_deeply(\@commands,
[ "ip addr del 10\\.0\\.0\\.2/24 dev eth0\\.10 >/dev/null 2>&1",
"cd / ; ip addr add 10\\.0\\.0\\.3/24 dev eth0\\.10 2>&1" ],
"Linux active VLAN interface is updated without being recreated");
@commands = ( );
{
no warnings 'redefine';
no warnings 'once';
local $main::gconfig{'os_type'} = 'debian-linux';
local $main::gconfig{'os_version'} = 12;
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" :
$_[0] eq "ifup" ? "/sbin/ifup" : undef;
};
local *main::active_interfaces = sub {
return ( );
};
main::activate_interface({ %$vlan,
'ether' => '02:00:00:00:00:10',
'mtu' => '1400',
'address6' => [ '2001:db8::2' ],
'netmask6' => [ '64' ] });
}
is_deeply(\@commands,
[ "ip link add link eth0 name eth0\\.10 type vlan id 10 2>&1",
"ip link set dev eth0\\.10 up 2>&1",
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev eth0\\.10 2>&1",
"ip link set dev eth0\\.10 address 02\\:00\\:00\\:00\\:00\\:10 2>&1",
"ip link set dev eth0\\.10 mtu 1400 2>&1",
"ip -6 addr add 2001\\:db8\\:\\:2/64 dev eth0\\.10 2>&1" ],
"Linux active VLAN settings use the created device with ifup present");
@commands = ( );
{
no warnings 'redefine';
no warnings 'once';
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" : undef;
};
local *main::active_interfaces = sub {
return ({ 'name' => 'eth0.10',
'fullname' => 'eth0.10',
'virtual' => '',
'vlan' => 1,
'physical' => 'eth0',
'vlanid' => '10',
'address' => '10.0.0.2',
'netmask' => '255.255.255.0',
'address6' => [ ],
'netmask6' => [ ],
'up' => 1 });
};
main::activate_interface({ %$vlan, 'up' => 0 });
}
is_deeply(\@commands,
[ "ip link set dev eth0\\.10 down 2>&1" ],
"Linux active VLAN is brought down without running an empty command");
@commands = ( );
{
no warnings 'redefine';
no warnings 'once';
local $main::gconfig{'os_type'} = 'debian-linux';
local $main::gconfig{'os_version'} = 12;
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" :
$_[0] eq "ifup" ? "/sbin/ifup" : undef;
};
local *main::active_interfaces = sub {
return ( );
};
main::activate_interface({ %$vlan,
'ether' => '02:00:00:00:00:10',
'mtu' => '1400',
'address6' => [ '2001:db8::2' ],
'netmask6' => [ '64' ],
'up' => 0 });
}
is_deeply(\@commands, [ ],
"Linux absent disabled VLAN does not receive live link settings");
@commands = ( );
my @active_include_empty;
{
no warnings 'redefine';
no warnings 'once';
local $main::gconfig{'os_type'} = 'suse-linux';
local $main::gconfig{'os_version'} = 15;
local *main::has_command = sub {
return $_[0] eq "ifconfig" ? "/sbin/ifconfig" :
$_[0] eq "vconfig" ? "/sbin/vconfig" : undef;
};
local *main::active_interfaces = sub {
push(@active_include_empty, @_ ? $_[0] : undef);
return @_ && $_[0] ?
({ 'name' => 'eth0.10',
'fullname' => 'eth0.10',
'virtual' => '',
'address6' => [ ],
'netmask6' => [ ],
'up' => 0 }) : ( );
};
local *main::backquote_command = sub {
return "";
};
main::activate_interface({ %$vlan });
}
is_deeply(\@active_include_empty, [ 1 ],
"Linux activation checks for interfaces without IPv4");
is_deeply(\@commands,
[ "cd / ; ifconfig eth0.10 10\\.0\\.0\\.2 netmask 255\\.255\\.255\\.0 up 2>&1" ],
"Linux existing addressless VLAN is configured without being recreated");
@commands = ( );
{
no warnings 'redefine';
no warnings 'once';
local $main::gconfig{'os_type'} = 'suse-linux';
local $main::gconfig{'os_version'} = 15;
local *main::has_command = sub {
return $_[0] eq "ifconfig" ? "/sbin/ifconfig" :
$_[0] eq "vconfig" ? "/sbin/vconfig" : undef;
};
local *main::active_interfaces = sub {
return ( );
};
local *main::backquote_command = sub {
return "";
};
main::activate_interface({ %$vlan });
}
is_deeply(\@commands,
[ "vconfig add eth0 10 2>&1",
"cd / ; ifconfig eth0.10 10\\.0\\.0\\.2 netmask 255\\.255\\.255\\.0 up 2>&1" ],
"Linux VLAN interface falls back to vconfig without ip");
done_testing();