#lang racket/base ;; Simple imperative DNS server. (require racket/match) (require racket/udp) (require racket/set) (require racket/bool) (require "../racket-bitsyntax/main.rkt") (require "api.rkt") (require "codec.rkt") (require "dump-bytes.rkt") ;; Instantiated with a SOA record for the zone it is serving as well ;; as a zone's worth of DNS data which is used to answer queries ;; authoritatively. Never caches information, never performs recursive ;; queries. ;; Rules: ;; - Answers authoritative NXDOMAIN answers for queries falling within ;; its zone. (This is the only responder entitled to answer NXDOMAIN!) ;; - Answers with referrals for queries falling in subzones. It ;; determines subzones based on the RRs it is configured with at ;; startup. ;; TODO: maybe store domain names big-end first? It'd make bailiwick ;; and subzone checks into prefix rather than suffix checks. It makes ;; domain names into paths through the DNS DB tree. ;; compile-db : ListOf -> Hash> ;; Builds an immutable hash table from the given RRs, suitable for ;; quickly looking up answers to queries. (define (compile-db rrs) ;; RR Hash -> Hash (define (incorporate-rr rr db) (hash-set db (rr-name rr) (set-add (hash-ref db (rr-name rr) (set)) rr))) (foldl incorporate-rr (make-immutable-hash) rrs)) (define (in-bailiwick? dn root) (cond ((equal? dn root) #t) ((null? dn) #f) (else (in-bailiwick? (cdr dn) root)))) (define (authoritativeness-for dn soa-rr) (if (in-bailiwick? dn (rr-name soa-rr)) 'authoritative 'non-authoritative)) (define (filter-by-type rrset type) (set-filter (lambda (rr) (eqv? (rr-type rr) type)) rrset)) (define (referral-for name soa-rr zone) (define limit (rr-name soa-rr)) (let search ((name name)) (cond ((or (null? name) (equal? name limit)) ;; We've walked up the tree to the top of the zone. No referrals ;; are possible. #f) ((hash-ref zone name #f) => ;; There's an entry for this suffix of the original name. Check ;; to see if it has an NS record indicating a subzone. (lambda (rrset) (define ns-rrset (filter-by-type rrset 'ns)) (if (set-empty? ns-rrset) (search (cdr name)) ;; no NS records for this suffix. Keep looking. ns-rrset))) (else ;; Nothing for this suffix. Keep lookup. (search (cdr name)))))) (define (additional-section/a zone names) ;; RFC 3596 (section 3) requires that we process AAAA here as well ;; as A. (foldl (lambda (name section) (set-union section (set-filter (lambda (rr) (and (memv (rr-type rr) '(a aaaa)) (eqv? (rr-class rr) 'in))) (hash-ref zone name)))) (set) names)) ;; ASSUMPTION: r1 and r2 are both DNS replies to the same query. ;; ASSUMPTION: no response-codes other than no-error or name-error are in use. (define (merge-replies r1 r2) (dns-message (dns-message-id r1) 'response 'query (if (and (eqv? (dns-message-authoritative r1) 'authoritative) (eqv? (dns-message-authoritative r2) 'authoritative)) 'authoritative 'non-authoritative) 'not-truncated (dns-message-recursion-desired r1) 'no-recursion-available (if (and (eqv? (dns-message-response-code r1) 'name-error) (eqv? (dns-message-response-code r2) 'name-error)) 'name-error 'no-error) (dns-message-questions r1) (listset-union (dns-message-answers r1) (dns-message-answers r2)) (listset-union (dns-message-authorities r1) (dns-message-authorities r2)) (listset-union (dns-message-additional r1) (dns-message-additional r2)))) (define (listset-union xs1 xs2) (set->list (set-union (list->set xs1) (list->set xs2)))) ;; set-filter : (X -> Boolean) SetOf -> SetOf ;; Retains only those elements of its argument for which the predicate ;; answers #t. (define (set-filter predicate in) (for/set ([x (in-set in)] #:when (predicate x)) x)) ;; filter-rrs : SetOf QueryType QueryClass ;; Returns a set like its argument with RRs not matching the given ;; type and class removed. (define (filter-rrs rrs qtype qclass) (define filtered-by-type (case qtype ((*) rrs) (else (filter-by-type rrs qtype)))) (define filtered-by-type-and-class (case qclass ((*) filtered-by-type) (else (set-filter (lambda (rr) (eqv? (rr-class rr) qclass)) filtered-by-type)))) filtered-by-type-and-class) ;; start-server : UInt16 RR ListOf -> Void ;; Starts a server that will answer questions received on the given ;; UDP port based on the RRs it is given and the zone origin specified ;; in the soa-rr given. (require racket/pretty) (define (start-server port-number soa-rr rrs) ;; Compile the zone hash table (define zone (compile-db (cons soa-rr rrs))) (pretty-print zone) ;; Set up the socket (define s (udp-open-socket #f #f)) (udp-bind! s #f port-number) (define (service-loop) (with-handlers ((exn:break? (lambda (e) (raise e))) (exn? (lambda (e) (display "Error in DNS service handler:") (newline) (write e) (newline) (newline)))) (read-and-process-request)) (service-loop)) (define (read-and-process-request) (define buffer (make-bytes 512)) (define-values (packet-length source-hostname source-port) (udp-receive! s buffer)) (define (send-error error-response-code) (bit-string-case buffer ([ (id :: bits 16) (:: binary) ] (udp-send-to s source-hostname source-port (dns-message->packet (dns-message id 'response 'query 'non-authoritative 'not-truncated 'no-recursion-desired 'no-recursion-available error-response-code '() '() '() '())))) (else ;; We don't even have enough information in the packet to reply. (void)))) (display "----------------------------------------") (newline) (write (subbytes buffer 0 packet-length)) (newline) (dump-bytes! buffer packet-length) (flush-output) (define request-message (with-handlers ((exn? (lambda (e) (send-error 'format-error) (raise e)))) (packet->dns-message (subbytes buffer 0 packet-length)))) ;;(write request-message) (newline) (define (make-reply name send-name-error? answers authorities additional) (dns-message (dns-message-id request-message) 'response 'query (authoritativeness-for name soa-rr) 'not-truncated (dns-message-recursion-desired request-message) 'no-recursion-available (if send-name-error? 'name-error 'no-error) (dns-message-questions request-message) (set->list answers) (set->list authorities) (set->list additional))) (define reply-packet (with-handlers ((exn? (lambda (e) (send-error 'server-failure) (raise e)))) ;; TODO: check opcode and direction in request (define questions (dns-message-questions request-message)) (if (null? questions) #f ;; No questions -> don't send any replies (begin ;; TODO: what if there are multiple questions in one ;; request packet? Single reply, or multiple replies? ;; Process the additional questions, or ignore them? ;; djbdns looks like it handles exactly one question per ;; request, ignoring any excess... (dns-message->packet (answer-question (car questions) make-reply)))))) ;; TODO: Truncation (when reply-packet (udp-send-to s source-hostname source-port reply-packet))) (define (answer-question q make-reply) (let resolve ((name (question-name q))) ;; Notice that we claim to be authoritative for our configured ;; zone. If we ever answer name-error, that means there are no ;; RRs *at all* for the queried name. If there are RRs for the ;; queried name, but they happen not to be the ones asked for, ;; name-error must *not* be returned: instead, a normal ;; no-error reply is sent with an empty answer section. ;; ;; If we wanted to support caching of negative replies, we'd ;; follow the guidelines in section 4.3.4 "Negative response ;; caching" of RFC1034, adding our zone SOA with an ;; appropriate TTL to the additional section of the reply. ;; ;; TODO: We support returning out-of-bailiwick records (glue) ;; here. Reexamine the rules for doing so. (cond ((hash-ref zone name #f) => ;; The full name matches in our zone database. (lambda (rrset) (define filtered-rrs (filter-rrs rrset (question-type q) (question-class q))) (define cnames (filter-by-type rrset 'cname)) (define base-reply (make-reply name #f (set-union cnames filtered-rrs) (set soa-rr) (set))) ;; Algorithm for CNAME expansion from RFC 1034 4.3.2 step 3a. (if (and (not (set-empty? cnames)) (not (eqv? (question-type q) 'cname))) (foldl (lambda (cname-rr current-reply) (merge-replies current-reply (resolve (rr-rdata cname-rr)))) base-reply (set->list cnames)) base-reply))) ((referral-for name soa-rr zone) => ;; No full name match, but a referral to somewhere beneath our ;; SOA but within our zone. (lambda (ns-rrset) (make-reply name #f ns-rrset (set soa-rr) (additional-section/a zone (set-map ns-rrset rr-rdata))))) (else ;; Neither a full name match nor a referral is ;; available. Answer name-error (NXDOMAIN) if the queried ;; name is in-bailiwick, or a normal no-error otherwise. (make-reply name (in-bailiwick? name (rr-name soa-rr)) (set) (set) (set)))))) (service-loop)) (start-server 5555 (rr '(#"example") 'soa 'in 30 (soa '(#"ns" #"example") '(#"tonyg" #"example") 1 24 24 30 10)) (list (rr '(#"localhost" #"example") 'a 'in 30 '#(127 0 0 1)) (rr '(#"example") 'mx 'in 30 (mx 5 '(#"localhost" #"example"))) (rr '(#"example") 'mx 'in 30 (mx 10 '(#"subns" #"example"))) (rr '(#"google" #"example") 'cname 'in 30 '(#"www" #"google" #"com")) (rr '(#"roar" #"example") 'a 'in 30 '#(192 168 1 1)) (rr '(#"ns" #"example") 'a 'in 30 '#(127 0 0 1)) (rr '(#"hello" #"example") 'txt 'in 30 '(#"Hello CRASH")) (rr '(#"subzone" #"example") 'ns 'in 30 '(#"subns" #"example")) (rr '(#"subns" #"example") 'a 'in 30 '#(127 0 0 2))))