X-Git-Url: https://git.distorted.org.uk/~mdw/zone/blobdiff_plain/40ded1b8f413409cd756a9bd3919c389c9f841a6..ab87c7bf4977fe6b89e8e6d1a45c300e341d366a:/zone.lisp diff --git a/zone.lisp b/zone.lisp index 79c6c19..6b11880 100644 --- a/zone.lisp +++ b/zone.lisp @@ -13,12 +13,12 @@ ;;; it under the terms of the GNU General Public License as published by ;;; the Free Software Foundation; either version 2 of the License, or ;;; (at your option) any later version. -;;; +;;; ;;; This program is distributed in the hope that it will be useful, ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; GNU General Public License for more details. -;;; +;;; ;;; You should have received a copy of the GNU General Public License ;;; along with this program; if not, write to the Free Software Foundation, ;;; Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. @@ -33,8 +33,9 @@ #:*default-zone-retry* #:*default-zone-expire* #:*default-zone-min-ttl* #:*default-zone-ttl* #:*default-mx-priority* #:*default-zone-admin* - #:zone-find #:zone-parse #:zone-write #:zone-create #:defzone - #:defrevzone #:zone-save + #:*zone-output-path* + #:zone-find #:zone-parse #:zone-write #:zone-create #:defzone + #:defrevzone #:zone-save #:defzoneparse #:zone-parse-host #:timespec-seconds #:make-zone-serial)) @@ -49,7 +50,7 @@ (defun from-mixed-base (base val) "BASE is a list of the ranges for the `digits' of a mixed-base -representation. Convert VAL, a list of digits, into an integer." + representation. Convert VAL, a list of digits, into an integer." (do ((base base (cdr base)) (val (cdr val) (cdr val)) (a (car val) (+ (* a (car base)) (car val)))) @@ -57,7 +58,7 @@ representation. Convert VAL, a list of digits, into an integer." (defun to-mixed-base (base val) "BASE is a list of the ranges for the `digits' of a mixed-base -representation. Convert VAL, an integer, into a list of digits." + representation. Convert VAL, an integer, into a list of digits." (let ((base (reverse base)) (a nil)) (loop @@ -70,8 +71,8 @@ representation. Convert VAL, an integer, into a list of digits." (defun timespec-seconds (ts) "Convert a timespec TS to seconds. A timespec may be a real count of -seconds, or a list (COUNT UNIT): UNIT may be any of a number of obvious time -units." + seconds, or a list (COUNT UNIT): UNIT may be any of a number of obvious + time units." (cond ((null ts) 0) ((realp ts) (floor ts)) ((atom ts) @@ -99,9 +100,9 @@ units." (defun iso-date (&optional time &key datep timep (sep #\ )) "Construct a textual date or time in ISO format. The TIME is the universal -time to convert, which defaults to now; DATEP is whether to emit the date; -TIMEP is whether to emit the time, and SEP (default is space) is how to -separate the two." + time to convert, which defaults to now; DATEP is whether to emit the date; + TIMEP is whether to emit the time, and SEP (default is space) is how to + separate the two." (multiple-value-bind (sec min hr day mon yr dow dstp tz) (decode-universal-time (if (or (null time) (eq time :now)) @@ -144,8 +145,18 @@ separate the two." ;;;-------------------------------------------------------------------------- ;;; Zone defaults. It is intended that scripts override these. +#+ecl +(cffi:defcfun gethostname :int + (name :pointer) + (len :uint)) + (defvar *default-zone-source* - (let ((hn (unix:unix-gethostname))) + (let ((hn #+cmu (unix:unix-gethostname) + #+clisp (unix:get-host-name) + #+ecl (cffi:with-foreign-pointer-as-string (buffer 256 len) + (let ((rc (gethostname buffer len))) + (unless (zerop rc) + (error "gethostname(2) failed (rc = ~A)." rc)))))) (and hn (concatenate 'string (canonify-hostname hn) "."))) "The default zone source: the current host's name.") @@ -171,35 +182,6 @@ separate the two." "Default MX priority.") ;;;-------------------------------------------------------------------------- -;;; Serial numbering. - -(defun make-zone-serial (name) - "Given a zone NAME, come up with a new serial number. This will (very -carefully) update a file ZONE.serial in the current directory." - (let* ((file (format nil "~(~A~).serial" name)) - (last (with-open-file (in file - :direction :input - :if-does-not-exist nil) - (if in (read in) - (list 0 0 0 0)))) - (now (multiple-value-bind - (sec min hr dy mon yr dow dstp tz) - (get-decoded-time) - (declare (ignore sec min hr dow dstp tz)) - (list dy mon yr))) - (seq (cond ((not (equal now (cdr last))) 0) - ((< (car last) 99) (1+ (car last))) - (t (error "Run out of sequence numbers for ~A" name))))) - (safely-writing (out file) - (format out - ";; Serial number file for zone ~A~%~ - ;; (LAST-SEQ DAY MONTH YEAR)~%~ - ~S~%" - name - (cons seq now))) - (from-mixed-base '(100 100 100) (reverse (cons seq now))))) - -;;;-------------------------------------------------------------------------- ;;; Zone variables and structures. (defvar *zones* (make-hash-table :test #'equal) @@ -223,17 +205,26 @@ carefully) update a file ZONE.serial in the current directory." (defstruct (zone-subdomain (:conc-name zs-)) "A subdomain. Slightly weird. Used internally by zone-process-records -below, and shouldn't escape." + below, and shouldn't escape." name ttl records) +(defvar *zone-output-path* *default-pathname-defaults* + "Pathname defaults to merge into output files.") + ;;;-------------------------------------------------------------------------- ;;; Zone infrastructure. +(defun zone-file-name (zone type) + "Choose a file name for a given ZONE and TYPE." + (merge-pathnames (make-pathname :name (string-downcase zone) + :type (string-downcase type)) + *zone-output-path*)) + (defun zone-process-records (rec ttl func) "Sort out the list of records in REC, calling FUNC for each one. TTL is -the default time-to-live for records which don't specify one." + the default time-to-live for records which don't specify one." (labels ((sift (rec ttl) (collecting (top sub) (loop @@ -283,7 +274,7 @@ the default time-to-live for records which don't specify one." (defun zone-parse-host (f zname) "Parse a host name F: if F ends in a dot then it's considered absolute; -otherwise it's relative to ZNAME." + otherwise it's relative to ZNAME." (setf f (stringify f)) (cond ((string= f "@") (stringify zname)) ((and (plusp (length f)) @@ -293,7 +284,7 @@ otherwise it's relative to ZNAME." (stringify zname)))))) (defun default-rev-zone (base bytes) "Return the default reverse-zone name for the given BASE address and number -of fixed leading BYTES." + of fixed leading BYTES." (join-strings #\. (collecting () (loop for i from (- 3 bytes) downto 0 do (collect (ipaddr-byte base i))) @@ -301,7 +292,7 @@ of fixed leading BYTES." (defun zone-name-from-net (net &optional bytes) "Given a NET, and maybe the BYTES to use, convert to the appropriate -subdomain of in-addr.arpa." + subdomain of in-addr.arpa." (let ((ipn (net-get-as-ipnet net))) (with-ipnet (net mask) ipn (unless bytes @@ -339,54 +330,16 @@ subdomain of in-addr.arpa." (setf addr (ash addr (* 8 (- 4 n)))) (make-ipnet addr (* 8 n)))) -(defun zone-reverse-records (records net list bytes dom) - "Construct a reverse zone given a forward zone's RECORDS list, the NET that -the reverse zone is to serve, a LIST to collect the records into, how -many BYTES of data need to end up in the zone, and the DOM-ain suffix." - (dolist (zr records) - (when (and (eq (zr-type zr) :a) - (not (zr-defsubp zr)) - (ipaddr-networkp (zr-data zr) net)) - (collect (make-zone-record - :name (string-downcase - (join-strings - #\. - (collecting () - (dotimes (i bytes) - (collect (logand #xff (ash (zr-data zr) - (* -8 i))))) - (collect dom)))) - :type :ptr - :ttl (zr-ttl zr) - :data (zr-name zr)) - list)))) - -(defun zone-reverse (data name list) - "Process a :reverse record's DATA, for a domain called NAME, and add the -records to the LIST." - (destructuring-bind - (net &key bytes zones) - (listify data) - (setf net (zone-parse-net net name)) - (dolist (z (or (listify zones) - (hash-table-keys *zones*))) - (zone-reverse-records (zone-records (zone-find z)) - net - list - (or bytes - (ipnet-changeable-bytes (ipnet-mask net))) - name)))) - (defun zone-parse-net (net name) - "Given a NET, and the NAME of a domain to guess from if NET is null, -return the ipnet for the network." + "Given a NET, and the NAME of a domain to guess from if NET is null, return + the ipnet for the network." (if net (net-get-as-ipnet net) (zone-net-from-name name))) (defun zone-cidr-delg-default-name (ipn bytes) "Given a delegated net IPN and the parent's number of changing BYTES, -return the default deletate zone prefix." + return the default deletate zone prefix." (with-ipnet (net mask) ipn (join-strings #\. (reverse @@ -397,7 +350,7 @@ return the default deletate zone prefix." (defun zone-cidr-delegation (data name ttl list) "Given :cidr-delegation info DATA, for a record called NAME and the current -TTL, write lots of CNAME records to LIST." + TTL, write lots of CNAME records to LIST." (destructuring-bind (net &key bytes) (listify (car data)) @@ -412,7 +365,7 @@ TTL, write lots of CNAME records to LIST." (unless (ipnet-subnetp net tnet) (error "~A is not a subnet of ~A." (ipnet-pretty tnet) - (ipnet-pretty net))) + (ipnet-pretty net))) (unless tdom (setf tdom (join-strings #\. @@ -434,7 +387,36 @@ TTL, write lots of CNAME records to LIST." :ttl ttl :data (join-strings #\. (list tail tdom))) list))))))) - + +;;;-------------------------------------------------------------------------- +;;; Serial numbering. + +(defun make-zone-serial (name) + "Given a zone NAME, come up with a new serial number. This will (very + carefully) update a file ZONE.serial in the current directory." + (let* ((file (zone-file-name name :serial)) + (last (with-open-file (in file + :direction :input + :if-does-not-exist nil) + (if in (read in) + (list 0 0 0 0)))) + (now (multiple-value-bind + (sec min hr dy mon yr dow dstp tz) + (get-decoded-time) + (declare (ignore sec min hr dow dstp tz)) + (list dy mon yr))) + (seq (cond ((not (equal now (cdr last))) 0) + ((< (car last) 99) (1+ (car last))) + (t (error "Run out of sequence numbers for ~A" name))))) + (safely-writing (out file) + (format out + ";; Serial number file for zone ~A~%~ + ;; (LAST-SEQ DAY MONTH YEAR)~%~ + ~S~%" + name + (cons seq now))) + (from-mixed-base '(100 100 100) (reverse (cons seq now))))) + ;;;-------------------------------------------------------------------------- ;;; Zone form parsing. @@ -444,8 +426,8 @@ TTL, write lots of CNAME records to LIST." (NAME &key :source :admin :refresh :retry :expire :min-ttl :ttl :serial) -though a singleton NAME needn't be a list. Returns the default TTL and an -soa structure representing the zone head." + though a singleton NAME needn't be a list. Returns the default TTL and an + soa structure representing the zone head." (destructuring-bind (zname &key @@ -475,34 +457,35 @@ soa structure representing the zone head." (defsubp (gensym "DEFSUBP"))) &body body) "Define a new zone record type (or TYPES -- a list of synonyms is -permitted). The arguments are as follows: + permitted). The arguments are as follows: -NAME The name of the record to be added. + NAME The name of the record to be added. -DATA The content of the record to be added (a single object, unevaluated). + DATA The content of the record to be added (a single object, + unevaluated). -LIST A function to add a record to the zone. See below. + LIST A function to add a record to the zone. See below. -ZNAME The name of the zone being constructed. + ZNAME The name of the zone being constructed. -TTL The TTL for this record. + TTL The TTL for this record. -DEFSUBP Whether this is the default subdomain for this entry. + DEFSUBP Whether this is the default subdomain for this entry. -You get to choose your own names for these. ZNAME, TTL and DEFSUBP are -optional: you don't have to accept them if you're not interested. + You get to choose your own names for these. ZNAME, TTL and DEFSUBP are + optional: you don't have to accept them if you're not interested. -The LIST argument names a function to be bound in the body to add a new -low-level record to the zone. It has the prototype + The LIST argument names a function to be bound in the body to add a new + low-level record to the zone. It has the prototype - (LIST &key :name :type :data :ttl :defsubp) + (LIST &key :name :type :data :ttl :defsubp) -Except for defsubp, these default to the above arguments (even if you didn't -accept the arguments)." + Except for defsubp, these default to the above arguments (even if you + didn't accept the arguments)." (setf types (listify types)) (let* ((type (car types)) (func (intern (format nil "ZONE-PARSE/~:@(~A~)" type)))) - (multiple-value-bind (doc decls body) (parse-body body) + (with-parsed-body (body decls doc) body (with-gensyms (col tname ttype tttl tdata tdefsubp i) `(progn (dolist (,i ',types) @@ -551,18 +534,18 @@ accept the arguments)." (zr-defsubp zr))))) (zone-process-records records (zone-default-ttl zone) - #'parse-record )) + #'parse-record)) (setf (zone-records zone) (nconc (zone-records zone) rec))))) (defun zone-parse (zf) "Parse a ZONE form. The syntax of a zone form is as follows: -ZONE-FORM: - ZONE-HEAD ZONE-RECORD* + ZONE-FORM: + ZONE-HEAD ZONE-RECORD* -ZONE-RECORD: - ((NAME*) ZONE-RECORD*) -| SYM ARGS" + ZONE-RECORD: + ((NAME*) ZONE-RECORD*) + | SYM ARGS" (multiple-value-bind (zname ttl soa) (zone-parse-head (car zf)) (let ((zone (make-zone :name zname :default-ttl ttl @@ -573,7 +556,7 @@ ZONE-RECORD: (defun zone-create (zf) "Zone construction function. Given a zone form ZF, construct the zone and -add it to the table." + add it to the table." (let* ((zone (zone-parse zf)) (name (zone-name zone))) (setf (zone-find name) zone) @@ -648,7 +631,7 @@ add it to the table." (rec :name (zone-parse-host "broadcast" name) :type :a :data (ipnet-broadcast n))))) - + (defzoneparse (:rev :reverse) (name data rec) ":reverse ((NET :bytes BYTES) ZONE*)" (setf data (listify data)) @@ -692,7 +675,7 @@ add it to the table." (unless (ipnet-subnetp net tnet) (error "~A is not a subnet of ~A." (ipnet-pretty tnet) - (ipnet-pretty net))) + (ipnet-pretty net))) (unless tdom (with-ipnet (net mask) tnet (setf tdom @@ -753,8 +736,8 @@ add it to the table." ;;; Zone file `~(~A~)' ;;; (generated ~A) -$ORIGIN ~@0*~(~A.~) -$TTL ~@2*~D~2%" +$ORIGIN ~0@*~(~A.~) +$TTL ~2@*~D~2%" (zone-name zone) (iso-date :now :datep t :timep t) (zone-default-ttl zone)) @@ -775,14 +758,11 @@ $TTL ~@2*~D~2%" (soa-expire soa) (soa-min-ttl soa))) (dolist (zr (zone-records zone)) - (case (zr-type zr) + (ecase (zr-type zr) (:a (printrec zr) (format stream "~A~%" (ipaddr-string (zr-data zr)))) - ((:ptr :cname) - (printrec zr) - (format stream "~A~%" (fix-host (zr-data zr)))) - (:ns + ((:ptr :cname :ns) (printrec zr) (format stream "~A~%" (fix-host (zr-data zr)))) (:mx @@ -795,7 +775,7 @@ $TTL ~@2*~D~2%" (defun zone-save (zones) "Write the named ZONES to files. If no zones are given, write all the -zones." + zones." (unless zones (setf zones (hash-table-keys *zones*))) (safely (safe) @@ -804,9 +784,7 @@ zones." (unless zz (error "Unknown zone `~A'." z)) (let ((stream (safely-open-output-stream safe - (format nil - "~(~A~).zone" - z)))) + (zone-file-name z :zone)))) (zone-write zz stream)))))) ;;;----- That's all, folks --------------------------------------------------