chiark / gitweb /
zone: Write output to DOMAIN.zone, rather than just DOMAIN.
[zone] / zone.lisp
... / ...
CommitLineData
1;;; -*-lisp-*-
2;;;
3;;; $Id$
4;;;
5;;; DNS zone generation
6;;;
7;;; (c) 2005 Straylight/Edgeware
8;;;
9
10;;;----- Licensing notice ---------------------------------------------------
11;;;
12;;; This program is free software; you can redistribute it and/or modify
13;;; it under the terms of the GNU General Public License as published by
14;;; the Free Software Foundation; either version 2 of the License, or
15;;; (at your option) any later version.
16;;;
17;;; This program is distributed in the hope that it will be useful,
18;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;;; GNU General Public License for more details.
21;;;
22;;; You should have received a copy of the GNU General Public License
23;;; along with this program; if not, write to the Free Software Foundation,
24;;; Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26(defpackage #:zone
27 (:use #:common-lisp #:mdw.base #:mdw.str #:mdw.collect #:mdw.safely)
28 (:export #:ipaddr #:string-ipaddr #:ipaddr-byte #:ipaddr-string #:ipaddrp
29 #:integer-netmask #:ipmask #:ipmask-cidl-slash #:make-ipnet
30 #:string-ipnet #:ipnet #:ipnet-net #:ipnet-mask #:with-ipnet
31 #:ipnet-pretty #:ipnet-string #:ipnet-broadcast #:ipnet-hosts
32 #:ipnet-host #:ipaddr-networkp #:ipnet-subnetp
33 #:host-find# #:host-create #:defhost #:parse-ipaddr
34 #:net #:net-find #:net-get-as-ipnet #:net-create #:defnet
35 #:net-next-host #:net-host
36 #:soa #:mx #:zone #:zone-record #:zone-subdomain
37 #:*default-zone-source* #:*default-zone-refresh*
38 #:*default-zone-retry* #:*default-zone-expire*
39 #:*default-zone-min-ttl* #:*default-zone-ttl*
40 #:*default-mx-priority* #:*default-zone-admin*
41 #:zone-find #:zone-parse #:zone-write #:zone-create #:defzone
42 #:defrevzone #:zone-save
43 #:defzoneparse #:zone-parse-host
44 #:timespec-seconds #:make-zone-serial))
45(in-package #:zone)
46
47(defun mask (n)
48 "Return 2^N - 1: i.e., a mask of N set bits."
49 (1- (ash 1 n)))
50(deftype u32 ()
51 "The type of unsigned 32-bit values."
52 '(unsigned-byte 32))
53(deftype ipaddr ()
54 "The type of IP (version 4) addresses."
55 'u32)
56
57(defun string-ipaddr (str &key (start 0) (end nil))
58 "Parse STR as an IP address in dotted-quad form and return the integer
59equivalent. STR may be anything at all: it's converted as if by
60`stringify'. The START and END arguments may be used to parse out a
61substring."
62 (setf str (stringify str))
63 (unless end
64 (setf end (length str)))
65 (let ((addr 0) (noct 0))
66 (loop
67 (let* ((pos (position #\. str :start start :end end))
68 (i (parse-integer str :start start :end (or pos end))))
69 (unless (<= 0 i 256)
70 (error "IP address octet out of range"))
71 (setf addr (+ (* addr 256) i))
72 (incf noct)
73 (unless pos
74 (return))
75 (setf start (1+ pos))))
76 (unless (= noct 4)
77 (error "Wrong number of octets in IP address"))
78 addr))
79(defun ipaddr-byte (ip n)
80 "Return byte N (from most significant downwards) of an IP address."
81 (assert (<= 0 n 3))
82 (logand #xff (ash ip (* -8 (- 3 n)))))
83(defun ipaddr-string (ip)
84 "Transform the address IP into a string in dotted-quad form."
85 (check-type ip ipaddr)
86 (join-strings #\. (collecting ()
87 (dotimes (i 4)
88 (collect (ipaddr-byte ip i))))))
89(defun ipaddrp (ip)
90 "Answer true if IP is a valid IP address in integer form."
91 (typep ip 'ipaddr))
92(defun ipaddr (ip)
93 "Convert IP to an IP address. If it's an integer, return it unchanged;
94otherwise convert by `string-ipaddr'."
95 (typecase ip
96 (ipaddr ip)
97 (t (string-ipaddr ip))))
98
99(defun integer-netmask (i)
100 "Given an integer I, return a netmask with its I top bits set."
101 (- (ash 1 32) (ash 1 (- 32 i))))
102(defun ipmask (ip)
103 "Transform IP into a netmask. If it's a small integer then it's converted
104by `integer-netmask'; if nil, then all-bits-set; otherwise convert using
105`ipaddr'."
106 (typecase ip
107 (null (mask 32))
108 ((integer 0 32) (integer-netmask ip))
109 (t (ipaddr ip))))
110(defun ipmask-cidl-slash (mask)
111 "Given a netmask MASK, return an integer N such that (integer-netmask N) =
112MASK, or nil if this is impossible."
113 (dotimes (i 33)
114 (when (= mask (integer-netmask i))
115 (return i))))
116
117(defun make-ipnet (net mask)
118 "Construct an IP-network object given the NET and MASK; these are
119transformed as though by `ipaddr' and `ipmask'."
120 (let ((net (ipaddr net))
121 (mask (ipmask mask)))
122 (cons (logand net mask) mask)))
123(defun string-ipnet (str &key (start 0) (end nil))
124 "Parse an IP-network from the string STR."
125 (setf str (stringify str))
126 (unless end (setf end (length str)))
127 (let ((sl (position #\/ str :start start :end end)))
128 (if sl
129 (make-ipnet (parse-ipaddr (subseq str start sl))
130 (if (find #\. str :start (1+ sl) :end end)
131 (string-ipaddr str :start (1+ sl) :end end)
132 (integer-netmask (parse-integer str
133 :start (1+ sl)
134 :end end))))
135 (make-ipnet (parse-ipaddr (subseq str start end))
136 (integer-netmask 32)))))
137(defun ipnet (net &optional mask)
138 "Construct an IP-network object from the given arguments. A number of
139forms are acceptable:
140
141 * NET MASK -- as for `make-ipnet'.
142 * ADDR -- a single address (equivalent to ADDR 32)
143 * (NET . MASK|nil) -- a single-object representation.
144 * IPNET -- return an equivalent (`equal', not necessarily `eql') version."
145 (cond (mask (make-ipnet net mask))
146 ((or (stringp net) (symbolp net)) (string-ipnet net))
147 (t (apply #'make-ipnet (pairify net 32)))))
148(defun ipnet-net (ipn)
149 "Return the base network address of IPN."
150 (car ipn))
151(defun ipnet-mask (ipn)
152 "Return the netmask of IPN."
153 (cdr ipn))
154(defmacro with-ipnet ((net mask) ipn &body body)
155 "Evaluate BODY with NET and MASK bound to the base address and netmask of
156IPN. Either NET or MASK (or, less usefully, both) may be nil if not wanted."
157 (with-gensyms tmp
158 `(let ((,tmp ,ipn))
159 (let (,@(and net `((,net (ipnet-net ,tmp))))
160 ,@(and mask `((,mask (ipnet-mask ,tmp)))))
161 ,@body))))
162(defun ipnet-pretty (ipn)
163 "Convert IPN to a pretty cons-cell form."
164 (with-ipnet (net mask) ipn
165 (cons (ipaddr-string net)
166 (or (ipmask-cidl-slash mask) (ipaddr-string mask)))))
167(defun ipnet-string (ipn)
168 "Convert IPN to a string."
169 (with-ipnet (net mask) ipn
170 (format nil "~A/~A"
171 (ipaddr-string net)
172 (or (ipmask-cidl-slash mask) (ipaddr-string mask)))))
173(defun ipnet-broadcast (ipn)
174 "Return the broadcast address for the network IPN."
175 (with-ipnet (net mask) ipn
176 (logior net (logxor (mask 32) mask))))
177(defun ipnet-hosts (ipn)
178 "Return the number of available addresses in network IPN."
179 (ash 1 (- 32 (logcount (ipnet-mask ipn)))))
180(defun ipnet-host (ipn host)
181 "Return the address of the given HOST in network IPN. This works even with
182a non-contiguous netmask."
183 (check-type host u32)
184 (with-ipnet (net mask) ipn
185 (let ((i 0) (m 1) (a net) (h host))
186 (loop
187 (when (>= i 32)
188 (error "Host index ~D out of range for network ~A"
189 host (ipnet-pretty ipn)))
190 (cond ((zerop h)
191 (return a))
192 ((logbitp i mask)
193 (setf h (ash h 1)))
194 (t
195 (setf a (logior a (logand m h)))
196 (setf h (logandc2 h m))))
197 (setf m (ash m 1))
198 (incf i)))))
199(defun ipaddr-networkp (ip ipn)
200 "Returns true if address IP is within network IPN."
201 (with-ipnet (net mask) ipn
202 (= net (logand ip mask))))
203(defun ipnet-subnetp (ipn subn)
204 "Returns true if SUBN is a (non-strict) subnet of IPN."
205 (with-ipnet (net mask) ipn
206 (with-ipnet (subnet submask) subn
207 (and (= net (logand subnet mask))
208 (= submask (logior mask submask))))))
209
210(defun resolve-hostname (name)
211 "Resolve a hostname to an IP address using the DNS, or return nil."
212 (let ((he (ext:lookup-host-entry name)))
213 (and he
214 (ext:host-entry-addr he))))
215(defun canonify-hostname (name)
216 "Resolve a hostname to canonical form using the DNS, or return nil."
217 (let ((he (ext:lookup-host-entry name)))
218 (and he
219 (ext:host-entry-name he))))
220(defun parse-ipaddr (addr)
221 "Convert the string ADDR into an IP address: tries all sorts of things:
222
223 (NET [INDEX]) -- index a network: NET is a network name defined by defnet;
224 INDEX is an index or one of the special symbols understood by net-host,
225 and defaults to :next
226 INTEGER -- an integer IP address
227 IPADDR -- an IP address in dotted-quad form
228 HOST -- a host name defined by defhost
229 DNSNAME -- a name string to look up in the DNS"
230 (cond ((listp addr)
231 (destructuring-bind
232 (net host)
233 (pairify addr :next)
234 (net-host (or (net-find net)
235 (error "Network ~A not found" net))
236 host)))
237 ((ipaddrp addr) addr)
238 (t
239 (setf addr (string-downcase (stringify addr)))
240 (or (host-find addr)
241 (and (plusp (length addr))
242 (digit-char-p (char addr 0))
243 (string-ipaddr addr))
244 (resolve-hostname (stringify addr))
245 (error "Host name ~A unresolvable" addr)))))
246
247(defvar *hosts* (make-hash-table :test #'equal)
248 "The table of known hostnames.")
249(defun host-find (name)
250 "Find a host by NAME."
251 (gethash (string-downcase (stringify name)) *hosts*))
252(defun (setf host-find) (addr name)
253 "Make NAME map to ADDR (must be an ipaddr in integer form)."
254 (setf (gethash (string-downcase (stringify name)) *hosts*) addr))
255(defun host-create (name addr)
256 "Make host NAME map to ADDR (anything acceptable to parse-ipaddr)."
257 (setf (host-find name) (parse-ipaddr addr)))
258(defmacro defhost (name addr)
259 "Main host definition macro. Neither NAME nor ADDR is evaluated."
260 `(progn
261 (host-create ',name ',addr)
262 ',name))
263
264(defstruct (net (:predicate netp))
265 "A network structure. Slots:
266
267NAME The network's name, as a string
268IPNET The network base address and mask
269HOSTS Number of hosts in the network
270NEXT Index of the next unassigned host"
271 name
272 ipnet
273 hosts
274 next)
275
276(defvar *networks* (make-hash-table :test #'equal)
277 "The table of known networks.")
278(defun net-find (name)
279 "Find a network by NAME."
280 (gethash (string-downcase (stringify name)) *networks*))
281(defun (setf net-find) (net name)
282 "Make NAME map to NET."
283 (setf (gethash (string-downcase (stringify name)) *networks*) net))
284(defun net-get-as-ipnet (form)
285 "Transform FORM into an ipnet. FORM may be a network name, or something
286acceptable to the ipnet function."
287 (let ((net (net-find form)))
288 (if net (net-ipnet net)
289 (ipnet form))))
290(defun net-create (name &rest args)
291 "Construct a new network called NAME and add it to the map. The ARGS
292describe the new network, in a form acceptable to the ipnet function."
293 (let ((ipn (apply #'ipnet args)))
294 (setf (net-find name)
295 (make-net :name (string-downcase (stringify name))
296 :ipnet ipn
297 :hosts (ipnet-hosts ipn)
298 :next 1))))
299(defmacro defnet (name &rest args)
300 "Main network definition macro. Neither NAME nor any of the ARGS is
301evaluated."
302 `(progn
303 (net-create ',name ,@(mapcar (lambda (x) `',x) args))
304 ',name))
305(defun net-next-host (net)
306 "Given a NET, return the IP address (as integer) of the next available
307address in the network."
308 (unless (< (net-next net) (net-hosts net))
309 (error "No more hosts left in network ~A" (net-name net)))
310 (let ((next (net-next net)))
311 (incf (net-next net))
312 (net-host net next)))
313(defun net-host (net host)
314 "Return the given HOST on the NEXT. HOST may be an index (in range, of
315course), or one of the keywords:
316:NEXT next host, as by net-next-host
317:NET network base address
318:BROADCAST network broadcast address"
319 (case host
320 (:next (net-next-host net))
321 (:net (ipnet-net (net-ipnet net)))
322 (:broadcast (ipnet-broadcast (net-ipnet net)))
323 (t (ipnet-host (net-ipnet net) host))))
324
325(defun to-integer (x)
326 "Convert X to an integer in the most straightforward way."
327 (floor (rational x)))
328(defun timespec-seconds (ts)
329 "Convert a timespec TS to seconds. A timespec may be a real count of
330seconds, or a list (COUNT UNIT): UNIT may be any of a number of obvious time
331units."
332 (cond ((null ts) 0)
333 ((realp ts) (floor ts))
334 ((atom ts)
335 (error "Unknown timespec format ~A" ts))
336 ((null (cdr ts))
337 (timespec-seconds (car ts)))
338 (t (+ (to-integer (* (car ts)
339 (case (intern (string-upcase
340 (stringify (cadr ts)))
341 '#:zone)
342 ((s sec secs second seconds) 1)
343 ((m min mins minute minutes) 60)
344 ((h hr hrs hour hours) #.(* 60 60))
345 ((d dy dys day days) #.(* 24 60 60))
346 ((w wk wks week weeks) #.(* 7 24 60 60))
347 ((y yr yrs year years) #.(* 365 24 60 60))
348 (t (error "Unknown time unit ~A"
349 (cadr ts))))))
350 (timespec-seconds (cddr ts))))))
351
352(defstruct (soa (:predicate soap))
353 "Start-of-authority record information."
354 source
355 admin
356 refresh
357 retry
358 expire
359 min-ttl
360 serial)
361(defstruct (mx (:predicate mxp))
362 "Mail-exchange record information."
363 priority
364 domain)
365(defstruct (zone (:predicate zonep))
366 "Zone information."
367 soa
368 default-ttl
369 name
370 records)
371
372(defvar *default-zone-source*
373 (let ((hn (unix:unix-gethostname)))
374 (and hn (concatenate 'string (canonify-hostname hn) ".")))
375 "The default zone source: the current host's name.")
376(defvar *default-zone-refresh* (* 24 60 60)
377 "Default zone refresh interval: one day.")
378(defvar *default-zone-admin* nil
379 "Default zone administrator's email address.")
380(defvar *default-zone-retry* (* 60 60)
381 "Default znoe retry interval: one hour.")
382(defvar *default-zone-expire* (* 14 24 60 60)
383 "Default zone expiry time: two weeks.")
384(defvar *default-zone-min-ttl* (* 4 60 60)
385 "Default zone minimum TTL/negative TTL: four hours.")
386(defvar *default-zone-ttl* (* 8 60 60)
387 "Default zone TTL (for records without explicit TTLs): 8 hours.")
388(defvar *default-mx-priority* 50
389 "Default MX priority.")
390
391(defun from-mixed-base (base val)
392 "BASE is a list of the ranges for the `digits' of a mixed-base
393representation. Convert VAL, a list of digits, into an integer."
394 (do ((base base (cdr base))
395 (val (cdr val) (cdr val))
396 (a (car val) (+ (* a (car base)) (car val))))
397 ((or (null base) (null val)) a)))
398(defun to-mixed-base (base val)
399 "BASE is a list of the ranges for the `digits' of a mixed-base
400representation. Convert VAL, an integer, into a list of digits."
401 (let ((base (reverse base))
402 (a nil))
403 (loop
404 (unless base
405 (push val a)
406 (return a))
407 (multiple-value-bind (q r) (floor val (pop base))
408 (push r a)
409 (setf val q)))))
410
411(defun make-zone-serial (name)
412 "Given a zone NAME, come up with a new serial number. This will (very
413carefully) update a file ZONE.serial in the current directory."
414 (let* ((file (format nil "~(~A~).serial" name))
415 (last (with-open-file (in file
416 :direction :input
417 :if-does-not-exist nil)
418 (if in (read in)
419 (list 0 0 0 0))))
420 (now (multiple-value-bind
421 (sec min hr dy mon yr dow dstp tz)
422 (get-decoded-time)
423 (declare (ignore sec min hr dow dstp tz))
424 (list dy mon yr)))
425 (seq (cond ((not (equal now (cdr last))) 0)
426 ((< (car last) 99) (1+ (car last)))
427 (t (error "Run out of sequence numbers for ~A" name)))))
428 (safely-writing (out file)
429 (format out
430 ";; Serial number file for zone ~A~%~
431 ;; (LAST-SEQ DAY MONTH YEAR)~%~
432 ~S~%"
433 name
434 (cons seq now)))
435 (from-mixed-base '(100 100 100) (reverse (cons seq now)))))
436
437(defvar *zones* (make-hash-table :test #'equal)
438 "Map of known zones.")
439(defun zone-find (name)
440 "Find a zone given its NAME."
441 (gethash (string-downcase (stringify name)) *zones*))
442(defun (setf zone-find) (zone name)
443 "Make the zone NAME map to ZONE."
444 (setf (gethash (string-downcase (stringify name)) *zones*) zone))
445
446(defstruct (zone-record (:conc-name zr-))
447 "A zone record."
448 (name '<unnamed>)
449 ttl
450 type
451 (defsubp nil)
452 data)
453
454(defstruct (zone-subdomain (:conc-name zs-))
455 "A subdomain. Slightly weird. Used internally by zone-process-records
456below, and shouldn't escape."
457 name
458 ttl
459 records)
460
461(defun zone-process-records (rec ttl func)
462 "Sort out the list of records in REC, calling FUNC for each one. TTL is
463the default time-to-live for records which don't specify one."
464 (labels ((sift (rec ttl)
465 (collecting (top sub)
466 (loop
467 (unless rec
468 (return))
469 (let ((r (pop rec)))
470 (cond ((eq r :ttl)
471 (setf ttl (pop rec)))
472 ((symbolp r)
473 (collect (make-zone-record :type r
474 :ttl ttl
475 :data (pop rec))
476 top))
477 ((listp r)
478 (dolist (name (listify (car r)))
479 (collect (make-zone-subdomain :name name
480 :ttl ttl
481 :records (cdr r))
482 sub)))
483 (t
484 (error "Unexpected record form ~A" (car r))))))))
485 (process (rec dom ttl defsubp)
486 (multiple-value-bind (top sub) (sift rec ttl)
487 (if (and dom (null top) sub)
488 (let ((s (pop sub)))
489 (process (zs-records s)
490 dom
491 (zs-ttl s)
492 defsubp)
493 (process (zs-records s)
494 (cons (zs-name s) dom)
495 (zs-ttl s)
496 t))
497 (let ((name (and dom
498 (string-downcase
499 (join-strings #\. (reverse dom))))))
500 (dolist (zr top)
501 (setf (zr-name zr) name)
502 (setf (zr-defsubp zr) defsubp)
503 (funcall func zr))))
504 (dolist (s sub)
505 (process (zs-records s)
506 (cons (zs-name s) dom)
507 (zs-ttl s)
508 defsubp)))))
509 (process rec nil ttl nil)))
510
511(defun zone-parse-host (f zname)
512 "Parse a host name F: if F ends in a dot then it's considered absolute;
513otherwise it's relative to ZNAME."
514 (setf f (stringify f))
515 (cond ((string= f "@") (stringify zname))
516 ((and (plusp (length f))
517 (char= (char f (1- (length f))) #\.))
518 (string-downcase (subseq f 0 (1- (length f)))))
519 (t (string-downcase (concatenate 'string f "."
520 (stringify zname))))))
521(defun ipnet-changeable-bytes (mask)
522 "Answers how many low-order bytes of MASK are (entirely or partially)
523changeable. This is used when constructing reverse zones."
524 (dotimes (i 4 4)
525 (when (/= (ipaddr-byte mask i) 255)
526 (return (- 4 i)))))
527(defun default-rev-zone (base bytes)
528 (join-strings #\. (collecting ()
529 (loop for i from (- 3 bytes) downto 0
530 do (collect (ipaddr-byte base i)))
531 (collect "in-addr.arpa"))))
532
533(defun zone-name-from-net (net &optional bytes)
534 "Given a NET, and maybe the BYTES to use, convert to the appropriate
535subdomain of in-addr.arpa."
536 (let ((ipn (net-get-as-ipnet net)))
537 (with-ipnet (net mask) ipn
538 (unless bytes
539 (setf bytes (- 4 (ipnet-changeable-bytes mask))))
540 (join-strings #\.
541 (append (loop
542 for i from (- 4 bytes) below 4
543 collect (logand #xff (ash net (* -8 i))))
544 (list "in-addr.arpa"))))))
545
546(defun zone-net-from-name (name)
547 "Given a NAME in the in-addr.arpa space, convert it to an ipnet."
548 (let* ((name (string-downcase (stringify name)))
549 (len (length name))
550 (suffix ".in-addr.arpa")
551 (sufflen (length suffix))
552 (addr 0)
553 (n 0)
554 (end (- len sufflen)))
555 (unless (and (> len sufflen)
556 (string= name suffix :start1 end))
557 (error "`~A' not in ~A." name suffix))
558 (loop
559 with start = 0
560 for dot = (position #\. name :start start :end end)
561 for byte = (parse-integer name
562 :start start
563 :end (or dot end))
564 do (setf addr (logior addr (ash byte (* 8 n))))
565 (incf n)
566 when (>= n 4)
567 do (error "Can't deduce network from ~A." name)
568 while dot
569 do (setf start (1+ dot)))
570 (setf addr (ash addr (* 8 (- 4 n))))
571 (make-ipnet addr (* 8 n))))
572
573(defun zone-reverse-records (records net list bytes dom)
574 "Construct a reverse zone given a forward zone's RECORDS list, the NET that
575the reverse zone is to serve, a LIST to collect the records into, how
576many BYTES of data need to end up in the zone, and the DOM-ain suffix."
577 (dolist (zr records)
578 (when (and (eq (zr-type zr) :a)
579 (not (zr-defsubp zr))
580 (ipaddr-networkp (zr-data zr) net))
581 (collect (make-zone-record
582 :name (string-downcase
583 (join-strings
584 #\.
585 (collecting ()
586 (dotimes (i bytes)
587 (collect (logand #xff (ash (zr-data zr)
588 (* -8 i)))))
589 (collect dom))))
590 :type :ptr
591 :ttl (zr-ttl zr)
592 :data (zr-name zr))
593 list))))
594
595(defun zone-reverse (data name list)
596 "Process a :reverse record's DATA, for a domain called NAME, and add the
597records to the LIST."
598 (destructuring-bind
599 (net &key bytes zones)
600 (listify data)
601 (setf net (zone-parse-net net name))
602 (dolist (z (or (listify zones)
603 (hash-table-keys *zones*)))
604 (zone-reverse-records (zone-records (zone-find z))
605 net
606 list
607 (or bytes
608 (ipnet-changeable-bytes (ipnet-mask net)))
609 name))))
610
611(defun zone-parse-net (net name)
612 "Given a NET, and the NAME of a domain to guess from if NET is null,
613return the ipnet for the network."
614 (if net
615 (net-get-as-ipnet net)
616 (zone-net-from-name name)))
617
618(defun zone-cidr-delg-default-name (ipn bytes)
619 "Given a delegated net IPN and the parent's number of changing BYTES,
620return the default deletate zone prefix."
621 (with-ipnet (net mask) ipn
622 (join-strings #\.
623 (reverse
624 (loop
625 for i from (1- bytes) downto 0
626 until (zerop (logand mask (ash #xff (* 8 i))))
627 collect (logand #xff (ash net (* -8 i))))))))
628
629(defun zone-cidr-delegation (data name ttl list)
630 "Given :cidr-delegation info DATA, for a record called NAME and the current
631TTL, write lots of CNAME records to LIST."
632 (destructuring-bind
633 (net &key bytes)
634 (listify (car data))
635 (setf net (zone-parse-net net name))
636 (unless bytes
637 (setf bytes (ipnet-changeable-bytes (ipnet-mask net))))
638 (dolist (map (cdr data))
639 (destructuring-bind
640 (tnet &optional tdom)
641 (listify map)
642 (setf tnet (zone-parse-net tnet name))
643 (unless (ipnet-subnetp net tnet)
644 (error "~A is not a subnet of ~A."
645 (ipnet-pretty tnet)
646 (ipnet-pretty net)))
647 (unless tdom
648 (setf tdom
649 (join-strings #\.
650 (list (zone-cidr-delg-default-name tnet bytes)
651 name))))
652 (setf tdom (string-downcase tdom))
653 (dotimes (i (ipnet-hosts tnet))
654 (let* ((addr (ipnet-host tnet i))
655 (tail (join-strings #\.
656 (loop
657 for i from 0 below bytes
658 collect
659 (logand #xff
660 (ash addr (* 8 i)))))))
661 (collect (make-zone-record
662 :name (join-strings #\.
663 (list tail name))
664 :type :cname
665 :ttl ttl
666 :data (join-strings #\. (list tail tdom)))
667 list)))))))
668
669
670
671(defun zone-parse-head (head)
672 "Parse the HEAD of a zone form. This has the form
673
674 (NAME &key :source :admin :refresh :retry
675 :expire :min-ttl :ttl :serial)
676
677though a singleton NAME needn't be a list. Returns the default TTL and an
678soa structure representing the zone head."
679 (destructuring-bind
680 (zname
681 &key
682 (source *default-zone-source*)
683 (admin (or *default-zone-admin*
684 (format nil "hostmaster@~A" zname)))
685 (refresh *default-zone-refresh*)
686 (retry *default-zone-retry*)
687 (expire *default-zone-expire*)
688 (min-ttl *default-zone-min-ttl*)
689 (ttl min-ttl)
690 (serial (make-zone-serial zname)))
691 (listify head)
692 (values zname
693 (timespec-seconds ttl)
694 (make-soa :admin admin
695 :source (zone-parse-host source zname)
696 :refresh (timespec-seconds refresh)
697 :retry (timespec-seconds retry)
698 :expire (timespec-seconds expire)
699 :min-ttl (timespec-seconds min-ttl)
700 :serial serial))))
701
702(defun hash-table-keys (ht)
703 "Return a list of the keys in hashtable HT."
704 (collecting ()
705 (maphash (lambda (key val) (declare (ignore val)) (collect key)) ht)))
706
707(defmacro defzoneparse (types (name data list
708 &key (zname (gensym "ZNAME"))
709 (ttl (gensym "TTL"))
710 (defsubp (gensym "DEFSUBP")))
711 &body body)
712 (setf types (listify types))
713 (let* ((type (car types))
714 (func (intern (format nil "ZONE-PARSE/~:@(~A~)" type))))
715 (with-gensyms (col tname ttype tttl tdata tdefsubp i)
716 `(progn
717 (dolist (,i ',types)
718 (setf (get ,i 'zone-parse) ',func))
719 (defun ,func (,name ,data ,ttl ,col ,zname ,defsubp)
720 (declare (ignorable ,zname ,defsubp))
721 (flet ((,list (&key ((:name ,tname) ,name)
722 ((:type ,ttype) ,type)
723 ((:data ,tdata) ,data)
724 ((:ttl ,tttl) ,ttl)
725 ((:defsubp ,tdefsubp) nil))
726 (collect (make-zone-record :name ,tname
727 :type ,ttype
728 :data ,tdata
729 :ttl ,tttl
730 :defsubp ,tdefsubp)
731 ,col)))
732 ,@body))
733 ',type))))
734
735(defun zone-parse-records (zone records)
736 (let ((zname (zone-name zone)))
737 (with-collection (rec)
738 (flet ((parse-record (zr)
739 (let ((func (or (get (zr-type zr) 'zone-parse)
740 (error "No parser for record ~A."
741 (zr-type zr))))
742 (name (and (zr-name zr)
743 (stringify (zr-name zr)))))
744 (if (or (not name)
745 (string= name "@"))
746 (setf name zname)
747 (let ((len (length name)))
748 (if (or (zerop len)
749 (char/= (char name (1- len)) #\.))
750 (setf name (join-strings #\.
751 (list name zname))))))
752 (funcall func
753 name
754 (zr-data zr)
755 (zr-ttl zr)
756 rec
757 zname
758 (zr-defsubp zr)))))
759 (zone-process-records records
760 (zone-default-ttl zone)
761 #'parse-record ))
762 (setf (zone-records zone) (nconc (zone-records zone) rec)))))
763
764(defun zone-parse (zf)
765 "Parse a ZONE form. The syntax of a zone form is as follows:
766
767ZONE-FORM:
768 ZONE-HEAD ZONE-RECORD*
769
770ZONE-RECORD:
771 ((NAME*) ZONE-RECORD*)
772| SYM ARGS"
773 (multiple-value-bind (zname ttl soa) (zone-parse-head (car zf))
774 (let ((zone (make-zone :name zname
775 :default-ttl ttl
776 :soa soa
777 :records nil)))
778 (zone-parse-records zone (cdr zf))
779 zone)))
780
781(defzoneparse :a (name data rec :defsubp defsubp)
782 ":a IPADDR"
783 (rec :data (parse-ipaddr data) :defsubp defsubp))
784(defzoneparse :ptr (name data rec :zname zname)
785 ":ptr HOST"
786 (rec :data (zone-parse-host data zname)))
787(defzoneparse :cname (name data rec :zname zname)
788 ":cname HOST"
789 (rec :data (zone-parse-host data zname)))
790(defzoneparse :mx (name data rec :zname zname)
791 ":mx ((HOST :prio INT :ip IPADDR)*)"
792 (dolist (mx (listify data))
793 (destructuring-bind
794 (mxname &key (prio *default-mx-priority*) ip)
795 (listify mx)
796 (let ((host (zone-parse-host mxname zname)))
797 (when ip (rec :name host :type :a :data (parse-ipaddr ip)))
798 (rec :data (cons host prio))))))
799(defzoneparse :ns (name data rec :zname zname)
800 ":ns ((HOST :ip IPADDR)*)"
801 (dolist (ns (listify data))
802 (destructuring-bind
803 (nsname &key ip)
804 (listify ns)
805 (let ((host (zone-parse-host nsname zname)))
806 (when ip (rec :name host :type :a :data (parse-ipaddr ip)))
807 (rec :data host)))))
808(defzoneparse :alias (name data rec :zname zname)
809 ":alias (LABEL*)"
810 (dolist (a (listify data))
811 (rec :name (zone-parse-host a zname)
812 :type :cname
813 :data name)))
814(defzoneparse :net (name data rec)
815 ":net (NETWORK*)"
816 (dolist (net (listify data))
817 (let ((n (net-get-as-ipnet net)))
818 (rec :name (zone-parse-host "net" name)
819 :type :a
820 :data (ipnet-net n))
821 (rec :name (zone-parse-host "mask" name)
822 :type :a
823 :data (ipnet-mask n))
824 (rec :name (zone-parse-host "broadcast" name)
825 :type :a
826 :data (ipnet-broadcast n)))))
827
828(defzoneparse (:rev :reverse) (name data rec)
829 ":reverse ((NET :bytes BYTES) ZONE*)"
830 (setf data (listify data))
831 (destructuring-bind
832 (net &key bytes)
833 (listify (car data))
834 (setf net (zone-parse-net net name))
835 (unless bytes
836 (setf bytes (ipnet-changeable-bytes (ipnet-mask net))))
837 (dolist (z (or (cdr data)
838 (hash-table-keys *zones*)))
839 (dolist (zr (zone-records (zone-find z)))
840 (when (and (eq (zr-type zr) :a)
841 (not (zr-defsubp zr))
842 (ipaddr-networkp (zr-data zr) net))
843 (rec :name (string-downcase
844 (join-strings
845 #\.
846 (collecting ()
847 (dotimes (i bytes)
848 (collect (logand #xff (ash (zr-data zr)
849 (* -8 i)))))
850 (collect name))))
851 :type :ptr
852 :ttl (zr-ttl zr)
853 :data (zr-name zr)))))))
854
855(defzoneparse (:cidr-delegation :cidr) (name data rec)
856 ":cidr-delegation ((NET :bytes BYTES) (TARGET-NET [TARGET-ZONE])*)"
857 (destructuring-bind
858 (net &key bytes)
859 (listify (car data))
860 (setf net (zone-parse-net net name))
861 (unless bytes
862 (setf bytes (ipnet-changeable-bytes (ipnet-mask net))))
863 (dolist (map (cdr data))
864 (destructuring-bind
865 (tnet &optional tdom)
866 (listify map)
867 (setf tnet (zone-parse-net tnet name))
868 (unless (ipnet-subnetp net tnet)
869 (error "~A is not a subnet of ~A."
870 (ipnet-pretty tnet)
871 (ipnet-pretty net)))
872 (unless tdom
873 (with-ipnet (net mask) tnet
874 (setf tdom
875 (join-strings
876 #\.
877 (append (reverse (loop
878 for i from (1- bytes) downto 0
879 until (zerop (logand mask
880 (ash #xff
881 (* 8 i))))
882 collect (logand #xff
883 (ash net (* -8 i)))))
884 (list name))))))
885 (setf tdom (string-downcase tdom))
886 (dotimes (i (ipnet-hosts tnet))
887 (let* ((addr (ipnet-host tnet i))
888 (tail (join-strings #\.
889 (loop
890 for i from 0 below bytes
891 collect
892 (logand #xff
893 (ash addr (* 8 i)))))))
894 (rec :name (format nil "~A.~A" tail name)
895 :type :cname
896 :data (format nil "~A.~A" tail tdom))))))))
897
898(defun iso-date (&optional time &key datep timep (sep #\ ))
899 "Construct a textual date or time in ISO format. The TIME is the universal
900time to convert, which defaults to now; DATEP is whether to emit the date;
901TIMEP is whether to emit the time, and SEP (default is space) is how to
902separate the two."
903 (multiple-value-bind
904 (sec min hr day mon yr dow dstp tz)
905 (decode-universal-time (if (or (null time) (eq time :now))
906 (get-universal-time)
907 time))
908 (declare (ignore dow dstp tz))
909 (with-output-to-string (s)
910 (when datep
911 (format s "~4,'0D-~2,'0D-~2,'0D" yr mon day)
912 (when timep
913 (write-char sep s)))
914 (when timep
915 (format s "~2,'0D:~2,'0D:~2,'0D" hr min sec)))))
916
917(defun zone-write (zone &optional (stream *standard-output*))
918 "Write a ZONE's records to STREAM."
919 (labels ((fix-admin (a)
920 (let ((at (position #\@ a))
921 (s (concatenate 'string (string-downcase a) ".")))
922 (when s
923 (setf (char s at) #\.))
924 s))
925 (fix-host (h)
926 (if (not h)
927 "@"
928 (let* ((h (string-downcase (stringify h)))
929 (hl (length h))
930 (r (string-downcase (zone-name zone)))
931 (rl (length r)))
932 (cond ((string= r h) "@")
933 ((and (> hl rl)
934 (char= (char h (- hl rl 1)) #\.)
935 (string= h r :start1 (- hl rl)))
936 (subseq h 0 (- hl rl 1)))
937 (t (concatenate 'string h "."))))))
938 (printrec (zr)
939 (format stream "~A~20T~@[~8D~]~30TIN ~A~40T"
940 (fix-host (zr-name zr))
941 (and (/= (zr-ttl zr) (zone-default-ttl zone))
942 (zr-ttl zr))
943 (string-upcase (symbol-name (zr-type zr))))))
944 (format stream "~
945;;; Zone file `~(~A~)'
946;;; (generated ~A)
947
948$ORIGIN ~@0*~(~A.~)
949$TTL ~@2*~D~2%"
950 (zone-name zone)
951 (iso-date :now :datep t :timep t)
952 (zone-default-ttl zone))
953 (let ((soa (zone-soa zone)))
954 (format stream "~
955~A~30TIN SOA~40T~A ~A (
956~45T~10D~60T ;serial
957~45T~10D~60T ;refresh
958~45T~10D~60T ;retry
959~45T~10D~60T ;expire
960~45T~10D )~60T ;min-ttl~2%"
961 (fix-host (zone-name zone))
962 (fix-host (soa-source soa))
963 (fix-admin (soa-admin soa))
964 (soa-serial soa)
965 (soa-refresh soa)
966 (soa-retry soa)
967 (soa-expire soa)
968 (soa-min-ttl soa)))
969 (dolist (zr (zone-records zone))
970 (case (zr-type zr)
971 (:a
972 (printrec zr)
973 (format stream "~A~%" (ipaddr-string (zr-data zr))))
974 ((:ptr :cname)
975 (printrec zr)
976 (format stream "~A~%" (fix-host (zr-data zr))))
977 (:ns
978 (printrec zr)
979 (format stream "~A~%" (fix-host (zr-data zr))))
980 (:mx
981 (printrec zr)
982 (let ((mx (zr-data zr)))
983 (format stream "~2D ~A~%" (cdr mx) (fix-host (car mx)))))
984 (:txt
985 (printrec zr)
986 (format stream "~S~%" (stringify (zr-data zr))))))))
987
988(defun zone-create (zf)
989 "Zone construction function. Given a zone form ZF, construct the zone and
990add it to the table."
991 (let* ((zone (zone-parse zf))
992 (name (zone-name zone)))
993 (setf (zone-find name) zone)
994 name))
995(defmacro defzone (soa &rest zf)
996 "Zone definition macro."
997 `(zone-create '(,soa ,@zf)))
998(defmacro defrevzone (head &rest zf)
999 "Define a reverse zone, with the correct name."
1000 (destructuring-bind
1001 (net &rest soa-args)
1002 (listify head)
1003 (let ((bytes nil))
1004 (when (and soa-args (integerp (car soa-args)))
1005 (setf bytes (pop soa-args)))
1006 `(zone-create '((,(zone-name-from-net net bytes) ,@soa-args) ,@zf)))))
1007
1008
1009(defun zone-save (zones)
1010 "Write the named ZONES to files. If no zones are given, write all the
1011zones."
1012 (unless zones
1013 (setf zones (hash-table-keys *zones*)))
1014 (safely (safe)
1015 (dolist (z zones)
1016 (let ((zz (zone-find z)))
1017 (unless zz
1018 (error "Unknown zone `~A'." z))
1019 (let ((stream (safely-open-output-stream safe
1020 (format nil
1021 "~(~A~).zone"
1022 z))))
1023 (zone-write zz stream))))))
1024
1025;;;----- That's all, folks --------------------------------------------------