Files
webmin/net/linux-lib.pl
Ilia Ross 7ebe3f7dfa Add dhcpcd network backend for Debian and Raspberry Pi OS
This PR adds dhcpcd backend support for Debian and Raspberry Pi OS network configuration. It detects dhcpcd only as a final fallback after Netplan, NetworkManager, and ifupdown, preventing Webmin from incorrectly falling back to `/etc/network/interfaces` on dhcpcd-managed systems.

The new backend reads and writes `/etc/dhcpcd.conf`, including DHCP and static IPv4/IPv6 configuration, gateways, static routes, DNS servers, search domains, MTU, and virtual IPv4 aliases. It also supports implicit DHCP-managed interfaces for default dhcpcd setups with no explicit interface blocks, and handles `allowinterfaces` / `denyinterfaces` behavior.

This PR also fixes apply/delete flows for dhcpcd-managed interfaces and virtual aliases, avoids rewriting generated `/etc/resolv.conf`, preserves spacing/comments in touched hosts and nsswitch files, and tightens Active Now handling so virtual aliases are treated as IP addresses rather than independent links.

https://github.com/webmin/webmin/issues/1607
2026-06-20 01:57:50 +02:00

1034 lines
29 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+\.(\d+))@([^ \t\r\n\@]+\d+):/) {
# 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;
}
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 ($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); }
}
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("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 $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 $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($a->{'name'});
&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'});
}
if($a->{'vlan'} == 1) {
$cmd .= " dev ".quotemeta($a->{'physical'}).".".
quotemeta($a->{'vlanid'});
}
else {
$cmd .= " dev ".quotemeta($a->{'name'});
}
}
}
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 = &backquote_logged("cd / ; $cmd 2>&1");
if ($?) { &error("$cmd failed : $out"); }
# Apply ethernet address
if ($a->{'ether'} && !&use_ifup_command($a) && &has_command("ifconfig")) {
# With ifconfig command
$out = &backquote_logged("ifconfig ".quotemeta($a->{'name'}).
" hw ether ".quotemeta($a->{'ether'})." 2>&1");
if ($?) { &error($out); }
}
elsif ($a->{'ether'} && !&use_ifup_command($a) && &has_command("ip")) {
# With ip link command
$out = &backquote_logged("ip link set dev ".quotemeta($a->{'name'}).
" address ".quotemeta($a->{'ether'})." 2>&1");
if ($?) { &error($out); }
}
# Apply MTU
if ($a->{'mtu'} && !&use_ifup_command($a) && &has_command("ip")) {
$out = &backquote_logged(
"ip link set dev ".quotemeta($a->{'name'})." 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'}));
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 ".
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($a->{'name'})." 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($a->{'name'});
$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($a->{'name'});
$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 ($?);
}
# 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;