From 6fa6b2d1f6867f14708149b0ef95efbaf40c15b7 Mon Sep 17 00:00:00 2001
From: Sam <sam@samtherapy.net>
Date: Sat, 10 Sep 2022 18:41:25 +0000
Subject: [PATCH] feat: Allow opt pseudosection to be printed (#91)

...when not using the standard print
(JSON/XML/YAML)

This should make making an API for this even easier :)

Reviewed-on: https://git.froth.zone/sam/awl/pulls/91
Reviewed-by: grumbulon <grumbulon@grumbulon.xyz>
---
 conf/plan9.go         |   1 +
 conf/wasm.go          |  18 ++++++
 go.mod                |   2 +-
 go.sum                |   2 +
 main.go               |   1 +
 query/general_test.go |   3 -
 query/print.go        | 129 +++++++++++++++++++++++++++++++++++++++++-
 query/query_test.go   |   6 +-
 query/struct.go       |  27 ++++++---
 util/options.go       |   2 +-
 10 files changed, 175 insertions(+), 16 deletions(-)
 create mode 100644 conf/wasm.go

diff --git a/conf/plan9.go b/conf/plan9.go
index 350394c..0b19102 100644
--- a/conf/plan9.go
+++ b/conf/plan9.go
@@ -13,6 +13,7 @@ import (
 )
 
 // GetDNSConfig gets DNS information from Plan 9, because it's different from UNIX and Windows.
+//
 // Plan 9 stores its network data in /net/ndb, which seems to be formatted a specific way
 // Yoink it and use it.
 //
diff --git a/conf/wasm.go b/conf/wasm.go
new file mode 100644
index 0000000..3deeef0
--- /dev/null
+++ b/conf/wasm.go
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: BSD-3-Clause
+//go:build js
+
+package conf
+
+import (
+	"errors"
+
+	"github.com/miekg/dns"
+)
+
+// GetDNSConfig doesn't do anything, because it is impossible (and bad security)
+// if it could, as that is the definition of a DNS leak.
+func GetDNSConfig() (*dns.ClientConfig, error) {
+	return nil, errNotImplemented
+}
+
+var errNotImplemented = errors.New("not implemented")
diff --git a/go.mod b/go.mod
index be3137f..9f7fcb1 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/google/go-cmp v0.5.8 // indirect
 	github.com/kr/pretty v0.3.0 // indirect
 	github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
