From d93eccc06437753e763d93c9f763dde678d3c33c Mon Sep 17 00:00:00 2001 From: grumbulon Date: Tue, 27 Dec 2022 20:07:09 +0000 Subject: [PATCH] feat: RFC-8427 (#171) This PR will make the JSON response body be based off [RFC-8427](https://www.rfc-editor.org/rfc/rfc8427.html) which will be similar to [kdig's](https://www.knot-dns.cz/docs/2.6/html/man_kdig.html) JSON output. Co-authored-by: Sam Therapy Reviewed-on: https://git.froth.zone/sam/awl/pulls/171 Reviewed-by: Sam Therapy Co-authored-by: grumbulon Co-committed-by: grumbulon --- pkg/query/print.go | 303 +++++--------------------------------------- pkg/query/struct.go | 156 ++++++++++++++--------- pkg/query/util.go | 258 +++++++++++++++++++++++++++++++++++++ 3 files changed, 387 insertions(+), 330 deletions(-) create mode 100644 pkg/query/util.go diff --git a/pkg/query/print.go b/pkg/query/print.go index 20881c7..fc24c8c 100644 --- a/pkg/query/print.go +++ b/pkg/query/print.go @@ -3,7 +3,6 @@ package query import ( - "encoding/hex" "encoding/json" "encoding/xml" "errors" @@ -250,297 +249,61 @@ func MakePrintable(res util.Response, opts *util.Options) (*Message, error) { msg = res.DNS ) // The things I do for compatibility - ret := Message{ - Header: Header{ - ID: msg.Id, - Response: msg.Response, - Opcode: dns.OpcodeToString[msg.Opcode], - Authoritative: msg.Authoritative, - Truncated: msg.Truncated, - RecursionDesired: msg.RecursionDesired, - RecursionAvailable: msg.RecursionAvailable, - Zero: msg.Zero, - AuthenticatedData: msg.AuthenticatedData, - CheckingDisabled: msg.CheckingDisabled, - Status: dns.RcodeToString[msg.Rcode], - }, + ret := &Message{ + DateString: time.Now().Format(time.RFC3339), + DateSeconds: time.Now().Unix(), + MsgSize: res.DNS.Len(), + ID: msg.Id, + Opcode: msg.Opcode, + Response: msg.Response, + + Authoritative: msg.Authoritative, + Truncated: msg.Truncated, + RecursionDesired: msg.RecursionDesired, + RecursionAvailable: msg.RecursionAvailable, + AuthenticatedData: msg.AuthenticatedData, + CheckingDisabled: msg.CheckingDisabled, + Zero: msg.Zero, + + QdCount: len(msg.Question), + AnCount: len(msg.Answer), + NsCount: len(msg.Ns), + ArCount: len(msg.Extra), } opt := msg.IsEdns0() if opt != nil && opts.Display.Opt { - ret.Opt, err = parseOpt(*opt) + ret.EDNS0, err = ret.ParseOpt(msg.Rcode, *opt) if err != nil { return nil, fmt.Errorf("edns print: %w", err) } } if opts.Display.Question { - for _, question := range msg.Question { - var name string - if opts.Display.UcodeTranslate { - name, err = idna.ToUnicode(question.Name) - if err != nil { - return nil, fmt.Errorf("punycode to unicode: %w", err) - } - } else { - name = question.Name - } - - ret.Question = append(ret.Question, Question{ - Name: name, - Type: dns.TypeToString[question.Qtype], - Class: dns.ClassToString[question.Qclass], - }) + err = ret.displayQuestion(msg, opts, opt) + if err != nil { + return nil, fmt.Errorf("unable to display questions: %w", err) } } if opts.Display.Answer { - for _, answer := range msg.Answer { - temp := strings.Split(answer.String(), "\t") - - var ( - ttl any - name string - ) - - if opts.Display.TTL { - if opts.Display.HumanTTL { - ttl = (time.Duration(answer.Header().Ttl) * time.Second).String() - } else { - ttl = answer.Header().Ttl - } - } - - if opts.Display.UcodeTranslate { - name, err = idna.ToUnicode(answer.Header().Name) - if err != nil { - return nil, fmt.Errorf("punycode to unicode: %w", err) - } - } else { - name = answer.Header().Name - } - - ret.Answer = append(ret.Answer, Answer{ - RRHeader: RRHeader{ - Name: name, - Type: dns.TypeToString[answer.Header().Rrtype], - Class: dns.ClassToString[answer.Header().Class], - Rdlength: answer.Header().Rdlength, - TTL: ttl, - }, - Value: temp[len(temp)-1], - }) + err = ret.displayAnswers(msg, opts, opt) + if err != nil { + return nil, fmt.Errorf("unable to display answers: %w", err) } } if opts.Display.Authority { - for _, ns := range msg.Ns { - temp := strings.Split(ns.String(), "\t") - - var ( - ttl string - name string - ) - - if opts.Display.TTL { - if opts.Display.HumanTTL { - ttl = (time.Duration(ns.Header().Ttl) * time.Second).String() - } else { - ttl = strconv.Itoa(int(ns.Header().Ttl)) - } - } - - if opts.Display.UcodeTranslate { - name, err = idna.ToUnicode(ns.Header().Name) - if err != nil { - return nil, fmt.Errorf("punycode to unicode: %w", err) - } - } else { - name = ns.Header().Name - } - - ret.Authority = append(ret.Authority, Answer{ - RRHeader: RRHeader{ - Name: name, - Type: dns.TypeToString[ns.Header().Rrtype], - Class: dns.ClassToString[ns.Header().Class], - Rdlength: ns.Header().Rdlength, - TTL: ttl, - }, - Value: temp[len(temp)-1], - }) + err = ret.displayAuthority(msg, opts, opt) + if err != nil { + return nil, fmt.Errorf("unable to display authority: %w", err) } } if opts.Display.Additional { - for _, additional := range msg.Extra { - if additional.Header().Rrtype == dns.StringToType["OPT"] { - continue - } else { - temp := strings.Split(additional.String(), "\t") - - var ( - ttl string - name string - ) - - if opts.Display.TTL { - if opts.Display.HumanTTL { - ttl = (time.Duration(additional.Header().Ttl) * time.Second).String() - } else { - ttl = strconv.Itoa(int(additional.Header().Ttl)) - } - } - - if opts.Display.UcodeTranslate { - name, err = idna.ToUnicode(additional.Header().Name) - if err != nil { - return nil, fmt.Errorf("punycode to unicode: %w", err) - } - } else { - name = additional.Header().Name - } - - ret.Additional = append(ret.Additional, Answer{ - RRHeader: RRHeader{ - Name: name, - Type: dns.TypeToString[additional.Header().Rrtype], - Class: dns.ClassToString[additional.Header().Class], - Rdlength: additional.Header().Rdlength, - TTL: ttl, - }, - Value: temp[len(temp)-1], - }) - } - } - } - - if opts.Display.Statistics { - ret.Statistics = Statistics{ - RTT: res.RTT.String(), - Server: opts.Request.Server + serverExtra(opts), - When: time.Now().Format(time.RFC3339), - MsgSize: res.DNS.Len(), - } - } else { - ret.Statistics = Statistics{} - } - - 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(), - }) + err = ret.displayAdditional(msg, opts, opt) + if err != nil { + return nil, fmt.Errorf("unable to display additional: %w", err) } } diff --git a/pkg/query/struct.go b/pkg/query/struct.go index 1b063c3..f4cb5eb 100644 --- a/pkg/query/struct.go +++ b/pkg/query/struct.go @@ -8,73 +8,109 @@ import ( // Message is for overall DNS responses. // -//nolint:govet // Better looking output is worth a few bytes. +//nolint:govet,tagliatelle // Better looking output is worth a few bytes. type Message struct { - // Header section - Header `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"` - // Opt Pseudosection - Opt []Opts `json:"opt,omitempty" xml:"opt,omitempty" yaml:"opt,omitempty"` - // Question Section - Question []Question `json:"question,omitempty" xml:"question,omitempty" yaml:"question,omitempty"` + DateString string `json:"dateString,omitempty" xml:"dateString,omitempty" yaml:"dateString,omitempty"` + DateSeconds int64 `json:"dateSeconds,omitempty" xml:"dateSeconds,omitempty" yaml:"dateSeconds,omitempty"` + MsgSize int `json:"msgLength,omitempty" xml:"msgSize,omitempty" yaml:"msgSize,omitempty"` + ID uint16 `json:"ID" xml:"ID" yaml:"ID" example:"12"` + + Opcode int `json:"opcode" xml:"opcode" yaml:"opcode" example:"QUERY"` + Response bool `json:"QR" xml:"QR" yaml:"QR" example:"true"` + Authoritative bool `json:"AA" xml:"AA" yaml:"AA" example:"false"` + Truncated bool `json:"TC" xml:"TC" yaml:"TC" example:"false"` + RecursionDesired bool `json:"RD" xml:"RD" yaml:"RD" example:"true"` + RecursionAvailable bool `json:"RA" xml:"RA" yaml:"RA" example:"true"` + AuthenticatedData bool `json:"AD" xml:"AD" yaml:"AD" example:"false"` + CheckingDisabled bool `json:"CD" xml:"CD" yaml:"CD" example:"false"` + Zero bool `json:"Z" xml:"Z" yaml:"Z" example:"false"` + + QdCount int `json:"QDCOUNT" xml:"QDCOUNT" yaml:"QDCOUNT" example:"0"` + AnCount int `json:"ANCOUNT" xml:"ANCOUNT" yaml:"ANCOUNT" example:"0"` + NsCount int `json:"NSCOUNT" xml:"NSCOUNT" yaml:"NSCOUNT" example:"0"` + ArCount int `json:"ARCOUNT" xml:"ARCOUNT" yaml:"ARCOUNT" example:"0"` + + Name string `json:"QNAME,omitempty" xml:"QNAME,omitempty" yaml:"QNAME,omitempty" example:"localhost"` + Type uint16 `json:"QTYPE,omitempty" xml:"QTYPE,omitempty" yaml:"QTYPE,omitempty" example:"IN"` + TypeName string `json:"QTYPEname,omitempty" xml:"QTYPEname,omitempty" yaml:"QTYPEname,omitempty" example:"IN"` + Class uint16 `json:"QCLASS,omitempty" xml:"QCLASS,omitempty" yaml:"QCLASS,omitempty" example:"A"` + ClassName string `json:"QCLASSname,omitempty" xml:"QCLASSname,omitempty" yaml:"QCLASSname,omitempty" example:"1"` + + EDNS0 EDNS0 `json:",omitempty" xml:",omitempty" yaml:",omitempty"` + // Answer Section - Answer []Answer `json:"answer,omitempty" xml:"answer,omitempty" yaml:"answer,omitempty"` - // Authority Section - Authority []Answer `json:"authority,omitempty" xml:"authority,omitempty" yaml:"authority,omitempty"` - // Additional Section - Additional []Answer `json:"additional,omitempty" xml:"additional,omitempty" yaml:"additional,omitempty"` - // Statistics :) - Statistics `json:"statistics,omitempty" xml:"statistics,omitempty" yaml:"statistics,omitempty"` + AnswerRRs []Answer `json:"answersRRs,omitempty" xml:"answersRRs,omitempty" yaml:"answersRRs,omitempty" example:"false"` + AuthoritativeRRs []Answer `json:"authorityRRs,omitempty" xml:"authorityRRs,omitempty" yaml:"authorityRRs,omitempty" example:"false"` + AdditionalRRs []Answer `json:"additionalRRs,omitempty" xml:"additionalRRs,omitempty" yaml:"additionalRRs,omitempty" example:"false"` } -// Header is the header. -type Header struct { - Opcode string `json:"opcode," xml:"opcode," yaml:"opcode" example:"QUERY"` - Status string `json:"status," xml:"status," yaml:"status" example:"NOERR"` - ID uint16 `json:"id," xml:"id," yaml:"id" example:"12"` - Response bool `json:"response," xml:"response," yaml:"response" example:"true"` - Authoritative bool `json:"authoritative," xml:"authoritative," yaml:"authoritative" example:"false"` - Truncated bool `json:"truncated," xml:"truncated," yaml:"truncated" example:"false"` - RecursionDesired bool `json:"recursionDesired," xml:"recursionDesired," yaml:"recursionDesired" example:"true"` - RecursionAvailable bool `json:"recursionAvailable," xml:"recursionAvailable," yaml:"recursionAvailable" example:"true"` - Zero bool `json:"zero," xml:"zero," yaml:"zero" example:"false"` - AuthenticatedData bool `json:"authenticatedData," xml:"authenticatedData," yaml:"authenticatedData" example:"false"` - CheckingDisabled bool `json:"checkingDisabled," xml:"checkingDisabled," yaml:"checkingDisabled" example:"false"` -} - -// Question is a DNS Query. -type Question struct { - Name string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty" example:"localhost"` - Class string `json:"class,omitempty" xml:"class,omitempty" yaml:"class,omitempty" example:"A"` - Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty" example:"IN"` -} - -// RRHeader is for DNS Resource Headers. -type RRHeader struct { - Name string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty" example:"127.0.0.1"` - TTL any `json:"ttl,omitempty" xml:"ttl,omitempty" yaml:"ttl,omitempty" example:"0ms"` - Class string `json:"class,omitempty" xml:"class,omitempty" yaml:"class,omitempty" example:"A"` - Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty" example:"IN"` - 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:"name,omitempty"` - Value string `json:"value" xml:"value" yaml:"value"` -} - -// Answer is for a DNS Response. +// Answer is for DNS Resource Headers. +// +//nolint:govet,tagliatelle type Answer struct { - Value string `json:"response,omitempty" xml:"response,omitempty" yaml:"response,omitempty"` - RRHeader `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"` + Name string `json:"NAME,omitempty" xml:"NAME,omitempty" yaml:"NAME,omitempty" example:"127.0.0.1"` + Type uint16 `json:"TYPE,omitempty" xml:"TYPE,omitempty" yaml:"TYPE,omitempty" example:"1"` + TypeName string `json:"TYPEname,omitempty" xml:"TYPEname,omitempty" yaml:"TYPEname,omitempty" example:"A"` + Class uint16 `json:"CLASS,omitempty" xml:"CLASS,omitempty" yaml:"CLASS,omitempty" example:"1"` + ClassName string `json:"CLASSname,omitempty" xml:"CLASSname,omitempty" yaml:"CLASSname,omitempty" example:"IN"` + TTL any `json:"TTL,omitempty" xml:"TTL,omitempty" yaml:"TTL,omitempty" example:"0ms"` + Value string `json:"rdata,omitempty" xml:"rdata,omitempty" yaml:"rdata,omitempty"` + Rdlength uint16 `json:"RDLENGTH,omitempty" xml:"RDLENGTH,omitempty" yaml:"RDLENGTH,omitempty"` + Rdhex string `json:"RDATAHEX,omitempty" xml:"RDATAHEX,omitempty" yaml:"RDATAHEX,omitempty"` } -// Statistics is the little bit at the bottom :). -type Statistics struct { - RTT string `json:"queryTime,omitempty" xml:"queryTime,omitempty" yaml:"queryTime,omitempty"` - Server string `json:"server,omitempty" xml:"server,omitempty" yaml:"server,omitempty"` - When string `json:"when,omitempty" xml:"when,omitempty" yaml:"when,omitempty"` - MsgSize int `json:"msgSize,omitempty" xml:"msgSize,omitempty" yaml:"msgSize,omitempty"` +// EDNS0 is for all EDNS options. +// +// RFC: https://datatracker.ietf.org/doc/draft-peltan-edns-presentation-format/ +// +//nolint:govet,tagliatelle +type EDNS0 struct { + Flags []string `json:"FLAGS" xml:"FLAGS" yaml:"FLAGS"` + Rcode string `json:"RCODE" xml:"RCODE" yaml:"RCODE"` + PayloadSize uint16 `json:"UDPSIZE" xml:"UDPSIZE" yaml:"UDPSIZE"` + LLQ *EdnsLLQ `json:"LLQ,omitempty" xml:"LLQ,omitempty" yaml:"LLQ,omitempty"` + NsidHex string `json:"NSIDHEX,omitempty" xml:"NSIDHEX,omitempty" yaml:"NSIDHEX,omitempty"` + Nsid string `json:"NSID,omitempty" xml:"NSID,omitempty" yaml:"NSID,omitempty"` + Dau []uint8 `json:"DAU,omitempty" xml:"DAU,omitempty" yaml:"DAU,omitempty"` + Dhu []uint8 `json:"DHU,omitempty" xml:"DHU,omitempty" yaml:"DHU,omitempty"` + N3u []uint8 `json:"N3U,omitempty" xml:"N3U,omitempty" yaml:"N3U,omitempty"` + Subnet *EDNSSubnet `json:"ECS,omitempty" xml:"ECS,omitempty" yaml:"ECS,omitempty"` + Expire uint32 `json:"EXPIRE,omitempty" xml:"EXPIRE,omitempty" yaml:"EXPIRE,omitempty"` + Cookie []string `json:"COOKIE,omitempty" xml:"COOKIE,omitempty" yaml:"COOKIE,omitempty"` + KeepAlive uint16 `json:"KEEPALIVE,omitempty" xml:"KEEPALIVE,omitempty" yaml:"KEEPALIVE,omitempty"` + Padding string `json:"PADDING,omitempty" xml:"PADDING,omitempty" yaml:"PADDING,omitempty"` + Chain string `json:"CHAIN,omitempty" xml:"CHAIN,omitempty" yaml:"CHAIN,omitempty"` + EDE *EDNSErr `json:"EDE,omitempty" xml:"EDE,omitempty" yaml:"EDE,omitempty"` +} + +// EdnsLLQ is for Long-lived queries. +// +//nolint:tagliatelle +type EdnsLLQ struct { + Version uint16 `json:"LLQ-VERSION" xml:"LLQ-VERSION" yaml:"LLQ-VERSION"` + Opcode uint16 `json:"LLQ-OPCODE" xml:"LLQ-OPCODE" yaml:"LLQ-OPCODE"` + Error uint16 `json:"LLQ-ERROR" xml:"LLQ-ERROR" yaml:"LLQ-ERROR"` + ID uint64 `json:"LLQ-ID" xml:"LLQ-ID" yaml:"LLQ-ID"` + Lease uint32 `json:"LLQ-LEASE" xml:"LLQ-LEASE" yaml:"LLQ-LEASE"` +} + +// EDNSSubnet is for EDNS subnet options, +// +//nolint:govet,tagliatelle +type EDNSSubnet struct { + Family uint16 `json:"FAMILY" xml:"FAMILY" yaml:"FAMILY"` + IP string + Source uint8 `json:"SOURCE" xml:"SOURCE" yaml:"SOURCE"` + Scope uint8 `json:"SCOPE,omitempty" xml:"SCOPE,omitempty" yaml:"SCOPE,omitempty"` +} + +// EDNSErr is for EDE codes +// +//nolint:govet,tagliatelle +type EDNSErr struct { + Code uint16 `json:"INFO-CODE" xml:"INFO-CODE" yaml:"INFO-CODE"` + Purpose string + Text string `json:"EXTRA-TEXT,omitempty" xml:"EXTRA-TEXT,omitempty" yaml:"EXTRA-TEXT,omitempty"` } var errNoMessage = errors.New("no message") diff --git a/pkg/query/util.go b/pkg/query/util.go new file mode 100644 index 0000000..10e51fb --- /dev/null +++ b/pkg/query/util.go @@ -0,0 +1,258 @@ +package query + +import ( + "encoding/hex" + "fmt" + "strings" + "time" + + "git.froth.zone/sam/awl/pkg/util" + "github.com/miekg/dns" + "golang.org/x/net/idna" +) + +func (message *Message) displayQuestion(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error { + var ( + name string + err error + ) + + for _, question := range msg.Question { + if opts.Display.UcodeTranslate { + name, err = idna.ToUnicode(question.Name) + if err != nil { + return fmt.Errorf("punycode to unicode: %w", err) + } + } else { + name = question.Name + } + + message.Name = name + message.Type = question.Qtype + message.TypeName = dns.TypeToString[question.Qtype] + message.Class = question.Qclass + message.ClassName = dns.ClassToString[question.Qclass] + } + + return nil +} + +func (message *Message) displayAnswers(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error { + var ( + ttl any + name string + err error + ) + + for _, answer := range msg.Answer { + temp := strings.Split(answer.String(), "\t") + + if opts.Display.TTL { + if opts.Display.HumanTTL { + ttl = (time.Duration(answer.Header().Ttl) * time.Second).String() + } else { + ttl = answer.Header().Ttl + } + } + + if opts.Display.UcodeTranslate { + name, err = idna.ToUnicode(answer.Header().Name) + if err != nil { + return fmt.Errorf("punycode to unicode: %w", err) + } + } else { + name = answer.Header().Name + } + + message.AnswerRRs = append(message.AnswerRRs, Answer{ + Name: name, + ClassName: dns.ClassToString[answer.Header().Class], + Class: answer.Header().Class, + TypeName: dns.TypeToString[answer.Header().Rrtype], + Type: answer.Header().Rrtype, + Rdlength: answer.Header().Rdlength, + TTL: ttl, + + Value: temp[len(temp)-1], + }) + } + + return nil +} + +func (message *Message) displayAuthority(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error { + var ( + ttl any + name string + err error + ) + + for _, ns := range msg.Ns { + temp := strings.Split(ns.String(), "\t") + + if opts.Display.TTL { + if opts.Display.HumanTTL { + ttl = (time.Duration(ns.Header().Ttl) * time.Second).String() + } else { + ttl = ns.Header().Ttl + } + } + + if opts.Display.UcodeTranslate { + name, err = idna.ToUnicode(ns.Header().Name) + if err != nil { + return fmt.Errorf("punycode to unicode: %w", err) + } + } else { + name = ns.Header().Name + } + + message.AuthoritativeRRs = append(message.AuthoritativeRRs, Answer{ + Name: name, + TypeName: dns.TypeToString[ns.Header().Rrtype], + Type: ns.Header().Rrtype, + Class: ns.Header().Class, + ClassName: dns.ClassToString[ns.Header().Class], + Rdlength: ns.Header().Rdlength, + TTL: ttl, + + Value: temp[len(temp)-1], + }) + } + + return nil +} + +func (message *Message) displayAdditional(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error { + var ( + ttl any + name string + err error + ) + + for _, additional := range msg.Extra { + if additional.Header().Rrtype == dns.StringToType["OPT"] { + continue + } else { + temp := strings.Split(additional.String(), "\t") + + if opts.Display.TTL { + if opts.Display.HumanTTL { + ttl = (time.Duration(additional.Header().Ttl) * time.Second).String() + } else { + ttl = additional.Header().Ttl + } + } + + if opts.Display.UcodeTranslate { + name, err = idna.ToUnicode(additional.Header().Name) + if err != nil { + return fmt.Errorf("punycode to unicode: %w", err) + } + } else { + name = additional.Header().Name + } + message.AdditionalRRs = append(message.AdditionalRRs, Answer{ + Name: name, + TypeName: dns.TypeToString[additional.Header().Rrtype], + Type: additional.Header().Rrtype, + Class: additional.Header().Class, + ClassName: dns.ClassToString[additional.Header().Class], + Rdlength: additional.Header().Rdlength, + TTL: ttl, + Value: temp[len(temp)-1], + }) + } + } + + return nil +} + +// ParseOpt parses opts. +func (message *Message) ParseOpt(rcode int, rr dns.OPT) (ret EDNS0, err error) { + ret.Rcode = dns.RcodeToString[rcode] + + // Most of this is taken from https://github.com/miekg/dns/blob/master/edns.go#L76 + if rr.Do() { + ret.Flags = append(ret.Flags, "DO") + } + + for i := uint32(1); i <= 0x7FFF; i <<= 1 { + if rr.Hdr.Ttl&i != 0 { + ret.Flags = append(ret.Flags, fmt.Sprintf("BIT%d", i)) + } + } + + ret.PayloadSize = rr.UDPSize() + + for _, opt := range rr.Option { + switch opt := opt.(type) { + case *dns.EDNS0_NSID: + str := opt.String() + + hex, err := hex.DecodeString(str) + if err != nil { + return ret, fmt.Errorf("%w", err) + } + + ret.NsidHex = string(hex) + ret.Nsid = str + + case *dns.EDNS0_SUBNET: + ret.Subnet = &EDNSSubnet{ + Source: opt.SourceNetmask, + Family: opt.Family, + } + + // 1: IPv4 2: IPv6 + if ret.Subnet.Family <= 2 { + ret.Subnet.IP = opt.Address.String() + } else { + ret.Subnet.IP = hex.EncodeToString([]byte(opt.Address)) + } + + if opt.SourceScope != 0 { + ret.Subnet.Scope = opt.SourceScope + } + + case *dns.EDNS0_COOKIE: + ret.Cookie = append(ret.Cookie, opt.String()) + + case *dns.EDNS0_EXPIRE: + ret.Expire = opt.Expire + + case *dns.EDNS0_TCP_KEEPALIVE: + ret.KeepAlive = opt.Timeout + + case *dns.EDNS0_LLQ: + ret.LLQ = &EdnsLLQ{ + Version: opt.Version, + Opcode: opt.Opcode, + Error: opt.Error, + ID: opt.Id, + Lease: opt.LeaseLife, + } + + case *dns.EDNS0_DAU: + ret.Dau = opt.AlgCode + + case *dns.EDNS0_DHU: + ret.Dhu = opt.AlgCode + + case *dns.EDNS0_N3U: + ret.N3u = opt.AlgCode + + case *dns.EDNS0_PADDING: + ret.Padding = string(opt.Padding) + + case *dns.EDNS0_EDE: + ret.EDE = &EDNSErr{ + Code: opt.InfoCode, + Purpose: dns.ExtendedErrorCodeToString[opt.InfoCode], + Text: opt.ExtraText, + } + } + } + + return ret, nil +}