diff --git a/pkg/query/print.go b/pkg/query/print.go index 3179924..5444bf6 100644 --- a/pkg/query/print.go +++ b/pkg/query/print.go @@ -253,28 +253,30 @@ func MakePrintable(res util.Response, opts *util.Options) (*Message, error) { ) // The things I do for compatibility ret := &Message{ - RTT: res.RTT.String(), - Server: opts.Request.Server + serverExtra(opts), DateString: time.Now().Format(time.RFC3339), DateSeconds: time.Now().Unix(), MsgSize: res.DNS.Len(), ID: msg.Id, - Opcode: dns.OpcodeToString[msg.Opcode], - Status: dns.RcodeToString[msg.Rcode], + Opcode: msg.Opcode, Response: msg.Response, Authoritative: msg.Authoritative, Truncated: msg.Truncated, RecursionDesired: msg.RecursionDesired, RecursionAvailable: msg.RecursionAvailable, - Zero: msg.Zero, 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 = ret.ParseOpt(*opt) + ret.EDNS0, err = ret.ParseOpt(msg.Rcode, *opt) if err != nil { return nil, fmt.Errorf("edns print: %w", err) } diff --git a/pkg/query/struct.go b/pkg/query/struct.go index 76e9d71..f4cb5eb 100644 --- a/pkg/query/struct.go +++ b/pkg/query/struct.go @@ -13,30 +13,30 @@ type Message struct { 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"` + ID uint16 `json:"ID" xml:"ID" yaml:"ID" example:"12"` - Opcode string `json:"opcode," xml:"opcode," yaml:"opcode" example:"QUERY"` - Status string `json:"status," xml:"status," yaml:"status" example:"NOERR"` - 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:"RD," xml:"recursionDesired," yaml:"recursionDesired" example:"true"` - RecursionAvailable bool `json:"RA," 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"` - 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"` - 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"` + 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"` - Opt []Opts `json:"opt,omitempty" xml:"opt,omitempty" yaml:"opt,omitempty"` - // Question Section - Question []Question `json:"question,omitempty" xml:"question,omitempty" yaml:"question,omitempty"` + 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 AnswerRRs []Answer `json:"answersRRs,omitempty" xml:"answersRRs,omitempty" yaml:"answersRRs,omitempty" example:"false"` @@ -44,36 +44,73 @@ type Message struct { AdditionalRRs []Answer `json:"additionalRRs,omitempty" xml:"additionalRRs,omitempty" yaml:"additionalRRs,omitempty" example:"false"` } -// Question is a DNS Query. -// -//nolint:govet,tagliatelle -type Question struct { - Name string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty" example:"localhost"` - Type uint16 `json:"TYPE,omitempty" xml:"type,omitempty" yaml:"type,omitempty" example:"IN"` - TypeName string `json:"TYPEname,omitempty" xml:"TYPEname,omitempty" yaml:"TYPEname,omitempty" example:"IN"` - Class uint16 `json:"CLASS,omitempty" xml:"class,omitempty" yaml:"class,omitempty" example:"A"` - ClassName string `json:"CLASSname,omitempty" xml:"CLASSname,omitempty" yaml:"CLASSname,omitempty" example:"1"` -} - // Answer is for DNS Resource Headers. // //nolint:govet,tagliatelle type Answer struct { - 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:"IN"` - TypeName string `json:"TYPEname,omitempty" xml:"TYPEname,omitempty" yaml:"TYPEname,omitempty" example:"IN"` - Class uint16 `json:"CLASS,omitempty" xml:"class,omitempty" yaml:"class,omitempty" example:"A"` - ClassName string `json:"CLASSname,omitempty" xml:"CLASSname,omitempty" yaml:"CLASSname,omitempty" example:"A"` - TTL any `json:"TTL,omitempty" xml:"ttl,omitempty" yaml:"ttl,omitempty" example:"0ms"` + 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"` } -// 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"` +// 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 index 0782f89..97b52c6 100644 --- a/pkg/query/util.go +++ b/pkg/query/util.go @@ -170,118 +170,88 @@ func (message *Message) displayAdditional(msg *dns.Msg, opts *util.Options, opt } // ParseOpt parses opts. -func (message *Message) ParseOpt(rr dns.OPT) ([]Opts, error) { - ret := []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 - - 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: "", - }) + ret.Flags = append(ret.Flags, "DO") } - if rr.Hdr.Ttl&0x7FFF != 0 { - ret = append(ret, Opts{ - Name: "MBZ", - Value: fmt.Sprintf("0x%04x", rr.Hdr.Ttl&0x7FFF), - }) + for i := uint32(1); i <= 0x7FFF; i <<= 1 { + if rr.Hdr.Ttl&i != 0 { + ret.Flags = append(ret.Flags, fmt.Sprintf("FLAG%d", i)) + } } - ret = append(ret, Opts{ - Name: "UDP Buffer Size", - Value: strconv.Itoa(int(rr.UDPSize())), - }) + ret.PayloadSize = rr.UDPSize() for _, opt := range rr.Option { - switch opt.(type) { + switch opt := opt.(type) { case *dns.EDNS0_NSID: str := opt.String() hex, err := hex.DecodeString(str) if err != nil { - return nil, fmt.Errorf("%w", err) + return ret, fmt.Errorf("%w", err) } - ret = append(ret, Opts{ - Name: "NSID", - Value: fmt.Sprintf("%s (%s)", str, string(hex)), - }) + ret.NsidHex = string(hex) + ret.Nsid = str + case *dns.EDNS0_SUBNET: - ret = append(ret, Opts{ - Name: "Subnet", - Value: opt.String(), - }) + 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 = append(ret, Opts{ - Name: "Cookie", - Value: opt.String(), - }) + ret.Cookie = append(ret.Cookie, opt.String()) + case *dns.EDNS0_EXPIRE: - ret = append(ret, Opts{ - Name: "Expire", - Value: opt.String(), - }) + ret.Expire = opt.Expire + 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(), - }) + ret.KeepAlive = opt.Timeout + case *dns.EDNS0_LLQ: - ret = append(ret, Opts{ - Name: "Long Lived Queries", - Value: opt.String(), - }) + ret.LLQ = &EdnsLLQ{ + Version: opt.Version, + Opcode: opt.Opcode, + Error: opt.Error, + ID: opt.Id, + Lease: opt.LeaseLife, + } + case *dns.EDNS0_DAU: - ret = append(ret, Opts{ - Name: "DNSSEC Algorithm Understood", - Value: opt.String(), - }) + ret.Dau = opt.AlgCode + case *dns.EDNS0_DHU: - ret = append(ret, Opts{ - Name: "DS Hash Understood", - Value: opt.String(), - }) + ret.Dhu = opt.AlgCode + 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(), - }) + ret.N3u = opt.AlgCode + case *dns.EDNS0_PADDING: - ret = append(ret, Opts{ - Name: "Padding", - Value: opt.String(), - }) + ret.Padding = string(opt.Padding) + 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(), - }) + ret.EDE = &EDNSErr{ + Code: opt.InfoCode, + Purpose: dns.ExtendedErrorCodeToString[opt.InfoCode], + Text: opt.ExtraText, + } } }