From 9798b2f5d3ee2e569360c130c46836387b7aa4dc Mon Sep 17 00:00:00 2001 From: Denis Machard <5562930+dmachard@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:08:27 +0200 Subject: [PATCH] add arcount, nscount and ancount (#829) * add arcount, nscount and ancount * rename questions-count to qdcount support ancount, nscount and arcount add tests * fix linter update README.md --- README.md | 4 +-- dnsutils/dnsmessage.go | 31 +++++++++++++----- dnsutils/dnsmessage_json_test.go | 10 ++++-- dnsutils/dnsmessage_template_test.go | 4 +-- dnsutils/dnsmessage_text_test.go | 10 ++++++ dnsutils/helper.go | 30 +++++++++++++++++ docs/configuration.md | 2 +- docs/dnsjson.md | 10 ++++-- workers/dnsprocessor.go | 7 ++-- workers/dnsprocessor_test.go | 40 +++++++++++++++++++++++ workers/dnstapserver.go | 5 ++- workers/dnstapserver_test.go | 49 +++++++++++++++++++++++++++- 12 files changed, 180 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0cd4fed7..d9eedb33 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@
- + - +
diff --git a/dnsutils/dnsmessage.go b/dnsutils/dnsmessage.go index 974f7aa5..4b72495f 100644 --- a/dnsutils/dnsmessage.go +++ b/dnsutils/dnsmessage.go @@ -86,7 +86,10 @@ type DNS struct { Qname string `json:"qname"` Qclass string `json:"qclass"` - QuestionsCount int `json:"questions-count"` + QdCount int `json:"qdcount"` + AnCount int `json:"ancount"` + NsCount int `json:"nscount"` + ArCount int `json:"arcount"` Qtype string `json:"qtype"` Flags DNSFlags `json:"flags"` @@ -611,7 +614,7 @@ func (dm *DNSMessage) String(format []string, fieldDelimiter string, fieldBounda func (dm *DNSMessage) ToTextLine(format []string, fieldDelimiter string, fieldBoundary string) ([]byte, error) { var s strings.Builder - answers := dm.DNS.DNSRRs.Answers + an := dm.DNS.DNSRRs.Answers qname := dm.DNS.Qname flags := dm.DNS.Flags @@ -741,19 +744,26 @@ func (dm *DNSMessage) ToTextLine(format []string, fieldDelimiter string, fieldBo s.WriteByte('-') } case directive == "ttl": - if len(answers) > 0 { - s.WriteString(strconv.Itoa(answers[0].TTL)) + if len(an) > 0 { + s.WriteString(strconv.Itoa(an[0].TTL)) } else { s.WriteByte('-') } case directive == "answer": - if len(answers) > 0 { - s.WriteString(answers[0].Rdata) + if len(an) > 0 { + s.WriteString(an[0].Rdata) } else { s.WriteByte('-') } - case directive == "answercount": - s.WriteString(strconv.Itoa(len(answers))) + + case directive == "questionscount" || directive == "qdcount": + s.WriteString(strconv.Itoa(dm.DNS.QdCount)) + case directive == "answercount" || directive == "ancount": + s.WriteString(strconv.Itoa(dm.DNS.AnCount)) + case directive == "nscount": + s.WriteString(strconv.Itoa(dm.DNS.NsCount)) + case directive == "arcount": + s.WriteString(strconv.Itoa(dm.DNS.ArCount)) case directive == "edns-csubnet": if len(dm.EDNS.Options) > 0 { @@ -1130,7 +1140,10 @@ func (dm *DNSMessage) Flatten() (map[string]interface{}, error) { "dns.qtype": dm.DNS.Qtype, "dns.qclass": dm.DNS.Qclass, "dns.rcode": dm.DNS.Rcode, - "dns.questions-count": dm.DNS.QuestionsCount, + "dns.qdcount": dm.DNS.QdCount, + "dns.ancount": dm.DNS.AnCount, + "dns.arcount": dm.DNS.ArCount, + "dns.nscount": dm.DNS.NsCount, "dnstap.identity": dm.DNSTap.Identity, "dnstap.latency": dm.DNSTap.Latency, "dnstap.operation": dm.DNSTap.Operation, diff --git a/dnsutils/dnsmessage_json_test.go b/dnsutils/dnsmessage_json_test.go index d3cfbfe5..de3cdfae 100644 --- a/dnsutils/dnsmessage_json_test.go +++ b/dnsutils/dnsmessage_json_test.go @@ -31,7 +31,10 @@ func TestDnsMessage_Json_Reference(t *testing.T) { "qname": "-", "qtype": "-", "qclass": "-", - "questions-count": 0, + "qdcount": 0, + "ancount": 0, + "nscount": 0, + "arcount": 0, "flags": { "qr": false, "tc": false, @@ -287,7 +290,10 @@ func TestDnsMessage_JsonFlatten_Reference(t *testing.T) { "dns.qtype": "-", "dns.rcode": "-", "dns.qclass": "-", - "dns.questions-count": 0, + "dns.qdcount": 0, + "dns.ancount": 0, + "dns.arcount": 0, + "dns.nscount": 0, "dns.resource-records.an.0.name": "google.nl", "dns.resource-records.an.0.rdata": "142.251.39.99", "dns.resource-records.an.0.rdatatype": "A", diff --git a/dnsutils/dnsmessage_template_test.go b/dnsutils/dnsmessage_template_test.go index 4b8821cf..be648fb4 100644 --- a/dnsutils/dnsmessage_template_test.go +++ b/dnsutils/dnsmessage_template_test.go @@ -15,7 +15,7 @@ func TestDnsMessage_ToJinjaFormat(t *testing.T) { template := ` ;; Got {% if dm.DNS.Type == "QUERY" %}query{% else %}answer{% endif %} from {{ dm.NetworkInfo.QueryIP }}#{{ dm.NetworkInfo.QueryPort }}: ;; ->>HEADER<<- opcode: {{ dm.DNS.Opcode }}, status: {{ dm.DNS.Rcode }}, id: {{ dm.DNS.ID }} -;; flags: {{ dm.DNS.Flags.QR | yesno:"qr ," }}{{ dm.DNS.Flags.RD | yesno:"rd ," }}{{ dm.DNS.Flags.RA | yesno:"ra ," }}; QUERY: {{ dm.DNS.QuestionsCount }}, ANSWER: {{ dm.DNS.DNSRRs.Answers | length }}, AUTHORITY: {{ dm.DNS.DNSRRs.Nameservers | length }}, ADDITIONAL: {{ dm.DNS.DNSRRs.Records | length }} +;; flags: {{ dm.DNS.Flags.QR | yesno:"qr ," }}{{ dm.DNS.Flags.RD | yesno:"rd ," }}{{ dm.DNS.Flags.RA | yesno:"ra ," }}; QUERY: {{ dm.DNS.QdCount }}, ANSWER: {{ dm.DNS.DNSRRs.AnCount }}, AUTHORITY: {{ dm.DNS.DNSRRs.NsCount }}, ADDITIONAL: {{ dm.DNS.DNSRRs.ArCount }} ;; QUESTION SECTION: ;{{ dm.DNS.Qname }} {{ dm.DNS.Qclass }} {{ dm.DNS.Qtype }} @@ -44,7 +44,7 @@ func BenchmarkDnsMessage_ToJinjaFormat(b *testing.B) { template := ` ;; Got {% if dm.DNS.Type == "QUERY" %}query{% else %}answer{% endif %} from {{ dm.NetworkInfo.QueryIP }}#{{ dm.NetworkInfo.QueryPort }}: ;; ->>HEADER<<- opcode: {{ dm.DNS.Opcode }}, status: {{ dm.DNS.Rcode }}, id: {{ dm.DNS.ID }} -;; flags: {{ dm.DNS.Flags.QR | yesno:"qr ," }}{{ dm.DNS.Flags.RD | yesno:"rd ," }}{{ dm.DNS.Flags.RA | yesno:"ra ," }}; QUERY: {{ dm.DNS.QuestionsCount }}, ANSWER: {{ dm.DNS.DNSRRs.Answers | length }}, AUTHORITY: {{ dm.DNS.DNSRRs.Nameservers | length }}, ADDITIONAL: {{ dm.DNS.DNSRRs.Records | length }} +;; flags: {{ dm.DNS.Flags.QR | yesno:"qr ," }}{{ dm.DNS.Flags.RD | yesno:"rd ," }}{{ dm.DNS.Flags.RA | yesno:"ra ," }}; QUERY: {{ dm.DNS.QdCount }}, ANSWER: {{ dm.DNS.DNSRRs.AnCount }}, AUTHORITY: {{ dm.DNS.DNSRRs.NsCount }}, ADDITIONAL: {{ dm.DNS.ArCount }} ;; QUESTION SECTION: ;{{ dm.DNS.Qname }} {{ dm.DNS.Qclass }} {{ dm.DNS.Qtype }} diff --git a/dnsutils/dnsmessage_text_test.go b/dnsutils/dnsmessage_text_test.go index 7ba3a6b8..3a4fc447 100644 --- a/dnsutils/dnsmessage_text_test.go +++ b/dnsutils/dnsmessage_text_test.go @@ -224,6 +224,16 @@ func TestDnsMessage_TextFormat_DefaultDirectives(t *testing.T) { dm: DNSMessage{DNSTap: DNSTap{QueryZone: "queryzone.test"}}, expected: "queryzone.test", }, + { + format: "qdcount", + dm: DNSMessage{DNS: DNS{QdCount: 1}}, + expected: "1", + }, + { + format: "ancount nscount arcount", + dm: DNSMessage{DNS: DNS{AnCount: 1, ArCount: 2, NsCount: 3}}, + expected: "1 3 2", + }, } for _, tc := range testcases { diff --git a/dnsutils/helper.go b/dnsutils/helper.go index 5a929401..4e0ef55a 100644 --- a/dnsutils/helper.go +++ b/dnsutils/helper.go @@ -16,6 +16,36 @@ func GetFakeDNS() ([]byte, error) { return dnsmsg.Pack() } +func GetDNSResponsePacket() ([]byte, error) { + dnsmsg := new(dns.Msg) + dnsmsg.SetQuestion("dns.collector.", dns.TypeA) + + // Build a fake response for the question + rr, err := dns.NewRR("dns.collector. 3600 IN A 192.168.1.1") + if err != nil { + return nil, err + } + + // Add the resource record (the answer) to the message + dnsmsg.Answer = append(dnsmsg.Answer, rr) + + // Build an authoritative NS record (Authoritative section) + nsRR, err := dns.NewRR("collector. 3600 IN NS ns1.collector.") + if err != nil { + return nil, err + } + dnsmsg.Ns = append(dnsmsg.Ns, nsRR) + + // Build an additional A record for the authoritative NS (Additional section) + additionalRR, err := dns.NewRR("ns1.collector. 3600 IN A 192.168.2.1") + if err != nil { + return nil, err + } + dnsmsg.Extra = append(dnsmsg.Extra, additionalRR) + + return dnsmsg.Pack() +} + func GetFakeDNSMessage() DNSMessage { dm := DNSMessage{} dm.Init() diff --git a/docs/configuration.md b/docs/configuration.md index a6dbfdd7..4c86710f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -150,7 +150,7 @@ Example to enable output similiar to dig style: text-jinja: |+ ;; Got {% if dm.DNS.Type == "QUERY" %}query{% else %}answer{% endif %} from {{ dm.NetworkInfo.QueryIP }}#{{ dm.NetworkInfo.QueryPort }}: ;; ->>HEADER<<- opcode: {{ dm.DNS.Opcode }}, status: {{ dm.DNS.Rcode }}, id: {{ dm.DNS.ID }} - ;; flags: {{ dm.DNS.Flags.QR | yesno:"qr ," }}{{ dm.DNS.Flags.RD | yesno:"rd ," }}{{ dm.DNS.Flags.RA | yesno:"ra ," }}; QUERY: {{ dm.DNS.QuestionsCount }}, ANSWER: {{ dm.DNS.DNSRRs.Answers | length }}, AUTHORITY: {{ dm.DNS.DNSRRs.Nameservers | length }}, ADDITIONAL: {{ dm.DNS.DNSRRs.Records | length }} + ;; flags: {{ dm.DNS.Flags.QR | yesno:"qr ," }}{{ dm.DNS.Flags.RD | yesno:"rd ," }}{{ dm.DNS.Flags.RA | yesno:"ra ," }}; QUERY: {{ dm.DNS.QdCount }}, ANSWER: {{ dm.DNS.AnCount }}, AUTHORITY: {{ dm.DNS.NsCount }}, ADDITIONAL: {{ dm.DNS.ArCount }} ;; QUESTION SECTION: ;{{ dm.DNS.Qname }} {{ dm.DNS.Qclass }} {{ dm.DNS.Qtype }} diff --git a/docs/dnsjson.md b/docs/dnsjson.md index e59f970c..4b97d30f 100644 --- a/docs/dnsjson.md +++ b/docs/dnsjson.md @@ -29,7 +29,10 @@ Example: "qtype": "A", "id": 23455, "qclass": "IN", - "questions-count": 0, + "qdcount": 1, + "ancount": 1, + "nscount": 0, + "arcount": 0, "flags": { "qr": true, "tc": false, @@ -117,7 +120,10 @@ Here's a flat JSON output formatted using `jq`: "dns.qtype": "A", "dns.rcode": "NOERROR", "dns.qclass": "IN", - "dns.questions-count": 0, + "dns.qdcount": 0, + "dns.ancount": 1, + "dns.arcount": 0, + "dns.nscount": 0, "dns.resource-records.an.0.name": "google.nl", "dns.resource-records.an.0.rdata": "142.251.39.99", "dns.resource-records.an.0.rdatatype": "A", diff --git a/workers/dnsprocessor.go b/workers/dnsprocessor.go index 94994045..760b56e8 100644 --- a/workers/dnsprocessor.go +++ b/workers/dnsprocessor.go @@ -60,8 +60,11 @@ func (w *DNSProcessor) StartCollect() { w.LogError("dns parser malformed packet: %s - %v+", err, dm) } - // get number of questions - dm.DNS.QuestionsCount = dnsHeader.Qdcount + // get number of questions and answers + dm.DNS.QdCount = dnsHeader.Qdcount + dm.DNS.AnCount = dnsHeader.Ancount + dm.DNS.ArCount = dnsHeader.Arcount + dm.DNS.NsCount = dnsHeader.Nscount // dns reply ? if dnsHeader.Qr == 1 { diff --git a/workers/dnsprocessor_test.go b/workers/dnsprocessor_test.go index 830e35d3..e91c111a 100644 --- a/workers/dnsprocessor_test.go +++ b/workers/dnsprocessor_test.go @@ -35,6 +35,46 @@ func Test_DnsProcessor(t *testing.T) { } } +func Test_DnsProcessor_DecodeCounters(t *testing.T) { + logger := logger.New(true) + var o bytes.Buffer + logger.SetOutput(&o) + + // init and run the dns processor + fl := GetWorkerForTest(pkgconfig.DefaultBufferSize) + + consumer := NewDNSProcessor(pkgconfig.GetDefaultConfig(), logger, "test", 512) + consumer.AddDefaultRoute(fl) + consumer.AddDroppedRoute(fl) + go consumer.StartCollect() + + // get dns packet + responsePacket, _ := dnsutils.GetDNSResponsePacket() + + // prepare dns message + dm := dnsutils.GetFakeDNSMessage() + dm.DNS.Payload = responsePacket + dm.DNS.Length = len(responsePacket) + + // send dm to consumer + consumer.GetInputChannel() <- dm + + // read dns message from dnstap consumer + dmOut := <-fl.GetInputChannel() + if dmOut.DNS.QdCount != 1 { + t.Errorf("invalid number of questions in dns message: got %d expect 1", dmOut.DNS.QdCount) + } + if dmOut.DNS.NsCount != 1 { + t.Errorf("invalid number of nscount in dns message: got %d expect 1", dmOut.DNS.NsCount) + } + if dmOut.DNS.AnCount != 1 { + t.Errorf("invalid number of ancount in dns message: got %d expect 1", dmOut.DNS.AnCount) + } + if dmOut.DNS.ArCount != 1 { + t.Errorf("invalid number of arcount in dns message: got %d expect 1", dmOut.DNS.ArCount) + } +} + func Test_DnsProcessor_BufferLoggerIsFull(t *testing.T) { // redirect stdout output to bytes buffer logsChan := make(chan logger.LogEntry, 10) diff --git a/workers/dnstapserver.go b/workers/dnstapserver.go index 528a7faa..5337dbbd 100644 --- a/workers/dnstapserver.go +++ b/workers/dnstapserver.go @@ -512,7 +512,10 @@ func (w *DNSTapProcessor) StartCollect() { } // get number of questions - dm.DNS.QuestionsCount = dnsHeader.Qdcount + dm.DNS.QdCount = dnsHeader.Qdcount + dm.DNS.AnCount = dnsHeader.Ancount + dm.DNS.ArCount = dnsHeader.Arcount + dm.DNS.NsCount = dnsHeader.Nscount if err = dnsutils.DecodePayload(&dm, &dnsHeader, w.GetConfig()); err != nil { dm.DNS.MalformedPacket = true diff --git a/workers/dnstapserver_test.go b/workers/dnstapserver_test.go index 65f6cf12..74c6ad70 100644 --- a/workers/dnstapserver_test.go +++ b/workers/dnstapserver_test.go @@ -199,7 +199,7 @@ func Test_DnstapCollector_CloseFrameStream(t *testing.T) { c.Stop() } -func Test_DnstapProcessor(t *testing.T) { +func Test_DnstapProcessor_toDNSMessage(t *testing.T) { logger := logger.New(true) var o bytes.Buffer logger.SetOutput(&o) @@ -240,6 +240,53 @@ func Test_DnstapProcessor(t *testing.T) { } } +func Test_DnstapProcessor_DecodeCounters(t *testing.T) { + logger := logger.New(true) + var o bytes.Buffer + logger.SetOutput(&o) + + // run the consumer with a fake logger + fl := GetWorkerForTest(pkgconfig.DefaultBufferSize) + + // init the dnstap consumer + consumer := NewDNSTapProcessor(0, "peertest", pkgconfig.GetDefaultConfig(), logger, "test", 512) + consumer.AddDefaultRoute(fl) + consumer.AddDroppedRoute(fl) + + // get dns packet + responsePacket, _ := dnsutils.GetDNSResponsePacket() + + // prepare dnstap + dt := &dnstap.Dnstap{} + dt.Type = dnstap.Dnstap_Type.Enum(1) + + dt.Message = &dnstap.Message{} + dt.Message.Type = dnstap.Message_Type.Enum(6) // CLIENT_RESPONSE + dt.Message.ResponseMessage = responsePacket + data, _ := proto.Marshal(dt) + + // start the consumer + go consumer.StartCollect() + + // add packet to consumer + consumer.GetDataChannel() <- data + + // read dns message from dnstap consumer + dm := <-fl.GetInputChannel() + if dm.DNS.QdCount != 1 { + t.Errorf("invalid number of questions in dns message: got %d expect 1", dm.DNS.QdCount) + } + if dm.DNS.NsCount != 1 { + t.Errorf("invalid number of nscount in dns message: got %d expect 1", dm.DNS.NsCount) + } + if dm.DNS.AnCount != 1 { + t.Errorf("invalid number of ancount in dns message: got %d expect 1", dm.DNS.AnCount) + } + if dm.DNS.ArCount != 1 { + t.Errorf("invalid number of arcount in dns message: got %d expect 1", dm.DNS.ArCount) + } +} + func Test_DnstapProcessor_MalformedDnsHeader(t *testing.T) { // run the consumer with a fake logger fl := GetWorkerForTest(pkgconfig.DefaultBufferSize)