X-Git-Url: https://git.distorted.org.uk/~mdw/zone/blobdiff_plain/8bd2576ecb4dc8c7ba9d917021d923af40320b5e..679775ba19206747f3a1682cd0090bdae29c13a2:/zone.lisp diff --git a/zone.lisp b/zone.lisp index be6a16e..7318c2a 100644 --- a/zone.lisp +++ b/zone.lisp @@ -1,7 +1,5 @@ ;;; -*-lisp-*- ;;; -;;; $Id$ -;;; ;;; DNS zone generation ;;; ;;; (c) 2005 Straylight/Edgeware @@ -27,19 +25,9 @@ ;;; Packaging. (defpackage #:zone - (:use #:common-lisp #:mdw.base #:mdw.str #:collect #:safely #:net) - (:export #:soa #:mx #:zone #:zone-record #:zone-subdomain - #:*default-zone-source* #:*default-zone-refresh* - #:*default-zone-retry* #:*default-zone-expire* - #:*default-zone-min-ttl* #:*default-zone-ttl* - #:*default-mx-priority* #:*default-zone-admin* - #:*zone-output-path* - #:*preferred-subnets* #:zone-preferred-subnet-p - #:preferred-subnet-case - #:zone-find #:zone-parse #:zone-write #:zone-create #:defzone - #:defrevzone #:zone-save #:zone-make-name - #:defzoneparse #:zone-parse-host - #:timespec-seconds #:make-zone-serial)) + (:use #:common-lisp + #:mdw.base #:mdw.str #:collect #:safely + #:net #:services)) (in-package #:zone) @@ -71,6 +59,7 @@ (push r a) (setf val q))))) +(export 'timespec-seconds) (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 @@ -122,6 +111,7 @@ ;;;-------------------------------------------------------------------------- ;;; Zone types. +(export 'soa) (defstruct (soa (:predicate soap)) "Start-of-authority record information." source @@ -132,11 +122,13 @@ min-ttl serial) +(export 'mx) (defstruct (mx (:predicate mxp)) "Mail-exchange record information." priority domain) +(export 'zone) (defstruct (zone (:predicate zonep)) "Zone information." soa @@ -147,39 +139,37 @@ ;;;-------------------------------------------------------------------------- ;;; Zone defaults. It is intended that scripts override these. -#+ecl -(cffi:defcfun gethostname :int - (name :pointer) - (len :uint)) - +(export '*default-zone-source*) (defvar *default-zone-source* - (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)))))) + (let ((hn (gethostname))) (and hn (concatenate 'string (canonify-hostname hn) "."))) "The default zone source: the current host's name.") +(export '*default-zone-refresh*) (defvar *default-zone-refresh* (* 24 60 60) "Default zone refresh interval: one day.") +(export '*default-zone-admin*) (defvar *default-zone-admin* nil "Default zone administrator's email address.") +(export '*default-zone-retry*) (defvar *default-zone-retry* (* 60 60) "Default znoe retry interval: one hour.") +(export '*default-zone-expire*) (defvar *default-zone-expire* (* 14 24 60 60) "Default zone expiry time: two weeks.") +(export '*default-zone-min-ttl*) (defvar *default-zone-min-ttl* (* 4 60 60) "Default zone minimum TTL/negative TTL: four hours.") +(export '*default-zone-ttl*) (defvar *default-zone-ttl* (* 8 60 60) "Default zone TTL (for records without explicit TTLs): 8 hours.") +(export '*default-mx-priority*) (defvar *default-mx-priority* 50 "Default MX priority.") @@ -189,14 +179,15 @@ (defvar *zones* (make-hash-table :test #'equal) "Map of known zones.") +(export 'zone-find) (defun zone-find (name) "Find a zone given its NAME." (gethash (string-downcase (stringify name)) *zones*)) - (defun (setf zone-find) (zone name) "Make the zone NAME map to ZONE." (setf (gethash (string-downcase (stringify name)) *zones*) zone)) +(export 'zone-record) (defstruct (zone-record (:conc-name zr-)) "A zone record." (name ') @@ -205,6 +196,7 @@ (make-ptr-p nil) data) +(export 'zone-subdomain) (defstruct (zone-subdomain (:conc-name zs-)) "A subdomain. Slightly weird. Used internally by zone-process-records below, and shouldn't escape." @@ -212,9 +204,11 @@ ttl records) +(export '*zone-output-path*) (defvar *zone-output-path* *default-pathname-defaults* "Pathname defaults to merge into output files.") +(export '*preferred-subnets*) (defvar *preferred-subnets* nil "Subnets to prefer when selecting defaults.") @@ -227,10 +221,12 @@ :type (string-downcase type)) *zone-output-path*)) +(export 'zone-preferred-subnet-p) (defun zone-preferred-subnet-p (name) "Answer whether NAME (a string or symbol) names a preferred subnet." (member name *preferred-subnets* :test #'string-equal)) +(export 'preferred-subnet-case) (defmacro preferred-subnet-case (&body clauses) "CLAUSES have the form (SUBNETS . FORMS) -- evaluate the first FORMS whose SUBNETS (a list or single symbol, not evaluated) are considered preferred @@ -301,6 +297,7 @@ (zs-ttl s)))))) (process rec nil ttl))) +(export 'zone-parse-host) (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." @@ -377,49 +374,10 @@ until (zerop (logand mask (ash #xff (* 8 i)))) collect (logand #xff (ash net (* -8 i)))))))) -(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." - (destructuring-bind - (net &key bytes) - (listify (car data)) - (setf net (zone-parse-net net name)) - (unless bytes - (setf bytes (ipnet-changeable-bytes (ipnet-mask net)))) - (dolist (map (cdr data)) - (destructuring-bind - (tnet &optional tdom) - (listify map) - (setf tnet (zone-parse-net tnet name)) - (unless (ipnet-subnetp net tnet) - (error "~A is not a subnet of ~A." - (ipnet-pretty tnet) - (ipnet-pretty net))) - (unless tdom - (setf tdom - (join-strings #\. - (list (zone-cidr-delg-default-name tnet bytes) - name)))) - (setf tdom (string-downcase tdom)) - (dotimes (i (ipnet-hosts tnet)) - (let* ((addr (ipnet-host tnet i)) - (tail (join-strings #\. - (loop - for i from 0 below bytes - collect - (logand #xff - (ash addr (* 8 i))))))) - (collect (make-zone-record - :name (join-strings #\. - (list tail name)) - :type :cname - :ttl ttl - :data (join-strings #\. (list tail tdom))) - list))))))) - ;;;-------------------------------------------------------------------------- ;;; Serial numbering. +(export 'make-zone-serial) (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." @@ -480,6 +438,7 @@ :min-ttl (timespec-seconds min-ttl) :serial serial)))) +(export 'zone-make-name) (defun zone-make-name (prefix zone-name) (if (or (not prefix) (string= prefix "@")) zone-name @@ -488,6 +447,7 @@ (join-strings #\. (list prefix zone-name)) prefix)))) +(export 'defzoneparse) (defmacro defzoneparse (types (name data list &key (prefix (gensym "PREFIX")) (zname (gensym "ZNAME")) @@ -536,6 +496,7 @@ ((:data ,tdata) ,data) ((:ttl ,tttl) ,ttl) ((:make-ptr-p ,tmakeptrp) nil)) + #+cmu (declare (optimize ext:inhibit-warnings)) (collect (make-zone-record :name ,tname :type ,ttype :data ,tdata @@ -564,6 +525,7 @@ #'parse-record)) (setf (zone-records zone) (nconc (zone-records zone) rec))))) +(export 'zone-parse) (defun zone-parse (zf) "Parse a ZONE form. The syntax of a zone form is as follows: @@ -581,6 +543,7 @@ (zone-parse-records zone (cdr zf)) zone))) +(export 'zone-create) (defun zone-create (zf) "Zone construction function. Given a zone form ZF, construct the zone and add it to the table." @@ -589,10 +552,12 @@ (setf (zone-find name) zone) name)) +(export 'defzone) (defmacro defzone (soa &rest zf) "Zone definition macro." `(zone-create '(,soa ,@zf))) +(export 'defrevzone) (defmacro defrevzone (head &rest zf) "Define a reverse zone, with the correct name." (destructuring-bind @@ -622,6 +587,10 @@ ":cname HOST" (rec :data (zone-parse-host data zname))) +(defzoneparse :txt (name data rec) + ":txt TEXT" + (rec :data data)) + (defzoneparse :mx (name data rec :zname zname) ":mx ((HOST :prio INT :ip IPADDR)*)" (dolist (mx (listify data)) @@ -649,6 +618,31 @@ :type :cname :data name))) +(defzoneparse :srv (name data rec :zname zname) + ":srv (((SERVICE &key :port) (PROVIDER &key :port :prio :weight :ip)*)*)" + (dolist (srv data) + (destructuring-bind (servopts &rest providers) srv + (destructuring-bind + (service &key ((:port default-port)) (protocol :tcp)) + (listify servopts) + (unless default-port + (let ((serv (serv-by-name service protocol))) + (setf default-port (and serv (serv-port serv))))) + (let ((rname (format nil "~(_~A._~A~).~A" service protocol name))) + (dolist (prov providers) + (destructuring-bind + (srvname + &key + (port default-port) + (prio *default-mx-priority*) + (weight 0) + ip) + (listify prov) + (let ((host (zone-parse-host srvname zname))) + (when ip (rec :name host :type :a :data (parse-ipaddr ip))) + (rec :name rname + :data (list prio weight port host)))))))))) + (defzoneparse :net (name data rec) ":net (NETWORK*)" (dolist (net (listify data)) @@ -664,11 +658,13 @@ :data (ipnet-broadcast n))))) (defzoneparse (:rev :reverse) (name data rec) - ":reverse ((NET :bytes BYTES) ZONE*)" + ":reverse ((NET :bytes BYTES) ZONE*) + + Add a reverse record each host in the ZONEs (or all zones) that lies + within NET. The BYTES give the number of prefix labels generated; this + defaults to the smallest number of bytes needed to enumerate the net." (setf data (listify data)) - (destructuring-bind - (net &key bytes) - (listify (car data)) + (destructuring-bind (net &key bytes) (listify (car data)) (setf net (zone-parse-net net name)) (unless bytes (setf bytes (ipnet-changeable-bytes (ipnet-mask net)))) @@ -692,18 +688,27 @@ :ttl (zr-ttl zr) :data (zr-name zr)) (setf (gethash name seen) t))))))))) -(defzoneparse (:cidr-delegation :cidr) (name data rec) - ":cidr-delegation ((NET :bytes BYTES) (TARGET-NET [TARGET-ZONE])*)" - (destructuring-bind - (net &key bytes) - (listify (car data)) +(defzoneparse (:cidr-delegation :cidr) (name data rec :zname zname) + ":cidr-delegation ((NET :bytes BYTES) ((TARGET-NET*) [TARGET-ZONE])*) + + Insert CNAME records for delegating a portion of the reverse-lookup + namespace which doesn't align with an octet boundary. + + The NET specifies the origin network, in which the reverse records + naturally lie. The BYTES are the number of labels to supply for each + address; the default is the smallest number which suffices to enumerate + the entire NET. The TARGET-NETs are subnets of NET which are to be + delegated. The TARGET-ZONEs are the zones to which we are delegating + authority for the reverse records: the default is to append labels for those + octets of the subnet base address which are not the same in all address in + the subnet." + (setf data (listify data)) + (destructuring-bind (net &key bytes) (listify (car data)) (setf net (zone-parse-net net name)) (unless bytes (setf bytes (ipnet-changeable-bytes (ipnet-mask net)))) - (dolist (map (cdr data)) - (destructuring-bind - (tnet &optional tdom) - (listify map) + (dolist (map (or (cdr data) (list (list net)))) + (destructuring-bind (tnet &optional tdom) (listify map) (setf tnet (zone-parse-net tnet name)) (unless (ipnet-subnetp net tnet) (error "~A is not a subnet of ~A." @@ -715,57 +720,79 @@ (join-strings #\. (append (reverse (loop - for i from (1- bytes) downto 0 - until (zerop (logand mask - (ash #xff - (* 8 i)))) - collect (logand #xff - (ash net (* -8 i))))) + for i from (1- bytes) downto 0 + until (zerop (logand mask + (ash #xff + (* 8 i)))) + collect (ldb (byte 8 (* i 8)) net))) (list name)))))) - (setf tdom (string-downcase tdom)) + (setf tdom (string-downcase (stringify tdom))) (dotimes (i (ipnet-hosts tnet)) - (let* ((addr (ipnet-host tnet i)) - (tail (join-strings #\. - (loop + (unless (zerop i) + (let* ((addr (ipnet-host tnet i)) + (tail (join-strings #\. + (loop for i from 0 below bytes collect - (logand #xff - (ash addr (* 8 i))))))) - (rec :name (format nil "~A.~A" tail name) - :type :cname - :data (format nil "~A.~A" tail tdom)))))))) + (logand #xff + (ash addr (* 8 i))))))) + (rec :name (format nil "~A.~A" tail name) + :type :cname + :data (format nil "~A.~A" tail tdom))))))))) ;;;-------------------------------------------------------------------------- ;;; Zone file output. -(defun zone-write (zone &optional (stream *standard-output*)) - "Write a ZONE's records to STREAM." - (labels ((fix-admin (a) - (let ((at (position #\@ a)) - (s (concatenate 'string (string-downcase a) "."))) - (when s - (setf (char s at) #\.)) - s)) - (fix-host (h) - (if (not h) - "@" - (let* ((h (string-downcase (stringify h))) - (hl (length h)) - (r (string-downcase (zone-name zone))) - (rl (length r))) - (cond ((string= r h) "@") - ((and (> hl rl) - (char= (char h (- hl rl 1)) #\.) - (string= h r :start1 (- hl rl))) - (subseq h 0 (- hl rl 1))) - (t (concatenate 'string h ".")))))) - (printrec (zr) - (format stream "~A~20T~@[~8D~]~30TIN ~A~40T" - (fix-host (zr-name zr)) - (and (/= (zr-ttl zr) (zone-default-ttl zone)) - (zr-ttl zr)) - (string-upcase (symbol-name (zr-type zr)))))) - (format stream "~ +(export 'zone-write) +(defgeneric zone-write (format zone stream) + (:documentation "Write ZONE's records to STREAM in the specified FORMAT.")) + +(defvar *writing-zone* nil + "The zone currently being written.") + +(defvar *zone-output-stream* nil + "Stream to write zone data on.") + +(defmethod zone-write :around (format zone stream) + (let ((*writing-zone* zone) + (*zone-output-stream* stream)) + (call-next-method))) + +(export 'zone-save) +(defun zone-save (zones &key (format :bind)) + "Write the named ZONES to files. If no zones are given, write all the + zones." + (unless zones + (setf zones (hash-table-keys *zones*))) + (safely (safe) + (dolist (z zones) + (let ((zz (zone-find z))) + (unless zz + (error "Unknown zone `~A'." z)) + (let ((stream (safely-open-output-stream safe + (zone-file-name z :zone)))) + (zone-write format zz stream)))))) + +;;;-------------------------------------------------------------------------- +;;; Bind format output. + +(export 'bind-hostname) +(defun bind-hostname (hostname) + (if (not hostname) + "@" + (let* ((h (string-downcase (stringify hostname))) + (hl (length h)) + (r (string-downcase (zone-name *writing-zone*))) + (rl (length r))) + (cond ((string= r h) "@") + ((and (> hl rl) + (char= (char h (- hl rl 1)) #\.) + (string= h r :start1 (- hl rl))) + (subseq h 0 (- hl rl 1))) + (t (concatenate 'string h ".")))))) + +(defmethod zone-write ((format (eql :bind)) zone stream) + (format stream "~ ;;; Zone file `~(~A~)' ;;; (generated ~A) @@ -774,7 +801,13 @@ $TTL ~2@*~D~2%" (zone-name zone) (iso-date :now :datep t :timep t) (zone-default-ttl zone)) - (let ((soa (zone-soa zone))) + (let* ((soa (zone-soa zone)) + (admin (let* ((name (soa-admin soa)) + (at (position #\@ name)) + (copy (format nil "~(~A~)." name))) + (when at + (setf (char copy at) #\.)) + copy))) (format stream "~ ~A~30TIN SOA~40T~A ~A ( ~45T~10D~60T ;serial @@ -782,42 +815,53 @@ $TTL ~2@*~D~2%" ~45T~10D~60T ;retry ~45T~10D~60T ;expire ~45T~10D )~60T ;min-ttl~2%" - (fix-host (zone-name zone)) - (fix-host (soa-source soa)) - (fix-admin (soa-admin soa)) + (bind-hostname (zone-name zone)) + (bind-hostname (soa-source soa)) + admin (soa-serial soa) (soa-refresh soa) (soa-retry soa) (soa-expire soa) (soa-min-ttl soa))) - (dolist (zr (zone-records zone)) - (ecase (zr-type zr) - (:a - (printrec zr) - (format stream "~A~%" (ipaddr-string (zr-data zr)))) - ((:ptr :cname :ns) - (printrec zr) - (format stream "~A~%" (fix-host (zr-data zr)))) - (:mx - (printrec zr) - (let ((mx (zr-data zr))) - (format stream "~2D ~A~%" (cdr mx) (fix-host (car mx))))) - (:txt - (printrec zr) - (format stream "~S~%" (stringify (zr-data zr)))))))) - -(defun zone-save (zones) - "Write the named ZONES to files. If no zones are given, write all the - zones." - (unless zones - (setf zones (hash-table-keys *zones*))) - (safely (safe) - (dolist (z zones) - (let ((zz (zone-find z))) - (unless zz - (error "Unknown zone `~A'." z)) - (let ((stream (safely-open-output-stream safe - (zone-file-name z :zone)))) - (zone-write zz stream)))))) + (dolist (zr (zone-records zone)) + (bind-record (zr-type zr) zr))) + +(export 'bind-record) +(defgeneric bind-record (type zr)) + +(export 'bind-format-record) +(defun bind-format-record (name ttl type format args) + (format *zone-output-stream* + "~A~20T~@[~8D~]~30TIN ~A~40T~?~%" + (bind-hostname name) + (and (/= ttl (zone-default-ttl *writing-zone*)) + ttl) + (string-upcase (symbol-name type)) + format args)) + +(defmethod bind-record (type zr) + (destructuring-bind (format &rest args) + (bind-record-format-args type (zr-data zr)) + (bind-format-record (zr-name zr) + (zr-ttl zr) + (bind-record-type type) + format args))) + +(export 'bind-record-type) +(defgeneric bind-record-type (type) + (:method (type) type)) + +(export 'bind-record-format-args) +(defgeneric bind-record-format-args (type data) + (:method ((type (eql :a)) data) (list "~A" (ipaddr-string data))) + (:method ((type (eql :ptr)) data) (list "~A" (bind-hostname data))) + (:method ((type (eql :cname)) data) (list "~A" (bind-hostname data))) + (:method ((type (eql :ns)) data) (list "~A" (bind-hostname data))) + (:method ((type (eql :mx)) data) + (list "~2D ~A" (cdr data) (bind-hostname (car data)))) + (:method ((type (eql :srv)) data) + (destructuring-bind (prio weight port host) data + (list "~2D ~5D ~5D ~A" prio weight port (bind-hostname host)))) + (:method ((type (eql :txt)) data) (list "~S" (stringify data)))) ;;;----- That's all, folks --------------------------------------------------