Files
webmin/net/dhcpcd-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

1005 lines
29 KiB
Perl

# Networking functions for dhcpcd
$dhcpcd_config = "/etc/dhcpcd.conf";
$sysctl_config = "/etc/sysctl.conf";
do 'linux-lib.pl';
# boot_interfaces()
# Returns a list of all interfaces configured in dhcpcd.conf
sub boot_interfaces
{
my $cfg = &read_dhcpcd_config($dhcpcd_config);
my @rv;
foreach my $block (@$cfg) {
# Only interface blocks map to Webmin boot-time interfaces.
next if ($block->{'type'} ne 'interface');
my $iface = { 'name' => $block->{'name'},
'fullname' => $block->{'name'},
'virtual' => '',
'file' => $dhcpcd_config,
'block' => $block,
'edit' => 1,
'up' => 1,
'address6' => [ ],
'netmask6' => [ ] };
# The first static ip_address is the primary IPv4 address.
my @ip4 = &dhcpcd_values($block, "ip_address");
if (@ip4) {
my ($addr, $prefix) = split(/\//, $ip4[0]);
$iface->{'address'} = $addr;
$iface->{'netmask'} = $prefix ? &prefix_to_mask($prefix) : undef;
# Extra static ip_address directives become Webmin virtual aliases.
for(my $i=1; $i<@ip4; $i++) {
my ($vaddr, $vprefix) = split(/\//, $ip4[$i]);
push(@rv, { 'name' => $iface->{'name'},
'fullname' => $iface->{'name'}.":".($i-1),
'virtual' => $i-1,
'file' => $dhcpcd_config,
'block' => $block,
'edit' => 1,
'up' => 1,
'address' => $vaddr,
'netmask' => $vprefix ?
&prefix_to_mask($vprefix) : undef });
}
}
else {
# dhcpcd uses DHCP when no static ip_address is configured.
$iface->{'dhcp'} = 1;
}
# Parse gateways and static IPv4 routes from dhcpcd's static keys.
my ($routers) = &dhcpcd_values($block, "routers");
($iface->{'gateway'}) = split(/\s+/, $routers) if ($routers);
my ($routes) = &dhcpcd_values($block, "routes");
if ($routes) {
my @r = split(/\s+/, $routes);
while(@r >= 2) {
my $dest = shift(@r);
my $gw = shift(@r);
push(@{$iface->{'routes'}}, "$dest,$gw");
}
}
# DNS servers and search domains live inside the interface block.
my ($ns) = &dhcpcd_values($block, "domain_name_servers");
$iface->{'nameserver'} = [ split(/\s+/, $ns) ] if ($ns);
my ($search) = &dhcpcd_values($block, "domain_search");
$search ||= (&dhcpcd_values($block, "domain_name"))[0];
$iface->{'search'} = [ split(/\s+/, $search) ] if ($search);
# IPv6 addresses and router settings use separate dhcpcd directives.
foreach my $ip6 (&dhcpcd_values($block, "ip6_address")) {
my ($addr, $prefix) = split(/\//, $ip6);
push(@{$iface->{'address6'}}, $addr);
push(@{$iface->{'netmask6'}}, $prefix || 64);
}
my ($routers6) = &dhcpcd_values($block, "ip6_routers");
($iface->{'gateway6'}) = split(/\s+/, $routers6) if ($routers6);
my ($mtu) = &dhcpcd_plain_values($block, "mtu");
$iface->{'mtu'} = $mtu if ($mtu);
push(@rv, $iface);
}
# Default dhcpcd setups may manage DHCP interfaces without explicit blocks.
push(@rv, &dhcpcd_implicit_interfaces($cfg, \@rv));
@rv = sort { $a->{'fullname'} cmp $b->{'fullname'} } @rv;
for(my $i=0; $i<@rv; $i++) {
$rv[$i]->{'index'} = $i;
}
return @rv;
}
# save_interface(&iface, [&all-interfaces])
# Create or update a dhcpcd interface block
sub save_interface
{
my ($iface, $boot) = @_;
$boot ||= [ &boot_interfaces() ];
if ($iface->{'virtual'} ne '') {
# Virtual aliases are stored as extra ip_address lines on the parent.
my ($parent) = grep { $_->{'fullname'} eq $iface->{'name'} } @$boot;
$parent || &error("No interface named $iface->{'name'} exists");
if (!$iface->{'block'}) {
push(@$boot, $iface);
}
else {
# Replace the existing alias in the in-memory boot list.
my ($oldiface) = grep { $_->{'fullname'} eq $iface->{'fullname'} } @$boot;
$oldiface || &error("No existing interface named $iface->{'fullname'} found");
$boot->[$oldiface->{'index'}] = $iface;
}
&save_interface($parent, $boot);
return;
}
my $cfg = &read_dhcpcd_config($dhcpcd_config);
my ($old) = grep { $_->{'type'} eq 'interface' &&
$_->{'name'} eq $iface->{'fullname'} } @$cfg;
my @lines = &dhcpcd_interface_lines($iface, $boot);
&lock_file($dhcpcd_config);
my $lref = &read_file_lines($dhcpcd_config);
if ($old) {
# Replace only the old interface block, preserving file-level comments.
splice(@$lref, $old->{'line'}, $old->{'eline'} - $old->{'line'} + 1,
@lines);
}
else {
# Append a new explicit block, used for first edits to implicit DHCP rows.
push(@$lref, "") if (@$lref && $lref->[@$lref-1] =~ /\S/);
push(@$lref, @lines);
}
# Saving an interface means dhcpcd should no longer deny managing it.
&dhcpcd_delete_global_word($lref, "denyinterfaces", $iface->{'fullname'});
# If allowinterfaces is present, the saved interface must be included there.
&dhcpcd_add_global_word_if_exists($lref, "allowinterfaces",
$iface->{'fullname'});
&flush_file_lines($dhcpcd_config);
&unlock_file($dhcpcd_config);
}
# delete_interface(&iface)
# Remove a dhcpcd interface block, or one virtual address from it
sub delete_interface
{
my ($iface) = @_;
if ($iface->{'virtual'} ne '') {
# Removing an alias means re-saving the parent without that address.
my @boot = grep { $_->{'fullname'} ne $iface->{'fullname'} }
&boot_interfaces();
my ($parent) = grep { $_->{'fullname'} eq $iface->{'name'} } @boot;
$parent || &error("No interface named $iface->{'name'} exists");
&save_interface($parent, \@boot);
return;
}
&lock_file($dhcpcd_config);
my $lref = &read_file_lines($dhcpcd_config);
my $block = $iface->{'block'};
if ($block) {
# Explicit blocks can be removed directly.
splice(@$lref, $block->{'line'}, $block->{'eline'} - $block->{'line'} + 1);
# A deleted explicit block should be gone, not replaced by a deny.
&dhcpcd_delete_global_word($lref, "denyinterfaces",
$iface->{'fullname'});
}
else {
# Implicit DHCP interfaces need a deny line, or they reappear.
&dhcpcd_add_global_word($lref, "denyinterfaces",
$iface->{'fullname'});
}
&flush_file_lines($dhcpcd_config);
&unlock_file($dhcpcd_config);
}
# can_edit(setting, [&iface])
# Returns true if a boot-time interface setting can be edited
sub can_edit
{
my ($what) = @_;
return $what !~ /^(up|bootp|broadcast|ether|bridgestp|bridgefd|bridgewait)$/;
}
# can_broadcast_def()
# Returns true if broadcast address can be left to the OS
sub can_broadcast_def
{
return 1;
}
# valid_boot_address(address)
# Returns true if an IPv4 or IPv6 boot-time address is valid
sub valid_boot_address
{
return &check_ipaddress_any($_[0]);
}
# supports_address6([&iface])
# Returns true if this backend supports IPv6 addresses
sub supports_address6
{
return 1;
}
# supports_no_address([&iface])
# Returns true if this backend supports explicit no-address interfaces
sub supports_no_address
{
return 0;
}
# supports_bridges()
# Returns true if this backend can create persistent bridge devices
sub supports_bridges
{
return 0;
}
# supports_bonding()
# Returns true if this backend can create persistent bonded devices
sub supports_bonding
{
return 0;
}
# supports_vlans()
# Returns true if this backend can create persistent VLAN devices
sub supports_vlans
{
return 0;
}
# apply_network()
# Applies dhcpcd configuration by restarting the dhcpcd service
sub apply_network
{
&dhcpcd_remove_stale_virtual_addresses();
my $cmd = &has_command("systemctl") ? "systemctl restart dhcpcd" :
&has_command("service") ? "service dhcpcd restart" :
"/etc/init.d/dhcpcd restart";
my $out = &backquote_logged("$cmd 2>&1 </dev/null");
return $? ? $out || "$cmd failed" : undef;
}
# apply_interface(&iface)
# Applies one interface change by restarting dhcpcd
sub apply_interface
{
return "Cannot find device \"".&dhcpcd_apply_device($_[0])."\""
if (!&dhcpcd_interface_exists($_[0]));
return &apply_network();
}
# unapply_interface(&iface)
# Applies one interface removal by restarting dhcpcd
sub unapply_interface
{
return &apply_network();
}
# unapply_interface_after_delete(&active, &boot)
# Applies one interface removal after its dhcpcd config has been deleted
sub unapply_interface_after_delete
{
my ($active, $boot) = @_;
# Apply after the file edit so dhcpcd sees the deleted address/block.
return &apply_network();
}
# delete_active_interface(&iface)
# Deactivates an active interface row under dhcpcd control
sub delete_active_interface
{
my ($active) = @_;
my @boot = &boot_interfaces();
if ($active->{'virtual'} ne '' && $active->{'address'}) {
# dhcpcd can recreate configured aliases unless the config changes too.
my ($boot) = grep { $_->{'virtual'} ne '' &&
&dhcpcd_address_key($_) eq
&dhcpcd_address_key($active) } @boot;
if ($boot) {
# Remove the persistent alias, then drop only the selected live IP.
&delete_interface($boot);
&deactivate_interface($active);
return undef;
}
}
# Unmanaged or real active rows can still use the normal Linux action.
&deactivate_interface($active);
return undef;
}
# dhcpcd_remove_stale_virtual_addresses()
# Removes live virtual IPv4 addresses missing from dhcpcd boot config
sub dhcpcd_remove_stale_virtual_addresses
{
my @boot = &boot_interfaces();
my %managed;
my %wanted;
foreach my $iface (@boot) {
if ($iface->{'virtual'} eq '') {
# Only clean up live aliases on dhcpcd-managed parents.
$managed{$iface->{'fullname'}} = 1;
}
elsif ($iface->{'address'}) {
# Keep aliases that still exist in dhcpcd.conf.
$wanted{&dhcpcd_address_key($iface)} = 1;
}
}
foreach my $active (&dhcpcd_active_interfaces()) {
# Live aliases missing from boot config would survive a restart.
next if ($active->{'virtual'} eq '' || !$active->{'address'});
next if (!$managed{$active->{'name'}});
next if ($wanted{&dhcpcd_address_key($active)});
&deactivate_interface($active);
}
}
# dhcpcd_address_key(&iface)
# Returns a stable parent/address key for comparing virtual IPv4 aliases
sub dhcpcd_address_key
{
my ($iface) = @_;
return join("\0", $iface->{'name'}, $iface->{'address'},
$iface->{'netmask'} || "");
}
# dhcpcd_apply_device(&iface)
# Returns the real device name needed to apply an interface config
sub dhcpcd_apply_device
{
my ($iface) = @_;
return $iface->{'virtual'} ne '' ? $iface->{'name'} :
$iface->{'name'} || $iface->{'fullname'};
}
# dhcpcd_interface_exists(&iface)
# Returns true if the interface's real device exists right now
sub dhcpcd_interface_exists
{
my ($iface) = @_;
my $dev = &dhcpcd_apply_device($iface);
return grep { $_->{'fullname'} eq $dev || $_->{'name'} eq $dev }
&dhcpcd_active_interfaces();
}
# routing_config_files()
# Returns files that affect boot-time routing
sub routing_config_files
{
return ( $dhcpcd_config, $sysctl_config );
}
# network_config_files()
# Returns files that affect hostname/domain network settings
sub network_config_files
{
return ( "/etc/hostname", "/etc/HOSTNAME", "/etc/mailname" );
}
# routing_input()
# Prints the boot-time routing form for dhcpcd
sub routing_input
{
my ($addr, $router) = &get_default_gateway();
my ($addr6, $router6) = &get_default_ipv6_gateway();
my @ifaces = grep { $_->{'virtual'} eq '' } &boot_interfaces();
my @inames = map { $_->{'name'} } @ifaces;
# Default IPv4 and IPv6 gateways are stored on one interface block.
print &ui_table_row($text{'routes_default'},
&ui_radio("gateway_def", $addr ? 0 : 1,
[ [ 1, $text{'routes_none'} ],
[ 0, $text{'routes_gateway'}." ".
&ui_textbox("gateway", $addr, 15)." ".
&ui_select("gatewaydev", $router, \@inames) ] ]));
print &ui_table_row($text{'routes_default6'},
&ui_radio("gateway6_def", $addr6 ? 0 : 1,
[ [ 1, $text{'routes_none'} ],
[ 0, $text{'routes_gateway'}." ".
&ui_textbox("gateway6", $addr6, 30)." ".
&ui_select("gatewaydev6", $router6, \@inames) ] ]));
# Static routes are stored as destination/gateway pairs per interface.
my @routes;
foreach my $iface (@ifaces) {
foreach my $route (@{$iface->{'routes'} || [ ]}, "") {
my ($dest, $gw) = split(/,/, $route, 2);
push(@routes, [
&ui_select("route_dev", $iface->{'name'}, \@inames),
&ui_textbox("route_dest", $dest, 20),
&ui_textbox("route_gw", $gw, 15),
]);
}
}
push(@routes, [
&ui_select("route_dev", undef, \@inames),
&ui_textbox("route_dest", undef, 20),
&ui_textbox("route_gw", undef, 15),
]) if (!@ifaces);
print &ui_table_row($text{'routes_static'},
&ui_columns_table([ $text{'routes_ifc'}, $text{'routes_dest'},
$text{'routes_gateway'} ], 100, 0, \@routes));
# Forwarding remains the standard Linux sysctl setting.
my %sysctl;
&read_env_file($sysctl_config, \%sysctl);
print &ui_table_row($text{'routes_forward'},
&ui_yesno_radio("forward",
$sysctl{'net.ipv4.ip_forward'} ? 1 : 0));
}
# parse_routing()
# Saves the boot-time routing form for dhcpcd
sub parse_routing
{
my @boot = &boot_interfaces();
# Save the default IPv4 route, clearing it from any other interface.
my ($dev, $gw);
if (!$in{'gateway_def'}) {
&check_ipaddress($in{'gateway'}) ||
&error(&text('routes_egateway', &html_escape($in{'gateway'})));
$gw = $in{'gateway'};
$dev = $in{'gatewaydev'};
}
&set_default_gateway($gw, $dev, \@boot);
# Save the default IPv6 route in the same single-interface style.
my ($dev6, $gw6);
if (!$in{'gateway6_def'}) {
&check_ip6address($in{'gateway6'}) ||
&error(&text('routes_egateway6',&html_escape($in{'gateway6'})));
$gw6 = $in{'gateway6'};
$dev6 = $in{'gatewaydev6'};
}
&set_default_ipv6_gateway($gw6, $dev6, \@boot);
# Rebuild static routes from the submitted rows.
foreach my $iface (@boot) {
delete($iface->{'routes'});
}
my @rdev = split(/\0/, $in{'route_dev'} || "");
my @rdest = split(/\0/, $in{'route_dest'} || "");
my @rgw = split(/\0/, $in{'route_gw'} || "");
for(my $i=0; $i<@rdest; $i++) {
next if ($rdest[$i] eq '' && $rgw[$i] eq '');
# Each route must point at an existing boot-time interface.
my ($iface) = grep { $_->{'fullname'} eq $rdev[$i] } @boot;
$iface || &error(&text('routes_edevice',
&html_escape($rdev[$i])));
# dhcpcd static routes are IPv4 destination/gateway pairs here.
&valid_dhcpcd_route_dest($rdest[$i]) ||
&error(&text('routes_enet', &html_escape($rdest[$i])));
&check_ipaddress($rgw[$i]) ||
&error(&text('routes_egateway',
&html_escape($rgw[$i])));
push(@{$iface->{'routes'}}, "$rdest[$i],$rgw[$i]");
}
# Save all real interfaces so removed routes are written out too.
foreach my $iface (@boot) {
next if ($iface->{'virtual'} ne '');
&save_interface($iface, \@boot);
}
# Save Linux IPv4 forwarding alongside the dhcpcd routing settings.
my %sysctl;
&lock_file($sysctl_config);
&read_env_file($sysctl_config, \%sysctl);
$sysctl{'net.ipv4.ip_forward'} = $in{'forward'};
&write_env_file($sysctl_config, \%sysctl);
&unlock_file($sysctl_config);
}
# get_hostname()
# Returns the system hostname
sub get_hostname
{
my $hn = &read_file_contents("/etc/hostname");
$hn =~ s/\r|\n//g;
return $hn if ($hn);
return &get_system_hostname();
}
# save_hostname(hostname)
# Sets the system hostname in common Linux hostname files
sub save_hostname
{
my ($hostname) = @_;
&system_logged("hostname ".quotemeta($hostname)." >/dev/null 2>&1");
# Update all hostname files that already exist on this system.
foreach my $f ("/etc/hostname", "/etc/HOSTNAME", "/etc/mailname") {
if (-r $f) {
&open_lock_tempfile(HOST, ">$f");
&print_tempfile(HOST, $hostname,"\n");
&close_tempfile(HOST);
}
}
# hostnamectl keeps systemd's transient/static hostname in sync.
if (&has_command("hostnamectl")) {
&system_logged("hostnamectl set-hostname ".quotemeta($hostname).
" >/dev/null 2>&1");
}
&get_system_hostname(undef, undef, 2);
}
# get_domainname()
# Returns the current NIS domain name
sub get_domainname
{
my $d;
&execute_command("domainname", undef, \$d, undef);
chop($d);
return $d;
}
# save_domainname(domain)
# Sets the current NIS domain name
sub save_domainname
{
my ($domain) = @_;
&execute_command("domainname ".quotemeta($domain));
}
# get_default_gateway()
# Returns the default IPv4 gateway and interface name
sub get_default_gateway
{
foreach my $iface (&boot_interfaces()) {
return ( $iface->{'gateway'}, $iface->{'fullname'} )
if ($iface->{'gateway'});
}
return ( );
}
# set_default_gateway([gateway], [interface], [&boot-interfaces])
# Sets the default IPv4 gateway on one dhcpcd interface
sub set_default_gateway
{
my ($gw, $dev, $boot) = @_;
$boot ||= [ &boot_interfaces() ];
foreach my $iface (@$boot) {
next if ($iface->{'virtual'} ne '');
if ($iface->{'fullname'} eq $dev && $iface->{'gateway'} ne $gw) {
# Add or update the gateway on the selected interface.
$iface->{'gateway'} = $gw;
&save_interface($iface, $boot);
}
elsif ($iface->{'fullname'} ne $dev && $iface->{'gateway'}) {
# Remove old default gateways from all other interfaces.
delete($iface->{'gateway'});
&save_interface($iface, $boot);
}
}
}
# get_default_ipv6_gateway()
# Returns the default IPv6 gateway and interface name
sub get_default_ipv6_gateway
{
foreach my $iface (&boot_interfaces()) {
return ( $iface->{'gateway6'}, $iface->{'fullname'} )
if ($iface->{'gateway6'});
}
return ( );
}
# set_default_ipv6_gateway([gateway], [interface], [&boot-interfaces])
# Sets the default IPv6 gateway on one dhcpcd interface
sub set_default_ipv6_gateway
{
my ($gw, $dev, $boot) = @_;
$boot ||= [ &boot_interfaces() ];
foreach my $iface (@$boot) {
next if ($iface->{'virtual'} ne '');
if ($iface->{'fullname'} eq $dev && $iface->{'gateway6'} ne $gw) {
# Add or update the IPv6 gateway on the selected interface.
$iface->{'gateway6'} = $gw;
&save_interface($iface, $boot);
}
elsif ($iface->{'fullname'} ne $dev && $iface->{'gateway6'}) {
# Remove old IPv6 default gateways from all other interfaces.
delete($iface->{'gateway6'});
&save_interface($iface, $boot);
}
}
}
# os_save_dns_config(&config)
# Saves DNS servers and search domains into dhcpcd interface blocks
sub os_save_dns_config
{
my ($conf) = @_;
my @boot = &boot_interfaces();
my $newns = @{$conf->{'nameserver'} || [ ]} ? $conf->{'nameserver'} : undef;
my $newsearch = @{$conf->{'domain'} || [ ]} ? $conf->{'domain'} : undef;
# Prefer updating interfaces that already have DNS; otherwise use all real ones.
my @fix = grep { $_->{'nameserver'} } @boot;
@fix = grep { $_->{'virtual'} eq '' } @boot if (!@fix);
my $need_apply = 0;
foreach my $iface (@fix) {
# Skip unchanged interfaces to avoid needless dhcpcd restarts.
next if (&dhcpcd_same_list($iface->{'nameserver'}, $newns) &&
&dhcpcd_same_list($iface->{'search'}, $newsearch));
if ($newns) {
$iface->{'nameserver'} = $newns;
}
else {
delete($iface->{'nameserver'});
}
if ($newsearch) {
$iface->{'search'} = $newsearch;
}
else {
delete($iface->{'search'});
}
&save_interface($iface, \@boot);
$need_apply = 1;
}
return ($need_apply, 1);
}
# dhcpcd_same_list(&list1, &list2)
# Returns true if two optional array refs contain the same values
sub dhcpcd_same_list
{
my ($a, $b) = @_;
my @a = $a ? @$a : ( );
my @b = $b ? @$b : ( );
return 0 if (@a != @b);
for(my $i=0; $i<@a; $i++) {
return 0 if ($a[$i] ne $b[$i]);
}
return 1;
}
# read_dhcpcd_config(file)
# Parses dhcpcd.conf into global/interface/profile/ssid blocks
sub read_dhcpcd_config
{
my ($file) = @_;
my $lref = &read_file_lines($file);
# Global settings are represented as a synthetic first block.
my $cur = { 'type' => 'global',
'name' => '',
'line' => 0,
'eline' => -1,
'members' => [ ] };
my @rv = ( $cur );
for(my $i=0; $i<@$lref; $i++) {
my $line = $lref->[$i];
if ($line =~ /^\s*(interface|profile|ssid)\s+(\S+)/) {
# New dhcpcd block starts; close the previous explicit block.
$cur->{'eline'} = $i - 1 if ($cur);
$cur = { 'type' => $1,
'name' => $2,
'line' => $i,
'eline' => $i,
'members' => [ ] };
push(@rv, $cur);
}
else {
# Blank/comment lines are not members but still belong to blocks.
$cur->{'eline'} = $i if ($cur->{'type'} ne 'global');
my $member = &dhcpcd_line_member($line, $i);
push(@{$cur->{'members'}}, $member) if ($member);
}
}
return \@rv;
}
# dhcpcd_line_member(line, line-number)
# Parses one non-block dhcpcd.conf line into a member hash
sub dhcpcd_line_member
{
my ($line, $lnum) = @_;
# Strip comments and surrounding whitespace before classifying the line.
my $clean = $line;
$clean =~ s/#.*$//;
$clean =~ s/^\s+//;
$clean =~ s/\s+$//;
return undef if ($clean eq '');
# Static dhcpcd settings use "static name=value" syntax.
if ($clean =~ /^static\s+([^=\s]+)=(.*)$/) {
return { 'static' => 1,
'name' => $1,
'value' => $2,
'line' => $lnum };
}
elsif ($clean =~ /^(\S+)\s+(.*)$/) {
# Plain settings usually use "name value" syntax.
return { 'name' => $1,
'value' => $2,
'line' => $lnum };
}
elsif ($clean =~ /^(\S+)$/) {
# Flag settings such as noipv6rs have no value.
return { 'name' => $1,
'value' => '',
'line' => $lnum };
}
return undef;
}
# dhcpcd_values(&block, name)
# Returns all static values with the given name from a block
sub dhcpcd_values
{
my ($block, $name) = @_;
return map { $_->{'value'} }
grep { $_->{'static'} && $_->{'name'} eq $name }
@{$block->{'members'}};
}
# dhcpcd_plain_values(&block, name)
# Returns all non-static values with the given name from a block
sub dhcpcd_plain_values
{
my ($block, $name) = @_;
return map { $_->{'value'} }
grep { !$_->{'static'} && $_->{'name'} eq $name }
@{$block->{'members'}};
}
# dhcpcd_interface_lines(&iface, [&all-interfaces])
# Converts one Webmin boot interface into dhcpcd.conf lines
sub dhcpcd_interface_lines
{
my ($iface, $boot) = @_;
my @lines = ( "interface ".$iface->{'fullname'} );
my @keep;
if ($iface->{'block'}) {
# Managed settings are regenerated; all other directives are preserved.
my %replace = map { $_ => 1 }
qw(ip_address routers domain_name_servers domain_search
domain_name ip6_address ip6_routers routes);
foreach my $m (@{$iface->{'block'}->{'members'}}) {
next if ($m->{'static'} && $replace{$m->{'name'}});
next if (!$m->{'static'} && $m->{'name'} eq 'mtu');
my $line = $m->{'static'} ?
"static $m->{'name'}=$m->{'value'}" :
$m->{'value'} eq '' ? $m->{'name'} :
"$m->{'name'} $m->{'value'}";
push(@keep, $line);
}
}
push(@lines, @keep);
# Virtual aliases are stored as extra static ip_address directives.
my @vifaces = grep { $_->{'name'} eq $iface->{'name'} &&
$_->{'virtual'} ne '' } @$boot;
if ($iface->{'dhcp'} && !@vifaces) {
# dhcpcd uses DHCP by default when no static ip_address is set.
}
elsif ($iface->{'address'} || @vifaces) {
# Write the primary static IPv4 address first, then aliases.
my $prefix = $iface->{'netmask'} ?
&mask_to_prefix($iface->{'netmask'}) : 24;
push(@lines, "static ip_address=$iface->{'address'}/$prefix")
if ($iface->{'address'});
foreach my $viface (@vifaces) {
my $vprefix = $viface->{'netmask'} ?
&mask_to_prefix($viface->{'netmask'}) : $prefix;
push(@lines, "static ip_address=$viface->{'address'}/$vprefix");
}
}
# Default gateway and static routes use separate dhcpcd directives.
if ($iface->{'gateway'}) {
push(@lines, "static routers=$iface->{'gateway'}");
}
if ($iface->{'routes'} && @{$iface->{'routes'}}) {
my @routes;
foreach my $route (@{$iface->{'routes'}}) {
my ($dest, $gw) = split(/,/, $route, 2);
push(@routes, $dest, $gw) if ($dest && $gw);
}
push(@lines, "static routes=".join(" ", @routes)) if (@routes);
}
# DNS servers and search domains stay inside the interface block.
if ($iface->{'nameserver'} && @{$iface->{'nameserver'}}) {
push(@lines, "static domain_name_servers=".
join(" ", @{$iface->{'nameserver'}}));
}
if ($iface->{'search'} && @{$iface->{'search'}}) {
push(@lines, "static domain_search=".join(" ", @{$iface->{'search'}}));
}
# IPv6 static addresses and gateway are written after IPv4 settings.
for(my $i=0; $i<@{$iface->{'address6'}}; $i++) {
push(@lines, "static ip6_address=$iface->{'address6'}->[$i]/".
($iface->{'netmask6'}->[$i] || 64));
}
if ($iface->{'gateway6'}) {
push(@lines, "static ip6_routers=$iface->{'gateway6'}");
}
# MTU is a plain dhcpcd directive, not a static key.
if ($iface->{'mtu'}) {
push(@lines, "mtu $iface->{'mtu'}");
}
return @lines;
}
# valid_dhcpcd_route_dest(destination)
# Returns true if a static IPv4 route destination is valid
sub valid_dhcpcd_route_dest
{
my ($dest) = @_;
return 0 if ($dest eq '');
my ($addr, $prefix) = split(/\//, $dest, 2);
return 0 if (!&check_ipaddress($addr));
return !defined($prefix) || $prefix =~ /^\d+$/ && $prefix >= 0 &&
$prefix <= 32;
}
# dhcpcd_implicit_interfaces(&config, [&explicit-interfaces])
# Returns DHCP interfaces managed by dhcpcd without explicit blocks
sub dhcpcd_implicit_interfaces
{
my ($cfg, $explicit) = @_;
return ( ) if (!&dhcpcd_should_synthesize_implicit());
# Do not synthesize rows for names already represented by interface blocks.
my %explicit = map { $_->{'fullname'}, 1 } @$explicit;
my @rv;
foreach my $active (&dhcpcd_active_interfaces()) {
my $name = $active->{'fullname'} || $active->{'name'};
# Only real, non-loopback interfaces can be implicit dhcpcd rows.
next if (!$name || $name eq "lo" || $active->{'virtual'} ne "");
next if ($explicit{$name});
next if (!&dhcpcd_interface_managed_by_default($name, $cfg));
# Limit this to physical and wireless interfaces, not bridges/tunnels.
my $type = &iface_type($name);
next if ($type !~ /Ethernet|Wireless/);
push(@rv, { 'name' => $name,
'fullname' => $name,
'virtual' => '',
'file' => $dhcpcd_config,
'edit' => 1,
'up' => 1,
'dhcp' => 1,
'implicit' => 1,
'address6' => [ ],
'netmask6' => [ ] });
}
return @rv;
}
# dhcpcd_should_synthesize_implicit()
# Returns true if implicit default-DHCP interfaces should be shown
sub dhcpcd_should_synthesize_implicit
{
return $dhcpcd_synthesize_implicit
if (defined($dhcpcd_synthesize_implicit));
# In normal use, implicit rows only make sense when dhcpcd is the service.
return defined(&net_dhcpcd_service_active) &&
&net_dhcpcd_service_active();
}
# dhcpcd_active_interfaces()
# Returns active interfaces, with a test override for unit tests
sub dhcpcd_active_interfaces
{
return @$dhcpcd_test_active_interfaces
if (ref($dhcpcd_test_active_interfaces));
return &active_interfaces(1);
}
# dhcpcd_interface_managed_by_default(name, &config)
# Returns true if global allow/deny rules let dhcpcd manage an interface
sub dhcpcd_interface_managed_by_default
{
my ($name, $cfg) = @_;
my ($global) = grep { $_->{'type'} eq 'global' } @$cfg;
# dhcpcd supports global allowinterfaces and denyinterfaces filters.
my @allow = $global ? map { split(/\s+/, $_) }
&dhcpcd_plain_values($global, "allowinterfaces") : ( );
my @deny = $global ? map { split(/\s+/, $_) }
&dhcpcd_plain_values($global, "denyinterfaces") : ( );
# denyinterfaces wins first; allowinterfaces narrows the default set.
return 0 if (grep { &dhcpcd_pattern_match($name, $_) } @deny);
return 1 if (!@allow);
return grep { &dhcpcd_pattern_match($name, $_) } @allow;
}
# dhcpcd_pattern_match(name, pattern)
# Returns true if an interface name matches a dhcpcd glob pattern
sub dhcpcd_pattern_match
{
my ($name, $pattern) = @_;
return 0 if ($pattern eq "");
# Convert dhcpcd-style shell globs into a safely quoted regexp.
my $re = quotemeta($pattern);
$re =~ s/\\\*/.*/g;
$re =~ s/\\\?/./g;
return $name =~ /^$re$/;
}
# dhcpcd_add_global_word(&lines, directive, word)
# Adds one word to a global dhcpcd directive before any interface block
sub dhcpcd_add_global_word
{
my ($lref, $name, $word) = @_;
return if ($word eq "");
for(my $i=0; $i<@$lref; $i++) {
my $line = $lref->[$i];
# Global directives must appear before interface/profile/ssid blocks.
last if ($line =~ /^\s*(interface|profile|ssid)\s+\S+/);
my $clean = $line;
$clean =~ s/#.*$//;
if ($clean =~ /^(\s*)\Q$name\E\s+(.*)$/) {
# Extend an existing directive without adding duplicates.
my @words = split(/\s+/, $2);
return if (grep { $_ eq $word } @words);
push(@words, $word);
$lref->[$i] = $1.$name." ".join(" ", @words);
return;
}
}
# No matching directive existed, so add a new global directive at the top.
unshift(@$lref, "$name $word");
}
# dhcpcd_add_global_word_if_exists(&lines, directive, word)
# Adds one word to an existing global dhcpcd directive
sub dhcpcd_add_global_word_if_exists
{
my ($lref, $name, $word) = @_;
return if ($word eq "");
for(my $i=0; $i<@$lref; $i++) {
my $line = $lref->[$i];
# Only global allow/deny directives apply before interface blocks.
last if ($line =~ /^\s*(interface|profile|ssid)\s+\S+/);
my $clean = $line;
$clean =~ s/#.*$//;
if ($clean =~ /^(\s*)\Q$name\E\s+(.*)$/) {
my @words = split(/\s+/, $2);
# Existing exact or glob patterns already allow this interface.
return if (grep { $_ eq $word ||
&dhcpcd_pattern_match($word, $_) } @words);
push(@words, $word);
$lref->[$i] = $1.$name." ".join(" ", @words);
return;
}
}
}
# dhcpcd_delete_global_word(&lines, directive, word)
# Removes one word from a global dhcpcd directive
sub dhcpcd_delete_global_word
{
my ($lref, $name, $word) = @_;
return if ($word eq "");
for(my $i=0; $i<@$lref; $i++) {
my $line = $lref->[$i];
# Stop before per-interface blocks so line numbers remain predictable.
last if ($line =~ /^\s*(interface|profile|ssid)\s+\S+/);
my $clean = $line;
$clean =~ s/#.*$//;
if ($clean =~ /^(\s*)\Q$name\E\s+(.*)$/) {
# Keep the directive if other words remain, otherwise remove it.
my @words = grep { $_ ne $word } split(/\s+/, $2);
if (@words) {
$lref->[$i] = $1.$name." ".join(" ", @words);
}
else {
splice(@$lref, $i, 1);
}
return;
}
}
}
1;