diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..9e3e8fbcf0def5ab7d75df3e9d53c1e045fffd69
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,38 @@
+.PHONY: all install check clean
+
+# Tests are run in the order noted here
+tests = state.scm \
+	bv.scm \
+	label.scm \
+	enum.scm
+
+sources = dns.scm \
+	$(shell find dns -type f -name \*.scm)
+
+targets = $(sources:%.scm=obj/%.go)
+
+GUILE = guile
+
+TEST_ENV = GUILE_AUTO_COMPILE=0 GUILE_LOAD_PATH=$(PWD)
+TEST_FLAGS = $(if $(VERBOSE),--verbose) --coverage coverage.info
+PREFIX = /usr
+
+GUILE_CFLAGS = -O2 --warn=unused-variable,unused-toplevel,shadowed-toplevel,unbound-variable,macro-use-before-definition,arity-mismatch,duplicate-case-datum,bad-case-datum,format
+
+obj/%.go: %.scm
+	guild compile -L $(PWD) -o $@ $<
+
+all: $(targets) doc/guile-dns.info
+
+install: all
+	install -m644 -D -t $(DESTDIR)/$(PREFIX)/share doc/guile-dns.info
+
+check: all
+	env $(TEST_ENV) $(GUILE) --debug -e main -s run-tests.scm $(TEST_FLAGS) $(tests)
+
+coverage/index.html: coverage.info
+	genhtml --output-directory coverage $<
+
+clean:
+	-rm -r coverage
+	-rm -r obj
diff --git a/README b/README
new file mode 100644
index 0000000000000000000000000000000000000000..aeff5def092bb97893ff62baf7bb342bd6a18475
--- /dev/null
+++ b/README
@@ -0,0 +1,3 @@
+See RFC1035 for a description of DNS
+See doc/guile-dns.info for information about the library
+See main.scm for example usage.
diff --git a/TODO.org b/TODO.org
new file mode 100644
index 0000000000000000000000000000000000000000..d904ee009712e7eed8a101f83152cd7987aa44f1
--- /dev/null
+++ b/TODO.org
@@ -0,0 +1,9 @@
+** TODO Write rr-data->bytes
+** TODO Extend define-record-type to generate type validators
+** TODO Standardized names for rcode types?
+Currently the following are used, but are there more standardized names?
+- NOERROR :: 0
+- NXDOMAIN :: 1
+- SERVFIL :: 2
+** TODO [#A] Install SCM and GO files
+In the Makefile
diff --git a/dns.scm b/dns.scm
index 9ffa3f8c1848d667f220b12e439804f9c7d0390b..0c48130fa2505de7379e66339d74c9037f80931d 100644
--- a/dns.scm
+++ b/dns.scm
@@ -1,490 +1,15 @@
 (define-module (dns)
   :use-module (rnrs base)
-  :use-module (rnrs lists)
   :use-module (rnrs bytevectors)
   ;; :use-module (rnrs records syntactic)
-  :use-module (rnrs io ports)
   :use-module (ice-9 curried-definitions)
   :use-module (srfi srfi-88)
   :use-module (srfi srfi-9 gnu)
-  :use-module (object)
-  :use-module (util)
-  :export ()
-  )
-
-;; See RFC1035 for most stuff
-
-;; TODO add type annotations to define-record-type, and generate code
-;; for validating them.
-
-
-(define (flip-cons c)
-  (cons (cdr c) (car c)))
-
-(define (make-mappings table)
-  (values
-   (lambda (i)
-     (cond ((assv i (map flip-cons table)) => cdr)
-           (else i)))
-
-   (lambda (value)
-     (cond ((number? value) value)
-           ((assv value table) => cdr)
-           (else (scm-error 'wrong-type-arg "<something>->int"
-                            "Unknown value ~s"
-                            (list value) #f)))  )))
-(define rr-types
-  '((A     . 1)
-    (NS    . 2)
-    (MD    . 3)
-    (MF    . 4)
-    (CNAME . 5)
-    (SOA   . 6)
-    (MB    . 7)
-    (MG    . 8)
-    (MR    . 9)
-    (NULL  . 10)
-    (WKS   . 11)
-    (PTR   . 12)
-    (HINFO . 13)
-    (MINFO . 14)
-    (MX    . 15)
-    (TXT   . 16)
-    ;; I don't know where this is specified
-    (AAAA  . 28)))
-
-(define-values (int->rr rr->int)
-  (make-mappings rr-types))
-
-(define class-types
-  '((IN . 1)
-    (CS . 2)
-    (CH . 3)
-    (HS . 4)))
-
-(define-values (int->class class->int)
-  (make-mappings class-types))
-
-;; TODO what are the standard names?
-(define rcode-types
-  '((NOERROR . 0) ; no error condition
-    (NXDOMAIN . 1) ; format error - name server can't interpret
-    (SERVFAIL . 2) ; server failure
-    ;; 3 ; name error
-    ))
-
-(define-values (int->rcode rcode->int)
-               (make-mappings rcode-types))
-
-
-(define opcode-types
-  '((QUERY  . 0)
-    (IQUERY . 1)
-    (STATUS . 2)))
-
-(define-values (int->opcode opcode->int)
-               (make-mappings opcode-types))
-
-(define-record-type dns-question
-  (fields name type (class default: 'IN)))
-
-(define ((u8 bv) ptr)
-  (values (bytevector-u8-ref bv ptr)
-          (1+ ptr)))
-
-(define* ((u16 bv optional: (e (endianness big))) ptr)
-  (values (bytevector-u16-ref bv ptr e)
-          (+ ptr 2)))
-
-(define* ((u32 bv optional: (e (endianness big))) ptr)
-  (values (bytevector-u32-ref bv ptr e)
-          (+ ptr 4)))
-
-
-(define ((bv-get bv len) ptr)
-  (define ret (make-bytevector len))
-  (bytevector-copy! bv ptr
-                    ret 0
-                    len)
-  (values ret (+ ptr len)))
-
-(define (uint-get bv len)
-  (do vec <- (bv-get bv len)
-      (return (bytevector-uint-ref vec 0 (endianness big) len))))
-
-(define-record-type dns-header
-  (fields (id default: 0)
-          ;; flags
-          qr (opcode default: 'QUERY)
-          aa tc rd ra
-          (z default: 0)
-          (rcode default: 'NOERROR)
-          ;; lengths
-          (qdcount default: 0) (ancount default: 0)
-          (nscount default: 0) (arcount default: 0)))
-
-#;
-(set-record-type-printer!
-  dns-header
-  (lambda (record port)
-    (format port "#<dns-header id: ~a, qr: ~a, ~s, rcode: ~aqd: ~a, an: ~a, ns: ~a, ar: ~a>"
-            (dns-header-id record)
-            (dns-header-qr record)
-            (dns-header-opcode record)
-            (string-append
-              (if (dns-header-aa record) "AA, " "")
-              (if (dns-header-tc record) "TC, " "")
-              (if (dns-header-rd record) "RD, " "")
-              (if (dns-header-ra record) "RA, " ""))
-            (dns-header-qdcount record)
-            (dns-header-ancount record)
-            (dns-header-nscount record)
-            (dns-header-arcount record))))
-
-
-#;
-(define* (make-dns-header*
-           key:
-           (id 0)
-           qr
-           (opcode 0) ; TODO symbols for this
-           aa
-           tc
-           rd
-           ra
-           (z 0)
-           (rcode 0)
-           (qdcount 0)
-           (ancount 0)
-           (nscount 0)
-           (arcount 0))
-         (make-dns-header id qr opcode aa tc rd ra z rcode
-                          qdcount ancount nscount arcount))
-
-
-(define (encode-dns-header-flags msg)
-  (logior
-    (if (qr msg) (ash 1 15) 0)
-    (cond ((opcode msg) => (lambda (op) (ash (opcode->int op) 11))) (else 0))
-    (if (aa msg) (ash 1 10) 0)
-    (if (tc msg) (ash 1 9) 0)
-    (if (rd msg) (ash 1 8) 0)
-    (if (ra msg) (ash 1 7) 0)
-    (cond ((z msg) => (lambda (z) (ash z 4))) (else 0))
-    (cond ((rcode msg) => rcode->int) (else 0))))
-
-(define (decode-dns-header-flags u16)
-  (define (byte x)
-    (not (zero? (logand u16 (ash 1 x)))))
-  (list
-    (byte 15)
-    (logand #xF (ash u16 -11))
-    (byte 10)
-    (byte 9)
-    (byte 8)
-    (byte 7)
-    (logand 7 (ash u16 -4))
-    (logand #xF u16)))
-
-(define* (dns-header->bytes msg optional: (bv (make-bytevector 12)))
-  (define e (endianness big))
-  (bytevector-u16-set! bv 0  (or (id msg) 0) e)
-  (bytevector-u16-set! bv 2  (encode-dns-header-flags msg) (endianness little))
-  (bytevector-u16-set! bv 4  (or (qdcount msg) 0) e)
-  (bytevector-u16-set! bv 6  (or (ancount msg) 0) e)
-  (bytevector-u16-set! bv 8  (or (nscount msg) 0) e)
-  (bytevector-u16-set! bv 10 (or (arcount msg) 0) e)
-  bv)
-
-(define (bytes->dns-header bv)
-  (do
-    id <- (u16 bv)
-    flags <- (<$> decode-dns-header-flags (u16 bv))
-    qd <- (u16 bv)
-    an <- (u16 bv)
-    ns <- (u16 bv)
-    ar <- (u16 bv)
-    ;; TODO
-    (return (make-dns-header
-             id: id
-             qr:     (list-ref flags 0)
-             opcode: (int->opcode (list-ref flags 1))
-             aa:     (list-ref flags 2)
-             tc:     (list-ref flags 3)
-             rd:     (list-ref flags 4)
-             ra:     (list-ref flags 5)
-             z:      (list-ref flags 6)
-             rcode:  (int->rcode (list-ref flags 7))
-             qdcount: qd
-             ancount: an
-             nscount: ns
-             arcount: ar))))
-
-(define (string-list->labels lst*)
-  (define trans (native-transcoder))
-  (define lst
-    (let ((last (last-pair lst*)))
-      (cond ((null? last) '(""))
-            ((string-null? (car last)) lst*)
-            (else (append lst* '(""))))))
-  (call-with-values (lambda () (open-bytevector-output-port))
-                    (lambda (port get-bytevector)
-                      (for-each (lambda (s)
-                                  (define bv (string->bytevector s trans))
-                                  (put-u8 port (bytevector-length bv))
-                                  (put-bytevector port bv))
-                                lst)
-                      (get-bytevector))))
-
-(define (label-pointer? byte)
-  (= 3 (ash byte -6)))
-
-(define (labels->string-list bv)
-  (do len <- (u8 bv)
-    (cond ((label-pointer? len)
-           (do
-             (mod 1-) ; unreads first char
-             ptr* <- (u16 bv)
-             old-ptr <- (get)
-             (put (logand ptr* #x1FFF))
-             str-list <- (labels->string-list bv)
-             (put old-ptr)
-             (return str-list)))
-          ((zero? len) (return '()))
-          (else
-            (do
-              this <- (bv-get bv len)
-              rest <- (labels->string-list bv)
-              (return
-                (cons (bytevector->string this (native-transcoder))
-                      rest)))))))
-
-#;
-(set-record-type-printer!
-  dns-question
-  (lambda (record port)
-    (format port "<dns-question name: ~s, type: ~a, class: ~a>"
-            (dns-question-name record)
-            (dns-question-type record)
-            (dns-question-class record))))
-
-(define (dns-question->bytes q)
-  (define bv-base (-> q
-                      name
-                      (string-split #\.)
-                      string-list->labels))
-  (define bv (make-bytevector (+ 4 (bytevector-length bv-base))))
-  (bytevector-copy! bv-base 0 bv 0 (bytevector-length bv-base))
-  (bytevector-u16-set! bv (+ 0 (bytevector-length bv-base))
-                       (rr->int (type q))
-                       (endianness big))
-  (bytevector-u16-set! bv (+ 2 (bytevector-length bv-base))
-                       (class->int (class q)) (endianness big))
-  bv)
-
-(define (bytes->dns-question bv)
-  (do
-    name <- (labels->string-list bv)
-    type <- (<$> int->rr (u16 bv))
-    class <- (<$> int->class (u16 bv))
-    (return (make-dns-question name: (string-join name ".") type: type class: class))))
-
-(define-record-type dns-rr-data
-  (fields name type (class default: 'IN) ttl rdata))
-
-#;
-(set-record-type-printer!
-  dns-rr-data
-  (lambda (record port)
-    (format port "#<dns-rr-data name: ~s, type: ~a, class: ~a, ttl: ~a, rdata: ~s>"
-            (dns-rr-data-name record)
-            (dns-rr-data-type record)
-            (dns-rr-data-class record)
-            (dns-rr-data-ttl record)
-            ;; TODO interpret dependin on type
-            (dns-rr-data-rdata record))))
-
-;; (define (dns-rr-data->bytes data)
-;;   (string-list->labels (dns-rr-data-name data))
-;;   (rr->int (dns-rr-data-type rr))
-;;   1 ; class = IN
-;;   u
-;;   )
-
-(define (domain-join lst)
-  (string-join lst "."))
-
-(define (bytes->rr-data bv)
-  (do
-    name <- (<$> domain-join (labels->string-list bv))
-    type <- (<$> int->rr (u16 bv))
-    class <- (<$> int->class (u16 bv))
-    ttl <- (u32 bv)
-    rdlength <- (u16 bv)
-    rdata <- (case type
-               ((A) (<$> (lambda (n) (inet-ntop AF_INET n)) (u32 bv)))
-               ((AAAA) (<$> (lambda (n) (inet-ntop AF_INET6 n))
-                            (uint-get bv rdlength)))
-               ((NS CNAME PTR TXT) (do labels <- (labels->string-list bv)
-                                       (return (string-join labels "."))))
-               ((SOA)
-                (do
-                    mname <- (<$> domain-join (labels->string-list bv))
-                    rname <- (<$> domain-join (labels->string-list bv))
-                    serial <- (u32 bv)
-                    refresh <- (u32 bv)
-                    retry <- (u32 bv)
-                    expire <- (u32 bv)
-                    nttl <- (u32 bv)
-                    (return (list mname rname serial refresh retry expire nttl))))
-               ((HINFO)
-                ;; TODO replace with lift
-                (do
-                  cpu <- (<$> domain-join (labels->string-list bv))
-                  os <-  (<$> domain-join (labels->string-list bv))
-                  (return (list cpu os))))
-               ((MX)
-                (do
-                  priority <- (u16 bv)
-                  exchange <- (<$> domain-join (labels->string-list bv))
-                  (return (list priority exchange))))
-               (else (bv-get bv rdlength)))
-    (return (make-dns-rr-data name: name type: type class: class
-                              ttl: ttl rdata: rdata))))
-
-(define-record-type dns-message
-  (fields header
-          (questions default: '())
-          (answers default: '())
-          (authorities default: '())
-          (additionals default: '())))
-
-#;
-(set-record-type-printer!
-  dns-message
-  (lambda (record port)
-    (format port "#<dns-message header: ~s, qs: ~s, an: ~s, ns: ~s, ar: ~s> "
-            (dns-message-header record)
-            (dns-message-questions record)
-            (dns-message-answers record)
-            (dns-message-authorities record)
-            (dns-message-additionals record)
-            )))
-
-(define (dns-message->bytes msg)
-  (define bvs
-    (cons (dns-header->bytes (header msg))
-          (map dns-question->bytes (questions msg))))
-  (define bv (make-bytevector (apply + (map bytevector-length bvs))))
-  (fold-left (lambda (off v)
-               (bytevector-copy! v 0 bv off (bytevector-length v))
-               (bytevector-length v))
-             0
-             bvs)
-  bv)
-
-(define (bytes->dns-message bv)
-  (do
-    header <- (bytes->dns-header bv)
-    qs <- (sequence
-            (repeat (qdcount header)
-                    (bytes->dns-question bv)))
-    an <- (sequence
-            (repeat (ancount header)
-                    (bytes->rr-data bv)))
-    ns <- (sequence
-            (repeat (nscount header)
-                    (bytes->rr-data bv)))
-    ar <- (sequence
-            (repeat (arcount header)
-                    (bytes->rr-data bv)))
-    (return (make-dns-message header: header questions: qs answers: an
-                              authorities: ns additionals: ar))))
-
-
-(define serial-id 100)
-
-(define (sample-message)
-  (make-dns-message
-    header: (make-dns-header id: serial-id rd: #f qdcount: 1)
-    questions: (list (make-dns-question name: "www.lysator.liu.se." type: 'A))))
-
-(define (setup-sock addr)
-  (define ai (car (getaddrinfo addr "domain" 0 0 SOCK_DGRAM)))
-  (define sock (socket (addrinfo:fam ai)
-                       (addrinfo:socktype ai)
-                       (addrinfo:protocol ai)))
-  (connect sock (addrinfo:addr ai))
-  sock)
-
-(define (run addr msg)
-  (define sock (setup-sock addr))
-  (define resp (make-bytevector 512))
-  (send sock (dns-message->bytes msg))
-  (recv! sock resp)
-  resp)
-
-;; (define (format-bytevector bv)
-;;   (format #f "~{~2,'0x~^ ~}" (bytevector->u8-list bv)))
-
-;; (define (format-question record)
-;;   (format #t "; ~s      ~a  ~a~%"
-;;           (dns-question-name record)
-;;           (dns-question-type record)
-;;           (dns-question-class record)
-
-;;           ))
-
-;; (define (format-rr record)
-;;   (format #t "~s    ~a  ~a  ~a  ~a~%"
-;;             (dns-rr-data-name record)
-;;             (dns-rr-data-ttl record)
-;;             (dns-rr-data-class record)
-;;             (dns-rr-data-type record)
-;;             ;; TODO interpret dependin on type
-;;             (dns-rr-data-rdata record)))
-
-;; (define (format-dns-message msg)
-;;   ;; (define head (dns-message-header msg))
-;;   (format #t ";; Questions~%")
-;;   (for-each format-question (dns-message-questions msg))
-;;   (format #t ";; Answers~%")
-;;   (for-each format-rr (dns-message-answers msg))
-;;   (format #t ";; Authorities~%")
-;;   (for-each format-rr (dns-message-authorities msg))
-;;   (format #t ";; Additionals~%")
-;;   (for-each format-rr (dns-message-additionals msg)))
-
-
-;; https://datatracker.ietf.org/doc/html/draft-bortzmeyer-dns-json
-;; for dns->json mapping
-
-(use-modules (ice-9 match)
-             (ice-9 getopt-long))
-
-(define option-spec
-  '((server (value #t))
-    (recurse (value #f))
-    (norecurse (value #f))))
-
-(define-public (main args)
-  (define options (getopt-long args option-spec
-                               stop-at-first-non-option: #f))
-  (define msg
-    (let loop ((rest (option-ref options '() '())))
-      (match rest
-        ((record type)
-         (make-dns-message
-          header: (make-dns-header id: serial-id rd: #f qdcount: 1)
-          questions: (list (make-dns-question name: record
-                                              type: (string->symbol type)))))
-        ((record) (loop (list record "A")))
-        (() (exit 1)))))
-  (define addr
-    (or (option-ref options 'server #f)
-        (car ((@ (resolvconf) resolvconf)))))
-  ((@ (ice-9 format) format) #t "~y" (serialize msg))
-  ((@ (ice-9 format) format) #t "~y"
-   (serialize
-    (call-with-values (lambda () ((bytes->dns-message (run addr msg)) 0))
-      (lambda (resp _) resp)))))
+  :use-module (dns util)
+  :use-module (dns internal object)
+  :use-module (dns internal util)
+  :use-module (dns internal state-monad)
+  :use-module (dns internal bv)
+  :use-module (dns enum)
+  :use-module (dns label)
+  :export ())
diff --git a/dns/enum.scm b/dns/enum.scm
new file mode 100644
index 0000000000000000000000000000000000000000..18ce9ec561e90a552e1981f51f228f1bdc6dae2c
--- /dev/null
+++ b/dns/enum.scm
@@ -0,0 +1,79 @@
+(define-module (dns enum)
+  :export (rr-types
+           int->rr rr->int
+           class-types
+           int->class class->int
+           rcode-types
+           int->rcode rcode->int
+           opcode-types
+           int->opcode opcode->int))
+
+(define (flip-cons c)
+  (cons (cdr c) (car c)))
+
+(define (make-mappings table)
+  (values
+   (lambda (i)
+     (cond ((assv i (map flip-cons table)) => cdr)
+           ((integer? i) (inexact->exact i))
+           (else (scm-error 'wrong-type-arg "int-><something>"
+                            "Unknown value ~s, expected known integer or one of ~a"
+                            (list i (map car table)) (list (map car table))))))
+
+   (lambda (value)
+     (cond ((integer? value) (inexact->exact value))
+           ((assv value table) => cdr)
+           (else (scm-error 'wrong-type-arg "<something>->int"
+                            "Unknown value ~s"
+                            (list value) #f)))  )))
+
+(define rr-types
+  '((A     . 1)
+    (NS    . 2)
+    (MD    . 3)
+    (MF    . 4)
+    (CNAME . 5)
+    (SOA   . 6)
+    (MB    . 7)
+    (MG    . 8)
+    (MR    . 9)
+    (NULL  . 10)
+    (WKS   . 11)
+    (PTR   . 12)
+    (HINFO . 13)
+    (MINFO . 14)
+    (MX    . 15)
+    (TXT   . 16)
+    ;; I don't know where this is specified
+    (AAAA  . 28)))
+
+(define-values (int->rr rr->int)
+  (make-mappings rr-types))
+
+(define class-types
+  '((IN . 1)
+    (CS . 2)
+    (CH . 3)
+    (HS . 4)))
+
+(define-values (int->class class->int)
+  (make-mappings class-types))
+
+(define rcode-types
+  '((NOERROR . 0)  ; no error condition
+    (NXDOMAIN . 1) ; format error - name server can't interpret
+    (SERVFAIL . 2) ; server failure
+    ;; 3 ; name error
+    ))
+
+(define-values (int->rcode rcode->int)
+               (make-mappings rcode-types))
+
+
+(define opcode-types
+  '((QUERY  . 0)
+    (IQUERY . 1)
+    (STATUS . 2)))
+
+(define-values (int->opcode opcode->int)
+               (make-mappings opcode-types))
diff --git a/dns/internal/bv.scm b/dns/internal/bv.scm
new file mode 100644
index 0000000000000000000000000000000000000000..3de20a4db45e7ec909133209c3805bf69efbc2be
--- /dev/null
+++ b/dns/internal/bv.scm
@@ -0,0 +1,50 @@
+;;; Commentary:
+;;; These procedures uses our state monad to read bytes from a bytevector, while
+;;; keeping a seek pointer as the implicit explicit state.
+;;;
+;;; So all use the state as the offset into the vector, and "sets" the state to
+;;; the new offset after the read.
+;;; Code:
+
+(define-module (dns internal bv)
+  :use-module (ice-9 curried-definitions)
+  :use-module (dns internal state-monad)
+  :use-module (rnrs bytevectors)
+  :use-module (srfi srfi-88)
+  :export (u8 u16 u32 bv-get uint-get)
+  )
+
+
+;;; The procedures u8, u16, and u32 reads an unsigned integer of the respective
+;;; width from @var{bv}, and updates the seek pointer appropriately.
+
+
+(define ((u8 bv) ptr)
+  (values (bytevector-u8-ref bv ptr)
+          (1+ ptr)))
+
+;; Endianess is big by default for these, since we are working with network
+;; stuff.
+
+(define* ((u16 bv optional: (e (endianness big))) ptr)
+  (values (bytevector-u16-ref bv ptr e)
+          (+ ptr 2)))
+
+(define* ((u32 bv optional: (e (endianness big))) ptr)
+  (values (bytevector-u32-ref bv ptr e)
+          (+ ptr 4)))
+
+;; Copies @var{len} bytes from @var{bv}, and updates the seek pointer
+;; accordingly.
+(define ((bv-get bv len) ptr)
+  (define ret (make-bytevector len))
+  (bytevector-copy! bv ptr
+                    ret 0
+                    len)
+  (values ret (+ ptr len)))
+
+;; Reads an unsigned integer @var{len} octets wide, and updates the seek
+;; pointer accordingly
+(define (uint-get bv len)
+  (do vec <- (bv-get bv len)
+      (return (bytevector-uint-ref vec 0 (endianness big) len))))
diff --git a/object.scm b/dns/internal/object.scm
similarity index 70%
rename from object.scm
rename to dns/internal/object.scm
index e6885c8570560abf99a9528139c905c111d9d71d..3a1bc8700e0c92426489ca4bad4cbcb4b235f956 100644
--- a/object.scm
+++ b/dns/internal/object.scm
@@ -1,9 +1,23 @@
-(define-module (object)
+;;; Commentary:
+;;; Another definition of define-record-type.
+;;; Aims to be syntastically compatible with (rnrs records syntastic),
+;;; But instead declares the object as a GOOPS object,
+;;; creates a constructor which takes keyword arguments,
+;;; sets a propper write method,
+;;; and names all its getters directly be their fields
+;;; Code:
+
+(define-module (dns internal object)
   :use-module (rnrs base)
   :use-module (oop goops)
-  :use-module (util)
-  :export (define-record-type serialize))
+  :use-module (dns internal util)
+  :use-module (srfi srfi-88)
+  ;; Define-record-type exported and not #:replace:d, since we want a warning if
+  ;; multiple instances of it is imported at once.
+  :export (define-record-type))
 
+;;; Internal method ONLY used for `write'.
+(define-generic serialize)
 
 (define-method (serialize (this <top>))
   this)
@@ -19,9 +33,11 @@
 
 (define-method (serialize (this <hashtable>))
   (hash-map->list (lambda (key value)
-                    (cons key (serialize value)))
+                    (list key (serialize value)))
                   this))
 
+;; Helper procedure for define-record-type macro, which if @var{stx} is a syntax
+;; object of a list takes the first argument of it, keeping it a syntax object
 (define (unlist stx)
   (if (list? (syntax->datum stx))
       (->> stx syntax->datum car (datum->syntax stx))
@@ -71,6 +87,10 @@
                      (map unlist #'(field ...)))
              (define-method (serialize (this name))
                `(#,@(map (lambda (name)
-                           #`(#,name . ,(serialize (#,name this))))
+                           #`(#,name ,(#,name this)))
                          (map unlist #'(field ...)))))
+             (define-method (write (this name) port)
+               ((@ (ice-9 format) format) port "#<~a~:{ ~a=~s~}>"
+                (class-name name)
+                (serialize this)))
              ))))))
diff --git a/util.scm b/dns/internal/state-monad.scm
similarity index 55%
rename from util.scm
rename to dns/internal/state-monad.scm
index f2c63071554d251586134dfcf0a6d5d2aebe5ce2..253c88821fb292b5aed0b40f0936834875c2639c 100644
--- a/util.scm
+++ b/dns/internal/state-monad.scm
@@ -1,36 +1,31 @@
-(define-module (util)
+;;; Commentary:
+;;; A state monad similar to (and directly influenced by) the one found in in
+;;; Haskell
+;;; Each procedure can either explicitly take the state as a curried last
+;;; argument, or use the `do' notation, which handles that implicitly.
+;;; Each procedure MUST return two values, where the second value is the state
+;;; value which will be chained.
+;;; Code:
+
+(define-module (dns internal state-monad)
   :use-module (ice-9 curried-definitions)
   :replace (do mod)
-  :export (-> ->>
-              <$> return get put
-              sequence repeat flip))
-
-
-(define-syntax ->
-  (syntax-rules ()
-    [(-> obj) obj]
-    [(-> obj (func args ...) rest ...)
-     (-> (func obj args ...) rest ...)]
-    [(-> obj func rest ...)
-     (-> (func obj) rest ...)]))
-
-(define-syntax ->>
-  (syntax-rules ()
-    ((->> obj)
-     obj)
-    ((->> obj (func args ...) rest ...)
-     (->> (func args ... obj) rest ...))
-    ((->> obj func rest ...)
-     (->> (func obj) rest ...))))
+  :export (<$> return get put sequence))
 
 (define-syntax do
-  (syntax-rules (<-)
+  (syntax-rules (<- let =)
     ((_ a <- b rest ...)
      (lambda (ptr)
        (call-with-values (lambda () (b ptr))
                          (lambda (a next-ptr)
                            ((do rest ...)
                             next-ptr)))))
+    ;; let is unused in the actual code, and fails to expand in some circumstanses.
+    ;; See relevant tests.
+    #;
+    ((_ let a = b rest ...)
+     (let ((a b))
+       (do rest ...)))
     ((_ a)
      (lambda (ptr) (a ptr)))
     ((_ a rest ...)
@@ -40,10 +35,9 @@
                            ((do rest ...)
                             next-ptr)))))))
 
-
 (define (<$> f y)
   (do tmp <- y
-    (return (f tmp))))
+      (return (f tmp))))
 
 (define ((return x) y)
   (values x y))
@@ -51,8 +45,8 @@
 (define ((get) ptr)
   (values ptr ptr))
 
-(define ((put ptr) _)
-  (values #f ptr))
+(define ((put ptr) old-ptr)
+  (values old-ptr ptr))
 
 (define (mod proc)
   (do
@@ -68,10 +62,3 @@
       rest <- (sequence (cdr ms))
       (return (cons fst rest)))))
 
-(define (repeat times proc)
-  (map (lambda _ proc)
-       (iota times)))
-
-(define (flip f)
-  (lambda args
-    (apply f (reverse args))))
diff --git a/dns/internal/util.scm b/dns/internal/util.scm
new file mode 100644
index 0000000000000000000000000000000000000000..3daf0eef97cde176d82e931c86a66919293f1168
--- /dev/null
+++ b/dns/internal/util.scm
@@ -0,0 +1,29 @@
+(define-module (dns internal util)
+  :export (-> ->> repeat flip))
+
+(define-syntax ->
+  (syntax-rules ()
+    [(-> obj) obj]
+    [(-> obj (func args ...) rest ...)
+     (-> (func obj args ...) rest ...)]
+    [(-> obj func rest ...)
+     (-> (func obj) rest ...)]))
+
+(define-syntax ->>
+  (syntax-rules ()
+    ((->> obj)
+     obj)
+    ((->> obj (func args ...) rest ...)
+     (->> (func args ... obj) rest ...))
+    ((->> obj func rest ...)
+     (->> (func obj) rest ...))))
+
+
+
+(define (repeat times proc)
+  (map (lambda _ proc)
+       (iota times)))
+
+(define (flip f)
+  (lambda args
+    (apply f (reverse args))))
diff --git a/dns/label.scm b/dns/label.scm
new file mode 100644
index 0000000000000000000000000000000000000000..d1de47fcd8ef0005b66c6ae4fc99bcfadeb4b801
--- /dev/null
+++ b/dns/label.scm
@@ -0,0 +1,51 @@
+(define-module (dns label)
+  :use-module (rnrs io ports)
+  :use-module (dns internal util)
+  :use-module (dns internal state-monad)
+  :use-module (dns internal bv)
+  :use-module (rnrs bytevectors)
+  :export (string-list->labels labels->string-list
+           ))
+
+;; Encodes the scheme list of strings into a bytevector on the label format
+;; expected by the DNS wire protocol.
+(define (string-list->labels lst*)
+  (define trans (native-transcoder))
+  (define lst
+    (let ((last (last-pair lst*)))
+      (cond ((null? last) '(""))
+            ((string-null? (car last)) lst*)
+            (else (append lst* '(""))))))
+  (call-with-values (lambda () (open-bytevector-output-port))
+                    (lambda (port get-bytevector)
+                      (for-each (lambda (s)
+                                  (define bv (string->bytevector s trans))
+                                  (put-u8 port (bytevector-length bv))
+                                  (put-bytevector port bv))
+                                lst)
+                      (get-bytevector))))
+
+;; Is the byte starting this label a pointer somewhere else?
+(define (label-pointer? byte)
+  (= 3 (ash byte -6)))
+
+(define (labels->string-list bv)
+  (do len <- (u8 bv)
+    (cond ((label-pointer? len)
+           (do
+             (mod 1-) ; unreads first char
+             ptr* <- (u16 bv)
+             old-ptr <- (get)
+             (put (logand ptr* #x1FFF))
+             str-list <- (labels->string-list bv)
+             (put old-ptr)
+             (return str-list)))
+          ((zero? len) (return '()))
+          (else
+            (do
+              this <- (bv-get bv len)
+              rest <- (labels->string-list bv)
+              (return
+                (cons (bytevector->string this (native-transcoder))
+                      rest)))))))
+
diff --git a/dns/types.scm b/dns/types.scm
new file mode 100644
index 0000000000000000000000000000000000000000..e8b9077e9822c4b01cc059c8e030b87549803a97
--- /dev/null
+++ b/dns/types.scm
@@ -0,0 +1,27 @@
+(define-module (dns types)
+  :use-module (oop goops)
+  :export (aa additionals ancount answers arcount authorities class header id name nscount opcode qdcount qr questions ra rcode rd rdata tc ttl type z))
+
+(define-generic aa)
+(define-generic additionals)
+(define-generic ancount)
+(define-generic answers)
+(define-generic arcount)
+(define-generic authorities)
+(define-generic class)
+(define-generic header)
+(define-generic id)
+(define-generic name)
+(define-generic nscount)
+(define-generic opcode)
+(define-generic qdcount)
+(define-generic qr)
+(define-generic questions)
+(define-generic ra)
+(define-generic rcode)
+(define-generic rd)
+(define-generic rdata)
+(define-generic tc)
+(define-generic ttl)
+(define-generic type)
+(define-generic z)
diff --git a/dns/types/header.scm b/dns/types/header.scm
new file mode 100644
index 0000000000000000000000000000000000000000..0e34b52f27a7cca232c31c2ba28da6149a5bd3d6
--- /dev/null
+++ b/dns/types/header.scm
@@ -0,0 +1,90 @@
+(define-module (dns types header)
+  :use-module (srfi srfi-88)
+  :use-module (rnrs bytevectors)
+  :use-module (dns internal util)
+  :use-module (dns internal state-monad)
+  :use-module (dns internal bv)
+  :use-module (dns internal object)
+  :use-module (dns enum)
+  :use-module (dns types)
+  :re-export (id qr opcode aa tc rd ra z rcode qdcount ancount nscount arcount)
+  :export (make-dns-header
+           dns-header?
+
+           dns-header->bytes
+           bytes->dns-header
+
+           encode-dns-header-flags
+           decode-dns-header-flags
+           ))
+
+
+(define-record-type dns-header
+  (fields (id default: 0)               ; integer, u16
+          ;; flags
+          qr                            ; boolean
+          (opcode default: 'QUERY)      ; symbol, on of (@ (dns enum) opcode-types)
+          aa tc rd ra                   ; booleans
+          (z default: 0)                ; Always 0
+          (rcode default: 'NOERROR)     ; symbol v on of (@ (dns enum) rcode-types)
+          ;; lengths, all u16 integers
+          (qdcount default: 0) (ancount default: 0)
+          (nscount default: 0) (arcount default: 0)))
+
+
+(define (encode-dns-header-flags msg)
+  (logior
+    (if (qr msg) (ash 1 15) 0)
+    (cond ((opcode msg) => (lambda (op) (ash (opcode->int op) 11))) (else 0))
+    (if (aa msg) (ash 1 10) 0)
+    (if (tc msg) (ash 1 9) 0)
+    (if (rd msg) (ash 1 8) 0)
+    (if (ra msg) (ash 1 7) 0)
+    (cond ((z msg) => (lambda (z) (ash z 4))) (else 0))
+    (cond ((rcode msg) => rcode->int) (else 0))))
+
+(define (decode-dns-header-flags u16)
+  (define (byte x)
+    (not (zero? (logand u16 (ash 1 x)))))
+  (list
+    (byte 15)                           ; qr
+    (logand #xF (ash u16 -11))          ; opcode
+    (byte 10)                           ; aa
+    (byte 9)                            ; tc
+    (byte 8)                            ; rd
+    (byte 7)                            ; ra
+    (logand 7 (ash u16 -4))             ; z
+    (logand #xF u16)))                  ; rcode
+
+(define* (dns-header->bytes msg optional: (bv (make-bytevector 12)))
+  (define e (endianness big))
+  (bytevector-u16-set! bv 0  (or (id msg) 0) e)
+  (bytevector-u16-set! bv 2  (encode-dns-header-flags msg) e)
+  (bytevector-u16-set! bv 4  (or (qdcount msg) 0) e)
+  (bytevector-u16-set! bv 6  (or (ancount msg) 0) e)
+  (bytevector-u16-set! bv 8  (or (nscount msg) 0) e)
+  (bytevector-u16-set! bv 10 (or (arcount msg) 0) e)
+  bv)
+
+(define (bytes->dns-header bv)
+  (do
+    id <- (u16 bv)
+    flags <- (<$> decode-dns-header-flags (u16 bv))
+    qd <- (u16 bv)
+    an <- (u16 bv)
+    ns <- (u16 bv)
+    ar <- (u16 bv)
+    (return (make-dns-header
+             id: id
+             qr:     (list-ref flags 0)
+             opcode: (int->opcode (list-ref flags 1))
+             aa:     (list-ref flags 2)
+             tc:     (list-ref flags 3)
+             rd:     (list-ref flags 4)
+             ra:     (list-ref flags 5)
+             z:      (list-ref flags 6)
+             rcode:  (int->rcode (list-ref flags 7))
+             qdcount: qd
+             ancount: an
+             nscount: ns
+             arcount: ar))))
diff --git a/dns/types/message.scm b/dns/types/message.scm
new file mode 100644
index 0000000000000000000000000000000000000000..ae4fb0a0bdf54c12ebd21e3d7c94061223cd572f
--- /dev/null
+++ b/dns/types/message.scm
@@ -0,0 +1,57 @@
+(define-module (dns types message)
+  :use-module (srfi srfi-88)
+  :use-module (rnrs lists)
+  :use-module (rnrs bytevectors)
+  :use-module (dns internal util)
+  :use-module (dns internal state-monad)
+  :use-module (dns internal bv)
+  :use-module (dns internal object)
+
+  :use-module (dns types)
+  :use-module (dns types header)
+  :use-module (dns types question)
+  :use-module (dns types rr)
+
+  :re-export (header questions answers authorities additionals)
+  :export (make-dns-message
+           dns-message?
+           bytes->dns-message dns-message->bytes
+           ))
+
+(define-record-type dns-message
+  (fields header                        ; dns-header
+          (questions   default: '())    ; list of dns-question
+          (answers     default: '())    ; list of dns-rr-data
+          (authorities default: '())    ; list of dns-rr-data
+          (additionals default: '())))  ; list of dns-rr-data
+
+
+(define (dns-message->bytes msg)
+  (define bvs
+    (cons (dns-header->bytes (header msg))
+          (map dns-question->bytes (questions msg))))
+  (define bv (make-bytevector (apply + (map bytevector-length bvs))))
+  (fold-left (lambda (off v)
+               (bytevector-copy! v 0 bv off (bytevector-length v))
+               (bytevector-length v))
+             0
+             bvs)
+  bv)
+
+(define (bytes->dns-message bv)
+  (do
+    header <- (bytes->dns-header bv)
+    qs <- (sequence
+            (repeat (qdcount header)
+                    (bytes->dns-question bv)))
+    an <- (sequence
+            (repeat (ancount header)
+                    (bytes->rr-data bv)))
+    ns <- (sequence
+            (repeat (nscount header)
+                    (bytes->rr-data bv)))
+    ar <- (sequence
+            (repeat (arcount header)
+                    (bytes->rr-data bv)))
+    (return (make-dns-message header: header questions: qs answers: an
+                              authorities: ns additionals: ar))))
diff --git a/dns/types/question.scm b/dns/types/question.scm
new file mode 100644
index 0000000000000000000000000000000000000000..83af6be68ced8b03aa01ab74b5ac15022a31bbe9
--- /dev/null
+++ b/dns/types/question.scm
@@ -0,0 +1,45 @@
+(define-module (dns types question)
+  :use-module (srfi srfi-88)
+  :use-module (rnrs bytevectors)
+  :use-module (dns internal util)
+  :use-module (dns internal state-monad)
+  :use-module (dns internal bv)
+  :use-module (dns internal object)
+  :use-module (dns label)
+  :use-module (dns enum)
+  :use-module (dns types)
+  :use-module (dns types rr)
+  :re-export (name type class)
+  :export (make-dns-question
+           dns-question?
+
+           dns-question->bytes
+           bytes->dns-question
+           ))
+
+
+(define-record-type dns-question
+  (fields name ; string
+          type ; symbol, on of (@ (dns enum) rr-types)
+          (class default: 'IN))) ; symbol, on of (@ (dns enum) class-types)
+
+(define (dns-question->bytes q)
+  (define bv-base (-> q
+                      name
+                      (string-split #\.)
+                      string-list->labels))
+  (define bv (make-bytevector (+ 4 (bytevector-length bv-base))))
+  (bytevector-copy! bv-base 0 bv 0 (bytevector-length bv-base))
+  (bytevector-u16-set! bv (+ 0 (bytevector-length bv-base))
+                       (rr->int (type q))
+                       (endianness big))
+  (bytevector-u16-set! bv (+ 2 (bytevector-length bv-base))
+                       (class->int (class q)) (endianness big))
+  bv)
+
+(define (bytes->dns-question bv)
+  (do
+    name <- (labels->string-list bv)
+    type <- (<$> int->rr (u16 bv))
+    class <- (<$> int->class (u16 bv))
+    (return (make-dns-question name: (string-join name ".") type: type class: class))))
diff --git a/dns/types/rr.scm b/dns/types/rr.scm
new file mode 100644
index 0000000000000000000000000000000000000000..ebabe136c97f5102528392f0152396b073bc8cce
--- /dev/null
+++ b/dns/types/rr.scm
@@ -0,0 +1,67 @@
+(define-module (dns types rr)
+  :use-module (srfi srfi-88)
+  :use-module (dns internal util)
+  :use-module (dns internal object)
+  :use-module (dns internal state-monad)
+  :use-module (dns internal bv)
+  :use-module (dns util)
+  :use-module (dns label)
+  :use-module (dns enum)
+  :use-module (dns types)
+  :re-export (name type class ttl rdata)
+  :export (make-dns-rr-data
+           dns-rr-data?
+           bytes->rr-data)
+  )
+
+(define-record-type dns-rr-data
+  (fields name                          ; string
+          type                          ; symbol, one of (@ (dns enum) rr-types)
+          (class default: 'IN)          ; symbol, one of (@ (dns enum) class-types)
+          ttl                           ; integer, 0 ≤ x ≤ 2¹⁶-1
+          rdata))                       ; dependant on type
+
+
+;; (define (dns-rr-data->bytes data)
+;;   (string-list->labels (dns-rr-data-name data))
+;;   (rr->int (dns-rr-data-type rr))
+;;   1 ; class = IN
+;;   u
+;;   )
+
+(define (bytes->rr-data bv)
+  (do
+    name <- (<$> domain-join (labels->string-list bv))
+    type <- (<$> int->rr (u16 bv))
+    class <- (<$> int->class (u16 bv))
+    ttl <- (u32 bv)
+    rdlength <- (u16 bv)
+    rdata <- (case type
+               ((A) (<$> (lambda (n) (inet-ntop AF_INET n)) (u32 bv)))
+               ((AAAA) (<$> (lambda (n) (inet-ntop AF_INET6 n))
+                            (uint-get bv rdlength)))
+               ((NS CNAME PTR TXT) (do labels <- (labels->string-list bv)
+                                       (return (string-join labels "."))))
+               ((SOA)
+                (do
+                    mname <- (<$> domain-join (labels->string-list bv))
+                    rname <- (<$> domain-join (labels->string-list bv))
+                    serial <- (u32 bv)
+                    refresh <- (u32 bv)
+                    retry <- (u32 bv)
+                    expire <- (u32 bv)
+                    nttl <- (u32 bv)
+                    (return (list mname rname serial refresh retry expire nttl))))
+               ((HINFO)
+                (do
+                  cpu <- (<$> domain-join (labels->string-list bv))
+                  os <-  (<$> domain-join (labels->string-list bv))
+                  (return (list cpu os))))
+               ((MX)
+                (do
+                  priority <- (u16 bv)
+                  exchange <- (<$> domain-join (labels->string-list bv))
+                  (return (list priority exchange))))
+               (else (bv-get bv rdlength)))
+    (return (make-dns-rr-data name: name type: type class: class
+                              ttl: ttl rdata: rdata))))
diff --git a/dns/util.scm b/dns/util.scm
new file mode 100644
index 0000000000000000000000000000000000000000..e3b85f3cd0c0832d6c36417b1068064386ee2e38
--- /dev/null
+++ b/dns/util.scm
@@ -0,0 +1,5 @@
+(define-module (dns util)
+  :export (domain-join))
+
+(define (domain-join lst)
+  (string-join lst "."))
diff --git a/doc/guile-dns.texi b/doc/guile-dns.texi
new file mode 100644
index 0000000000000000000000000000000000000000..790d24517c74f9c081661d9b8ec280ba18491c0e
--- /dev/null
+++ b/doc/guile-dns.texi
@@ -0,0 +1,190 @@
+\input texinfo
+@settitle Guile DNS
+
+@copying
+Copyright @copyright{} 2022 Hugo Hörnquist
+@end copying
+
+@dircategory The Algorithmic Language Scheme
+@direntry
+* Guile-DNS: (guile-dns).       A DNS library for Guile
+@end direntry
+
+@titlepage
+@title Guile DNS
+@author Hugo Hörnquist
+
+@page
+@vskip 0pt plus 1filll
+@insertcopying
+@end titlepage
+
+@contents
+
+@ifnottex
+@node Top
+@top Guile DNS
+@end ifnottex
+
+@node Enumerations
+@chapter Enumerations
+
+@defvar rr-types
+Association list from RR-type symbols (such as @code{'A}, @code{'MX},
+and so on, to their numerical equivalents.
+@end defvar
+
+@defun int->rr int
+Translates integers into their respective RR-symbols. Or return the
+given integer if of an unknown type.
+@end defun
+
+@defun rr->int rr
+Translate a given RR symbol into an integer. If given an integer,
+return it directly.
+@end defun
+
+@defvar class-types
+@end defvar
+
+@defun int->class int
+@end defun
+
+@defun class->int class-symbol
+@end defun
+
+@defvar rcode-types
+@end defvar
+
+@defun int->rcode int
+@end defun
+
+@defun rcode->int rcode-symbol
+@end defun
+
+@defvar opcode-types
+@end defvar
+
+@defun int->opcode int
+@end defun
+
+@defun opcode->int opcode-symbol
+@end defun
+
+@node Types
+@chapter Types
+
+@defun make-dns-question #:name name #:type type [#:class='IN]
+@tindex dns-question
+
+@defun dns-question? x
+@end defun
+
+@defun name question
+@end defun
+
+@defun type question
+@end defun
+
+@defun class question
+@end defun
+@end defun
+
+@defun make-dns-header [#:id=0] [#:qr qr] [#:opcode='QUERY] [#:aa aa] [#:tc tc] [#:rd rd] [#:ra ra] [#:z=0] [#:rcode='NOERROR] [#:qdcount=0] [#:ancount=0] [#:nscount=0] [#:arcount=0]
+@tindex dns-header
+
+@defun dns-header? x
+@end defun
+
+@defun id header
+@end defun
+
+@defun qr header
+@end defun
+
+@defun opcode header
+@end defun
+
+@defun aa header
+@end defun
+
+@defun tc header
+@end defun
+
+@defun rd header
+@end defun
+
+@defun ra header
+@end defun
+
+@defun z header
+@end defun
+
+@defun rcode header
+@end defun
+
+@defun qdcount header
+@end defun
+
+@defun ancount header
+@end defun
+
+@defun nscount header
+@end defun
+
+@defun arcount header
+@end defun
+@end defun
+
+
+@defun make-dns-message #:header header-value [#:questions='()] [#:answers='()] [#:authorities='()] [#:additionals='()]
+@tindex dns-message
+Create a new DNS message, usable both as a question and a response.
+
+The header field must be a dns-header object @c TODO link
+which also must have the qdcount (and similar) set correctly by the user.
+
+All the remaining fields should be lists of dns-questions @c TODO
+@c link, and check if that is correct
+
+@defun dns-message? x
+@end defun
+
+
+@defun header msg
+@end defun
+
+@defun questions msg
+@end defun
+
+@defun answers msg
+@end defun
+
+@defun authorities msg
+@end defun
+
+@defun additionals msg
+@end defun
+
+@end defun
+
+
+
+
+@defun dns-message->bytes
+@end defun
+
+@defun bytes->dns-message
+@end defun
+
+
+@node Index
+@unnumbered Index
+@printindex cp
+@printindex fn
+@printindex ky
+@printindex pg
+@printindex tp
+@printindex vr
+
+@bye
diff --git a/main.scm b/main.scm
new file mode 100755
index 0000000000000000000000000000000000000000..fca586660d49ed6dec7f29d4686221e2b3bc2466
--- /dev/null
+++ b/main.scm
@@ -0,0 +1,116 @@
+#!/usr/bin/guile \
+-L . -e main -s
+!#
+
+(add-to-load-path (dirname (current-filename)))
+
+(use-modules (dns)
+
+             (dns types header)
+             (dns types message)
+             (dns types question)
+             (dns types rr)
+
+             (rnrs bytevectors)
+             (ice-9 match)
+             (ice-9 getopt-long)
+             (ice-9 format)
+             (texinfo string-utils))
+
+(define serial-id 100)
+
+(define (setup-sock addr)
+  (define ai (car (getaddrinfo addr "domain" 0 0 SOCK_DGRAM)))
+  (define sock (socket (addrinfo:fam ai)
+                       (addrinfo:socktype ai)
+                       (addrinfo:protocol ai)))
+  (connect sock (addrinfo:addr ai))
+  sock)
+
+(define (run addr msg)
+  (define sock (setup-sock addr))
+  (define resp (make-bytevector 512))
+  (send sock (dns-message->bytes msg))
+  (recv! sock resp)
+  resp)
+
+;; https://datatracker.ietf.org/doc/html/draft-bortzmeyer-dns-json
+;; for dns->json mapping
+
+(define option-spec
+  '((server (value #t))
+    ;; (recurse (value #f))
+    ;; (norecurse (value #f))
+    ))
+
+
+
+
+(define* (format-header header optional: (port #t))
+  (format port ";; ->>HEADER<<- opcode: ~a, status: ~a, id: ~a~%"
+          (opcode header) (rcode header) (id header))
+  (format port ";; flags:~{ ~a~}; QUERY: ~a, ANSWER: ~a, AUTHORITY: ~a, ADDITIONAL: ~a~%"
+          (list (if (aa header) 'aa "")
+                (if (tc header) 'tc "")
+                (if (rd header) 'rd "")
+                (if (ra header) 'ra ""))
+          (qdcount header)
+          (ancount header)
+          (nscount header)
+          (arcount header)))
+
+(define* (format-question question optional: (port (current-output-port)))
+  (display ";" port)
+  (display (left-justify-string (format #f "~a.~/" (name question)) 23) port)
+  (format port "~/~/~a~/~a~%"
+          (class question)
+          (type question)))
+
+(define* (format-record rr optional: (port (current-output-port)))
+  (display (left-justify-string (format #f "~a.~/" (name rr)) 24) port)
+  (format port "~a~/~a~/~a~/~a~%"
+          (ttl rr)
+          (class rr)
+          (type rr)
+          (rdata rr)))
+
+;; Both question and answer should me dns-message objects
+(define* (format-everything question answer optional: (port (current-output-port)))
+  (format-header (header question) port)
+  (format-header (header answer) port)
+  (format port "~%;; QUESTION SECTION:~%")
+  (for-each format-question (questions question))
+  (format port "~%;; ANSWER SECTION:~%")
+  (for-each format-record (answers answer))
+  (format port "~%;; AUTHORITY SECTION:~%")
+  (for-each format-record (authorities answer))
+  (format port "~%;; ADDITIONAL SECTION:~%")
+  (for-each format-record (additionals answer))
+  )
+
+
+
+(define (main args)
+  (define options (getopt-long args option-spec
+                               stop-at-first-non-option: #f))
+  (define msg
+    (let loop ((rest (option-ref options '() '())))
+      (match rest
+        ((record type)
+         (make-dns-message
+          header: (make-dns-header id: serial-id rd: #t qdcount: 1)
+          questions: (list (make-dns-question name: record
+                                              type: (string->symbol type)))))
+        ((record) (loop (list record "A")))
+        (() (exit 1)))))
+
+  (define dns-server-address
+    (or (option-ref options 'server #f)
+        (car ((@ (resolvconf) resolvconf)))))
+
+  ;; (highlight-record (format #f "~s~%" msg))
+  (call-with-values (lambda () ((bytes->dns-message (run dns-server-address msg)) 0))
+    (lambda (resp _) (format-everything msg resp)))
+
+  (newline)
+  (format #t ";; Server: ~a (UDP)~%" dns-server-address))
diff --git a/run b/run
deleted file mode 100755
index d7641e8795a3459b85735efd644c574cb54dd29d..0000000000000000000000000000000000000000
--- a/run
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/guile \
--L . -e main -s
-!#
-(define main (@ (dns) main))
diff --git a/run-tests.scm b/run-tests.scm
new file mode 100644
index 0000000000000000000000000000000000000000..86b61ed027957a8b803cd24afa555f0bcce93b34
--- /dev/null
+++ b/run-tests.scm
@@ -0,0 +1,112 @@
+#!/usr/bin/guile \
+--debug -e main -s
+!#
+
+(use-modules (srfi srfi-64)
+             (srfi srfi-71)
+             (ice-9 getopt-long)
+             (ice-9 format)
+             (ice-9 match)
+             (system vm coverage))
+
+(define (µs x)
+  (* x #e1e6))
+
+(define (transform-time-of-day tod)
+  (+ (* (µs 1) (car tod))
+     (cdr tod)))
+
+(define* (construct-test-runner #:key verbose?)
+  (lambda ()
+    (define runner (test-runner-null))
+    ;; end of individual test case
+    (test-runner-on-test-begin! runner
+      (lambda (runner)
+        (test-runner-aux-value! runner (transform-time-of-day (gettimeofday)))))
+    (test-runner-on-test-end! runner
+      (lambda (runner)
+        (case (test-result-kind runner)
+          ((pass)  (display "\x1b[0;32mX\x1b[m"))
+          ((fail)  (newline) (display "\x1b[0;31mE\x1b[m"))
+          ((xpass) (display "\x1b[0;33mX\x1b[m"))
+          ((xfail) (display "\x1b[0;33mE\x1b[m"))
+          ((skip)  (display "\x1B[0;33m-\x1b[m")))
+        (when (or verbose? (eq? 'fail (test-result-kind)))
+          (format #t " ~a~%" (test-runner-test-name runner)))
+        (when (eq? 'fail (test-result-kind))
+          (cond ((test-result-ref runner 'actual-error)
+                 => (match-lambda
+                      ((err-type proc fmt args data)
+                       (catch #t (lambda () (format #t "~a in ~a: ~?~%" err-type proc fmt args))
+                         (lambda err-err
+                           (format #t "~a~%" err-err)
+                           (format #t "~s ~s ~s ~s ~s~%"
+                                   err-type proc fmt args data))))
+                      (err (format #t "Error: ~s~%" err))))
+                (else
+                 (format #t "Expected: ~s~%Received: ~s~%"
+                         (test-result-ref runner 'expected-value "[UNKNOWN]")
+                         (test-result-ref runner 'actual-value "[UNKNOWN]"))))
+          (format #t "Near ~a:~a~%~y"
+                  (test-result-ref runner 'source-file)
+                  (test-result-ref runner 'source-line)
+                  (test-result-ref runner 'source-form)))
+
+        (let ((start (test-runner-aux-value runner))
+              (end (transform-time-of-day (gettimeofday))))
+          (when (< (µs 1) (- end start))
+            (format #t "~%Slow test: ~s, took ~a~%"
+                    (test-runner-test-name runner)
+                    (exact->inexact (/ (- end start) (µs 1)))
+                    )))))
+
+    ;; on start of group
+    (test-runner-on-group-begin! runner
+      ;; count is number of #f
+      (lambda (runner name count)
+        (format #t "~a ~a ~a~%"
+                (make-string 10 #\=)
+                name
+                (make-string 10 #\=))))
+    (test-runner-on-group-end! runner
+      (lambda (runner) (newline)))
+    ;; after everything else is done
+    (test-runner-on-final! runner
+      (lambda (runner)
+        (format #t "Guile version ~a~%~%" (version))
+        (format #t "pass:  ~a~%" (test-runner-pass-count runner))
+        (format #t "fail:  ~a~%" (test-runner-fail-count runner))
+        (format #t "xpass: ~a~%" (test-runner-xpass-count runner))
+        (format #t "xfail: ~a~%" (test-runner-xfail-count runner))
+        ))
+
+    runner))
+
+
+
+(define (run-test-file test-file)
+  (format #t "Running test in ~a~%" test-file)
+  (load
+   (format #f "~a/tests/~a"
+           (dirname (current-filename))
+           test-file)))
+
+(define option-spec
+  '((verbose (single-char #\v) (value #f))
+    (coverage (value #t))))
+
+(define (main args)
+  (define options (getopt-long args option-spec))
+  (define verbose? (option-ref options 'verbose #f))
+  (define coverage-file )
+  (define tests (option-ref options '() '()))
+  (define run-tests (lambda () (for-each run-test-file tests)))
+  (test-runner-factory (construct-test-runner #:verbose? verbose?))
+  (test-begin "All Tests")
+  (cond ((option-ref options 'coverage #f)
+         => (lambda (file)
+              (let ((coverage _ (with-code-coverage run-tests)))
+                (call-with-output-file file
+                  (lambda (port) (coverage-data->lcov coverage port))))))
+        (else (run-tests)))
+  (test-end))
diff --git a/tests/bv.scm b/tests/bv.scm
new file mode 100644
index 0000000000000000000000000000000000000000..1c6add822653d7299d268d9ac765620b6d16e39a
--- /dev/null
+++ b/tests/bv.scm
@@ -0,0 +1,46 @@
+(use-modules (srfi srfi-64)
+             (srfi srfi-1)
+             (rnrs bytevectors)
+             (rnrs io ports)
+             (dns internal bv))
+
+(test-begin "Bytevectors in state monad")
+
+
+(define bv (string->bytevector "Hello, World"
+                               (make-transcoder (utf-8-codec))))
+
+
+;; Returns in `do' allow arbitrary code to be run for side effects
+;; order is guaranteed to be maintained, since each procedure in a
+;; state monad depends on the procedures before it.
+((do c1 <- (u8 bv)
+     (return (test-equal "Get first u8 from bv" (char->integer #\H) c1))
+     c2 <- (u8 bv)
+     (return (test-equal "Get second u8 from bv" (char->integer #\e) c2))
+     i <- (u16 bv)
+     (return (test-equal "Get u16 (endianess unspecified)"
+               (let ((l (char->integer #\l)))
+                 (logior (ash l 8) l))
+               i))
+     j <- (u32 bv)
+     (return (test-equal "Get u32, and check that endianess is big"
+               (apply logior
+                      (map ash (map char->integer (string->list "o, Wo"))
+                           (reverse (iota 4 0 8))))
+               j))
+     (return #f))
+ 0)
+
+(test-equal "Bv-get"
+  #vu8(2 3)
+  ((bv-get #vu8(1 2 3 4) 2) 1))
+
+
+(test-equal "Uint-get"
+  #x010203 ((uint-get #vu8(1 2 3 4) 3) 0))
+(test-equal "Uint-get, with an offset"
+  #x020304 ((uint-get #vu8(1 2 3 4) 3) 1))
+
+
+(test-end)
diff --git a/tests/enum.scm b/tests/enum.scm
new file mode 100644
index 0000000000000000000000000000000000000000..abd19e1c01a71c0aa02f49ba59661d71d220dcfd
--- /dev/null
+++ b/tests/enum.scm
@@ -0,0 +1,34 @@
+(use-modules (srfi srfi-64)
+             (dns enum))
+
+;;; These tests claim to be testing rr->int, but is really testing make-mappings.
+;;; Explicit data could be checked, but a test for that would just be duplicating the data
+
+(test-assert "rr->int returns a number for a valid symbol" (integer? (rr->int 'A)))
+
+(let ((r (rr->int 10)))
+  (test-assert "rr->int returns an integer given an integer" (integer? r))
+  (test-assert "rr->int returns an exact integer given an exact integer" (exact? r)))
+
+(let ((r (rr->int 10.0)))
+  (test-assert "rr->int returns an integer given an inexact integer" (integer? r))
+  (test-assert "rr->int returns an exact integer an inexact integer" (exact? r)))
+
+(catch 'wrong-type-arg
+  (lambda ()
+    (rr->int 'INVALID-SYMBOL)
+    (test-assert "rr->int didn't fail on invalid symbol" #f))
+  (lambda _ (test-assert "rr->int fails on an unknown symbol" #t)))
+
+
+(test-equal "Int->rr returns a symbol for a known value" 'A (int->rr 1))
+(test-equal "Int->rr is effectivly `identity' for unknown integers" 100 (int->rr 100))
+(test-equal "Int->rr is effectivly `inexact->exact' for unknown inexact integers" 100 (int->rr 100.0))
+(catch 'wrong-type-arg
+  (lambda ()
+    (int->rr 27.5)
+    (test-assert "Int->rr didn't fail on invalid data" #f))
+  (lambda (err proc fmt args data)
+    (test-assert "Int->rr failed on invalid data" #t)
+    (test-assert "Int->rr returned a list of valid symbols as aux data upon failure"
+      (every symbol? (car data)))))
diff --git a/tests/header.scm b/tests/header.scm
new file mode 100644
index 0000000000000000000000000000000000000000..c4e9b4736d907468a1fead5834c6df52023404ad
--- /dev/null
+++ b/tests/header.scm
@@ -0,0 +1,12 @@
+(use-modules (srfi srfi-64)
+             (srfi srfi-88)
+             (dns types header))
+
+(test-equal 256 (encode-dns-header-flags (make-dns-header rd: #t)))
+
+(test-equal '(#f 0 #f #f #t #f 0 0)
+  (decode-dns-header-flags 256))
+
+;; TODO more tests here
+;;
+;; TODO also test all the something->bytevector and back procedures
diff --git a/tests/label.scm b/tests/label.scm
new file mode 100644
index 0000000000000000000000000000000000000000..490b24995f1ccadfb1efbf5046c35d00490908d9
--- /dev/null
+++ b/tests/label.scm
@@ -0,0 +1,46 @@
+(use-modules (srfi srfi-64)
+             (rnrs io ports)
+             (dns label))
+
+(define codec (make-transcoder (latin-1-codec)))
+
+(test-begin "DNS labels")
+
+(test-equal "Empty list to null label"
+  #vu8(0)
+  (string-list->labels '()))
+
+(test-equal "Empty label set gives empty list"
+  '() ((labels->string-list #vu8(0)) 0))
+
+(let* ((bv (string->bytevector "\x05Hello\0" codec)))
+  (test-equal "Single string to label"
+    bv (string-list->labels (list "Hello")))
+  (test-equal "Single label to strirng"
+    '("Hello")
+    ((labels->string-list bv) 0)))
+
+(let* ((bv (string->bytevector "\x05Hello\x04Test\0" codec)))
+  (test-equal "Actual list to labels"
+    bv (string-list->labels (list "Hello" "Test")))
+  (test-equal "Multiple labels to list"
+    '("Hello" "Test")
+    ((labels->string-list bv) 0)))
+
+(let* ((bv (string->bytevector "\x05Hello\x05Hello\x05Hello\0" codec)))
+  (test-equal "Test that pointers are still not implented"
+    bv (string-list->labels (list "Hello" "Hello" "Hello"))))
+
+(let* ((bv (string->bytevector "\xC0\x02\x05Hello\0" codec)))
+  (test-equal "Test simple label pointer"
+    '("Hello")
+    ((labels->string-list bv) 0)))
+
+(let* ((bv (string->bytevector "\x05Hello\0\xC0\x00" codec)))
+  (test-equal "Test pointer to label before start."
+      '("Hello")
+      ((labels->string-list bv)
+       7 ; Index of \xC0
+       )))
+
+(test-end)
diff --git a/tests/state.scm b/tests/state.scm
new file mode 100644
index 0000000000000000000000000000000000000000..a29bf0bda0142f1c5b684c6b7556db6d51da4b8f
--- /dev/null
+++ b/tests/state.scm
@@ -0,0 +1,83 @@
+(use-modules (srfi srfi-64)
+             (dns internal state-monad))
+
+(test-begin "State Monad")
+
+(call-with-values (lambda () ((return 1) 2))
+  (lambda (value state)
+    (test-equal "Return returns the value unmodified" 1 value)
+    (test-equal "Return also returns the state as a second value" 2 state)))
+
+(test-equal "Get returns the current state as primary value, while kepping the state"
+  '(state state)
+  (call-with-values (lambda () ((get) 'state)) list))
+
+;; Return value of put untested, since it's undefined
+(test-equal "Put replaces the old state with a new one, and return old one"
+  '(old-state new-state)
+  (call-with-values (lambda () ((put 'new-state) 'old-state))
+    list))
+
+(test-equal "A simple do is effectively a `values' call"
+  '(value initial-state)
+  (call-with-values (lambda () ((do (return 'value)) 'initial-state))
+    list))
+
+;; Something with the let "statement" in the do notation causes the macro
+;; expander to fail with the following error in some cases:
+
+;; $ env GUILE_AUTO_COMPILE=1 GUILE_LOAD_PATH=/home/hugo/code/guile-dns guile -e main -s run-tests.scm  --coverage coverage.info state.scm bv.scm label.scm
+;; ice-9/psyntax.scm:1585:32: In procedure expand-macro:
+;; Syntax error:
+;; tests/state.scm:28:31: source expression failed to match any pattern in form (let ptr)
+
+#;
+(test-equal "Let statement in do"
+  '(10 state)
+  (call-with-values (lambda () ((do let x = 10
+                               (return x))
+                           'state))
+    list))
+
+
+(test-equal "Set and get through do, along with <- in do."
+  '(5 1)
+  (call-with-values (lambda () ((do old <- (get)
+                               (put (1+ old))
+                               (return 5))
+                           0))
+    list))
+
+
+
+(test-equal "<$> Updates stuff before being removed from the monad context"
+    '(11 10)
+ (call-with-values (lambda ()
+                     ((do x <- (<$> 1+ (get))
+                          (return x))
+                      10))
+   list))
+
+(test-equal "Sequence should update the state accordingly"
+  3
+  (call-with-values
+      (lambda ()
+        ((sequence
+           (list (mod 1+)
+                 (mod 1+)
+                 (mod 1+)))
+         0))
+    (lambda (_ st) st)))
+
+(test-equal "Sequence should also act as map on the primary value"
+  '((0 1 2) 3)
+  (call-with-values
+      (lambda ()
+        ((sequence
+           (list (mod 1+)
+                 (mod 1+)
+                 (mod 1+)))
+         0))
+    list))
+
+(test-end "State Monad")