php IHDR w Q )Ba pHYs sRGB gAMA a IDATxMk\U s&uo,mD )Xw+e?tw.oWp;QHZnw`gaiJ9̟灙a=nl[ ʨ G;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ y H@E7j 1j+OFRg}ܫ;@Ea~ j`u'o> j- $_q?qS XzG'ay
files >> /usr/libexec/webmin/virtual-server/ |
files >> //usr/libexec/webmin/virtual-server/feature-dns.pl |
sub require_bind { return if ($require_bind++); &foreign_require("bind8", "bind8-lib.pl"); %bconfig = &foreign_config("bind8"); } # check_depends_dns(&domain) # For a sub-domain that is being added to a parent DNS domain, make sure the # parent zone actually exists sub check_depends_dns { if ($_[0]->{'subdom'}) { local $tmpl = &get_template($_[0]->{'template'}); local $parent = &get_domain($_[0]->{'subdom'}); if ($tmpl->{'dns_sub'} && !$parent->{'dns'}) { return $text{'setup_edepdnssub'}; } } return undef; } # setup_dns(&domain) # Set up a zone for a domain sub setup_dns { &require_bind(); local $tmpl = &get_template($_[0]->{'template'}); local $ip = $_[0]->{'dns_ip'} || $_[0]->{'ip'}; local @extra_slaves = split(/\s+/, $tmpl->{'dns_ns'}); if ($_[0]->{'provision_dns'}) { # Create on provisioning server &$first_print($text{'setup_bind_provision'}); local $info = { 'domain' => $_[0]->{'dom'} }; if (@extra_slaves) { $info->{'slave'} = [ grep { $_ } map { &to_ipaddress($_) } @extra_slaves ]; } local $temp = &transname(); local $bind8::config{'auto_chroot'} = undef; local $bind8::config{'chroot'} = undef; if ($_[0]->{'alias'}) { &create_alias_records($temp, $_[0], $ip); } else { &create_standard_records($temp, $_[0], $ip); } local @recs = &bind8::read_zone_file($temp, $_[0]->{'dom'}); $info->{'record'} = [ &records_to_text($_[0], \@recs) ]; my ($ok, $msg) = &provision_api_call( "provision-dns-zone", $info, 0); if (!$ok || $msg !~ /host=(\S+)/) { &$second_print(&text('setup_ebind_provision', $msg)); return 0; } $_[0]->{'provision_dns_host'} = $1; &$second_print(&text('setup_bind_provisioned', $_[0]->{'provision_dns_host'})); } elsif (!$_[0]->{'subdom'} && !&under_parent_domain($_[0]) || $tmpl->{'dns_sub'} ne 'yes' || $_[0]->{'alias'}) { # Creating a new real zone &$first_print($text{'setup_bind'}); &obtain_lock_dns($_[0], 1); local $conf = &bind8::get_config(); local $base = $bconfig{'master_dir'} ? $bconfig{'master_dir'} : &bind8::base_directory($conf); local $file = &bind8::automatic_filename($_[0]->{'dom'}, 0, $base); local $dir = { 'name' => 'zone', 'values' => [ $_[0]->{'dom'} ], 'type' => 1, 'members' => [ { 'name' => 'type', 'values' => [ 'master' ] }, { 'name' => 'file', 'values' => [ $file ] } ] }; if ($tmpl->{'namedconf'} && $tmpl->{'namedconf'} ne 'none') { push(@{$dir->{'members'}}, &text_to_named_conf($tmpl->{'namedconf'})); } # Also notify slave servers, unless already added local @slaves = &bind8::list_slave_servers(); if (@slaves && !$tmpl->{'namedconf_no_also_notify'}) { local ($also) = grep { $_->{'name'} eq 'also-notify' } @{$dir->{'members'}}; if (!$also) { $also = { 'name' => 'also-notify', 'type' => 1 }; local @also; foreach my $s (@slaves) { push(@also, { 'name' => &to_ipaddress($s->{'host'}) }); } foreach my $s (@extra_slaves) { push(@also, { 'name' => &to_ipaddress($s) }); } @also = grep { $_->{'name'} } @also; $also->{'members'} = \@also; push(@{$dir->{'members'}}, $also); push(@{$dir->{'members'}}, { 'name' => 'notify', 'values' => [ 'yes' ] }); } } # Allow only localhost and slaves to transfer local @trans = ( { 'name' => '127.0.0.1' }, { 'name' => 'localnets' }, ); foreach my $s (@slaves) { push(@trans, { 'name' => &to_ipaddress($s->{'host'}) }); my $s6 = &to_ip6address($s->{'host'}); if ($s6) { push(@trans, { 'name' => $s6 }); } } foreach my $s (@extra_slaves) { push(@trans, { 'name' => &to_ipaddress($s) }); my $s6 = &to_ip6address($s); if ($s6) { push(@trans, { 'name' => $s6 }); } } @trans = grep { $_->{'name'} } @trans; local ($trans) = grep { $_->{'name'} eq 'allow-transfer' } @{$dir->{'members'}}; if (!$trans && !$tmpl->{'namedconf_no_allow_transfer'}) { $trans = { 'name' => 'allow-transfer', 'type' => 1, 'members' => \@trans }; push(@{$dir->{'members'}}, $trans); } local $pconf; local $indent = 0; if ($tmpl->{'dns_view'}) { # Adding inside a view. This may use named.conf, or an include # file references inside the view, if any $pconf = &bind8::get_config_parent(); local $view = &get_bind_view($conf, $tmpl->{'dns_view'}); if ($view) { local $addfile = &bind8::add_to_file(); local $addfileok; if ($bind8::config{'zones_file'} && $view->{'file'} ne $bind8::config{'zones_file'}) { # BIND module config asks for a file .. make # sure it is included in the view foreach my $vm (@{$view->{'members'}}) { if ($vm->{'file'} eq $addfile) { # Add file is OK $addfileok = 1; } } } if (!$addfileok) { # Add to named.conf $pconf = $view; $indent = 1; $dir->{'file'} = $view->{'file'}; } else { # Add to the file $dir->{'file'} = $addfile; $pconf = &bind8::get_config_parent($addfile); } $_[0]->{'dns_view'} = $tmpl->{'dns_view'}; } else { &error(&text('setup_ednsview', $tmpl->{'dns_view'})); } } else { # Adding at top level .. but perhaps in a different file $dir->{'file'} = &bind8::add_to_file(); $pconf = &bind8::get_config_parent($dir->{'file'}); } &bind8::save_directive($pconf, undef, [ $dir ], $indent); &flush_file_lines(); unlink($bind8::zone_names_cache); undef(@bind8::list_zone_names_cache); undef(@bind8::get_config_cache); # Work out if can copy from alias target - not possible if target # is a sub-domain, as they don't have their own domain. Also not # possible if target uses another domain's zone file to store its # records. local $copyfromalias = 0; if ($_[0]->{'alias'}) { local $target = &get_domain($_[0]->{'alias'}); if ($target && !$target->{'subdom'} && !$target->{'dns_submode'}) { $copyfromalias = 1; } } # Create the records file local $rootfile = &bind8::make_chroot($file); if (!-r $rootfile) { if ($copyfromalias) { &create_alias_records($file, $_[0], $ip); } else { &create_standard_records($file, $_[0], $ip); } &bind8::set_ownership($rootfile); } &$second_print($text{'setup_done'}); # If DNSSEC was requested, set it up if ($tmpl->{'dnssec'} eq 'yes') { &$first_print($text{'setup_dnssec'}); local $zone = &get_bind_zone($_[0]->{'dom'}); if (!defined(&bind8::supports_dnssec) || !&bind8::supports_dnssec()) { # Not supported &$second_print($text{'setup_enodnssec'}); } else { local ($ok, $size) = &bind8::compute_dnssec_key_size( $tmpl->{'dnssec_alg'}, 1); local $err; if (!$ok) { # Key size failed &$second_print( &text('setup_ednssecsize', $size)); } elsif ($err = &bind8::create_dnssec_key( $zone, $tmpl->{'dnssec_alg'}, $size, $tmpl->{'dnssec_single'})) { # Key generation failed &$second_print( &text('setup_ednsseckey', $err)); } elsif ($err = &bind8::sign_dnssec_zone($zone)) { # Zone signing failed &$second_print( &text('setup_ednssecsign', $err)); } else { # All done! &$second_print($text{'setup_done'}); } } } # Create on slave servers local $myip = $bconfig{'this_ip'} || &to_ipaddress(&get_system_hostname()); if (@slaves && !$_[0]->{'noslaves'}) { local $slaves = join(" ", map { $_->{'nsname'} || $_->{'host'} } @slaves); &create_zone_on_slaves($_[0], $slaves); } # If website has a *.domain.com ServerAlias, add * DNS record now if ($_[0]->{'web'} && &get_domain_web_star($_[0])) { &save_domain_matchall_record($_[0], 1); } &release_lock_dns($_[0], 1); } else { # Creating a sub-domain - add to parent's DNS zone. # This only happens if the parent zone has the same owner, and this # feature is enabled in templates, and this zone isn't an alias. local $parent = &get_domain($_[0]->{'subdom'}) || &get_domain($_[0]->{'parent'}); &$first_print(&text('setup_bindsub', $parent->{'dom'})); &obtain_lock_dns($parent); local $z = &get_bind_zone($parent->{'dom'}); if (!$z) { &error(&text('setup_ednssub', $parent->{'dom'})); } local $file = &bind8::find("file", $z->{'members'}); local $fn = $file->{'values'}->[0]; local @recs = &bind8::read_zone_file($fn, $parent->{'dom'}); $_[0]->{'dns_submode'} = 1; # So we know how this was done local ($already) = grep { $_->{'name'} eq $_[0]->{'dom'}."." } grep { $_->{'type'} eq 'A' } @recs; if ($already) { # A record with the same name as the sub-domain exists .. we # don't want to delete this later $_[0]->{'dns_subalready'} = 1; } local $ip = $_[0]->{'dns_ip'} || $_[0]->{'ip'}; &create_standard_records($fn, $_[0], $ip); &post_records_change($parent, \@recs); &release_lock_dns($parent); &$second_print($text{'setup_done'}); } ®ister_post_action(\&restart_bind, $_[0]); } sub slave_error_handler { $slave_error = $_[0]; } # delete_dns(&domain) # Delete a domain from the BIND config sub delete_dns { &require_bind(); if ($_[0]->{'provision_dns'}) { # Delete from provisioning server &$first_print($text{'delete_bind_provision'}); if ($_[0]->{'provision_dns_host'}) { local $info = { 'domain' => $_[0]->{'dom'}, 'host' => $_[0]->{'provision_dns_host'} }; my ($ok, $msg) = &provision_api_call( "unprovision-dns-zone", $info, 0); if (!$ok) { &$second_print(&text('delete_ebind_provision', $msg)); return 0; } delete($_[0]->{'provision_dns_host'}); &$second_print($text{'setup_done'}); } else { &$second_print($text{'delete_bind_provision_none'}); } } elsif (!$_[0]->{'dns_submode'}) { # Delete real domain &$first_print($text{'delete_bind'}); &obtain_lock_dns($_[0], 1); local $z = &get_bind_zone($_[0]->{'dom'}); if ($z) { # Delete any dnssec key if (defined(&bind8::supports_dnssec) && &bind8::supports_dnssec()) { &bind8::delete_dnssec_key($z); } # Delete the records file local $file = &bind8::find("file", $z->{'members'}); if ($file) { local $zonefile = &bind8::make_chroot($file->{'values'}->[0]); &unlink_file($zonefile); local $logfile = $zonefile.".log"; if (!-r $logfile) { $logfile = $zonefile.".jnl"; } if (-r $logfile) { &unlink_logged($logfile); } } # Delete from named.conf local $rootfile = &bind8::make_chroot($z->{'file'}); local $lref = &read_file_lines($rootfile); splice(@$lref, $z->{'line'}, $z->{'eline'} - $z->{'line'} + 1); &flush_file_lines($rootfile); # Clear zone names caches unlink($bind8::zone_names_cache); undef(@bind8::list_zone_names_cache); undef(@bind8::get_config_cache); &$second_print($text{'setup_done'}); } else { &$second_print($text{'save_nobind'}); } &delete_zone_on_slaves($_[0]); &release_lock_dns($_[0], 1); } else { # Delete records from parent zone local $parent = &get_domain($_[0]->{'subdom'}) || &get_domain($_[0]->{'parent'}); &$first_print(&text('delete_bindsub', $parent->{'dom'})); &obtain_lock_dns($parent); local $z = &get_bind_zone($parent->{'dom'}); if (!$z) { &$second_print($text{'save_nobind'}); return; } local $file = &bind8::find("file", $z->{'members'}); local $fn = $file->{'values'}->[0]; local @recs = &bind8::read_zone_file($fn, $parent->{'dom'}); local $withdot = $_[0]->{'dom'}."."; foreach $r (reverse(@recs)) { # Don't delete if outside sub-domain next if ($r->{'name'} !~ /\Q$withdot\E$/); # Don't delete if the same as an existing record next if ($r->{'name'} eq $withdot && $r->{'type'} eq 'A' && $_[0]->{'dns_subalready'}); &bind8::delete_record($fn, $r); } &post_records_change($parent, \@recs); &release_lock_dns($parent); &$second_print($text{'setup_done'}); $_[0]->{'dns_submode'} = 0; } ®ister_post_action(\&restart_bind, $_[0]); } # clone_dns(&domain, &old-domain) # Copy all DNS records to a new domain sub clone_dns { local ($d, $oldd) = @_; &$first_print($text{'clone_dns'}); if ($d->{'dns_submode'}) { # Record cloning not supported for DNS sub-domains &$second_print($text{'clone_dnssub'}); return 1; } local ($orecs, $ofile) = &get_domain_dns_records_and_file($oldd); local ($recs, $file) = &get_domain_dns_records_and_file($d); if (!$orecs) { &$second_print($text{'clone_dnsold'}); return 0; } if (!$recs) { &$second_print($text{'clone_dnsnew'}); return 0; } &obtain_lock_dns($d); # Copy over the records file local $absfile = &bind8::make_chroot($file); local $absofile = &bind8::make_chroot($ofile); ©_source_dest($absofile, $absfile); $recs = [ &bind8::read_zone_file($file, $d->{'dom'}) ]; &modify_records_domain_name($recs, $file, $oldd->{'dom'}, $d->{'dom'}); local $oldip = $oldd->{'dns_ip'} || $oldd->{'ip'}; local $newip = $d->{'dns_ip'} || $d->{'ip'}; if ($oldip ne $newip) { &modify_records_ip_address($recs, $file, $oldip, $newip); } if ($d->{'ip6'} && $d->{'ip6'} ne $oldd->{'ip6'}) { &modify_records_ip_address($recs, $file, $oldd->{'ip6'}, $d->{'ip6'}); } # Find and delete sub-domain records local @sublist = grep { $_->{'id'} ne $oldd->{'id'} && $_->{'id'} ne $d->{'id'} && $_->{'dom'} =~ /\.\Q$oldd->{'dom'}\E$/ } &list_domains(); foreach my $r (reverse(@$recs)) { foreach my $sd (@sublist) { if ($r->{'name'} eq $sd->{'dom'}."." || $r->{'name'} =~ /\.\Q$sd->{'dom'}\E\.$/) { &bind8::delete_record($file, $r); } } } &post_records_change($d, $recs, $file); &release_lock_dns($d); ®ister_post_action(\&restart_bind, $_[0]); &$second_print($text{'setup_done'}); return 1; } # create_zone_on_slaves(&domain, space-separate-slave-list) # Create a zone on all specified slaves, and updates the dns_slave key. # May print messages. sub create_zone_on_slaves { local ($d, $slaves) = @_; &require_bind(); local $myip = $bconfig{'this_ip'} || &to_ipaddress(&get_system_hostname()); &$first_print(&text('setup_bindslave', $slaves)); local @slaveerrs = &bind8::create_on_slaves( $d->{'dom'}, $myip, undef, $slaves, $d->{'dns_view'} || $tmpl->{'dns_view'}); if (@slaveerrs) { &$second_print($text{'setup_eslaves'}); foreach my $sr (@slaveerrs) { &$second_print( ($sr->[0]->{'nsname'} || $sr->[0]->{'host'}). " : ".$sr->[1]); } } else { &$second_print($text{'setup_done'}); } # Add to list of slaves where it succeeded local @newslaves; foreach my $s (split(/\s+/, $slaves)) { local ($err) = grep { $_->[0]->{'host'} eq $s } @slaveerrs; if (!$err) { push(@newslaves, $s); } } local @oldslaves = split(/\s+/, $d->{'dns_slave'}); $d->{'dns_slave'} = join(" ", &unique(@oldslaves, @newslaves)); ®ister_post_action(\&restart_bind, $d); } # delete_zone_on_slaves(&domain, [space-separate-slave-list]) # Delete a zone on all slave servers, from the dns_slave key. May print messages sub delete_zone_on_slaves { local ($d, $slaveslist) = @_; local @delslaves = $slaveslist ? split(/\s+/, $slaveslist) : split(/\s+/, $d->{'dns_slave'}); &require_bind(); if (@delslaves) { # Delete from slave servers &$first_print(&text('delete_bindslave', join(" ", @delslaves))); local $tmpl = &get_template($d->{'template'}); local @slaveerrs = &bind8::delete_on_slaves( $d->{'dom'}, \@delslaves, $d->{'dns_view'} || $tmpl->{'dns_view'}); if (@slaveerrs) { &$second_print($text{'delete_bindeslave'}); foreach my $sr (@slaveerrs) { &$second_print( ($sr->[0]->{'nsname'} || $sr->[0]->{'host'}). " : ".$sr->[1]); } } else { &$second_print($text{'setup_done'}); } # Update domain data my @newslaves; if ($slaveslist) { foreach my $s (split(/\s+/, $d->{'dns_slave'})) { if (&indexof($s, @delslaves) < 0) { push(@newslaves, $s); } } } if (@newslaves) { $d->{'dns_slave'} = join(" ", @newslaves); } else { delete($d->{'dns_slave'}); } } ®ister_post_action(\&restart_bind, $d); } # exists_on_slave(zone-name, &slave) # Returns "OK" if some zone exists on the given DNS slave, undef if not, or # an error message otherwise. sub exists_on_slave { my ($name, $slave) = @_; &remote_error_setup(\&bind8::slave_error_handler); &remote_foreign_require($slave, "bind8", "bind8-lib.pl"); return $bind8::slave_error if ($bind8::slave_error); my $z = &remote_foreign_call($slave, "bind8", "get_zone_name", $name, "any"); return $z ? "OK" : undef; } # modify_dns(&domain, &olddomain) # If the IP for this server has changed, update all records containing the old # IP to the new. sub modify_dns { if (!$_[0]->{'subdom'} && $_[1]->{'subdom'} && $_[0]->{'dns_submode'} || !&under_parent_domain($_[0]) && $_[0]->{'dns_submode'}) { # Converting from a sub-domain to top-level .. just delete and re-create &delete_dns($_[1]); delete($_[0]->{'dns_submode'}); &setup_dns($_[0]); return 1; } if ($_[0]->{'alias'} && $_[1]->{'alias'} && $_[0]->{'alias'} != $_[1]->{'alias'}) { # Alias target changed &delete_dns($_[1]); &setup_dns($_[0]); return 1; } &require_bind(); local $tmpl = &get_template($_[0]->{'template'}); local ($oldzonename, $newzonename, $lockon, $lockconf, $zdom); if ($_[0]->{'dns_submode'}) { # Get parent domain local $parent = &get_domain($_[0]->{'subdom'}) || &get_domain($_[0]->{'parent'}); &obtain_lock_dns($parent); $lockon = $parent; $zdom = $parent; $oldzonename = $newzonename = $parent->{'dom'}; } else { # Get this domain &obtain_lock_dns($_[0], 1); $lockon = $_[0]; $lockconf = 1; $zdom = $_[1]; $newzonename = $_[1]->{'dom'}; $oldzonename = $_[1]->{'dom'}; } local $oldip = $_[1]->{'dns_ip'} || $_[1]->{'ip'}; local $newip = $_[0]->{'dns_ip'} || $_[0]->{'ip'}; local $rv = 0; # Zone file name and records, if we read them local ($file, $recs); if ($_[0]->{'dom'} ne $_[1]->{'dom'} && $_[0]->{'provision_dns'}) { # Domain name has changed .. rename via API call &$first_print($text{'save_dns2_provision'}); local $info = { 'domain' => $_[1]->{'dom'}, 'host' => $_[0]->{'provision_dns_host'}, 'new-domain' => $_[0]->{'dom'} }; my ($ok, $msg) = &provision_api_call("modify-dns-zone", $info, 0); if (!$ok) { &$second_print(&text('disable_ebind_provision', $msg)); return 0; } &$second_print($text{'setup_done'}); # Rename records ($recs, $file) = &get_domain_dns_records_and_file($_[0]) if (!$file); if (!$file) { &$second_print($text{'save_nobind'}); &release_lock_dns($lockon, $lockconf); return 0; } &modify_records_domain_name($recs, $file, $_[1]->{'dom'}, $_[0]->{'dom'}); } elsif ($_[0]->{'dom'} ne $_[1]->{'dom'} && !$_[0]->{'provision_dns'}) { # Domain name has changed .. rename locally local $z = &get_bind_zone($zdom->{'dom'}); if (!$z) { # Zone not found! &$second_print($text{'save_dns2_ezone'}); &release_lock_dns($lockon, $lockconf); return 0; } local $nfn; local $file = &bind8::find("file", $z->{'members'}); if (!$_[0]->{'dns_submode'}) { # Domain name has changed .. rename zone file &$first_print($text{'save_dns2'}); local $fn = $file->{'values'}->[0]; $nfn = $fn; $nfn =~ s/$_[1]->{'dom'}/$_[0]->{'dom'}/; if ($fn ne $nfn) { &rename_logged(&bind8::make_chroot($fn), &bind8::make_chroot($nfn)) } $file->{'values'}->[0] = $nfn; $file->{'value'} = $nfn; # Change zone in .conf file $z->{'values'}->[0] = $_[0]->{'dom'}; $z->{'value'} = $_[0]->{'dom'}; &bind8::save_directive(&bind8::get_config_parent(), [ $z ], [ $z ], 0); &flush_file_lines(); } else { &$first_print($text{'save_dns6'}); $nfn = $file->{'values'}->[0]; } # Modify any records containing the old name &lock_file(&bind8::make_chroot($nfn)); local @recs = &bind8::read_zone_file($nfn, $oldzonename); &modify_records_domain_name(\@recs, $nfn, $_[1]->{'dom'}, $_[0]->{'dom'}); # Update SOA record &post_records_change($_[0], \@recs); $recs = \@recs; &unlock_file(&bind8::make_chroot($nfn)); $rv++; # Clear zone names caches unlink($bind8::zone_names_cache); undef(@bind8::list_zone_names_cache); &$second_print($text{'setup_done'}); if (!$_[0]->{'dns_submode'}) { local @slaves = split(/\s+/, $_[0]->{'dns_slave'}); if (@slaves) { # Rename on slave servers too &$first_print(&text('save_dns3', $_[0]->{'dns_slave'})); local @slaveerrs = &bind8::rename_on_slaves( $_[1]->{'dom'}, $_[0]->{'dom'}, \@slaves); if (@slaveerrs) { &$second_print($text{'save_bindeslave'}); foreach $sr (@slaveerrs) { &$second_print( ($sr->[0]->{'nsname'} || $sr->[0]->{'host'})." : ".$sr->[1]); } } else { &$second_print($text{'setup_done'}); } } } } if ($oldip ne $newip) { # IP address has changed .. need to update any records that use # the old IP &$first_print($text{'save_dns'}); ($recs, $file) = &get_domain_dns_records_and_file($_[0]) if (!$file); if (!$file) { &$second_print($text{'save_nobind'}); &release_lock_dns($lockon, $lockconf); return 0; } &modify_records_ip_address($recs, $file, $oldip, $newip, $_[0]->{'dom'}); $rv++; &$second_print($text{'setup_done'}); } if ($_[0]->{'mail'} && !$_[1]->{'mail'} && !$tmpl->{'dns_replace'}) { # Email was enabled .. add MX records ($recs, $file) = &get_domain_dns_records_and_file($_[0]) if (!$file); if (!$file) { &$second_print($text{'save_nobind'}); &release_lock_dns($lockon, $lockconf); return 0; } local ($mx) = grep { $_->{'type'} eq 'MX' && $_->{'name'} eq $_[0]->{'dom'}."." || $_->{'type'} eq 'A' && $_->{'name'} eq "mail.".$_[0]->{'dom'}."."} @$recs; if (!$mx) { &$first_print($text{'save_dns4'}); local $ip = $_[0]->{'dns_ip'} || $_[0]->{'ip'}; local $ip6 = $_[0]->{'ip6'}; &create_mx_records($file, $_[0], $ip, $ip6); &$second_print($text{'setup_done'}); $rv++; } } elsif (!$_[0]->{'mail'} && $_[1]->{'mail'} && !$tmpl->{'dns_replace'}) { # Email was disabled .. remove MX records, but only those that # point to this system or secondaries. ($recs, $file) = &get_domain_dns_records_and_file($_[0]) if (!$file); if (!$file) { &$second_print($text{'save_nobind'}); &release_lock_dns($lockon, $lockconf); return 0; } local $ip = $_[0]->{'dns_ip'} || $_[0]->{'ip'}; local $ip6 = $_[0]->{'ip6'}; local %ids = map { $_, 1 } split(/\s+/, $_[0]->{'mx_servers'}); local @slaves = grep { $ids{$_->{'id'}} } &list_mx_servers(); local @slaveips = map { &to_ipaddress($_->{'mxname'} || $_->{'host'}) } @slaves; foreach my $r (@$recs) { if ($r->{'type'} eq 'A' && $r->{'name'} eq "mail.".$_[0]->{'dom'}."." && $r->{'values'}->[0] eq $ip) { # mail.domain A record, pointing to our IP push(@mx, $r); } elsif ($r->{'type'} eq 'AAAA' && $r->{'name'} eq "mail.".$_[0]->{'dom'}."." && $r->{'values'}->[0] eq $ip6) { # mail.domain AAAA record, pointing to our IP push(@mx, $r); } elsif ($r->{'type'} eq 'MX' && $r->{'name'} eq $_[0]->{'dom'}.".") { # MX record for domain .. does it point to our IP? local $mxip = &to_ipaddress($r->{'values'}->[1]); if ($mxip eq $ip || &indexof($mxip, @slaveips) >= 0) { push(@mx, $r); } } } if (@mx) { &$first_print($text{'save_dns5'}); foreach my $r (reverse(@mx)) { &bind8::delete_record($file, $r); } &$second_print($text{'setup_done'}); $rv++; } } if ($_[0]->{'mx_servers'} ne $_[1]->{'mx_servers'} && $_[0]->{'mail'} && !$config{'secmx_nodns'}) { # Secondary MX servers have been changed - add or remove MX records &$first_print($text{'save_dns7'}); ($recs, $file) = &get_domain_dns_records_and_file($_[0]) if (!$file); if (!$file) { &$second_print($text{'save_nobind'}); &release_lock_dns($lockon, $lockconf); return 0; } local @newmxs = split(/\s+/, $_[0]->{'mx_servers'}); local @oldmxs = split(/\s+/, $_[1]->{'mx_servers'}); &foreign_require("servers", "servers-lib.pl"); local %servers = map { $_->{'id'}, $_ } (&servers::list_servers(), &list_mx_servers()); local $withdot = $_[0]->{'dom'}."."; # Add missing MX records foreach my $id (@newmxs) { if (&indexof($id, @oldmxs) < 0) { # A new MX .. add a record for it, if there isn't one local $s = $servers{$id}; local $mxhost = $s->{'mxname'} || $s->{'host'}; local $already = 0; foreach my $r (@$recs) { if ($r->{'type'} eq 'MX' && $r->{'name'} eq $withdot && $r->{'values'}->[1] eq $mxhost.".") { $already = 1; } } if (!$already) { &bind8::create_record($file, $withdot, undef, "IN", "MX", "10 $mxhost."); } } } # Remove those that are no longer needed local @mxs; foreach my $id (@oldmxs) { if (&indexof($id, @newmxs) < 0) { # An old MX .. remove it local $s = $servers{$id}; local $mxhost = $s->{'mxname'} || $s->{'host'}; foreach my $r (@$recs) { if ($r->{'type'} eq 'MX' && $r->{'name'} eq $withdot && $r->{'values'}->[1] eq $mxhost.".") { push(@mxs, $r); } } } } foreach my $r (reverse(@mxs)) { &bind8::delete_record($file, $r); } &$second_print($text{'setup_done'}); $rv++; } if ($_[0]->{'ip6'} && !$_[1]->{'ip6'}) { # IPv6 enabled &$first_print($text{'save_dnsip6on'}); ($recs, $file) = &get_domain_dns_records_and_file($_[0]) if (!$file); if (!$file) { &$second_print($text{'save_nobind'}); &release_lock_dns($lockon, $lockconf); return 0; } &add_ip6_records($_[0], $file); &$second_print($text{'setup_done'}); $rv++; } elsif (!$_[0]->{'ip6'} && $_[1]->{'ip6'}) { # IPv6 disabled &$first_print($text{'save_dnsip6off'}); ($recs, $file) = &get_domain_dns_records_and_file($_[0]) if (!$file); if (!$file) { &$second_print($text{'save_nobind'}); &release_lock_dns($lockon, $lockconf); return 0; } &remove_ip6_records($_[1], $file); &$second_print($text{'setup_done'}); $rv++; } elsif ($_[0]->{'ip6'} && $_[1]->{'ip6'} && $_[0]->{'ip6'} ne $_[1]->{'ip6'}) { # IPv6 address changed &$first_print($text{'save_dnsip6'}); ($recs, $file) = &get_domain_dns_records_and_file($_[0]) if (!$file); if (!$file) { &$second_print($text{'save_nobind'}); &release_lock_dns($lockon, $lockconf); return 0; } &modify_records_ip_address($recs, $file, $_[1]->{'ip6'}, $_[0]->{'ip6'}, $_[0]->{'dom'}); $rv++; &$second_print($text{'setup_done'}); } # Update SOA and upload records to provisioning server if ($file) { &post_records_change($_[0], $recs, $file); } # Release locks &release_lock_dns($lockon, $lockconf); ®ister_post_action(\&restart_bind, $_[0]) if ($rv); return $rv; } # join_record_values(&record, [always-one-line]) # Given the values for a record, joins them into a space-separated string # with quoting if needed sub join_record_values { local ($r, $oneline) = @_; local $j = join("", @{$r->{'values'}}); if ($r->{'type'} eq 'SOA' && !$oneline) { # Multiliple lines, with brackets local $v = $r->{'values'}; local $sep = "\n\t\t\t"; return "$v->[0] $v->[1] ($sep$v->[2]$sep$v->[3]". "$sep$v->[4]$sep$v->[5]$sep$v->[6] )"; } elsif (($r->{'type'} eq 'TXT' || $r->{'type'} eq 'SPF') && !$oneline && (length($j) > 255 || @{$r->{'values'}} > 1)) { # Multi-line text, possibly with brackets my $rv = &split_long_txt_record($j); $rv =~ s/\r?\n\s*/ /g; return $rv; } else { # All one one line local @rv; foreach my $v (@{$r->{'values'}}) { push(@rv, $v =~ /\s/ ? "\"$v\"" : $v); } return join(" ", @rv); } } # split_long_txt_record(string) # Split a TXT record at 80 char boundaries sub split_long_txt_record { local ($str) = @_; $str =~ s/^"//; $str =~ s/"$//; local @rv; while($str) { local $first = substr($str, 0, 80); $str = substr($str, 80); push(@rv, $first); } return "( ".join("\n\t", map { '"'.$_.'"' } @rv)." )"; } # create_mx_records(file, &domain, ip, ip6) # Adds MX records to a DNS domain sub create_mx_records { local ($file, $d, $ip, $ip6) = @_; local $withdot = $d->{'dom'}."."; &bind8::create_record($file, "mail.$withdot", undef, "IN", "A", $ip); if ($d->{'ip6'} && $ip6) { &bind8::create_record($file, "mail.$withdot", undef, "IN", "AAAA", $ip6); } &bind8::create_record($file, $withdot, undef, "IN", "MX", "5 mail.$withdot"); # Add MX records for slaves, if enabled if (!$config{'secmx_nodns'}) { local %ids = map { $_, 1 } split(/\s+/, $d->{'mx_servers'}); local @servers = grep { $ids{$_->{'id'}} } &list_mx_servers(); local $n = 10; foreach my $s (@servers) { local $mxhost = $s->{'mxname'} || $s->{'host'}; &bind8::create_record($file, $withdot, undef, "IN", "MX", "$n $mxhost."); $n += 5; } } } # create_standard_records(file, &domain, ip) # Adds to a records file the needed records for some domain sub create_standard_records { local ($file, $d, $ip) = @_; &require_bind(); local $rootfile = &bind8::make_chroot($file); local $tmpl = &get_template($d->{'template'}); local $serial = $bconfig{'soa_style'} ? &bind8::date_serial().sprintf("%2.2d", $bconfig{'soa_start'}) : time(); local %zd; &bind8::get_zone_defaults(\%zd); local @created_ns; if (!$tmpl->{'dns_replace'} || $d->{'dns_submode'}) { # Create records that are appropriate for this domain, as long as the # user hasn't selected a completely custom template, or records are # being added to an existing domain if (!$d->{'dns_submode'}) { # Only add SOA and NS if this is a new file, not a sub-domain # in an existing file &open_tempfile(RECS, ">$rootfile"); if ($bconfig{'master_ttl'}) { # Add a default TTL if ($tmpl->{'dns_ttl'} eq '') { &print_tempfile(RECS, "\$ttl $zd{'minimum'}$zd{'minunit'}\n"); } elsif ($tmpl->{'dns_ttl'} ne 'none') { &print_tempfile(RECS, "\$ttl $tmpl->{'dns_ttl'}\n"); } } &close_tempfile(RECS); local $master = &get_master_nameserver($tmpl); local $email = $bconfig{'tmpl_email'} || "root\@$master"; $email = &bind8::email_to_dotted($email); local $soa = "$master $email (\n". "\t\t\t$serial\n". "\t\t\t$zd{'refresh'}$zd{'refunit'}\n". "\t\t\t$zd{'retry'}$zd{'retunit'}\n". "\t\t\t$zd{'expiry'}$zd{'expunit'}\n". "\t\t\t$zd{'minimum'}$zd{'minunit'} )"; &bind8::create_record($file, "@", undef, "IN", "SOA", $soa); # Get nameservers from reseller, if any my @reselns; if ($d->{'reseller'} && defined(&get_reseller)) { foreach my $r (split(/\s+/, $d->{'reseller'})) { my $resel = &get_reseller($r); if ($resel->{'acl'}->{'defns'}) { @reselns = split(/\s+/, $resel->{'acl'}->{'defns'}); last; } } } if (@reselns) { # NS records come from reseller foreach my $ns (@reselns) { $ns .= "." if ($ns !~ /\.$/); &bind8::create_record($file, "@", undef, "IN", "NS", $ns); push(@created_ns, $ns); } } else { # Add NS records for master and auto-configured slaves if ($tmpl->{'dns_prins'}) { &bind8::create_record($file, "@", undef, "IN", "NS", $master); push(@created_ns, $master); } local $slave; local @slaves = &bind8::list_slave_servers(); foreach $slave (@slaves) { local @bn = $slave->{'nsname'} ? ( $slave->{'nsname'} ) : gethostbyname($slave->{'host'}); if ($bn[0]) { local $full = "$bn[0]."; &bind8::create_record( $file, "@", undef, "IN", "NS", $bn[0]."."); push(@created_ns, $bn[0]."."); } } # Add NS records from template foreach my $ns (&get_slave_nameservers($tmpl)) { &bind8::create_record($file, "@", undef, "IN", "NS", $ns); push(@created_ns, $ns); } } } # Work out which records are already in the file local $rd = $d; if ($d->{'dns_submode'}) { $rd = &get_domain($d->{'subdom'}) || &get_domain($d->{'parent'}); } local %already = map { $_->{'name'}, $_ } grep { $_->{'type'} eq 'A' } &bind8::read_zone_file($file, $rd->{'dom'}); # Work out which records to add local $withdot = $d->{'dom'}."."; local @addrecs = split(/\s+/, $tmpl->{'dns_records'}); if (!@addrecs || $addrecs[0] eq 'none') { @addrecs = @automatic_dns_records; } local %addrecs = map { $_ eq "@" ? $withdot : $_.".".$withdot, 1 } @addrecs; delete($addrescs{'noneselected'}); # Add standard records we don't have yet foreach my $n ($withdot, "www.$withdot", "ftp.$withdot", "m.$withdot") { if (!$already{$n} && $addrecs{$n}) { &bind8::create_record($file, $n, undef, "IN", "A", $ip); } } # If the master NS is in this zone and there is no A for it yet, add now foreach my $ns (@created_ns) { if ($ns !~ /\.$/) { $ns .= ".".$withdot; } if ($ns =~ /^([^\.]+)\.(\S+\.)$/ && $2 eq $withdot && !$already{$ns}) { &bind8::create_record($file, $ns, undef, "IN", "A", $ip); } } # Add the localhost record - yes, I know it's lame, but some # registrars require it! local $n = "localhost.$withdot"; if (!$already{$n} && $addrecs{$n}) { &bind8::create_record($file, $n, undef, "IN", "A", "127.0.0.1"); } # If the hostname of the system is within this domain, add a record # for it my $hn = &get_system_hostname(); if ($hn =~ /\.\Q$d->{'dom'}\E$/ && !$already{$hn."."}) { &bind8::create_record($file, $hn.".", undef, "IN", "A", &get_default_ip()); } # If requested, add webmail and admin records if ($d->{'web'} && &has_webmail_rewrite($d)) { &add_webmail_dns_records_to_file($d, $tmpl, $file, \%already); } # For mail domains, add MX to this server. Any IPv6 AAAA record is # cloned later if ($d->{'mail'}) { &create_mx_records($file, $d, $ip, undef); } # Add SPF record for domain, if defined and if it's not a sub-domain if ($tmpl->{'dns_spf'} ne "none" && !$d->{'dns_submode'}) { local $str = &bind8::join_spf(&default_domain_spf($d)); &bind8::create_record($file, $withdot, undef, "IN", "TXT", "\"$str\""); if ($bind8::config{'spf_record'}) { &bind8::create_record($file, $withdot, undef, "IN", "SPF", "\"$str\""); } } # Add DMARC record for domain, if defined and if it's not a sub-domain if ($tmpl->{'dns_dmarc'} ne "none" && !$d->{'dns_submode'}) { local $str = &join_dmarc(&default_domain_dmarc($d)); &bind8::create_record($file, "_dmarc.".$withdot, undef, "IN", "TXT", "\"$str\""); } } if ($tmpl->{'dns'} && (!$d->{'dns_submode'} || !$tmpl->{'dns_replace'})) { # Add or use the user-defined records template, if defined and if this # isn't a sub-domain being added to an existing file OR if we are just # adding records &open_tempfile(RECS, ">>$rootfile"); local %subs = %$d; $subs{'serial'} = $serial; $subs{'dnsemail'} = $d->{'emailto_addr'}; $subs{'dnsemail'} =~ s/\@/./g; local $recs = &substitute_domain_template( join("\n", split(/\t+/, $tmpl->{'dns'}))."\n", \%subs); &print_tempfile(RECS, $recs); &close_tempfile(RECS); } if ($d->{'ip6'}) { # Create IPv6 records for IPv4 &add_ip6_records($d, $file); } } # create_alias_records(file, &domain, ip) # For a domain that is an alias, copy records from its target sub create_alias_records { local ($file, $d, $ip) = @_; local $tmpl = &get_template($d->{'template'}); local $aliasd = &get_domain($d->{'alias'}); local ($recs, $aliasfile) = &get_domain_dns_records_and_file($aliasd); $aliasfile || &error("No zone file for alias target $aliasd->{'dom'} found"); @$recs || &error("No records for alias target $aliasd->{'dom'} found"); local $olddom = $aliasd->{'dom'}; local $dom = $d->{'dom'}; local $oldip = $aliasd->{'ip'}; local @sublist = grep { $_->{'id'} ne $aliasd->{'id'} && $_->{'dom'} =~ /\.\Q$aliasd->{'dom'}\E$/ } &list_domains(); RECORD: foreach my $r (@$recs) { if ($d->{'dns_submode'} && ($r->{'type'} eq 'NS' || $r->{'type'} eq 'SOA')) { # Skip SOA and NS records for sub-domains in the same file next; } if ($r->{'type'} eq 'NSEC' || $r->{'type'} eq 'NSEC3' || $r->{'type'} eq 'RRSIG' || $r->{'type'} eq 'DNSKEY') { # Skip DNSSEC records, as they get re-generated next; } if ($r->{'defttl'}) { # Add default TTL &bind8::create_defttl($file, $r->{'defttl'}); next; } if (!$r->{'type'}) { # Skip special directives, like $generate next; } foreach my $sd (@sublist) { if ($r->{'name'} eq $sd->{'dom'}."." || $r->{'name'} =~ /\.\Q$sd->{'dom'}\E\.$/) { # Skip records in sub-domains of the source next RECORD; } } $r->{'name'} =~ s/\Q$olddom\E\.$/$dom\./i; # Change domain name to alias in record values, unless it is an NS # that is set in the template my %tmplns; my $master = &get_master_nameserver($tmpl); $tmplns{$master} = 1; foreach my $ns (&get_slave_nameservers($tmpl)) { $tmplns{$ns} = 1; } if ($r->{'type'} ne 'NS' || !$tmplns{$r->{'values'}->[0]}) { foreach my $v (@{$r->{'values'}}) { $v =~ s/\Q$olddom\E/$dom/i; $v =~ s/\Q$oldip\E$/$ip/i; } } my $str; my $joined = join("", @{$r->{'values'}}); if ($r->{'type'} eq 'TXT' && length($joined) > 80) { $str = &split_long_txt_record($joined); } else { $str = &join_record_values($r); } &bind8::create_record($file, $r->{'name'}, $r->{'ttl'}, 'IN', $r->{'type'}, $str); } } # get_master_nameserver(&template) # Returns default primary NS name sub get_master_nameserver { local ($tmpl) = @_; &require_bind(); local $tmaster = $tmpl->{'dns_master'} eq 'none' ? undef : $tmpl->{'dns_master'}; local $master = $tmaster || $bconfig{'default_prins'} || &get_system_hostname(); $master .= "." if ($master !~ /\.$/); return $master; } # get_slave_nameserver(&template) # Returns default additional slave NS names sub get_slave_nameservers { local ($tmpl) = @_; local @rv; foreach my $ns (split(/\s+/, $tmpl->{'dns_ns'})) { $ns .= "." if ($ns !~ /\.$/); push(@rv, $ns); } return @rv; } # add_webmail_dns_records(&domain) # Adds the webmail and admin DNS records, if requested in the template sub add_webmail_dns_records { local ($d) = @_; local $tmpl = &get_template($d->{'template'}); local ($recs, $file) = &get_domain_dns_records_and_file($d); return 0 if (!$file); local $count = &add_webmail_dns_records_to_file($d, $tmpl, $file); if ($count) { &post_records_change($d, $recs, $file); ®ister_post_action(\&restart_bind, $d); } return $count; } # add_webmail_dns_records_to_file(&domain, &tmpl, file, [&already-got]) # Adds the webmail and admin DNS records to a specific file, if requested # in the template sub add_webmail_dns_records_to_file { local ($d, $tmpl, $file, $already) = @_; local $count = 0; local $ip = $d->{'dns_ip'} || $d->{'ip'}; foreach my $r ('webmail', 'admin') { local $n = "$r.$d->{'dom'}."; if ($tmpl->{'web_'.$r} && (!$already || !$already->{$n})) { &bind8::create_record($file, $n, undef, "IN", "A", $ip); $count++; } } return $count; } # remove_webmail_dns_records(&domain) # Remove the webmail and admin DNS records sub remove_webmail_dns_records { local ($d) = @_; local ($recs, $file) = &get_domain_dns_records_and_file($d); return 0 if (!$file); local $count = 0; foreach my $r (reverse('webmail', 'admin')) { local $n = "$r.$d->{'dom'}."; local ($rec) = grep { $_->{'name'} eq $n } @$recs; if ($rec) { &bind8::delete_record($rec->{'file'}, $rec); $count++; } } if ($count) { &post_records_change($d, $recs, $file); ®ister_post_action(\&restart_bind, $d); } return $count; } # add_ip6_records(&domain, [file]) # For each A record for the domain whose value is it's IPv4 address, add an # AAAA record with the v6 address. sub add_ip6_records { local ($d, $file) = @_; &require_bind(); $file ||= &get_domain_dns_file($d); return 0 if (!$file); # Work out which AAAA records we already have local @recs = &bind8::read_zone_file($file, $d->{'dom'}); local %already; foreach my $r (@recs) { if ($r->{'type'} eq 'AAAA' && $r->{'values'}->[0] eq $d->{'ip6'}) { $already{$r->{'name'}}++; } } # Find all possible sub-domains, so we don't clone IPs for them my @dnames; foreach my $od (&list_domains()) { if ($od->{'dns'} && $od->{'id'} ne $d->{'id'} && $od->{'dom'} =~ /\.\Q$d->{'dom'}\E$/) { push(@dnames, $od->{'dom'}); } } # Clone A records my $count = 0; my $withdot = $d->{'dom'}."."; foreach my $r (@recs) { if ($r->{'type'} eq 'A' && $r->{'values'}->[0] eq $d->{'ip'} && !$already{$r->{'name'}} && ($r->{'name'} eq $withdot || $r->{'name'} =~ /\.\Q$withdot\E$/)) { # Check if this record is in any sub-domain of this one my $insub = 0; foreach my $od (@dnames) { my $odwithdot = $od."."; if ($r->{'name'} eq $odwithdot || $r->{'name'} =~ /\.\Q$odwithdot\E$/) { $insub = 1; last; } } if (!$insub) { &bind8::create_record($file, $r->{'name'}, $r->{'ttl'}, 'IN', 'AAAA', $d->{'ip6'}); $count++; } } } return $count; } # remove_ip6_records(&domain, [file], [&records]) # Delete all AAAA records whose value is the domain's IP6 address sub remove_ip6_records { local ($d, $file, $recs) = @_; &require_bind(); $file ||= &get_domain_dns_file($d); return 0 if (!$file); $recs ||= [ &bind8::read_zone_file($file, $d->{'dom'}) ]; my $withdot = $d->{'dom'}."."; for(my $i=@$recs-1; $i>=0; $i--) { my $r = $recs->[$i]; if ($r->{'type'} eq 'AAAA' && $r->{'values'}->[0] eq $d->{'ip6'} && ($r->{'name'} eq $withdot || $r->{'name'} =~ /\.\Q$withdot\E$/)) { &bind8::delete_record($file, $r); splice(@$recs, $i, 1); } } } # save_domain_matchall_record(&domain, star) # Add or remove a *.domain.com wildcard DNS record, pointing to the main # IP address. Used in conjuction with save_domain_web_star. sub save_domain_matchall_record { local ($d, $star) = @_; local ($recs, $file) = &get_domain_dns_records_and_file($d); return 0 if (!$file); local $withstar = "*.".$d->{'dom'}."."; local ($r) = grep { $_->{'name'} eq $withstar } @$recs; local $any = 0; if ($star && !$r) { # Need to add my $ip = $d->{'dns_ip'} || $d->{'ip'}; &bind8::create_record($file, $withstar, undef, "IN", "A", $ip); $any++; } elsif (!$star && $r) { # Need to remove &bind8::delete_record($file, $r); $any++; } if ($any) { my $err = &post_records_change($d, $recs, $file); return 0 if ($err); ®ister_post_action(\&restart_bind, $d); } return $any; } # validate_dns(&domain, [&records], [records-only]) # Check for the DNS domain and records file sub validate_dns { local ($d, $recs, $recsonly) = @_; local $file; if ($d->{'dns_submode'}) { # For a sub-domain, don't complain if parent is disabled my $parent = &get_domain($d->{'parent'}); if ($parent && $parent->{'disabled'}) { return undef; } } if (!$recs) { ($recs, $file) = &get_domain_dns_records_and_file($d); return &text('validate_edns', "<tt>$d->{'dom'}</tt>") if (!$file); } return &text('validate_ednsfile', "<tt>$d->{'dom'}</tt>") if (!@$recs); local $absfile; if (!$d->{'provision_dns'} && $file) { $absfile = &bind8::make_chroot( &bind8::absolute_path($file)); return &text('validate_ednsfile2', "<tt>$absfile</tt>") if (!-r $absfile); } if (!$d->{'provision_dns'} && !$d->{'dns_submode'}) { # Make sure it is a master local $zone = &get_bind_zone($d->{'dom'}); return &text('validate_edns', "<tt>$d->{'dom'}</tt>") if (!$zone); local $type = &bind8::find_value("type", $zone->{'members'}); return &text('validate_ednsnotype', "<tt>$d->{'dom'}</tt>") if (!$type); return &text('validate_ednstype', "<tt>$d->{'dom'}</tt>", "<tt>$type</tt>", "<tt>master</tt>") if ($type ne "master"); } # Check for critical records, and that www.$dom and $dom resolve to the # expected IP address (if we have a website) if ($d->{'dns_submode'}) { # Only care about records within this domain $recs = [ grep { $_->{'name'} eq $d->{'dom'}.'.' || $_->{'name'} =~ /\.\Q$d->{'dom'}\E\.$/ } @$recs ]; } local %got; local $ip = $d->{'dns_ip'} || $d->{'ip'}; foreach my $r (@$recs) { $got{uc($r->{'type'})}++; } $d->{'dns_submode'} || $got{'SOA'} || return $text{'validate_ednssoa2'}; $got{'A'} || return $text{'validate_ednsa2'}; if (&domain_has_website($d)) { foreach my $n ($d->{'dom'}.'.', 'www.'.$d->{'dom'}.'.') { my @nips = map { $_->{'values'}->[0] } grep { $_->{'type'} eq 'A' && $_->{'name'} eq $n } @$recs; if (@nips && &indexof($ip, @nips) < 0) { return &text('validate_ednsip', "<tt>$n</tt>", "<tt>".join(' or ', @nips)."</tt>", "<tt>$ip</tt>"); } } } # If domain has email, make sure MX record points to this system if ($d->{'mail'} && $config{'mx_validate'}) { local @mxs = grep { $_->{'name'} eq $d->{'dom'}.'.' && $_->{'type'} eq 'MX' } @$recs; local $defip = &get_default_ip(); local %inuse = &interface_ip_addresses(); if (@mxs) { local $found; local @mxips; foreach my $mx (@mxs) { local $mxh = $mx->{'values'}->[1]; $mxh .= ".".$d->{'dom'} if ($mxh !~ /\.$/); $mxh =~ s/\.$//; local $ip = &to_ipaddress($mxh); if ($ip eq $d->{'ip'} || $ip eq $d->{'dns_ip'} || $ip eq $defip || $inuse{$ip}) { $found = $ip; last; } local ($arec) = grep { $_->{'name'} eq $mxh."." && $_->{'type'} eq 'A' } @$recs; if ($arec) { $ip = $arec->{'values'}->[0]; if ($ip eq $d->{'ip'} || $ip eq $d->{'dns_ip'} || $ip eq $defip) { $found = $ip; last; } } push(@mxips, $mxh); } if (!$found) { return &text('validate_ednsmx', join(" ", @mxips)); } } } # If possible, run named-checkzone if (defined(&bind8::supports_check_zone) && &bind8::supports_check_zone() && !$d->{'provision_dns'} && !$d->{'dns_submode'} && !$recsonly) { local $z = &get_bind_zone($d->{'dom'}); if ($z) { local @errs = &bind8::check_zone_records($z); if (@errs) { return &text('validate_ednscheck', join("<br>", map { &html_escape($_) } @errs)); } } } # Check slave servers if (!$d->{'dns_submode'} && !$recsonly) { my @slaves = &bind8::list_slave_servers(); foreach my $sn (split(/\s+/, $d->{'dns_slave'})) { my ($slave) = grep { $_->{'nsname'} eq $sn || $_->{'host'} eq $sn } @slaves; if ($slave) { my $ok = &exists_on_slave($d->{'dom'}, $slave); if (!$ok) { return &text('validate_ednsslave', $slave->{'host'}); } elsif ($ok ne "OK") { return &text('validate_ednsslave2', $slave->{'host'}, $ok); } } } } return undef; } # disable_dns(&domain) # Re-names this domain in named.conf with the .disabled suffix sub disable_dns { local ($d) = @_; if ($d->{'provision_dns'}) { # Lock on provisioning server &$first_print($text{'disable_bind_provision'}); local $info = { 'domain' => $d->{'dom'}, 'host' => $d->{'provision_dns_host'}, 'disable' => '' }; my ($ok, $msg) = &provision_api_call("modify-dns-zone", $info, 0); if (!$ok) { &$second_print(&text('disable_ebind_provision', $msg)); return 0; } &$second_print($text{'setup_done'}); } else { # Lock locally &$first_print($text{'disable_bind'}); if ($d->{'dns_submode'}) { # Disable is not done for sub-domains &$second_print($text{'disable_bindnosub'}); return; } &obtain_lock_dns($d, 1); &require_bind(); local $z = &get_bind_zone($d->{'dom'}); if ($z) { local $rootfile = &bind8::make_chroot($z->{'file'}); $z->{'values'}->[0] = $d->{'dom'}.".disabled"; &bind8::save_directive(&bind8::get_config_parent(), [ $z ], [ $z ], 0); &flush_file_lines(); # Rename all records in the domain with the new .disabled name local $file = &bind8::find("file", $z->{'members'}); local $fn = $file->{'values'}->[0]; local @recs = &bind8::read_zone_file( $fn, $d->{'dom'}.".disabled"); foreach my $r (@recs) { if ($r->{'name'} =~ /\.\Q$d->{'dom'}\E\.$/ || $r->{'name'} eq $d->{'dom'}.".") { # Need to rename &bind8::modify_record($fn, $r, $r->{'name'}."disabled.", $r->{'ttl'}, $r->{'class'}, $r->{'type'}, &join_record_values($r), $r->{'comment'}); } } # Clear zone names caches undef(@bind8::list_zone_names_cache); &$second_print($text{'setup_done'}); ®ister_post_action(\&restart_bind, $d); # If on any slaves, delete there too $d->{'old_dns_slave'} = $d->{'dns_slave'}; &delete_zone_on_slaves($d); } else { &$second_print($text{'save_nobind'}); } &release_lock_dns($d, 1); } } # enable_dns(&domain) # Re-names this domain in named.conf to remove the .disabled suffix sub enable_dns { if ($d->{'provision_dns'}) { # Unlock on provisioning server &$first_print($text{'enable_bind_provision'}); local $info = { 'domain' => $d->{'dom'}, 'host' => $d->{'provision_dns_host'}, 'enable' => '' }; my ($ok, $msg) = &provision_api_call("modify-dns-zone", $info, 0); if (!$ok) { &$second_print(&text('disable_ebind_provision', $msg)); return 0; } &$second_print($text{'setup_done'}); } else { &$first_print($text{'enable_bind'}); if ($d->{'dns_submode'}) { # Disable is not done for sub-domains &$second_print($text{'enable_bindnosub'}); return; } &obtain_lock_dns($d, 1); &require_bind(); local $z = &get_bind_zone($d->{'dom'}); if ($z) { local $rootfile = &bind8::make_chroot($z->{'file'}); $z->{'values'}->[0] = $d->{'dom'}; &bind8::save_directive( &bind8::get_config_parent(), [ $z ], [ $z ], 0); &flush_file_lines(); # Fix all records in the domain with the .disabled name local $file = &bind8::find("file", $z->{'members'}); local $fn = $file->{'values'}->[0]; local @recs = &bind8::read_zone_file($fn, $d->{'dom'}); foreach my $r (@recs) { if ($r->{'name'} =~ /\.\Q$d->{'dom'}\E\.disabled\.$/ || $r->{'name'} eq $d->{'dom'}.".disabled.") { # Need to rename $r->{'name'} =~ s/\.disabled\.$/\./; &bind8::modify_record($fn, $r, $r->{'name'}, $r->{'ttl'}, $r->{'class'}, $r->{'type'}, &join_record_values($r), $r->{'comment'}); } } # Clear zone names caches undef(@bind8::list_zone_names_cache); &$second_print($text{'setup_done'}); ®ister_post_action(\&restart_bind, $d); # If it used to be on any slaves, enable too $d->{'dns_slave'} = $d->{'old_dns_slave'}; &create_zone_on_slaves($d, $d->{'dns_slave'}); delete($d->{'old_dns_slave'}); } else { &$second_print($text{'save_nobind'}); } &release_lock_dns($d, 1); } } # get_bind_zone(name, [&config], [file]) # Returns the zone structure for the named domain, possibly with .disabled sub get_bind_zone { &require_bind(); local $conf = $_[1] ? $_[1] : $_[2] ? [ &bind8::read_config_file($_[2]) ] : &bind8::get_config(); local @zones = &bind8::find("zone", $conf); local ($v, $z); foreach $v (&bind8::find("view", $conf)) { push(@zones, &bind8::find("zone", $v->{'members'})); } local ($z) = grep { lc($_->{'value'}) eq lc($_[0]) || lc($_->{'value'}) eq lc("$_[0].disabled") } @zones; return $z; } # restart_bind(&domain) # Signal BIND to re-load its configuration sub restart_bind { local $p = $_[0] ? $_[0]->{'provision_dns'} : $config{'provision_dns'}; if ($p) { # Hosted on a provisioning server, so nothing to do return 1; } &$first_print($text{'setup_bindpid'}); &require_bind(); local $bindlock = "$module_config_directory/bind-restart"; &lock_file($bindlock); local $pid = &get_bind_pid(); if ($pid) { my $err = &bind8::restart_bind(); if ($err) { &$second_print(&text('setup_ebindpid', $err)); } else { &$second_print($text{'setup_done'}); } $rv = 1; } else { &$second_print($text{'setup_notrun'}); $rv = 0; } if (&bind8::list_slave_servers()) { # Re-start on slaves too &$first_print(&text('setup_bindslavepids')); local @slaveerrs = &bind8::restart_on_slaves(); if (@slaveerrs) { &$second_print($text{'setup_bindeslave'}); foreach $sr (@slaveerrs) { &$second_print($sr->[0]->{'host'}." : ".$sr->[1]); } } else { &$second_print($text{'setup_done'}); } } &unlock_file($bindlock); return $rv; } # reload_bind_records(&domain) # Tell BIND to reload the DNS records in some zone, using rndc / ndc if possible sub reload_bind_records { local ($d) = @_; if ($d->{'provision_dns'}) { # Done remotely when records are uploaded return undef; } &require_bind(); if (defined(&bind8::restart_zone)) { local $err = &bind8::restart_zone($d->{'dom'}, $d->{'dns_view'}); return undef if (!$err); } &push_all_print(); &set_all_null_print(); local $rv = &restart_bind($d); &pop_all_print(); return $rv; } # check_dns_clash(&domain, [changing]) # Returns 1 if a domain already exists in BIND sub check_dns_clash { local ($d, $field) = @_; if ($d->{'provision_dns'}) { # Check on remote provisioning server if (!$field || $field eq 'dom') { my ($ok, $msg) = &provision_api_call( "check-dns-zone", { 'domain' => $d->{'dom'} }); return &text('provision_ednscheck', $msg) if (!$ok); if ($msg =~ /host=/) { return &text('provision_edns', $d->{'db'}); } } } else { # Check locally if (!$field || $field eq 'dom') { local ($czone) = &get_bind_zone($d->{'dom'}); return $czone ? 1 : 0; } } return 0; } # get_bind_pid() # Returns the BIND PID, if it is running sub get_bind_pid { &require_bind(); local $pidfile = &bind8::get_pid_file(); return &check_pid_file(&bind8::make_chroot($pidfile, 1)); } # backup_dns(&domain, file) # Save all the virtual server's DNS records as a separate file sub backup_dns { my ($d, $file) = @_; &require_bind(); return 1 if ($d->{'dns_submode'}); # backed up in parent &$first_print($text{'backup_dnscp'}); local ($recs, $zonefile) = &get_domain_dns_records_and_file($d); if ($zonefile) { local $absfile = &bind8::make_chroot( &bind8::absolute_path($zonefile)); if (-r $absfile) { ©_write_as_domain_user($d, $absfile, $file); &$second_print($text{'setup_done'}); return 1; } else { &$second_print(&text('backup_dnsnozonefile', "<tt>$zonefile</tt>")); return 0; } } else { &$second_print($text{'backup_dnsnozone'}); return 0; } } # restore_dns(&domain, file, &options) # Update the virtual server's DNS records from the backup file, except the SOA sub restore_dns { &require_bind(); return 1 if ($_[0]->{'dns_submode'}); # restored in parent &$first_print($text{'restore_dnscp'}); &obtain_lock_dns($_[0], 1); local ($recs, $file) = &get_domain_dns_records_and_file($_[0]); if ($file) { local $absfile = &bind8::make_chroot( &bind8::absolute_path($file)); local @thisrecs; if ($_[2]->{'wholefile'}) { # Copy whole file ©_source_dest($_[1], $absfile); &bind8::set_ownership($file); } else { # Only copy section after SOA @thisrecs = &bind8::read_zone_file($file, $_[0]->{'dom'}.($_[0]->{'disabled'} ? ".disabled" : "")); local $srclref = &read_file_lines($_[1], 1); local $dstlref = &read_file_lines($absfile); local ($srcstart, $srcend) = &except_soa($_[0], $_[1]); local ($dststart, $dstend) = &except_soa($_[0], $absfile); splice(@$dstlref, $dststart, $dstend - $dststart + 1, @$srclref[$srcstart .. $srcend]); &flush_file_lines($absfile); } # Re-read records, bump SOA and upload records to provisioning server local @recs = &bind8::read_zone_file($file, $_[0]->{'dom'}); &post_records_change($_[0], \@recs, $file); # Need to update IP addresses local $r; local ($baserec) = grep { $_->{'type'} eq "A" && ($_->{'name'} eq $_[0]->{'dom'}."." || $_->{'name'} eq '@') } @recs; local $ip = $_[0]->{'dns_ip'} || $_[0]->{'ip'}; local $baseip = $_[0]->{'old_dns_ip'} ? $_[0]->{'old_dns_ip'} : $_[0]->{'old_ip'} ? $_[0]->{'old_ip'} : $baserec ? $baserec->{'values'}->[0] : undef; if ($baseip) { &modify_records_ip_address(\@recs, $file, $baseip, $ip); } # Need to update IPv6 address local ($baserec6) = grep { $_->{'type'} eq "AAAA" && ($_->{'name'} eq $_[0]->{'dom'}."." || $_->{'name'} eq '@') } @recs; local $ip6 = $_[0]->{'ip6'}; local $baseip6 = $_[0]->{'old_ip6'} ? $_[0]->{'old_ip6'} : $baserec6 ? $baserec6->{'values'}->[0] : undef; if ($baseip6 && $ip6) { # Update to new v6 address &modify_records_ip_address(\@recs, $file, $baseip6, $ip6); } elsif ($baseip6 && !$ip6) { # This domain doesn't have a v6 address now, so remove AAAAs &remove_ip6_records($_[0], $file, \@recs); } # Replace NS records with those from new system if (!$_[2]->{'wholefile'}) { local @thisns = grep { $_->{'type'} eq 'NS' } @thisrecs; local @ns = grep { $_->{'type'} eq 'NS' } @recs; foreach my $r (@thisns) { # Create NS records that were in new system's file &bind8::create_record($file, $r->{'name'}, $r->{'ttl'}, $r->{'class'}, $r->{'type'}, &join_record_values($r), $r->{'comment'}); } foreach my $r (reverse(@ns)) { # Remove old NS records that we copied over &bind8::delete_record($file, $r); } } # Make sure any SPF record contains this system's default IP local @types = $bind8::config{'spf_record'} ? ( "SPF", "TXT" ) : ( "SPF" ); foreach my $t (@types) { local ($r) = grep { $_->{'type'} eq $t && $r->{'name'} eq $d->{'dom'}.'.' } @recs; next if (!$r); local $spf = &bind8::parse_spf(@{$r->{'values'}}); local $defip = &get_default_ip(); if (&indexof($defip, @{$spf->{'ip4'}}) < 0) { push(@{$spf->{'ip4'}}, $defip); local $str = &bind8::join_spf($spf); &bind8::modify_record($r->{'file'}, $r, $r->{'name'}, $r->{'ttl'}, $r->{'class'}, $r->{'type'}, "\"$str\"", $r->{'comment'}); } } &$second_print($text{'setup_done'}); ®ister_post_action(\&restart_bind, $_[0]); return 1; } else { &$second_print($text{'backup_dnsnozone'}); return 0; } &release_lock_dns($_[0], 1); } # modify_records_ip_address(&records, filename, oldip, newip, [domain]) # Update the IP address in all DNS records sub modify_records_ip_address { local ($recs, $fn, $oldip, $newip, $dname) = @_; local $count = 0; foreach my $r (@$recs) { my $changed = 0; if ($dname && $r->{'name'} !~ /\.\Q$dname\E\.$/i && $r->{'name'} !~ /^\Q$dname\E\.$/i) { # Out of zone record .. skip it next; } if (($r->{'type'} eq "A" || $r->{'type'} eq "AAAA") && $r->{'values'}->[0] eq $oldip) { # Address record - just replace IP $r->{'values'}->[0] = $newip; $changed = 1; } elsif (($r->{'type'} eq "SPF" || $r->{'type'} eq "TXT" && $r->{'values'}->[0] =~ /^v=spf/) && $r->{'values'}->[0] =~ /$oldip/) { # SPF record - replace ocurrances of IP $r->{'values'}->[0] =~ s/$oldip/$newip/g; $changed = 1; } if ($changed) { &bind8::modify_record($fn, $r, $r->{'name'}, $r->{'ttl'},$r->{'class'}, $r->{'type'}, &join_record_values($r, $r->{'eline'} == $r->{'line'}), $r->{'comment'}); $count++; } } return $count; } # modify_records_domain_name(&records, file, old-domain, new-domain) # Change the domain name in DNS record names and values sub modify_records_domain_name { local ($recs, $fn, $olddom, $newdom) = @_; foreach my $r (@$recs) { next if (!$r->{'name'}); # TTL or generator if ($r->{'name'} eq $olddom.".") { $r->{'name'} = $newdom."."; } elsif ($r->{'name'} eq $olddom.".disabled.") { $r->{'name'} = $newdom.".disabled."; } else { $r->{'name'} =~ s/\.$olddom(\.disabled)?\.$/\.$newdom$1\./; } if ($r->{'realname'} eq $olddom.".") { $r->{'realname'} = $newdom."."; } elsif ($r->{'realname'} eq $olddom.".disabled.") { $r->{'realname'} = $newdom."."; } else { $r->{'realname'} =~ s/\.$olddom(\.disabled)?\.$/\.$newdom$1\./; } if ($r->{'type'} eq 'SPF' || $r->{'type'} eq 'TXT' && $r->{'values'}->[0] =~ /^v=spf/) { # Fix SPF TXT record $r->{'values'}->[0] =~ s/$olddom/$newdom/; } if ($r->{'type'} eq 'MX') { # Fix mail server in MX record $r->{'values'}->[1] =~ s/$olddom/$newdom/; } &bind8::modify_record($fn, $r, $r->{'name'}, $r->{'ttl'}, $r->{'class'}, $r->{'type'}, &join_record_values($r, $r->{'eline'} == $r->{'line'}), $r->{'comment'}); } } # except_soa(&domain, file) # Returns the start and end lines of a records file for the entries # after the SOA. sub except_soa { local $bind8::config{'chroot'} = "/"; # make sure path is absolute local $bind8::config{'auto_chroot'} = undef; undef($bind8::get_chroot_cache); local @recs = &bind8::read_zone_file($_[1], $_[0]->{'dom'}); local ($r, $start, $end); foreach $r (@recs) { if ($r->{'type'} ne "SOA" && !$r->{'generate'} && !$r->{'defttl'} && !defined($start)) { $start = $r->{'line'}; } $end = $r->{'eline'}; } undef($bind8::get_chroot_cache); # Reset cache back return ($start, $end); } # get_bind_view([&conf], view) # Returns the view object for the view to add domains to sub get_bind_view { &require_bind(); local $conf = $_[0] || &bind8::get_config(); local @views = &bind8::find("view", $conf); local ($view) = grep { $_->{'values'}->[0] eq $_[1] } @views; return $view; } # show_restore_dns(&options) # Returns HTML for DNS restore option inputs sub show_restore_dns { local ($opts, $d) = @_; return &ui_checkbox("dns_wholefile", 1, $text{'restore_dnswholefile'}, $opts->{'wholefile'}); } # parse_restore_dns(&in) # Parses the inputs for DNS restore options sub parse_restore_dns { local ($in, $d) = @_; return { 'wholefile' => $in->{'dns_wholefile'} }; } # sysinfo_dns() # Returns the BIND version sub sysinfo_dns { &require_bind(); if ($config{'provision_dns'}) { # No local BIND in provisioning mode return ( ); } if (!$bind8::bind_version) { local $out = `$bind8::config{'named_path'} -v 2>&1`; if ($out =~ /(bind|named)\s+([0-9\.]+)/i) { $bind8::bind_version = $2; } } return ( [ $text{'sysinfo_bind'}, $bind8::bind_version ] ); } sub startstop_dns { local ($typestatus) = @_; if ($config{'provision_dns'}) { # Cannot start or stop when remote return (); } local $bpid = defined($typestatus{'bind8'}) ? $typestatus{'bind8'} == 1 : &get_bind_pid(); local @links = ( { 'link' => '/bind8/', 'desc' => $text{'index_bmanage'}, 'manage' => 1 } ); if ($bpid && kill(0, $bpid)) { return ( { 'status' => 1, 'name' => $text{'index_bname'}, 'desc' => $text{'index_bstop'}, 'restartdesc' => $text{'index_brestart'}, 'longdesc' => $text{'index_bstopdesc'}, 'links' => \@links } ); } else { return ( { 'status' => 0, 'name' => $text{'index_bname'}, 'desc' => $text{'index_bstart'}, 'longdesc' => $text{'index_bstartdesc'}, 'links' => \@links } ); } } sub start_service_dns { &require_bind(); return &bind8::start_bind(); } sub stop_service_dns { &require_bind(); return &bind8::stop_bind(); } # show_template_dns(&tmpl) # Outputs HTML for editing BIND related template options sub show_template_dns { local ($tmpl) = @_; &require_bind(); local ($conf, @views); if (!$config{'provision_dns'}) { $conf = &bind8::get_config(); @views = &bind8::find("view", $conf); } # DNS records local $ndi = &none_def_input("dns", $tmpl->{'dns'}, $text{'tmpl_dnsbelow'}, 1, 0, undef, [ "dns", "bind_replace", "dnsns", "dns_ttl_def", "dns_ttl", "dnsprins", "dns_records", @views || $tmpl->{'dns_view'} ? ( "view" ) : ( ) ]); print &ui_table_row(&hlink($text{'tmpl_dns'}, "template_dns"), $ndi."<br>\n". &ui_textarea("dns", $tmpl->{'dns'} eq "none" ? "" : join("\n", split(/\t/, $tmpl->{'dns'})), 10, 60)."<br>\n". &ui_radio("bind_replace", int($tmpl->{'dns_replace'}), [ [ 0, $text{'tmpl_replace0'} ], [ 1, $text{'tmpl_replace1'} ] ])); # Address records to add my @add_records = split(/\s+/, $tmpl->{'dns_records'}); if (!@add_records || $add_records[0] eq 'none') { @add_records = @automatic_dns_records; } my @grid = map { &ui_checkbox("dns_records", $_, $text{'tmpl_dns_record_'.$_}, &indexof($_, @add_records) >= 0) } @automatic_dns_records; print &ui_table_row(&hlink($text{'tmpl_dnsrecords'}, "template_dns_records"), &ui_grid_table(\@grid, scalar(@grid))); # Default TTL local $tmode = $tmpl->{'dns_ttl'} eq 'none' ? 0 : $tmpl->{'dns_ttl'} eq 'skip' ? 1 : 2; print &ui_table_row(&hlink($text{'tmpl_dnsttl'}, "template_dns_ttl"), &ui_radio("dns_ttl_def", $tmpl->{'dns_ttl'} eq '' ? 0 : $tmpl->{'dns_ttl'} eq 'none' ? 1 : 2, [ [ 0, $text{'tmpl_dnsttl0'} ], [ 1, $text{'tmpl_dnsttl1'} ], [ 2, $text{'tmpl_dnsttl2'}." ". &ui_textbox("dns_ttl", $tmode == 2 ? $tmpl->{'dns_ttl'} : "", 15) ] ])); # Manual NS records print &ui_table_row(&hlink($text{'tmpl_dnsns'}, "template_dns_ns"), &ui_textarea("dnsns", join("\n", split(/\s+/, $tmpl->{'dns_ns'})), 3, 50)."<br>\n". &ui_checkbox("dnsprins", 1, $text{'tmpl_dnsprins'}, $tmpl->{'dns_prins'})); # Option for view to add to, for BIND 9 if (@views || $tmpl->{'dns_view'}) { print &ui_table_row($text{'newdns_view'}, &ui_select("view", $tmpl->{'dns_view'}, [ [ "", $text{'newdns_noview'} ], map { [ $_->{'values'}->[0] ] } @views ])); } # Add sub-domains to parent domain DNS print &ui_table_row(&hlink($text{'tmpl_dns_sub'}, "template_dns_sub"), &none_def_input("dns_sub", $tmpl->{'dns_sub'}, $text{'yes'}, 0, 0, $text{'no'})); print &ui_table_hr(); # Master NS hostnames print &ui_table_row(&hlink($text{'tmpl_dnsmaster'}, "template_dns_master"), &none_def_input("dns_master", $tmpl->{'dns_master'}, $text{'tmpl_dnsmnames'}, 0, 0, $text{'tmpl_dnsmauto'}."<br>", [ "dns_master" ])." ". &ui_textbox("dns_master", $tmpl->{'dns_master'} eq 'none' ? '' : $tmpl->{'dns_master'}, 40)); print &ui_table_hr(); # Option for SPF record print &ui_table_row(&hlink($text{'tmpl_spf'}, "template_dns_spf_mode"), &none_def_input("dns_spf", $tmpl->{'dns_spf'}, $text{'tmpl_spfyes'}, 0, 0, $text{'no'}, [ "dns_spfhosts", "dns_spfall", "dns_spfincludes" ])); # Extra SPF hosts print &ui_table_row(&hlink($text{'tmpl_spfhosts'}, "template_dns_spfhosts"), &ui_textbox("dns_spfhosts", $tmpl->{'dns_spfhosts'}, 40)); # Extra SPF includes print &ui_table_row(&hlink($text{'tmpl_spfincludes'}, "template_dns_spfincludes"), &ui_textbox("dns_spfincludes", $tmpl->{'dns_spfincludes'}, 40)); # SPF ~all mode print &ui_table_row(&hlink($text{'tmpl_spfall'}, "template_dns_spfall"), &ui_radio("dns_spfall", $tmpl->{'dns_spfall'}, [ [ 0, $text{'tmpl_spfall0'} ], [ 1, $text{'tmpl_spfall1'} ], [ 2, $text{'tmpl_spfall2'} ] ])); print &ui_table_hr(); # Option for DMARC record print &ui_table_row(&hlink($text{'tmpl_dmarc'}, "template_dns_dmarc_mode"), &none_def_input("dns_dmarc", $tmpl->{'dns_dmarc'}, $text{'tmpl_dmarcyes'}, 0, 0, $text{'no'}, [ "dns_dmarcp", "dns_dmarcpct" ])); # DMARC policy print &ui_table_row(&hlink($text{'tmpl_dmarcp'}, "template_dns_dmarcp"), &ui_radio("dns_dmarcp", $tmpl->{'dns_dmarcp'}, [ [ "none", $text{'tmpl_dmarcnone'} ], [ "quarantine", $text{'tmpl_dmarcquar'} ], [ "reject", $text{'tmpl_dmarcreject'} ] ])); # DMARC percentage print &ui_table_row(&hlink($text{'tmpl_dmarcpct'}, "template_dns_dmarcpct"), &ui_textbox("dns_dmarcpct", $tmpl->{'dns_dmarcpct'}, 5)."%"); if (!$config{'provision_dns'}) { print &ui_table_hr(); # Extra named.conf directives print &ui_table_row(&hlink($text{'tmpl_namedconf'}, "namedconf"), &none_def_input("namedconf", $tmpl->{'namedconf'}, $text{'tmpl_namedconfbelow'}, 0, 0, undef, [ "namedconf", "namedconf_also_notify", "namedconf_allow_transfer" ])."<br>". &ui_textarea("namedconf", $tmpl->{'namedconf'} eq 'none' ? '' : join("\n", split(/\t/, $tmpl->{'namedconf'})), 5, 60)); # Add also-notify and allow-transfer print &ui_table_row(&hlink($text{'tmpl_dnsalso'}, "template_dns_also"), &ui_checkbox("namedconf_also_notify", 1, 'also-notify', !$tmpl->{'namedconf_no_also_notify'})." ". &ui_checkbox("namedconf_allow_transfer", 1, 'allow-transfer', !$tmpl->{'namedconf_no_allow_transfer'})); # DNSSEC for new domains if (defined(&bind8::supports_dnssec) && &bind8::supports_dnssec()) { print &ui_table_hr(); # Setup for new domains? print &ui_table_row(&hlink($text{'tmpl_dnssec'}, "dnssec"), &none_def_input("dnssec", $tmpl->{'dnssec'}, $text{'yes'}, 0, 0, $text{'no'}, [ "dnssec_alg", "dnssec_single" ])); # Encryption algorithm print &ui_table_row(&hlink($text{'tmpl_dnssec_alg'}, "dnssec_alg"), &ui_select("dnssec_alg", $tmpl->{'dnssec_alg'} || "RSASHA1", [ &bind8::list_dnssec_algorithms() ])); # One key or two? print &ui_table_row(&hlink($text{'tmpl_dnssec_single'}, "dnssec_single"), &ui_radio("dnssec_single", $tmpl->{'dnssec_single'} ? 1 : 0, [ [ 0, $bind8::text{'zonedef_two'} ], [ 1, $bind8::text{'zonedef_one'} ] ])); } } } # parse_template_dns(&tmpl) # Updates BIND related template options from %in sub parse_template_dns { local ($tmpl) = @_; # Save DNS settings $tmpl->{'dns'} = &parse_none_def("dns"); if ($in{"dns_mode"} == 2) { $tmpl->{'default'} || $tmpl->{'dns'} =~ /\S/ || $in{'bind_replace'} == 0 || &error($text{'tmpl_edns'}); $tmpl->{'dns_replace'} = $in{'bind_replace'}; $tmpl->{'dns_view'} = $in{'view'}; &require_bind(); local $fakeip = "1.2.3.4"; local $fakedom = "foo.com"; local $recs = &substitute_virtualmin_template( join("\n", split(/\t+/, $in{'dns'}))."\n", { 'ip' => $fakeip, 'dom' => $fakedom, 'web' => 1, }); local $temp = &transname(); &open_tempfile(TEMP, ">$temp"); &print_tempfile(TEMP, $recs); &close_tempfile(TEMP); local $bind8::config{'short_names'} = 0; # force canonicalization local $bind8::config{'chroot'} = '/'; # turn off chroot for temp path local $bind8::config{'auto_chroot'} = undef; undef($bind8::get_chroot_cache); local @recs = &bind8::read_zone_file($temp, $fakedom); unlink($temp); foreach $r (@recs) { $soa++ if ($r->{'name'} eq $fakedom."." && $r->{'type'} eq "SOA"); $ns++ if ($r->{'name'} eq $fakedom."." && $r->{'type'} eq "NS"); $dom++ if ($r->{'name'} eq $fakedom."." && ($r->{'type'} eq "A" || $r->{'type'} eq "MX")); $www++ if ($r->{'name'} eq "www.".$fakedom."." && $r->{'type'} eq "A" || $r->{'type'} eq "CNAME"); } undef($bind8::get_chroot_cache); # reset cache back if ($in{'bind_replace'}) { # Make sure an SOA and NS records exist $soa == 1 || &error($text{'newdns_esoa'}); $ns || &error($text{'newdns_ens'}); $dom || &error($text{'newdns_edom'}); $www || &error($text{'newdns_ewww'}); } else { # Make sure SOA doesn't exist $soa && &error($text{'newdns_esoa2'}); } # Save default TTL if ($in{'dns_ttl_def'} == 0) { $tmpl->{'dns_ttl'} = ''; } elsif ($in{'dns_ttl_def'} == 1) { $tmpl->{'dns_ttl'} = 'none'; } else { $in{'dns_ttl'} =~ /^\d+(h|d|m|y|w|)$/i || &error($text{'tmpl_ednsttl'}); $tmpl->{'dns_ttl'} = $in{'dns_ttl'}; } # Save automatic A records $tmpl->{'dns_records'} = join(" ", split(/\0/, $in{'dns_records'})) || 'noneselected'; # Save additional nameservers $in{'dnsns'} =~ s/\r//g; local @ns = split(/\n+/, $in{'dnsns'}); foreach my $n (@ns) { &check_ipaddress($n) && &error(&text('newdns_ensip', $n)); &to_ipaddress($n) || &error(&text('newdns_enshost', $n)); } $tmpl->{'dns_ns'} = join(" ", @ns); $tmpl->{'dns_prins'} = $in{'dnsprins'}; } # Save NS hostname $in{'dns_master_mode'} != 2 || ($in{'dns_master'} =~ /^[a-z0-9\.\-\_]+$/i && $in{'dns_master'} =~ /\./ && !&check_ipaddress($in{'dns_master'})) || &error($text{'tmpl_ednsmaster'}); $tmpl->{'dns_master'} = $in{'dns_master_mode'} == 0 ? "none" : $in{'dns_master_mode'} == 1 ? undef : $in{'dns_master'}; # Save SPF $tmpl->{'dns_spf'} = $in{'dns_spf_mode'} == 0 ? "none" : $in{'dns_spf_mode'} == 1 ? undef : "yes"; $tmpl->{'dns_spfhosts'} = $in{'dns_spfhosts'}; $tmpl->{'dns_spfincludes'} = $in{'dns_spfincludes'}; $tmpl->{'dns_spfall'} = $in{'dns_spfall'}; # Save DMARC $tmpl->{'dns_dmarc'} = $in{'dns_dmarc_mode'} == 0 ? "none" : $in{'dns_dmarc_mode'} == 1 ? undef : "yes"; if ($in{'dns_dmarc_mode'} == 2) { $in{'dns_dmarcpct'} =~ /^\d+$/ && $in{'dns_dmarcpct'} >= 0 && $in{'dns_dmarcpct'} <= 100 || &error($text{'tmpl_edmarcpct'}); } $tmpl->{'dns_dmarcp'} = $in{'dns_dmarcp'}; $tmpl->{'dns_dmarcpct'} = $in{'dns_dmarcpct'}; # Save sub-domain DNS mode $tmpl->{'dns_sub'} = $in{'dns_sub_mode'} == 0 ? "none" : $in{'dns_sub_mode'} == 1 ? undef : "yes"; if (!$config{'provision_dns'}) { # Save named.conf $tmpl->{'namedconf'} = &parse_none_def("namedconf"); if ($in{'namedconf_mode'} == 2) { # Make sure the directives are valid local @recs = &text_to_named_conf($tmpl->{'namedconf'}); if ($tmpl->{'namedconf'} =~ /\S/ && !@recs) { &error($text{'newdns_enamedconf'}); } $tmpl->{'namedconf'} ||= " "; # So it can be empty # Save other auto-add directives $tmpl->{'namedconf_no_also_notify'} = !$in{'namedconf_also_notify'}; $tmpl->{'namedconf_no_allow_transfer'} = !$in{'namedconf_allow_transfer'}; } # Save DNSSEC if (defined($in{'dnssec_mode'})) { $tmpl->{'dnssec'} = $in{'dnssec_mode'} == 0 ? "none" : $in{'dnssec_mode'} == 1 ? undef : "yes"; $tmpl->{'dnssec_alg'} = $in{'dnssec_alg'}; $tmpl->{'dnssec_single'} = $in{'dnssec_single'}; } } } # get_domain_spf(&domain) # Returns the SPF object for a domain from its DNS records, or undef. sub get_domain_spf { local ($d) = @_; &require_bind(); local @recs = &get_domain_dns_records($d); foreach my $r (@recs) { if ($r->{'type'} eq 'SPF' && $r->{'name'} eq $d->{'dom'}.'.') { return &bind8::parse_spf(@{$r->{'values'}}); } } return undef; } # save_domain_spf(&domain, &spf) # Updates/creates/deletes a domain's SPF record. sub save_domain_spf { local ($d, $spf) = @_; &require_bind(); local @types = $bind8::config{'spf_record'} ? ( "SPF", "TXT" ) : ( "SPF" ); foreach my $t (@types) { local ($recs, $file) = &get_domain_dns_records_and_file($d); if (!$file) { # Domain not found! return; } local $bump = 0; local ($r) = grep { $_->{'type'} eq $t && $_->{'values'}->[0] =~ /^v=spf/ && $_->{'name'} eq $d->{'dom'}.'.' } @$recs; local $str = $spf ? &bind8::join_spf($spf) : undef; if ($r && $spf) { # Update record &bind8::modify_record( $r->{'file'}, $r, $r->{'name'}, $r->{'ttl'}, $r->{'class'}, $r->{'type'}, "\"$str\"", $r->{'comment'}); $bump = 1; } elsif ($r && !$spf) { # Remove record &bind8::delete_record($r->{'file'}, $r); $bump = 1; } elsif (!$r && $spf) { # Add record &bind8::create_record($file, $d->{'dom'}.'.', undef, "IN", $t, "\"$str\""); $bump = 1; } if ($bump) { &post_records_change($d, $recs, $file); ®ister_post_action(\&restart_bind, $d); } } } # get_domain_dmarc(&domain) # Returns the DMARC object for a domain from its DNS records, or undef. sub get_domain_dmarc { local ($d) = @_; &require_bind(); local @recs = &get_domain_dns_records($d); foreach my $r (@recs) { if (($r->{'type'} eq 'DMARC' || $r->{'type'} eq 'TXT') && $r->{'name'} eq '_dmarc.'.$d->{'dom'}.'.') { return &parse_dmarc(@{$r->{'values'}}); } } return undef; } # save_domain_dmarc(&domain, &dmarc) # Updates/creates/deletes a domain's SPF record. sub save_domain_dmarc { local ($d, $dmarc) = @_; &require_bind(); local ($recs, $file) = &get_domain_dns_records_and_file($d); if (!$file) { # Domain not found! return; } local $bump = 0; local ($r) = grep { ($_->{'type'} eq 'TXT' || $_->{'type'} eq 'DMARC') && $_->{'values'}->[0] =~ /^v=DMARC1/i && $_->{'name'} eq '_dmarc.'.$d->{'dom'}.'.' } @$recs; local $str = $dmarc ? &join_dmarc($dmarc) : undef; if ($r && $dmarc) { # Update record &bind8::modify_record( $r->{'file'}, $r, $r->{'name'}, $r->{'ttl'}, $r->{'class'}, $r->{'type'}, "\"$str\"", $r->{'comment'}); $bump = 1; } elsif ($r && !$dmarc) { # Remove record &bind8::delete_record($r->{'file'}, $r); $bump = 1; } elsif (!$r && $dmarc) { # Add record &bind8::create_record($file, '_dmarc.'.$d->{'dom'}.'.', undef, "IN", "TXT", "\"$str\""); $bump = 1; } if ($bump) { &post_records_change($d, $recs, $file); ®ister_post_action(\&restart_bind, $d); } } # parse_dmarc(text, ...) # If some text looks like an DMARC TXT record, return a parsed hash ref sub parse_dmarc { &require_bind(); return &bind8::parse_dmarc(@_) if (defined(&bind8::parse_dmarc)); # XXX remove this once Webmin 1.740 is out for Virtualmin my $txt = join(" ", @_); if ($txt =~ /^v=dmarc1/i) { local @w = split(/;\s*/, $txt); local $dmarc = { }; foreach my $w (@w) { $w = lc($w); if ($w =~ /^(v|pct|ruf|rua|p|sp|adkim|aspf)=(\S+)$/i) { $dmarc->{$1} = $2; } else { push(@{$dmarc->{'other'}}, $w); } } return $dmarc; } return undef; } # join_dmarc(&dmarc) # Converts a DMARC record structure to a string, designed to be inserted into # quotes in a TXT record. If it is longer than 255 bytes, it will be split # into multiple quoted strings. sub join_dmarc { local ($dmarc) = @_; &require_bind(); return &bind8::join_dmarc(@_) if (defined(&bind8::join_dmarc)); local @rv = ( "v=DMARC1" ); foreach my $s ("pct", "ruf", "rua", "p", "sp", "adkim", "aspf") { if ($dmarc->{$s} ne '') { push(@rv, $s."=".$dmarc->{$s}); } } push(@rv, @{$dmarc->{'other'}}); local @rvwords; local $rvword; while(@rv) { my $w = shift(@rv); if (length($rvword)+length($w)+1 >= 255) { push(@rvwords, $rvword); $rvword = ""; } $rvword .= "; " if ($rvword); $rvword .= $w; } push(@rvwords, $rvword); return join("\" \"", @rvwords); } # get_domain_dns_records(&domain) # Returns an array of DNS records for a domain, or empty if the file couldn't # be found. sub get_domain_dns_records { local ($d) = @_; local ($recs, $file) = &get_domain_dns_records_and_file($d); return ( ) if (!$file); return @$recs; } # get_domain_dns_file(&domain) # Returns the chroot-relative path to a domain's DNS records sub get_domain_dns_file { local ($d) = @_; if ($d->{'provision_dns'}) { &error("get_domain_dns_file($d->{'dom'}) cannot be called ". "for provisioning domains"); } &require_bind(); local $z; if ($d->{'dns_submode'}) { # Records are in super-domain local $parent = &get_domain($d->{'subdom'}) || &get_domain($d->{'parent'}); $z = &get_bind_zone($parent->{'dom'}); } else { # In this domain $z = &get_bind_zone($d->{'dom'}); } return undef if (!$z); local $file = &bind8::find("file", $z->{'members'}); return undef if (!$file); return $file->{'values'}->[0]; } # get_domain_dns_records_and_file(&domain) # Returns an array ref of a domain's DNS records and the file they are in. # For a provisioned domain, this may be a local temp file. sub get_domain_dns_records_and_file { local ($d) = @_; &require_bind(); local $bind8::config{'short_names'} = 0; if ($d->{'provision_dns'}) { # Download to temp file, and read it local $temp = &transname(); local $abstemp = $temp; local $chroot = &bind8::get_chroot(); if ($chroot && $chroot ne "/") { # Actual temp file needs to be under chroot dir $abstemp = &bind8::make_chroot($temp); local $absdir = $abstemp; $absdir =~ s/\/[^\/]+$//; if (!-d $absdir) { &make_dir($absdir, 0755, 1); } } local $info = { 'domain' => $d->{'dom'}, 'host' => $d->{'provision_dns_host'} }; my ($ok, $msg) = &provision_api_call( "list-dns-records", $info, 1); if (!$ok) { return ("Failed to fetch DNS records from provisioning ". "server : $msg"); } local @recs; local $lnum = 0; foreach my $r (@$msg) { local $rec; if ($r->{'name'} eq '$ttl') { $rec = { 'defttl' => $r->{'values'}->{'value'}->[0] }; &bind8::create_defttl($temp, $rec->{'defttl'}); } elsif ($r->{'name'} eq '$generate') { $rec = { 'generate' => $r->{'values'}->{'value'} }; &bind8::create_generator($temp, @{$rec->{'generate'}}); } else { $rec = { 'name' => $r->{'name'}, 'realname' => $r->{'name'}, 'class' => $r->{'values'}->{'class'}->[0], 'type' => $r->{'values'}->{'type'}->[0], 'ttl' => $r->{'values'}->{'ttl'}->[0], 'comment' => $r->{'values'}->{'comment'}->[0], 'values' => $r->{'values'}->{'value'}, }; &bind8::create_record($temp, $rec->{'name'}, $rec->{'ttl'}, $rec->{'class'}, $rec->{'type'}, &join_record_values($rec, 1), $rec->{'comment'}); } $rec->{'line'} = $lnum; $rec->{'eline'} = $lnum; $rec->{'num'} = $lnum; $rec->{'file'} = $temp; $rec->{'rootfile'} = $abstemp; push(@recs, $rec); $lnum++; } &set_record_ids(\@recs); return (\@recs, $temp); } else { # Find local file local $file = &get_domain_dns_file($d); return ("No zone file found for $d->{'dom'}") if (!$file); local $rd = $d->{'dns_submode'} ? &get_domain($d->{'subdom'} || $d->{'parent'}) : $d; local @recs = &bind8::read_zone_file($file, $rd->{'dom'}); &set_record_ids(\@recs); return (\@recs, $file); } } # set_record_ids(&records) # Sets the ID field on a bunch of DNS records sub set_record_ids { local ($recs) = @_; foreach my $r (@$recs) { if ($r->{'defttl'}) { $r->{'id'} = join("/", '$ttl', $r->{'defttl'}); } elsif ($r->{'generate'}) { $r->{'id'} = join("/", '$generate', @{$r->{'generate'}}); } else { $r->{'id'} = join("/", $r->{'name'}, $r->{'type'}, @{$r->{'values'}}); } } } # default_domain_spf(&domain) # Returns a default SPF object for a domain, based on its template sub default_domain_spf { local ($d) = @_; local $tmpl = &get_template($d->{'template'}); local $defip = &get_default_ip(); local $defip6 = &get_default_ip6(); local $spf = { 'a' => 1, 'mx' => 1, 'a:' => [ $d->{'dom'} ], 'ip4:' => [ ], 'ip6:' => [ ] }; if ($defip ne "127.0.0.1") { push(@{$spf->{'ip4:'}}, $defip); } if ($defip6) { push(@{$spf->{'ip6:'}}, $defip6); } local $hosts = &substitute_domain_template($tmpl->{'dns_spfhosts'}, $d); foreach my $h (split(/\s+/, $hosts)) { if (&check_ipaddress($h) || $h =~ /^(\S+)\// && &check_ipaddress($1)) { push(@{$spf->{'ip4:'}}, $h); } else { push(@{$spf->{'a:'}}, $h); } } local $includes = &substitute_domain_template($tmpl->{'dns_spfincludes'}, $d); foreach my $i (split(/\s+/, $includes)) { push(@{$spf->{'include:'}}, $i); } if ($d->{'dns_ip'}) { push(@{$spf->{'ip4:'}}, $d->{'dns_ip'}); } if ($d->{'ip'} ne $defip) { push(@{$spf->{'ip4:'}}, $d->{'ip'}); } if ($d->{'ip6'} && $d->{'ip6'} ne $defip6) { push(@{$spf->{'ip6:'}}, $d->{'ip6'}); } $spf->{'all'} = $tmpl->{'dns_spfall'} + 1; return $spf; } # default_domain_dmarc(&domain) # Returns a default DMARC object for a domain, based on its template sub default_domain_dmarc { local ($d) = @_; local $tmpl = &get_template($d->{'template'}); local $pm = 'postmaster@'.$d->{'dom'}; local $dmarc = { 'p' => $tmpl->{'dns_dmarcp'} || 'none', 'pct' => $tmpl->{'dns_dmarcpct'} || '100', 'ruf' => 'mailto:'.$pm, 'rua' => 'mailto:'.$pm, }; return $dmarc; } # text_to_named_conf(text) # Converts a text string which contains zero or more BIND directives into an # array of directive objects. sub text_to_named_conf { local ($str) = @_; local $temp = &transname(); &open_tempfile(TEMP, ">$temp"); &print_tempfile(TEMP, $str); &close_tempfile(TEMP); &require_bind(); local $bind8::config{'chroot'} = undef; # turn off chroot temporarily local $bind8::config{'auto_chroot'} = undef; undef($bind8::get_chroot_cache); local @rv = grep { $_->{'name'} ne 'dummy' } &bind8::read_config_file($temp, 0); undef($bind8::get_chroot_cache); # reset cache back return @rv; } # post_records_change(&domain, &recs, [file]) # Called after some records in a domain are changed, to bump to SOA # and possibly re-sign sub post_records_change { local ($d, $recs, $fn) = @_; &require_bind(); local $z; if (!$fn) { # Use local file by default $z = &get_bind_zone($d->{'dom'}); return "Failed to find zone for $d->{'dom'}" if (!$z); local $file = &bind8::find("file", $z->{'members'}); return "Failed to find records file for $d->{'dom'}" if (!$file); $fn = $file->{'values'}->[0]; } # Increase the SOA &bind8::bump_soa_record($fn, $recs); # If the domain is disabled, make sure all records end with .disabled if ($d->{'disabled'} && &indexof("dns", split(/,/, $d->{'disabled'})) >= 0) { local @disrecs = &bind8::read_zone_file($fn, $d->{'dom'}); foreach my $r (@disrecs) { if ($r->{'name'} =~ /\.\Q$d->{'dom'}\E\.$/ || $r->{'name'} eq $d->{'dom'}.".") { # Not disabled - make it so &bind8::modify_record($fn, $r, $r->{'name'}."disabled.", $r->{'ttl'}, $r->{'class'}, $r->{'type'}, &join_record_values($r), $r->{'comment'}); } } } if (defined(&bind8::supports_dnssec) && &bind8::supports_dnssec() && !$d->{'provision_dns'}) { # Re-sign too $z ||= &get_bind_zone($d->{'dom'}); eval { local $main::error_must_die = 1; &bind8::sign_dnssec_zone_if_key($z, $recs, 0); }; if ($@) { return "DNSSEC signing failed : $@"; } } if ($d->{'provision_dns'}) { # Upload records to provisioning server local $info = { 'domain' => $d->{'dom'}, 'replace' => '', 'host' => $d->{'provision_dns_host'} }; local @newrecs = &bind8::read_zone_file($fn, $d->{'dom'}); $info->{'record'} = [ &records_to_text($d, \@newrecs) ]; my ($ok, $msg) = &provision_api_call("modify-dns-records", $info, 0); if (!ok) { return "Error from provisioning server updating records : $msg"; } } # If this domain has aliases, update their DNS records too if (!$d->{'subdom'} && !$d->{'dns_submode'}) { local @aliases = grep { $_->{'dns'} && !$_>{'dns_submode'} } &get_domain_by("alias", $d->{'id'}); foreach my $ad (@aliases) { &obtain_lock_dns($ad); local $file; local $recs; if ($ad->{'provision_dns'}) { # On provisioning server $file = &transname(); local $bind8::config{'auto_chroot'} = undef; local $bind8::config{'chroot'} = undef; &create_alias_records($file, $ad, $ad->{'dns_ip'} || $ad->{'ip'}); $recs = [ &bind8::read_zone_file($temp, $ad->{'dom'}) ]; } else { # On local BIND $file = &get_domain_dns_file($ad); &open_tempfile(EMPTY, ">$file", 0, 1); &close_tempfile(EMPTY); &create_alias_records($file, $ad, $ad->{'dns_ip'} || $ad->{'ip'}); $recs = [ get_domain_dns_records($ad) ]; } &post_records_change($ad, $recs, $file); &reload_bind_records($ad); &release_lock_dns($ad); } } return undef; } # records_to_text(&domain, &records) # Given a list of record hashes, return text-format equivalents for an API call sub records_to_text { local ($d, $recs) = @_; local @rv; &require_bind(); foreach my $r (@$recs) { next if ($r->{'type'} eq 'NS' && # Exclude NS for domain $r->{'name'} eq $d->{'dom'}."."); if ($r->{'defttl'}) { push(@rv, '$ttl '.$r->{'defttl'}); } elsif ($r->{'generate'}) { push(@rv, '$generate '.join(' ', @{$r->{'generate'}})); } elsif ($r->{'type'}) { my $t = $r->{'type'}; $t = "TXT" if ($t eq "SPF" && $bind8::config{'spf_record'} == 0); push(@rv, join(" ", $r->{'name'}, $r->{'ttl'}, $r->{'class'}, $t, &join_record_values($r, 1))); } } return @rv; } # under_parent_domain(&domain, [&parent]) # Returns 1 if some domain's DNS zone is under a given parent's DNS zone sub under_parent_domain { local ($d, $parent) = @_; if (!$parent && $d->{'parent'}) { $parent = &get_domain($d->{'parent'}); } if ($parent && $d->{'dom'} =~ /\.\Q$parent->{'dom'}\E$/i && $parent->{'dns'}) { return 1; } return 0; } # can_edit_record(&record, &domain) # Returns 1 if some DNS record can be edited. sub can_edit_record { local ($r, $d) = @_; if ($r->{'type'} eq 'NS' && $r->{'name'} eq $d->{'dom'}.'.' && $d->{'provision_dns'}) { # NS record for domain is automatically set in provisioning mode return 0; } elsif (($r->{'type'} eq 'SPF' || $r->{'type'} eq 'TXT' && $r->{'values'}->[0] =~ /^v=spf/) && $r->{'name'} eq $d->{'dom'}.'.') { # SPF is edited separately return 0; } elsif ($r->{'type'} eq 'TXT' && $r->{'values'}->[0] =~ /^(t=|k=|v=)/ && $config{'dkim_enabled'}) { # DKIM, managed by Virtualmin return 0; } elsif ($r->{'type'} eq 'SOA') { # Always auto-generate return 0; } return 1; } # can_delete_record(&record, &domain) # Returns 1 if some DNS record can be removed. sub can_delete_record { local ($r, $d) = @_; if ($r->{'type'} eq 'NS' && $r->{'name'} eq $d->{'dom'}.'.' && $d->{'provision_dns'}) { # NS record for domain is automatically set in provisioning mode return 0; } elsif ($r->{'type'} eq 'SOA') { # Don't allow removal of SOA ever return 0; } return 1; } # list_dns_record_types(&domain) # Returns a list of hash refs, one per supported record type. Each contains the # following keys : # type - A, NS, etc.. # desc - Human-readable description # domain - Can be same as domain name # values - Array ref of hash refs, with keys : # desc - Human-readable description of this value # regexp - Validation regexp for value # func - Validation function ref for value sub list_dns_record_types { local ($d) = @_; return ( { 'type' => 'A', 'desc' => $text{'records_typea'}, 'domain' => 1, 'create' => 1, 'values' => [ { 'desc' => $text{'records_valuea'}, 'size' => 20, 'func' => sub { &check_ipaddress($_[0]) ? undef : $text{'records_evaluea'} } }, ], }, { 'type' => 'AAAA', 'desc' => $text{'records_typeaaaa'}, 'domain' => 1, 'create' => 1, 'values' => [ { 'desc' => $text{'records_valueaaaa'}, 'size' => 20, 'func' => sub { &check_ip6address($_[0]) ? undef : $text{'records_evalueaaaa'} } }, ], }, { 'type' => 'CNAME', 'desc' => $text{'records_typecname'}, 'domain' => 0, 'create' => 1, 'values' => [ { 'desc' => $text{'records_valuecname'}, 'size' => 40, 'func' => sub { $_[0] =~ /^[a-z0-9\.\_\-]+$/i ? undef : $text{'records_evaluecname'} }, 'dot' => 1, }, ], }, { 'type' => 'NS', 'desc' => $text{'records_typens'}, 'domain' => 1, 'create' => 1, 'values' => [ { 'desc' => $text{'records_valuens'}, 'size' => 40, 'func' => sub { $_[0] =~ /^[a-z0-9\.\_\-]+$/i ? undef : $text{'records_evaluens'} }, 'dot' => 1, }, ], }, { 'type' => 'MX', 'desc' => $text{'records_typemx'}, 'domain' => 1, 'create' => 1, 'values' => [ { 'desc' => $text{'records_valuemx1'}, 'size' => 5, 'func' => sub { $_[0] =~ /^\d+$/ ? undef : $text{'records_evaluemx1'} }, 'suffix' => $text{'records_valuemx1a'}, }, { 'desc' => $text{'records_valuemx2'}, 'size' => 40, 'func' => sub { $_[0] =~ /^[a-z0-9\.\_\-]+$/i ? undef : $text{'records_evaluemx2'} }, 'dot' => 1, }, ], }, { 'type' => 'TXT', 'desc' => $text{'records_typetxt'}, 'domain' => 1, 'create' => 1, 'values' => [ { 'desc' => $text{'records_valuetxt'}, 'width' => 60, 'height' => 5, 'regexp' => '\S', 'dot' => 0, }, ], }, { 'type' => 'SOA', 'desc' => $text{'records_typesoa'}, 'domain' => 1, 'create' => 0, }, { 'type' => 'SPF', 'desc' => $text{'records_typespf'}, 'domain' => 1, 'create' => 0, 'values' => [ { 'desc' => $text{'records_valuespf'}, 'size' => 60, 'regexp' => '\S', 'dot' => 0, }, ], }, { 'type' => 'PTR', 'desc' => $text{'records_typeptr'}, 'domain' => 0, 'create' => 1, 'values' => [ { 'desc' => $text{'records_valueptr'}, 'size' => 40, 'func' => sub { $_[0] =~ /^[a-z0-9\.\_\-]+\.$/i ? undef : $text{'records_evalueptr'} } }, ], }, { 'type' => 'SRV', 'desc' => $text{'records_typesrv'}, 'domain' => 1, 'create' => 1, 'values' => [ { 'desc' => $text{'records_valuesrv1'}, 'size' => 5, 'func' => sub { $_[0] =~ /^\d+$/ ? undef : $text{'records_evaluesrv1'} }, }, { 'desc' => $text{'records_valuesrv2'}, 'size' => 5, 'func' => sub { $_[0] =~ /^\d+$/i ? undef : $text{'records_evaluesrv2'} }, }, { 'desc' => $text{'records_valuesrv3'}, 'size' => 10, 'func' => sub { $_[0] =~ /^\d+$/i ? undef : $text{'records_evaluesrv3'} }, }, { 'desc' => $text{'records_valuesrv4'}, 'size' => 40, 'func' => sub { $_[0] =~ /^[a-z0-9\.\_\-]+$/i ? undef : $text{'records_evaluesrv4'} }, 'dot' => 1, }, ], }, ); } # ttl_to_seconds(string) # Converts a TTL string like 1h to a number of seconds, like 3600 sub ttl_to_seconds { my ($str) = @_; return $str =~ /^(\d+)s$/i ? $1 : $str =~ /^(\d+)m$/i ? $1*60 : $str =~ /^(\d+)h$/i ? $1*3600 : $str =~ /^(\d+)d$/i ? $1*86400 : $str =~ /^(\d+)w$/i ? $1*7*86400 : $str; } # obtain_lock_dns(&domain, [named-conf-too]) # Lock a domain's zone file and named.conf file sub obtain_lock_dns { local ($d, $conftoo) = @_; return if (!$config{'dns'}); &obtain_lock_anything($d); local $prov = $d ? $d->{'provision_dns'} : $config{'provision_dns'}; # Lock records file if ($d && !$prov) { if ($main::got_lock_dns_zone{$d->{'id'}} == 0) { &require_bind(); local $conf = &bind8::get_config(); local $z = &get_bind_zone($d->{'dom'}, $conf); local $fn; if ($z) { local $file = &bind8::find("file", $z->{'members'}); $fn = $file->{'values'}->[0]; } else { local $base = $bconfig{'master_dir'} || &bind8::base_directory($conf); $fn = &bind8::automatic_filename($d->{'dom'}, 0, $base); } local $rootfn = &bind8::make_chroot($fn); &lock_file($rootfn); $main::got_lock_dns_file{$d->{'id'}} = $rootfn; } $main::got_lock_dns_zone{$d->{'id'}}++; } # Lock named.conf for this domain, if needed. We assume that all domains are # in the same .conf file, even though that may not be true. if ($conftoo && !$prov) { if ($main::got_lock_dns == 0) { &require_bind(); undef(@bind8::get_config_cache); undef(%bind8::get_config_parent_cache); &lock_file(&bind8::make_chroot($bind8::config{'zones_file'} || $bind8::config{'named_conf'})); } $main::got_lock_dns++; } } # release_lock_dns(&domain, [named-conf-too]) # Unlock the zone's records file and possibly named.conf entry sub release_lock_dns { local ($d, $conftoo) = @_; return if (!$config{'dns'}); local $prov = $d ? $d->{'provision_dns'} : $config{'provision_dns'}; # Unlock records file if ($d && !$prov) { if ($main::got_lock_dns_zone{$d->{'id'}} == 1) { local $rootfn = $main::got_lock_dns_file{$d->{'id'}}; &unlock_file($rootfn) if ($rootfn); } $main::got_lock_dns_zone{$d->{'id'}}-- if ($main::got_lock_dns_zone{$d->{'id'}}); } # Unlock named.conf if ($conftoo && !$prov) { if ($main::got_lock_dns == 1) { &require_bind(); &unlock_file(&bind8::make_chroot($bind8::config{'zones_file'} || $bind8::config{'named_conf'})); } $main::got_lock_dns-- if ($main::got_lock_dns); } &release_lock_anything($d); } $done_feature_script{'dns'} = 1; 1;y~or5J={Eeu磝Qk ᯘG{?+]ן?wM3X^歌>{7پK>on\jy Rg/=fOroNVv~Y+ NGuÝHWyw[eQʨSb> >}Gmx[o[<{Ϯ_qFvM IENDB`