;;; -*-lisp-*-
;;;
-;;; $Id$
-;;;
;;; DNS zone generation
;;;
;;; (c) 2005 Straylight/Edgeware
;;; Packaging.
(defpackage #:zone
- (:use #:common-lisp #:mdw.base #:mdw.str #:collect #:safely #:net)
- (:export #:soa #:mx #:zone #:zone-record #:zone-subdomain
- #:*default-zone-source* #:*default-zone-refresh*
- #:*default-zone-retry* #:*default-zone-expire*
- #:*default-zone-min-ttl* #:*default-zone-ttl*
- #:*default-mx-priority* #:*default-zone-admin*
- #:*zone-output-path*
- #:*preferred-subnets* #:zone-preferred-subnet-p
- #:preferred-subnet-case
- #:zone-find #:zone-parse #:zone-write #:zone-create #:defzone
- #:defrevzone #:zone-save #:zone-make-name
- #:defzoneparse #:zone-parse-host
- #:bind-hostname #:bind-record #:bind-format-record
- #:bind-record-type #:bind-record-format-args
- #:timespec-seconds #:make-zone-serial))
+ (:use #:common-lisp
+ #:mdw.base #:mdw.str #:collect #:safely
+ #:net #:services))
(in-package #:zone)
(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
;;;--------------------------------------------------------------------------
;;; 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.
-#+ecl
-(cffi:defcfun gethostname :int
- (name :pointer)
- (len :uint))
-
+(export '*default-zone-source*)
(defvar *default-zone-source*
- (let ((hn #+cmu (unix:unix-gethostname)
- #+clisp (unix:get-host-name)
- #+ecl (cffi:with-foreign-pointer-as-string (buffer 256 len)
- (let ((rc (gethostname buffer len)))
- (unless (zerop rc)
- (error "gethostname(2) failed (rc = ~A)." rc))))))
+ (let ((hn (gethostname)))
(and hn (concatenate 'string (canonify-hostname hn) ".")))
"The default zone source: the current host's name.")
+(export '*default-zone-refresh*)
(defvar *default-zone-refresh* (* 24 60 60)
"Default zone refresh interval: one day.")
+(export '*default-zone-admin*)
(defvar *default-zone-admin* nil
"Default zone administrator's email address.")
+(export '*default-zone-retry*)
(defvar *default-zone-retry* (* 60 60)
"Default znoe retry interval: one hour.")
+(export '*default-zone-expire*)
(defvar *default-zone-expire* (* 14 24 60 60)
"Default zone expiry time: two weeks.")
+(export '*default-zone-min-ttl*)
(defvar *default-zone-min-ttl* (* 4 60 60)
"Default zone minimum TTL/negative TTL: four hours.")
+(export '*default-zone-ttl*)
(defvar *default-zone-ttl* (* 8 60 60)
"Default zone TTL (for records without explicit TTLs): 8 hours.")
+(export '*default-mx-priority*)
(defvar *default-mx-priority* 50
"Default MX priority.")
(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>)
(make-ptr-p nil)
data)
+(export 'zone-subdomain)
(defstruct (zone-subdomain (:conc-name zs-))
"A subdomain. Slightly weird. Used internally by zone-process-records
below, and shouldn't escape."
ttl
records)
+(export '*zone-output-path*)
(defvar *zone-output-path* *default-pathname-defaults*
"Pathname defaults to merge into output files.")
+(export '*preferred-subnets*)
(defvar *preferred-subnets* nil
"Subnets to prefer when selecting defaults.")
:type (string-downcase type))
*zone-output-path*))
+(export 'zone-preferred-subnet-p)
(defun zone-preferred-subnet-p (name)
"Answer whether NAME (a string or symbol) names a preferred subnet."
(member name *preferred-subnets* :test #'string-equal))
+(export 'preferred-subnet-case)
(defmacro preferred-subnet-case (&body clauses)
"CLAUSES have the form (SUBNETS . FORMS) -- evaluate the first FORMS whose
SUBNETS (a list or single symbol, not evaluated) are considered preferred
(zs-ttl s))))))
(process rec nil ttl)))
+(export 'zone-parse-host)
(defun zone-parse-host (f zname)
"Parse a host name F: if F ends in a dot then it's considered absolute;
otherwise it's relative to ZNAME."
;;;--------------------------------------------------------------------------
;;; 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."
:min-ttl (timespec-seconds min-ttl)
:serial serial))))
+(export 'zone-make-name)
(defun zone-make-name (prefix zone-name)
(if (or (not prefix) (string= prefix "@"))
zone-name
(join-strings #\. (list prefix zone-name))
prefix))))
+(export 'defzoneparse)
(defmacro defzoneparse (types (name data list
&key (prefix (gensym "PREFIX"))
(zname (gensym "ZNAME"))
((: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
#'parse-record))
(setf (zone-records zone) (nconc (zone-records zone) rec)))))
+(export 'zone-parse)
(defun zone-parse (zf)
"Parse a ZONE form. The syntax of a zone form is as follows:
(zone-parse-records zone (cdr zf))
zone)))
+(export 'zone-create)
(defun zone-create (zf)
"Zone construction function. Given a zone form ZF, construct the zone and
add it to the table."
(setf (zone-find name) zone)
name))
+(export 'defzone)
(defmacro defzone (soa &rest zf)
"Zone definition macro."
`(zone-create '(,soa ,@zf)))
+(export 'defrevzone)
(defmacro defrevzone (head &rest zf)
"Define a reverse zone, with the correct name."
(destructuring-bind
":cname HOST"
(rec :data (zone-parse-host data zname)))
+(defzoneparse :txt (name data rec)
+ ":txt TEXT"
+ (rec :data data))
+
(defzoneparse :mx (name data rec :zname zname)
":mx ((HOST :prio INT :ip IPADDR)*)"
(dolist (mx (listify data))
:type :cname
:data name)))
+(defzoneparse :srv (name data rec :zname zname)
+ ":srv (((SERVICE &key :port) (PROVIDER &key :port :prio :weight :ip)*)*)"
+ (dolist (srv data)
+ (destructuring-bind (servopts &rest providers) srv
+ (destructuring-bind
+ (service &key ((:port default-port)) (protocol :tcp))
+ (listify servopts)
+ (unless default-port
+ (let ((serv (serv-by-name service protocol)))
+ (setf default-port (and serv (serv-port serv)))))
+ (let ((rname (format nil "~(_~A._~A~).~A" service protocol name)))
+ (dolist (prov providers)
+ (destructuring-bind
+ (srvname
+ &key
+ (port default-port)
+ (prio *default-mx-priority*)
+ (weight 0)
+ ip)
+ (listify prov)
+ (let ((host (zone-parse-host srvname zname)))
+ (when ip (rec :name host :type :a :data (parse-ipaddr ip)))
+ (rec :name rname
+ :data (list prio weight port host))))))))))
+
(defzoneparse :net (name data rec)
":net (NETWORK*)"
(dolist (net (listify data))
(defzoneparse (:rev :reverse) (name data rec)
":reverse ((NET :bytes BYTES) ZONE*)"
(setf data (listify data))
- (destructuring-bind
- (net &key bytes)
- (listify (car data))
+ (destructuring-bind (net &key bytes) (listify (car data))
(setf net (zone-parse-net net name))
(unless bytes
(setf bytes (ipnet-changeable-bytes (ipnet-mask net))))
:ttl (zr-ttl zr) :data (zr-name zr))
(setf (gethash name seen) t)))))))))
-(defzoneparse (:cidr-delegation :cidr) (name data rec)
+(defzoneparse (:cidr-delegation :cidr) (name data rec :zname zname)
":cidr-delegation ((NET :bytes BYTES) (TARGET-NET [TARGET-ZONE])*)"
- (destructuring-bind
- (net &key bytes)
- (listify (car data))
+ (setf data (listify data))
+ (destructuring-bind (net &key bytes) (listify (car data))
(setf net (zone-parse-net net name))
(unless bytes
(setf bytes (ipnet-changeable-bytes (ipnet-mask net))))
- (dolist (map (cdr data))
- (destructuring-bind
- (tnet &optional tdom)
- (listify map)
+ (dolist (map (or (cdr data) (list (list net))))
+ (destructuring-bind (tnet &optional tdom) (listify map)
(setf tnet (zone-parse-net tnet name))
(unless (ipnet-subnetp net tnet)
(error "~A is not a subnet of ~A."
(join-strings
#\.
(append (reverse (loop
- for i from (1- bytes) downto 0
- until (zerop (logand mask
- (ash #xff
- (* 8 i))))
- collect (logand #xff
- (ash net (* -8 i)))))
+ for i from (1- bytes) downto 0
+ until (zerop (logand mask
+ (ash #xff
+ (* 8 i))))
+ collect (ldb (byte 8 (* i 8)) net)))
(list name))))))
- (setf tdom (string-downcase tdom))
+ (setf tdom (string-downcase (stringify tdom)))
(dotimes (i (ipnet-hosts tnet))
- (let* ((addr (ipnet-host tnet i))
- (tail (join-strings #\.
- (loop
+ (unless (zerop i)
+ (let* ((addr (ipnet-host tnet i))
+ (tail (join-strings #\.
+ (loop
for i from 0 below bytes
collect
- (logand #xff
- (ash addr (* 8 i)))))))
- (rec :name (format nil "~A.~A" tail name)
- :type :cname
- :data (format nil "~A.~A" tail tdom))))))))
+ (logand #xff
+ (ash addr (* 8 i)))))))
+ (rec :name (format nil "~A.~A" tail name)
+ :type :cname
+ :data (format nil "~A.~A" tail tdom)))))))))
;;;--------------------------------------------------------------------------
;;; Zone file output.
+(export 'zone-write)
(defgeneric zone-write (format zone stream)
(:documentation "Write ZONE's records to STREAM in the specified FORMAT."))
(*zone-output-stream* stream))
(call-next-method)))
+(export 'zone-save)
(defun zone-save (zones &key (format :bind))
"Write the named ZONES to files. If no zones are given, write all the
zones."
;;;--------------------------------------------------------------------------
;;; Bind format output.
+(export 'bind-hostname)
(defun bind-hostname (hostname)
(if (not hostname)
"@"
(dolist (zr (zone-records zone))
(bind-record (zr-type zr) zr)))
+(export 'bind-record)
(defgeneric bind-record (type zr))
+(export 'bind-format-record)
(defun bind-format-record (name ttl type format args)
(format *zone-output-stream*
"~A~20T~@[~8D~]~30TIN ~A~40T~?~%"
(bind-record-type type)
format args)))
+(export 'bind-record-type)
(defgeneric bind-record-type (type)
(:method (type) type))
+(export 'bind-record-format-args)
(defgeneric bind-record-format-args (type data)
(:method ((type (eql :a)) data) (list "~A" (ipaddr-string data)))
(:method ((type (eql :ptr)) data) (list "~A" (bind-hostname data)))
(:method ((type (eql :ns)) data) (list "~A" (bind-hostname data)))
(:method ((type (eql :mx)) data)
(list "~2D ~A" (cdr data) (bind-hostname (car data))))
+ (:method ((type (eql :srv)) data)
+ (destructuring-bind (prio weight port host) data
+ (list "~2D ~5D ~5D ~A" prio weight port (bind-hostname host))))
(:method ((type (eql :txt)) data) (list "~S" (stringify data))))
;;;----- That's all, folks --------------------------------------------------