X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/zone/blobdiff_plain/aac45ff729f5b78ee2e1ed6aac90d3608554842e..476808d8bacf084e6632b3aebbe14c28ec49e09a:/zone.lisp diff --git a/zone.lisp b/zone.lisp index ec081ef..177ded6 100644 --- a/zone.lisp +++ b/zone.lisp @@ -111,6 +111,110 @@ (defun iso-date (&optional time &key datep timep (sep #\ )) (when timep (format s "~2,'0D:~2,'0D:~2,'0D" hr min sec))))) +(deftype octet () '(unsigned-byte 8)) +(deftype octet-vector (&optional n) `(array octet (,n))) + +(defun decode-hex (hex &key (start 0) end) + "Decode a hexadecimal-encoded string, returning a vector of octets." + (let* ((end (or end (length hex))) + (len (- end start)) + (raw (make-array (floor len 2) :element-type 'octet))) + (unless (evenp len) + (error "Invalid hex string `~A' (odd length)" hex)) + (do ((i start (+ i 2))) + ((>= i end) raw) + (let ((high (digit-char-p (char hex i) 16)) + (low (digit-char-p (char hex (1+ i)) 16))) + (unless (and high low) + (error "Invalid hex string `~A' (bad digit)" hex)) + (setf (aref raw (/ (- i start) 2)) (+ (* 16 high) low)))))) + +(defun slurp-file (file &optional (element-type 'character)) + "Read and return the contents of FILE as a vector." + (with-open-file (in file :element-type element-type) + (let ((buf (make-array 1024 :element-type element-type)) + (pos 0)) + (loop + (let ((end (read-sequence buf in :start pos))) + (when (< end (length buf)) + (return (adjust-array buf end))) + (setf pos end + buf (adjust-array buf (* 2 pos)))))))) + +(defmacro defenum (name (&key export) &body values) + "Set up symbol properties for manifest constants. + + The VALUES are a list of (TAG VALUE) pairs. Each TAG is a symbol; we set + the NAME property on TAG to VALUE, and export TAG. There are also handy + hash-tables mapping in the forward and reverse directions, in the name + symbol's `enum-forward' and `enum-reverse' properties." + `(eval-when (:compile-toplevel :load-toplevel :execute) + ,(let*/gensyms (export) + (with-gensyms (forward reverse valtmp) + `(let ((,forward (make-hash-table)) + (,reverse (make-hash-table))) + (when ,export (export ',name)) + ,@(mapcar (lambda (item) + (destructuring-bind (tag value) item + (let ((constant + (intern (concatenate 'string + (symbol-name name) + "/" + (symbol-name tag))))) + `(let ((,valtmp ,value)) + (when ,export + (export ',constant) + (when (eq (symbol-package ',tag) *package*) + (export ',tag))) + (defconstant ,constant ,valtmp) + (setf (get ',tag ',name) ,value + (gethash ',tag ,forward) ,valtmp + (gethash ,valtmp ,reverse) ',tag))))) + values) + (setf (get ',name 'enum-forward) ,forward + (get ',name 'enum-reverse) ,reverse)))))) + +(defun lookup-enum (name tag &key min max) + "Look up a TAG in an enumeration. + + If TAG is a symbol, check its NAME property; if it's a fixnum then take it + as it is. Make sure that it's between MIN and MAX, if they're not nil." + (let ((value (etypecase tag + (fixnum tag) + (symbol (or (get tag name) + (error "~S is not a known ~A" tag name)))))) + (unless (and (or (null min) (<= min value)) + (or (null max) (<= value max))) + (error "Value ~S out of range for ~A" value name)) + value)) + +(defun reverse-enum (name value) + "Reverse-lookup of a VALUE in enumeration NAME. + + If a tag for the VALUE is found, return it and `t'; otherwise return VALUE + unchanged and `nil'." + (multiple-value-bind (tag foundp) (gethash value (get name 'enum-reverse)) + (if foundp + (values tag t) + (values value nil)))) + +(defun mapenum (func name) + "Call FUNC on TAG/VALUE pairs from the enumeration called NAME." + (maphash func (get name 'enum-forward))) + +(defun hash-file (hash file context) + "Hash the FILE using the OpenSSL HASH function, returning an octet string. + + CONTEXT is a temporary-files context." + (let ((temp (temporary-file context "hash"))) + (run-program (list "openssl" "dgst" (concatenate 'string "-" hash)) + :input file :output temp) + (with-open-file (in temp) + (let ((line (read-line in))) + (assert (and (>= (length line) 9) + (string= line "(stdin)= " :end1 9))) + (decode-hex line :start 9))))) + ;;;-------------------------------------------------------------------------- ;;; Zone types. @@ -125,6 +229,10 @@ (defstruct (soa (:predicate soap)) min-ttl serial) +(export 'zone-text-name) +(defun zone-text-name (zone) + (princ-to-string (zone-name zone))) + (export 'mx) (defstruct (mx (:predicate mxp)) "Mail-exchange record information." @@ -259,63 +367,31 @@ (defmacro preferred-subnet-case (&body clauses) 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)))) +(defun zone-parse-host (form &optional tail) + "Parse a host name FORM from a value in a zone form. + + The underlying parsing is done using `parse-domain-name'. Here, we + interpret various kinds of Lisp object specially. In particular: `nil' + refers to the TAIL zone (just like a plain `@'); and a symbol is downcased + before use." + (let ((name (etypecase form + (null (make-domain-name :labels nil :absolutep nil)) + (domain-name form) + (symbol (parse-domain-name (string-downcase form))) + (string (parse-domain-name form))))) + (if (null tail) name + (domain-name-concat name tail)))) (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) - (let* ((name-a (zr-name zr-a)) (pos-a (length name-a)) - (name-b (zr-name zr-b)) (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))) - (cond ((string< name-a name-b - :start1 (1+ dot-a) :end1 pos-a - :start2 (1+ dot-b) :end2 pos-b) - (return t)) - ((string> name-a name-b - :start1 (1+ dot-a) :end1 pos-a - :start2 (1+ dot-b) :end2 pos-b) - (return nil)) - ((= dot-a dot-b -1) - (return (string< (zr-type zr-a) (zr-type zr-b)))) - ((= dot-a -1) - (return t)) - ((= dot-b -1) - (return nil)) - (t - (setf pos-a dot-a) - (setf pos-b dot-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. @@ -408,9 +484,9 @@ (defun zone-process-records (rec ttl func) top)) ((listp r) (dolist (name (listify (car r))) - (collect (make-zone-subdomain :name name - :ttl ttl - :records (cdr r)) + (collect (make-zone-subdomain + :name (zone-parse-host name) + :ttl ttl :records (cdr r)) sub))) (t (error "Unexpected record form ~A" (car r)))))))) @@ -424,24 +500,25 @@ (defun zone-process-records (rec ttl func) (multiple-value-bind (top sub) (sift rec ttl) (if (and dom (null top) sub) (let ((preferred - (or (find-if (lambda (s) - (some #'zone-preferred-subnet-p - (listify (zs-name s)))) - sub) + (or (find-if + (lambda (s) + (let ((ll (domain-name-labels (zs-name s)))) + (and (consp ll) (null (cdr ll)) + (zone-preferred-subnet-p (car ll))))) + sub) (car sub)))) (when preferred (process (zs-records preferred) dom (zs-ttl preferred)))) - (let ((name (and dom - (string-downcase - (join-strings #\. (reverse dom)))))) + (let ((name dom)) (dolist (zr top) (setf (zr-name zr) name) (funcall func zr)))) (dolist (s sub) (process (zs-records s) - (cons (zs-name s) dom) + (if (null dom) (zs-name s) + (domain-name-concat dom (zs-name s))) (zs-ttl s)))))) ;; Process the records we're given with no prefix. @@ -458,19 +535,21 @@ (defun zone-parse-head (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 + (raw-zname &key (source *default-zone-source*) (admin (or *default-zone-admin* - (format nil "hostmaster@~A" zname))) + (format nil "hostmaster@~A" raw-zname))) (refresh *default-zone-refresh*) (retry *default-zone-retry*) (expire *default-zone-expire*) (min-ttl *default-zone-min-ttl*) (ttl min-ttl) - (serial (make-zone-serial zname))) + (serial (make-zone-serial raw-zname)) + &aux + (zname (zone-parse-host raw-zname root-domain))) (listify head) - (values (string-downcase zname) + (values zname (timespec-seconds ttl) (make-soa :admin admin :source (zone-parse-host source zname) @@ -515,6 +594,7 @@ (defmacro defzoneparse (types (name data list 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)))) @@ -526,7 +606,8 @@ (defmacro defzoneparse (types (name data list (defun ,func (,prefix ,zname ,data ,ttl ,col) ,@doc ,@decls - (let ((,name (zone-make-name ,prefix ,zname))) + (let ((,name (if (null ,prefix) ,zname + (domain-name-concat ,prefix ,zname)))) (flet ((,list (&key ((:name ,tname) ,name) ((:type ,ttype) ,type) ((:data ,tdata) ,data) @@ -553,7 +634,7 @@ (defun zone-parse-records (zname ttl records) (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))))) + (name (and (zr-name zr) (zr-name zr)))) (funcall func name zname (zr-data zr) (zr-ttl zr) rec)))) (zone-process-records records ttl #'parse-record)))) @@ -577,10 +658,11 @@ (defun zone-parse (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." + "Zone construction function. + + Given a zone form ZF, construct the zone and add it to the table." (let* ((zone (zone-parse zf)) - (name (zone-name zone))) + (name (zone-text-name zone))) (setf (zone-find name) zone) name)) @@ -604,7 +686,8 @@ (defmacro defrevzone (head &body 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) + (zone-create `((,(format nil "~A." (reverse-domain ,ipn + ,prefix-bits)) ,@',(loop for (k v) on args by #'cddr unless (member k '(:family :prefix-bits)) @@ -635,6 +718,101 @@ (defun zone-set-address (rec addrspec &rest args (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-octet-vector) +(defun rec-octet-vector (vector &key (start 0) end) + "Copy (part of) the VECTOR to the output." + (let* ((end (or end (length vector))) + (len (- end start))) + (rec-ensure len) + (do ((i start (1+ i))) + ((>= i end)) + (vector-push (aref vector i) *record-vector*)))) + +(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 (name) + "Append a domain NAME. + + No attempt is made to perform compression of the name." + (dolist (label (reverse (domain-name-labels name))) + (rec-string label :max 63)) + (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. @@ -642,10 +820,18 @@ (defzoneparse :a (name data rec) ":a IPADDR" (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)) @@ -658,14 +844,26 @@ (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") @@ -687,13 +885,15 @@ (defzoneparse :dkim (name data rec) (flush)) (when (plusp len) (cond ((< len 64) - (unless out (setf out (make-string-output-stream))) + (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)))))))) + (push (subseq text i (min j len)) + things)))))))) (do ((p plist (cddr p))) ((endp p)) (emit (format nil "~(~A~)=~A;" (car p) (cadr p)))) @@ -714,15 +914,8 @@ (defzoneparse :dkim (name data rec) (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)))) +(defenum sshfp-algorithm () (:rsa 1) (:dsa 2) (:ecdsa 3)) +(defenum sshfp-type () (:sha-1 1) (:sha-256 2)) (export '*sshfp-pathname-defaults*) (defvar *sshfp-pathname-defaults* @@ -746,17 +939,22 @@ (defzoneparse :sshfp (name data rec) (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))))))) + (dolist (item (listify data)) + (destructuring-bind (fpr &key (alg 'rsa) (type 'sha-1)) + (listify item) + (rec :data (list (lookup-enum alg 'sshfp-algorithm :min 0 :max 255) + (lookup-enum type 'sshfp-type :min 0 :max 255) + 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)*)" @@ -768,6 +966,13 @@ (defzoneparse :mx (name data rec :zname zname) (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)) @@ -778,6 +983,10 @@ (defzoneparse :ns (name data rec :zname zname) (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)) @@ -786,7 +995,8 @@ (defzoneparse :alias (name data rec :zname zname) :data name))) (defzoneparse :srv (name data rec :zname zname) - ":srv (((SERVICE &key :port) (PROVIDER &key :port :prio :weight :ip)*)*)" + ":srv (((SERVICE &key :port :protocol) + (PROVIDER &key :port :prio :weight :ip)*)*)" (dolist (srv data) (destructuring-bind (servopts &rest providers) srv (destructuring-bind @@ -795,7 +1005,12 @@ (defzoneparse :srv (name data rec :zname zname) (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))) + (let ((rname (flet ((prepend (tag tail) + (domain-name-concat + (make-domain-name + :labels (list (format nil "_~(~A~)" tag))) + tail))) + (prepend service (prepend protocol name))))) (dolist (prov providers) (destructuring-bind (srvname @@ -810,6 +1025,14 @@ (defzoneparse :srv (name data rec :zname zname) (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)) @@ -846,11 +1069,12 @@ (defzoneparse (:rev :reverse) (name data rec) (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) + (name (domain-name-concat frag name)) + (name-string (princ-to-string name))) + (unless (gethash name-string seen) (rec :name name :type :ptr :ttl (zr-ttl zr) :data (zr-name zr)) - (setf (gethash name seen) t)))))))))) + (setf (gethash name-string seen) t)))))))))) (defzoneparse :multi (name data rec :zname zname :ttl ttl) ":multi (((NET*) &key :start :end :family :suffix) . REC) @@ -876,109 +1100,45 @@ (defzoneparse :multi (name data rec :zname zname :ttl ttl) Obviously, nested `:multi' records won't work well." - (destructuring-bind (nets &key start end (family *address-family*) suffix) + (destructuring-bind (nets + &key start end ((:suffix raw-suffix)) + (family *address-family*)) (listify (car data)) - (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))))))))))) - -;;;-------------------------------------------------------------------------- -;;; 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)))))) - -(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*))) - -(defun rec-u8 (value) - "Append an 8-bit VALUE to the current record." - (rec-byte 1 value)) -(defun rec-u16 (value) - "Append a 16-bit VALUE to the current record." - (rec-byte 2 value)) -(defun rec-u32 (value) - "Append a 32-bit VALUE to the current record." - (rec-byte 4 value)) - -(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*))) - -(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-u8 (- lim i)) - (rec-raw-string s :start i :end lim) - (if dot - (setf i (1+ dot)) - (return)))) - (when (< i n) - (rec-u8 0)))) - -(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*))) + (let ((suffix (if (not raw-suffix) + (make-domain-name :labels nil :absolutep nil) + (zone-parse-host raw-suffix)))) + (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 (reduce #'domain-name-concat + (list frag suffix zname) + :from-end t + :initial-value root-domain))) + (dolist (zr (zone-parse-records (domain-name-concat 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. @@ -993,11 +1153,59 @@ (defvar *writing-zone* nil (defvar *zone-output-stream* nil "Stream to write zone data on.") -(defmethod zone-write :around (format zone stream) - (declare (ignore format)) +(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)) - (call-next-method))) + (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)) @@ -1017,26 +1225,44 @@ (defun zone-save (zones &key (format :bind)) ;;;-------------------------------------------------------------------------- ;;; 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) - (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 ".")))))) - -(export 'bind-record) -(defgeneric bind-record (type zr)) - -(defmethod zone-write ((format (eql :bind)) zone stream) - (format stream "~ + (let ((zone (domain-name-labels (zone-name *writing-zone*))) + (name (domain-name-labels hostname))) + (loop + (unless (and zone name (string= (car zone) (car name))) + (return)) + (pop zone) (pop name)) + (flet ((stitch (labels absolutep) + (format nil "~{~A~^.~}~@[.~]" + (reverse (mapcar #'quotify-label labels)) + absolutep))) + (cond (zone (stitch (domain-name-labels hostname) t)) + (name (stitch name nil)) + (t "@"))))) + +(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) + (declare (ignorable 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) @@ -1052,7 +1278,7 @@ (defmethod zone-write ((format (eql :bind)) zone stream) (when at (setf (char copy at) #\.)) copy))) - (format stream "~ + (format *zone-output-stream* "~ ~A~30TIN SOA~40T~A ( ~55@A~60T ;administrator ~45T~10D~60T ;serial @@ -1060,122 +1286,138 @@ (defmethod zone-write ((format (eql :bind)) zone stream) ~45T~10D~60T ;retry ~45T~10D~60T ;expire ~45T~10D )~60T ;min-ttl~2%" - (bind-hostname (zone-name zone)) + (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-sorted zone)) - (bind-record (zr-type zr) zr))) + (soa-min-ttl soa)))) (export 'bind-format-record) -(defun bind-format-record (name ttl type format args) +(defun bind-format-record (zr format &rest 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)) + "~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)) -(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 :aaaa)) 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 :sshfp)) data) - (cons "~2D ~2D ~A" data)) - (:method ((type (eql :txt)) data) - (cons "~#[\"\"~;~S~:;(~@{~%~8T~S~} )~]" - (mapcar #'stringify data)))) - -(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-write-hex) +(defun bind-write-hex (vector remain) + "Output the VECTOR as hex, in Bind format. + + If the length (in bytes) is less than REMAIN then it's placed on the + current line; otherwise the Bind line-continuation syntax is used." + (flet ((output-octet (octet) + (format *zone-output-stream* "~(~2,'0X~)" octet))) + (let ((len (length vector))) + (cond ((< len remain) + (dotimes (i len) (output-octet (aref vector i))) + (terpri *zone-output-stream*)) + (t + (format *zone-output-stream* "(") + (let ((i 0)) + (loop + (when (>= i len) (return)) + (let ((limit (min len (+ i 64)))) + (format *zone-output-stream* "~%~8T") + (loop + (when (>= i limit) (return)) + (output-octet (aref vector i)) + (incf i))))) + (format *zone-output-stream* " )~%")))))) + +(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)) + (bind-write-hex data 12)) + +(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. +(export 'tinydns-output) (defun tinydns-output (code &rest fields) (format *zone-output-stream* "~C~{~@[~A~]~^:~}~%" code fields)) -(defun tinydns-raw-record (type zr data) +(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) + (>= byte 127) (member byte '(#\: #\\) :key #'char-code)) (format out "\\~3,'0O" byte) (write-char (code-char byte) out))))) (zr-ttl zr))) -(defgeneric tinydns-record (type zr) - (:method ((type (eql :a)) zr) - (tinydns-output #\+ (zr-name zr) - (ipaddr-string (zr-data zr)) (zr-ttl zr))) - (:method ((type (eql :aaaa)) zr) - (tinydns-output #\3 (zr-name zr) - (format nil "~(~32,'0X~)" (ipaddr-addr (zr-data zr))) - (zr-ttl zr))) - (:method ((type (eql :ptr)) zr) - (tinydns-output #\^ (zr-name zr) (zr-data zr) (zr-ttl zr))) - (:method ((type (eql :cname)) zr) - (tinydns-output #\C (zr-name zr) (zr-data zr) (zr-ttl zr))) - (:method ((type (eql :ns)) zr) - (tinydns-output #\& (zr-name zr) nil (zr-data zr) (zr-ttl zr))) - (:method ((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)))) - (:method ((type (eql :txt)) zr) - (tinydns-raw-record 16 zr - (build-record - (dolist (s (zr-data zr)) - (rec-u8 (length s)) - (rec-raw-string s))))) - (:method ((type (eql :srv)) zr) - (destructuring-bind (prio weight port host) (zr-data zr) - (tinydns-raw-record 33 zr - (build-record - (rec-u16 prio) - (rec-u16 weight) - (rec-u16 port) - (rec-name host))))) - (:method ((type (eql :sshfp)) zr) - (destructuring-bind (alg type fpr) (zr-data zr) - (tinydns-raw-record 44 zr - (build-record - (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)))))))) - -(defmethod zone-write ((format (eql :tinydns)) zone stream) - (format stream "~ +(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) ~%" @@ -1193,8 +1435,6 @@ (defmethod zone-write ((format (eql :tinydns)) zone stream) (soa-refresh soa) (soa-expire soa) (soa-min-ttl soa) - (zone-default-ttl zone))) - (dolist (zr (zone-records-sorted zone)) - (tinydns-record (zr-type zr) zr))) + (zone-default-ttl zone)))) ;;;----- That's all, folks --------------------------------------------------