From 24d7c5ef6bea7b6521d309757d2fa0461935f89d Mon Sep 17 00:00:00 2001 From: Stefan Schantl Date: Tue, 7 Jan 2020 10:30:37 +0100 Subject: [PATCH] dns.cgi: Rework to allow central DNS configuration. Fixes #12237. Signed-off-by: Stefan Schantl --- html/cgi-bin/dns.cgi | 864 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 751 insertions(+), 113 deletions(-) mode change 100644 => 100755 html/cgi-bin/dns.cgi diff --git a/html/cgi-bin/dns.cgi b/html/cgi-bin/dns.cgi old mode 100644 new mode 100755 index eee7a90b40..f4838b0464 --- a/html/cgi-bin/dns.cgi +++ b/html/cgi-bin/dns.cgi @@ -2,7 +2,7 @@ ############################################################################### # # # IPFire.org - A linux based firewall # -# Copyright (C) 2007-2019 IPFire Team # +# Copyright (C) 2020 IPFire Development Team # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # @@ -20,166 +20,804 @@ ############################################################################### use strict; +use IO::Socket; # enable only the following on debugging purpose #use warnings; #use CGI::Carp 'fatalsToBrowser'; require '/var/ipfire/general-functions.pl'; +require "${General::swroot}/geoip-functions.pl"; require "${General::swroot}/lang.pl"; require "${General::swroot}/header.pl"; -my %dnssettings=(); +#workaround to suppress a warning when a variable is used only once +my @dummy = ( ${Header::colouryellow} ); +undef (@dummy); + +my %cgiparams=(); +my %checked=(); +my %selected=(); my $errormessage = ''; +# Config file which stores the DNS settings. +my $settings_file = "${General::swroot}/dns/settings"; + +# File which stores the configured DNS-Servers. +my $servers_file = "${General::swroot}/dns/servers"; + +# Create files if the does not exist. +unless (-f $settings_file) { system("touch $settings_file") }; +unless (-f $servers_file) { system("touch $servers_file") }; + +# File which stores the ISP assigned DNS servers. +my @ISP_nameserver_files = ( "${General::swroot}/dns/dns1", "${General::swroot}/dns/dns2" ); + +# File which contains the ca-certificates. +my $ca_certs_file = "/etc/ssl/certs/ca-bundle.crt"; + +my %color = (); +my %mainsettings = (); +&General::readhash("${General::swroot}/main/settings", \%mainsettings); +&General::readhash("/srv/web/ipfire/html/themes/".$mainsettings{'THEME'}."/include/colors.txt", \%color); + &Header::showhttpheaders(); +&Header::getcgihash(\%cgiparams); -&General::readhash("${General::swroot}/dns/settings", \%dnssettings); +## +# Save general settings. +# +if ($cgiparams{'GENERAL'} eq $Lang::tr{'save'}) { + # Prevent form name from been stored in conf file. + delete $cgiparams{'GENERAL'}; -&Header::getcgihash(\%dnssettings); + # Add value for non-checked checkbox. + if ($cgiparams{'USE_ISP_NAMESERVERS'} ne "on") { + $cgiparams{'USE_ISP_NAMESERVERS'} = "off"; + } -&Header::openpage($Lang::tr{'dns title'}, 1, ); + # Add value for non-checked checkbox. + if ($cgiparams{'ENABLE_SAFE_SEARCH'} ne "on") { + $cgiparams{'ENABLE_SAFE_SEARCH'} = "off"; + } -&Header::openbigbox('100%', 'left', '', $errormessage); + # Store settings into settings file. + &General::writehash("$settings_file", \%cgiparams); +} + +### +# Add / Edit entries. +# +if (($cgiparams{'SERVERS'} eq $Lang::tr{'save'}) || ($cgiparams{'SERVERS'} eq $Lang::tr{'update'})) { + # Hash to store the generic DNS settings. + my %settings = (); + + # Read-in generic settings. + &General::readhash("$settings_file", \%settings); -if ($dnssettings{'ACTION'} eq $Lang::tr{'save'}) { - if ((&General::validip($dnssettings{"DNS0"}) == 1)&&(&General::validip($dnssettings{"DNS1"}) == 1)) { - if ($errormessage eq "") { - &General::writehash("${General::swroot}/dns/settings", \%dnssettings); - &Header::openbox('100%', 'left', $Lang::tr{'dns saved'}); - print "$Lang::tr{'dns saved txt'}\n"; - &Header::closebox(); + # Check if the given DNS server is valid. + if(!&General::validip($cgiparams{"NAMESERVER"})) { + $errormessage = "$Lang::tr{'invalid ip'}: $cgiparams{'NAMESERVER'}"; + } + + # Check if a TLS is enabled and no TLS_HOSTNAME has benn specified. + elsif($settings{'PROTO'} eq "TLS") { + unless($cgiparams{"TLS_HOSTNAME"}) { + $errormessage = "$Lang::tr{'dns no tls hostname given'}"; + } else { + # Check if the provided domain is valid. + unless(&General::validfqdn($cgiparams{"TLS_HOSTNAME"})) { + $errormessage = "$Lang::tr{'invalid ip or hostname'}: $cgiparams{'TLS_HOSTNAME'}"; + } } - } else { - if ((&General::validip($dnssettings{"DNS0"}) == 0)&&(&General::validip($dnssettings{"DNS1"}) == 1)){ - $errormessage = $Lang::tr{'dns error 0'}; - } - if ((&General::validip($dnssettings{"DNS1"}) == 0)&&(&General::validip($dnssettings{"DNS0"}) == 1)){ - $errormessage = $Lang::tr{'dns error 1'}; + } + + # Check the nameserver. + my $status = &check_nameserver("$cgiparams{'NAMESERVER'}", "ping.ipfire.org", "$settings{'PROTO'}", "$cgiparams{'TLS_HOSTNAME'}"); + + # Assign errormessage, if the nameserver does not support dnssec or any other kind of error happened. + if ($status eq "0") { + $errormessage = "$Lang::tr{'dns could not add server'} $Lang::tr{'dnssec not supported'}"; + } elsif (($status ne "1") && ($status ne "2")) { + $errormessage = "$Lang::tr{'dns could not add server'} $status"; + } + + # Go further if there was no error. + if ( ! $errormessage) { + # Check if a remark has been entered. + $cgiparams{'REMARK'} = &Header::cleanhtml($cgiparams{'REMARK'}); + + my %dns_servers = (); + my $id; + my $status; + + # Read-in configfile. + &General::readhasharray($servers_file, \%dns_servers); + + # Check if we should edit an existing entry and got an ID. + if (($cgiparams{'SERVERS'} eq $Lang::tr{'update'}) && ($cgiparams{'ID'})) { + # Assin the provided id. + $id = $cgiparams{'ID'}; + + # Undef the given ID. + undef($cgiparams{'ID'}); + + # Grab the configured status of the corresponding entry. + $status = $dns_servers{$id}[2]; + } else { + # Each newly added entry automatically should be enabled. + $status = "enabled"; + + # Generate the ID for the new entry. + # + # Sort the keys by their ID and store them in an array. + my @keys = sort { $a <=> $b } keys %dns_servers; + + # Reverse the key array. + my @reversed = reverse(@keys); + + # Obtain the last used id. + my $last_id = @reversed[0]; + + # Increase the last id by one and use it as id for the new entry. + $id = ++$last_id; + + # The first allowed id is 3 to keep space for + # possible ISP assigned DNS servers. + if ($id le "2") { + $id = "3"; + } } - if ((&General::validip($dnssettings{"DNS1"}) == 0)&&(&General::validip($dnssettings{"DNS0"}) == 0)){ - $errormessage = $Lang::tr{'dns error 01'}; + + # Add/Modify the entry to/in the dns_servers hash. + $dns_servers{$id} = ["$cgiparams{'NAMESERVER'}", "$cgiparams{'TLS_HOSTNAME'}", "$status", "$cgiparams{'REMARK'}"]; + + # Write the changed hash to the config file. + &General::writehasharray($servers_file, \%dns_servers); + } else { + # Switch back to previous mode. + $cgiparams{'SERVERS'} = $cgiparams{'MODE'}; + } +### +# Toggle enable / disable. +# +} elsif ($cgiparams{'SERVERS'} eq $Lang::tr{'toggle enable disable'}) { + my %dns_servers = (); + + # Only go further, if an ID has been passed. + if ($cgiparams{'ID'}) { + # Assign the given ID. + my $id = $cgiparams{'ID'}; + + # Undef the given ID. + undef($cgiparams{'ID'}); + + # Read-in configfile. + &General::readhasharray($servers_file, \%dns_servers); + + # Grab the configured status of the corresponding entry. + my $status = $dns_servers{$id}[2]; + + # Switch the status. + if ($status eq "disabled") { + $status = "enabled"; + } else { + $status = "disabled"; } + + # Modify the status of the existing entry. + $dns_servers{$id} = ["$dns_servers{$id}[0]", "$dns_servers{$id}[1]", "$status", "$dns_servers{$id}[3]"]; + + # Write the changed hash back to the config file. + &General::writehasharray($servers_file, \%dns_servers); } + +## Remove entry from DNS servers list. +# +} elsif ($cgiparams{'SERVERS'} eq $Lang::tr{'remove'}) { + my %dns_servers = (); + + # Read-in configfile. + &General::readhasharray($servers_file, \%dns_servers); + + # Drop entry from the hash. + delete($dns_servers{$cgiparams{'ID'}}); + + # Undef the given ID. + undef($cgiparams{'ID'}); + + # Write the changed hash to the config file. + &General::writehasharray($servers_file, \%dns_servers); } -if ($dnssettings{'ACTION'} eq $Lang::tr{'reconnect'}) { - system("/usr/local/bin/redctrl restart >/dev/null 2>&1 &"); - &Header::openbox('100%', 'left', $Lang::tr{'dns address recon'} ); - print "$Lang::tr{'dns address done'}\n"; - &Header::closebox(); +# Hash to store the generic DNS settings. +my %settings = (); + +# Read-in general DNS settings. +&General::readhash("$settings_file", \%settings); + +# Hash which contains the configured DNS servers. +my %dns_servers = (); + +# Read-in config file. +&General::readhasharray("$servers_file", \%dns_servers); + +&Header::openpage($Lang::tr{'dns'}, 1, ''); + +&Header::openbigbox('100%', 'left', '', $errormessage); + +### +# Error messages layout. +# +if ($errormessage) { + &Header::openbox('100%', 'left', $Lang::tr{'error messages'}); + print "$errormessage\n"; + print " \n"; + &Header::closebox(); } -if ($dnssettings{'ACTION'} eq $Lang::tr{'delete'}) { - system("cat /dev/null > ${General::swroot}/dns/settings &"); - &Header::openbox('100%', 'left', $Lang::tr{'dns address deleted'} ); - print "$Lang::tr{'dns address deleted txt'}\n"; - &Header::closebox(); +# Handle if a nameserver should be added or edited. +if (($cgiparams{'SERVERS'} eq "$Lang::tr{'add'}") || ($cgiparams{'SERVERS'} eq "$Lang::tr{'edit'}")) { + # Display the sub page. + &show_add_edit_nameserver(); + + # Close webpage. + &Header::closebigbox(); + &Header::closepage(); + + # Finished here for the moment. + exit(0); } -# DPC move error message to top so it is seen! -if ($errormessage) { - &Header::openbox('100%', 'left', $Lang::tr{'error messages'}); - print "$errormessage \n"; +$cgiparams{'GENERAL'} = ''; +$cgiparams{'SERVERS'} = ''; +$cgiparams{'NAMESERVER'} = ''; +$cgiparams{'TLS_HOSTNAME'} = ''; +$cgiparams{'REMARK'} =''; + +$checked{'USE_ISP_NAMESERVERS'}{'off'} = ''; +$checked{'USE_ISP_NAMESERVERS'}{'on'} = ''; +$checked{'USE_ISP_NAMESERVERS'}{$settings{'USE_ISP_NAMESERVERS'}} = "checked='checked'"; + +$checked{'ENABLE_SAFE_SEARCH'}{'off'} = ''; +$checked{'ENABLE_SAFE_SEARCH'}{'on'} = ''; +$checked{'ENABLE_SAFE_SEARCH'}{$settings{'ENABLE_SAFE_SEARCH'}} = "checked='checked'"; + +$selected{'PROTO'}{'UDP'} = ''; +$selected{'PROTO'}{'TLS'} = ''; +$selected{'PROTO'}{'TCP'} = ''; +$selected{'PROTO'}{$settings{'PROTO'}} = "selected='selected'"; + +$selected{'QNAME_MIN'}{'standard'} = ''; +$selected{'QNAME_MIN'}{'strict'} = ''; +$selected{'QNAME_MIN'}{$settings{'QNAME_MIN'}} = "selected='selected'"; + +# Display nameserver and configuration sections. +&show_nameservers(); +&show_general_dns_configuration(); + +&Header::closebigbox(); +&Header::closepage(); + +### +# General DNS-Servers sektion. +# +sub show_general_dns_configuration () { + &Header::openbox('100%', 'center', "$Lang::tr{'dns configuration'}"); + + print < + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ $Lang::tr{'dns use isp assigned nameservers'} + + +
+
+
+ $Lang::tr{'dns use protocol for dns queries'} + + +
+
+
+ $Lang::tr{'dns enable safe-search'} + + +
+
+
+ $Lang::tr{'dns mode for qname minimisation'} + + +
+ +
+ +END + &Header::closebox(); } -print "
\n"; +### +# Section to display the configured and used DNS servers. +# +sub show_nameservers () { + &Header::openbox('100%', 'center', "DNS-Servers"); -&Header::openbox('100%', 'left', $Lang::tr{'dns header'}); -print < + + + $Lang::tr{'nameserver'} + + + + $Lang::tr{'country'} + + + + $Lang::tr{'rdns'} + + + + $Lang::tr{'remark'} + + + + $Lang::tr{'status'} + - - - - - - - - - + + END -; -if ($dnssettings{'ACTION'} eq $Lang::tr{'delete'}) { -print < + + # Check the usage of ISP assigned nameservers is enabled. + if ($settings{'USE_ISP_NAMESERVERS'} eq "on") { + my $id="1"; + + # Loop through the array which stores the files. + foreach my $file (@ISP_nameserver_files) { + # Grab the address of the nameserver. + my $address = &grab_address_from_file($file); + + # Check if we got an address. + if ($address) { + # Add the address to the hash of nameservers. + $dns_servers{$id} = [ "$address", "none", "enabled", "$Lang::tr{'dns isp assigned nameserver'}" ]; + + # Increase id by one. + $id++; + } + } + } + + # Check some DNS servers have been configured. In this case + # the hash contains at least one key. + my $server_amount; + if (keys %dns_servers) { + # Sort the keys by their ID and store them in an array. + my @keys = sort { $a <=> $b } keys %dns_servers; + + # Loop through all entries of the array/hash. + foreach my $id (@keys) { + # Inrease server_amount. + $server_amount++; + + # Assign data array positions to some nice variable names. + my $nameserver = $dns_servers{$id}[0]; + my $tls_hostname = $dns_servers{$id}[1]; + my $enabled = $dns_servers{$id}[2]; + my $remark = $dns_servers{$id}[3]; + + my $col = ''; + my $toggle = ''; + my $gif = ''; + my $gdesc = ''; + my $notice = ""; + + # Colorize columns. + if ($server_amount % 2) { + $col="bgcolor='$color{'color22'}'"; } + else { + $col="bgcolor='$color{'color20'}'"; + } + + if ($enabled eq 'enabled') { + $gif='on.gif'; $toggle='off'; $gdesc=$Lang::tr{'click to disable'}; + } else { + $gif='off.gif'; $toggle='on'; $gdesc=$Lang::tr{'click to enable'}; + } + + my $status; + my $status_short; + my $status_message; + my $status_colour; + + # Only grab the status if the nameserver is enabled. + if ($enabled eq "enabled") { + $status = &check_nameserver("$nameserver", "ping.ipfire.org", "$settings{'PROTO'}", "$tls_hostname"); + } + + if (!$status) { + $status_short = "$Lang::tr{'disabled'}"; + + # DNSSEC Not supported + } elsif ($status eq 0) { + $status_short = "$Lang::tr{'broken'}"; + $status_message = $Lang::tr{'dnssec not supported'}; + $status_colour = ${Header::colourred}; + + # DNSSEC Aware + } elsif ($status eq 1) { + $status_short = "$Lang::tr{'not validating'}"; + $status_message = $Lang::tr{'dnssec aware'}; + $status_colour = ${Header::colourblack}; + + # DNSSEC Validating + } elsif ($status eq 2) { + $status_short = "$Lang::tr{'ok'}"; + $status_message = $Lang::tr{'dnssec validating'}; + $status_colour = ${Header::colourgreen}; + + # Error + } else { + $status_short = "$Lang::tr{'error'}"; + $status_message = $status; + $status_colour = ${Header::colourred}; + } + + # collect more information about name server (rDNS, GeoIP country code) + my $ccode = &GeoIP::lookup($nameserver); + my $flag_icon = &GeoIP::get_flag_icon($ccode); + + my $iaddr = inet_aton($nameserver); + my $rdns = gethostbyaddr($iaddr, AF_INET); + + if (!$rdns) { $rdns = $Lang::tr{'lookup failed'}; } + +print < + + + + + + + + + END -; -} else { -print < +; + # Check if the id is greater than "2". + # + # Nameservers with an ID's of one or two are ISP assigned, + # and we cannot perform any actions on them, so hide the tools for + # them. + if ($id gt "2") { + +print < + + + + + + + + + + + END -; -} -print < - - +; + } else { + print "\n"; + } + + + print"\n"; + + } + + print"
$Lang::tr{'dns desc'}
$Lang::tr{'dns list'}
 
