;;; -*-lisp-*-
;;;
-;;; $Id$
-;;;
;;; DNS zone generation
;;;
;;; (c) 2005 Straylight/Edgeware
;;; 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.
;;; Packaging.
(defpackage #:zone
- (:use #:common-lisp #:mdw.base #:mdw.str #:mdw.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-find #:zone-parse #:zone-write #:zone-create #:defzone
- #:defrevzone #:zone-save
- #:defzoneparse #:zone-parse-host
- #:timespec-seconds #:make-zone-serial))
+ (:use #:common-lisp
+ #:mdw.base #:mdw.str #:collect #:safely
+ #:net #:services)
+ (:import-from #:net #:round-down #:round-up))
(in-package #:zone)
(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))))
(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
(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 time
-units."
+ "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."
(cond ((null ts) 0)
((realp ts) (floor ts))
((atom ts)
(maphash (lambda (key val) (declare (ignore val)) (collect key)) ht)))
(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."
+ "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."
(multiple-value-bind
(sec min hr day mon yr dow dstp tz)
(decode-universal-time (if (or (null time) (eq time :now))
(when timep
(format s "~2,'0D:~2,'0D:~2,'0D" hr min sec)))))
+(defun natural-string< (string1 string2
+ &key (start1 0) (end1 nil)
+ (start2 0) (end2 nil))
+ "Answer whether STRING1 precedes STRING2 in a vaguely natural ordering.
+
+ In particular, digit sequences are handled in a moderately sensible way.
+ Split the strings into maximally long alternating sequences of non-numeric
+ and numeric characters, such that the non-numeric sequences are
+ non-empty. Compare these lexicographically; numeric sequences order
+ according to their integer values, non-numeric sequences in the usual
+ lexicographic ordering.
+
+ Returns two values: whether STRING1 strictly precedes STRING2, and whether
+ STRING1 strictly follows STRING2."
+
+ (let ((end1 (or end1 (length string1)))
+ (end2 (or end2 (length string2))))
+ (loop
+ (cond ((>= start1 end1)
+ (let ((eqp (>= start2 end2)))
+ (return (values (not eqp) nil))))
+ ((>= start2 end2)
+ (return (values nil t)))
+ ((and (digit-char-p (char string1 start1))
+ (digit-char-p (char string2 start2)))
+ (let* ((lim1 (or (position-if-not #'digit-char-p string1
+ :start start1 :end end1)
+ end1))
+ (n1 (parse-integer string1 :start start1 :end lim1))
+ (lim2 (or (position-if-not #'digit-char-p string2
+ :start start2 :end end2)
+ end2))
+ (n2 (parse-integer string2 :start start2 :end lim2)))
+ (cond ((< n1 n2) (return (values t nil)))
+ ((> n1 n2) (return (values nil t))))
+ (setf start1 lim1
+ start2 lim2)))
+ (t
+ (let ((lim1 (or (position-if #'digit-char-p string1
+ :start start1 :end end1)
+ end1))
+ (lim2 (or (position-if #'digit-char-p string2
+ :start start2 :end end2)
+ end2)))
+ (cond ((string< string1 string2
+ :start1 start1 :end1 lim1
+ :start2 start2 :end2 lim2)
+ (return (values t nil)))
+ ((string> string1 string2
+ :start1 start1 :end1 lim1
+ :start2 start2 :end2 lim2)
+ (return (values nil t))))
+ (setf start1 lim1
+ start2 lim2)))))))
+
+(defun domain-name< (name-a name-b)
+ "Answer whether NAME-A precedes NAME-B in an ordering of domain names.
+
+ Split the names into labels at the dots, and then lexicographically
+ compare the sequences of labels, right to left, using `natural-string<'.
+
+ Returns two values: whether NAME-A strictly precedes NAME-B, and whether
+ NAME-A strictly follows NAME-B."
+ (let ((pos-a (length name-a))
+ (pos-b (length name-b)))
+ (loop (let ((dot-a (or (position #\. name-a
+ :from-end t :end pos-a)
+ -1))
+ (dot-b (or (position #\. name-b
+ :from-end t :end pos-b)
+ -1)))
+ (multiple-value-bind (precp follp)
+ (natural-string< name-a name-b
+ :start1 (1+ dot-a) :end1 pos-a
+ :start2 (1+ dot-b) :end2 pos-b)
+ (cond (precp
+ (return (values t nil)))
+ (follp
+ (return (values nil t)))
+ ((= dot-a -1)
+ (let ((eqp (= dot-b -1)))
+ (return (values (not eqp) nil))))
+ ((= dot-b -1)
+ (return (values nil t)))
+ (t
+ (setf pos-a dot-a
+ pos-b dot-b))))))))
+
;;;--------------------------------------------------------------------------
;;; Zone types.
+(export 'soa)
(defstruct (soa (:predicate soap))
"Start-of-authority record information."
source
min-ttl
serial)
+(export 'mx)
(defstruct (mx (:predicate mxp))
"Mail-exchange record information."
priority
domain)
+(export 'zone)
(defstruct (zone (:predicate zonep))
"Zone information."
soa
;;;--------------------------------------------------------------------------
;;; Zone defaults. It is intended that scripts override these.
+(export '*default-zone-source*)
(defvar *default-zone-source*
- (let ((hn (unix:unix-gethostname)))
+ (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.")
+;;;--------------------------------------------------------------------------
+;;; Zone variables and structures.
+
+(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 '<unnamed>)
+ ttl
+ type
+ (make-ptr-p nil)
+ data)
+
+(export 'zone-subdomain)
+(defstruct (zone-subdomain (:conc-name zs-))
+ "A subdomain.
+
+ Slightly weird. Used internally by `zone-process-records', and shouldn't
+ escape."
+ name
+ ttl
+ records)
+
+(export '*zone-output-path*)
+(defvar *zone-output-path* nil
+ "Pathname defaults to merge into output files.
+
+ If this is nil then use the prevailing `*default-pathname-defaults*'.
+ This is not the same as capturing the `*default-pathname-defaults*' from
+ load time.")
+
+(export '*preferred-subnets*)
+(defvar *preferred-subnets* nil
+ "Subnets to prefer when selecting defaults.")
+
+;;;--------------------------------------------------------------------------
+;;; 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))
+ (or *zone-output-path* *default-pathname-defaults*)))
+
+(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)
+ "Execute a form based on which networks are considered preferred.
+
+ The CLAUSES have the form (SUBNETS . FORMS) -- evaluate the first FORMS
+ whose SUBNETS (a list or single symbol, not evaluated) are listed in
+ `*preferred-subnets*'. If SUBNETS is the symbol `t' then the clause
+ always matches."
+ `(cond
+ ,@(mapcar (lambda (clause)
+ (let ((subnets (car clause)))
+ (cons (cond ((eq subnets t)
+ t)
+ ((listp subnets)
+ `(or ,@(mapcar (lambda (subnet)
+ `(zone-preferred-subnet-p
+ ',subnet))
+ subnets)))
+ (t
+ `(zone-preferred-subnet-p ',subnets)))
+ (cdr clause))))
+ clauses)))
+
+(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."
+ (setf f (stringify f))
+ (cond ((string= f "@") (stringify zname))
+ ((and (plusp (length f))
+ (char= (char f (1- (length f))) #\.))
+ (string-downcase (subseq f 0 (1- (length f)))))
+ (t (string-downcase (concatenate 'string f "."
+ (stringify zname))))))
+
+(export 'zone-make-name)
+(defun zone-make-name (prefix zone-name)
+ "Compute a full domain name from a PREFIX and a ZONE-NAME.
+
+ If the PREFIX ends with `.' then it's absolute already; otherwise, append
+ the ZONE-NAME, separated with a `.'. If PREFIX is nil, or `@', then
+ return the ZONE-NAME only."
+ (if (or (not prefix) (string= prefix "@"))
+ zone-name
+ (let ((len (length prefix)))
+ (if (or (zerop len) (char/= (char prefix (1- len)) #\.))
+ (join-strings #\. (list prefix zone-name))
+ prefix))))
+
+(export 'zone-records-sorted)
+(defun zone-records-sorted (zone)
+ "Return the ZONE's records, in a pleasant sorted order."
+ (sort (copy-seq (zone-records zone))
+ (lambda (zr-a zr-b)
+ (multiple-value-bind (precp follp)
+ (domain-name< (zr-name zr-a) (zr-name zr-b))
+ (cond (precp t)
+ (follp nil)
+ (t (string< (zr-type zr-a) (zr-type zr-b))))))))
+
;;;--------------------------------------------------------------------------
;;; 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."
- (let* ((file (format nil "~(~A~).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)
(safely-writing (out file)
(format out
";; Serial number file for zone ~A~%~
- ;; (LAST-SEQ DAY MONTH YEAR)~%~
- ~S~%"
+ ;; (LAST-SEQ DAY MONTH YEAR)~%~
+ ~S~%"
name
(cons seq now)))
(from-mixed-base '(100 100 100) (reverse (cons seq now)))))
;;;--------------------------------------------------------------------------
-;;; Zone variables and structures.
+;;; Zone form parsing.
-(defvar *zones* (make-hash-table :test #'equal)
- "Map of known zones.")
+(defun zone-process-records (rec ttl func)
+ "Sort out the list of records in REC, calling FUNC for each one.
-(defun zone-find (name)
- "Find a zone given its NAME."
- (gethash (string-downcase (stringify name)) *zones*))
+ TTL is the default time-to-live for records which don't specify one.
-(defun (setf zone-find) (zone name)
- "Make the zone NAME map to ZONE."
- (setf (gethash (string-downcase (stringify name)) *zones*) zone))
+ REC is a list of records of the form
-(defstruct (zone-record (:conc-name zr-))
- "A zone record."
- (name '<unnamed>)
- ttl
- type
- (defsubp nil)
- data)
+ ({ :ttl TTL | TYPE DATA | (LABEL . REC) }*)
-(defstruct (zone-subdomain (:conc-name zs-))
- "A subdomain. Slightly weird. Used internally by zone-process-records
-below, and shouldn't escape."
- name
- ttl
- records)
+ The various kinds of entries have the following meanings.
-;;;--------------------------------------------------------------------------
-;;; Zone infrastructure.
+ :ttl TTL Set the TTL for subsequent records (at this level of
+ nesting only).
+
+ TYPE DATA Define a record with a particular TYPE and DATA.
+ Record types are defined using `defzoneparse' and
+ the syntax of the data is idiosyncratic.
+
+ ((LABEL ...) . REC) Define records for labels within the zone. Any
+ records defined within REC will have their domains
+ prefixed by each of the LABELs. A singleton list
+ of labels may instead be written as a single
+ label. Note, therefore, that
+
+ (host (sub :a \"169.254.1.1\"))
+
+ defines a record for `host.sub' -- not `sub.host'.
+
+ If REC contains no top-level records, but it does define records for a
+ label listed in `*preferred-subnets*', then the records for the first such
+ label are also promoted to top-level.
+
+ The FUNC is called for each record encountered, represented as a
+ `zone-record' object. Zone parsers are not called: you get the record
+ types and data from the input form; see `zone-parse-records' if you want
+ the raw output."
-(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."
(labels ((sift (rec ttl)
+ ;; Parse the record list REC into lists of `zone-record' and
+ ;; `zone-subdomain' objects, sorting out TTLs and so on.
+ ;; Returns them as two values.
+
(collecting (top sub)
(loop
(unless rec
sub)))
(t
(error "Unexpected record form ~A" (car r))))))))
- (process (rec dom ttl defsubp)
+
+ (process (rec dom ttl)
+ ;; Recursirvely process the record list REC, with a list DOM of
+ ;; prefix labels, and a default TTL. Promote records for a
+ ;; preferred subnet to toplevel if there are no toplevel records
+ ;; already.
+
(multiple-value-bind (top sub) (sift rec ttl)
(if (and dom (null top) sub)
- (let ((s (pop sub)))
- (process (zs-records s)
- dom
- (zs-ttl s)
- defsubp)
- (process (zs-records s)
- (cons (zs-name s) dom)
- (zs-ttl s)
- t))
- (let ((name (and dom
- (string-downcase
- (join-strings #\. (reverse dom))))))
- (dolist (zr top)
- (setf (zr-name zr) name)
- (setf (zr-defsubp zr) defsubp)
- (funcall func zr))))
+ (let ((preferred
+ (or (find-if (lambda (s)
+ (some #'zone-preferred-subnet-p
+ (listify (zs-name s))))
+ sub)
+ (car sub))))
+ (when preferred
+ (process (zs-records preferred)
+ dom
+ (zs-ttl preferred))))
+ (let ((name (and dom
+ (string-downcase
+ (join-strings #\. (reverse dom))))))
+ (dolist (zr top)
+ (setf (zr-name zr) name)
+ (funcall func zr))))
(dolist (s sub)
(process (zs-records s)
(cons (zs-name s) dom)
- (zs-ttl s)
- defsubp)))))
- (process rec nil ttl nil)))
+ (zs-ttl s))))))
-(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."
- (setf f (stringify f))
- (cond ((string= f "@") (stringify zname))
- ((and (plusp (length f))
- (char= (char f (1- (length f))) #\.))
- (string-downcase (subseq f 0 (1- (length f)))))
- (t (string-downcase (concatenate 'string f "."
- (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."
- (join-strings #\. (collecting ()
- (loop for i from (- 3 bytes) downto 0
- do (collect (ipaddr-byte base i)))
- (collect "in-addr.arpa"))))
-
-(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."
- (let ((ipn (net-get-as-ipnet net)))
- (with-ipnet (net mask) ipn
- (unless bytes
- (setf bytes (- 4 (ipnet-changeable-bytes mask))))
- (join-strings #\.
- (append (loop
- for i from (- 4 bytes) below 4
- collect (logand #xff (ash net (* -8 i))))
- (list "in-addr.arpa"))))))
-
-(defun zone-net-from-name (name)
- "Given a NAME in the in-addr.arpa space, convert it to an ipnet."
- (let* ((name (string-downcase (stringify name)))
- (len (length name))
- (suffix ".in-addr.arpa")
- (sufflen (length suffix))
- (addr 0)
- (n 0)
- (end (- len sufflen)))
- (unless (and (> len sufflen)
- (string= name suffix :start1 end))
- (error "`~A' not in ~A." name suffix))
- (loop
- with start = 0
- for dot = (position #\. name :start start :end end)
- for byte = (parse-integer name
- :start start
- :end (or dot end))
- do (setf addr (logior addr (ash byte (* 8 n))))
- (incf n)
- when (>= n 4)
- do (error "Can't deduce network from ~A." name)
- while dot
- do (setf start (1+ dot)))
- (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."
- (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."
- (with-ipnet (net mask) ipn
- (join-strings #\.
- (reverse
- (loop
- for i from (1- bytes) downto 0
- 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)))))))
-
-;;;--------------------------------------------------------------------------
-;;; Zone form parsing.
+ ;; Process the records we're given with no prefix.
+ (process rec nil ttl)))
(defun zone-parse-head (head)
- "Parse the HEAD of a zone form. This has the form
+ "Parse the HEAD of a zone form.
+
+ This has the form
(NAME &key :source :admin :refresh :retry
- :expire :min-ttl :ttl :serial)
+ :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
(ttl min-ttl)
(serial (make-zone-serial zname)))
(listify head)
- (values zname
+ (values (string-downcase zname)
(timespec-seconds ttl)
(make-soa :admin admin
:source (zone-parse-host source zname)
:min-ttl (timespec-seconds min-ttl)
:serial serial))))
+(export 'defzoneparse)
(defmacro defzoneparse (types (name data list
- &key (zname (gensym "ZNAME"))
- (ttl (gensym "TTL"))
- (defsubp (gensym "DEFSUBP")))
+ &key (prefix (gensym "PREFIX"))
+ (zname (gensym "ZNAME"))
+ (ttl (gensym "TTL")))
&body body)
- "Define a new zone record type (or TYPES -- a list of synonyms is
-permitted). The arguments are as follows:
+ "Define a new zone record type.
+
+ The arguments are as follows:
-NAME The name of the record to be added.
+ TYPES A singleton type symbol, or a list of aliases.
-DATA The content of the record to be added (a single object, unevaluated).
+ NAME The name of the record to be added.
-LIST A function to add a record to the zone. See below.
+ DATA The content of the record to be added (a single object,
+ unevaluated).
-ZNAME The name of the zone being constructed.
+ LIST A function to add a record to the zone. See below.
-TTL The TTL for this record.
+ PREFIX The prefix tag used in the original form.
-DEFSUBP Whether this is the default subdomain for this entry.
+ ZNAME The name of the zone being constructed.
-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.
+ TTL The TTL for this record.
-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
+ You get to choose your own names for these. ZNAME, PREFIX and TTL are
+ optional: you don't have to accept them if you're not interested.
- (LIST &key :name :type :data :ttl :defsubp)
+ 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
-Except for defsubp, these default to the above arguments (even if you didn't
-accept the arguments)."
+ (LIST &key :name :type :data :ttl :make-ptr-p)
+
+ These (except MAKE-PTR-P, which defaults to nil) 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))))
- (with-gensyms (col tname ttype tttl tdata tdefsubp i)
- `(progn
- (dolist (,i ',types)
- (setf (get ,i 'zone-parse) ',func))
- (defun ,func (,name ,data ,ttl ,col ,zname ,defsubp)
- (declare (ignorable ,zname ,defsubp))
- (flet ((,list (&key ((:name ,tname) ,name)
- ((:type ,ttype) ,type)
- ((:data ,tdata) ,data)
- ((:ttl ,tttl) ,ttl)
- ((:defsubp ,tdefsubp) nil))
- (collect (make-zone-record :name ,tname
- :type ,ttype
- :data ,tdata
- :ttl ,tttl
- :defsubp ,tdefsubp)
- ,col)))
- ,@body))
- ',type))))
-
-(defun zone-parse-records (zone records)
- (let ((zname (zone-name zone)))
- (with-collection (rec)
- (flet ((parse-record (zr)
- (let ((func (or (get (zr-type zr) 'zone-parse)
- (error "No parser for record ~A."
- (zr-type zr))))
- (name (and (zr-name zr)
- (stringify (zr-name zr)))))
- (if (or (not name)
- (string= name "@"))
- (setf name zname)
- (let ((len (length name)))
- (if (or (zerop len)
- (char/= (char name (1- len)) #\.))
- (setf name (join-strings #\.
- (list name zname))))))
- (funcall func
- name
- (zr-data zr)
- (zr-ttl zr)
- rec
- zname
- (zr-defsubp zr)))))
- (zone-process-records records
- (zone-default-ttl zone)
- #'parse-record ))
- (setf (zone-records zone) (nconc (zone-records zone) rec)))))
-
+ (with-parsed-body (body decls doc) body
+ (with-gensyms (col tname ttype tttl tdata tmakeptrp i)
+ `(progn
+ (dolist (,i ',types)
+ (setf (get ,i 'zone-parse) ',func))
+ (defun ,func (,prefix ,zname ,data ,ttl ,col)
+ ,@doc
+ ,@decls
+ (let ((,name (zone-make-name ,prefix ,zname)))
+ (flet ((,list (&key ((:name ,tname) ,name)
+ ((:type ,ttype) ,type)
+ ((: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
+ :ttl ,tttl
+ :make-ptr-p ,tmakeptrp)
+ ,col)))
+ ,@body)))
+ ',type)))))
+
+(export 'zone-parse-records)
+(defun zone-parse-records (zname ttl records)
+ "Parse a sequence of RECORDS and return a list of raw records.
+
+ The records are parsed relative to the zone name ZNAME, and using the
+ given default TTL."
+ (collecting (rec)
+ (flet ((parse-record (zr)
+ (let ((func (or (get (zr-type zr) 'zone-parse)
+ (error "No parser for record ~A."
+ (zr-type zr))))
+ (name (and (zr-name zr) (stringify (zr-name zr)))))
+ (funcall func name zname (zr-data zr) (zr-ttl zr) rec))))
+ (zone-process-records records ttl #'parse-record))))
+
+(export 'zone-parse)
(defun zone-parse (zf)
- "Parse a ZONE form. The syntax of a zone form is as follows:
+ "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
- :soa soa
- :records nil)))
- (zone-parse-records zone (cdr zf))
- zone)))
+ (make-zone :name zname
+ :default-ttl ttl
+ :soa soa
+ :records (zone-parse-records zname ttl (cdr zf)))))
+(export 'zone-create)
(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)
name))
-(defmacro defzone (soa &rest zf)
+(export 'defzone)
+(defmacro defzone (soa &body zf)
"Zone definition macro."
`(zone-create '(,soa ,@zf)))
-(defmacro defrevzone (head &rest zf)
+(export '*address-family*)
+(defvar *address-family* t
+ "The default address family. This is bound by `defrevzone'.")
+
+(export 'defrevzone)
+(defmacro defrevzone (head &body zf)
"Define a reverse zone, with the correct name."
- (destructuring-bind
- (net &rest soa-args)
+ (destructuring-bind (nets &rest args
+ &key &allow-other-keys
+ (family '*address-family*)
+ prefix-bits)
(listify head)
- (let ((bytes nil))
- (when (and soa-args (integerp (car soa-args)))
- (setf bytes (pop soa-args)))
- `(zone-create '((,(zone-name-from-net net bytes) ,@soa-args) ,@zf)))))
+ (with-gensyms (ipn)
+ `(dolist (,ipn (net-parse-to-ipnets ',nets ,family))
+ (let ((*address-family* (ipnet-family ,ipn)))
+ (zone-create `((,(reverse-domain ,ipn ,prefix-bits)
+ ,@',(loop for (k v) on args by #'cddr
+ unless (member k
+ '(:family :prefix-bits))
+ nconc (list k v)))
+ ,@',zf)))))))
+
+(export 'map-host-addresses)
+(defun map-host-addresses (func addr &key (family *address-family*))
+ "Call FUNC for each address denoted by ADDR (a `host-parse' address)."
+
+ (dolist (a (host-addrs (host-parse addr family)))
+ (funcall func a)))
+
+(export 'do-host)
+(defmacro do-host ((addr spec &key (family *address-family*)) &body body)
+ "Evaluate BODY, binding ADDR to each address denoted by SPEC."
+ `(dolist (,addr (host-addrs (host-parse ,spec ,family)))
+ ,@body))
+
+(export 'zone-set-address)
+(defun zone-set-address (rec addrspec &rest args
+ &key (family *address-family*) name ttl make-ptr-p)
+ "Write records (using REC) defining addresses for ADDRSPEC."
+ (declare (ignore name ttl make-ptr-p))
+ (let ((key-args (loop for (k v) on args by #'cddr
+ unless (eq k :family)
+ nconc (list k v))))
+ (do-host (addr addrspec :family family)
+ (apply rec :type (ipaddr-rrtype addr) :data addr key-args))))
+
+;;;--------------------------------------------------------------------------
+;;; Building raw record vectors.
+
+(defvar *record-vector* nil
+ "The record vector under construction.")
+
+(defun rec-ensure (n)
+ "Ensure that at least N octets are spare in the current record."
+ (let ((want (+ n (fill-pointer *record-vector*)))
+ (have (array-dimension *record-vector* 0)))
+ (unless (<= want have)
+ (adjust-array *record-vector*
+ (do ((new (* 2 have) (* 2 new)))
+ ((<= want new) new))))))
+
+(export 'rec-byte)
+(defun rec-byte (octets value)
+ "Append an unsigned byte, OCTETS octets wide, with VALUE, to the record."
+ (rec-ensure octets)
+ (do ((i (1- octets) (1- i)))
+ ((minusp i))
+ (vector-push (ldb (byte 8 (* 8 i)) value) *record-vector*)))
+
+(export 'rec-u8)
+(defun rec-u8 (value)
+ "Append an 8-bit VALUE to the current record."
+ (rec-byte 1 value))
+
+(export 'rec-u16)
+(defun rec-u16 (value)
+ "Append a 16-bit VALUE to the current record."
+ (rec-byte 2 value))
+
+(export 'rec-u32)
+(defun rec-u32 (value)
+ "Append a 32-bit VALUE to the current record."
+ (rec-byte 4 value))
+
+(export 'rec-raw-string)
+(defun rec-raw-string (s &key (start 0) end)
+ "Append (a (substring of) a raw string S to the current record.
+
+ No arrangement is made for reporting the length of the string. That must
+ be done by the caller, if necessary."
+ (setf-default end (length s))
+ (rec-ensure (- end start))
+ (do ((i start (1+ i)))
+ ((>= i end))
+ (vector-push (char-code (char s i)) *record-vector*)))
+
+(export 'rec-string)
+(defun rec-string (s &key (start 0) end (max 255))
+ (let* ((end (or end (length s)))
+ (len (- end start)))
+ (unless (<= len max)
+ (error "String `~A' too long" (subseq s start end)))
+ (rec-u8 (- end start))
+ (rec-raw-string s :start start :end end)))
+
+(export 'rec-name)
+(defun rec-name (s)
+ "Append a domain name S.
+
+ No attempt is made to perform compression of the name."
+ (let ((i 0) (n (length s)))
+ (loop (let* ((dot (position #\. s :start i))
+ (lim (or dot n)))
+ (rec-string s :start i :end lim :max 63)
+ (if dot
+ (setf i (1+ dot))
+ (return))))
+ (when (< i n)
+ (rec-u8 0))))
+
+(export 'build-record)
+(defmacro build-record (&body body)
+ "Build a raw record, and return it as a vector of octets."
+ `(let ((*record-vector* (make-array 256
+ :element-type '(unsigned-byte 8)
+ :fill-pointer 0
+ :adjustable t)))
+ ,@body
+ (copy-seq *record-vector*)))
+
+(export 'zone-record-rrdata)
+(defgeneric zone-record-rrdata (type zr)
+ (:documentation "Emit (using the `build-record' protocol) RRDATA for ZR.
+
+ The TYPE is a keyword naming the record type. Return the numeric RRTYPE
+ code."))
;;;--------------------------------------------------------------------------
;;; Zone record parsers.
-(defzoneparse :a (name data rec :defsubp defsubp)
+(defzoneparse :a (name data rec)
":a IPADDR"
- (rec :data (parse-ipaddr data) :defsubp defsubp))
+ (zone-set-address #'rec data :make-ptr-p t :family :ipv4))
+
+(defmethod zone-record-rrdata ((type (eql :a)) zr)
+ (rec-u32 (ipaddr-addr (zr-data zr)))
+ 1)
+
+(defzoneparse :aaaa (name data rec)
+ ":aaaa IPADDR"
+ (zone-set-address #'rec data :make-ptr-p t :family :ipv6))
+
+(defmethod zone-record-rrdata ((type (eql :aaaa)) zr)
+ (rec-byte 16 (ipaddr-addr (zr-data zr)))
+ 28)
+
+(defzoneparse :addr (name data rec)
+ ":addr IPADDR"
+ (zone-set-address #'rec data :make-ptr-p t))
+
+(defzoneparse :svc (name data rec)
+ ":svc IPADDR"
+ (zone-set-address #'rec data))
(defzoneparse :ptr (name data rec :zname zname)
":ptr HOST"
(rec :data (zone-parse-host data zname)))
+(defmethod zone-record-rrdata ((type (eql :ptr)) zr)
+ (rec-name (zr-data zr))
+ 12)
+
(defzoneparse :cname (name data rec :zname zname)
":cname HOST"
(rec :data (zone-parse-host data zname)))
+(defmethod zone-record-rrdata ((type (eql :cname)) zr)
+ (rec-name (zr-data zr))
+ 5)
+
+(defzoneparse :txt (name data rec)
+ ":txt (TEXT*)"
+ (rec :data (listify data)))
+
+(defmethod zone-record-rrdata ((type (eql :txt)) zr)
+ (mapc #'rec-string (zr-data zr))
+ 16)
+
+(export '*dkim-pathname-defaults*)
+(defvar *dkim-pathname-defaults*
+ (make-pathname :directory '(:relative "keys")
+ :type "dkim"))
+
+(defzoneparse :dkim (name data rec)
+ ":dkim (KEYFILE {:TAG VALUE}*)"
+ (destructuring-bind (file &rest plist) (listify data)
+ (let ((things nil) (out nil))
+ (labels ((flush ()
+ (when out
+ (push (get-output-stream-string out) things)
+ (setf out nil)))
+ (emit (text)
+ (let ((len (length text)))
+ (when (and out (> (+ (file-position out)
+ (length text))
+ 64))
+ (flush))
+ (when (plusp len)
+ (cond ((< len 64)
+ (unless out (setf out (make-string-output-stream)))
+ (write-string text out))
+ (t
+ (do ((i 0 j)
+ (j 64 (+ j 64)))
+ ((>= i len))
+ (push (subseq text i (min j len)) things))))))))
+ (do ((p plist (cddr p)))
+ ((endp p))
+ (emit (format nil "~(~A~)=~A;" (car p) (cadr p))))
+ (emit (with-output-to-string (out)
+ (write-string "p=" out)
+ (when file
+ (with-open-file
+ (in (merge-pathnames file *dkim-pathname-defaults*))
+ (loop
+ (when (string= (read-line in)
+ "-----BEGIN PUBLIC KEY-----")
+ (return)))
+ (loop
+ (let ((line (read-line in)))
+ (if (string= line "-----END PUBLIC KEY-----")
+ (return)
+ (write-string line out)))))))))
+ (rec :type :txt
+ :data (nreverse things)))))
+
+(eval-when (:load-toplevel :execute)
+ (dolist (item '((sshfp-algorithm rsa 1)
+ (sshfp-algorithm dsa 2)
+ (sshfp-algorithm ecdsa 3)
+ (sshfp-type sha-1 1)
+ (sshfp-type sha-256 2)))
+ (destructuring-bind (prop sym val) item
+ (setf (get sym prop) val)
+ (export sym))))
+
+(export '*sshfp-pathname-defaults*)
+(defvar *sshfp-pathname-defaults*
+ (make-pathname :directory '(:relative "keys")
+ :type "sshfp"))
+
+(defzoneparse :sshfp (name data rec)
+ ":sshfp { FILENAME | ((FPR :alg ALG :type HASH)*) }"
+ (if (stringp data)
+ (with-open-file (in (merge-pathnames data *sshfp-pathname-defaults*))
+ (loop (let ((line (read-line in nil)))
+ (unless line (return))
+ (let ((words (str-split-words line)))
+ (pop words)
+ (when (string= (car words) "IN") (pop words))
+ (unless (and (string= (car words) "SSHFP")
+ (= (length words) 4))
+ (error "Invalid SSHFP record."))
+ (pop words)
+ (destructuring-bind (alg type fpr) words
+ (rec :data (list (parse-integer alg)
+ (parse-integer type)
+ fpr)))))))
+ (flet ((lookup (what prop)
+ (etypecase what
+ (fixnum what)
+ (symbol (or (get what prop)
+ (error "~S is not a known ~A" what prop))))))
+ (dolist (item (listify data))
+ (destructuring-bind (fpr &key (alg 'rsa) (type 'sha-1))
+ (listify item)
+ (rec :data (list (lookup alg 'sshfp-algorithm)
+ (lookup type 'sshfp-type)
+ fpr)))))))
+
+(defmethod zone-record-rrdata ((type (eql :sshfp)) zr)
+ (destructuring-bind (alg type fpr) (zr-data zr)
+ (rec-u8 alg)
+ (rec-u8 type)
+ (do ((i 0 (+ i 2))
+ (n (length fpr)))
+ ((>= i n))
+ (rec-u8 (parse-integer fpr :start i :end (+ i 2) :radix 16))))
+ 44)
+
(defzoneparse :mx (name data rec :zname zname)
":mx ((HOST :prio INT :ip IPADDR)*)"
(dolist (mx (listify data))
(mxname &key (prio *default-mx-priority*) ip)
(listify mx)
(let ((host (zone-parse-host mxname zname)))
- (when ip (rec :name host :type :a :data (parse-ipaddr ip)))
+ (when ip (zone-set-address #'rec ip :name host))
(rec :data (cons host prio))))))
+(defmethod zone-record-rrdata ((type (eql :mx)) zr)
+ (let ((name (car (zr-data zr)))
+ (prio (cdr (zr-data zr))))
+ (rec-u16 prio)
+ (rec-name name))
+ 15)
+
(defzoneparse :ns (name data rec :zname zname)
":ns ((HOST :ip IPADDR)*)"
(dolist (ns (listify data))
(nsname &key ip)
(listify ns)
(let ((host (zone-parse-host nsname zname)))
- (when ip (rec :name host :type :a :data (parse-ipaddr ip)))
+ (when ip (zone-set-address #'rec ip :name host))
(rec :data host)))))
+(defmethod zone-record-rrdata ((type (eql :ns)) zr)
+ (rec-name (zr-data zr))
+ 2)
+
(defzoneparse :alias (name data rec :zname zname)
":alias (LABEL*)"
(dolist (a (listify data))
: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 (zone-set-address #'rec ip :name host))
+ (rec :name rname
+ :data (list prio weight port host))))))))))
+
+(defmethod zone-record-rrdata ((type (eql :srv)) zr)
+ (destructuring-bind (prio weight port host) (zr-data zr)
+ (rec-u16 prio)
+ (rec-u16 weight)
+ (rec-u16 port)
+ (rec-name host))
+ 33)
+
(defzoneparse :net (name data rec)
":net (NETWORK*)"
(dolist (net (listify data))
- (let ((n (net-get-as-ipnet net)))
- (rec :name (zone-parse-host "net" name)
- :type :a
- :data (ipnet-net n))
- (rec :name (zone-parse-host "mask" name)
- :type :a
- :data (ipnet-mask n))
- (rec :name (zone-parse-host "broadcast" name)
- :type :a
- :data (ipnet-broadcast n)))))
-
+ (dolist (ipn (net-ipnets (net-must-find net)))
+ (let* ((base (ipnet-net ipn))
+ (rrtype (ipaddr-rrtype base)))
+ (flet ((frob (kind addr)
+ (when addr
+ (rec :name (zone-parse-host kind name)
+ :type rrtype
+ :data addr))))
+ (frob "net" base)
+ (frob "mask" (ipaddr (ipnet-mask ipn) (ipnet-family ipn)))
+ (frob "bcast" (ipnet-broadcast ipn)))))))
+
(defzoneparse (:rev :reverse) (name data rec)
- ":reverse ((NET :bytes BYTES) ZONE*)"
+ ":reverse ((NET &key :prefix-bits :family) ZONE*)
+
+ Add a reverse record each host in the ZONEs (or all zones) that lies
+ within NET."
(setf data (listify data))
- (destructuring-bind
- (net &key bytes)
+ (destructuring-bind (net &key prefix-bits (family *address-family*))
(listify (car data))
- (setf net (zone-parse-net net name))
- (unless bytes
- (setf bytes (ipnet-changeable-bytes (ipnet-mask net))))
- (dolist (z (or (cdr data)
- (hash-table-keys *zones*)))
- (dolist (zr (zone-records (zone-find z)))
- (when (and (eq (zr-type zr) :a)
- (not (zr-defsubp zr))
- (ipaddr-networkp (zr-data zr) net))
- (rec :name (string-downcase
- (join-strings
- #\.
- (collecting ()
- (dotimes (i bytes)
- (collect (logand #xff (ash (zr-data zr)
- (* -8 i)))))
- (collect name))))
- :type :ptr
- :ttl (zr-ttl zr)
- :data (zr-name zr)))))))
-
-(defzoneparse (:cidr-delegation :cidr) (name data rec)
- ":cidr-delegation ((NET :bytes BYTES) (TARGET-NET [TARGET-ZONE])*)"
- (destructuring-bind
- (net &key bytes)
+
+ (dolist (ipn (net-parse-to-ipnets net family))
+ (let* ((seen (make-hash-table :test #'equal))
+ (width (ipnet-width ipn))
+ (frag-len (if prefix-bits (- width prefix-bits)
+ (ipnet-changeable-bits width (ipnet-mask ipn)))))
+ (dolist (z (or (cdr data) (hash-table-keys *zones*)))
+ (dolist (zr (zone-records (zone-find z)))
+ (when (and (eq (zr-type zr) (ipaddr-rrtype (ipnet-net ipn)))
+ (zr-make-ptr-p zr)
+ (ipaddr-networkp (ipaddr-addr (zr-data zr)) ipn))
+ (let* ((frag (reverse-domain-fragment (zr-data zr)
+ 0 frag-len))
+ (name (concatenate 'string frag "." name)))
+ (unless (gethash name seen)
+ (rec :name name :type :ptr
+ :ttl (zr-ttl zr) :data (zr-name zr))
+ (setf (gethash name seen) t))))))))))
+
+(defzoneparse :multi (name data rec :zname zname :ttl ttl)
+ ":multi (((NET*) &key :start :end :family :suffix) . REC)
+
+ Output multiple records covering a portion of the reverse-resolution
+ namespace corresponding to the particular NETs. The START and END bounds
+ default to the most significant variable component of the
+ reverse-resolution domain.
+
+ The REC tail is a sequence of record forms (as handled by
+ `zone-process-records') to be emitted for each covered address. Within
+ the bodies of these forms, the symbol `*' will be replaced by the
+ domain-name fragment corresponding to the current host, optionally
+ followed by the SUFFIX.
+
+ Examples:
+
+ (:multi ((delegated-subnet :start 8)
+ :ns (some.ns.delegated.example :ip \"169.254.5.2\")))
+
+ (:multi ((tiny-subnet :suffix \"128.10.254.169.in-addr.arpa\")
+ :cname *))
+
+ Obviously, nested `:multi' records won't work well."
+
+ (destructuring-bind (nets &key start end (family *address-family*) suffix)
(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
- (with-ipnet (net mask) tnet
- (setf tdom
- (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)))))
- (list 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)))))))
- (rec :name (format nil "~A.~A" tail name)
- :type :cname
- :data (format nil "~A.~A" tail tdom))))))))
+ (dolist (net (listify nets))
+ (dolist (ipn (net-parse-to-ipnets net family))
+ (let* ((addr (ipnet-net ipn))
+ (width (ipaddr-width addr))
+ (comp-width (reverse-domain-component-width addr))
+ (end (round-up (or end
+ (ipnet-changeable-bits width
+ (ipnet-mask ipn)))
+ comp-width))
+ (start (round-down (or start (- end comp-width))
+ comp-width))
+ (map (ipnet-host-map ipn)))
+ (multiple-value-bind (host-step host-limit)
+ (ipnet-index-bounds map start end)
+ (do ((index 0 (+ index host-step)))
+ ((> index host-limit))
+ (let* ((addr (ipnet-index-host map index))
+ (frag (reverse-domain-fragment addr start end))
+ (target (concatenate 'string
+ (zone-make-name
+ (if (not suffix) frag
+ (concatenate 'string
+ frag "." suffix))
+ zname)
+ ".")))
+ (dolist (zr (zone-parse-records (zone-make-name frag zname)
+ ttl
+ (subst target '*
+ (cdr data))))
+ (rec :name (zr-name zr)
+ :type (zr-type zr)
+ :data (zr-data zr)
+ :ttl (zr-ttl zr)
+ :make-ptr-p (zr-make-ptr-p zr)))))))))))
;;;--------------------------------------------------------------------------
;;; 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.")
+
+(export 'zone-write-raw-rrdata)
+(defgeneric zone-write-raw-rrdata (format zr type data)
+ (:documentation "Write an otherwise unsupported record in a given FORMAT.
+
+ ZR gives the record object, which carries the name and TTL; the TYPE is
+ the numeric RRTYPE code; and DATA is an octet vector giving the RRDATA.
+ This is used by the default `zone-write-record' method to handle record
+ types which aren't directly supported by the format driver."))
+
+(export 'zone-write-header)
+(defgeneric zone-write-header (format zone)
+ (:documentation "Emit the header for a ZONE, in a given FORMAT.
+
+ The header includes any kind of initial comment, the SOA record, and any
+ other necessary preamble. There is no default implementation.
+
+ This is part of the protocol used by the default method on `zone-write';
+ if you override that method."))
+
+(export 'zone-write-trailer)
+(defgeneric zone-write-trailer (format zone)
+ (:documentation "Emit the header for a ZONE, in a given FORMAT.
+
+ The footer may be empty, and is so by default.
+
+ This is part of the protocol used by the default method on `zone-write';
+ if you override that method.")
+ (:method (format zone)
+ (declare (ignore format zone))
+ nil))
+
+(export 'zone-write-record)
+(defgeneric zone-write-record (format type zr)
+ (:documentation "Emit a record of the given TYPE (a keyword).
+
+ The default implementation builds the raw RRDATA and passes it to
+ `zone-write-raw-rrdata'.")
+ (:method (format type zr)
+ (let* (code
+ (data (build-record (setf code (zone-record-rrdata type zr)))))
+ (zone-write-raw-rrdata format zr code data))))
+
+(defmethod zone-write (format zone stream)
+ "This default method calls `zone-write-header', then `zone-write-record'
+ for each record in the zone, and finally `zone-write-trailer'. While it's
+ running, `*writing-zone*' is bound to the zone object, and
+ `*zone-output-stream*' to the output stream."
+ (let ((*writing-zone* zone)
+ (*zone-output-stream* stream))
+ (zone-write-header format zone)
+ (dolist (zr (zone-records-sorted zone))
+ (zone-write-record format (zr-type zr) zr))
+ (zone-write-trailer format zone)))
+
+(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.
+
+(defvar *bind-last-record-name* nil
+ "The previously emitted record name.
+
+ Used for eliding record names on output.")
+
+(export 'bind-hostname)
+(defun bind-hostname (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 ".")))))
+
+(export 'bind-output-hostname)
+(defun bind-output-hostname (hostname)
+ (let ((name (bind-hostname hostname)))
+ (cond ((and *bind-last-record-name*
+ (string= name *bind-last-record-name*))
+ "")
+ (t
+ (setf *bind-last-record-name* name)
+ name))))
+
+(defmethod zone-write :around ((format (eql :bind)) zone stream)
+ (let ((*bind-last-record-name* nil))
+ (call-next-method)))
+
+(defmethod zone-write-header ((format (eql :bind)) zone)
+ (format *zone-output-stream* "~
;;; 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))
- (let ((soa (zone-soa zone)))
- (format stream "~
-~A~30TIN SOA~40T~A ~A (
+ (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 *zone-output-stream* "~
+~A~30TIN SOA~40T~A (
+~55@A~60T ;administrator
~45T~10D~60T ;serial
~45T~10D~60T ;refresh
~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-output-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))
- (case (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
- (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
- (format nil
- "~(~A~).zone"
- z))))
- (zone-write zz stream))))))
+ (soa-min-ttl soa))))
+
+(export 'bind-format-record)
+(defun bind-format-record (zr format &rest args)
+ (format *zone-output-stream*
+ "~A~20T~@[~8D~]~30TIN ~A~40T~?~%"
+ (bind-output-hostname (zr-name zr))
+ (let ((ttl (zr-ttl zr)))
+ (and (/= ttl (zone-default-ttl *writing-zone*))
+ ttl))
+ (string-upcase (symbol-name (zr-type zr)))
+ format args))
+
+(defmethod zone-write-raw-rrdata ((format (eql :bind)) zr type data)
+ (format *zone-output-stream*
+ "~A~20T~@[~8D~]~30TIN TYPE~A~40T\\# ~A"
+ (bind-output-hostname (zr-name zr))
+ (let ((ttl (zr-ttl zr)))
+ (and (/= ttl (zone-default-ttl *writing-zone*))
+ ttl))
+ type
+ (length data))
+ (let* ((hex (with-output-to-string (out)
+ (dotimes (i (length data))
+ (format out "~(~2,'0X~)" (aref data i)))))
+ (len (length hex)))
+ (cond ((< len 24)
+ (format *zone-output-stream* " ~A~%" hex))
+ (t
+ (format *zone-output-stream* " (")
+ (let ((i 0))
+ (loop
+ (when (>= i len) (return))
+ (let ((j (min (+ i 64) len)))
+ (format *zone-output-stream* "~%~8T~A" (subseq hex i j))
+ (setf i j))))
+ (format *zone-output-stream* " )~%")))))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :a)) zr)
+ (bind-format-record zr "~A" (ipaddr-string (zr-data zr))))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :aaaa)) zr)
+ (bind-format-record zr "~A" (ipaddr-string (zr-data zr))))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :ptr)) zr)
+ (bind-format-record zr "~A" (bind-hostname (zr-data zr))))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :cname)) zr)
+ (bind-format-record zr "~A" (bind-hostname (zr-data zr))))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :ns)) zr)
+ (bind-format-record zr "~A" (bind-hostname (zr-data zr))))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :mx)) zr)
+ (bind-format-record zr "~2D ~A"
+ (cdr (zr-data zr))
+ (bind-hostname (car (zr-data zr)))))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :srv)) zr)
+ (destructuring-bind (prio weight port host) (zr-data zr)
+ (bind-format-record zr "~2D ~5D ~5D ~A"
+ prio weight port (bind-hostname host))))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :sshfp)) zr)
+ (bind-format-record zr "~{~2D ~2D ~A~}" (zr-data zr)))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :txt)) zr)
+ (bind-format-record zr "~{~#[\"\"~;~S~:;(~@{~%~8T~S~} )~]~}" (zr-data zr)))
+
+;;;--------------------------------------------------------------------------
+;;; tinydns-data output format.
+
+(defun tinydns-output (code &rest fields)
+ (format *zone-output-stream* "~C~{~@[~A~]~^:~}~%" code fields))
+
+(defmethod zone-write-raw-rrdata ((format (eql :tinydns)) zr type data)
+ (tinydns-output #\: (zr-name zr) type
+ (with-output-to-string (out)
+ (dotimes (i (length data))
+ (let ((byte (aref data i)))
+ (if (or (<= byte 32)
+ (>= byte 128)
+ (member byte '(#\: #\\) :key #'char-code))
+ (format out "\\~3,'0O" byte)
+ (write-char (code-char byte) out)))))
+ (zr-ttl zr)))
+
+(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :a)) zr)
+ (tinydns-output #\+ (zr-name zr)
+ (ipaddr-string (zr-data zr)) (zr-ttl zr)))
+
+(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :aaaa)) zr)
+ (tinydns-output #\3 (zr-name zr)
+ (format nil "~(~32,'0X~)" (ipaddr-addr (zr-data zr)))
+ (zr-ttl zr)))
+
+(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :ptr)) zr)
+ (tinydns-output #\^ (zr-name zr) (zr-data zr) (zr-ttl zr)))
+
+(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :cname)) zr)
+ (tinydns-output #\C (zr-name zr) (zr-data zr) (zr-ttl zr)))
+
+(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :ns)) zr)
+ (tinydns-output #\& (zr-name zr) nil (zr-data zr) (zr-ttl zr)))
+
+(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :mx)) zr)
+ (let ((name (car (zr-data zr)))
+ (prio (cdr (zr-data zr))))
+ (tinydns-output #\@ (zr-name zr) nil name prio (zr-ttl zr))))
+
+(defmethod zone-write-header ((format (eql :tinydns)) zone)
+ (format *zone-output-stream* "~
+### Zone file `~(~A~)'
+### (generated ~A)
+~%"
+ (zone-name zone)
+ (iso-date :now :datep t :timep t))
+ (let ((soa (zone-soa zone)))
+ (tinydns-output #\Z
+ (zone-name zone)
+ (soa-source soa)
+ (let* ((name (copy-seq (soa-admin soa)))
+ (at (position #\@ name)))
+ (when at (setf (char name at) #\.))
+ name)
+ (soa-serial soa)
+ (soa-refresh soa)
+ (soa-expire soa)
+ (soa-min-ttl soa)
+ (zone-default-ttl zone))))
;;;----- That's all, folks --------------------------------------------------