Files
webmin/net/linux-lib.pl
Ilia Ross 77610666cc
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
Fix live activation of Linux VLAN interfaces
ⓘ 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
2026-07-11 13:33:58 +02:00

1122 lines
32 KiB
Perl
Executable File

# linux-lib.pl
# Active interface functions for all flavours of linux
# active_interfaces([include-no-ipv4-address])
# Returns a list of currently ifconfig'd interfaces
sub active_interfaces
{
local ($empty) = @_;
my @rv;
my $ethtool = &has_command("ethtool");
if (&has_command("ip")) {
# Get status from new ip command
&clean_language();
&open_execute_command(IFC, "ip addr", 1, 1);
my @lines;
while(<IFC>) {
s/\r|\n//g;
if (/^\S+/) { push(@lines, $_); }
else { $lines[$#lines] .= $_; }
}
close(IFC);
&reset_environment();
foreach my $l (@lines) {
my %ifc;
my $iface_name = $ifc{'name'};
my $iface_auto_ip;
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{'physical'} = $3;
$ifc{'vlan'} = 1;
$ifc{'virtual'} = '';
}
elsif ($l =~ /^\d+:\s+([^ \t\r\n\@]+):/) {
# Line like :
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
$ifc{'fullname'} = $1;
$ifc{'name'} = $ifc{'fullname'}
}
elsif ($l =~ /^\d+:\s+([^ \t\r\n\@]+\d+)@([^ \t\r\n\@]+\d+):/) {
# Line like :
# 3: eth0@if0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
$ifc{'fullname'} = $1;
$ifc{'name'} = $ifc{'fullname'};
$ifc{'ifname'} = $2;
}
else {
# Unknown line!
next;
}
if ($l =~ /\sinet\s+([0-9\.]+)\s+peer\s+([0-9\.]+)\/(\d+)(\s+brd\s+([0-9\.]+))?\s+scope\s+global(\s+(noprefixroute|dynamic))*\s+(\Q$iface_name\E)/) {
# Line like :
# inet 193.9.101.120 peer 193.9.101.104/32 brd 193.9.101.120 scope global eth0
$ifc{'address'} = $1;
$ifc{'netmask'} = &prefix_to_mask("$3");
}
elsif ($l =~ /\sinet\s+([0-9\.]+)\/(\d+)\s+(?:metric\s+\d+\s+|)brd\s+(\S+)\s+scope\s+global(?:\s+secondary|)\s(dynamic)\s+.*?(\Q$iface_name\E)/) {
# Line like :
# inet 193.9.101.120/24 brd 193.9.101.255 scope global secondary dynamic br0
# inet 10.211.55.81/24 metric 100 brd 10.211.55.255 scope global secondary dynamic enp0s5
# inet 10.211.55.65/24 brd 10.211.55.255 scope global secondary dynamic noprefixroute enp0s5
# inet 10.211.55.10/24 brd 10.211.55.255 scope global dynamic enp0s5
$iface_auto_ip = $1;
$ifc{'address'} = $1;
$ifc{'netmask'} = &prefix_to_mask("$2");
}
elsif ($l =~ /\sinet\s+([0-9\.]+)\/(\d+)(\s+metric\s+\d+)?(\s+brd\s+(\S+))?\s+scope\s+global(\s+(noprefixroute|secondary))*\s+(\Q$iface_name\E)/) {
# Line like :
# inet 193.9.101.120/24 brd 193.9.101.255 scope global br0
$ifc{'address'} = $1;
$ifc{'netmask'} = &prefix_to_mask("$2");
}
elsif ($l =~ /\sinet\s+([0-9\.]+)\s+peer\s+([0-9\.]+)\/(\d+)/) {
# Line like :
# inet 46.4.13.87 peer 46.4.13.65/32
$ifc{'address'} = $1;
$ifc{'netmask'} = &prefix_to_mask("$3");
}
elsif ($l =~ /\sinet\s+([0-9\.]+)\/(\d+)(\s+brd\s+(\S+))?\s+scope\s+host\s+(\Q$iface_name\E)/) {
# Line like :
# inet 127.0.0.1/8 scope host lo
$ifc{'address'} = $1;
$ifc{'netmask'} = &prefix_to_mask("$2");
}
if ($l =~ /\sbrd\s+([0-9\.]+)/) {
$ifc{'broadcast'} = $1;
}
if ($l =~ /\smtu\s+(\d+)/) {
$ifc{'mtu'} = $1;
}
if ($l =~ /ether\s+([0-9a-f:]+)/i) {
$ifc{'ether'} = $1;
}
my (@address6, @netmask6, @scope6);
while($l =~ s/inet6\s+(\S+)\/(\d+)\s+scope\s+(\S+)//i) {
local ($address6, $netmask6, $scope6) = ($1, $2, $3);
push(@address6, $address6);
push(@netmask6, $netmask6);
push(@scope6, $scope6);
}
$ifc{'address6'} = \@address6;
$ifc{'netmask6'} = \@netmask6;
$ifc{'scope6'} = \@scope6;
$ifc{'up'}++ if ($l =~ /\sUP\s|<\S*UP\S*>/);
$ifc{'promisc'}++ if ($l =~ /\sPROMISC\s/);
$ifc{'edit'} = ($ifc{'name'} !~ /^ppp/);
$ifc{'index'} = scalar(@rv);
push(@rv, \%ifc);
# Strip off the line for the primary IP, but only if there is one
if ($ifc{'address'}) {
$l =~ s/\sinet\s+([0-9\.]+)\s+peer//;
if ($l =~ /\sinet\s+([0-9\.]+)\/(\d+)/) {
# If primary IP equals dynamic or no
# dynamic IP is found stirp it off
if (!$iface_auto_ip || $iface_auto_ip eq $1) {
$l =~ s/\sinet\s+([0-9\.]+)\/(\d+)//;
}
# Just strip off dynamic from the list
elsif ($iface_auto_ip) {
$l =~ s/\sinet\s+(\Q$iface_auto_ip\E)\/(\d+)//;
}
}
}
# Add extra IPs as fake virtual interfaces, from lines like
my $i = 0;
my $bn = $ifc{'name'};
while($l =~ s/\sinet\s+([0-9\.]+)\/(\d+).*?\s\Q$bn\E:(\d+)// ||
$l =~ s/\sinet\s+([0-9\.]+)\/(\d+).*?(?:secondary|dynamic|global).*?\s\Q$bn\E//) {
my %vifc;
my $vn = defined($3) ? $3 : $i;
$vifc{'name'} = $ifc{'name'};
$vifc{'fullname'} = $ifc{'name'}.":".$vn;
$vifc{'address'} = $1;
$vifc{'netmask'} = &prefix_to_mask("$2");
$vifc{'broadcast'} = &compute_broadcast(
$vifc{'address'}, $vifc{'netmask'});
$vifc{'mtu'} = $ifc{'mtu'};
$vifc{'up'} = $ifc{'up'};
$vifc{'virtual'} = $vn;
$vifc{'edit'} = ($vifc{'name'} !~ /^ppp/);
$vifc{'index'} = scalar(@rv);
push(@rv, \%vifc);
$i = $vn + 1;
}
}
}
elsif (&has_command("ifconfig")) {
&clean_language();
&open_execute_command(IFC, "ifconfig -a", 1, 1);
my @lines;
while(<IFC>) {
s/\r|\n//g;
if (/^\S+/) { push(@lines, $_); }
else { $lines[$#lines] .= $_; }
}
close(IFC);
&reset_environment();
foreach my $l (@lines) {
my %ifc;
$l =~ /^([^:\s]+)/ || next;
$ifc{'name'} = $1;
$l =~ /^(\S+)/;
$ifc{'fullname'} = $1;
$ifc{'fullname'} =~ s/:$//;
if ($l =~ /^(\S+):(\d+)/) { $ifc{'virtual'} = $2; }
if ($l =~ /^(\S+)\.(\d+)/) { $ifc{'vlanid'} = $2; }
if ($l =~ /inet addr:(\S+)/) { $ifc{'address'} = $1; }
elsif ($l =~ /inet (\S+)/) { $ifc{'address'} = $1; }
elsif (!$empty) { next; }
if ($l =~ /Mask:(\S+)/) { $ifc{'netmask'} = $1; }
elsif ($l =~ /netmask (\S+)/) { $ifc{'netmask'} = $1; }
if ($l =~ /Bcast:(\S+)/) { $ifc{'broadcast'} = $1; }
elsif ($l =~ /broadcast (\S+)/) { $ifc{'broadcast'} = $1; }
if ($l =~ /HWaddr (\S+)/) { $ifc{'ether'} = $1; }
elsif ($l =~ /ether (\S+)/) { $ifc{'ether'} = $1; }
if ($l =~ /MTU:(\d+)/) { $ifc{'mtu'} = $1; }
elsif ($l =~ /mtu (\d+)/) { $ifc{'mtu'} = $1; }
if ($l =~ /P-t-P:(\S+)/) { $ifc{'ptp'} = $1; }
elsif ($l =~ /ptp (\S+)/) { $ifc{'ptp'} = $1; }
$ifc{'up'}++ if ($l =~ /\sUP\s|<\S*UP\S*>/);
$ifc{'promisc'}++ if ($l =~ /\sPROMISC\s/);
my (@address6, @netmask6, @scope6);
while($l =~ s/inet6 addr:\s*(\S+)\/(\d+)\s+Scope:(Global)//i) {
local ($address6, $netmask6, $scope6) = ($1, $2, $3);
push(@address6, $address6);
push(@netmask6, $netmask6);
push(@scope6, $scope6);
}
while($l =~ s/inet6 (\S+)\s+prefixlen (\d+)\s+scopeid\s+(\S+)<global>//i) {
local ($address6, $netmask6, $scope6) = ($1, $2, $3);
push(@address6, $address6);
push(@netmask6, $netmask6);
push(@scope6, $scope6);
}
$ifc{'address6'} = \@address6;
$ifc{'netmask6'} = \@netmask6;
$ifc{'scope6'} = \@scope6;
$ifc{'edit'} = ($ifc{'name'} !~ /^ppp/);
$ifc{'index'} = scalar(@rv);
push(@rv, \%ifc);
}
}
else {
&error("Both the ifconfig and ip commands are missing");
}
foreach my $ifc (@rv) {
# For each ethernet interface, merge in data from ethtool
if (&iface_type($ifc->{'fullname'}) eq 'Ethernet' &&
$ifc->{'virtual'} eq '' && $ethtool) {
my $out = &backquote_command(
"$ethtool $ifc->{'fullname'} 2>/dev/null");
if ($out =~ /Speed:\s+(\S+)/i && $1 ne "Unknown!") {
$ifc->{'speed'} = $1;
}
if ($out =~ /Duplex:\s+(\S+)/i && $1 ne "Unknown!") {
$ifc->{'duplex'} = $1;
}
if ($out =~ /Link\s+detected:\s+(\S+)/i) {
$ifc->{'link'} = lc($1) eq 'yes' ? 1 : 0;
}
}
}
return @rv;
}
# activate_interface(&details)
# Create or modify an interface
sub activate_interface
{
my ($a) = @_;
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'}) {
# Linux virtual aliases are addresses, not independent links.
&deactivate_interface($old) if ($old && $old->{'address'});
return;
}
if (&has_command("ip") && $a->{'bond'} && $a->{'up'} && !$old) {
# Create the bond before assigning addresses to it.
my $bcmd = "ip link add ".quotemeta($a->{'name'})." type bond";
if (defined($a->{'mode'}) && $a->{'mode'} ne '') {
$bcmd .= " mode ".quotemeta(&bond_mode_name($a->{'mode'}));
}
if ($a->{'miimon'}) {
$bcmd .= " miimon ".quotemeta($a->{'miimon'});
}
if ($a->{'updelay'}) {
$bcmd .= " updelay ".quotemeta($a->{'updelay'});
}
if ($a->{'downdelay'}) {
$bcmd .= " downdelay ".quotemeta($a->{'downdelay'});
}
my $out = &backquote_logged("$bcmd 2>&1");
&error("Failed to create bond device : $out") if ($?);
foreach my $slave (grep { $_ ne '' } split(/\s+/, $a->{'partner'})) {
$bcmd = "ip link set dev ".quotemeta($slave)." down";
$out = &backquote_logged("$bcmd 2>&1");
&error("Failed to bring down bond slave : $out") if ($?);
$bcmd = "ip link set dev ".quotemeta($slave)." master ".
quotemeta($a->{'name'});
$out = &backquote_logged("$bcmd 2>&1");
&error("Failed to add bond slave : $out") if ($?);
$bcmd = "ip link set dev ".quotemeta($slave)." up";
$out = &backquote_logged("$bcmd 2>&1");
&error("Failed to bring up bond slave : $out") if ($?);
}
if ($a->{'primary'}) {
$bcmd = "ip link set dev ".quotemeta($a->{'name'}).
" type bond primary ".quotemeta($a->{'primary'});
$out = &backquote_logged("$bcmd 2>&1");
&error("Failed to set bond primary interface : $out") if ($?);
}
}
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($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($devname)." down";
my $out = &backquote_logged("$cmd 2>&1");
&error("Failed to bring down link : $out") if ($?);
}
}
my $cmd;
if (&has_command("ip")) {
# If the IP is changing, first remove it then re-add
my $readd = 0;
if ($old && $old->{'address'}) {
if ($old->{'address'} ne $a->{'address'} ||
$old->{'netmask'} ne $a->{'netmask'}) {
my $rcmd = "ip addr del ".quotemeta($old->{'address'}).
"/".&mask_to_prefix($old->{'netmask'}).
" dev ".quotemeta($devname);
&system_logged("$rcmd >/dev/null 2>&1");
$readd = 1;
}
}
else {
$readd = 1;
}
# Build ip command to add the new IP
if ($readd && $a->{'address'}) {
$cmd .= "ip addr add ".quotemeta($a->{'address'});
if ($a->{'netmask'}) {
$cmd .= "/".&mask_to_prefix($a->{'netmask'});
}
if ($a->{'broadcast'}) {
$cmd .= " broadcast ".quotemeta($a->{'broadcast'});
}
$cmd .= " dev ".quotemeta($devname);
}
}
elsif (&use_ifup_command($a)) {
# Use Debian / Redhat ifup command
if($a->{'vlan'} == 1) {
# Name and fullname for VLAN tagged interfaces are "auto" so
# we need to ifup using physical and vlanid.
if ($a->{'up'}) {
if(($a->{'mtu'}) && (($gconfig{'os_type'} eq 'redhat-linux') && ($gconfig{'os_version'} >= 13))) {
my $cmd2 = "ifconfig ".quotemeta($a->{'physical'})." mtu ".quotemeta($a->{'mtu'});
my $out = &backquote_logged("$cmd2 2>&1");
if ($?) { &error($out); }
}
$cmd .= "ifup ".quotemeta($a->{'physical'}).".".
quotemeta($a->{'vlanid'});
}
else {
$cmd .= "ifdown ".quotemeta($a->{'physical'}).".".
quotemeta($a->{'vlanid'});
}
}
elsif ($a->{'up'}) {
$cmd .= "ifdown ".quotemeta($a->{'fullname'}).
"; ifup ".quotemeta($a->{'fullname'});
}
else {
$cmd .= "ifdown ".quotemeta($a->{'fullname'});
}
}
elsif (&has_command("ifconfig")) {
# Build ifconfig command manually
if($a->{'vlan'} == 1) {
$cmd .= "ifconfig ".quotemeta($a->{'physical'}).".".
quotemeta($a->{'vlanid'});
}
else {
$cmd .= "ifconfig ".quotemeta($a->{'name'});
if ($a->{'virtual'} ne "") {
$cmd .= ":".quotemeta($a->{'virtual'});
}
}
$cmd .= " ".quotemeta($a->{'address'});
if ($a->{'netmask'}) {
$cmd .= " netmask ".quotemeta($a->{'netmask'});
}
if ($a->{'broadcast'}) {
$cmd .= " broadcast ".quotemeta($a->{'broadcast'});
}
if ($a->{'mtu'} && $a->{'virtual'} eq "") {
$cmd .= " mtu ".quotemeta($a->{'mtu'});
}
if ($a->{'up'}) {
$cmd .= " up";
}
else {
$cmd .= " down";
}
}
else {
&error("Both the ifconfig and ip commands are missing");
}
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'} && $apply_link_settings && &has_command("ifconfig")) {
# With ifconfig command
$out = &backquote_logged("ifconfig ".quotemeta($devname).
" hw ether ".quotemeta($a->{'ether'})." 2>&1");
if ($?) { &error($out); }
}
elsif ($a->{'ether'} && $apply_link_settings && &has_command("ip")) {
# With ip link command
$out = &backquote_logged("ip link set dev ".quotemeta($devname).
" address ".quotemeta($a->{'ether'})." 2>&1");
if ($?) { &error($out); }
}
# Apply MTU
if ($a->{'mtu'} && $apply_link_settings && &has_command("ip")) {
$out = &backquote_logged(
"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($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($devname)." inet6 del ".
quotemeta("$1/$2")." 2>&1";
$out = &backquote_logged($cmd);
&error("Failed to remove old IPv6 address : $out") if ($?);
}
# Add IPv6 addresses
for(my $i=0; $i<@{$a->{'address6'}}; $i++) {
my $cmd = "ifconfig ".quotemeta($devname)." inet6 add ".
quotemeta($a->{'address6'}->[$i])."/".
quotemeta($a->{'netmask6'}->[$i])." 2>&1";
$out = &backquote_logged($cmd);
&error("Failed to add IPv6 address with $cmd : $out") if ($?);
}
}
elsif ($a->{'virtual'} eq '' && &has_command("ip")) {
# Remove old IPv6 addresses
if ($old) {
for(my $i=0; $i<@{$old->{'address6'}}; $i++) {
my $cmd = "ip -6 addr del ".
quotemeta($old->{'address6'}->[$i])."/".
quotemeta($old->{'netmask6'}->[$i])." dev ".
quotemeta($devname);
$out = &backquote_logged("$cmd 2>&1");
&error("Failed to remove old IPv6 address : $out") if ($?);
}
}
# Add IPv6 addresses
for(my $i=0; $i<@{$a->{'address6'}}; $i++) {
my $cmd = "ip -6 addr add ".
quotemeta($a->{'address6'}->[$i])."/".
quotemeta($a->{'netmask6'}->[$i])." dev ".
quotemeta($devname);
$out = &backquote_logged("$cmd 2>&1");
&error("Failed to add IPv6 address with $cmd : $out") if ($?);
}
}
}
# deactivate_interface(&details)
# Shutdown some active interface
sub deactivate_interface
{
my ($a) = @_;
if (&has_command("ip")) {
# Use new ip command to remove all IPs
my @del;
if ($a->{'address'}) {
push(@del, $a->{'address'}."/".
&mask_to_prefix($a->{'netmask'}));
}
for(my $i=0; $i<@{$a->{'address6'}}; $i++) {
push(@del, $a->{'address6'}->[$i]."/".
$a->{'netmask6'}->[$i]);
}
foreach my $d (@del) {
my $cmd = "ip addr del ".quotemeta($d)." dev ".
quotemeta($a->{'name'});
my $out = &backquote_logged("$cmd 2>&1");
&error("Failed to remove old address : $out") if ($?);
}
if ($a->{'virtual'} eq '') {
my $cmd = "ip link set dev ".quotemeta($a->{'name'})." down";
my $out = &backquote_logged("$cmd 2>&1");
&error("<pre>".&html_escape($out)."</pre>") if ($?);
}
}
elsif (&has_command("ifconfig")) {
# Use old ifconfig command
my $name = $a->{'name'}.
($a->{'virtual'} ne "" ? ":$a->{'virtual'}" : "");
my $address = $a->{'address'}.
($a->{'virtual'} ne "" ? ":$a->{'virtual'}" : "");
my $netmask = $a->{'netmask'};
if ($a->{'virtual'} ne "") {
# Shutdown virtual interface by setting address to 0
&system_logged("ifconfig $name 0 >/dev/null 2>&1");
}
# Delete all v6 addresses
for(my $i=0; $i<@{$a->{'address6'}}; $i++) {
my $cmd = "ifconfig ".quotemeta($a->{'name'})." inet6 del ".
quotemeta($a->{'address6'}->[$i])."/".
quotemeta($a->{'netmask6'}->[$i]);
&backquote_logged("$cmd 2>&1");
}
# Check if still up somehow
my ($still) = grep { $_->{'fullname'} eq $name } &active_interfaces();
if ($still) {
# Old version of ifconfig or non-virtual interface.. down it
my $out;
if (&use_ifup_command($a)) {
$out = &backquote_logged(
"ifdown ".quotemeta($name)." 2>&1");
}
else {
$out = &backquote_logged(
"ifconfig ".quotemeta($name)." down 2>&1");
}
my ($still) = grep { $_->{'fullname'} eq $name }
&active_interfaces();
if ($still && $still->{'up'}) {
&error($out ? "<pre>".&html_escape($out)."</pre>"
: "Interface is still active even after ".
"being shut down");
}
if (&iface_type($name) =~ /^(.*) (VLAN)$/) {
$out = &backquote_logged(
"vconfig rem ".quotemeta($name)." 2>&1");
}
}
}
else {
&error("Both the ifconfig and ip commands are missing");
}
}
# use_ifup_command(&iface)
# Returns 1 if the ifup command must be used to bring up some interface.
# True on Debian 5.0+ for non-ethernet, typically bonding and VLAN tagged interfaces.
sub use_ifup_command
{
local ($iface) = @_;
return &has_command("ifup") &&
($gconfig{'os_type'} eq 'debian-linux' &&
$gconfig{'os_version'} >= 5 ||
$gconfig{'os_type'} eq 'redhat-linux' &&
$gconfig{'os_version'} >= 13) &&
($iface->{'name'} !~ /^(eth|em|eno|ens|enp|enx|enX|lo|br)/ ||
$iface->{'name'} =~ /^(\S+)\.(\d+)/) &&
$iface->{'virtual'} eq '';
}
# iface_type(name)
# Returns a human-readable interface type name
sub iface_type
{
my ($name) = @_;
if ($name =~ /^(.*)\.(\d+)$/) {
return &iface_type("$1")." VLAN";
}
return "PPP" if ($name =~ /^ppp/);
return "SLIP" if ($name =~ /^sl/);
return "PLIP" if ($name =~ /^plip/);
return "Ethernet" if ($name =~ /^eth|em|eno|ens|enp|enx|enX|p\d+p\d+|vtnet/);
return "Wireless Ethernet" if ($name =~ /^(wlan|ath)/);
return "Arcnet" if ($name =~ /^arc/);
return "Token Ring" if ($name =~ /^tr/);
return "Pocket/ATP" if ($name =~ /^atp/);
return "Loopback" if ($name =~ /^lo/);
return "ISDN rawIP" if ($name =~ /^isdn/);
return "ISDN syncPPP" if ($name =~ /^ippp/);
return "CIPE" if ($name =~ /^cip/);
return "VmWare" if ($name =~ /^vmnet/);
return "Wireless" if ($name =~ /^wlan/);
return "Bonded" if ($name =~ /^bond/);
return "OpenVZ" if ($name =~ /^venet/);
return "Bridge" if ($name =~ /^(br|xenbr|virbr)/);
return $text{'ifcs_unknown'};
}
# list_routes()
# Returns a list of active routes
sub list_routes
{
local @rv;
if (&has_command("netstat")) {
# Use old netstat command if installed
&open_execute_command(ROUTES, "netstat -rn", 1, 1);
while(<ROUTES>) {
s/\s+$//;
if (/^([0-9\.]+)\s+([0-9\.]+)\s+([0-9\.]+)\s+\S+\s+\S+\s+\S+\s*\S+\s+(\S+)$/) {
push(@rv, { 'dest' => $1,
'gateway' => $2,
'netmask' => $3,
'iface' => $4 });
}
}
close(ROUTES);
&open_execute_command(ROUTES, "netstat -rn -A inet6", 1, 1);
while(<ROUTES>) {
s/\s+$//;
if (/^([0-9a-z:]+)\/([0-9]+)\s+([0-9a-z:]+)\s+\S+\s+\S+\s+\S+\s*\S+\s+(\S+)$/) {
push(@rv, { 'dest' => $1,
'gateway' => $3,
'netmask' => $2,
'iface' => $4 });
}
}
close(ROUTES);
}
elsif (&has_command("ip")) {
# Use new ip command
&open_execute_command(ROUTES, "ip route", 1, 1);
while(<ROUTES>) {
s/\r|\n//g;
if (/^(\S+)\s+/) {
my $r = { 'dest' => $1 };
if ($r->{'dest'} =~ /^(\S+)\/(\d+)/) {
$r->{'dest'} = $1;
$r->{'netmask'} = &prefix_to_mask("$2");
}
if (/\sdev\s+(\S+)/) {
$r->{'iface'} = $1;
}
if (/\svia\s+(\S+)/) {
$r->{'gateway'} = $1;
}
push(@rv, $r);
}
}
close(ROUTES);
}
else {
&error("Both the ip and netstat commands are missing!");
}
return @rv;
}
# load_module(&details)
# Load or modify a loaded module
sub load_module
{
local $a = $_[0];
local $cmd = "modprobe bonding";
if ($a->{'mode'}) {
$cmd .= " mode=".quotemeta($a->{'mode'});
}
if ($a->{'miimon'}) {
$cmd .= " miimon=".quotemeta($a->{'miimon'});
}
if ($a->{'downdelay'}) {
$cmd .= " downdelay=".quotemeta($a->{'downdelay'});
}
if ($a->{'updelay'}) {
$cmd .= " updelay=".quotemeta($a->{'updelay'});
}
local $out = &backquote_logged("$cmd 2>&1");
&error($out) if ($?);
}
# bond_mode_name(mode)
# Convert Webmin's numeric bonding mode to the name expected by ip(8).
sub bond_mode_name
{
my ($mode) = @_;
my @modes = ("balance-rr", "active-backup", "balance-xor", "broadcast",
"802.3ad", "balance-tlb", "balance-alb");
if ($mode =~ /^\d+$/ && defined($modes[$mode])) {
return $modes[$mode];
}
return $mode eq "activebackup" ? "active-backup" : $mode;
}
# Tries to unload the module
# unload_module(name)
sub unload_module
{
my ($name) = @_;
my $cmd = "modprobe -r bonding";
local $out = &backquote_logged("$cmd 2>&1");
&error($out) if ($?);
}
# list_interfaces()
# Returns a list of active interface names
sub list_interfaces
{
my @ret;
$cmd = "ifconfig -a";
local $out = &backquote_logged("$cmd 2>&1");
&error($out) if ($?);
my @lines = split("\n", $out);
foreach my $line (@lines) {
$line =~ /^([\w|.]*)/m;
if ($1) {
push(@ret, $1);
}
}
return @ret;
}
# delete_route(&route)
# Delete one active route, as returned by list_routes. Returns an error message
# on failure, or undef on success
sub delete_route
{
my ($route) = @_;
my $cmd;
my $proto = &check_ip6address($route->{'dest'}) ||
$route->{'dest'} eq '::' ? 6 : 4;
if (&has_command("route")) {
# Use old route command
$cmd = "route ".
($proto == 6 ? "-A inet6" : "-A inet")." del ";
if (!$route->{'dest'} || $route->{'dest'} eq '0.0.0.0' ||
$route->{'dest'} eq '::') {
$cmd .= " default";
}
elsif ($route->{'netmask'} eq '255.255.255.255') {
$cmd .= " -host ".quotemeta($route->{'dest'});
}
elsif (!&check_ip6address($route->{'dest'})) {
$cmd .= " -net ".quotemeta($route->{'dest'});
if ($route->{'netmask'} && $route->{'netmask'} ne '0.0.0.0') {
$cmd .= " netmask ".quotemeta($route->{'netmask'});
}
}
else {
$cmd .= quotemeta("$route->{'dest'}/$route->{'netmask'}");
}
if ($route->{'gateway'}) {
$cmd .= " gw ".quotemeta($route->{'gateway'});
}
elsif ($route->{'iface'}) {
$cmd .= " dev ".quotemeta($route->{'iface'});
}
}
elsif (&has_command("ip")) {
# Use new ip command
$cmd = "ip -".$proto." route delete";
if (!$route->{'dest'} || $route->{'dest'} eq '0.0.0.0' ||
$route->{'dest'} eq '::') {
$cmd .= " default";
}
else {
$cmd .= " ".quotemeta($route->{'dest'});
if ($route->{'netmask'} && $route->{'netmask'} ne '0.0.0.0' &&
$route->{'netmask'} != 32) {
if ($route->{'netmask'} =~ /^\d+$/) {
$cmd .= "/".quotemeta($route->{'netmask'});
}
else {
$cmd .= "/".&mask_to_prefix($route->{'netmask'});
}
}
}
}
else {
return "Missing the route and ip commands";
}
my $out = &backquote_logged("$cmd 2>&1 </dev/null");
return $? ? $out : undef;
}
# create_route(&route)
# Adds a new active route
sub create_route
{
local ($route) = @_;
my $cmd;
my $proto = &check_ip6address($route->{'dest'}) ||
&check_ip6address($route->{'gateway'}) ? 6 : 4;
if (&has_command("route")) {
# Use old route command
$cmd = "route ".($proto == 6 ? "-A inet6" : "-A inet")." add";
if (!$route->{'dest'} || $route->{'dest'} eq '0.0.0.0' ||
$route->{'dest'} eq '::') {
$cmd .= " default";
}
elsif ($route->{'netmask'} eq '255.255.255.255') {
$cmd .= " -host ".quotemeta($route->{'dest'});
}
elsif (!&check_ip6address($route->{'dest'})) {
$cmd .= " -net ".quotemeta($route->{'dest'});
if ($route->{'netmask'} && $route->{'netmask'} ne '0.0.0.0') {
$cmd .= " netmask ".quotemeta($route->{'netmask'});
}
}
else {
$cmd .= quotemeta("$route->{'dest'}/$route->{'netmask'}");
}
if ($route->{'gateway'}) {
$cmd .= " gw ".quotemeta($route->{'gateway'});
}
elsif ($route->{'iface'}) {
$cmd .= " dev ".quotemeta($route->{'iface'});
}
}
elsif (&has_command("ip")) {
# Use new ip command
$cmd = "ip -".$proto." route add";
if (!$route->{'dest'} || $route->{'dest'} eq '0.0.0.0' ||
$route->{'dest'} eq '::') {
$cmd .= " default";
}
else {
$cmd .= " ".quotemeta($route->{'dest'});
if ($route->{'netmask'} && $route->{'netmask'} ne '0.0.0.0' &&
$route->{'netmask'} != 32) {
if ($route->{'netmask'} =~ /^\d+$/) {
$cmd .= "/".quotemeta($route->{'netmask'});
}
else {
$cmd .= "/".&mask_to_prefix($route->{'netmask'});
}
}
}
if ($route->{'gateway'}) {
$cmd .= " via ".quotemeta($route->{'gateway'});
}
if ($route->{'iface'}) {
$cmd .= " dev ".quotemeta($route->{'iface'});
}
}
else {
return "Missing the route and ip commands";
}
my $out = &backquote_logged("$cmd 2>&1 </dev/null");
return $? ? $out : undef;
}
# iface_hardware(name)
# Does some interface have an editable hardware address
sub iface_hardware
{
return $_[0] =~ /^(eth|em|eno|ens|enp|enx|enX)/;
}
# allow_interface_clash()
# Returns 0 to indicate that two virtual interfaces with the same IP
# are not allowed
sub allow_interface_clash
{
return 0;
}
# get_dns_config()
# Returns a hashtable containing keys nameserver, domain, search & order
# XXX factor out to os_ functions
sub get_dns_config
{
local $dns = { };
local $rc;
local $dnsfile;
if ($use_suse_dns && ($rc = &parse_rc_config()) && $rc->{'NAMESERVER'}) {
# Special case - get DNS settings from SuSE config
local @ns = split(/\s+/, $rc->{'NAMESERVER'}->{'value'});
$dns->{'nameserver'} = [ grep { $_ ne "YAST_ASK" } @ns ];
local $src = $rc->{'SEARCHLIST'};
$dns->{'domain'} = [ split(/\s+/, $src->{'value'}) ] if ($src);
$dnsfile = $rc_config;
}
elsif ($gconfig{'os_type'} eq 'debian-linux' && -l "/etc/resolv.conf" &&
$netplan_dir) {
# On Ubuntu 18+, /etc/resolv.conf is auto-generated from netplan config
my @boot = &boot_interfaces();
foreach my $b (@boot) {
if ($b->{'nameserver'}) {
$dns->{'nameserver'} = $b->{'nameserver'};
$dns->{'domain'} = $b->{'search'};
$dnsfile = $b->{'file'};
last;
}
}
}
elsif ($gconfig{'os_type'} eq 'debian-linux' && -l "/etc/resolv.conf") {
# On Ubuntu 12+, /etc/resolv.conf is auto-generated from network
# interface config
my @ifaces = &get_interface_defs();
foreach my $i (@ifaces) {
local ($ns) = grep { $_->[0] eq 'dns-nameservers' } @{$i->[3]};
local @dom = grep { $_->[0] eq 'dns-domain' ||
$_->[0] eq 'dns-search' } @{$i->[3]};
if ($ns) {
$dns->{'nameserver'} = [ split(/\s+/, $ns->[1]) ];
if (@dom) {
$dns->{'domain'} =
[ map { split(/\s+/, $_->[1]) } @dom ];
}
$dnsfile = "/etc/network/interfaces";
last;
}
}
}
if (!$dnsfile) {
# Just read resolv.conf
&open_readfile(RESOLV, "/etc/resolv.conf");
while(<RESOLV>) {
s/\r|\n//g;
s/#.*$//;
s/;.*$//;
if (/nameserver\s+(.*)/) {
push(@{$dns->{'nameserver'}}, split(/\s+/, $1));
}
elsif (/domain\s+(\S+)/) {
$dns->{'domain'} = [ $1 ];
}
elsif (/search\s+(.*)/) {
$dns->{'domain'} = [ split(/\s+/, $1) ];
}
}
close(RESOLV);
$dnsfile = "/etc/resolv.conf";
}
&open_readfile(SWITCH, "/etc/nsswitch.conf");
while(<SWITCH>) {
s/\r|\n//g;
if (/^\s*hosts:\s+(.*)/) {
$dns->{'order'} = $1;
}
}
close(SWITCH);
$dns->{'files'} = [ $dnsfile, "/etc/nsswitch.conf" ];
return $dns;
}
# save_dns_config(&config)
# Writes out the resolv.conf and nsswitch.conf files
sub save_dns_config
{
local ($conf) = @_;
local ($need_apply, $generated_resolv) = (0, 0);
# Call OS-specific function to update resolvers elsewhere
if (defined(&os_save_dns_config)) {
($need_apply, $generated_resolv) = &os_save_dns_config($conf);
}
if (!$generated_resolv) {
# Update standard resolv.conf file
&lock_file("/etc/resolv.conf");
&open_readfile(RESOLV, "/etc/resolv.conf");
local @resolv = <RESOLV>;
close(RESOLV);
&open_tempfile(RESOLV, ">/etc/resolv.conf");
foreach (@{$conf->{'nameserver'}}) {
&print_tempfile(RESOLV, "nameserver $_\n");
}
if ($conf->{'domain'}) {
if ($conf->{'domain'}->[1]) {
&print_tempfile(RESOLV,
"search ",join(" ", @{$conf->{'domain'}}),"\n");
}
else {
&print_tempfile(RESOLV,
"domain $conf->{'domain'}->[0]\n");
}
}
foreach (@resolv) {
&print_tempfile(RESOLV, $_)
if (!/^\s*(nameserver|domain|search)\s+/);
}
&close_tempfile(RESOLV);
&unlock_file("/etc/resolv.conf");
}
# Update resolution order in nsswitch.conf
&lock_file("/etc/nsswitch.conf");
&open_readfile(SWITCH, "/etc/nsswitch.conf");
local @switch = <SWITCH>;
close(SWITCH);
&open_tempfile(SWITCH, ">/etc/nsswitch.conf");
foreach (@switch) {
if (/^\s*hosts:\s+/) {
&print_tempfile(SWITCH,
&linux_nsswitch_hosts_line($_, $conf->{'order'}));
}
else {
&print_tempfile(SWITCH, $_);
}
}
&close_tempfile(SWITCH);
&unlock_file("/etc/nsswitch.conf");
# Update SuSE config file for resolution order
if ($use_suse_dns) {
&lock_file($rc_config);
local $rc = &parse_rc_config();
if ($rc->{'USE_NIS_FOR_RESOLVING'}) {
if ($conf->{'order'} =~ /nis/) {
&save_rc_config($rc, "USE_NIS_FOR_RESOLVING", "yes");
}
else {
&save_rc_config($rc, "USE_NIS_FOR_RESOLVING", "no");
}
}
&unlock_file($rc_config);
}
# Update resolv.conf from network interfaces config
if ($need_apply) {
my $err = &apply_network();
&error("<pre>".&html_escape($err)."</pre>") if ($err);
}
}
$max_dns_servers = 3;
# order_input(&dns)
# Returns HTML for selecting the name resolution order
sub order_input
{
my @o = split(/\s+/, $_[0]->{'order'});
@o = map { s/nis\+/nisplus/; s/yp/nis/; $_; } @o;
my @opts = ( [ "files", "Hosts file" ], [ "dns", "DNS" ], [ "nis", "NIS" ],
[ "nisplus", "NIS+" ], [ "ldap", "LDAP" ], [ "db", "DB" ],
[ "mdns4", "Multicast DNS" ], [ "myhostname", "Local hostname" ] );
if (&indexof("mdns4_minimal", @o) >= 0) {
push(@opts, [ "mdns4_minimal", "Multicast DNS (minimal)" ]);
}
return &common_order_input("order", join(" ", @o), \@opts);
}
# parse_order(&dns)
# Parses the form created by order_input()
sub parse_order
{
if (defined($in{'order'})) {
$in{'order'} =~ /\S/ || &error($text{'dns_eorder'});
$_[0]->{'order'} = $in{'order'};
}
else {
local($i, @order);
for($i=0; defined($in{"order_$i"}); $i++) {
push(@order, $in{"order_$i"}) if ($in{"order_$i"});
}
$_[0]->{'order'} = join(" ", @order);
}
}
# linux_nsswitch_hosts_line(line, order)
# Returns an updated nsswitch hosts line preserving existing spacing
sub linux_nsswitch_hosts_line
{
my ($line, $order) = @_;
$line =~ s/\r?\n$//;
my $comment = "";
if ($line =~ s/(\s+#.*)$//) {
# Keep inline comments while replacing only the lookup order.
$comment = $1;
}
return $1.$2.$order.$comment."\n"
if ($line =~ /^(\s*hosts:)(\s+)\S/);
return "hosts:\t$order$comment\n";
}
1;