bin/zoneconf: Reload zones after signing them.
[zoneconf] / bin / zoneconf
index 820f62f..772088a 100755 (executable)
@@ -164,6 +164,40 @@ proc set* {names values} {
   }
 }
 
+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.
 ###
@@ -953,16 +987,32 @@ define-configuration-space zone ZONECFG {
   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
   }
@@ -1020,7 +1070,9 @@ define-configuration-space toplevel ZONECFG {
   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
@@ -1097,26 +1149,34 @@ proc compute-zone-properties {view 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]
@@ -1151,6 +1211,21 @@ proc write-ddns-update-policy {prefix chan config} {
   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.
@@ -1170,7 +1245,7 @@ proc write-zone-stanza {view chan config} {
   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 }
       }
@@ -1181,6 +1256,9 @@ proc write-zone-stanza {view 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; };" }
       }
@@ -1208,22 +1286,40 @@ defcmd update {} {
 } {
   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)
@@ -1238,18 +1334,22 @@ defcmd update {} {
 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 {
@@ -1271,6 +1371,7 @@ provided by the named USER"
     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" \
@@ -1285,6 +1386,7 @@ provided by the named USER"
     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]
@@ -1297,45 +1399,100 @@ provided by the named USER"
     }
     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.