packetbeat/protos/dns/dns.go (705 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 dns provides support for parsing DNS messages and reporting the // results. This package supports the DNS protocol as defined by RFC 1034 // and RFC 1035. It does not have any special support for RFC 2671 (EDNS) or // RFC 4035 (DNS Security Extensions), but since those specifications only // add backwards compatible features there will be no issues handling the // messages. package dns import ( "bytes" "fmt" "sort" "strconv" "strings" "time" mkdns "github.com/miekg/dns" "golang.org/x/net/publicsuffix" "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/packetbeat/pb" "github.com/elastic/beats/v7/packetbeat/procs" "github.com/elastic/beats/v7/packetbeat/protos" conf "github.com/elastic/elastic-agent-libs/config" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/mapstr" "github.com/elastic/elastic-agent-libs/monitoring" ) type dnsPlugin struct { // Configuration data. ports []int sendRequest bool sendResponse bool includeAuthorities bool includeAdditionals bool // Cache of active DNS transactions. The map key is the HashableDnsTuple // associated with the request. transactions *common.Cache transactionTimeout time.Duration results protos.Reporter // Channel where results are pushed. watcher *procs.ProcessesWatcher logger *logp.Logger } // Transport protocol. type transport uint8 var ( unmatchedRequests = monitoring.NewInt(nil, "dns.unmatched_requests") unmatchedResponses = monitoring.NewInt(nil, "dns.unmatched_responses") ) const ( transportTCP = iota transportUDP ) var transportNames = []string{ "tcp", "udp", } func (t transport) String() string { if int(t) >= len(transportNames) { return "impossible" } return transportNames[t] } type hashableDNSTuple [maxDNSTupleRawSize]byte const ( maxDNSTupleRawSize = 2*(sizeofIP+sizeofPort) + sizeofID + sizeofTransport sizeofIP = 16 sizeofPort = 2 sizeofID = 2 sizeofTransport = 1 ) // DnsMessage contains a single DNS message. type dnsMessage struct { ts time.Time // Time when the message was received. tuple common.IPPortTuple // Source and destination addresses of packet. cmdlineTuple *common.ProcessTuple data *mkdns.Msg // Parsed DNS packet data. length int // Length of the DNS message in bytes (without DecodeOffset). } // DnsTuple contains source IP/port, destination IP/port, transport protocol, // and DNS ID. type dnsTuple struct { common.BaseTuple ipLength int transport transport id uint16 raw hashableDNSTuple // Src_ip:Src_port:Dst_ip:Dst_port:ID:Transport revRaw hashableDNSTuple // Dst_ip:Dst_port:Src_ip:Src_port:ID:Transport } func dnsTupleFromIPPort(t *common.IPPortTuple, trans transport, id uint16) dnsTuple { tuple := dnsTuple{ ipLength: t.IPLength, BaseTuple: common.BaseTuple{ SrcIP: t.SrcIP, DstIP: t.DstIP, SrcPort: t.SrcPort, DstPort: t.DstPort, }, transport: trans, id: id, } tuple.computeHashables() return tuple } func (t dnsTuple) reverse() dnsTuple { return dnsTuple{ ipLength: t.ipLength, BaseTuple: common.BaseTuple{ SrcIP: t.DstIP, DstIP: t.SrcIP, SrcPort: t.DstPort, DstPort: t.SrcPort, }, transport: t.transport, id: t.id, raw: t.revRaw, revRaw: t.raw, } } func (t *dnsTuple) computeHashables() { copy(t.raw[0:16], t.SrcIP) copy(t.raw[16:18], []byte{byte(t.SrcPort >> 8), byte(t.SrcPort)}) copy(t.raw[18:34], t.DstIP) copy(t.raw[34:36], []byte{byte(t.DstPort >> 8), byte(t.DstPort)}) copy(t.raw[36:38], []byte{byte(t.id >> 8), byte(t.id)}) t.raw[38] = byte(t.transport) copy(t.revRaw[0:16], t.DstIP) copy(t.revRaw[16:18], []byte{byte(t.DstPort >> 8), byte(t.DstPort)}) copy(t.revRaw[18:34], t.SrcIP) copy(t.revRaw[34:36], []byte{byte(t.SrcPort >> 8), byte(t.SrcPort)}) copy(t.revRaw[36:38], []byte{byte(t.id >> 8), byte(t.id)}) t.revRaw[38] = byte(t.transport) } func (t *dnsTuple) String() string { return fmt.Sprintf("DnsTuple src[%s:%d] dst[%s:%d] transport[%s] id[%d]", t.SrcIP, t.SrcPort, t.DstIP, t.DstPort, t.transport, t.id) } // Hashable returns a hashable value that uniquely identifies // the DNS tuple. func (t *dnsTuple) hashable() hashableDNSTuple { return t.raw } // Hashable returns a hashable value that uniquely identifies // the DNS tuple after swapping the source and destination. func (t *dnsTuple) revHashable() hashableDNSTuple { return t.revRaw } // getTransaction returns the transaction associated with the given // HashableDnsTuple. The lookup key should be the HashableDnsTuple associated // with the request (src is the requestor). Nil is returned if the entry // does not exist. func (dns *dnsPlugin) getTransaction(k hashableDNSTuple) *dnsTransaction { v := dns.transactions.Get(k) if v != nil { return v.(*dnsTransaction) } return nil } type dnsTransaction struct { ts time.Time // Time when the request was received. tuple dnsTuple // Key used to track this transaction in the transactionsMap. src common.Endpoint dst common.Endpoint transport transport notes []string request *dnsMessage response *dnsMessage } func init() { protos.Register("dns", New) } func New(testMode bool, results protos.Reporter, watcher *procs.ProcessesWatcher, cfg *conf.C) (protos.Plugin, error) { p := &dnsPlugin{logger: logp.NewLogger("dns")} config := defaultConfig if !testMode { if err := cfg.Unpack(&config); err != nil { return nil, err } } if err := p.init(results, watcher, &config); err != nil { return nil, err } return p, nil } func (dns *dnsPlugin) init(results protos.Reporter, watcher *procs.ProcessesWatcher, config *dnsConfig) error { dns.setFromConfig(config) dns.transactions = common.NewCacheWithRemovalListener( dns.transactionTimeout, protos.DefaultTransactionHashSize, func(k common.Key, v common.Value) { trans, ok := v.(*dnsTransaction) if !ok { dns.logger.Error("Expired value is not a *DnsTransaction.") return } dns.expireTransaction(trans) }) dns.transactions.StartJanitor(dns.transactionTimeout) dns.results = results dns.watcher = watcher return nil } func (dns *dnsPlugin) setFromConfig(config *dnsConfig) { dns.ports = config.Ports dns.sendRequest = config.SendRequest dns.sendResponse = config.SendResponse dns.includeAuthorities = config.IncludeAuthorities dns.includeAdditionals = config.IncludeAdditionals dns.transactionTimeout = config.TransactionTimeout } func newTransaction(ts time.Time, tuple dnsTuple, cmd common.ProcessTuple) *dnsTransaction { trans := &dnsTransaction{ transport: tuple.transport, ts: ts, tuple: tuple, } trans.src, trans.dst = common.MakeEndpointPair(tuple.BaseTuple, &cmd) return trans } // deleteTransaction deletes an entry from the transaction map and returns // the deleted element. If the key does not exist then nil is returned. func (dns *dnsPlugin) deleteTransaction(k hashableDNSTuple) *dnsTransaction { v := dns.transactions.Delete(k) if v != nil { return v.(*dnsTransaction) } return nil } func (dns *dnsPlugin) GetPorts() []int { return dns.ports } func (dns *dnsPlugin) ConnectionTimeout() time.Duration { return dns.transactionTimeout } func (dns *dnsPlugin) receivedDNSRequest(tuple *dnsTuple, msg *dnsMessage) { dns.logger.Debugf("Processing query. %s", tuple) trans := dns.deleteTransaction(tuple.hashable()) if trans != nil { // This happens if a client puts multiple requests in flight // with the same ID. trans.notes = append(trans.notes, duplicateQueryMsg.Error()) dns.logger.Debugf("%v %s", duplicateQueryMsg, tuple) dns.publishTransaction(trans) dns.deleteTransaction(trans.tuple.hashable()) } trans = newTransaction(msg.ts, *tuple, *msg.cmdlineTuple) if tuple.transport == transportUDP && (msg.data.IsEdns0() != nil) && msg.length > maxDNSPacketSize { trans.notes = append(trans.notes, udpPacketTooLarge.Error()) dns.logger.Debugf("%v", udpPacketTooLarge) } dns.transactions.Put(tuple.hashable(), trans) trans.request = msg } func (dns *dnsPlugin) receivedDNSResponse(tuple *dnsTuple, msg *dnsMessage) { dns.logger.Debugf("Processing response. %s", tuple) trans := dns.getTransaction(tuple.revHashable()) if trans == nil { trans = newTransaction(msg.ts, tuple.reverse(), msg.cmdlineTuple.Reverse()) trans.notes = append(trans.notes, orphanedResponse.Error()) dns.logger.Debugf("%v %s", orphanedResponse, tuple) unmatchedResponses.Add(1) } trans.response = msg if tuple.transport == transportUDP { respIsEdns := msg.data.IsEdns0() != nil if !respIsEdns && msg.length > maxDNSPacketSize { trans.notes = append(trans.notes, udpPacketTooLarge.responseError()) dns.logger.Debugf("%s", udpPacketTooLarge.responseError()) } request := trans.request if request != nil { reqIsEdns := request.data.IsEdns0() != nil switch { case reqIsEdns && !respIsEdns: trans.notes = append(trans.notes, respEdnsNoSupport.Error()) dns.logger.Debugf("%v %s", respEdnsNoSupport, tuple) case !reqIsEdns && respIsEdns: trans.notes = append(trans.notes, respEdnsUnexpected.Error()) dns.logger.Debugf("%v %s", respEdnsUnexpected, tuple) } } } dns.publishTransaction(trans) dns.deleteTransaction(trans.tuple.hashable()) } func (dns *dnsPlugin) publishTransaction(t *dnsTransaction) { if dns.results == nil { return } dns.logger.Debugf("Publishing transaction. %s", &t.tuple) evt, pbf := pb.NewBeatEvent(t.ts) pbf.SetSource(&t.src) pbf.SetDestination(&t.dst) pbf.Network.Transport = t.transport.String() pbf.Network.Protocol = "dns" pbf.Error.Message = t.notes fields := evt.Fields fields["type"] = "dns" fields["status"] = common.ERROR_STATUS dnsEvent := mapstr.M{} fields["dns"] = dnsEvent if t.request != nil && t.response != nil { pbf.Source.Bytes = int64(t.request.length) pbf.Destination.Bytes = int64(t.response.length) pbf.Event.Start = t.request.ts pbf.Event.End = t.response.ts dnsEvent["type"] = "answer" fields["method"] = dnsOpCodeToString(t.request.data.Opcode) if len(t.request.data.Question) > 0 { fields["query"] = dnsQuestionToString(t.request.data.Question[0]) fields["resource"] = t.request.data.Question[0].Name } addDNSToMapStr(dnsEvent, pbf, t.response.data, dns.includeAuthorities, dns.includeAdditionals, dns.logger) if t.response.data.Rcode == 0 { fields["status"] = common.OK_STATUS } if dns.sendRequest { fields["request"] = dnsToString(t.request.data, dns.logger) } if dns.sendResponse { fields["response"] = dnsToString(t.response.data, dns.logger) } } else if t.request != nil { pbf.Source.Bytes = int64(t.request.length) pbf.Event.Start = t.request.ts dnsEvent["type"] = "query" fields["method"] = dnsOpCodeToString(t.request.data.Opcode) if len(t.request.data.Question) > 0 { fields["query"] = dnsQuestionToString(t.request.data.Question[0]) fields["resource"] = t.request.data.Question[0].Name } addDNSToMapStr(dnsEvent, pbf, t.request.data, dns.includeAuthorities, dns.includeAdditionals, dns.logger) if dns.sendRequest { fields["request"] = dnsToString(t.request.data, dns.logger) } } else if t.response != nil { pbf.Destination.Bytes = int64(t.response.length) pbf.Event.End = t.response.ts dnsEvent["type"] = "answer" fields["method"] = dnsOpCodeToString(t.response.data.Opcode) if len(t.response.data.Question) > 0 { fields["query"] = dnsQuestionToString(t.response.data.Question[0]) fields["resource"] = t.response.data.Question[0].Name } addDNSToMapStr(dnsEvent, pbf, t.response.data, dns.includeAuthorities, dns.includeAdditionals, dns.logger) if dns.sendResponse { fields["response"] = dnsToString(t.response.data, dns.logger) } } dns.results(evt) } func (dns *dnsPlugin) expireTransaction(t *dnsTransaction) { t.notes = append(t.notes, noResponse.Error()) dns.logger.Debugf("%v %s", noResponse, &t.tuple) dns.publishTransaction(t) unmatchedRequests.Add(1) } // Adds the DNS message data to the supplied MapStr. func addDNSToMapStr(m mapstr.M, pbf *pb.Fields, dns *mkdns.Msg, authority bool, additional bool, logger *logp.Logger) { m["id"] = dns.Id m["op_code"] = dnsOpCodeToString(dns.Opcode) m["flags"] = mapstr.M{ "authoritative": dns.Authoritative, "truncated_response": dns.Truncated, "recursion_desired": dns.RecursionDesired, "recursion_available": dns.RecursionAvailable, "authentic_data": dns.AuthenticatedData, // [RFC4035] "checking_disabled": dns.CheckingDisabled, // [RFC4035] } m["response_code"] = dnsResponseCodeToString(dns.Rcode) // Add a list of header flags. var hf []string if dns.Authoritative { hf = append(hf, "AA") } if dns.Truncated { hf = append(hf, "TC") } if dns.RecursionDesired { hf = append(hf, "RD") } if dns.RecursionAvailable { hf = append(hf, "RA") } if dns.AuthenticatedData { hf = append(hf, "AD") } if dns.CheckingDisabled { hf = append(hf, "CD") } if opt := dns.IsEdns0(); opt != nil && opt.Do() { hf = append(hf, "DO") } m["header_flags"] = hf if len(dns.Question) > 0 { q := dns.Question[0] qMapStr := mapstr.M{ "name": q.Name, "type": dnsTypeToString(q.Qtype), "class": dnsClassToString(q.Qclass), } m["question"] = qMapStr eTLDPlusOne, err := publicsuffix.EffectiveTLDPlusOne(q.Name) if err == nil && eTLDPlusOne != "" { eTLDPlusOne = strings.TrimRight(eTLDPlusOne, ".") // etld_plus_one should be removed for 8.0.0. qMapStr["etld_plus_one"] = eTLDPlusOne qMapStr["registered_domain"] = eTLDPlusOne if idx := strings.IndexByte(eTLDPlusOne, '.'); idx != -1 { qMapStr["top_level_domain"] = eTLDPlusOne[idx+1:] } subdomain := strings.TrimRight(strings.TrimSuffix(q.Name, eTLDPlusOne), ".") if subdomain != "" { qMapStr["subdomain"] = subdomain } } else if strings.Count(q.Name, ".") == 1 { // Handle publicsuffix.EffectiveTLDPlusOne eTLD+1 error with 1 dot in the domain. s := strings.Split(q.Name, ".") if len(s) == 2 && s[1] != "" { qMapStr["top_level_domain"] = s[1] } qMapStr["registered_domain"] = q.Name } } rrOPT := dns.IsEdns0() if rrOPT != nil { m["opt"] = optToMapStr(rrOPT) } m["answers_count"] = len(dns.Answer) if len(dns.Answer) > 0 { var resolvedIPs []string m["answers"], resolvedIPs = rrsToMapStrs(dns.Answer, true, logger) if len(resolvedIPs) > 0 { m["resolved_ip"] = resolvedIPs pbf.AddIP(resolvedIPs...) } } m["authorities_count"] = len(dns.Ns) if authority && len(dns.Ns) > 0 { m["authorities"], _ = rrsToMapStrs(dns.Ns, false, logger) } if rrOPT != nil { m["additionals_count"] = len(dns.Extra) - 1 } else { m["additionals_count"] = len(dns.Extra) } if additional && len(dns.Extra) > 0 { rrsMapStrs, _ := rrsToMapStrs(dns.Extra, false, logger) // We do not want OPT RR to appear in the 'additional' section, // that's why rrsMapStrs could be empty even though len(dns.Extra) > 0 if len(rrsMapStrs) > 0 { m["additionals"] = rrsMapStrs } } } func optToMapStr(rrOPT *mkdns.OPT) mapstr.M { optMapStr := mapstr.M{ "do": rrOPT.Do(), // true if DNSSEC "version": strconv.FormatUint(uint64(rrOPT.Version()), 10), "udp_size": rrOPT.UDPSize(), "ext_rcode": dnsResponseCodeToString(rrOPT.ExtendedRcode()), } for _, o := range rrOPT.Option { switch o.(type) { case *mkdns.EDNS0_DAU: optMapStr["dau"] = o.String() case *mkdns.EDNS0_DHU: optMapStr["dhu"] = o.String() case *mkdns.EDNS0_EXPIRE: optMapStr["local"] = o.String() case *mkdns.EDNS0_LLQ: optMapStr["llq"] = o.String() case *mkdns.EDNS0_LOCAL: optMapStr["local"] = o.String() case *mkdns.EDNS0_N3U: optMapStr["n3u"] = o.String() case *mkdns.EDNS0_NSID: optMapStr["nsid"] = o.String() case *mkdns.EDNS0_SUBNET: optMapStr["subnet"] = o.String() case *mkdns.EDNS0_COOKIE: optMapStr["cookie"] = o.String() case *mkdns.EDNS0_UL: optMapStr["ul"] = o.String() } } return optMapStr } // rrsToMapStr converts an slice of RR's to an slice of MapStr's and optionally // returns a list of the IP addresses found in the resource records. func rrsToMapStrs(records []mkdns.RR, ipList bool, logger *logp.Logger) ([]mapstr.M, []string) { var allIPs []string mapStrSlice := make([]mapstr.M, 0, len(records)) for _, rr := range records { rrHeader := rr.Header() mapStr, ips := rrToMapStr(rr, ipList, logger) if len(mapStr) == 0 { // OPT pseudo-RR returns an empty MapStr continue } allIPs = append(allIPs, ips...) mapStr["name"] = trimRightDot(rrHeader.Name) mapStr["type"] = dnsTypeToString(rrHeader.Rrtype) mapStr["class"] = dnsClassToString(rrHeader.Class) mapStr["ttl"] = strconv.FormatInt(int64(rrHeader.Ttl), 10) mapStrSlice = append(mapStrSlice, mapStr) } if len(mapStrSlice) == 0 { mapStrSlice = nil } return mapStrSlice, allIPs } // Convert all RDATA fields of a RR to a single string // fields are ordered alphabetically with 'data' as the last element // // TODO An improvement would be to replace 'data' by the real field name // It would require some changes in unit tests func rrToString(rr mkdns.RR, logger *logp.Logger) string { var st string var keys []string mapStr, _ := rrToMapStr(rr, false, logger) data, ok := mapStr["data"] delete(mapStr, "data") for k := range mapStr { keys = append(keys, k) } sort.Strings(keys) var b bytes.Buffer for _, k := range keys { v := mapStr[k] switch x := v.(type) { case int: fmt.Fprintf(&b, "%s %d, ", k, x) case string: fmt.Fprintf(&b, "%s %s, ", k, x) } } if !ok { st = strings.TrimSuffix(b.String(), ", ") return st } switch x := data.(type) { case int: fmt.Fprintf(&b, "%d", x) case string: fmt.Fprintf(&b, "%s", x) } return b.String() } func rrToMapStr(rr mkdns.RR, ipList bool, logger *logp.Logger) (mapstr.M, []string) { mapStr := mapstr.M{} rrType := rr.Header().Rrtype var ips []string appendIP := func(ip string) string { if ipList { ips = append(ips, ip) } return ip } switch x := rr.(type) { default: // We don't have special handling for this type logger.Debugf("No special handling for RR type %s", dnsTypeToString(rrType)) unsupportedRR := new(mkdns.RFC3597) err := unsupportedRR.ToRFC3597(x) if err == nil { rData, err := hexStringToString(unsupportedRR.Rdata) mapStr["data"] = rData if err != nil { logger.Debugf("%v", err) } } else { logger.Debugf("Rdata for the unhandled RR type %s could not be fetched", dnsTypeToString(rrType)) } // Don't attempt to render IPs for answers that are incomplete. case *mkdns.A: if x.A == nil { break } mapStr["data"] = appendIP(x.A.String()) case *mkdns.AAAA: if x.AAAA == nil { break } mapStr["data"] = appendIP(x.AAAA.String()) case *mkdns.CNAME: mapStr["data"] = trimRightDot(x.Target) case *mkdns.DNSKEY: mapStr["flags"] = strconv.Itoa(int(x.Flags)) mapStr["protocol"] = strconv.Itoa(int(x.Protocol)) mapStr["algorithm"] = dnsAlgorithmToString(x.Algorithm) mapStr["data"] = x.PublicKey case *mkdns.DS: mapStr["key_tag"] = strconv.Itoa(int(x.KeyTag)) mapStr["algorithm"] = dnsAlgorithmToString(x.Algorithm) mapStr["digest_type"] = dnsHashToString(x.DigestType) mapStr["data"] = strings.ToUpper(x.Digest) case *mkdns.MX: mapStr["preference"] = x.Preference mapStr["data"] = trimRightDot(x.Mx) case *mkdns.NS: mapStr["data"] = trimRightDot(x.Ns) case *mkdns.NSEC: mapStr["type_bits"] = dnsTypeBitsMapToString(x.TypeBitMap) mapStr["data"] = x.NextDomain case *mkdns.NSEC3: mapStr["hash"] = dnsHashToString(x.Hash) mapStr["flags"] = strconv.Itoa(int(x.Flags)) mapStr["iterations"] = strconv.Itoa(int(x.Iterations)) mapStr["salt"] = dnsSaltToString(x.Salt) mapStr["type_bits"] = dnsTypeBitsMapToString(x.TypeBitMap) mapStr["data"] = x.NextDomain case *mkdns.NSEC3PARAM: mapStr["hash"] = dnsHashToString(x.Hash) mapStr["flags"] = strconv.Itoa(int(x.Flags)) mapStr["iterations"] = strconv.Itoa(int(x.Iterations)) mapStr["data"] = dnsSaltToString(x.Salt) case *mkdns.OPT: // EDNS [RFC6891] // OPT pseudo-RR is managed in addDnsToMapStr function return nil, nil case *mkdns.PTR: mapStr["data"] = trimRightDot(x.Ptr) case *mkdns.RFC3597: // Miekg/dns lib doesn't handle this type logger.Debugf("Unknown RR type %s", dnsTypeToString(rrType)) rData, err := hexStringToString(x.Rdata) mapStr["data"] = rData if err != nil { logger.Debugf("%v", err) } case *mkdns.RRSIG: mapStr["type_covered"] = dnsTypeToString(x.TypeCovered) mapStr["algorithm"] = dnsAlgorithmToString(x.Algorithm) mapStr["labels"] = strconv.Itoa(int(x.Labels)) mapStr["original_ttl"] = strconv.FormatInt(int64(x.OrigTtl), 10) mapStr["expiration"] = mkdns.TimeToString(x.Expiration) mapStr["inception"] = mkdns.TimeToString(x.Inception) mapStr["key_tag"] = strconv.Itoa(int(x.KeyTag)) mapStr["signer_name"] = trimRightDot(x.SignerName) mapStr["data"] = x.Signature case *mkdns.SOA: mapStr["rname"] = trimRightDot(x.Mbox) mapStr["serial"] = x.Serial mapStr["refresh"] = x.Refresh mapStr["retry"] = x.Retry mapStr["expire"] = x.Expire mapStr["minimum"] = x.Minttl mapStr["data"] = trimRightDot(x.Ns) case *mkdns.SRV: mapStr["priority"] = x.Priority mapStr["weight"] = x.Weight mapStr["port"] = x.Port mapStr["data"] = trimRightDot(x.Target) case *mkdns.TXT: mapStr["data"] = strings.Join(x.Txt, " ") } return mapStr, ips } // dnsQuestionToString converts a Question to a string. func dnsQuestionToString(q mkdns.Question) string { name := q.Name return fmt.Sprintf("class %s, type %s, %s", dnsClassToString(q.Qclass), dnsTypeToString(q.Qtype), name) } // rrsToString converts an array of RR's to a // string. func rrsToString(r []mkdns.RR, logger *logp.Logger) string { var rrStrs []string for _, rr := range r { rrStrs = append(rrStrs, rrToString(rr, logger)) } return strings.Join(rrStrs, "; ") } // dnsToString converts a DNS message to a string. func dnsToString(dns *mkdns.Msg, logger *logp.Logger) string { var msgType string if dns.Response { msgType = "response" } else { msgType = "query" } var t []string if dns.Authoritative { t = append(t, "aa") } if dns.Truncated { t = append(t, "tc") } if dns.RecursionDesired { t = append(t, "rd") } if dns.RecursionAvailable { t = append(t, "ra") } if dns.AuthenticatedData { t = append(t, "ad") } if dns.CheckingDisabled { t = append(t, "cd") } flags := strings.Join(t, " ") var a []string a = append(a, fmt.Sprintf("ID %d; QR %s; OPCODE %s; FLAGS %s; RCODE %s", dns.Id, msgType, dnsOpCodeToString(dns.Opcode), flags, dnsResponseCodeToString(dns.Rcode))) if len(dns.Question) > 0 { t = []string{} for _, question := range dns.Question { t = append(t, dnsQuestionToString(question)) } a = append(a, fmt.Sprintf("QUESTION %s", strings.Join(t, "; "))) } if len(dns.Answer) > 0 { a = append(a, fmt.Sprintf("ANSWER %s", rrsToString(dns.Answer, logger))) } if len(dns.Ns) > 0 { a = append(a, fmt.Sprintf("AUTHORITY %s", rrsToString(dns.Ns, logger))) } if len(dns.Extra) > 0 { a = append(a, fmt.Sprintf("ADDITIONAL %s", rrsToString(dns.Extra, logger))) } return strings.Join(a, "; ") } // decodeDnsData decodes a byte array into a DNS struct. If an error occurs // then the returned dns pointer will be nil. This is concurrency-safe. // We do not handle Unpack ErrTruncated for now. See https://github.com/miekg/dns/pull/281 func decodeDNSData(transp transport, rawData []byte) (dns *mkdns.Msg, err error) { var offset int if transp == transportTCP { offset = decodeOffset } if len(rawData) < offset { return nil, nonDNSMsg } msg := &mkdns.Msg{} err = msg.Unpack(rawData[offset:]) // Message should be more than 12 bytes. // The 12 bytes value corresponds to a message header length. // We use this check because Unpack does not return an error for some invalid messages. // TODO: can a better solution be found? if msg.Len() <= 12 || err != nil { return nil, nonDNSMsg } // Normalize question names. for i, q := range msg.Question { msg.Question[i].Name = trimRightDot(q.Name) } return msg, nil } func trimRightDot(name string) string { if len(name) == 0 || name == "." || name[len(name)-1] != '.' { return name } return name[:len(name)-1] }