diff --git a/lisp/dns/client.lisp b/lisp/dns/client.lisp new file mode 100644 index 000000000..2dbe9ff31 --- /dev/null +++ b/lisp/dns/client.lisp @@ -0,0 +1,73 @@ +;; Implementation of a DoH-client, see RFC 8484 (DNS Queries over +;; HTTPS (DoH)) + +(in-package #:dns) + +;; The DoH client is configured with a URI Template [RFC6570] +(defvar *doh-base-url* "https://dns.google/resolve" + "Base URL of the service providing DNS-over-HTTP(S). Defaults to the + Google-hosted API.") + +(define-condition doh-error (error) + ((query-name :initarg :query-name + :reader doh-error-query-name + :type string) + (query-type :initarg :query-type + :reader doh-error-query-type + :type string) + (doh-url :initarg :doh-url + :reader doh-error-doh-url + :type string) + (status-code :initarg :status-code + :reader doh-error-status-code + :type integer) + (response-body :initarg :response-body + :reader doh-error-response-body + :type (or nil (vector (unsigned-byte 8)) string))) + + (:report (lambda (condition stream) + (let ((url (doh-error-doh-url condition)) + (status (doh-error-status-code condition)) + (body (doh-error-response-body condition))) + (format stream "DoH service at '~A' responded with non-success (~A): ~%~%~A" + url status body))))) + +(defun lookup-generic (name type &key (doh-url *doh-base-url*)) + (multiple-value-bind (body status) + (drakma:http-request doh-url + :decode-content t + ;; TODO(tazjin): Figure out why 'want-stream' doesn't work + :parameters `(("type" . ,type) + ("name" . ,name) + ("ct" . "application/dns-message"))) + (if (= 200 status) + (read-binary 'dns-message (flexi-streams:make-in-memory-input-stream body)) + + (restart-case (error 'doh-error + :query-name name + :query-type type + :doh-url doh-url + :status-code status + :response-body body) + (call-with-other-name (new-name) + :interactive (lambda () (list (the string (read)))) + :test (lambda (c) (typep c 'doh-error)) + (lookup-generic new-name type :doh-url doh-url)) + + (call-with-other-type (new-type) + :interactive (lambda () (list (the string (read)))) + :test (lambda (c) (typep c 'doh-error)) + (lookup-generic name new-type :doh-url doh-url)) + + (call-with-other-url (new-url) + :interactive (lambda () (list (the string (read)))) + :test (lambda (c) (typep c 'doh-error)) + (lookup-generic name type :doh-url new-url)))))) + +(defun lookup-txt (name) + "Look up the TXT records at NAME." + (lookup-generic name "TXT")) + +(defun lookup-mx (name) + "Look up the MX records at NAME." + (lookup-generic name "MX")) diff --git a/lisp/dns/default.nix b/lisp/dns/default.nix index c41b02f97..242608658 100644 --- a/lisp/dns/default.nix +++ b/lisp/dns/default.nix @@ -4,12 +4,16 @@ pkgs.nix.buildLisp.library { name = "dns"; deps = with pkgs.third_party.lisp; [ + drakma + lisp-binary + iterate alexandria cl-json - drakma ]; srcs = [ - ./resolver.lisp + ./package.lisp + ./message.lisp + ./client.lisp ]; } diff --git a/lisp/dns/message.lisp b/lisp/dns/message.lisp new file mode 100644 index 000000000..d4120da38 --- /dev/null +++ b/lisp/dns/message.lisp @@ -0,0 +1,729 @@ +(in-package :dns) + +;; 3.2.2. TYPE values + +;; TYPE fields are used in resource records. Note that these types are a +;; subset of QTYPEs. + +;; TYPE value and meaning + +;; A 1 a host address + +;; NS 2 an authoritative name server + +;; MD 3 a mail destination (Obsolete - use MX) + +;; MF 4 a mail forwarder (Obsolete - use MX) + +;; CNAME 5 the canonical name for an alias + +;; SOA 6 marks the start of a zone of authority + +;; MB 7 a mailbox domain name (EXPERIMENTAL) + +;; MG 8 a mail group member (EXPERIMENTAL) + +;; MR 9 a mail rename domain name (EXPERIMENTAL) + +;; NULL 10 a null RR (EXPERIMENTAL) + +;; WKS 11 a well known service description + +;; PTR 12 a domain name pointer + +;; HINFO 13 host information + +;; MINFO 14 mailbox or mail list information + +;; MX 15 mail exchange + +;; TXT 16 text strings + +;; 3.2.3. QTYPE values + +;; QTYPE fields appear in the question part of a query. QTYPES are a +;; superset of TYPEs, hence all TYPEs are valid QTYPEs. In addition, the +;; following QTYPEs are defined: + +;; AXFR 252 A request for a transfer of an entire zone + +;; MAILB 253 A request for mailbox-related records (MB, MG or MR) + +;; MAILA 254 A request for mail agent RRs (Obsolete - see MX) + +;; * 255 A request for all records + +;; 3.2.4. CLASS values + +;; CLASS fields appear in resource records. The following CLASS mnemonics +;; and values are defined: + +;; IN 1 the Internet + +;; CS 2 the CSNET class (Obsolete - used only for examples in +;; some obsolete RFCs) + +;; CH 3 the CHAOS class + +;; HS 4 Hesiod [Dyer 87] + +;; 3.2.5. QCLASS values + +;; QCLASS fields appear in the question section of a query. QCLASS values +;; are a superset of CLASS values; every CLASS is a valid QCLASS. In +;; addition to CLASS values, the following QCLASSes are defined: + +;; * 255 any class + +;; 3.3. Standard RRs + +;; The following RR definitions are expected to occur, at least +;; potentially, in all classes. In particular, NS, SOA, CNAME, and PTR +;; will be used in all classes, and have the same format in all classes. +;; Because their RDATA format is known, all domain names in the RDATA +;; section of these RRs may be compressed. + +;; is a domain name represented as a series of labels, and +;; terminated by a label with zero length. is a single +;; length octet followed by that number of characters. +;; is treated as binary information, and can be up to 256 characters in +;; length (including the length octet). + +;; 3.3.1. CNAME RDATA format + +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; / CNAME / +;; / / +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + +;; where: + +;; CNAME A which specifies the canonical or primary +;; name for the owner. The owner name is an alias. + +;; CNAME RRs cause no additional section processing, but name servers may +;; choose to restart the query at the canonical name in certain cases. See +;; the description of name server logic in [RFC-1034] for details. + + +;; 3.3.11. NS RDATA format + +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; / NSDNAME / +;; / / +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + +;; where: + +;; NSDNAME A which specifies a host which should be +;; authoritative for the specified class and domain. + +;; NS records cause both the usual additional section processing to locate +;; a type A record, and, when used in a referral, a special search of the +;; zone in which they reside for glue information. + +;; The NS RR states that the named host should be expected to have a zone +;; starting at owner name of the specified class. Note that the class may +;; not indicate the protocol family which should be used to communicate +;; with the host, although it is typically a strong hint. For example, +;; hosts which are name servers for either Internet (IN) or Hesiod (HS) +;; class information are normally queried using IN class protocols. + +;; 3.3.12. PTR RDATA format + +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; / PTRDNAME / +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + +;; where: + +;; PTRDNAME A which points to some location in the +;; domain name space. + +;; PTR records cause no additional section processing. These RRs are used +;; in special domains to point to some other location in the domain space. +;; These records are simple data, and don't imply any special processing +;; similar to that performed by CNAME, which identifies aliases. See the +;; description of the IN-ADDR.ARPA domain for an example. + +;; 3.3.13. SOA RDATA format + +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; / MNAME / +;; / / +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; / RNAME / +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; | SERIAL | +;; | | +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; | REFRESH | +;; | | +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; | RETRY | +;; | | +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; | EXPIRE | +;; | | +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; | MINIMUM | +;; | | +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + +;; where: + +;; MNAME The of the name server that was the +;; original or primary source of data for this zone. + +;; RNAME A which specifies the mailbox of the +;; person responsible for this zone. + +;; SERIAL The unsigned 32 bit version number of the original copy +;; of the zone. Zone transfers preserve this value. This +;; value wraps and should be compared using sequence space +;; arithmetic. + +;; REFRESH A 32 bit time interval before the zone should be +;; refreshed. + +;; RETRY A 32 bit time interval that should elapse before a +;; failed refresh should be retried. + +;; EXPIRE A 32 bit time value that specifies the upper limit on +;; the time interval that can elapse before the zone is no +;; longer authoritative. + +;; MINIMUM The unsigned 32 bit minimum TTL field that should be +;; exported with any RR from this zone. + +;; SOA records cause no additional section processing. + +;; All times are in units of seconds. + +;; Most of these fields are pertinent only for name server maintenance +;; operations. However, MINIMUM is used in all query operations that +;; retrieve RRs from a zone. Whenever a RR is sent in a response to a +;; query, the TTL field is set to the maximum of the TTL field from the RR +;; and the MINIMUM field in the appropriate SOA. Thus MINIMUM is a lower +;; bound on the TTL field for all RRs in a zone. Note that this use of +;; MINIMUM should occur when the RRs are copied into the response and not +;; when the zone is loaded from a master file or via a zone transfer. The +;; reason for this provison is to allow future dynamic update facilities to +;; change the SOA RR with known semantics. + + +;; 3.3.14. TXT RDATA format + +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; / TXT-DATA / +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + +;; where: + +;; TXT-DATA One or more s. + +;; TXT RRs are used to hold descriptive text. The semantics of the text +;; depends on the domain where it is found. + +;; 3.4. Internet specific RRs + +;; 3.4.1. A RDATA format + +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; | ADDRESS | +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + +;; where: + +;; ADDRESS A 32 bit Internet address. + +;; Hosts that have multiple Internet addresses will have multiple A +;; records. + + +;; A records cause no additional section processing. The RDATA section of +;; an A line in a master file is an Internet address expressed as four +;; decimal numbers separated by dots without any imbedded spaces (e.g., +;; "10.2.0.52" or "192.0.5.6"). + +;; 3.4.2. WKS RDATA format + +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; | ADDRESS | +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +;; | PROTOCOL | | +;; +--+--+--+--+--+--+--+--+ | +;; | | +;; / / +;; / / +;; +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + +;; where: + +;; ADDRESS An 32 bit Internet address + +;; PROTOCOL An 8 bit IP protocol number + +;; A variable length bit map. The bit map must be a +;; multiple of 8 bits long. + +;; The WKS record is used to describe the well known services supported by +;; a particular protocol on a particular internet address. The PROTOCOL +;; field specifies an IP protocol number, and the bit map has one bit per +;; port of the specified protocol. The first bit corresponds to port 0, +;; the second to port 1, etc. If the bit map does not include a bit for a +;; protocol of interest, that bit is assumed zero. The appropriate values +;; and mnemonics for ports and protocols are specified in [RFC-1010]. + +;; For example, if PROTOCOL=TCP (6), the 26th bit corresponds to TCP port +;; 25 (SMTP). If this bit is set, a SMTP server should be listening on TCP +;; port 25; if zero, SMTP service is not supported on the specified +;; address. + +;; The purpose of WKS RRs is to provide availability information for +;; servers for TCP and UDP. If a server supports both TCP and UDP, or has +;; multiple Internet addresses, then multiple WKS RRs are used. + +;; WKS RRs cause no additional section processing. + +;; In master files, both ports and protocols are expressed using mnemonics +;; or decimal numbers. + +;; 3.5. IN-ADDR.ARPA domain + +;; The Internet uses a special domain to support gateway location and +;; Internet address to host mapping. Other classes may employ a similar +;; strategy in other domains. The intent of this domain is to provide a +;; guaranteed method to perform host address to host name mapping, and to +;; facilitate queries to locate all gateways on a particular network in the +;; Internet. + +;; Note that both of these services are similar to functions that could be +;; performed by inverse queries; the difference is that this part of the +;; domain name space is structured according to address, and hence can +;; guarantee that the appropriate data can be located without an exhaustive +;; search of the domain space. + +;; The domain begins at IN-ADDR.ARPA and has a substructure which follows +;; the Internet addressing structure. + +;; Domain names in the IN-ADDR.ARPA domain are defined to have up to four +;; labels in addition to the IN-ADDR.ARPA suffix. Each label represents +;; one octet of an Internet address, and is expressed as a character string +;; for a decimal value in the range 0-255 (with leading zeros omitted +;; except in the case of a zero octet which is represented by a single +;; zero). + +;; Host addresses are represented by domain names that have all four labels +;; specified. Thus data for Internet address 10.2.0.52 is located at +;; domain name 52.0.2.10.IN-ADDR.ARPA. The reversal, though awkward to +;; read, allows zones to be delegated which are exactly one network of +;; address space. For example, 10.IN-ADDR.ARPA can be a zone containing +;; data for the ARPANET, while 26.IN-ADDR.ARPA can be a separate zone for +;; MILNET. Address nodes are used to hold pointers to primary host names +;; in the normal domain space. + +;; Network numbers correspond to some non-terminal nodes at various depths +;; in the IN-ADDR.ARPA domain, since Internet network numbers are either 1, +;; 2, or 3 octets. Network nodes are used to hold pointers to the primary +;; host names of gateways attached to that network. Since a gateway is, by +;; definition, on more than one network, it will typically have two or more +;; network nodes which point at it. Gateways will also have host level +;; pointers at their fully qualified addresses. + +;; Both the gateway pointers at network nodes and the normal host pointers +;; at full address nodes use the PTR RR to point back to the primary domain +;; names of the corresponding hosts. + +;; For example, the IN-ADDR.ARPA domain will contain information about the +;; ISI gateway between net 10 and 26, an MIT gateway from net 10 to MIT's + +;; net 18, and hosts A.ISI.EDU and MULTICS.MIT.EDU. Assuming that ISI +;; gateway has addresses 10.2.0.22 and 26.0.0.103, and a name MILNET- +;; GW.ISI.EDU, and the MIT gateway has addresses 10.0.0.77 and 18.10.0.4 +;; and a name GW.LCS.MIT.EDU, the domain database would contain: + +;; 10.IN-ADDR.ARPA. PTR MILNET-GW.ISI.EDU. +;; 10.IN-ADDR.ARPA. PTR GW.LCS.MIT.EDU. +;; 18.IN-ADDR.ARPA. PTR GW.LCS.MIT.EDU. +;; 26.IN-ADDR.ARPA. PTR MILNET-GW.ISI.EDU. +;; 22.0.2.10.IN-ADDR.ARPA. PTR MILNET-GW.ISI.EDU. +;; 103.0.0.26.IN-ADDR.ARPA. PTR MILNET-GW.ISI.EDU. +;; 77.0.0.10.IN-ADDR.ARPA. PTR GW.LCS.MIT.EDU. +;; 4.0.10.18.IN-ADDR.ARPA. PTR GW.LCS.MIT.EDU. +;; 103.0.3.26.IN-ADDR.ARPA. PTR A.ISI.EDU. +;; 6.0.0.10.IN-ADDR.ARPA. PTR MULTICS.MIT.EDU. + +;; Thus a program which wanted to locate gateways on net 10 would originate +;; a query of the form QTYPE=PTR, QCLASS=IN, QNAME=10.IN-ADDR.ARPA. It +;; would receive two RRs in response: + +;; 10.IN-ADDR.ARPA. PTR MILNET-GW.ISI.EDU. +;; 10.IN-ADDR.ARPA. PTR GW.LCS.MIT.EDU. + +;; The program could then originate QTYPE=A, QCLASS=IN queries for MILNET- +;; GW.ISI.EDU. and GW.LCS.MIT.EDU. to discover the Internet addresses of +;; these gateways. + +;; A resolver which wanted to find the host name corresponding to Internet +;; host address 10.0.0.6 would pursue a query of the form QTYPE=PTR, +;; QCLASS=IN, QNAME=6.0.0.10.IN-ADDR.ARPA, and would receive: + +;; 6.0.0.10.IN-ADDR.ARPA. PTR MULTICS.MIT.EDU. + +;; Several cautions apply to the use of these services: +;; - Since the IN-ADDR.ARPA special domain and the normal domain +;; for a particular host or gateway will be in different zones, +;; the possibility exists that that the data may be inconsistent. + +;; - Gateways will often have two names in separate domains, only +;; one of which can be primary. + +;; - Systems that use the domain database to initialize their +;; routing tables must start with enough gateway information to +;; guarantee that they can access the appropriate name server. + +;; - The gateway data only reflects the existence of a gateway in a +;; manner equivalent to the current HOSTS.TXT file. It doesn't +;; replace the dynamic availability information from GGP or EGP. + +;; 3.6. Defining new types, classes, and special namespaces + +;; The previously defined types and classes are the ones in use as of the +;; date of this memo. New definitions should be expected. This section +;; makes some recommendations to designers considering additions to the +;; existing facilities. The mailing list NAMEDROPPERS@SRI-NIC.ARPA is the +;; forum where general discussion of design issues takes place. + +;; In general, a new type is appropriate when new information is to be +;; added to the database about an existing object, or we need new data +;; formats for some totally new object. Designers should attempt to define +;; types and their RDATA formats that are generally applicable to all +;; classes, and which avoid duplication of information. New classes are +;; appropriate when the DNS is to be used for a new protocol, etc which +;; requires new class-specific data formats, or when a copy of the existing +;; name space is desired, but a separate management domain is necessary. + +;; New types and classes need mnemonics for master files; the format of the +;; master files requires that the mnemonics for type and class be disjoint. + +;; TYPE and CLASS values must be a proper subset of QTYPEs and QCLASSes +;; respectively. + +;; The present system uses multiple RRs to represent multiple values of a +;; type rather than storing multiple values in the RDATA section of a +;; single RR. This is less efficient for most applications, but does keep +;; RRs shorter. The multiple RRs assumption is incorporated in some +;; experimental work on dynamic update methods. + +;; The present system attempts to minimize the duplication of data in the +;; database in order to insure consistency. Thus, in order to find the +;; address of the host for a mail exchange, you map the mail domain name to +;; a host name, then the host name to addresses, rather than a direct +;; mapping to host address. This approach is preferred because it avoids +;; the opportunity for inconsistency. + +;; In defining a new type of data, multiple RR types should not be used to +;; create an ordering between entries or express different formats for +;; equivalent bindings, instead this information should be carried in the +;; body of the RR and a single type used. This policy avoids problems with +;; caching multiple types and defining QTYPEs to match multiple types. + +;; For example, the original form of mail exchange binding used two RR +;; types one to represent a "closer" exchange (MD) and one to represent a +;; "less close" exchange (MF). The difficulty is that the presence of one +;; RR type in a cache doesn't convey any information about the other +;; because the query which acquired the cached information might have used +;; a QTYPE of MF, MD, or MAILA (which matched both). The redesigned + + + + +;; service used a single type (MX) with a "preference" value in the RDATA +;; section which can order different RRs. However, if any MX RRs are found +;; in the cache, then all should be there. + +;; 4. MESSAGES + +;; 4.1. Format + +;; All communications inside of the domain protocol are carried in a single +;; format called a message. The top level format of message is divided +;; into 5 sections (some of which are empty in certain cases) shown below: + +;; The names of the sections after the header are derived from their use in +;; standard queries. The question section contains fields that describe a +;; question to a name server. These fields are a query type (QTYPE), a +;; query class (QCLASS), and a query domain name (QNAME). The last three +;; sections have the same format: a possibly empty list of concatenated +;; resource records (RRs). The answer section contains RRs that answer the +;; question; the authority section contains RRs that point toward an +;; authoritative name server; the additional records section contains RRs +;; which relate to the query, but are not strictly answers for the +;; question. + +;; The header section is always present. The header includes fields that +;; specify which of the remaining sections are present, and also specify +;; whether the message is a query or a response, a standard query or some +;; other opcode, etc. + +;; 4.1.1. Header section format + +(defbinary dns-header (:byte-order :big-endian) + ;; A 16 bit identifier assigned by the program that + ;; generates any kind of query. This identifier is copied + ;; the corresponding reply and can be used by the requester + ;; to match up replies to outstanding queries. + (id 0 :type 16) + + ;; A one bit field that specifies whether this message is a + ;; query (0), or a response (1). + (qr 0 :type 1) + + ;; A four bit field that specifies kind of query in this + ;; message. This value is set by the originator of a query + ;; and copied into the response. The values are: + ;; + ;; 0 a standard query (QUERY) + ;; 1 an inverse query (IQUERY) + ;; 2 a server status request (STATUS) + ;; 3-15 reserved for future use + (opcode 0 :type 4) ; TODO(tazjin): use define-enum + + ;; Authoritative Answer - this bit is valid in responses, + ;; and specifies that the responding name server is an + ;; authority for the domain name in question section. + (aa nil :type 1) + + ;; TrunCation - specifies that this message was truncated + ;; due to length greater than that permitted on the + ;; transmission channel. + (tc nil :type 1) + + ;; Recursion Desired - this bit may be set in a query and + ;; is copied into the response. If RD is set, it directs + ;; the name server to pursue the query recursively. + ;; Recursive query support is optional. + (rd nil :type 1) + + ;; Recursion Available - this be is set or cleared in a + ;; response, and denotes whether recursive query support is + ;; available in the name server. + (ra nil :type 1) + + ;; Reserved for future use. Must be zero in all queries and + ;; responses. + (z 0 :type 3) + + ;; Response code - this 4 bit field is set as part of + ;; responses. The values have the following + ;; interpretation: + ;; 0 No error condition + ;; 1 Format error - The name server was + ;; unable to interpret the query. + ;; 2 Server failure - The name server was + ;; unable to process this query due to a + ;; problem with the name server. + ;; 3 Name Error - Meaningful only for + ;; responses from an authoritative name + ;; server, this code signifies that the + ;; domain name referenced in the query does + ;; not exist. + ;; 4 Not Implemented - The name server does + ;; not support the requested kind of query. + ;; 5 Refused - The name server refuses to + ;; perform the specified operation for + ;; policy reasons. For example, a name + ;; server may not wish to provide the + ;; information to the particular requester, + ;; or a name server may not wish to perform + ;; a particular operation (e.g., zone + ;; transfer) for particular data. + ;; 6-15 Reserved for future use. + (rcode 0 :type 4) + + ;; an unsigned 16 bit integer specifying the number of + ;; entries in the question section. + (qdcount 0 :type 16) + + ;; an unsigned 16 bit integer specifying the number of + ;; resource records in the answer section. + (ancount 0 :type 16) + + ;; an unsigned 16 bit integer specifying the number of name + ;; server resource records in the authority records + ;; section. + (nscount 0 :type 16) + + ;; an unsigned 16 bit integer specifying the number of + ;; resource records in the additional records section. + (arcount 0 :type 16)) + + +;; Representation of DNS QNAMEs. +;; +;; A QNAME can be either made up entirely of labels, which is +;; basically a list of strings, or be terminated with a pointer to an +;; offset within the original message. + +(deftype qname-field () + '(or + ;; pointer + (unsigned-byte 14) + ;; label + string)) + +(defstruct qname + (start-at 0 :type (unsigned-byte 14)) + (names #() :type (vector qname-field))) + +;; Domain names in questions and resource records are represented as a +;; sequence of labels, where each label consists of a length octet +;; followed by that number of octets. +;; +;; The domain name terminates with the zero length octet for the null +;; label of the root. Note that this field may be an odd number of +;; octets; no padding is used. +(declaim (ftype (function (stream) (values qname integer)) read-qname)) +(defun read-qname (stream) + "Reads a DNS QNAME from STREAM." + + (let ((start-at (file-position stream))) + (iter (for byte next (read-byte stream)) + ;; Each fragment is collected into this byte vector pre-allocated + ;; with the correct size. + (for fragment = (make-array byte :element-type '(unsigned-byte 8) + :fill-pointer 0)) + + ;; If the bit sequence (1 1) is encountered at the beginning of + ;; the fragment, a qname pointer is being read. + (let ((byte-copy byte)) + (when (equal #b11 (lisp-binary/integer:pop-bits 2 8 byte-copy)) + (let ((next (read-byte stream))) + (lisp-binary/integer:push-bits byte-copy 8 next) + (collect next into fragments result-type vector) + (sum 2 into size) + (finish)))) + + ;; Total size is needed, count for each iteration byte, plus its + ;; own value. + (sum (+ 1 byte) into size) + (until (equal byte 0)) + + ;; On each iteration, this will interpret the current byte as an + ;; unsigned integer and read from STREAM an equivalent amount of + ;; times to assemble the current fragment. + ;; + ;; Advancing the stream like this also ensures that the next + ;; iteration occurs on a new fragment or the final terminating + ;; byte. + ;; + ;; TODO(tazjin): Use lisp-binary:read-counted-string. + (dotimes (_ byte (collect (babel:octets-to-string fragment) + into fragments result-type vector)) + (vector-push (read-byte stream) fragment)) + + (finally (return (values (make-qname :start-at start-at + :names fragments) + size)))))) + +(declaim (ftype (function (stream qname)) write-qname)) +(defun write-qname (stream qname) + "Write a DNS qname to STREAM." + + ;; Write each fragment starting with its (byte-) length, followed by + ;; the bytes. + (iter (for fragment in-vector (qname-names qname)) + (for bytes = (babel:string-to-octets fragment)) + (write-byte (length bytes) stream) + (iter (for byte in-vector bytes) + (write-byte byte stream))) + + ;; Always finish off the serialisation with a null-byte! + (write-byte 0 stream)) + +;; 4.1.2. Question section format +(defbinary dns-question (:byte-order :big-endian :export t) + ;; a domain name represented + (qname "" :type (custom :lisp-type qname + :reader #'read-qname + :writer #'write-qname)) + + ;; a two octet code which specifies the type of the query. + ;; The values for this field include all codes valid for a + ;; TYPE field, together with some more general codes which + ;; can match more than one type of RR. + (qtype 0 :type 16) ;; TODO(tazjin): define type after the RR binary + + ;; a two octet code that specifies the class of the query. For + ;; example, the QCLASS field is IN for the Internet. + (qclass 0 :type 16)) ; TODO(tazjin): enum? + +;; 4.1.3. Resource record format + +(define-enum dns-type 2 + (:byte-order :big-endian) + + ;; http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml + (A 1) + (NS 2) + (CNAME 5) + (SOA 6) + (PTR 12) + (MX 15) + (TXT 16) + (SRV 33) + (AAAA 28) + (ANY 255)) ;; (typically wants SOA, MX, NS and MX) + +(defbinary dns-rr (:byte-order :big-endian :export t) + (name nil :type (custom :lisp-type qname + :reader #'read-qname + :writer #'write-qname)) + + ;; two octets containing one of the RR type codes. This + ;; field specifies the meaning of the data in the RDATA + ;; field. + (type 0 :type dns-type) + + ;; two octets which specify the class of the data in the + ;; RDATA field. + (class 0 :type 16) ; TODO(tazjin): enum + + ;; a 32 bit unsigned integer that specifies the time + ;; interval (in seconds) that the resource record may be + ;; cached before it should be discarded. Zero values are + ;; interpreted to mean that the RR can only be used for the + ;; transaction in progress, and should not be cached. + (ttl 0 :type 32) + + ;; an unsigned 16 bit integer that specifies the length in + ;; octets of the RDATA field. + (rdlength 0 :type 16) + + ;; a variable length string of octets that describes the + ;; resource. The format of this information varies + ;; according to the TYPE and CLASS of the resource record. + ;; For example, the if the TYPE is A and the CLASS is IN, + ;; the RDATA field is a 4 octet ARPA Internet address. + (rdata #() :type (eval (case type + ;; TODO(tazjin): Deal with multiple strings in single RRDATA + ((TXT) '(counted-string 1)) + ((A) '(simple-array (unsigned-byte 8) (4))) + (otherwise `(simple-array (unsigned-byte 8) (,rdlength))))))) + +(defbinary dns-message (:byte-order :big-endian :export t) + (header nil :type dns-header) + + ;; the question for the name server + (question #() :type (simple-array dns-question ((dns-header-qdcount header)))) + + ;; ;; RRs answering the question + ;; (answer #() :type (simple-array (unsigned-byte 8) (16))) + (answer #() :type (simple-array dns-rr ((dns-header-ancount header)))) + + ;; ;; ;; RRs pointing toward an authority + (authority #() :type (simple-array dns-rr ((dns-header-nscount header)))) + + ;; ;; RRs holding additional information + (additional #() :type (simple-array dns-rr ((dns-header-arcount header))))) diff --git a/lisp/dns/package.lisp b/lisp/dns/package.lisp new file mode 100644 index 000000000..639d9994a --- /dev/null +++ b/lisp/dns/package.lisp @@ -0,0 +1,4 @@ +(defpackage #:dns + (:documentation "Simple DNS resolver in Common Lisp") + (:use #:cl #:iterate #:lisp-binary) + (:export #:lookup-txt #:lookup-mx)) diff --git a/lisp/dns/resolver.lisp b/lisp/dns/resolver.lisp deleted file mode 100644 index 774be525c..000000000 --- a/lisp/dns/resolver.lisp +++ /dev/null @@ -1,29 +0,0 @@ -;; Initial implementation is a simple client for -;; https://developers.google.com/speed/public-dns/docs/doh/json - -(defpackage #:dns - (:documentation "Simple DNS resolver in Common Lisp") - (:use #:cl) - (:export #:lookup-txt #:lookup-mx)) - -(defvar *doh-base-url* "https://dns.google/resolve" - "Base URL of the service providing DNS-over-HTTP(S). Defaults to the - Google-hosted API.") - -(defun lookup-generic (name type) - (multiple-value-bind (body) - (drakma:http-request *doh-base-url* - :decode-content t - :want-stream t - :parameters `(("type" . ,type) - ("name" . ,name) - ("ct" . "application/x-javascript"))) - (cl-json:decode-json body))) - -(defun lookup-txt (name) - "Look up the TXT records at NAME." - (lookup-generic name "TXT")) - -(defun lookup-mx (name) - "Look up the MX records at NAME." - (lookup-generic name "MX"))