$Lang::tr{'dns new 0'} + $Lang::tr{'action'} +
+ $nameserver + + $ccode + + $rdns + + $remark + + $status_short + +
+ + + +
+
+
+ + + +
+
$Lang::tr{'dns new 1'} 
\n"; + + print"\n"; + + # Check if the usage of the ISP nameservers is enabled and there are more than 2 servers. + if (($settings{'USE_ISP_NAMESERVERS'} eq "on") && ($server_amount gt "2")) { +print < + + + + + + + + + + END -; -if ($dnssettings{'ACTION'} eq $Lang::tr{'delete'}) { -print < +; + } +print < +
+
+ + +
  $Lang::tr{'legend'}:  $Lang::tr{$Lang::tr{'click to disable'}    $Lang::tr{$Lang::tr{'click to enable'}    $Lang::tr{$Lang::tr{'edit'}    $Lang::tr{$Lang::tr{'remove'}
END -; -} else { -print < +; + + } else { +print < + + +
$Lang::tr{'guardian no entries'}
+ + + + +
+ +
+ + + END -; +; + } + + &Header::closebox(); } -print < - -
- - -
-END -; -if ($dnssettings{'ACTION'} eq $Lang::tr{'save'} && $errormessage eq "") { -print < -      - -      - -END -; -} elsif ($dnssettings{'ACTION'} eq $Lang::tr{'delete'}) { -print < -      - + +### +# Section to display the add or edit subpage. +# +sub show_add_edit_nameserver() { + print "
\n"; + + my $buttontext = $Lang::tr{'save'}; + my $dnssec_checked; + my $dot_checked; + if ($cgiparams{'SERVERS'} eq $Lang::tr{'edit'}) { + &Header::openbox('100%', 'left', $Lang::tr{'dnsforward edit an entry'}); + + # Update button text for upate the existing entry. + $buttontext = $Lang::tr{'update'}; + + # Add hidden input for sending ID. + print"\n"; + + # Check if an ID has been given. + if ($cgiparams{'ID'}) { + # Assign cgiparams values. + $cgiparams{'NAMESERVER'} = $dns_servers{$cgiparams{'ID'}}[0]; + $cgiparams{'TLS_HOSTNAME'} = $dns_servers{$cgiparams{'ID'}}[1]; + $cgiparams{'REMARK'} = $dns_servers{$cgiparams{'ID'}}[3]; + } + } else { + &Header::openbox('100%', 'left', $Lang::tr{'dnsforward add a new entry'}); + } + + # Add hidden input to store the mode. + print "\n"; + +print < + + $Lang::tr{'ip address'}: * + + END ; -} elsif ($dnssettings{'ACTION'} eq $Lang::tr{'save'}) { -print < -END -; -} else { + # If the protocol is TLS, display the TLS hostname input. + if ($settings{'PROTO'} eq "TLS") { print < -      - + + $Lang::tr{'dns tls hostname'}: * + + END -; -} -print < - - - +; + } + +print < + $Lang::tr{'remark'}: + + + + +
+
+ + + + + + +
* $Lang::tr{'required field'} + + +
END ; -&Header::closebox(); + &Header::closebox(); + print "\n"; -print "\n"; + &Header::closebox(); +} -&Header::closebigbox(); +# Tiny function to grab an IP-address of a given file. +sub grab_address_from_file($) { + my ($file) = @_; -&Header::closepage(); + my $address; + + # Check if the given file exists. + if(-f $file) { + # Open the file for reading. + open(FILE, $file) or die "Could not read from $file. $!\n"; + + # Read the address from the file. + $address = ; + # Close filehandle. + close(FILE); + + # Remove newlines. + chomp($address); + + # Check if the obtained address is valid. + if (&General::validip($address)) { + # Return the address. + return $address; + } + } + + # Return nothing. + return; +} + +# Function to check a given nameserver against propper work. +sub check_nameserver($$$$) { + my ($nameserver, $record, $proto, $tls_hostname) = @_; + + # Timout for the query in seconds. + my $timeout; + my $retry = "+retry=0"; + + # Default values. + my @command = ("kdig", "$timeout", "$retry", "+dnssec"); + + # Handle different protols. + if ($proto eq "TCP") { + # Add TCP switch to the command. + push(@command, "+tcp"); + + } elsif($proto eq "TLS") { + # Add TLS switch to the command and provide the + # path to our file which contains the ca certs. + push(@command, "+tls-ca=$ca_certs_file"); + + # Check if a TLS hostname has been provided. + if ($tls_hostname) { + # Add TLS hostname to the command. + push(@command, "+tls-hostname=$tls_hostname"); + } else { + return "$Lang::tr{'dns no tls hostname given'}"; + } + } + + # Add record to the command array. + push(@command, "$record"); + + # Add nameserver to the command array. + push(@command, "\@$nameserver"); + + # Connect to STDOUT and STDERR. + push(@command, "2>&1"); + + my @output = qx(@command); + my $output = join("", @output); + + my $status = 0; + if ($output =~ m/WARNING: (.*)/) { + return $1; + + } + + if ($output =~ m/status: (\w+)/) { + $status = ($1 eq "NOERROR"); + + if (!$status) { + return -1; + } + } + + my @flags = (); + if ($output =~ m/Flags: (.*);/) { + @flags = split(/ /, $1); + } + + my $aware = ($output =~ m/RRSIG/); + my $validating = ("ad;" ~~ @flags); + + return $aware + $validating; +} -- 2.39.2