-	golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect
+	golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 // indirect
 )
 
 require (
diff --git a/go.sum b/go.sum
index 7944a46..f7afb59 100644
--- a/go.sum
+++ b/go.sum
@@ -81,6 +81,8 @@ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5
 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
 golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
diff --git a/main.go b/main.go
index bf737af..641deac 100644
--- a/main.go
+++ b/main.go
@@ -1,4 +1,5 @@
 // SPDX-License-Identifier: BSD-3-Clause
+//go:build !js
 
 package main
 
diff --git a/query/general_test.go b/query/general_test.go
index 9243a7a..312cf60 100644
--- a/query/general_test.go
+++ b/query/general_test.go
@@ -36,10 +36,7 @@ func TestResolve(t *testing.T) {
 	assert.ErrorContains(t, err, "timeout")
 }
 
-// FIXME: See when this is fixed and fix it.
-
 func TestTruncate(t *testing.T) {
-	t.Skip("Sadly this is broken.")
 	t.Parallel()
 
 	opts := util.Options{
diff --git a/query/print.go b/query/print.go
index 0c81761..5eaa6bf 100644
--- a/query/print.go
+++ b/query/print.go
@@ -3,6 +3,7 @@
 package query
 
 import (
+	"encoding/hex"
 	"encoding/json"
 	"encoding/xml"
 	"errors"
@@ -178,7 +179,7 @@ func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) {
 				name = additional.Header().Name
 			}
 
-			ret.Extra = append(ret.Extra, Answer{
+			ret.Additional = append(ret.Additional, Answer{
 				RRHeader: RRHeader{
 					Name:     name,
 					Type:     dns.TypeToString[additional.Header().Rrtype],
@@ -191,7 +192,133 @@ func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) {
 		}
 	}
 
+	opt := msg.IsEdns0()
+	if opt != nil && opts.Display.Opt {
+		ret.Opt, err = parseOpt(*opt)
+		if err != nil {
+			return nil, fmt.Errorf("edns print: %w", err)
+		}
+	}
+
 	return &ret, nil
 }
 
+func parseOpt(rr dns.OPT) ([]Opts, error) {
+	ret := []Opts{}
+	// Most of this is taken from https://github.com/miekg/dns/blob/master/edns.go#L76
+
+	ret = append(ret, Opts{
+		Name:  "Version",
+		Value: strconv.Itoa(int(rr.Version())),
+	})
+
+	if rr.Do() {
+		ret = append(ret, Opts{
+			Name:  "Flags",
+			Value: "do",
+		})
+	} else {
+		ret = append(ret, Opts{
+			Name:  "Flags",
+			Value: "",
+		})
+	}
+
+	if rr.Hdr.Ttl&0x7FFF != 0 {
+		ret = append(ret, Opts{
+			Name:  "MBZ",
+			Value: fmt.Sprintf("0x%04x", rr.Hdr.Ttl&0x7FFF),
+		})
+	}
+
+	ret = append(ret, Opts{
+		Name:  "UDP Buffer Size",
+		Value: strconv.Itoa(int(rr.UDPSize())),
+	})
+
+	for _, opt := range rr.Option {
+		switch opt.(type) {
+		case *dns.EDNS0_NSID:
+			str := opt.String()
+
+			hex, err := hex.DecodeString(str)
+			if err != nil {
+				return nil, fmt.Errorf("%w", err)
+			}
+
+			ret = append(ret, Opts{
+				Name:  "NSID",
+				Value: fmt.Sprintf("%s (%s)", str, string(hex)),
+			})
+		case *dns.EDNS0_SUBNET:
+			ret = append(ret, Opts{
+				Name:  "Subnet",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_COOKIE:
+			ret = append(ret, Opts{
+				Name:  "Cookie",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_EXPIRE:
+			ret = append(ret, Opts{
+				Name:  "Expire",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_TCP_KEEPALIVE:
+			ret = append(ret, Opts{
+				Name:  "TCP Keepalive",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_UL:
+			ret = append(ret, Opts{
+				Name:  "Update Lease",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_LLQ:
+			ret = append(ret, Opts{
+				Name:  "Long Lived Queries",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_DAU:
+			ret = append(ret, Opts{
+				Name:  "DNSSEC Algorithm Understood",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_DHU:
+			ret = append(ret, Opts{
+				Name:  "DS Hash Understood",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_N3U:
+			ret = append(ret, Opts{
+				Name:  "NSEC3 Hash Understood",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_LOCAL:
+			ret = append(ret, Opts{
+				Name:  "Local OPT",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_PADDING:
+			ret = append(ret, Opts{
+				Name:  "Padding",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_EDE:
+			ret = append(ret, Opts{
+				Name:  "EDE",
+				Value: opt.String(),
+			})
+		case *dns.EDNS0_ESU:
+			ret = append(ret, Opts{
+				Name:  "ESU",
+				Value: opt.String(),
+			})
+		}
+	}
+
+	return ret, nil
+}
+
 var errInvalidFormat = errors.New("this should never happen")
diff --git a/query/query_test.go b/query/query_test.go
index b9c2be9..6b4d504 100644
--- a/query/query_test.go
+++ b/query/query_test.go
@@ -37,12 +37,16 @@ func TestCreateQ(t *testing.T) {
 				Statistics: true,
 			},
 			EDNS: util.EDNS{
+				ZFlag:      1,
+				BufSize:    1500,
 				EnableEDNS: true,
-				DNSSEC:     true,
 				Cookie:     true,
+				DNSSEC:     true,
 				Expire:     true,
 				KeepOpen:   true,
 				Nsid:       true,
+				Padding:    true,
+				Version:    0,
 			},
 		},
 		{
diff --git a/query/struct.go b/query/struct.go
index 908c10f..2617b4f 100644
--- a/query/struct.go
+++ b/query/struct.go
@@ -15,30 +15,39 @@ import (
 )
 
 // Message is for overall DNS responses.
+//
+//nolint:govet // Better output is worth 32 bytes.
 type Message struct {
-	Question []Question `json:"question,omitempty" xml:"question,omitempty" yaml:",omitempty"`
-	Answer   []Answer   `json:"answer,omitempty" xml:"answer,omitempty" yaml:",omitempty"`
-	Ns       []Answer   `json:"ns,omitempty" xml:"ns,omitempty" yaml:",omitempty"`
-	Extra    []Answer   `json:"extra,omitempty" xml:"extra,omitempty" yaml:",omitempty"`
-	Header   dns.MsgHdr `json:"header,omitempty" xml:"header,omitempty" yaml:",omitempty"`
+	Header     dns.MsgHdr `json:"header,omitempty" xml:"header,omitempty" yaml:",omitempty"`
+	Opt        []Opts     `json:"opt,omitempty" xml:"opt,omitempty" yaml:"opt,omitempty"`
+	Question   []Question `json:"question,omitempty" xml:"question,omitempty" yaml:",omitempty"`
+	Answer     []Answer   `json:"answer,omitempty" xml:"answer,omitempty" yaml:",omitempty"`
+	Ns         []Answer   `json:"ns,omitempty" xml:"ns,omitempty" yaml:",omitempty"`
+	Additional []Answer   `json:"additional,omitempty" xml:"additional,omitempty" yaml:",omitempty"`
 }
 
 // Question is a DNS Query.
 type Question struct {
 	Name  string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"`
-	Type  string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"`
 	Class string `json:"class,omitempty" xml:"class,omitempty" yaml:",omitempty"`
+	Type  string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"`
 }
 
 // RRHeader is for DNS Resource Headers.
 type RRHeader struct {
 	Name     string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"`
-	Type     string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"`
-	Class    string `json:"class,omitempty" xml:"class,omitempty" yaml:",omitempty"`
 	TTL      string `json:"ttl,omitempty" xml:"ttl,omitempty" yaml:",omitempty"`
+	Class    string `json:"class,omitempty" xml:"class,omitempty" yaml:",omitempty"`
+	Type     string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"`
 	Rdlength uint16 `json:"-" xml:"-" yaml:"-"`
 }
 
+// Opts is for the OPT pseudosection, nearly exclusively for EDNS.
+type Opts struct {
+	Name  string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"`
+	Value string `json:"value" xml:"value" yaml:""`
+}
+
 // Answer is for a DNS Response.
 type Answer struct {
 	Value    string `json:"response,omitempty" xml:"response,omitempty" yaml:"response,omitempty"`
@@ -212,7 +221,7 @@ func stringParse(str string, isAns bool, opts util.Options) (string, error) {
 			}
 		}
 
-		if opts.HumanTTL {
+		if opts.ShowTTL && opts.HumanTTL {
 			ttl, _ := strconv.Atoi(split[1])
 			split[1] = (time.Duration(ttl) * time.Second).String()
 		}
diff --git a/util/options.go b/util/options.go
index aaf1a74..91e5f14 100644
--- a/util/options.go
+++ b/util/options.go
@@ -10,7 +10,7 @@ import (
 	"github.com/miekg/dns"
 )
 
-// Options is the grand CLI options structure.
+// Options is the grand structure for all query options.
 type Options struct {
 	Logger  *logawl.Logger
 	TLSHost string
