Sam
dc4edd55bb
All checks were successful
continuous-integration/drone/push Build is passing
1. Before, the port printed was duplicated, eg. `;; SERVER: [::1]:53:53 (UDP)` when making query. This has been fixed (not sure what caused it) 2. JSON/XML/YAML date formatting to be compliant with RFC3339 (thanks, std/time!) Co-authored-by: Sam Therapy <sam@samtherapy.net> Reviewed-on: #140 Reviewed-by: grumbulon <grumbulon@grumbulon.xyz>
545 lines
12 KiB
Go
545 lines
12 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package query
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.froth.zone/sam/awl/pkg/util"
|
|
"github.com/miekg/dns"
|
|
"golang.org/x/net/idna"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ToString turns the response into something that looks a lot like dig
|
|
//
|
|
// Much of this is taken from https://github.com/miekg/dns/blob/master/msg.go#L900
|
|
func ToString(res util.Response, opts *util.Options) (string, error) {
|
|
if res.DNS == nil {
|
|
return "<nil> MsgHdr", errNoMessage
|
|
}
|
|
|
|
var (
|
|
s string
|
|
opt *dns.OPT
|
|
)
|
|
|
|
if !opts.Short {
|
|
if opts.Display.Comments {
|
|
s += res.DNS.MsgHdr.String() + " "
|
|
s += "QUERY: " + strconv.Itoa(len(res.DNS.Question)) + ", "
|
|
s += "ANSWER: " + strconv.Itoa(len(res.DNS.Answer)) + ", "
|
|
s += "AUTHORITY: " + strconv.Itoa(len(res.DNS.Ns)) + ", "
|
|
s += "ADDITIONAL: " + strconv.Itoa(len(res.DNS.Extra)) + "\n"
|
|
opt = res.DNS.IsEdns0()
|
|
|
|
if opt != nil && opts.Display.Opt {
|
|
// OPT PSEUDOSECTION
|
|
s += opt.String() + "\n"
|
|
}
|
|
}
|
|
|
|
if opts.Display.Question {
|
|
if len(res.DNS.Question) > 0 {
|
|
if opts.Display.Comments {
|
|
s += "\n;; QUESTION SECTION:\n"
|
|
}
|
|
|
|
for _, r := range res.DNS.Question {
|
|
str, err := stringParse(r.String(), false, opts)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w", err)
|
|
}
|
|
|
|
s += str + "\n"
|
|
}
|
|
}
|
|
}
|
|
|
|
if opts.Display.Answer {
|
|
if len(res.DNS.Answer) > 0 {
|
|
if opts.Display.Comments {
|
|
s += "\n;; ANSWER SECTION:\n"
|
|
}
|
|
|
|
for _, r := range res.DNS.Answer {
|
|
if r != nil {
|
|
str, err := stringParse(r.String(), true, opts)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w", err)
|
|
}
|
|
|
|
s += str + "\n"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if opts.Display.Authority {
|
|
if len(res.DNS.Ns) > 0 {
|
|
if opts.Display.Comments {
|
|
s += "\n;; AUTHORITY SECTION:\n"
|
|
}
|
|
|
|
for _, r := range res.DNS.Ns {
|
|
if r != nil {
|
|
str, err := stringParse(r.String(), true, opts)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w", err)
|
|
}
|
|
|
|
s += str + "\n"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if opts.Display.Additional {
|
|
if len(res.DNS.Extra) > 0 && (opt == nil || len(res.DNS.Extra) > 1) {
|
|
if opts.Display.Comments {
|
|
s += "\n;; ADDITIONAL SECTION:\n"
|
|
}
|
|
|
|
for _, r := range res.DNS.Extra {
|
|
if r != nil && r.Header().Rrtype != dns.TypeOPT {
|
|
str, err := stringParse(r.String(), true, opts)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w", err)
|
|
}
|
|
|
|
s += str + "\n"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if opts.Display.Statistics {
|
|
s += "\n;; Query time: " + res.RTT.String()
|
|
s += "\n;; SERVER: " + opts.Request.Server + serverExtra(opts)
|
|
s += "\n;; WHEN: " + time.Now().Format(time.RFC1123Z)
|
|
s += "\n;; MSG SIZE rcvd: " + strconv.Itoa(res.DNS.Len()) + "\n"
|
|
}
|
|
} else {
|
|
// Print just the responses, nothing else
|
|
for i, resp := range res.DNS.Answer {
|
|
temp := strings.Split(resp.String(), "\t")
|
|
s += temp[len(temp)-1]
|
|
|
|
if opts.Identify {
|
|
s += " from server " + opts.Request.Server + " in " + res.RTT.String()
|
|
}
|
|
|
|
// Don't print newline on last line
|
|
if i != len(res.DNS.Answer)-1 {
|
|
s += "\n"
|
|
}
|
|
}
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func serverExtra(opts *util.Options) string {
|
|
switch {
|
|
case opts.TCP:
|
|
return " (TCP)"
|
|
case opts.TLS:
|
|
return " (TLS)"
|
|
case opts.HTTPS, opts.DNSCrypt:
|
|
return ""
|
|
case opts.QUIC:
|
|
return " (QUIC)"
|
|
default:
|
|
return " (UDP)"
|
|
}
|
|
}
|
|
|
|
// stringParse edits the raw responses to user requests.
|
|
func stringParse(str string, isAns bool, opts *util.Options) (string, error) {
|
|
split := strings.Split(str, "\t")
|
|
|
|
// Make edits if so requested
|
|
|
|
// TODO: make less ew?
|
|
// This exists because the question section should be left alone EXCEPT for punycode.
|
|
|
|
if isAns {
|
|
if !opts.Display.TTL {
|
|
// Remove from existence
|
|
split = append(split[:1], split[2:]...)
|
|
}
|
|
|
|
if !opts.Display.ShowClass {
|
|
// Position depends on if the TTL is there or not.
|
|
if opts.Display.TTL {
|
|
split = append(split[:2], split[3:]...)
|
|
} else {
|
|
split = append(split[:1], split[2:]...)
|
|
}
|
|
}
|
|
|
|
if opts.Display.TTL && opts.Display.HumanTTL {
|
|
ttl, _ := strconv.Atoi(split[1])
|
|
split[1] = (time.Duration(ttl) * time.Second).String()
|
|
}
|
|
}
|
|
|
|
if opts.Display.UcodeTranslate {
|
|
var (
|
|
err error
|
|
semi string
|
|
)
|
|
|
|
if strings.HasPrefix(split[0], ";") {
|
|
split[0] = strings.TrimPrefix(split[0], ";")
|
|
semi = ";"
|
|
}
|
|
|
|
split[0], err = idna.ToUnicode(split[0])
|
|
if err != nil {
|
|
return "", fmt.Errorf("punycode: %w", err)
|
|
}
|
|
|
|
split[0] = semi + split[0]
|
|
}
|
|
|
|
return strings.Join(split, "\t"), nil
|
|
}
|
|
|
|
// PrintSpecial is for printing as JSON, XML or YAML.
|
|
// As of now JSON and XML use the stdlib version.
|
|
func PrintSpecial(res util.Response, opts *util.Options) (string, error) {
|
|
formatted, err := MakePrintable(res, opts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
switch {
|
|
case opts.JSON:
|
|
opts.Logger.Info("Printing as JSON")
|
|
|
|
json, err := json.MarshalIndent(formatted, " ", " ")
|
|
|
|
return string(json), err
|
|
case opts.XML:
|
|
opts.Logger.Info("Printing as XML")
|
|
|
|
xml, err := xml.MarshalIndent(formatted, " ", " ")
|
|
|
|
return string(xml), err
|
|
case opts.YAML:
|
|
opts.Logger.Info("Printing as YAML")
|
|
|
|
yaml, err := yaml.Marshal(formatted)
|
|
|
|
return string(yaml), err
|
|
default:
|
|
return "", errInvalidFormat
|
|
}
|
|
}
|
|
|
|
// MakePrintable takes a DNS message and makes it nicer to be printed as JSON,YAML,
|
|
// and XML. Little is changed beyond naming.
|
|
func MakePrintable(res util.Response, opts *util.Options) (*Message, error) {
|
|
var (
|
|
err 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],
|
|
},
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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],
|
|
})
|
|
}
|
|
|
|
for _, answer := range msg.Answer {
|
|
temp := strings.Split(answer.String(), "\t")
|
|
|
|
var (
|
|
ttl string
|
|
name string
|
|
)
|
|
|
|
if opts.Display.TTL {
|
|
if opts.Display.HumanTTL {
|
|
ttl = (time.Duration(answer.Header().Ttl) * time.Second).String()
|
|
} else {
|
|
ttl = strconv.Itoa(int(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],
|
|
})
|
|
}
|
|
|
|
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],
|
|
})
|
|
}
|
|
|
|
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(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
var errInvalidFormat = errors.New("this should never happen")
|