From 7838f061c569fa855f979f99d0086296fa727a4d Mon Sep 17 00:00:00 2001 From: phillip-stephens Date: Sun, 24 Mar 2024 17:55:48 -0700 Subject: [PATCH] phillip/refactor: moving towards a minimal working A lookup library --- pkg/refactored_zdns/answers.go | 905 +++++++++++++++++++++++++++++++++ pkg/refactored_zdns/cache.go | 161 ++++++ pkg/refactored_zdns/conf.go | 125 +++++ pkg/refactored_zdns/edns.go | 114 +++++ pkg/refactored_zdns/lookup.go | 424 +++++++++++++++ pkg/refactored_zdns/qa.go | 61 +++ pkg/refactored_zdns/util.go | 150 ++++++ pkg/refactored_zdns/zdns.go | 95 ++++ 8 files changed, 2035 insertions(+) create mode 100644 pkg/refactored_zdns/answers.go create mode 100644 pkg/refactored_zdns/cache.go create mode 100644 pkg/refactored_zdns/conf.go create mode 100644 pkg/refactored_zdns/edns.go create mode 100644 pkg/refactored_zdns/lookup.go create mode 100644 pkg/refactored_zdns/qa.go create mode 100644 pkg/refactored_zdns/util.go create mode 100644 pkg/refactored_zdns/zdns.go diff --git a/pkg/refactored_zdns/answers.go b/pkg/refactored_zdns/answers.go new file mode 100644 index 00000000..edf1dc7c --- /dev/null +++ b/pkg/refactored_zdns/answers.go @@ -0,0 +1,905 @@ +/* + * ZDNS Copyright 2022 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package refactored_zdns + +import ( + "encoding/hex" + "fmt" + "net" + "strconv" + "strings" + + "github.com/zmap/dns" +) + +type Answer struct { + Ttl uint32 `json:"ttl" groups:"ttl,normal,long,trace"` + Type string `json:"type,omitempty" groups:"short,normal,long,trace"` + RrType uint16 `json:"-"` + Class string `json:"class,omitempty" groups:"short,normal,long,trace"` + RrClass uint16 `json:"-"` + Name string `json:"name,omitempty" groups:"short,normal,long,trace"` + Answer string `json:"answer,omitempty" groups:"short,normal,long,trace"` +} + +// Complex Answers (in alphabetical order) + +type AFSDBAnswer struct { + Answer + Subtype uint16 `json:"subtype" groups:"short,normal,long,trace"` + Hostname string `json:"hostname" groups:"short,normal,long,trace"` +} + +type CAAAnswer struct { + Answer + Tag string `json:"tag" groups:"short,normal,long,trace"` + Value string `json:"value" groups:"short,normal,long,trace"` + Flag uint8 `json:"flag" groups:"short,normal,long,trace"` +} + +type CERTAnswer struct { + Answer + Type string `json:"type" groups:"short,normal,long,trace"` + KeyTag uint16 `json:"keytag" groups:"short,normal,long,trace"` + Algorithm string `json:"algorithm" groups:"short,normal,long,trace"` + Certificate string `json:"certificate" groups:"short,normal,long,trace"` +} + +type DNSKEYAnswer struct { + Answer + Flags uint16 `json:"flags" groups:"short,normal,long,trace"` + Protocol uint8 `json:"protocol" groups:"short,normal,long,trace"` + Algorithm uint8 `json:"algorithm" groups:"short,normal,long,trace"` + PublicKey string `json:"public_key" groups:"short,normal,long,trace"` +} + +type DSAnswer struct { + Answer + KeyTag uint16 `json:"key_tag" groups:"short,normal,long,trace"` + Algorithm uint8 `json:"algorithm" groups:"short,normal,long,trace"` + DigestType uint8 `json:"digest_type" groups:"short,normal,long,trace"` + Digest string `json:"digest" groups:"short,normal,long,trace"` +} + +type GPOSAnswer struct { + Answer + Longitude string `json:"preference" groups:"short,normal,long,trace"` + Latitude string `json:"map822" groups:"short,normal,long,trace"` + Altitude string `json:"mapx400" groups:"short,normal,long,trace"` +} + +type HINFOAnswer struct { + Answer + Cpu string `json:"cpu" groups:"short,normal,long,trace"` + Os string `json:"os" groups:"short,normal,long,trace"` +} + +type HIPAnswer struct { + Answer + HitLength uint8 `json:"hit_length" groups:"short,normal,long,trace"` + PublicKeyAlgorithm uint8 `json:"pubkey_algo" groups:"short,normal,long,trace"` + PublicKeyLength uint16 `json:"pubkey_len" groups:"short,normal,long,trace"` + Hit string `json:"hit" groups:"short,normal,long,trace"` + PublicKey string `json:"pubkey" groups:"short,normal,long,trace"` + RendezvousServers []string `json:"rendezvous_servers" groups:"short,normal,long,trace"` +} + +type LOCAnswer struct { + Answer + Version uint8 `json:"version" groups:"short,normal,long,trace"` + Size uint8 `json:"size" groups:"short,normal,long,trace"` + HorizPre uint8 `json:"horizontal_pre" groups:"short,normal,long,trace"` + VertPre uint8 `json:"vertical_pre" groups:"short,normal,long,trace"` + Latitude uint32 `json:"latitude" groups:"short,normal,long,trace"` + Longitude uint32 `json:"longitude" groups:"short,normal,long,trace"` + Altitude uint32 `json:"altitude" groups:"short,normal,long,trace"` +} + +type MINFOAnswer struct { + Answer + Rmail string `json:"rmail" groups:"short,normal,long,trace"` + Email string `json:"email" groups:"short,normal,long,trace"` +} + +type NAPTRAnswer struct { + Answer + Order uint16 `json:"order" groups:"short,normal,long,trace"` + Preference uint16 `json:"preference" groups:"short,normal,long,trace"` + Flags string `json:"flags" groups:"short,normal,long,trace"` + Service string `json:"service" groups:"short,normal,long,trace"` + Regexp string `json:"regexp" groups:"short,normal,long,trace"` + Replacement string `json:"replacement" groups:"short,normal,long,trace"` +} + +type NSECAnswer struct { + Answer + NextDomain string `json:"next_domain" groups:"short,normal,long,trace"` + // TODO(zakir): this name doesn't seem right. Look at RFC. + TypeBitMap string `json:"type_bit_map" groups:"short,normal,long,trace"` +} + +type NSEC3Answer struct { + Answer + HashAlgorithm uint8 `json:"hash_algorithm" groups:"short,normal,long,trace"` + Flags uint8 `json:"flags" groups:"short,normal,long,trace"` + Iterations uint16 `json:"iterations" groups:"short,normal,long,trace"` + Salt string `json:"salt" groups:"short,normal,long,trace"` + NextDomain string `json:"next_domain" groups:"short,normal,long,trace"` + TypeBitMap string `json:"type_bit_map" groups:"short,normal,long,trace"` +} + +type NSEC3ParamAnswer struct { + Answer + HashAlgorithm uint8 `json:"hash_algorithm" groups:"short,normal,long,trace"` + Flags uint8 `json:"flags" groups:"short,normal,long,trace"` + Iterations uint16 `json:"iterations" groups:"short,normal,long,trace"` + Salt string `json:"salt" groups:"short,normal,long,trace"` +} + +type PrefAnswer struct { + Answer + Preference uint16 `json:"preference" groups:"short,normal,long,trace"` +} + +type PXAnswer struct { + Answer + Preference uint16 `json:"preference" groups:"short,normal,long,trace"` + Map822 string `json:"map822" groups:"short,normal,long,trace"` + Mapx400 string `json:"mapx400" groups:"short,normal,long,trace"` +} + +type RRSIGAnswer struct { + Answer + TypeCovered uint16 `json:"type_covered" groups:"short,normal,long,trace"` + Algorithm uint8 `json:"algorithm" groups:"short,normal,long,trace"` + Labels uint8 `json:"labels" groups:"short,normal,long,trace"` + OriginalTtl uint32 `json:"original_ttl" groups:"short,normal,long,trace"` + Expiration string `json:"expiration" groups:"short,normal,long,trace"` + Inception string `json:"inception" groups:"short,normal,long,trace"` + KeyTag uint16 `json:"keytag" groups:"short,normal,long,trace"` + SignerName string `json:"signer_name" groups:"short,normal,long,trace"` + Signature string `json:"signature" groups:"short,normal,long,trace"` +} + +type RPAnswer struct { + Answer + Mbox string `json:"mbox" groups:"short,normal,long,trace"` + Txt string `json:"txt" groups:"short,normal,long,trace"` +} + +type SMIMEAAnswer struct { + Answer + Usage uint8 `json:"usage" groups:"short,normal,long,trace"` + Selector uint8 `json:"selector" groups:"short,normal,long,trace"` + MatchingType uint8 `json:"matching_type" groups:"short,normal,long,trace"` + Certificate string `json:"certificate" groups:"short,normal,long,trace"` +} + +type SOAAnswer struct { + Answer + Ns string `json:"ns" groups:"short,normal,long,trace"` + Mbox string `json:"mbox" groups:"short,normal,long,trace"` + Serial uint32 `json:"serial" groups:"short,normal,long,trace"` + Refresh uint32 `json:"refresh" groups:"short,normal,long,trace"` + Retry uint32 `json:"retry" groups:"short,normal,long,trace"` + Expire uint32 `json:"expire" groups:"short,normal,long,trace"` + Minttl uint32 `json:"min_ttl" groups:"short,normal,long,trace"` +} + +type SSHFPAnswer struct { + Answer + Algorithm uint8 `json:"algorithm" groups:"short,normal,long,trace"` + Type uint8 `json:"type" groups:"short,normal,long,trace"` + FingerPrint string `json:"fingerprint" groups:"short,normal,long,trace"` +} + +type SRVAnswer struct { + Answer + Priority uint16 `json:"priority" groups:"short,normal,long,trace"` + Weight uint16 `json:"weight" groups:"short,normal,long,trace"` + Port uint16 `json:"port" groups:"short,normal,long,trace"` + Target string `json:"target" groups:"short,normal,long,trace"` +} + +type SVCBAnswer struct { + Answer + Priority uint16 `json:"priority" groups:"short,normal,long,trace"` + Target string `json:"target" groups:"short,normal,long,trace"` + SVCParams map[string]interface{} `json:"svcparams,omitempty" groups:"short,normal,long,trace"` +} + +type TKEYAnswer struct { + Answer + Algorithm string `json:"algorithm" groups:"short,normal,long,trace"` + Inception string `json:"inception" groups:"short,normal,long,trace"` + Expiration string `json:"expiration" groups:"short,normal,long,trace"` + Mode uint16 `json:"mode" groups:"short,normal,long,trace"` + Error uint16 `json:"error" groups:"short,normal,long,trace"` + KeySize uint16 `json:"key_size" groups:"short,normal,long,trace"` + Key string `json:"key" groups:"short,normal,long,trace"` + OtherLen uint16 `json:"other_len" groups:"short,normal,long,trace"` + OtherData string `json:"other_data" groups:"short,normal,long,trace"` +} + +type TLSAAnswer struct { + Answer + CertUsage uint8 `json:"cert_usage" groups:"short,normal,long,trace"` + Selector uint8 `json:"selector" groups:"short,normal,long,trace"` + MatchingType uint8 `json:"matching_type" groups:"short,normal,long,trace"` + Certificate string `json:"certificate" groups:"short,normal,long,trace"` +} + +type TALINKAnswer struct { + Answer + PreviousName string `json:"previous_name" groups:"short,normal,long,trace"` + NextName string `json:"next_name" groups:"short,normal,long,trace"` +} + +type URIAnswer struct { + Answer + Priority uint16 `json:"previous_name" groups:"short,normal,long,trace"` + Weight uint16 `json:"previous_name" groups:"short,normal,long,trace"` + Target string `json:"previous_name" groups:"short,normal,long,trace"` +} + +// copy-paste from zmap/dns/types.go >>>>> +// +// Copyright (c) 2009 The Go Authors. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +const ( + escapedByteSmall = "" + + `\000\001\002\003\004\005\006\007\008\009` + + `\010\011\012\013\014\015\016\017\018\019` + + `\020\021\022\023\024\025\026\027\028\029` + + `\030\031` + escapedByteLarge = `\127\128\129` + + `\130\131\132\133\134\135\136\137\138\139` + + `\140\141\142\143\144\145\146\147\148\149` + + `\150\151\152\153\154\155\156\157\158\159` + + `\160\161\162\163\164\165\166\167\168\169` + + `\170\171\172\173\174\175\176\177\178\179` + + `\180\181\182\183\184\185\186\187\188\189` + + `\190\191\192\193\194\195\196\197\198\199` + + `\200\201\202\203\204\205\206\207\208\209` + + `\210\211\212\213\214\215\216\217\218\219` + + `\220\221\222\223\224\225\226\227\228\229` + + `\230\231\232\233\234\235\236\237\238\239` + + `\240\241\242\243\244\245\246\247\248\249` + + `\250\251\252\253\254\255` +) + +func escapeByte(b byte) string { + if b < ' ' { + return escapedByteSmall[b*4 : b*4+4] + } + + b -= '~' + 1 + // The cast here is needed as b*4 may overflow byte. + return escapedByteLarge[int(b)*4 : int(b)*4+4] +} + +func isDigit(b byte) bool { return b >= '0' && b <= '9' } + +func dddToByte(s []byte) byte { + _ = s[2] // bounds check hint to compiler; see golang.org/issue/14808 + return (s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0') +} + +func dddStringToByte(s string) byte { + _ = s[2] // bounds check hint to compiler; see golang.org/issue/14808 + return (s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0') +} + +func nextByte(s string, offset int) (byte, int) { + if offset >= len(s) { + return 0, 0 + } + if s[offset] != '\\' { + // not an escape sequence + return s[offset], 1 + } + switch len(s) - offset { + case 1: // dangling escape + return 0, 0 + case 2, 3: // too short to be \ddd + default: // maybe \ddd + if isDigit(s[offset+1]) && isDigit(s[offset+2]) && isDigit(s[offset+3]) { + return dddStringToByte(s[offset+1:]), 4 + } + } + // not \ddd, just an RFC 1035 "quoted" character + return s[offset+1], 2 +} +func euiToString(eui uint64, bits int) (hex string) { + switch bits { + case 64: + hex = fmt.Sprintf("%16.16x", eui) + hex = hex[0:2] + "-" + hex[2:4] + "-" + hex[4:6] + "-" + hex[6:8] + + "-" + hex[8:10] + "-" + hex[10:12] + "-" + hex[12:14] + "-" + hex[14:16] + case 48: + hex = fmt.Sprintf("%12.12x", eui) + hex = hex[0:2] + "-" + hex[2:4] + "-" + hex[4:6] + "-" + hex[6:8] + + "-" + hex[8:10] + "-" + hex[10:12] + } + return +} + +func sprintTxtOctet(s string) string { + var dst strings.Builder + dst.Grow(2 + len(s)) + dst.WriteByte('"') + for i := 0; i < len(s); { + if i+1 < len(s) && s[i] == '\\' && s[i+1] == '.' { + dst.WriteString(s[i : i+2]) + i += 2 + continue + } + b, n := nextByte(s, i) + switch { + case n == 0: + i++ // dangling back slash + case b == '.': + dst.WriteByte('.') + case b < ' ' || b > '~': + dst.WriteString(escapeByte(b)) + default: + dst.WriteByte(b) + } + i += n + } + dst.WriteByte('"') + return dst.String() +} + +// <<<<< END GOOGLE CODE + +func makeBitString(bm []uint16) string { + retv := "" + for _, v := range bm { + if retv == "" { + retv += dns.Type(v).String() + } else { + retv += " " + dns.Type(v).String() + } + } + return retv +} + +func makeBaseAnswer(hdr *dns.RR_Header, answer string) Answer { + return Answer{ + Ttl: hdr.Ttl, + Type: dns.Type(hdr.Rrtype).String(), + RrType: hdr.Rrtype, + Class: dns.Class(hdr.Class).String(), + RrClass: hdr.Class, + Name: strings.TrimSuffix(hdr.Name, "."), + Answer: answer} +} + +func makeSVCBAnswer(cAns *dns.SVCB) SVCBAnswer { + var params map[string]interface{} + if len(cAns.Value) > 0 { + params = make(map[string]interface{}) + for _, ikv := range cAns.Value { + // this could be reduced by adding, e.g., a new Data() + // method to the zmap/dns SVCBKeyValue interface + switch kv := ikv.(type) { + case *dns.SVCBMandatory: + keys := make([]string, len(kv.Code)) + for i, e := range kv.Code { + keys[i] = e.String() + } + params[ikv.Key().String()] = keys + case *dns.SVCBAlpn: + params[ikv.Key().String()] = kv.Alpn + case *dns.SVCBNoDefaultAlpn: + params[ikv.Key().String()] = true + case *dns.SVCBPort: + params[ikv.Key().String()] = kv.Port + case *dns.SVCBIPv4Hint: + params[ikv.Key().String()] = kv.Hint + case *dns.SVCBECHConfig: + params[ikv.Key().String()] = kv.ECH + case *dns.SVCBIPv6Hint: + params[ikv.Key().String()] = kv.Hint + case *dns.SVCBLocal: //SVCBLocal is the default case for unknown keys + params[ikv.Key().String()] = kv.Data + default: //should not happen + params["unknown"] = true + } + } + } + return SVCBAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Priority: cAns.Priority, + Target: cAns.Target, + SVCParams: params, + } +} + +func makeEDNSAnswer(cAns *dns.OPT) EDNSAnswer { + opttype := "EDNS" + flags := "" + if cAns.Do() { + flags = "do" + } + optRes := EDNSAnswer{ + Type: opttype + strconv.Itoa(int(cAns.Version())), + Version: cAns.Version(), + // RCODE omitted for now as no EDNS0 extension is supported in + // lookups for which an RCODE is defined. + //Rcode: dns.RcodeToString[cAns.ExtendedRcode()], + Flags: flags, + UDPSize: cAns.UDPSize(), + } + + for _, o := range cAns.Option { + switch o.(type) { + case *dns.EDNS0_LLQ: //OPT 1 + optRes.LLQ = &Edns0LLQ{ + Code: o.(*dns.EDNS0_LLQ).Code, + Version: o.(*dns.EDNS0_LLQ).Version, + Opcode: o.(*dns.EDNS0_LLQ).Opcode, + Error: o.(*dns.EDNS0_LLQ).Error, + Id: o.(*dns.EDNS0_LLQ).Id, + LeaseLife: o.(*dns.EDNS0_LLQ).LeaseLife, + } + case *dns.EDNS0_UL: // OPT 2 + optRes.UL = &Edns0UL{ + Code: o.(*dns.EDNS0_UL).Code, + Lease: o.(*dns.EDNS0_UL).Lease, + KeyLease: o.(*dns.EDNS0_UL).KeyLease, + } + case *dns.EDNS0_NSID: //OPT 3 + hexDecoded, err := hex.DecodeString(o.(*dns.EDNS0_NSID).Nsid) + if err != nil { + continue + } + optRes.NSID = &Edns0NSID{Nsid: string(hexDecoded)} + case *dns.EDNS0_DAU: //OPT 5 + optRes.DAU = &Edns0DAU{ + Code: o.(*dns.EDNS0_DAU).Code, + AlgCode: o.(*dns.EDNS0_DAU).String(), + } + case *dns.EDNS0_DHU: //OPT 6 + optRes.DHU = &Edns0DHU{ + Code: o.(*dns.EDNS0_DHU).Code, + AlgCode: o.(*dns.EDNS0_DHU).String(), + } + case *dns.EDNS0_N3U: //OPT 7 + optRes.N3U = &Edns0N3U{ + Code: o.(*dns.EDNS0_N3U).Code, + AlgCode: o.(*dns.EDNS0_N3U).String(), + } + case *dns.EDNS0_SUBNET: //OPT 8 + optRes.ClientSubnet = &Edns0ClientSubnet{ + SourceScope: o.(*dns.EDNS0_SUBNET).SourceScope, + Family: o.(*dns.EDNS0_SUBNET).Family, + Address: o.(*dns.EDNS0_SUBNET).Address.String(), + SourceNetmask: o.(*dns.EDNS0_SUBNET).SourceNetmask, + } + case *dns.EDNS0_EXPIRE: //OPT 9 + optRes.Expire = &Edns0Expire{ + Code: o.(*dns.EDNS0_EXPIRE).Code, + Expire: o.(*dns.EDNS0_EXPIRE).Expire, + } + case *dns.EDNS0_COOKIE: //OPT 11 + optRes.Cookie = &Edns0Cookie{Cookie: o.(*dns.EDNS0_COOKIE).Cookie} + case *dns.EDNS0_TCP_KEEPALIVE: //OPT 11 + optRes.TcpKeepalive = &Edns0TCPKeepalive{ + Code: o.(*dns.EDNS0_TCP_KEEPALIVE).Code, + Timeout: o.(*dns.EDNS0_TCP_KEEPALIVE).Timeout, + Length: o.(*dns.EDNS0_TCP_KEEPALIVE).Length, // deprecated, always equal to 0, keeping it here for a better readability + } + case *dns.EDNS0_PADDING: //OPT 12 + optRes.Padding = &Edns0Padding{Padding: o.(*dns.EDNS0_PADDING).String()} + case *dns.EDNS0_EDE: //OPT 15 + optRes.EDE = append(optRes.EDE, &Edns0Ede{ + InfoCode: o.(*dns.EDNS0_EDE).InfoCode, + ErrorCodeText: dns.ExtendedErrorCodeToString[o.(*dns.EDNS0_EDE).InfoCode], + ExtraText: o.(*dns.EDNS0_EDE).ExtraText, + }) + } + } + return optRes +} + +func ParseAnswer(ans dns.RR) interface{} { + switch cAns := ans.(type) { + // Prioritize common types in expected order + case *dns.A: + return makeBaseAnswer(&cAns.Hdr, cAns.A.String()) + case *dns.AAAA: + ip := cAns.AAAA.String() + // verify we really got full 16-byte address + if !cAns.AAAA.IsLoopback() && !cAns.AAAA.IsUnspecified() && len(cAns.AAAA) == net.IPv6len { + if cAns.AAAA.To4() != nil { + // we have a IPv4-mapped address, append prefix (#164) + ip = "::ffff:" + ip + } else { + v4compat := true + for _, o := range cAns.AAAA[:11] { + if o != 0 { + v4compat = false + break + } + } + if v4compat { + // we have a IPv4-compatible address, append prefix (#164) + ip = "::" + cAns.AAAA[12:].String() + } + } + } + return makeBaseAnswer(&cAns.Hdr, ip) + case *dns.NS: + return makeBaseAnswer(&cAns.Hdr, cAns.Ns) + case *dns.CNAME: + return makeBaseAnswer(&cAns.Hdr, cAns.Target) + case *dns.DNAME: + return makeBaseAnswer(&cAns.Hdr, cAns.Target) + case *dns.PTR: + return makeBaseAnswer(&cAns.Hdr, cAns.Ptr) + case *dns.MX: + return PrefAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, cAns.Mx), + Preference: cAns.Preference, + } + case *dns.SOA: + return SOAAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Ns: strings.TrimSuffix(cAns.Ns, "."), + Mbox: strings.TrimSuffix(cAns.Mbox, "."), + Serial: cAns.Serial, + Refresh: cAns.Refresh, + Retry: cAns.Retry, + Expire: cAns.Expire, + Minttl: cAns.Minttl, + } + case *dns.TXT: + return makeBaseAnswer(&cAns.Hdr, strings.Join(cAns.Txt, "\n")) + case *dns.CAA: + return CAAAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Tag: cAns.Tag, + Value: cAns.Value, + Flag: cAns.Flag, + } + case *dns.SRV: + return SRVAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Priority: cAns.Priority, + Weight: cAns.Weight, + Port: cAns.Port, + Target: cAns.Target, + } + case *dns.SPF: + return makeBaseAnswer(&cAns.Hdr, cAns.String()) + case *dns.DS: + return DSAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + KeyTag: cAns.KeyTag, + Algorithm: cAns.Algorithm, + DigestType: cAns.DigestType, + Digest: cAns.Digest, + } + case *dns.CDS: + return DSAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + KeyTag: cAns.KeyTag, + Algorithm: cAns.Algorithm, + DigestType: cAns.DigestType, + Digest: cAns.Digest, + } + case *dns.RRSIG: + return RRSIGAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + TypeCovered: cAns.TypeCovered, + Algorithm: cAns.Algorithm, + Labels: cAns.Labels, + OriginalTtl: cAns.OrigTtl, + Expiration: dns.TimeToString(cAns.Expiration), + Inception: dns.TimeToString(cAns.Inception), + KeyTag: cAns.KeyTag, + SignerName: cAns.SignerName, + Signature: cAns.Signature, + } + // begin "the rest". Protocols we won't very likely ever see and order + // would is effectively random. Hopefully, folks who are you using these + // are going to use them everywhere and branch prediction helps out. Not + // much else that we could do other than not try to parse them, which is + // worse + case *dns.NULL: + return makeBaseAnswer(&cAns.Hdr, cAns.Data) + case *dns.MB: + return makeBaseAnswer(&cAns.Hdr, cAns.Mb) + case *dns.MG: + return makeBaseAnswer(&cAns.Hdr, cAns.Mg) + case *dns.MF: + return makeBaseAnswer(&cAns.Hdr, cAns.Mf) + case *dns.MD: + return makeBaseAnswer(&cAns.Hdr, cAns.Md) + case *dns.NSAPPTR: + return makeBaseAnswer(&cAns.Hdr, cAns.Ptr) + case *dns.NIMLOC: + return makeBaseAnswer(&cAns.Hdr, cAns.Locator) + case *dns.OPENPGPKEY: + return makeBaseAnswer(&cAns.Hdr, cAns.PublicKey) + case *dns.AVC: + return makeBaseAnswer(&cAns.Hdr, strings.Join(cAns.Txt, "\n")) + case *dns.EID: + return makeBaseAnswer(&cAns.Hdr, cAns.Endpoint) + case *dns.UINFO: + return makeBaseAnswer(&cAns.Hdr, cAns.Uinfo) + case *dns.DHCID: + return makeBaseAnswer(&cAns.Hdr, cAns.Digest) + case *dns.NINFO: + return makeBaseAnswer(&cAns.Hdr, strings.Join(cAns.ZSData, "\n")) + case *dns.TKEY: + return TKEYAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Algorithm: cAns.Algorithm, + Expiration: dns.TimeToString(cAns.Expiration), + Inception: dns.TimeToString(cAns.Inception), + Mode: cAns.Mode, + Error: cAns.Error, + KeySize: cAns.KeySize, + Key: cAns.Key, + OtherLen: cAns.OtherLen, + OtherData: cAns.OtherData, + } + case *dns.TLSA: + return TLSAAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + CertUsage: cAns.Usage, + Selector: cAns.Selector, + MatchingType: cAns.MatchingType, + Certificate: cAns.Certificate, + } + case *dns.NSEC: + return NSECAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + NextDomain: strings.TrimSuffix(cAns.NextDomain, "."), + TypeBitMap: makeBitString(cAns.TypeBitMap), + } + case *dns.NAPTR: + return NAPTRAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Order: cAns.Order, + Preference: cAns.Preference, + Flags: cAns.Flags, + Service: cAns.Service, + Regexp: cAns.Regexp, + Replacement: cAns.Replacement, + } + case *dns.SIG: + return RRSIGAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + TypeCovered: cAns.TypeCovered, + Algorithm: cAns.Algorithm, + Labels: cAns.Labels, + OriginalTtl: cAns.OrigTtl, + Expiration: dns.TimeToString(cAns.Expiration), + Inception: dns.TimeToString(cAns.Inception), + KeyTag: cAns.KeyTag, + SignerName: cAns.SignerName, + Signature: cAns.Signature, + } + case *dns.HINFO: + return HINFOAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Cpu: cAns.Cpu, + Os: cAns.Os, + } + case *dns.MINFO: + return MINFOAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Rmail: cAns.Rmail, + Email: cAns.Email, + } + case *dns.NSEC3: + return NSEC3Answer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + HashAlgorithm: cAns.Hash, + Flags: cAns.Flags, + Iterations: cAns.Iterations, + Salt: cAns.Salt, + NextDomain: cAns.NextDomain, + TypeBitMap: makeBitString(cAns.TypeBitMap), + } + case *dns.NSEC3PARAM: + return NSEC3Answer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + HashAlgorithm: cAns.Hash, + Flags: cAns.Flags, + Iterations: cAns.Iterations, + Salt: cAns.Salt, + } + case *dns.DNSKEY: + return DNSKEYAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Flags: cAns.Flags, + Protocol: cAns.Protocol, + Algorithm: cAns.Algorithm, + PublicKey: cAns.PublicKey, + } + case *dns.CDNSKEY: + return DNSKEYAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Flags: cAns.Flags, + Protocol: cAns.Protocol, + Algorithm: cAns.Algorithm, + PublicKey: cAns.PublicKey, + } + case *dns.AFSDB: + return AFSDBAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Subtype: cAns.Subtype, + Hostname: cAns.Hostname, + } + case *dns.RT: + return PrefAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, cAns.Host), + Preference: cAns.Preference, + } + case *dns.NID: + node := fmt.Sprintf("%0.16x", cAns.NodeID) + return PrefAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, node), + Preference: cAns.Preference, + } + case *dns.X25: + return makeBaseAnswer(&cAns.Hdr, cAns.PSDNAddress) + case *dns.CERT: + return CERTAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Type: dns.CertTypeToString[cAns.Type], + KeyTag: cAns.KeyTag, + Algorithm: dns.AlgorithmToString[cAns.Algorithm], + Certificate: cAns.Certificate, + } + case *dns.PX: + return PXAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Preference: cAns.Preference, + Map822: cAns.Map822, + Mapx400: cAns.Mapx400, + } + case *dns.GPOS: + return GPOSAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Longitude: cAns.Longitude, + Latitude: cAns.Latitude, + Altitude: cAns.Altitude, + } + case *dns.LOC: + // This has the raw DNS values, which are not very human readable + // TODO: convert DNS types into usable values + return LOCAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Version: cAns.Version, + Size: cAns.Size, + HorizPre: cAns.HorizPre, + VertPre: cAns.VertPre, + Longitude: cAns.Longitude, + Latitude: cAns.Latitude, + Altitude: cAns.Altitude, + } + case *dns.HIP: + return HIPAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + HitLength: cAns.HitLength, + PublicKeyAlgorithm: cAns.PublicKeyAlgorithm, + PublicKeyLength: cAns.PublicKeyLength, + Hit: cAns.Hit, + PublicKey: cAns.PublicKey, + RendezvousServers: cAns.RendezvousServers, + } + case *dns.KX: + return PrefAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, cAns.Exchanger), + Preference: cAns.Preference, + } + case *dns.SSHFP: + return SSHFPAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Algorithm: cAns.Algorithm, + Type: cAns.Type, + FingerPrint: cAns.FingerPrint, + } + case *dns.SMIMEA: + return SMIMEAAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + Usage: cAns.Usage, + Selector: cAns.Selector, + MatchingType: cAns.MatchingType, + Certificate: cAns.Certificate, + } + case *dns.TALINK: + return TALINKAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, ""), + PreviousName: cAns.PreviousName, + NextName: cAns.NextName, + } + case *dns.L32: + return PrefAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, cAns.Locator32.String()), + Preference: cAns.Preference, + } + case *dns.L64: + node := fmt.Sprintf("%0.16X", cAns.Locator64) + return PrefAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, node), + Preference: cAns.Preference, + } + case *dns.EUI48: + return makeBaseAnswer(&cAns.Hdr, euiToString(cAns.Address, 48)) + case *dns.EUI64: + return makeBaseAnswer(&cAns.Hdr, euiToString(cAns.Address, 64)) + case *dns.UID: + return makeBaseAnswer(&cAns.Hdr, strconv.FormatInt(int64(cAns.Uid), 10)) + case *dns.GID: + return makeBaseAnswer(&cAns.Hdr, strconv.FormatInt(int64(cAns.Gid), 10)) + case *dns.LP: + return PrefAnswer{ + Answer: makeBaseAnswer(&cAns.Hdr, cAns.Fqdn), + Preference: cAns.Preference, + } + case *dns.HTTPS: + return makeSVCBAnswer(&cAns.SVCB) + case *dns.SVCB: + return makeSVCBAnswer(cAns) + case *dns.OPT: + return makeEDNSAnswer(cAns) + default: + return struct { + Type string `json:"type"` + rrType uint16 + Class string `json:"class"` + rrClass uint16 + Unparsed dns.RR `json:"-"` + }{ + Type: dns.Type(ans.Header().Rrtype).String(), + rrType: ans.Header().Rrtype, + Class: dns.Class(ans.Header().Class).String(), + rrClass: ans.Header().Class, + Unparsed: ans, + } + } +} diff --git a/pkg/refactored_zdns/cache.go b/pkg/refactored_zdns/cache.go new file mode 100644 index 00000000..839ff3cf --- /dev/null +++ b/pkg/refactored_zdns/cache.go @@ -0,0 +1,161 @@ +/* ZDNS Copyright 2024 Regents of the University of Michigan +* +* Licensed under the Apache License, Version 2.0 (the "License"); you may not +* use this file except in compliance with the License. You may obtain a copy +* of the License at http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +* implied. See the License for the specific language governing +* permissions and limitations under the License. + */ + +package refactored_zdns + +import ( + "time" + + log "github.com/sirupsen/logrus" + + "github.com/zmap/dns" + "github.com/zmap/zdns/cachehash" +) + +type IsCached bool + +type TimedAnswer struct { + Answer interface{} + ExpiresAt time.Time +} + +type CachedResult struct { + Answers map[interface{}]TimedAnswer +} + +type Cache struct { + IterativeCache cachehash.ShardedCacheHash +} + +func (s *Cache) Init(cacheSize int) { + s.IterativeCache.Init(cacheSize, 4096) +} + +func (s *Cache) VerboseLog(depth int, args ...interface{}) { + log.Debug(makeVerbosePrefix(depth), args) +} + +func (s *Cache) AddCachedAnswer(answer interface{}, depth int) { + a, ok := answer.(Answer) + if !ok { + // we can't cache this entry because we have no idea what to name it + return + } + q := questionFromAnswer(a) + + // only cache records that can help prevent future iteration: A(AAA), NS, (C|D)NAME. + // This will prevent some entries that will never help future iteration (e.g., PTR) + // from causing unnecessary cache evictions. + // TODO: this is overly broad right now and will unnecessarily cache some leaf A/AAAA records. However, + // it's a lot of work to understand _why_ we're doing a specific lookup and this will still help + // in other cases, e.g., PTR lookups + if !(q.Type == dns.TypeA || q.Type == dns.TypeAAAA || q.Type == dns.TypeNS || q.Type == dns.TypeDNAME || q.Type == dns.TypeCNAME) { + return + } + expiresAt := time.Now().Add(time.Duration(a.Ttl) * time.Second) + s.IterativeCache.Lock(q) + // don't bother to move this to the top of the linked list. we're going + // to add this record back in momentarily and that will take care of this + i, ok := s.IterativeCache.GetNoMove(q) + ca, ok := i.(CachedResult) + if !ok && i != nil { + panic("unable to cast cached result") + } + if !ok { + ca = CachedResult{} + ca.Answers = make(map[interface{}]TimedAnswer) + } + // we have an existing record. Let's add this answer to it. + ta := TimedAnswer{ + Answer: answer, + ExpiresAt: expiresAt} + ca.Answers[a] = ta + s.IterativeCache.Add(q, ca) + s.VerboseLog(depth+1, "Add cached answer ", q, " ", ca) + s.IterativeCache.Unlock(q) +} + +func (s *Cache) GetCachedResult(q Question, isAuthCheck bool, depth int) (Result, bool) { + s.VerboseLog(depth+1, "Cache request for: ", q.Name, " (", q.Type, ")") + var retv Result + s.IterativeCache.Lock(q) + unres, ok := s.IterativeCache.Get(q) + if !ok { // nothing found + s.VerboseLog(depth+2, "-> no entry found in cache") + s.IterativeCache.Unlock(q) + return retv, false + } + retv.Authorities = make([]interface{}, 0) + retv.Answers = make([]interface{}, 0) + retv.Additional = make([]interface{}, 0) + cachedRes, ok := unres.(CachedResult) + if !ok { + panic("bad cache entry") + } + // great we have a result. let's go through the entries and build + // and build a result. In the process, throw away anything that's expired + now := time.Now() + for k, cachedAnswer := range cachedRes.Answers { + if cachedAnswer.ExpiresAt.Before(now) { + // if we have a write lock, we can perform the necessary actions + // and then write this back to the cache. However, if we don't, + // we need to start this process over with a write lock + s.VerboseLog(depth+2, "Expiring cache entry ", k) + delete(cachedRes.Answers, k) + } else { + // this result is valid. append it to the Result we're going to hand to the user + if isAuthCheck { + retv.Authorities = append(retv.Authorities, cachedAnswer.Answer) + } else { + retv.Answers = append(retv.Answers, cachedAnswer.Answer) + } + } + } + s.IterativeCache.Unlock(q) + // Don't return an empty response. + if len(retv.Answers) == 0 && len(retv.Authorities) == 0 && len(retv.Additional) == 0 { + s.VerboseLog(depth+2, "-> no entry found in cache, after expiration") + var emptyRetv Result + return emptyRetv, false + } + + s.VerboseLog(depth+2, "Cache hit: ", retv) + return retv, true +} + +func (s *Cache) SafeAddCachedAnswer(a interface{}, layer string, debugType string, depth int) { + ans, ok := a.(Answer) + if !ok { + s.VerboseLog(depth+1, "unable to cast ", debugType, ": ", layer, ": ", a) + return + } + if ok, _ := nameIsBeneath(ans.Name, layer); !ok { + log.Info("detected poison ", debugType, ": ", ans.Name, "(", ans.Type, "): ", layer, ": ", a) + return + } + s.AddCachedAnswer(a, depth) +} + +func (s *Cache) CacheUpdate(layer string, result Result, depth int) { + for _, a := range result.Additional { + s.SafeAddCachedAnswer(a, layer, "additional", depth) + } + for _, a := range result.Authorities { + s.SafeAddCachedAnswer(a, layer, "authority", depth) + } + if result.Flags.Authoritative == true { + for _, a := range result.Answers { + s.SafeAddCachedAnswer(a, layer, "answer", depth) + } + } +} diff --git a/pkg/refactored_zdns/conf.go b/pkg/refactored_zdns/conf.go new file mode 100644 index 00000000..d8854c70 --- /dev/null +++ b/pkg/refactored_zdns/conf.go @@ -0,0 +1,125 @@ +/* + * ZDNS Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package refactored_zdns + +import ( + "net" + "time" + + "github.com/zmap/dns" +) + +type GlobalConf struct { + Threads int + Timeout time.Duration + IterationTimeout time.Duration + Retries int + AlexaFormat bool + MetadataFormat bool + NameServerInputFormat bool + IterativeResolution bool + LookupAllNameServers bool + + ResultVerbosity string + IncludeInOutput string + OutputGroups []string + + MaxDepth int + CacheSize int + GoMaxProcs int + Verbosity int + TimeFormat string + PassedName string + NameServersSpecified bool + NameServers []string + TCPOnly bool + UDPOnly bool + RecycleSockets bool + LocalAddrSpecified bool + LocalAddrs []net.IP + ClientSubnet *dns.EDNS0_SUBNET + NSID *dns.EDNS0_NSID + Dnssec bool + CheckingDisabled bool + + InputFilePath string + OutputFilePath string + LogFilePath string + MetadataFilePath string + + NamePrefix string + NameOverride string + NameServerMode bool + + Module string + Class uint16 +} + +type Metadata struct { + Names int `json:"names"` + Status map[string]int `json:"statuses"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + NameServers []string `json:"name_servers"` + Timeout int `json:"timeout"` + Retries int `json:"retries"` + Conf *GlobalConf `json:"conf"` +} + +type TargetedDomain struct { + Domain string `json:"domain"` + Nameservers []string `json:"nameservers"` +} + +type Status string + +const ( + // Standardized RCODE + STATUS_NOERROR Status = "NOERROR" // No Error + STATUS_FORMERR Status = "FORMERR" // Format Error + STATUS_SERVFAIL Status = "SERVFAIL" + STATUS_NXDOMAIN Status = "NXDOMAIN" + STATUS_NOTIMP Status = "NOT_IMPL" + STATUS_REFUSED Status = "REFUSED" + STATUS_TRUNCATED Status = "TRUNCATED" + + STATUS_ERROR Status = "ERROR" + STATUS_AUTHFAIL Status = "AUTHFAIL" + STATUS_NO_RECORD Status = "NORECORD" + STATUS_BLACKLIST Status = "BLACKLIST" + STATUS_NO_OUTPUT Status = "NO_OUTPUT" + STATUS_NO_ANSWER Status = "NO_ANSWER" + STATUS_ILLEGAL_INPUT Status = "ILLEGAL_INPUT" + STATUS_TIMEOUT Status = "TIMEOUT" + STATUS_ITER_TIMEOUT Status = "ITERATIVE_TIMEOUT" + STATUS_TEMPORARY Status = "TEMPORARY" + STATUS_NOAUTH Status = "NOAUTH" + STATUS_NODATA Status = "NODATA" +) + +var RootServers = [...]string{ + "198.41.0.4:53", + "192.228.79.201:53", + "192.33.4.12:53", + "199.7.91.13:53", + "192.203.230.10:53", + "192.5.5.241:53", + "192.112.36.4:53", + "198.97.190.53:53", + "192.36.148.17:53", + "192.58.128.30:53", + "193.0.14.129:53", + "199.7.83.42:53", + "202.12.27.33:53"} diff --git a/pkg/refactored_zdns/edns.go b/pkg/refactored_zdns/edns.go new file mode 100644 index 00000000..4749952e --- /dev/null +++ b/pkg/refactored_zdns/edns.go @@ -0,0 +1,114 @@ +/* + * ZDNS Copyright 2023 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package refactored_zdns + +// Structures covering DNS EDNS0 Option Codes (https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-11) + +// Edns0LLQ OPT 1 +type Edns0LLQ struct { + Code uint16 `json:"code" groups:"short,normal,long,trace"` + Version uint16 `json:"version" groups:"short,normal,long,trace"` + Opcode uint16 `json:"opcode" groups:"short,normal,long,trace"` + Error uint16 `json:"error" groups:"short,normal,long,trace"` + Id uint64 `json:"id" groups:"short,normal,long,trace"` + LeaseLife uint32 `json:"lease_life" groups:"short,normal,long,trace"` +} + +// Edns0UL OPT 2 +type Edns0UL struct { + Code uint16 `json:"code" groups:"short,normal,long,trace"` + Lease uint32 `json:"lease" groups:"short,normal,long,trace"` + KeyLease uint32 `json:"key_lease" groups:"short,normal,long,trace"` +} + +// Edns0NSID OPT 3 +type Edns0NSID struct { + Nsid string `json:"nsid" groups:"short,normal,long,trace"` +} + +// Edns0DAU OPT 5 +type Edns0DAU struct { + Code uint16 `json:"code" groups:"short,normal,long,trace"` + AlgCode string `json:"alg_code" groups:"short,normal,long,trace"` +} + +// Edns0DHU OPT 6 +type Edns0DHU struct { + Code uint16 `json:"code" groups:"short,normal,long,trace"` + AlgCode string `json:"alg_code" groups:"short,normal,long,trace"` +} + +// Edns0N3U OPT 7 +type Edns0N3U struct { + Code uint16 `json:"code" groups:"short,normal,long,trace"` + AlgCode string `json:"alg_code" groups:"short,normal,long,trace"` +} + +// Edns0ClientSubnet OPT 8 +type Edns0ClientSubnet struct { + Family uint16 `json:"family" groups:"short,normal,long,trace"` + SourceNetmask uint8 `json:"source_netmask" groups:"short,normal,long,trace"` + SourceScope uint8 `json:"source_scope" groups:"short,normal,long,trace"` + Address string `json:"address" groups:"short,normal,long,trace"` +} + +// Edns0Expire OPT 9 +type Edns0Expire struct { + Code uint16 `json:"code" groups:"short,normal,long,trace"` + Expire uint32 `json:"expire" groups:"short,normal,long,trace"` +} + +// Edns0Cookie OPT 10 +type Edns0Cookie struct { + Cookie string `json:"cookie" groups:"short,normal,long,trace"` +} + +// Edns0TCPKeepalive OPT 11 +type Edns0TCPKeepalive struct { + Code uint16 `json:"code" groups:"short,normal,long,trace"` + Timeout uint16 `json:"timeout" groups:"short,normal,long,trace"` + Length uint16 `json:"length" groups:"short,normal,long,trace"` +} + +// Edns0Padding OPT 12 +type Edns0Padding struct { + Padding string `json:"padding" groups:"short,normal,long,trace"` +} + +// Edns0Ede OPT15 +type Edns0Ede struct { + InfoCode uint16 `json:"info_code" groups:"short,normal,long,trace"` + ErrorCodeText string `json:"error_text" groups:"short,normal,long,trace"` + ExtraText string `json:"extra_text" groups:"short,normal,long,trace"` +} + +type EDNSAnswer struct { + Type string `json:"type" groups:"short,normal,long,trace"` + Version uint8 `json:"version" groups:"short,normal,long,trace"` + Flags string `json:"flags" groups:"short,normal,long,trace"` + UDPSize uint16 `json:"udpsize" groups:"short,normal,long,trace"` + LLQ *Edns0LLQ `json:"llq,omitempty" groups:"short,normal,long,trace"` //not implemented + UL *Edns0UL `json:"ul,omitempty" groups:"short,normal,long,trace"` //not implemented + NSID *Edns0NSID `json:"nsid,omitempty" groups:"short,normal,long,trace"` + DAU *Edns0DAU `json:"dau,omitempty" groups:"short,normal,long,trace"` //not implemented + DHU *Edns0DHU `json:"dhu,omitempty" groups:"short,normal,long,trace"` //not implemented + N3U *Edns0N3U `json:"n3u,omitempty" groups:"short,normal,long,trace"` //not implemented + ClientSubnet *Edns0ClientSubnet `json:"csubnet,omitempty" groups:"short,normal,long,trace"` + Expire *Edns0Expire `json:"expire,omitempty" groups:"short,normal,long,trace"` //not implemented + Cookie *Edns0Cookie `json:"cookie,omitempty" groups:"short,normal,long,trace"` //not implemented + TcpKeepalive *Edns0TCPKeepalive `json:"tcp_keepalive,omitempty" groups:"short,normal,long,trace"` //not implemented + Padding *Edns0Padding `json:"padding,omitempty" groups:"short,normal,long,trace"` //not implemented + EDE []*Edns0Ede `json:"ede,omitempty" groups:"short,normal,long,trace"` +} diff --git a/pkg/refactored_zdns/lookup.go b/pkg/refactored_zdns/lookup.go new file mode 100644 index 00000000..f8cb1ce8 --- /dev/null +++ b/pkg/refactored_zdns/lookup.go @@ -0,0 +1,424 @@ +package refactored_zdns + +import ( + "context" + "errors" + "fmt" + log "github.com/sirupsen/logrus" + "github.com/zmap/dns" + "net" + "strings" + "time" +) + +// GetDNSServers returns a list of DNS servers from a file, or an error if one occurs +func GetDNSServers(path string) ([]string, error) { + c, err := dns.ClientConfigFromFile(path) + if err != nil { + return []string{}, fmt.Errorf("error reading DNS config file: %w", err) + } + var servers []string + for _, s := range c.Servers { + if s[0:1] != "[" && strings.Contains(s, ":") { + s = "[" + s + "]" + } + full := strings.Join([]string{s, c.Port}, ":") + servers = append(servers, full) + } + return servers, nil +} + +func (r *Resolver) iterateOnAuthorities(q Question, depth int, result Result, layer string, trace []interface{}) (Result, []interface{}, Status, error) { + if len(result.Authorities) == 0 { + var r Result + return r, trace, STATUS_NOAUTH, nil + } + for i, elem := range result.Authorities { + r.VerboseLog(depth+1, "Trying Authority: ", elem) + ns, ns_status, layer, trace := r.extractAuthority(elem, layer, depth, result, trace) + r.VerboseLog((depth + 1), "Output from extract authorities: ", ns) + if ns_status == STATUS_ITER_TIMEOUT { + r.VerboseLog((depth + 2), "--> Hit iterative timeout: ") + var r Result + return r, trace, STATUS_ITER_TIMEOUT, nil + } + if ns_status != STATUS_NOERROR { + var err error + new_status, err := handleStatus(&ns_status, err) + // default case we continue + if new_status == nil && err == nil { + if i+1 == len(result.Authorities) { + r.VerboseLog((depth + 2), "--> Auth find Failed. Unknown error. No more authorities to try, terminating: ", ns_status) + var r Result + return r, trace, ns_status, err + } else { + r.VerboseLog((depth + 2), "--> Auth find Failed. Unknown error. Continue: ", ns_status) + continue + } + } else { + // otherwise we hit a status we know + var localResult Result + if i+1 == len(result.Authorities) { + // We don't allow the continue fall through in order to report the last auth falure code, not STATUS_EROR + r.VerboseLog((depth + 2), "--> Final auth find non-success. Last auth. Terminating: ", ns_status) + return localResult, trace, *new_status, err + } else { + r.VerboseLog((depth + 2), "--> Auth find non-success. Trying next: ", ns_status) + continue + } + } + } + iterateResult, trace, status, err := r.iterativeLookup(q, ns, depth+1, layer, trace) + if isStatusAnswer(status) { + r.VerboseLog((depth + 1), "--> Auth Resolution success: ", status) + return iterateResult, trace, status, err + } else if i+1 < len(result.Authorities) { + r.VerboseLog((depth + 2), "--> Auth resolution of ", ns, " Failed: ", status, ". Will try next authority") + continue + } else { + // We don't allow the continue fall through in order to report the last auth falure code, not STATUS_EROR + r.VerboseLog((depth + 2), "--> Iterative resolution of ", q.Name, " at ", ns, " Failed. Last auth. Terminating: ", status) + return iterateResult, trace, status, err + } + } + panic("should not be able to reach here") +} + +func handleStatus(status *Status, err error) (*Status, error) { + switch *status { + case STATUS_ITER_TIMEOUT: + return status, err + case STATUS_NXDOMAIN: + return status, nil + case STATUS_SERVFAIL: + return status, nil + case STATUS_REFUSED: + return status, nil + case STATUS_AUTHFAIL: + return status, nil + case STATUS_NO_RECORD: + return status, nil + case STATUS_BLACKLIST: + return status, nil + case STATUS_NO_OUTPUT: + return status, nil + case STATUS_NO_ANSWER: + return status, nil + case STATUS_TRUNCATED: + return status, nil + case STATUS_ILLEGAL_INPUT: + return status, nil + case STATUS_TEMPORARY: + return status, nil + default: + var s *Status + return s, nil + } +} + +func (r *Resolver) extractAuthority(authority interface{}, layer string, depth int, result Result, trace []interface{}) (string, Status, string, []interface{}) { + + // Is it an answer + ans, ok := authority.(Answer) + if !ok { + return "", STATUS_FORMERR, layer, trace + } + + // Is the layering correct + ok, layer = nameIsBeneath(ans.Name, layer) + if !ok { + return "", STATUS_AUTHFAIL, layer, trace + } + + server := strings.TrimSuffix(ans.Answer, ".") + + // Short circuit a lookup from the glue + // Normally this would be handled by caching, but we want to support following glue + // that would normally be cache poison. Because it's "ok" and quite common + res, status := checkGlue(server, depth, result) + if status != STATUS_NOERROR { + // Fall through to normal query + var q Question + q.Name = server + q.Type = dns.TypeA + q.Class = dns.ClassINET + res, trace, status, _ = r.iterativeLookup(q, r.NameServer, depth+1, ".", trace) + } + if status == STATUS_ITER_TIMEOUT { + return "", status, "", trace + } + if status == STATUS_NOERROR { + // XXX we don't actually check the question here + for _, inner_a := range res.Answers { + inner_ans, ok := inner_a.(Answer) + if !ok { + continue + } + if inner_ans.Type == "A" { + server := strings.TrimSuffix(inner_ans.Answer, ".") + ":53" + return server, STATUS_NOERROR, layer, trace + } + } + } + return "", STATUS_SERVFAIL, layer, trace +} + +/* +iterativeLookup + cachedRetryingLookup + retryingLookup + doLookup + wireLookup +*/ + +func (r *Resolver) iterativeLookup(q Question, nameServer string, + depth int, layer string, trace []interface{}) (Result, Trace, Status, error) { + // + if log.GetLevel() == log.DebugLevel { + r.VerboseLog((depth), "iterative lookup for ", q.Name, " (", q.Type, ") against ", nameServer, " layer ", layer) + } + if depth > r.maxDepth { + var result Result + r.VerboseLog((depth + 1), "-> Max recursion depth reached") + return result, trace, STATUS_ERROR, errors.New("Max recursion depth reached") + } + ctx, cancel := context.WithTimeout(context.Background(), r.iterativeTimeout) + defer cancel() + result, isCached, status, try, err := r.cachedRetryingLookup(ctx, q, nameServer, layer, depth) + if r.Trace && status == STATUS_NOERROR { + var t TraceStep + t.Result = result + t.DnsType = q.Type + t.DnsClass = q.Class + t.Name = q.Name + t.NameServer = nameServer + t.Layer = layer + t.Depth = depth + t.Cached = isCached + t.Try = try + trace = append(trace, t) + + } + if status != STATUS_NOERROR { + r.VerboseLog((depth + 1), "-> error occurred during lookup") + return result, trace, status, err + } else if len(result.Answers) != 0 || result.Flags.Authoritative == true { + if len(result.Answers) != 0 { + r.VerboseLog((depth + 1), "-> answers found") + if len(result.Authorities) > 0 { + r.VerboseLog((depth + 2), "Dropping ", len(result.Authorities), " authority answers from output") + result.Authorities = make([]interface{}, 0) + } + if len(result.Additional) > 0 { + r.VerboseLog((depth + 2), "Dropping ", len(result.Additional), " additional answers from output") + result.Additional = make([]interface{}, 0) + } + } else { + r.VerboseLog((depth + 1), "-> authoritative response found") + } + return result, trace, status, err + } else if len(result.Authorities) != 0 { + r.VerboseLog((depth + 1), "-> Authority found, iterating") + return r.iterateOnAuthorities(q, depth, result, layer, trace) + } else { + r.VerboseLog((depth + 1), "-> No Authority found, error") + return result, trace, STATUS_ERROR, errors.New("NOERROR record without any answers or authorities") + } +} + +func (r *Resolver) cachedRetryingLookup(ctx context.Context, q Question, nameServer, layer string, depth int) (Result, IsCached, Status, int, error) { + var isCached IsCached + isCached = false + r.VerboseLog(depth+1, "Cached retrying lookup. Name: ", q, ", Layer: ", layer, ", Nameserver: ", nameServer) + + // Check if the timeout has been reached + select { + case <-ctx.Done(): + r.VerboseLog(depth+2, "ITERATIVE_TIMEOUT ", q, ", Layer: ", layer, ", Nameserver: ", nameServer) + var r Result + return r, isCached, STATUS_ITER_TIMEOUT, 0, nil + default: + // Timeout not reached, continue + } + // First, we check the answer + cachedResult, ok := r.cache.GetCachedResult(q, false, depth+1) + if ok { + isCached = true + return cachedResult, isCached, STATUS_NOERROR, 0, nil + } + + nameServerIP, _, err := net.SplitHostPort(nameServer) + // Stop if we hit a nameserver we don't want to hit + // TODO Phillip, fix this locking code to be safer, ie within a function and a defer unlock() + // Could wrap the blacklist in a SafeBlacklist wrapper struct that handles the locking + if r.blacklist != nil { + r.blMu.Lock() + if blacklisted, err := r.blacklist.IsBlacklisted(nameServerIP); err != nil { + r.blMu.Unlock() + var r Result + return r, isCached, STATUS_ERROR, 0, err + } else if blacklisted { + r.blMu.Unlock() + var r Result + return r, isCached, STATUS_BLACKLIST, 0, nil + } + r.blMu.Unlock() + } + + // Now, we check the authoritative: + name := strings.ToLower(q.Name) + layer = strings.ToLower(layer) + authName, err := nextAuthority(name, layer) + if err != nil { + var r Result + return r, isCached, STATUS_AUTHFAIL, 0, err + } + if name != layer && authName != layer { + if authName == "" { + var r Result + return r, isCached, STATUS_AUTHFAIL, 0, nil + } + var qAuth Question + qAuth.Name = authName + qAuth.Type = dns.TypeNS + qAuth.Class = dns.ClassINET + + if cachedResult, ok = r.cache.GetCachedResult(qAuth, true, depth+2); ok { + isCached = true + return cachedResult, isCached, STATUS_NOERROR, 0, nil + } + } + + // Alright, we're not sure what to do, go to the wire. + result, status, try, err := r.retryingLookup(q, nameServer, false) + + r.cache.CacheUpdate(layer, result, depth+2) + return result, isCached, status, try, err +} + +// retryingLookup wraps around wireLookup to perform a DNS lookup with retries +func (r *Resolver) retryingLookup(q Question, nameServer string, recursive bool) (Result, Status, int, error) { + r.VerboseLog(1, "****WIRE LOOKUP*** ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) + + var origTimeout time.Duration + if r.networkConns.UDPClient != nil { + origTimeout = r.networkConns.UDPClient.Timeout + } else { + origTimeout = r.networkConns.TCPClient.Timeout + } + for i := 0; i <= r.retries; i++ { + udpClient := r.networkConns.UDPClient + tcpClient := r.networkConns.TCPClient + conn := r.networkConns.Conn + + result, status, err := wireLookup(udpClient, tcpClient, conn, q, nameServer, recursive, r.ednsOptions, r.dnsSecEnabled, r.checkingDisabled) + if (status != STATUS_TIMEOUT && status != STATUS_TEMPORARY) || i == r.retries { + if r.networkConns.UDPClient != nil { + r.networkConns.UDPClient.Timeout = origTimeout + } + if r.networkConns.TCPClient != nil { + r.networkConns.TCPClient.Timeout = origTimeout + } + return result, status, (i + 1), err + } + if r.networkConns.UDPClient != nil { + r.networkConns.UDPClient.Timeout = 2 * r.networkConns.UDPClient.Timeout + } + if r.networkConns.TCPClient != nil { + r.networkConns.TCPClient.Timeout = 2 * r.networkConns.TCPClient.Timeout + } + } + panic("loop must return") +} + +// wireLookup performs a DNS lookup on-the-wire with the given parameters +// Attempts a UDP lookup first, then falls back to TCP if necessary (if the UDP response encounters an error or is truncated) +func wireLookup(udp *dns.Client, tcp *dns.Client, conn *dns.Conn, q Question, nameServer string, recursive bool, ednsOptions []dns.EDNS0, dnssec bool, checkingDisabled bool) (Result, Status, error) { + res := Result{Answers: []interface{}{}, Authorities: []interface{}{}, Additional: []interface{}{}} + res.Resolver = nameServer + + m := new(dns.Msg) + m.SetQuestion(dotName(q.Name), q.Type) + m.Question[0].Qclass = q.Class + m.RecursionDesired = recursive + m.CheckingDisabled = checkingDisabled + + m.SetEdns0(1232, dnssec) + ednsOpt := m.IsEdns0() + if ednsOpt != nil { + ednsOpt.Option = append(ednsOpt.Option, ednsOptions...) + } + + var r *dns.Msg + var err error + if udp != nil { + res.Protocol = "udp" + if conn != nil { + dst, _ := net.ResolveUDPAddr("udp", nameServer) + r, _, err = udp.ExchangeWithConnTo(m, conn, dst) + } else { + r, _, err = udp.Exchange(m, nameServer) + } + // if record comes back truncated, but we have a TCP connection, try again with that + if r != nil && (r.Truncated || r.Rcode == dns.RcodeBadTrunc) { + if tcp != nil { + return wireLookup(nil, tcp, conn, q, nameServer, recursive, ednsOptions, dnssec, checkingDisabled) + } else { + return res, STATUS_TRUNCATED, err + } + } + } else { + res.Protocol = "tcp" + r, _, err = tcp.Exchange(m, nameServer) + } + if err != nil || r == nil { + if nerr, ok := err.(net.Error); ok { + if nerr.Timeout() { + return res, STATUS_TIMEOUT, nil + } else if nerr.Temporary() { + return res, STATUS_TEMPORARY, err + } + } + return res, STATUS_ERROR, err + } + + if r.Rcode != dns.RcodeSuccess { + for _, ans := range r.Extra { + inner := ParseAnswer(ans) + if inner != nil { + res.Additional = append(res.Additional, inner) + } + } + return res, TranslateDNSErrorCode(r.Rcode), nil + } + + res.Flags.Response = r.Response + res.Flags.Opcode = r.Opcode + res.Flags.Authoritative = r.Authoritative + res.Flags.Truncated = r.Truncated + res.Flags.RecursionDesired = r.RecursionDesired + res.Flags.RecursionAvailable = r.RecursionAvailable + res.Flags.Authenticated = r.AuthenticatedData + res.Flags.CheckingDisabled = r.CheckingDisabled + res.Flags.ErrorCode = r.Rcode + + for _, ans := range r.Answer { + inner := ParseAnswer(ans) + if inner != nil { + res.Answers = append(res.Answers, inner) + } + } + for _, ans := range r.Extra { + inner := ParseAnswer(ans) + if inner != nil { + res.Additional = append(res.Additional, inner) + } + } + for _, ans := range r.Ns { + inner := ParseAnswer(ans) + if inner != nil { + res.Authorities = append(res.Authorities, inner) + } + } + return res, STATUS_NOERROR, nil +} diff --git a/pkg/refactored_zdns/qa.go b/pkg/refactored_zdns/qa.go new file mode 100644 index 00000000..04e0968f --- /dev/null +++ b/pkg/refactored_zdns/qa.go @@ -0,0 +1,61 @@ +/* + * ZDNS Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package refactored_zdns + +type DNSFlags struct { + Response bool `json:"response" groups:"flags,long,trace"` + Opcode int `json:"opcode" groups:"flags,long,trace"` + Authoritative bool `json:"authoritative" groups:"flags,long,trace"` + Truncated bool `json:"truncated" groups:"flags,long,trace"` + RecursionDesired bool `json:"recursion_desired" groups:"flags,long,trace"` + RecursionAvailable bool `json:"recursion_available" groups:"flags,long,trace"` + Authenticated bool `json:"authenticated" groups:"flags,long,trace"` + CheckingDisabled bool `json:"checking_disabled" groups:"flags,long,trace"` + ErrorCode int `json:"error_code" groups:"flags,long,trace"` +} + +type Question struct { + Type uint16 + Class uint16 + Name string +} + +// result to be returned by scan of host +type Result struct { + Answers []interface{} `json:"answers,omitempty" groups:"short,normal,long,trace"` + Additional []interface{} `json:"additionals,omitempty" groups:"short,normal,long,trace"` + Authorities []interface{} `json:"authorities,omitempty" groups:"short,normal,long,trace"` + Protocol string `json:"protocol" groups:"protocol,normal,long,trace"` + Resolver string `json:"resolver" groups:"resolver,normal,long,trace"` + Flags DNSFlags `json:"flags" groups:"flags,long,trace"` +} + +type ExtendedResult struct { + Res Result `json:"result,omitempty" groups:"short,normal,long,trace"` + Status Status `json:"status" groups:"short,normal,long,trace"` + Nameserver string `json:"nameserver" groups:"short,normal,long,trace"` +} + +type TraceStep struct { + Result Result `json:"results" groups:"trace"` + DnsType uint16 `json:"type" groups:"trace"` + DnsClass uint16 `json:"class" groups:"trace"` + Name string `json:"name" groups:"trace"` + NameServer string `json:"name_server" groups:"trace"` + Depth int `json:"depth" groups:"trace"` + Layer string `json:"layer" groups:"trace"` + Cached IsCached `json:"cached" groups:"trace"` + Try int `json:"try" groups:"trace"` +} diff --git a/pkg/refactored_zdns/util.go b/pkg/refactored_zdns/util.go new file mode 100644 index 00000000..fe2d9927 --- /dev/null +++ b/pkg/refactored_zdns/util.go @@ -0,0 +1,150 @@ +/* + * ZDNS Copyright 2022 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package refactored_zdns + +import ( + "errors" + "fmt" + "net" + "strings" + + "github.com/zmap/dns" +) + +func dotName(name string) string { + return strings.Join([]string{name, "."}, "") +} + +func TranslateMiekgErrorCode(err int) Status { + return Status(dns.RcodeToString[err]) +} + +func isStatusAnswer(s Status) bool { + if s == STATUS_NOERROR || s == STATUS_NXDOMAIN { + return true + } + return false +} + +func questionFromAnswer(a Answer) Question { + return Question{Name: a.Name, Type: a.RrType, Class: a.RrClass} +} + +func nameIsBeneath(name, layer string) (bool, string) { + name = strings.ToLower(name) + layer = strings.ToLower(layer) + name = strings.TrimSuffix(name, ".") + if layer == "." { + return true, name + } + + if strings.HasSuffix(name, "."+layer) || name == layer { + return true, name + } + return false, "" +} + +func nextAuthority(name, layer string) (string, error) { + // We are our own authority for PTRs + // (This is dealt with elsewhere) + if strings.HasSuffix(name, "in-addr.arpa") && layer == "." { + return "in-addr.arpa", nil + } + + idx := strings.LastIndex(name, ".") + if idx < 0 || (idx+1) >= len(name) { + return name, nil + } + if layer == "." { + return name[idx+1:], nil + } + + if !strings.HasSuffix(name, layer) { + return "", errors.New("Server did not provide appropriate resolvers to continue recursion") + } + + // Limit the search space to the prefix of the string that isnt layer + idx = strings.LastIndex(name, layer) - 1 + if idx < 0 || (idx+1) >= len(name) { + // Out of bounds. We are our own authority + return name, nil + } + // Find the next step in the layer + idx = strings.LastIndex(name[0:idx], ".") + next := name[idx+1:] + return next, nil +} + +func checkGlue(server string, depth int, result Result) (Result, Status) { + for _, additional := range result.Additional { + ans, ok := additional.(Answer) + if !ok { + continue + } + if ans.Type == "A" && strings.TrimSuffix(ans.Name, ".") == server { + var retv Result + retv.Authorities = make([]interface{}, 0) + retv.Answers = make([]interface{}, 0) + retv.Additional = make([]interface{}, 0) + retv.Answers = append(retv.Answers, ans) + return retv, STATUS_NOERROR + } + } + var r Result + return r, STATUS_ERROR +} + +func makeVerbosePrefix(depth int) string { + return fmt.Sprintf("DEPTH %02d", depth) + ":" + strings.Repeat(" ", 2*depth) +} + +// Check whether the status is safe +func SafeStatus(status Status) bool { + return status == STATUS_NOERROR +} + +// Verify that A record is indeed IPv4 and AAAA is IPv6 +func VerifyAddress(ansType string, ip string) bool { + isIpv4 := false + isIpv6 := false + if net.ParseIP(ip) != nil { + isIpv6 = strings.Contains(ip, ":") + isIpv4 = !isIpv6 + } + if ansType == "A" { + return isIpv4 + } else if ansType == "AAAA" { + return isIpv6 + } + return !isIpv4 && !isIpv6 +} + +func Unique(a []string) []string { + seen := make(map[string]bool) + j := 0 + for _, v := range a { + if !seen[v] { + seen[v] = true + a[j] = v + j++ + } + } + return a[:j] +} + +// TranslateDNSErrorCode translates a DNS error code from the DNS library to a Status +func TranslateDNSErrorCode(err int) Status { + return Status(dns.RcodeToString[err]) +} diff --git a/pkg/refactored_zdns/zdns.go b/pkg/refactored_zdns/zdns.go new file mode 100644 index 00000000..a19b5466 --- /dev/null +++ b/pkg/refactored_zdns/zdns.go @@ -0,0 +1,95 @@ +package refactored_zdns + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "github.com/zmap/dns" + "github.com/zmap/go-iptree/blacklist" + "github.com/zmap/zdns/internal/util" + "strings" + "sync" + "time" +) + +const ( + defaultNameServerConfigFile = "/etc/resolv.conf" +) + +// TODO Phillip - Probably want to rename this +type Resolver struct { + cache *Cache + networkConns *NetworkConns + + blacklist *blacklist.Blacklist + blMu sync.Mutex + + retries int + + isIterative bool // whether the user desires iterative resolution or recursive + iterativeTimeout time.Duration + maxDepth int + nameServers []string + + dnsSecEnabled bool + ednsOptions []dns.EDNS0 + checkingDisabled bool +} + +type NetworkConns struct { + UDPClient *dns.Client + TCPClient *dns.Client + Conn *dns.Conn +} + +func (r *Resolver) init() {} + +func NewResolver(cache *Cache, networkConns *NetworkConns, isIterative bool) (*Resolver, error) { + r := &Resolver{} + if cache == nil { + // TODO Phillip create a new empty cache + } + if networkConns == nil { + // TODO Phillip create a new empty network Conns object + } + // if we're doing recursive resolution, figure out default OS name servers + // otherwise, use the set of 13 root name servers + if isIterative { + r.nameServers = RootServers[:] + } else { + ns, err := GetDNSServers(defaultNameServerConfigFile) + if err != nil { + ns = util.GetDefaultResolvers() + log.Warn("Unable to parse resolvers file with error %w. Using ZDNS defaults: ", err, strings.Join(ns, ", ")) + } + r.nameServers = ns + } + log.Info("No name servers specified. will use: ", strings.Join(r.nameServers, ", ")) + return &Resolver{ + cache: cache, + networkConns: networkConns, + blacklist: blacklist.New(), + blMu: sync.Mutex{}, + }, nil +} + +func (r *Resolver) WithNameServers(nameServers []string) *Resolver { + r.nameServers = nameServers + return r +} + +func (r *Resolver) Lookup(q *Question) ([]ExtendedResult, error) { + switch q.Type { + case dns.TypeA: + return r.doALookup(q) + default: + return nil, fmt.Errorf("type %d not supported", q.Type) + } +} + +func (r *Resolver) doALookup(q *Question) ([]ExtendedResult, error) { + return nil, nil +} + +func (r *Resolver) VerboseLog(depth int, args ...interface{}) { + log.Debug(makeVerbosePrefix(depth), args) +}