set cand {}
foreach list $lists { pushnew cand [lindex $list 0] }
- ## Remove candidate items which have not the first in some other list.
+ ## Remove candidate items which are not first in some other list.
set ncand {}
foreach cand $cand {
foreach list $lists {
- if {[lsearch -exact $list $cand] < 0} { lappend ncand $cand }
+ if {[lsearch -exact $list $cand] <= 0} { lappend ncand $cand }
}
}
+ set cand $ncand
## If there's nothing left, report an error.
if {![llength $cand]} {
}
}
+proc run {what command args} {
+ ## Run a command, reporting the result. WHAT is shown in the output;
+ ## COMMAND are the command and arguments as a list; these are substituted
+ ## according to the string map ARGS. Return true if the command succeeded,
+ ## false if it failed.
+
+ global QUIS
+
+ ## Substitute tokens in the command.
+ set cmd {}
+ set subst [concat [list "%%" "%"] $args]
+ foreach item $command { lappend cmd [string map $subst $item] }
+
+ ## Run the command.
+ set rc [catch {
+ set out [eval exec -ignorestderr $cmd 2>@1]
+ } msg]
+
+ ## Sort out the report.
+ if {$rc} { set out $msg }
+ set out "| [string map [list "\n" "\n| "] $out]"
+
+ ## Announce the result.
+ if {$rc} {
+ puts stderr "$QUIS: $what failed..."
+ puts stderr $out
+ return false
+ } else {
+ puts "$QUIS: $what output..."
+ puts $out
+ return true
+ }
+}
+
###--------------------------------------------------------------------------
### Configuration spaces.
###
## Evaluate BODY, but on exit restore the CONFVAR array so that the BODY
## has no lasting effect on it.
- upvar #1 $confvar CONFIG
+ upvar #0 $confvar CONFIG
set old [array get CONFIG]
unwind-protect {
uplevel 1 $body
define-simple slave-dir "/var/cache/bind"
define-simple dir-mode 2775
define-simple zone-file "%v/%z.zone"
+ define-simple soa-format increment
define-list views *
+ define-list sign-views {}
+ define-list signzone-command {
+ /usr/sbin/dnssec-signzone
+ -g
+ -S
+ -K/var/lib/bind/key
+ -d/var/lib/bind/ds
+ -s-3600 -e+176400
+ -N%q
+ -o%z
+ -f%o
+ %f
+ }
+ define-simple auto-dnssec off
define-list reload-command {/usr/sbin/rndc reload %z IN %v}
+ define-list autosign-command {/usr/sbin/rndc sign %z IN %v}
define-list checkzone-command {
/usr/sbin/named-checkzone
- -i full
- -k fail
- -M fail
- -n fail
- -S fail
- -W fail
+ -ifull
+ -kfail
+ -Mfail
+ -nfail
+ -Sfail
+ -Wfail
%z
%f
}
}
define view-map {map} {
+
+ ## OK, this needs careful documentation.
+ ##
+ ## The local nameserver presents a number of views according to its
+ ## configuration. It is our purpose here to generate a configuration
+ ## snippet for such a view.
+ ##
+ ## A user might have several different views of a zone which are meant to
+ ## be presented to different clients. These map on to the server views
+ ## in a one-to-many fashion. The `view-map' option defines this mapping.
+ ## The argument is a list of alternating SERVER-VIEW USER-VIEW pairs; the
+ ## SERVER-VIEW may be a glob pattern; the USER-VIEW may be the special
+ ## token `=' to mean `same as the SERVER-VIEW'.
+ ##
+ ## We only keep one copy of the zone file for each user view: if the user
+ ## view is used by many server views, then the zone stanza for each of
+ ## those views refers to the same zone file.
+
if {[llength $map] % 2} {
error "view map must have an even number of items"
}
define-simple max-zone-size [expr {512*1024}]
define-list reconfig-command {/usr/sbin/rndc reconfig}
- define scope {body} { preserving-config ZONECFG { uplevel 1 $body } }
+ define scope {body} {
+ preserving-config ZONECFG { uplevel 1 $body }
+ }
define zone {name {body {}}} {
global ZONES
proc compute-zone-properties {view config} {
## Derive interesting information from the zone configuration plist CONFIG,
- ## relative to the stated VIEW. Return a new plist.
+ ## relative to the stated server VIEW. Return a new plist.
array set zone $config
}
}
- ## Main dispatch for zone categorization.
- switch -exact -- $zone(config-type) {
- master {
- switch -exact -- $zone(type) {
- static {
- set zone(file-name) \
- [file join $zone(master-dir) \
- [zone-file-name $zone(mapped-view) $config]]
- }
- dynamic {
- set zone(file-name) [file join $zone(slave-dir) \
- [zone-file-name $view $config]]
- }
- }
+ ## Work out the file names.
+ switch -glob -- $zone(config-type):$zone(type) {
+ master:static {
+ set dir $zone(master-dir)
+ set nameview $zone(mapped-view)
}
- slave {
- set zone(file-name) [file join $zone(slave-dir) \
- [zone-file-name $view $config]]
+ default {
+ set dir $zone(slave-dir)
+ set nameview $view
}
}
+ set zone(file-name) [file join $dir \
+ [zone-file-name $nameview $config]]
+
+ ## Find out whether this zone wants signing.
+ set zone(sign) false
+ switch -glob -- $zone(config-type):$zone(type) {
+ master:static {
+ foreach sview $zone(sign-views) {
+ if {[string match $zone(mapped-view) $sview]} { set zone(sign) true }
+ }
+ }
+ }
+ if {$zone(sign)} {
+ set zone(server-file-name) "$zone(file-name).sig"
+ } else {
+ set zone(server-file-name) $zone(file-name)
+ }
## Done.
return [array get zone]
puts $chan "${prefix}};"
}
+proc sign-zone-file {info input soafmt} {
+ ## Sign the zone described by INFO. The input zone file is INPUT; the SOA
+ ## should be updated according to SOAFMT.
+
+ global QUIS
+
+ array set zone $info
+ return [run "zone `$zone(name)' in view `$zone(mapped-view)'" \
+ $zone(signzone-command) \
+ "%z" $zone(name) \
+ "%f" $zone(file-name) \
+ "%o" $zone(server-file-name) \
+ "%q" $soafmt]
+}
+
proc write-zone-stanza {view chan config} {
## Write a `zone' stanza to CHAN for the zone described by the CONFIG
## plist in the given VIEW.
switch -glob -- $zone(config-type) {
master {
puts $chan "\ttype master;"
- puts $chan "\tfile \"$zone(file-name)\";"
+ puts $chan "\tfile \"$zone(server-file-name)\";"
switch -exact -- $zone(type) {
dynamic { write-ddns-update-policy "\t" $chan $config }
}
foreach host $zone(masters) { lappend masters [host-addr $host] }
puts $chan "\tmasters { [join $masters {; }]; };"
puts $chan "\tfile \"$zone(file-name)\";"
+ if {![string equal $zone(auto-dnssec) off]} {
+ puts $chan "\tauto-dnssec $zone(auto-dnssec);"
+ }
switch -exact -- $zone(type) {
dynamic { puts $chan "\tallow-update-forwarding { any; };" }
}
} {
global ZONECFG ZONES CONFFILE
+ ## Read the configuration.
confspc-eval toplevel [list source $CONFFILE]
+
+ ## Safely update the files.
set win false
unwind-protect {
+
+ ## Work through each server view.
foreach view $ZONECFG(all-views) {
+
+ ## Open an output file.
set out($view) [output-file-name $view]
set chan($view) [open "$out($view).new" w]
+
+ ## Write a header.
set now [clock format [clock seconds] -format "%Y-%m-%d %H:%M:%S"]
puts $chan($view) "### -*-conf-javaprop-*-"
puts $chan($view) "### Generated at $now: do not edit"
+
+ ## Now print a stanza for each zone in the view.
foreach zone $ZONES {
write-zone-stanza $view $chan($view) $zone
}
}
+
+ ## Done: don't delete the output.
set win true
} {
+
+ ## Close the open files.
foreach view $ZONECFG(all-views) { close $chan($view) }
+
+ ## If we succeeded, rename the output files into their proper places;
+ ## otherwise, delete them.
if {$win} {
foreach view $ZONECFG(all-views) {
file rename -force -- "$out($view).new" $out($view)
defcmd install {user view name} {
help-text "Install a new zone file.
-The file is for the given zone NAME and the \(user-side) VIEW. The file is
+The file is for the given zone NAME and \(user-side) VIEW. The file is
provided by the named USER"
} {
global QUIS ZONECFG ZONES CONFFILE errorInfo errorCode
+ ## Read the configuration.
confspc-eval toplevel [list source $CONFFILE]
+ ## Make sure there's a temporary directory.
file mkdir [file join $ZONECFG(master-dir) "tmp"]
+ ## Keep track of cleanup jobs.
set cleanup {}
unwind-protect {
+ ## Find out which server views are affected by this update.
set matchview {}
foreach iview $ZONECFG(all-views) {
foreach info $ZONES {
array unset zone
array set zone $matchinfo
+ ## Make a new temporary file to read the zone into.
set pid [pid]
for {set i 0} {$i < 1000} {incr i} {
set tmp [file join $ZONECFG(master-dir) "tmp" \
if {![info exists chan]} { error "failed to create temporary file" }
set cleanup [list file delete $tmp]
+ ## Read the zone data from standard input into the file.
set total 0
while {true} {
set stuff [read stdin 4096]
}
close $chan
- set cmd {}
- foreach item $zone(checkzone-command) {
- lappend cmd [string map [list \
- "%z" $name \
- "%v" $view \
- "%f" $tmp] \
- $item]
- }
- set rc [catch {
- set out [eval exec $cmd]
- } msg]
- if {$rc} { set out $msg }
- set out "| [string map [list "\n" "\n| "] $out]"
- if {$rc} {
- puts stderr "$QUIS: zone check failed..."
- puts stderr $out
+ ## Check the zone for sanity.
+ if {![run "zone check" $zone(checkzone-command) \
+ "%z" $name \
+ "%v" $view \
+ "%f" $tmp]} {
+ eval $cleanup
exit 1
- } else {
- puts "$QUIS: zone check output..."
- puts $out
}
+ ## If the zone wants signing, better to do that now.
+ if {![sign-zone-file $matchinfo $tmp keep]} {
+ eval $cleanup
+ exit 2
+ }
+
+ ## All seems good: stash the file in the proper place and reload the
+ ## necessary server views.
file rename -force -- $tmp $zone(file-name)
set cleanup {}
foreach view $matchview {
- set cmd {}
- foreach item $zone(reload-command) {
- lappend cmd [string map [list \
- "%v" $view \
- "%z" $zone(name)] \
- $item]
+ if {![run "reload zone `$zone(name) in view `$view'" \
+ $zone(reload-command) \
+ "%v" $view \
+ "%z" $zone(name)]} {
+ exit 3
}
- eval exec $cmd
}
} {
eval $cleanup
}
}
+defcmd sign {} {
+ help-text "Sign DNSSEC zones."
+} {
+ global QUIS ZONECFG ZONES CONFFILE
+
+ set rc 0
+
+ ## Read the configuration.
+ confspc-eval toplevel [list source $CONFFILE]
+
+ ## Grind through all of the zones.
+ array unset seen
+ foreach view $ZONECFG(all-views) {
+ foreach info $ZONES {
+
+ ## Fetch the zone information.
+ array unset zone
+ set compinfo [compute-zone-properties $view $info]
+ array set zone $compinfo
+ if {![string equal $zone(config-type) master]} { continue }
+
+ if {[string equal $zone(type) static] && $zone(sign)} {
+ ## Static zone: re-sign it if we haven't seen this user view before,
+ ## and then reload.
+
+ ## Sign the zone file if we haven't tried before.
+ set id [list $zone(name) $zone(mapped-view)]
+ if {![info exists seen($id)]} {
+ if {[sign-zone-file $compinfo \
+ $zone(file-name) $zone(soa-format)]} {
+ set seen($id) true
+ } else {
+ set rc 2
+ set seen($id) failed
+ }
+ }
+
+ ## If we succeeded, reload the zone in this server view.
+ if {[string equal $seen($id) true]} {
+ if {![run "reload zone `$zone(name) in server view `$view'" \
+ $zone(reload-command) \
+ "%z" $zone(name) \
+ "%v" $view]} {
+ set rc 2
+ }
+ }
+ } elseif {[string equal $zone(type) dynamic] &&
+ ![string equal $zone(auto-dnssec) off]} {
+ ## Dynamic zone: get BIND to re-sign it.
+
+ if {![run "re-sign zone `$zone(name) in server view `$view'" \
+ $zone(autosign-command) \
+ "%z" $zone(name) \
+ "%v" $view]} {
+ set rc 2
+ }
+ }
+ }
+ }
+ exit $rc
+}
+
###--------------------------------------------------------------------------
### Main program.