diff --git a/README.md b/README.md index 8a90709f..834e1b89 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@

Go Report Go version -Go tests +Go tests Go bench -Go lines +Go lines

diff --git a/dnsutils/constant.go b/dnsutils/constant.go index 89bd7fa0..d37b2d3f 100644 --- a/dnsutils/constant.go +++ b/dnsutils/constant.go @@ -27,3 +27,7 @@ const ( ErrorUnexpectedDirective = "unexpected text format directive: " ) + +const ( + TestQName = "dnstapcollector.test." +) diff --git a/dnsutils/dns_parser_answer_test.go b/dnsutils/dns_parser_answer_test.go new file mode 100644 index 00000000..77b94495 --- /dev/null +++ b/dnsutils/dns_parser_answer_test.go @@ -0,0 +1,208 @@ +package dnsutils + +import ( + "errors" + "fmt" + "testing" + + "github.com/miekg/dns" +) + +func TestDecodeAnswer_Ns(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rrNs, _ := dns.NewRR("root-servers.net NS c.root-servers.net") + rrA, _ := dns.NewRR(fmt.Sprintf("%s A 127.0.0.1", fqdn)) + + m := new(dns.Msg) + m.SetReply(dm) + m.Authoritative = true + m.Answer = append(m.Answer, rrA) + m.Ns = append(m.Ns, rrNs) + + payload, _ := m.Pack() + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + _, offsetRRns, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + nsAnswers, _, _ := DecodeAnswer(len(m.Ns), offsetRRns, payload) + if len(nsAnswers) != len(m.Ns) { + t.Errorf("invalid decode answer, want %d, got: %d", len(m.Ns), len(nsAnswers)) + } +} + +func TestDecodeAnswer(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + rr1, _ := dns.NewRR(fmt.Sprintf("%s A 127.0.0.1", fqdn)) + rr2, _ := dns.NewRR(fmt.Sprintf("%s A 127.0.0.2", fqdn)) + dm.Answer = append(dm.Answer, rr1) + dm.Answer = append(dm.Answer, rr2) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if len(answer) != len(dm.Answer) { + t.Errorf("invalid decode answer, want %d, got: %d", len(dm.Answer), len(answer)) + } +} + +func TestDecodeAnswer_QnameMinimized(t *testing.T) { + payload := []byte{0x8d, 0xda, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x05, 0x74, + 0x65, 0x61, 0x6d, 0x73, 0x09, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05, 0x00, + 0x01, 0x00, 0x00, 0x50, 0xa8, 0x00, 0x0f, 0x05, 0x74, 0x65, 0x61, 0x6d, 0x73, 0x06, + 0x6f, 0x66, 0x66, 0x69, 0x63, 0x65, 0xc0, 0x1c, 0xc0, 0x31, 0x00, 0x05, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x3e, 0x00, 0x26, 0x10, 0x74, 0x65, 0x61, 0x6d, 0x73, 0x2d, 0x6f, 0x66, + 0x66, 0x69, 0x63, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x06, 0x73, 0x2d, 0x30, 0x30, 0x30, 0x35, + 0x08, 0x73, 0x2d, 0x6d, 0x73, 0x65, 0x64, 0x67, 0x65, 0x03, 0x6e, 0x65, 0x74, 0x00, 0xc0, + 0x4c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x13, 0x06, 0x73, 0x2d, 0x30, + 0x30, 0x30, 0x35, 0x09, 0x64, 0x63, 0x2d, 0x6d, 0x73, 0x65, 0x64, 0x67, 0x65, 0xc0, 0x6d, + 0xc0, 0x7e, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x04, 0x34, 0x71, 0xc3, + 0x84, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + _, _, err := DecodeAnswer(4, offsetRR, payload) + if err != nil { + t.Errorf("failed to decode valid dns packet with minimization") + } +} + +func TestDecodeDnsAnswer_PacketTooShort(t *testing.T) { + payload := []byte{46, 172, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 15, 100, 110, 115, 116, 97, 112, 99, 111, 108, 108, 101, 99, 116, + 111, 114, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, 15, 100, 110, 115, 116, 97, 112, 99, 111, 108, 108, 101, 99, 116, + 111, 114, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, 0, 0, 14, 16, 0} + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + _, _, err := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(err, ErrDecodeDNSAnswerTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsAnswer_PathologicalPacket(t *testing.T) { + // Create a message with one question and `n` answers (`n` determined later). + decoded := make([]byte, 65500) + copy(decoded, []byte{88, 27, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0}) + + // Create a rather suboptimal name for the question. + // The answers point to this a bajillion times later. + // This name breaks several rules:v + // * Label length is > 63 + // * Name length is > 255 bytes + // * Pointers jump all over the place, not just backwards + i := 12 + for { + // Create a bunch of interleaved labels of length 191, + // each label immediately followed by a pointer to the + // label next to it. The last label of the interleaved chunk + // is followed with a pointer to forwards to the next chunk + // of interleaved labels: + // + // [191 ... 191 ... 191 ... ... ptr1 ptr2 ... ptrN 191 ... 191 ...] + // ^ ^ │ │ │ ^ + // │ └────────────┼────┘ └────┘ + // └───────────────────┘ + // + // We then repeat this pattern as many times as we can within the + // first 16383 bytes (so that we can point to it later). + // Then cleanly closing the name with a null byte in the end allows us to + // create a name of around 700 kilobytes (I checked once, don't quote me on this). + if 16384-i < 384 { + decoded[i] = 0 + break + } + for j := 0; j < 192; j += 2 { + decoded[i] = 191 + i += 2 + } + for j := 0; j < 190; j += 2 { + offset := i - 192 + 2 + decoded[i] = 0xc0 | byte(offset>>8) + decoded[i+1] = byte(offset & 0xff) + i += 2 + } + offset := i + 2 + decoded[i] = 0xc0 | byte(offset>>8) + decoded[i+1] = byte(offset & 0xff) + i += 2 + } + + // Fill in the rest of the question + copy(decoded[i:], []byte{0, 5, 0, 1}) + i += 4 + + // Fit as many answers as we can that contain CNAME RDATA pointing to + // the bloated name created above. + ancount := 0 + for j := i; j+13 <= len(decoded); j += 13 { + copy(decoded[j:], []byte{0, 0, 5, 0, 0, 0, 0, 0, 1, 0, 2, 192, 12}) + ancount += 1 + } + + // Update the message with the answer count + decoded[6] = byte(ancount >> 8) + decoded[7] = byte(ancount & 0xff) + + _, _, err := DecodeAnswer(ancount, i, decoded) + if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsAnswer_RdataTooShort(t *testing.T) { + payload := []byte{46, 172, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 15, 100, 110, 115, 116, 97, 112, 99, 111, 108, 108, 101, 99, 116, + 111, 114, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, 15, 100, 110, 115, 116, 97, 112, 99, 111, 108, 108, 101, 99, 116, + 111, 114, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, 0, 0, 14, 16, 0, 4, 127, 0} + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + _, _, err := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(err, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsAnswer_InvalidPtr(t *testing.T) { + payload := []byte{128, 177, 129, 160, 0, 1, 0, 1, 0, 0, 0, 1, 5, 104, 101, 108, 108, 111, 4, + 109, 99, 104, 100, 2, 109, 101, 0, 0, 1, 0, 1, 192, 254, 0, 1, 0, 1, 0, 0, + 14, 16, 0, 4, 83, 112, 146, 176} + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + _, _, err := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsAnswer_InvalidPtr_Loop1(t *testing.T) { + // loop qname on himself + payload := []byte{128, 177, 129, 160, 0, 1, 0, 1, 0, 0, 0, 1, 5, 104, 101, 108, 108, 111, 4, + 109, 99, 104, 100, 2, 109, 101, 0, 0, 1, 0, 1, 192, 31, 0, 1, 0, 1, 0, 0, + 14, 16, 0, 4, 83, 112, 146, 176} + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + _, _, err := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsAnswer_InvalidPtr_Loop2(t *testing.T) { + // loop between qnames + payload := []byte{128, 177, 129, 160, 0, 1, 0, 2, 0, 0, 0, 1, 5, 104, 101, 108, 108, 111, 4, + 109, 99, 104, 100, 2, 109, 101, 0, 0, 1, 0, 1, 192, 47, 0, 1, 0, 1, 0, 0, + 14, 16, 0, 4, 83, 112, 146, 176, 192, 31, 0, 1, 0, 1, 0, 0, + 14, 16, 0, 4, 83, 112, 146, 176} + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + _, _, err := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} diff --git a/dnsutils/dns_parser_label_test.go b/dnsutils/dns_parser_label_test.go new file mode 100644 index 00000000..1ad9d71a --- /dev/null +++ b/dnsutils/dns_parser_label_test.go @@ -0,0 +1,296 @@ +package dnsutils + +import ( + "errors" + "testing" +) + +// Benchmark + +func BenchmarkDnsParseLabels(b *testing.B) { + payload := []byte{0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x76, 0x69, + 0x74, 0x79, 0x2d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x06, + 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x03, 0x63, 0x6f, 0x6d, 0x00, + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := ParseLabels(0, payload) + if err != nil { + b.Fatalf("could not parse labels: %v\n", err) + } + } +} + +func TestDecodeDnsLabel_InvalidOffset_NegativeOffset(t *testing.T) { + payload := []byte{0x01, 0x61, 0x00} + + _, _, err := ParseLabels(-1, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidOffset) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidOffset_StartOutOfBounds(t *testing.T) { + payload := []byte{0x01, 0x61, 0x00} + + _, _, err := ParseLabels(4, payload) + if !errors.Is(err, ErrDecodeDNSLabelTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidOffset_RunOutOfBounds(t *testing.T) { + payload := []byte{0x01, 0x61} + + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidOffset_PointerByteOutOfBounds(t *testing.T) { + payload := []byte{0x01, 0x61, 0xc0} + + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_LabelTooShort(t *testing.T) { + payload := []byte{0x01} + + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_NoExtraDotAfterPtr(t *testing.T) { + payload := []byte{0x00, 0x01, 0x61, 0xc0, 0x00} + + label, _, _ := ParseLabels(1, payload) + if label != "a" { + t.Errorf("bad label parsed: %v", label) + } +} + +func TestDecodeDnsLabel_InvalidLabelLengthByte1(t *testing.T) { + payload := []byte{0x40} + + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidLabelLengthByte2(t *testing.T) { + payload := []byte{0x80} + + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_ValidTotalLength(t *testing.T) { + // A 253-character label + payload := []byte{ + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3d, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x00, + } + valid := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + label, _, _ := ParseLabels(0, payload) + if label != valid { + t.Errorf("bad name parsed: %v", label) + } +} + +func TestDecodeDnsLabel_InvalidTotalLength_WithoutPtr(t *testing.T) { + // A 254-character label (including separator dots) + payload := []byte{ + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3e, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x00, + } + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelTooLong) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidTotalLength_WithPtr(t *testing.T) { + // A 254-character label (including separator dots), containing a pointer + payload := []byte{ + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x3c, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x00, 0x01, + 0x61, 0xc0, 0x00, + } + _, _, err := ParseLabels(255, payload) + if !errors.Is(err, ErrDecodeDNSLabelTooLong) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidPtr_SimpleLoop(t *testing.T) { + payload := []byte{0x01, 0x61, 0xc0, 0x00} + + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidPtr_Forwards(t *testing.T) { + payload := []byte{0xc0, 0x02, 0x00} + + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidPtr_BackwardsInsideCurrentLabel1(t *testing.T) { + payload := []byte{0x01, 0x02, 0xc0, 0x01, 0x00} + + _, _, err := ParseLabels(0, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidPtr_BackwardsOverlappingLoop(t *testing.T) { + payload := []byte{0x01, 0x61, 0x01, 0x61, 0xc0, 0x00} + + _, _, err := ParseLabels(2, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidPtr_BackwardsOverlappingTerminating(t *testing.T) { + payload := []byte{0x01, 0x01, 0x00, 0xc0, 0x00} + + _, _, err := ParseLabels(1, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_InvalidPtr_BackwardsOverlappingPtr(t *testing.T) { + // The second pointer byte overlaps the beginning of the label that starts at payload[3] + payload := []byte{0x00, 0x00, 0xc0, 0x01, 0x00, 0xc0, 0x02} + + _, _, err := ParseLabels(3, payload) + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsLabel_EndOffset_WithoutPtr(t *testing.T) { + payload := []byte{0x02, 0x61, 0x61, 0x00} + + _, offset, _ := ParseLabels(0, payload) + if offset != 4 { + t.Errorf("invalid end offset: %v", offset) + } +} + +func TestDecodeDnsLabel_EndOffset_WithPtr(t *testing.T) { + payload := []byte{0x01, 0x61, 0x00, 0x02, 0x61, 0x61, 0xc0, 0x00} + + _, offset, _ := ParseLabels(3, payload) + if offset != 8 { + t.Errorf("invalid end offset: %v", offset) + } +} diff --git a/dnsutils/dns_parser_payload_test.go b/dnsutils/dns_parser_payload_test.go new file mode 100644 index 00000000..dec9d58f --- /dev/null +++ b/dnsutils/dns_parser_payload_test.go @@ -0,0 +1,1000 @@ +package dnsutils + +import ( + "errors" + "fmt" + "testing" + + "github.com/dmachard/go-dnscollector/pkgconfig" +) + +func TestDecodePayload_QueryHappy(t *testing.T) { + payload := []byte{ + // header + 0x9e, 0x84, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + // name + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Additional records: EDNS OPT with no data, DO = 0, Z=0 + 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { + t.Errorf("Unexpected error while decoding payload: %v", err) + } + if dm.DNS.MalformedPacket != false { + t.Errorf("did not expect packet to be malformed") + } + + if dm.DNS.ID != 0x9e84 || + dm.DNS.Opcode != 0 || + dm.DNS.Rcode != RcodeToString(0) || + dm.DNS.Flags.QR || + dm.DNS.Flags.TC || + dm.DNS.Flags.AA || + !dm.DNS.Flags.AD || + dm.DNS.Flags.RA { + t.Error("Invalid DNS header data in message") + } + + if dm.DNS.Qname != "sensorfleet.com" { + t.Errorf("Unexpected query name: %s", dm.DNS.Qname) + } + if dm.DNS.Qtype != "A" { + t.Errorf("Unexpected query type: %s", dm.DNS.Qtype) + } + + if dm.EDNS.Do != 1 || + dm.EDNS.UDPSize != 4096 || + dm.EDNS.Z != 0 || + dm.EDNS.Version != 0 || + len(dm.EDNS.Options) != 0 { + t.Errorf("Unexpected EDNS data") + } + + if len(dm.DNS.DNSRRs.Answers) != 0 || + len(dm.DNS.DNSRRs.Nameservers) != 0 || + len(dm.DNS.DNSRRs.Records) != 0 { + t.Errorf("Unexpected sections parsed") + } + +} +func TestDecodePayload_QueryInvalid(t *testing.T) { + payload := []byte{ + // header + 0x9e, 0x84, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + // name + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x83, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Additional records: EDNS OPT with no data, DO = 1, Z=0 + 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { + t.Errorf("Expected error when parsing payload") + } + if dm.DNS.MalformedPacket != true { + t.Errorf("expected packet to be marked as malformed") + } + + // returned error should wrap the original error + if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodePayload_AnswerHappy(t *testing.T) { + payload := []byte{ + 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, + // Query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Answer 1 + 0xc0, 0x0c, // pointer to name + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.1 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, + // Answer 2 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.2 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, + // Answer 3 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.3 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, + // Answer 4 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.4 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, + // Additianl records, EDNS Option, 0 bytes DO=0, Z = 0 + 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { + t.Errorf("Unexpected error while decoding payload: %v", err) + } + if dm.DNS.MalformedPacket != false { + t.Errorf("did not expect packet to be malformed") + } + + if dm.DNS.ID != 0x9e84 || + dm.DNS.Opcode != 0 || + dm.DNS.Rcode != RcodeToString(0) || + !dm.DNS.Flags.QR || + dm.DNS.Flags.TC || + dm.DNS.Flags.AA || + dm.DNS.Flags.AD || + !dm.DNS.Flags.RA { + t.Error("Invalid DNS header data in message") + } + + if dm.DNS.Qname != "sensorfleet.com" { + t.Errorf("Unexpected query name: %s", dm.DNS.Qname) + } + if dm.DNS.Qtype != "A" { + t.Errorf("Unexpected query type: %s", dm.DNS.Qtype) + } + + if len(dm.DNS.DNSRRs.Answers) != 4 { + t.Errorf("expected 4 answers, got %d", len(dm.DNS.DNSRRs.Answers)) + } + + for i, ans := range dm.DNS.DNSRRs.Answers { + expected := DNSAnswer{ + Name: dm.DNS.Qname, + Rdatatype: RdatatypeToString(0x0001), + Class: "IN", // 0x0001, + TTL: 300, + Rdata: fmt.Sprintf("10.10.1.%d", i+1), + } + if expected != ans { + t.Errorf("unexpected answer (%d). expected %v, got %v", i, expected, ans) + } + } + + if dm.EDNS.Do != 0 || + dm.EDNS.UDPSize != 1232 || + dm.EDNS.Z != 0 || + dm.EDNS.Version != 0 || + len(dm.EDNS.Options) != 0 { + t.Errorf("Unexpected EDNS data") + } + + if len(dm.DNS.DNSRRs.Nameservers) != 0 || + len(dm.DNS.DNSRRs.Records) != 0 { + t.Errorf("Unexpected sections parsed") + } + +} + +func TestDecodePayload_AnswerMultipleQueries(t *testing.T) { + payload := []byte{ + 0x9e, 0x84, 0x81, 0x80, 0x00, 0x02, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, + // Query section + // query 1 + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // query 2 + 0x0a, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + + // Answer 1 + 0xc0, 0x0c, // pointer to name + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.1 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, + // Answer 2 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.2 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, + // Answer 3 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.3 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, + // Answer 4 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.4 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, + // Additianl records, EDNS Option, 0 bytes DO=0, Z = 0 + 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { + t.Errorf("Unexpected error while decoding payload: %v", err) + } + if dm.DNS.MalformedPacket != false { + t.Errorf("did not expect packet to be malformed") + } + + if dm.DNS.ID != 0x9e84 || + dm.DNS.Opcode != 0 || + dm.DNS.Rcode != RcodeToString(0) || + !dm.DNS.Flags.QR || + dm.DNS.Flags.TC || + dm.DNS.Flags.AA || + dm.DNS.Flags.AD || + !dm.DNS.Flags.RA { + t.Error("Invalid DNS header data in message") + } + + if dm.DNS.Qname != "ensorfleet.com" { + t.Errorf("Unexpected query name: %s", dm.DNS.Qname) + } + if dm.DNS.Qtype != "A" { + t.Errorf("Unexpected query type: %s", dm.DNS.Qtype) + } + + if len(dm.DNS.DNSRRs.Answers) != 4 { + t.Errorf("expected 4 answers, got %d", len(dm.DNS.DNSRRs.Answers)) + } + + for i, ans := range dm.DNS.DNSRRs.Answers { + expected := DNSAnswer{ + Name: "s" + dm.DNS.Qname, // answers have qname from 1st query data, 2nd data is missing 's' + Rdatatype: RdatatypeToString(0x0001), + Class: "IN", // 0x0001, + TTL: 300, + Rdata: fmt.Sprintf("10.10.1.%d", i+1), + } + if expected != ans { + t.Errorf("unexpected answer (%d). expected %v, got %v", i, expected, ans) + } + } + + if dm.EDNS.Do != 0 || + dm.EDNS.UDPSize != 1232 || + dm.EDNS.Z != 0 || + dm.EDNS.Version != 0 || + len(dm.EDNS.Options) != 0 { + t.Errorf("Unexpected EDNS data") + } + + if len(dm.DNS.DNSRRs.Nameservers) != 0 || + len(dm.DNS.DNSRRs.Records) != 0 { + t.Errorf("Unexpected sections parsed") + } + +} + +func TestDecodePayload_AnswerInvalid(t *testing.T) { + payload := []byte{ + 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, + // Query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Answer 1 + 0xc0, 0x0c, // pointer to name + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.1 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, + // Answer 2 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.2 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, + // Answer 3 + 0xc0, 0xff, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.3 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, + // Answer 4 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.4 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, + // Additianl records, EDNS Option, 0 bytes DO=0, Z = 0 + 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { + t.Error("expected decoding to fail") + } + // returned error should wrap the original error + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } + if dm.DNS.MalformedPacket != true { + t.Errorf("expected packet to be malformed") + } +} + +func TestDecodePayload_AnswerInvalidQuery(t *testing.T) { + payload := []byte{ + 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, + // Query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x83, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Answer 1 + 0xc0, 0x0c, // pointer to name + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.1 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, + // Answer 2 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.2 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, + // Answer 3 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.3 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, + // Answer 4 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.4 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, + // Additianl records, EDNS Option, 0 bytes DO=0, Z = 0 + 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { + t.Error("expected decoding to fail") + } + // returned error should wrap the original error + if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { + t.Errorf("bad error returned: %v", err) + } + if dm.DNS.MalformedPacket != true { + t.Errorf("expected packet to be malformed") + } + + // after error has been detected in the query part, we should not parse + // anything from answers + if len(dm.DNS.DNSRRs.Answers) != 0 { + t.Errorf("did not expect answers to be parsed, but there were %d parsed", len(dm.DNS.DNSRRs.Answers)) + } +} + +func TestDecodePayload_AnswerInvalidEdns(t *testing.T) { + payload := []byte{ + 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, + // Query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Answer 1 + 0xc0, 0x0c, // pointer to name + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.1 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, + // Answer 2 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.2 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, + // Answer 3 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.3 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, + // Answer 4 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.4 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, + // Additianl records, Invalid EDNS Option + 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x01, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { + t.Error("expected decoding to fail") + } + // returned error should wrap the original error + if !errors.Is(err, ErrDecodeEdnsOptionTooShort) { + t.Errorf("bad error returned: %v", err) + } + if dm.DNS.MalformedPacket != true { + t.Errorf("expected packet to be malformed") + } +} + +func TestDecodePayload_AnswerInvaliAdditional(t *testing.T) { + payload := []byte{ + 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, + // Query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Answer 1 + 0xc0, 0x0c, // pointer to name + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.1 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, + // Answer 2 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.2 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, + // Answer 3 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.3 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, + // Answer 4 + 0xc0, 0x0c, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x01, 0x2c, + // 10.10.1.4 + 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, + // Additianl records, Invalid RDLENGTH + 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { + t.Error("expected decoding to fail") + } + // returned error should wrap the original error + if !errors.Is(err, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", err) + } + if dm.DNS.MalformedPacket != true { + t.Errorf("expected packet to be malformed") + } +} + +func TestDecodePayload_AnswerError(t *testing.T) { + payload := []byte{ + // header + 0xa8, 0x1a, 0x81, 0x83, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + // query + 0x03, 0x66, 0x6f, 0x6f, + 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, + 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Authority section + // name + 0xc0, 0x10, + // type SOA, class IN + 0x00, 0x06, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x00, 0x3c, + // RDLENGTH + 0x00, 0x26, + // RDATA + // MNAME + 0x03, 0x6e, 0x73, 0x31, + 0xc0, 0x10, + // RNAME + 0x09, 0x64, 0x6e, 0x73, 0x2d, 0x61, + 0x64, 0x6d, 0x69, 0x6e, 0xc0, 0x10, + // serial + 0x19, 0xa1, 0x4a, 0xb4, + // refresh + 0x00, 0x00, 0x03, 0x84, + // retry + 0x00, 0x00, 0x03, 0x84, + // expire + 0x00, 0x00, 0x07, 0x08, + // minimum + 0x00, 0x00, 0x00, 0x3c, + // Additianl records, EDNS Option, 0 bytes DO=1, Z = 0 + 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, + 0x00, 0x80, 0x00, 0x00, 0x00, + } + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { + t.Errorf("Unexpected error while decoding payload: %v", err) + } + if dm.DNS.MalformedPacket != false { + t.Errorf("did not expect packet to be malformed") + } + + if dm.DNS.ID != 0xa81a || + dm.DNS.Opcode != 0 || + dm.DNS.Rcode != RcodeToString(3) || + !dm.DNS.Flags.QR || + dm.DNS.Flags.TC || + dm.DNS.Flags.AA || + dm.DNS.Flags.AD || + !dm.DNS.Flags.RA { + t.Error("Invalid DNS header data in message") + } + + if dm.DNS.Qname != "foo.google.com" { + t.Errorf("Unexpected query name: %s", dm.DNS.Qname) + } + if dm.DNS.Qtype != "A" { + t.Errorf("Unexpected query type: %s", dm.DNS.Qtype) + } + + if len(dm.DNS.DNSRRs.Answers) != 0 { + t.Errorf("did not expect any answers, got %d", len(dm.DNS.DNSRRs.Answers)) + } + + if len(dm.DNS.DNSRRs.Nameservers) != 1 { + t.Errorf("expected 1 authority RR, got %d", len(dm.DNS.DNSRRs.Nameservers)) + } + expected := DNSAnswer{ + Name: "google.com", + Rdatatype: RdatatypeToString(0x0006), + Class: "IN", // 0x0001, + TTL: 60, + Rdata: "ns1.google.com dns-admin.google.com 430000820 900 900 1800 60", + } + + if dm.DNS.DNSRRs.Nameservers[0] != expected { + t.Errorf("unexpected SOA record parsed, expected %v, git %v", expected, dm.DNS.DNSRRs.Nameservers[0]) + } + + if dm.EDNS.Do != 1 || + dm.EDNS.UDPSize != 1232 || + dm.EDNS.Z != 0 || + dm.EDNS.Version != 0 || + len(dm.EDNS.Options) != 0 { + t.Errorf("Unexpected EDNS data") + } + +} + +func TestDecodePayload_AnswerError_Invalid(t *testing.T) { + payload := []byte{ + // header + 0xa8, 0x1a, 0x81, 0x83, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + // query + 0x03, 0x66, 0x6f, 0x6f, + 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, + 0x63, 0x6f, 0x6d, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // Authority section + // name + 0xc0, 0x10, + // type SOA, class IN + 0x00, 0x06, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x00, 0x3c, + // RDLENGTH + 0x00, 0x26, + // RDATA + // MNAME, invalid offset in pointer + 0x03, 0x6e, 0x73, 0x31, + 0xc0, 0xff, + // RNAME + 0x09, 0x64, 0x6e, 0x73, 0x2d, 0x61, + 0x64, 0x6d, 0x69, 0x6e, 0xc0, 0x10, + // serial + 0x19, 0xa1, 0x4a, 0xb4, + // refresh + 0x00, 0x00, 0x03, 0x84, + // retry + 0x00, 0x00, 0x03, 0x84, + // expire + 0x00, 0x00, 0x07, 0x08, + // minimum + 0x00, 0x00, 0x00, 0x3c, + // Additianl records, EDNS Option, 0 bytes DO=1, Z = 0 + 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, + 0x00, 0x80, 0x00, 0x00, 0x00, + } + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { + t.Error("expected decoding to fail") + } + // returned error should wrap the original error + if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { + t.Errorf("bad error returned: %v", err) + } + if dm.DNS.MalformedPacket != true { + t.Errorf("expected packet to be malformed") + } + +} + +func TestDecodePayload_AdditionalRRAndEDNS(t *testing.T) { + // payload containing both addition RR and EDNS, ensure we are + // able to parse all of them + payload := []byte{ + 0x7b, 0x97, 0x84, 0x0, 0x0, 0x1, 0x0, 0x2, + 0x0, 0x2, 0x0, 0x5, 0xf, 0x6f, 0x63, 0x63, + 0x2d, 0x30, 0x2d, 0x31, 0x35, 0x30, 0x30, + 0x2d, 0x31, 0x35, 0x30, 0x31, 0x1, 0x31, + 0x6, 0x6e, 0x66, 0x6c, 0x78, 0x73, 0x6f, + 0x3, 0x6e, 0x65, 0x74, 0x0, 0x0, 0x1, 0x0, + 0x1, 0xc0, 0xc, 0x0, 0x1, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x1e, 0x0, 0x4, 0x2d, 0x39, 0x24, + 0x97, 0xc0, 0xc, 0x0, 0x1, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x1e, 0x0, 0x4, 0x2d, 0x39, 0x24, + 0x94, 0xc0, 0x1e, 0x0, 0x2, 0x0, 0x1, 0x0, + 0x1, 0x51, 0x80, 0x0, 0x7, 0x1, 0x65, 0x2, + 0x6e, 0x73, 0xc0, 0x1e, 0xc0, 0x1e, 0x0, + 0x2, 0x0, 0x1, 0x0, 0x1, 0x51, 0x80, 0x0, + 0x4, 0x1, 0x66, 0xc0, 0x5c, 0x0, 0x0, 0x29, + 0x4, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, + 0x5a, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x51, 0x80, + 0x0, 0x4, 0x2d, 0x39, 0x8, 0x1, 0xc0, 0x5a, + 0x0, 0x1c, 0x0, 0x1, 0x0, 0x1, 0x51, 0x80, 0x0, + 0x10, 0x2a, 0x0, 0x86, 0xc0, 0x20, 0x8, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xc0, 0x6d, + 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x51, 0x80, 0x0, 0x4, + 0x2d, 0x39, 0x9, 0x1, 0xc0, 0x6d, 0x0, 0x1c, 0x0, 0x1, + 0x0, 0x1, 0x51, 0x80, 0x0, 0x10, 0x2a, 0x0, 0x86, 0xc0, + 0x20, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1} + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("error when deocoding header: %v", err) + } + + if err := DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { + t.Errorf("unexpected error while decoding payload: %v", err) + } + + if len(dm.DNS.DNSRRs.Answers) != 2 || len(dm.DNS.DNSRRs.Nameservers) != 2 || + len(dm.DNS.DNSRRs.Records) != 4 || dm.EDNS.UDPSize != 1200 || + len(dm.EDNS.Options) != 0 { + t.Errorf("unexpected result while parsing payload: %#v", dm.DNS) + } + +} + +func TestDecodePayload_Truncated(t *testing.T) { + payload := []byte{ + // header + 0x77, 0xa0, 0x83, 0x80, 0x00, 0x01, 0x00, 0x23, + 0x00, 0x00, 0x00, 0x00, + // query + 0x02, 0x41, 0x64, 0x0d, + 0x6e, 0x6e, 0x6e, 0x6e, + 0x6e, 0x6e, 0x6e, 0x6e, + 0x6e, 0x6e, 0x6e, 0x6e, + 0x6e, 0x02, 0x46, 0x52, + 0x00, 0x00, 0x01, 0x00, 0x01, + // answer 1 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x01, + // answer 2 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x02, + // answer 3 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x03, + // answer 4 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x04, + // answer 5 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x05, + // answer 6 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x06, + // answer 7 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x07, + // answer 8 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x08, + // answer 9 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x09, + // answer 10 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x0a, + // answer 11 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x0b, + // answer 12 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x0c, + // answer 13 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x0d, + // answer 14 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x0e, + // answer 15 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x0f, + // answer 16 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x10, + // answer 17 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x11, + // answer 18 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x12, + // answer 19 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x13, + // answer 20 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x14, + // answer 21 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x15, + // answer 22 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x16, + // answer 23 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x17, + // answer 24 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x18, + // answer 25 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x19, + // answer 26 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x1a, + // answer 27 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x1b, + // answer 28 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x1c, + // answer 29 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, + 0x01, 0x1d, + // answer 30 + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x6b, 0x00, + } + + dm := DNSMessage{} + dm.DNS.Payload = payload + dm.DNS.Length = len(payload) + + header, err := DecodeDNS(payload) + if err != nil { + t.Errorf("unexpected error when decoding header: %v", err) + } + + if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { + t.Error("expected no error on decode") + } + + if dm.DNS.Flags.TC == false { + t.Error("truncated answer expected") + } + + if dm.DNS.MalformedPacket != true { + t.Errorf("expected packet to be malformed") + } + +} diff --git a/dnsutils/dns_parser_question_test.go b/dnsutils/dns_parser_question_test.go new file mode 100644 index 00000000..52cfef00 --- /dev/null +++ b/dnsutils/dns_parser_question_test.go @@ -0,0 +1,175 @@ +package dnsutils + +import ( + "errors" + "testing" + + "github.com/miekg/dns" +) + +func TestDecodeQuestion(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + payload, _ := dm.Pack() + + qname, qtype, qclass, offsetRR, _ := DecodeQuestion(1, payload) + if ClassToString(qclass) != "IN" { + t.Errorf("invalid qclass: %d", qclass) + } + + if qname+"." != fqdn { + t.Errorf("invalid qname: %s", qname) + } + + if RdatatypeToString(qtype) != "A" { + t.Errorf("invalid qtype: %d", qtype) + } + if offsetRR != len(payload) { + t.Errorf("invalid offset: %d, payload len: %d", offsetRR, len(payload)) + } +} + +func TestDecodeQuestion_Multiple(t *testing.T) { + paylaod := []byte{ + 0x9e, 0x84, 0x01, 0x20, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // query 1 + 0x01, 0x61, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // query 2 + 0x01, 0x62, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // query 3 + 0x01, 0x63, 0x00, + // type AAAA, class IN + 0x00, 0x1c, 0x00, 0x01, + } + + qname, qtype, qclass, offset, err := DecodeQuestion(3, paylaod) + if err != nil { + t.Errorf("unexpected error %v", err) + } + if qname != "c" || RdatatypeToString(qtype) != "AAAA" { + t.Errorf("expected qname=C, type=AAAA, got qname=%s, type=%s", qname, RdatatypeToString(qtype)) + } + if ClassToString(qclass) != "IN" { + t.Errorf("expected qclass=IN %s", ClassToString(qclass)) + } + if offset != 33 { + t.Errorf("expected resulting offset to be 33, got %d", offset) + } +} + +func TestDecodeQuestion_Multiple_InvalidCount(t *testing.T) { + paylaod := []byte{ + 0x9e, 0x84, 0x01, 0x20, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // query 1 + 0x01, 0x61, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // query 2 + 0x01, 0x62, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // query 3 + 0x01, 0x63, 0x00, + // type AAAA, class IN + 0x00, 0x1c, 0x00, 0x01, + } + + _, _, _, _, err := DecodeQuestion(4, paylaod) + if !errors.Is(err, ErrDecodeDNSLabelTooShort) { + t.Errorf("bad error received: %v", err) + } +} + +func TestDecodeQuestion_SkipOpt(t *testing.T) { + payload := []byte{ + 0x43, 0xac, 0x01, 0x00, 0x00, 0x01, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, + // Query section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer Resource Records + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type OPT, class IN + 0x00, 0x29, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x01, + // RDATA + 0x01, + // 2nd resource record + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x04, + // RDATA + 0x7f, 0x00, 0x00, 0x01, + } + _, _, _, offsetrr, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("Unexpected error decoding question: %v", err) + } + + answer, _, erra := DecodeAnswer(2, offsetrr, payload) + if erra != nil { + t.Errorf("Unexpected error decoding answer: %v", erra) + } + if len(answer) != 1 { + t.Fatalf("Expected answer to contain one resource record, got %d", len(answer)) + } + if answer[0].Rdatatype != RdatatypeToString(0x01) || answer[0].Rdata != "127.0.0.1" { + t.Errorf("unexpected answer %s %s, expected A 127.0.0.1", answer[0].Rdatatype, answer[0].Rdata) + } +} + +func TestDecodeDnsQuestion_InvalidOffset(t *testing.T) { + decoded := []byte{183, 59, 130, 217, 128, 16, 0, 51, 165, 67, 0, 0} + _, _, _, _, err := DecodeQuestion(1, decoded) + if !errors.Is(err, ErrDecodeDNSLabelTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsQuestion_PacketTooShort(t *testing.T) { + decoded := []byte{183, 59, 130, 217, 128, 16, 0, 51, 165, 67, 0, 0, 1, 1, 8, 10, 23} + _, _, _, _, err := DecodeQuestion(1, decoded) + if !errors.Is(err, ErrDecodeDNSLabelTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsQuestion_QtypeMissing(t *testing.T) { + decoded := []byte{88, 27, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 15, 100, 110, 115, 116, 97, 112, + 99, 111, 108, 108, 101, 99, 116, 111, 114, 4, 116, 101, 115, 116, 0} + _, _, _, _, err := DecodeQuestion(1, decoded) + if !errors.Is(err, ErrDecodeQuestionQtypeTooShort) { + t.Errorf("bad error returned: %v", err) + } +} + +func TestDecodeDnsQuestion_InvalidPointer(t *testing.T) { + decoded := []byte{88, 27, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 202} + _, _, _, _, err := DecodeQuestion(1, decoded) + if !errors.Is(err, ErrDecodeDNSLabelTooShort) { + t.Errorf("bad error returned: %v", err) + } +} diff --git a/dnsutils/dns_parser_rdata_test.go b/dnsutils/dns_parser_rdata_test.go new file mode 100644 index 00000000..3fb2ff7a --- /dev/null +++ b/dnsutils/dns_parser_rdata_test.go @@ -0,0 +1,661 @@ +package dnsutils + +import ( + "errors" + "fmt" + "testing" + + "github.com/miekg/dns" +) + +func TestRdatatypeValid(t *testing.T) { + rdt := RdatatypeToString(1) + if rdt != "A" { + t.Errorf("rdatatype A expected: %s", rdt) + } +} + +func TestRdatatypeInvalid(t *testing.T) { + rdt := RdatatypeToString(100000) + if rdt != "UNKNOWN" { + t.Errorf("rdatatype - expected: %s", rdt) + } +} + +func TestDecodeRdataA(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "127.0.0.1" + rr1, _ := dns.NewRR(fmt.Sprintf("%s A %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata A, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataA_Short(t *testing.T) { + payload := []byte{ + 0x43, 0xac, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // Query section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer Resource Record + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x03, + // RDATA (1 byte too short for A record) + 0x7f, 0x00, 0x00, + } + _, _, _, offsetrr, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("Unexpected error decoding question: %v", err) + } + + _, _, erra := DecodeAnswer(1, offsetrr, payload) + if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", erra) + } +} + +func TestDecodeRdataAAAA(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "fe8::2" + rr1, _ := dns.NewRR(fmt.Sprintf("%s AAAA %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata AAAA, want %s, got: %s", rdata, answer[0].Rdata) + } +} +func TestDecodeRdataAAAA_Short(t *testing.T) { + payload := []byte{ + // header + 0x3b, 0x33, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // Query section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer resource record + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type AAAA, class IN + 0x00, 0x1c, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x0c, + // RDATA + 0xfe, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + } + + _, _, _, offsetSetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + _, _, erra := DecodeAnswer(1, offsetSetRR, payload) + if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", erra) + } +} + +func TestDecodeRdataCNAME(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "test.collector.org" + rr1, _ := dns.NewRR(fmt.Sprintf("%s CNAME %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata CNAME, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataMX(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "5 gmail-smtp-in.l.google.com" + rr1, _ := dns.NewRR(fmt.Sprintf("%s MX %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata MX, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataMX_Short(t *testing.T) { + payload := []byte{ + // header + 0xed, 0x7f, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // Question seection + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer Resource Record + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type MX, class IN + 0x00, 0x0f, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x01, + // RDATA + 0x00, + } + _, _, _, offsetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + _, _, erra := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", erra) + } + +} + +func TestDecodeRdataMX_Minimal(t *testing.T) { + payload := []byte{ + // header + 0xed, 0x7f, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // Question seection + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer Resource Record + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type MX, class IN + 0x00, 0x0f, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x03, + // RDATA + 0x00, 0x00, 0x00, + } + _, _, _, offsetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + answer, _, erra := DecodeAnswer(1, offsetRR, payload) + if erra != nil { + t.Errorf("unexpected error while decoding answer: %v", err) + } + expected := "0 " + if answer[0].Rdata != expected { + t.Errorf("invalid decode for MX rdata, expected %s got %s", expected, answer[0].Rdata) + } +} + +func TestDecodeRdataSRV(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "20 0 5222 alt2.xmpp.l.google.com" + rr1, _ := dns.NewRR(fmt.Sprintf("%s SRV %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata SRV, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataSRV_Short(t *testing.T) { + payload := []byte{ + // header + 0xd9, 0x93, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // Question section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer section + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type SRV, class IN + 0x00, 0x21, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x04, + // RDATA + // priority + 0x00, 0x14, + // weight + 0x00, 0x00, + // missing port and target + } + + _, _, _, offsetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + _, _, erra := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", erra) + } +} + +func TestDecodeRdataSRV_Minimal(t *testing.T) { + payload := []byte{ + // header + 0xd9, 0x93, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // Question section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer section + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type SRV, class IN + 0x00, 0x21, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x07, + // RDATA + // priority + 0x00, 0x14, + // weight + 0x00, 0x00, + // port + 0x00, 0x10, + // empty target + 0x00, + } + + _, _, _, offsetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + answer, _, erra := DecodeAnswer(1, offsetRR, payload) + if erra != nil { + t.Errorf("unexpected error while decoding answer: %v", err) + } + expectedRdata := "20 0 16 " + if answer[0].Rdata != expectedRdata { + t.Errorf("invalid decode for rdata SRV, want %s, got: %s", expectedRdata, answer[0].Rdata) + } +} +func TestDecodeRdataNS(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "ns1.dnscollector" + rr1, _ := dns.NewRR(fmt.Sprintf("%s NS %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata NS, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataTXT(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "hello world" + rr1, _ := dns.NewRR(fmt.Sprintf("%s TXT \"%s\"", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata TXT, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataTXT_Empty(t *testing.T) { + payload := []byte{ + // header + 0x86, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // question section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // answer section + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type TXT, class IN + 0x00, 0x10, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x00, + // no data + } + + _, _, _, offsetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + _, _, erra := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", erra) + } + +} +func TestDecodeRdataTXT_Short(t *testing.T) { + payload := []byte{ + // header + 0x86, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // question section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // answer section + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type TXT, class IN + 0x00, 0x10, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x0a, + // RDATA + // length + 0x0b, + // characters + 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, + // missing two bytes + } + + _, _, _, offsetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + _, _, erra := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", erra) + } + +} +func TestDecodeRdataTXT_NoTxt(t *testing.T) { + payload := []byte{ + // header + 0x86, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // question section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // answer section + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type TXT, class IN + 0x00, 0x10, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH + 0x00, 0x01, + // RDATA + // length + 0x00, + // no txt-data + } + + _, _, _, offsetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + answer, _, erra := DecodeAnswer(1, offsetRR, payload) + if erra != nil { + t.Errorf("unexpected error while decoding answer: %v", err) + } + + if answer[0].Rdata != "" { + t.Errorf("expected empty string in RDATA, got: %s", answer[0].Rdata) + } + +} + +func TestDecodeRdataPTR(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "one.one.one.one" + rr1, _ := dns.NewRR(fmt.Sprintf("%s PTR %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata PTR, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataSOA(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeA) + + rdata := "ns1.google.com dns-admin.google.com 412412655 900 900 1800 60" + rr1, _ := dns.NewRR(fmt.Sprintf("%s SOA %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata SOA, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataSOA_Short(t *testing.T) { + payload := []byte{ + // header + 0x28, 0xba, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + // Query section + 0x0f, 0x64, 0x6e, 0x73, + 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer Resource Record, + 0x0f, 0x64, + 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, + // type SOA, class IN + 0x00, 0x06, 0x00, 0x01, + // TTL + 0x00, 0x00, 0x0e, 0x10, + // RDLENGTH 54 + 0x00, 0x36, + // RDATA + // MNAME + 0x03, 0x6e, + 0x73, 0x31, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // RNAME + 0x09, 0x64, + 0x6e, 0x73, 0x2d, 0x61, 0x64, 0x6d, 0x69, 0x6e, + 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, + 0x63, 0x6f, 0x6d, 0x00, + // serial + 0x18, 0x94, 0xea, 0xef, + // refresh + 0x00, 0x00, 0x03, 0x84, + // retry + 0x00, 0x00, 0x03, 0x84, + // expire + 0x00, 0x00, 0x07, 0x08, + // minimum -field missing from the RDATA + } + + _, _, _, offsetRR, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("Unable to decode question: %v", err) + } + _, _, erra := DecodeAnswer(1, offsetRR, payload) + if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { + t.Errorf("bad error returned: %v", erra) + } +} + +func TestDecodeRdataSOA_Minimization(t *testing.T) { + // loop between qnames + payload := []byte{164, 66, 129, 128, 0, 1, 0, 0, 0, 1, 0, 0, 8, 102, 114, 101, 115, 104, 114, 115, 115, 4, 109, + 99, 104, 100, 2, 109, 101, 0, 0, 28, 0, 1, 192, 21, 0, 6, 0, 1, 0, 0, 0, 60, 0, 43, 6, 100, 110, 115, 49, 48, + 51, 3, 111, 118, 104, 3, 110, 101, 116, 0, 4, 116, 101, 99, 104, 192, 53, + 120, 119, 219, 34, 0, 1, 81, 128, 0, 0, 14, 16, 0, 54, 238, 128, 0, 0, 0, 60} + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + _, _, err := DecodeAnswer(1, offsetRR, payload) + if err != nil { + t.Errorf(" error returned: %v", err) + } +} + +func TestDecodeRdataSVCB_alias(t *testing.T) { + fqdn := TestQName + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeSVCB) + + // draft-ietf-dnsop-svcb-https-12 Appendix D.1 + rdata := "0 foo.example.com" + rr1, _ := dns.NewRR(fmt.Sprintf("%s SVCB %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata SOA, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataSVCB_params(t *testing.T) { + fqdn := TestQName + + vectors := []string{ + "0 foo.example.com", // draft-ietf-dnsop-svcb-https-12 Appendix D.1 + "1 .", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 3 + "16 foo.example.com port=53", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 4 + "1 foo.example.com key667=hello", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 5 + `1 foo.example.com key667="hello\210qoo"`, // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 6 + "1 foo.example.com ipv6hint=2001:db8::1,2001:db8::53:1", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 7, modified (single line) + "16 foo.example.org mandatory=alpn,ipv4hint alpn=h2,h3-19 ipv4hint=192.0.2.1", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 9, modified (sorted) + "16 foo.example.org mandatory=alpn,ipv4hint alpn=h2,h3-19 ipv4hint=192.0.2.1,192.0.2.2", + } + + for _, rdata := range vectors { + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeSVCB) + rr1, _ := dns.NewRR(fmt.Sprintf("%s SVCB %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + payload, _ := dm.Pack() + _, _, _, offsetRR, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata SVCB, want %s, got: %s", rdata, answer[0].Rdata) + } + } +} diff --git a/dnsutils/dns_parser_test.go b/dnsutils/dns_parser_test.go index 9afb27a6..e4c76acf 100644 --- a/dnsutils/dns_parser_test.go +++ b/dnsutils/dns_parser_test.go @@ -2,34 +2,11 @@ package dnsutils import ( "errors" - "fmt" "testing" - "github.com/dmachard/go-dnscollector/pkgconfig" "github.com/miekg/dns" ) -const ( - TestQName = "dnstapcollector.test." -) - -// Benchmark - -func BenchmarkDnsParseLabels(b *testing.B) { - payload := []byte{0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x76, 0x69, - 0x74, 0x79, 0x2d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x06, - 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x03, 0x63, 0x6f, 0x6d, 0x00, - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _, err := ParseLabels(0, payload) - if err != nil { - b.Fatalf("could not parse labels: %v\n", err) - } - } -} - -// Regular tests func TestRcodeValid(t *testing.T) { rcode := RcodeToString(0) if rcode != "NOERROR" { @@ -44,20 +21,6 @@ func TestRcodeInvalid(t *testing.T) { } } -func TestRdatatypeValid(t *testing.T) { - rdt := RdatatypeToString(1) - if rdt != "A" { - t.Errorf("rdatatype A expected: %s", rdt) - } -} - -func TestRdatatypeInvalid(t *testing.T) { - rdt := RdatatypeToString(100000) - if rdt != "UNKNOWN" { - t.Errorf("rdatatype - expected: %s", rdt) - } -} - func TestDecodeDns(t *testing.T) { dm := new(dns.Msg) dm.SetQuestion(TestQName, dns.TypeA) @@ -69,843 +32,6 @@ func TestDecodeDns(t *testing.T) { } } -func TestDecodeQuestion(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - payload, _ := dm.Pack() - - qname, qtype, qclass, offsetRR, _ := DecodeQuestion(1, payload) - if ClassToString(qclass) != "IN" { - t.Errorf("invalid qclass: %d", qclass) - } - - if qname+"." != fqdn { - t.Errorf("invalid qname: %s", qname) - } - - if RdatatypeToString(qtype) != "A" { - t.Errorf("invalid qtype: %d", qtype) - } - if offsetRR != len(payload) { - t.Errorf("invalid offset: %d, payload len: %d", offsetRR, len(payload)) - } -} - -func TestDecodeQuestion_Multiple(t *testing.T) { - paylaod := []byte{ - 0x9e, 0x84, 0x01, 0x20, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - // query 1 - 0x01, 0x61, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // query 2 - 0x01, 0x62, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // query 3 - 0x01, 0x63, 0x00, - // type AAAA, class IN - 0x00, 0x1c, 0x00, 0x01, - } - - qname, qtype, qclass, offset, err := DecodeQuestion(3, paylaod) - if err != nil { - t.Errorf("unexpected error %v", err) - } - if qname != "c" || RdatatypeToString(qtype) != "AAAA" { - t.Errorf("expected qname=C, type=AAAA, got qname=%s, type=%s", qname, RdatatypeToString(qtype)) - } - if ClassToString(qclass) != "IN" { - t.Errorf("expected qclass=IN %s", ClassToString(qclass)) - } - if offset != 33 { - t.Errorf("expected resulting offset to be 33, got %d", offset) - } -} - -func TestDecodeQuestion_Multiple_InvalidCount(t *testing.T) { - paylaod := []byte{ - 0x9e, 0x84, 0x01, 0x20, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - // query 1 - 0x01, 0x61, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // query 2 - 0x01, 0x62, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // query 3 - 0x01, 0x63, 0x00, - // type AAAA, class IN - 0x00, 0x1c, 0x00, 0x01, - } - - _, _, _, _, err := DecodeQuestion(4, paylaod) - if !errors.Is(err, ErrDecodeDNSLabelTooShort) { - t.Errorf("bad error received: %v", err) - } -} - -func TestDecodeAnswer_Ns(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rrNs, _ := dns.NewRR("root-servers.net NS c.root-servers.net") - rrA, _ := dns.NewRR(fmt.Sprintf("%s A 127.0.0.1", fqdn)) - - m := new(dns.Msg) - m.SetReply(dm) - m.Authoritative = true - m.Answer = append(m.Answer, rrA) - m.Ns = append(m.Ns, rrNs) - - payload, _ := m.Pack() - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - _, offsetRRns, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - nsAnswers, _, _ := DecodeAnswer(len(m.Ns), offsetRRns, payload) - if len(nsAnswers) != len(m.Ns) { - t.Errorf("invalid decode answer, want %d, got: %d", len(m.Ns), len(nsAnswers)) - } -} - -func TestDecodeAnswer(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - rr1, _ := dns.NewRR(fmt.Sprintf("%s A 127.0.0.1", fqdn)) - rr2, _ := dns.NewRR(fmt.Sprintf("%s A 127.0.0.2", fqdn)) - dm.Answer = append(dm.Answer, rr1) - dm.Answer = append(dm.Answer, rr2) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if len(answer) != len(dm.Answer) { - t.Errorf("invalid decode answer, want %d, got: %d", len(dm.Answer), len(answer)) - } -} - -func TestDecodeRdataSVCB_alias(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeSVCB) - - // draft-ietf-dnsop-svcb-https-12 Appendix D.1 - rdata := "0 foo.example.com" - rr1, _ := dns.NewRR(fmt.Sprintf("%s SVCB %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata SOA, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataSVCB_params(t *testing.T) { - fqdn := TestQName - - vectors := []string{ - "0 foo.example.com", // draft-ietf-dnsop-svcb-https-12 Appendix D.1 - "1 .", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 3 - "16 foo.example.com port=53", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 4 - "1 foo.example.com key667=hello", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 5 - `1 foo.example.com key667="hello\210qoo"`, // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 6 - "1 foo.example.com ipv6hint=2001:db8::1,2001:db8::53:1", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 7, modified (single line) - "16 foo.example.org mandatory=alpn,ipv4hint alpn=h2,h3-19 ipv4hint=192.0.2.1", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 9, modified (sorted) - "16 foo.example.org mandatory=alpn,ipv4hint alpn=h2,h3-19 ipv4hint=192.0.2.1,192.0.2.2", - } - - for _, rdata := range vectors { - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeSVCB) - rr1, _ := dns.NewRR(fmt.Sprintf("%s SVCB %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - payload, _ := dm.Pack() - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata SVCB, want %s, got: %s", rdata, answer[0].Rdata) - } - } -} - -func TestDecodeAnswer_QnameMinimized(t *testing.T) { - payload := []byte{0x8d, 0xda, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x05, 0x74, - 0x65, 0x61, 0x6d, 0x73, 0x09, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05, 0x00, - 0x01, 0x00, 0x00, 0x50, 0xa8, 0x00, 0x0f, 0x05, 0x74, 0x65, 0x61, 0x6d, 0x73, 0x06, - 0x6f, 0x66, 0x66, 0x69, 0x63, 0x65, 0xc0, 0x1c, 0xc0, 0x31, 0x00, 0x05, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x3e, 0x00, 0x26, 0x10, 0x74, 0x65, 0x61, 0x6d, 0x73, 0x2d, 0x6f, 0x66, - 0x66, 0x69, 0x63, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x06, 0x73, 0x2d, 0x30, 0x30, 0x30, 0x35, - 0x08, 0x73, 0x2d, 0x6d, 0x73, 0x65, 0x64, 0x67, 0x65, 0x03, 0x6e, 0x65, 0x74, 0x00, 0xc0, - 0x4c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x13, 0x06, 0x73, 0x2d, 0x30, - 0x30, 0x30, 0x35, 0x09, 0x64, 0x63, 0x2d, 0x6d, 0x73, 0x65, 0x64, 0x67, 0x65, 0xc0, 0x6d, - 0xc0, 0x7e, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x04, 0x34, 0x71, 0xc3, - 0x84, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - _, _, err := DecodeAnswer(4, offsetRR, payload) - if err != nil { - t.Errorf("failed to decode valid dns packet with minimization") - } -} - -func TestDecodeRdataA(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "127.0.0.1" - rr1, _ := dns.NewRR(fmt.Sprintf("%s A %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata A, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataA_Short(t *testing.T) { - payload := []byte{ - 0x43, 0xac, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // Query section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // Answer Resource Record - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x03, - // RDATA (1 byte too short for A record) - 0x7f, 0x00, 0x00, - } - _, _, _, offsetrr, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("Unexpected error decoding question: %v", err) - } - - _, _, erra := DecodeAnswer(1, offsetrr, payload) - if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", erra) - } -} - -func TestDecodeRdataAAAA(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "fe8::2" - rr1, _ := dns.NewRR(fmt.Sprintf("%s AAAA %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata AAAA, want %s, got: %s", rdata, answer[0].Rdata) - } -} -func TestDecodeRdataAAAA_Short(t *testing.T) { - payload := []byte{ - // header - 0x3b, 0x33, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // Query section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // Answer resource record - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type AAAA, class IN - 0x00, 0x1c, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x0c, - // RDATA - 0xfe, 0x80, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - } - - _, _, _, offsetSetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - _, _, erra := DecodeAnswer(1, offsetSetRR, payload) - if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", erra) - } -} - -func TestDecodeRdataCNAME(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "test.collector.org" - rr1, _ := dns.NewRR(fmt.Sprintf("%s CNAME %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata CNAME, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataMX(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "5 gmail-smtp-in.l.google.com" - rr1, _ := dns.NewRR(fmt.Sprintf("%s MX %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata MX, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataMX_Short(t *testing.T) { - payload := []byte{ - // header - 0xed, 0x7f, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // Question seection - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // Answer Resource Record - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type MX, class IN - 0x00, 0x0f, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x01, - // RDATA - 0x00, - } - _, _, _, offsetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - _, _, erra := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", erra) - } - -} - -func TestDecodeRdataMX_Minimal(t *testing.T) { - payload := []byte{ - // header - 0xed, 0x7f, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // Question seection - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // Answer Resource Record - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type MX, class IN - 0x00, 0x0f, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x03, - // RDATA - 0x00, 0x00, 0x00, - } - _, _, _, offsetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - answer, _, erra := DecodeAnswer(1, offsetRR, payload) - if erra != nil { - t.Errorf("unexpected error while decoding answer: %v", err) - } - expected := "0 " - if answer[0].Rdata != expected { - t.Errorf("invalid decode for MX rdata, expected %s got %s", expected, answer[0].Rdata) - } -} - -func TestDecodeRdataSRV(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "20 0 5222 alt2.xmpp.l.google.com" - rr1, _ := dns.NewRR(fmt.Sprintf("%s SRV %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata SRV, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataSRV_Short(t *testing.T) { - payload := []byte{ - // header - 0xd9, 0x93, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // Question section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // Answer section - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type SRV, class IN - 0x00, 0x21, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x04, - // RDATA - // priority - 0x00, 0x14, - // weight - 0x00, 0x00, - // missing port and target - } - - _, _, _, offsetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - _, _, erra := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", erra) - } -} - -func TestDecodeRdataSRV_Minimal(t *testing.T) { - payload := []byte{ - // header - 0xd9, 0x93, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // Question section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // Answer section - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type SRV, class IN - 0x00, 0x21, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x07, - // RDATA - // priority - 0x00, 0x14, - // weight - 0x00, 0x00, - // port - 0x00, 0x10, - // empty target - 0x00, - } - - _, _, _, offsetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - answer, _, erra := DecodeAnswer(1, offsetRR, payload) - if erra != nil { - t.Errorf("unexpected error while decoding answer: %v", err) - } - expectedRdata := "20 0 16 " - if answer[0].Rdata != expectedRdata { - t.Errorf("invalid decode for rdata SRV, want %s, got: %s", expectedRdata, answer[0].Rdata) - } -} -func TestDecodeRdataNS(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "ns1.dnscollector" - rr1, _ := dns.NewRR(fmt.Sprintf("%s NS %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata NS, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataTXT(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "hello world" - rr1, _ := dns.NewRR(fmt.Sprintf("%s TXT \"%s\"", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata TXT, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataTXT_Empty(t *testing.T) { - payload := []byte{ - // header - 0x86, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // question section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // answer section - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type TXT, class IN - 0x00, 0x10, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x00, - // no data - } - - _, _, _, offsetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - _, _, erra := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", erra) - } - -} -func TestDecodeRdataTXT_Short(t *testing.T) { - payload := []byte{ - // header - 0x86, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // question section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // answer section - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type TXT, class IN - 0x00, 0x10, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x0a, - // RDATA - // length - 0x0b, - // characters - 0x68, - 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, - // missing two bytes - } - - _, _, _, offsetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - _, _, erra := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", erra) - } - -} -func TestDecodeRdataTXT_NoTxt(t *testing.T) { - payload := []byte{ - // header - 0x86, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // question section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // answer section - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type TXT, class IN - 0x00, 0x10, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x01, - // RDATA - // length - 0x00, - // no txt-data - } - - _, _, _, offsetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - answer, _, erra := DecodeAnswer(1, offsetRR, payload) - if erra != nil { - t.Errorf("unexpected error while decoding answer: %v", err) - } - - if answer[0].Rdata != "" { - t.Errorf("expected empty string in RDATA, got: %s", answer[0].Rdata) - } - -} - -func TestDecodeRdataPTR(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "one.one.one.one" - rr1, _ := dns.NewRR(fmt.Sprintf("%s PTR %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata PTR, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataSOA(t *testing.T) { - fqdn := TestQName - - dm := new(dns.Msg) - dm.SetQuestion(fqdn, dns.TypeA) - - rdata := "ns1.google.com dns-admin.google.com 412412655 900 900 1800 60" - rr1, _ := dns.NewRR(fmt.Sprintf("%s SOA %s", fqdn, rdata)) - dm.Answer = append(dm.Answer, rr1) - - payload, _ := dm.Pack() - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - answer, _, _ := DecodeAnswer(len(dm.Answer), offsetRR, payload) - - if answer[0].Rdata != rdata { - t.Errorf("invalid decode for rdata SOA, want %s, got: %s", rdata, answer[0].Rdata) - } -} - -func TestDecodeRdataSOA_Short(t *testing.T) { - payload := []byte{ - // header - 0x28, 0xba, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - // Query section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // Answer Resource Record, - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type SOA, class IN - 0x00, 0x06, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH 54 - 0x00, 0x36, - // RDATA - // MNAME - 0x03, 0x6e, - 0x73, 0x31, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, - // RNAME - 0x09, 0x64, - 0x6e, 0x73, 0x2d, 0x61, 0x64, 0x6d, 0x69, 0x6e, - 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, - 0x63, 0x6f, 0x6d, 0x00, - // serial - 0x18, 0x94, 0xea, 0xef, - // refresh - 0x00, 0x00, 0x03, 0x84, - // retry - 0x00, 0x00, 0x03, 0x84, - // expire - 0x00, 0x00, 0x07, 0x08, - // minimum -field missing from the RDATA - } - - _, _, _, offsetRR, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("Unable to decode question: %v", err) - } - _, _, erra := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(erra, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", erra) - } -} - -func TestDecodeRdataSOA_Minimization(t *testing.T) { - // loop between qnames - payload := []byte{164, 66, 129, 128, 0, 1, 0, 0, 0, 1, 0, 0, 8, 102, 114, 101, 115, 104, 114, 115, 115, 4, 109, - 99, 104, 100, 2, 109, 101, 0, 0, 28, 0, 1, 192, 21, 0, 6, 0, 1, 0, 0, 0, 60, 0, 43, 6, 100, 110, 115, 49, 48, - 51, 3, 111, 118, 104, 3, 110, 101, 116, 0, 4, 116, 101, 99, 104, 192, 53, - 120, 119, 219, 34, 0, 1, 81, 128, 0, 0, 14, 16, 0, 54, 238, 128, 0, 0, 0, 60} - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - _, _, err := DecodeAnswer(1, offsetRR, payload) - if err != nil { - t.Errorf(" error returned: %v", err) - } -} -func TestDecodeQuestion_SkipOpt(t *testing.T) { - payload := []byte{ - 0x43, 0xac, 0x01, 0x00, 0x00, 0x01, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, - // Query section - 0x0f, 0x64, 0x6e, 0x73, - 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, - // Answer Resource Records - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type OPT, class IN - 0x00, 0x29, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x01, - // RDATA - 0x01, - // 2nd resource record - 0x0f, 0x64, - 0x6e, 0x73, 0x74, 0x61, 0x70, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x0e, 0x10, - // RDLENGTH - 0x00, 0x04, - // RDATA - 0x7f, 0x00, 0x00, 0x01, - } - _, _, _, offsetrr, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("Unexpected error decoding question: %v", err) - } - - answer, _, erra := DecodeAnswer(2, offsetrr, payload) - if erra != nil { - t.Errorf("Unexpected error decoding answer: %v", erra) - } - if len(answer) != 1 { - t.Fatalf("Expected answer to contain one resource record, got %d", len(answer)) - } - if answer[0].Rdatatype != RdatatypeToString(0x01) || answer[0].Rdata != "127.0.0.1" { - t.Errorf("unexpected answer %s %s, expected A 127.0.0.1", answer[0].Rdatatype, answer[0].Rdata) - } -} - func TestDecodeDns_HeaderTooShort(t *testing.T) { decoded := []byte{183, 59} _, err := DecodeDNS(decoded) @@ -913,1434 +39,3 @@ func TestDecodeDns_HeaderTooShort(t *testing.T) { t.Errorf("bad error returned: %v", err) } } - -func TestDecodeDnsQuestion_InvalidOffset(t *testing.T) { - decoded := []byte{183, 59, 130, 217, 128, 16, 0, 51, 165, 67, 0, 0} - _, _, _, _, err := DecodeQuestion(1, decoded) - if !errors.Is(err, ErrDecodeDNSLabelTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsQuestion_PacketTooShort(t *testing.T) { - decoded := []byte{183, 59, 130, 217, 128, 16, 0, 51, 165, 67, 0, 0, 1, 1, 8, 10, 23} - _, _, _, _, err := DecodeQuestion(1, decoded) - if !errors.Is(err, ErrDecodeDNSLabelTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsQuestion_QtypeMissing(t *testing.T) { - decoded := []byte{88, 27, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 15, 100, 110, 115, 116, 97, 112, - 99, 111, 108, 108, 101, 99, 116, 111, 114, 4, 116, 101, 115, 116, 0} - _, _, _, _, err := DecodeQuestion(1, decoded) - if !errors.Is(err, ErrDecodeQuestionQtypeTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeQuestion_InvalidPointer(t *testing.T) { - decoded := []byte{88, 27, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 202} - _, _, _, _, err := DecodeQuestion(1, decoded) - if !errors.Is(err, ErrDecodeDNSLabelTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsAnswer_PacketTooShort(t *testing.T) { - payload := []byte{46, 172, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 15, 100, 110, 115, 116, 97, 112, 99, 111, 108, 108, 101, 99, 116, - 111, 114, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, 15, 100, 110, 115, 116, 97, 112, 99, 111, 108, 108, 101, 99, 116, - 111, 114, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, 0, 0, 14, 16, 0} - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - _, _, err := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(err, ErrDecodeDNSAnswerTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsAnswer_PathologicalPacket(t *testing.T) { - // Create a message with one question and `n` answers (`n` determined later). - decoded := make([]byte, 65500) - copy(decoded, []byte{88, 27, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0}) - - // Create a rather suboptimal name for the question. - // The answers point to this a bajillion times later. - // This name breaks several rules:v - // * Label length is > 63 - // * Name length is > 255 bytes - // * Pointers jump all over the place, not just backwards - i := 12 - for { - // Create a bunch of interleaved labels of length 191, - // each label immediately followed by a pointer to the - // label next to it. The last label of the interleaved chunk - // is followed with a pointer to forwards to the next chunk - // of interleaved labels: - // - // [191 ... 191 ... 191 ... ... ptr1 ptr2 ... ptrN 191 ... 191 ...] - // ^ ^ │ │ │ ^ - // │ └────────────┼────┘ └────┘ - // └───────────────────┘ - // - // We then repeat this pattern as many times as we can within the - // first 16383 bytes (so that we can point to it later). - // Then cleanly closing the name with a null byte in the end allows us to - // create a name of around 700 kilobytes (I checked once, don't quote me on this). - if 16384-i < 384 { - decoded[i] = 0 - break - } - for j := 0; j < 192; j += 2 { - decoded[i] = 191 - i += 2 - } - for j := 0; j < 190; j += 2 { - offset := i - 192 + 2 - decoded[i] = 0xc0 | byte(offset>>8) - decoded[i+1] = byte(offset & 0xff) - i += 2 - } - offset := i + 2 - decoded[i] = 0xc0 | byte(offset>>8) - decoded[i+1] = byte(offset & 0xff) - i += 2 - } - - // Fill in the rest of the question - copy(decoded[i:], []byte{0, 5, 0, 1}) - i += 4 - - // Fit as many answers as we can that contain CNAME RDATA pointing to - // the bloated name created above. - ancount := 0 - for j := i; j+13 <= len(decoded); j += 13 { - copy(decoded[j:], []byte{0, 0, 5, 0, 0, 0, 0, 0, 1, 0, 2, 192, 12}) - ancount += 1 - } - - // Update the message with the answer count - decoded[6] = byte(ancount >> 8) - decoded[7] = byte(ancount & 0xff) - - _, _, err := DecodeAnswer(ancount, i, decoded) - if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsAnswer_RdataTooShort(t *testing.T) { - payload := []byte{46, 172, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 15, 100, 110, 115, 116, 97, 112, 99, 111, 108, 108, 101, 99, 116, - 111, 114, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, 15, 100, 110, 115, 116, 97, 112, 99, 111, 108, 108, 101, 99, 116, - 111, 114, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, 0, 0, 14, 16, 0, 4, 127, 0} - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - _, _, err := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(err, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsAnswer_InvalidPtr(t *testing.T) { - payload := []byte{128, 177, 129, 160, 0, 1, 0, 1, 0, 0, 0, 1, 5, 104, 101, 108, 108, 111, 4, - 109, 99, 104, 100, 2, 109, 101, 0, 0, 1, 0, 1, 192, 254, 0, 1, 0, 1, 0, 0, - 14, 16, 0, 4, 83, 112, 146, 176} - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - _, _, err := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsAnswer_InvalidPtr_Loop1(t *testing.T) { - // loop qname on himself - payload := []byte{128, 177, 129, 160, 0, 1, 0, 1, 0, 0, 0, 1, 5, 104, 101, 108, 108, 111, 4, - 109, 99, 104, 100, 2, 109, 101, 0, 0, 1, 0, 1, 192, 31, 0, 1, 0, 1, 0, 0, - 14, 16, 0, 4, 83, 112, 146, 176} - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - _, _, err := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsAnswer_InvalidPtr_Loop2(t *testing.T) { - // loop between qnames - payload := []byte{128, 177, 129, 160, 0, 1, 0, 2, 0, 0, 0, 1, 5, 104, 101, 108, 108, 111, 4, - 109, 99, 104, 100, 2, 109, 101, 0, 0, 1, 0, 1, 192, 47, 0, 1, 0, 1, 0, 0, - 14, 16, 0, 4, 83, 112, 146, 176, 192, 31, 0, 1, 0, 1, 0, 0, - 14, 16, 0, 4, 83, 112, 146, 176} - - _, _, _, offsetRR, _ := DecodeQuestion(1, payload) - _, _, err := DecodeAnswer(1, offsetRR, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidOffset_NegativeOffset(t *testing.T) { - payload := []byte{0x01, 0x61, 0x00} - - _, _, err := ParseLabels(-1, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidOffset) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidOffset_StartOutOfBounds(t *testing.T) { - payload := []byte{0x01, 0x61, 0x00} - - _, _, err := ParseLabels(4, payload) - if !errors.Is(err, ErrDecodeDNSLabelTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidOffset_RunOutOfBounds(t *testing.T) { - payload := []byte{0x01, 0x61} - - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidOffset_PointerByteOutOfBounds(t *testing.T) { - payload := []byte{0x01, 0x61, 0xc0} - - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_LabelTooShort(t *testing.T) { - payload := []byte{0x01} - - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelTooShort) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_NoExtraDotAfterPtr(t *testing.T) { - payload := []byte{0x00, 0x01, 0x61, 0xc0, 0x00} - - label, _, _ := ParseLabels(1, payload) - if label != "a" { - t.Errorf("bad label parsed: %v", label) - } -} - -func TestDecodeDnsLabel_InvalidLabelLengthByte1(t *testing.T) { - payload := []byte{0x40} - - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidLabelLengthByte2(t *testing.T) { - payload := []byte{0x80} - - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_ValidTotalLength(t *testing.T) { - // A 253-character label - payload := []byte{ - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3d, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x00, - } - valid := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - - label, _, _ := ParseLabels(0, payload) - if label != valid { - t.Errorf("bad name parsed: %v", label) - } -} - -func TestDecodeDnsLabel_InvalidTotalLength_WithoutPtr(t *testing.T) { - // A 254-character label (including separator dots) - payload := []byte{ - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3e, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x00, - } - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelTooLong) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidTotalLength_WithPtr(t *testing.T) { - // A 254-character label (including separator dots), containing a pointer - payload := []byte{ - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x3c, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, - 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x00, 0x01, - 0x61, 0xc0, 0x00, - } - _, _, err := ParseLabels(255, payload) - if !errors.Is(err, ErrDecodeDNSLabelTooLong) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidPtr_SimpleLoop(t *testing.T) { - payload := []byte{0x01, 0x61, 0xc0, 0x00} - - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidPtr_Forwards(t *testing.T) { - payload := []byte{0xc0, 0x02, 0x00} - - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidPtr_BackwardsInsideCurrentLabel1(t *testing.T) { - payload := []byte{0x01, 0x02, 0xc0, 0x01, 0x00} - - _, _, err := ParseLabels(0, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidPtr_BackwardsOverlappingLoop(t *testing.T) { - payload := []byte{0x01, 0x61, 0x01, 0x61, 0xc0, 0x00} - - _, _, err := ParseLabels(2, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidPtr_BackwardsOverlappingTerminating(t *testing.T) { - payload := []byte{0x01, 0x01, 0x00, 0xc0, 0x00} - - _, _, err := ParseLabels(1, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_InvalidPtr_BackwardsOverlappingPtr(t *testing.T) { - // The second pointer byte overlaps the beginning of the label that starts at payload[3] - payload := []byte{0x00, 0x00, 0xc0, 0x01, 0x00, 0xc0, 0x02} - - _, _, err := ParseLabels(3, payload) - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodeDnsLabel_EndOffset_WithoutPtr(t *testing.T) { - payload := []byte{0x02, 0x61, 0x61, 0x00} - - _, offset, _ := ParseLabels(0, payload) - if offset != 4 { - t.Errorf("invalid end offset: %v", offset) - } -} - -func TestDecodeDnsLabel_EndOffset_WithPtr(t *testing.T) { - payload := []byte{0x01, 0x61, 0x00, 0x02, 0x61, 0x61, 0xc0, 0x00} - - _, offset, _ := ParseLabels(3, payload) - if offset != 8 { - t.Errorf("invalid end offset: %v", offset) - } -} - -func TestDecodePayload_QueryHappy(t *testing.T) { - payload := []byte{ - // header - 0x9e, 0x84, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - // name - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Additional records: EDNS OPT with no data, DO = 0, Z=0 - 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, - 0x80, 0x00, 0x00, 0x00, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { - t.Errorf("Unexpected error while decoding payload: %v", err) - } - if dm.DNS.MalformedPacket != false { - t.Errorf("did not expect packet to be malformed") - } - - if dm.DNS.ID != 0x9e84 || - dm.DNS.Opcode != 0 || - dm.DNS.Rcode != RcodeToString(0) || - dm.DNS.Flags.QR || - dm.DNS.Flags.TC || - dm.DNS.Flags.AA || - !dm.DNS.Flags.AD || - dm.DNS.Flags.RA { - t.Error("Invalid DNS header data in message") - } - - if dm.DNS.Qname != "sensorfleet.com" { - t.Errorf("Unexpected query name: %s", dm.DNS.Qname) - } - if dm.DNS.Qtype != "A" { - t.Errorf("Unexpected query type: %s", dm.DNS.Qtype) - } - - if dm.EDNS.Do != 1 || - dm.EDNS.UDPSize != 4096 || - dm.EDNS.Z != 0 || - dm.EDNS.Version != 0 || - len(dm.EDNS.Options) != 0 { - t.Errorf("Unexpected EDNS data") - } - - if len(dm.DNS.DNSRRs.Answers) != 0 || - len(dm.DNS.DNSRRs.Nameservers) != 0 || - len(dm.DNS.DNSRRs.Records) != 0 { - t.Errorf("Unexpected sections parsed") - } - -} -func TestDecodePayload_QueryInvalid(t *testing.T) { - payload := []byte{ - // header - 0x9e, 0x84, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - // name - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x83, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Additional records: EDNS OPT with no data, DO = 1, Z=0 - 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, - 0x80, 0x00, 0x00, 0x00, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { - t.Errorf("Expected error when parsing payload") - } - if dm.DNS.MalformedPacket != true { - t.Errorf("expected packet to be marked as malformed") - } - - // returned error should wrap the original error - if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { - t.Errorf("bad error returned: %v", err) - } -} - -func TestDecodePayload_AnswerHappy(t *testing.T) { - payload := []byte{ - 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x01, - // Query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Answer 1 - 0xc0, 0x0c, // pointer to name - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.1 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, - // Answer 2 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.2 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, - // Answer 3 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.3 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, - // Answer 4 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.4 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, - // Additianl records, EDNS Option, 0 bytes DO=0, Z = 0 - 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { - t.Errorf("Unexpected error while decoding payload: %v", err) - } - if dm.DNS.MalformedPacket != false { - t.Errorf("did not expect packet to be malformed") - } - - if dm.DNS.ID != 0x9e84 || - dm.DNS.Opcode != 0 || - dm.DNS.Rcode != RcodeToString(0) || - !dm.DNS.Flags.QR || - dm.DNS.Flags.TC || - dm.DNS.Flags.AA || - dm.DNS.Flags.AD || - !dm.DNS.Flags.RA { - t.Error("Invalid DNS header data in message") - } - - if dm.DNS.Qname != "sensorfleet.com" { - t.Errorf("Unexpected query name: %s", dm.DNS.Qname) - } - if dm.DNS.Qtype != "A" { - t.Errorf("Unexpected query type: %s", dm.DNS.Qtype) - } - - if len(dm.DNS.DNSRRs.Answers) != 4 { - t.Errorf("expected 4 answers, got %d", len(dm.DNS.DNSRRs.Answers)) - } - - for i, ans := range dm.DNS.DNSRRs.Answers { - expected := DNSAnswer{ - Name: dm.DNS.Qname, - Rdatatype: RdatatypeToString(0x0001), - Class: "IN", // 0x0001, - TTL: 300, - Rdata: fmt.Sprintf("10.10.1.%d", i+1), - } - if expected != ans { - t.Errorf("unexpected answer (%d). expected %v, got %v", i, expected, ans) - } - } - - if dm.EDNS.Do != 0 || - dm.EDNS.UDPSize != 1232 || - dm.EDNS.Z != 0 || - dm.EDNS.Version != 0 || - len(dm.EDNS.Options) != 0 { - t.Errorf("Unexpected EDNS data") - } - - if len(dm.DNS.DNSRRs.Nameservers) != 0 || - len(dm.DNS.DNSRRs.Records) != 0 { - t.Errorf("Unexpected sections parsed") - } - -} - -func TestDecodePayload_AnswerMultipleQueries(t *testing.T) { - payload := []byte{ - 0x9e, 0x84, 0x81, 0x80, 0x00, 0x02, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x01, - // Query section - // query 1 - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // query 2 - 0x0a, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - - // Answer 1 - 0xc0, 0x0c, // pointer to name - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.1 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, - // Answer 2 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.2 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, - // Answer 3 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.3 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, - // Answer 4 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.4 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, - // Additianl records, EDNS Option, 0 bytes DO=0, Z = 0 - 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { - t.Errorf("Unexpected error while decoding payload: %v", err) - } - if dm.DNS.MalformedPacket != false { - t.Errorf("did not expect packet to be malformed") - } - - if dm.DNS.ID != 0x9e84 || - dm.DNS.Opcode != 0 || - dm.DNS.Rcode != RcodeToString(0) || - !dm.DNS.Flags.QR || - dm.DNS.Flags.TC || - dm.DNS.Flags.AA || - dm.DNS.Flags.AD || - !dm.DNS.Flags.RA { - t.Error("Invalid DNS header data in message") - } - - if dm.DNS.Qname != "ensorfleet.com" { - t.Errorf("Unexpected query name: %s", dm.DNS.Qname) - } - if dm.DNS.Qtype != "A" { - t.Errorf("Unexpected query type: %s", dm.DNS.Qtype) - } - - if len(dm.DNS.DNSRRs.Answers) != 4 { - t.Errorf("expected 4 answers, got %d", len(dm.DNS.DNSRRs.Answers)) - } - - for i, ans := range dm.DNS.DNSRRs.Answers { - expected := DNSAnswer{ - Name: "s" + dm.DNS.Qname, // answers have qname from 1st query data, 2nd data is missing 's' - Rdatatype: RdatatypeToString(0x0001), - Class: "IN", // 0x0001, - TTL: 300, - Rdata: fmt.Sprintf("10.10.1.%d", i+1), - } - if expected != ans { - t.Errorf("unexpected answer (%d). expected %v, got %v", i, expected, ans) - } - } - - if dm.EDNS.Do != 0 || - dm.EDNS.UDPSize != 1232 || - dm.EDNS.Z != 0 || - dm.EDNS.Version != 0 || - len(dm.EDNS.Options) != 0 { - t.Errorf("Unexpected EDNS data") - } - - if len(dm.DNS.DNSRRs.Nameservers) != 0 || - len(dm.DNS.DNSRRs.Records) != 0 { - t.Errorf("Unexpected sections parsed") - } - -} - -func TestDecodePayload_AnswerInvalid(t *testing.T) { - payload := []byte{ - 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x01, - // Query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Answer 1 - 0xc0, 0x0c, // pointer to name - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.1 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, - // Answer 2 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.2 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, - // Answer 3 - 0xc0, 0xff, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.3 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, - // Answer 4 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.4 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, - // Additianl records, EDNS Option, 0 bytes DO=0, Z = 0 - 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { - t.Error("expected decoding to fail") - } - // returned error should wrap the original error - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } - if dm.DNS.MalformedPacket != true { - t.Errorf("expected packet to be malformed") - } -} - -func TestDecodePayload_AnswerInvalidQuery(t *testing.T) { - payload := []byte{ - 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x01, - // Query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x83, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Answer 1 - 0xc0, 0x0c, // pointer to name - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.1 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, - // Answer 2 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.2 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, - // Answer 3 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.3 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, - // Answer 4 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.4 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, - // Additianl records, EDNS Option, 0 bytes DO=0, Z = 0 - 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { - t.Error("expected decoding to fail") - } - // returned error should wrap the original error - if !errors.Is(err, ErrDecodeDNSLabelInvalidData) { - t.Errorf("bad error returned: %v", err) - } - if dm.DNS.MalformedPacket != true { - t.Errorf("expected packet to be malformed") - } - - // after error has been detected in the query part, we should not parse - // anything from answers - if len(dm.DNS.DNSRRs.Answers) != 0 { - t.Errorf("did not expect answers to be parsed, but there were %d parsed", len(dm.DNS.DNSRRs.Answers)) - } -} - -func TestDecodePayload_AnswerInvalidEdns(t *testing.T) { - payload := []byte{ - 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x01, - // Query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Answer 1 - 0xc0, 0x0c, // pointer to name - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.1 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, - // Answer 2 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.2 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, - // Answer 3 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.3 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, - // Answer 4 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.4 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, - // Additianl records, Invalid EDNS Option - 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x01, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { - t.Error("expected decoding to fail") - } - // returned error should wrap the original error - if !errors.Is(err, ErrDecodeEdnsOptionTooShort) { - t.Errorf("bad error returned: %v", err) - } - if dm.DNS.MalformedPacket != true { - t.Errorf("expected packet to be malformed") - } -} - -func TestDecodePayload_AnswerInvaliAdditional(t *testing.T) { - payload := []byte{ - 0x9e, 0x84, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x01, - // Query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Answer 1 - 0xc0, 0x0c, // pointer to name - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.1 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x01, - // Answer 2 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.2 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x02, - // Answer 3 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.3 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x03, - // Answer 4 - 0xc0, 0x0c, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x01, 0x2c, - // 10.10.1.4 - 0x00, 0x04, 0x0a, 0x0a, 0x01, 0x04, - // Additianl records, Invalid RDLENGTH - 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { - t.Error("expected decoding to fail") - } - // returned error should wrap the original error - if !errors.Is(err, ErrDecodeDNSAnswerRdataTooShort) { - t.Errorf("bad error returned: %v", err) - } - if dm.DNS.MalformedPacket != true { - t.Errorf("expected packet to be malformed") - } -} - -func TestDecodePayload_AnswerError(t *testing.T) { - payload := []byte{ - // header - 0xa8, 0x1a, 0x81, 0x83, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x01, - // query - 0x03, 0x66, 0x6f, 0x6f, - 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, - 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Authority section - // name - 0xc0, 0x10, - // type SOA, class IN - 0x00, 0x06, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x00, 0x3c, - // RDLENGTH - 0x00, 0x26, - // RDATA - // MNAME - 0x03, 0x6e, 0x73, 0x31, - 0xc0, 0x10, - // RNAME - 0x09, 0x64, 0x6e, 0x73, 0x2d, 0x61, - 0x64, 0x6d, 0x69, 0x6e, 0xc0, 0x10, - // serial - 0x19, 0xa1, 0x4a, 0xb4, - // refresh - 0x00, 0x00, 0x03, 0x84, - // retry - 0x00, 0x00, 0x03, 0x84, - // expire - 0x00, 0x00, 0x07, 0x08, - // minimum - 0x00, 0x00, 0x00, 0x3c, - // Additianl records, EDNS Option, 0 bytes DO=1, Z = 0 - 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, - 0x00, 0x80, 0x00, 0x00, 0x00, - } - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { - t.Errorf("Unexpected error while decoding payload: %v", err) - } - if dm.DNS.MalformedPacket != false { - t.Errorf("did not expect packet to be malformed") - } - - if dm.DNS.ID != 0xa81a || - dm.DNS.Opcode != 0 || - dm.DNS.Rcode != RcodeToString(3) || - !dm.DNS.Flags.QR || - dm.DNS.Flags.TC || - dm.DNS.Flags.AA || - dm.DNS.Flags.AD || - !dm.DNS.Flags.RA { - t.Error("Invalid DNS header data in message") - } - - if dm.DNS.Qname != "foo.google.com" { - t.Errorf("Unexpected query name: %s", dm.DNS.Qname) - } - if dm.DNS.Qtype != "A" { - t.Errorf("Unexpected query type: %s", dm.DNS.Qtype) - } - - if len(dm.DNS.DNSRRs.Answers) != 0 { - t.Errorf("did not expect any answers, got %d", len(dm.DNS.DNSRRs.Answers)) - } - - if len(dm.DNS.DNSRRs.Nameservers) != 1 { - t.Errorf("expected 1 authority RR, got %d", len(dm.DNS.DNSRRs.Nameservers)) - } - expected := DNSAnswer{ - Name: "google.com", - Rdatatype: RdatatypeToString(0x0006), - Class: "IN", // 0x0001, - TTL: 60, - Rdata: "ns1.google.com dns-admin.google.com 430000820 900 900 1800 60", - } - - if dm.DNS.DNSRRs.Nameservers[0] != expected { - t.Errorf("unexpected SOA record parsed, expected %v, git %v", expected, dm.DNS.DNSRRs.Nameservers[0]) - } - - if dm.EDNS.Do != 1 || - dm.EDNS.UDPSize != 1232 || - dm.EDNS.Z != 0 || - dm.EDNS.Version != 0 || - len(dm.EDNS.Options) != 0 { - t.Errorf("Unexpected EDNS data") - } - -} - -func TestDecodePayload_AnswerError_Invalid(t *testing.T) { - payload := []byte{ - // header - 0xa8, 0x1a, 0x81, 0x83, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x01, - // query - 0x03, 0x66, 0x6f, 0x6f, - 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, - 0x63, 0x6f, 0x6d, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // Authority section - // name - 0xc0, 0x10, - // type SOA, class IN - 0x00, 0x06, 0x00, 0x01, - // TTL - 0x00, 0x00, 0x00, 0x3c, - // RDLENGTH - 0x00, 0x26, - // RDATA - // MNAME, invalid offset in pointer - 0x03, 0x6e, 0x73, 0x31, - 0xc0, 0xff, - // RNAME - 0x09, 0x64, 0x6e, 0x73, 0x2d, 0x61, - 0x64, 0x6d, 0x69, 0x6e, 0xc0, 0x10, - // serial - 0x19, 0xa1, 0x4a, 0xb4, - // refresh - 0x00, 0x00, 0x03, 0x84, - // retry - 0x00, 0x00, 0x03, 0x84, - // expire - 0x00, 0x00, 0x07, 0x08, - // minimum - 0x00, 0x00, 0x00, 0x3c, - // Additianl records, EDNS Option, 0 bytes DO=1, Z = 0 - 0x00, 0x00, 0x29, 0x04, 0xd0, 0x00, - 0x00, 0x80, 0x00, 0x00, 0x00, - } - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err == nil { - t.Error("expected decoding to fail") - } - // returned error should wrap the original error - if !errors.Is(err, ErrDecodeDNSLabelInvalidPointer) { - t.Errorf("bad error returned: %v", err) - } - if dm.DNS.MalformedPacket != true { - t.Errorf("expected packet to be malformed") - } - -} - -func TestDecodePayload_AdditionalRRAndEDNS(t *testing.T) { - // payload containing both addition RR and EDNS, ensure we are - // able to parse all of them - payload := []byte{ - 0x7b, 0x97, 0x84, 0x0, 0x0, 0x1, 0x0, 0x2, - 0x0, 0x2, 0x0, 0x5, 0xf, 0x6f, 0x63, 0x63, - 0x2d, 0x30, 0x2d, 0x31, 0x35, 0x30, 0x30, - 0x2d, 0x31, 0x35, 0x30, 0x31, 0x1, 0x31, - 0x6, 0x6e, 0x66, 0x6c, 0x78, 0x73, 0x6f, - 0x3, 0x6e, 0x65, 0x74, 0x0, 0x0, 0x1, 0x0, - 0x1, 0xc0, 0xc, 0x0, 0x1, 0x0, 0x1, 0x0, - 0x0, 0x0, 0x1e, 0x0, 0x4, 0x2d, 0x39, 0x24, - 0x97, 0xc0, 0xc, 0x0, 0x1, 0x0, 0x1, 0x0, - 0x0, 0x0, 0x1e, 0x0, 0x4, 0x2d, 0x39, 0x24, - 0x94, 0xc0, 0x1e, 0x0, 0x2, 0x0, 0x1, 0x0, - 0x1, 0x51, 0x80, 0x0, 0x7, 0x1, 0x65, 0x2, - 0x6e, 0x73, 0xc0, 0x1e, 0xc0, 0x1e, 0x0, - 0x2, 0x0, 0x1, 0x0, 0x1, 0x51, 0x80, 0x0, - 0x4, 0x1, 0x66, 0xc0, 0x5c, 0x0, 0x0, 0x29, - 0x4, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, - 0x5a, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x51, 0x80, - 0x0, 0x4, 0x2d, 0x39, 0x8, 0x1, 0xc0, 0x5a, - 0x0, 0x1c, 0x0, 0x1, 0x0, 0x1, 0x51, 0x80, 0x0, - 0x10, 0x2a, 0x0, 0x86, 0xc0, 0x20, 0x8, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xc0, 0x6d, - 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x51, 0x80, 0x0, 0x4, - 0x2d, 0x39, 0x9, 0x1, 0xc0, 0x6d, 0x0, 0x1c, 0x0, 0x1, - 0x0, 0x1, 0x51, 0x80, 0x0, 0x10, 0x2a, 0x0, 0x86, 0xc0, - 0x20, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1} - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("error when deocoding header: %v", err) - } - - if err := DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { - t.Errorf("unexpected error while decoding payload: %v", err) - } - - if len(dm.DNS.DNSRRs.Answers) != 2 || len(dm.DNS.DNSRRs.Nameservers) != 2 || - len(dm.DNS.DNSRRs.Records) != 4 || dm.EDNS.UDPSize != 1200 || - len(dm.EDNS.Options) != 0 { - t.Errorf("unexpected result while parsing payload: %#v", dm.DNS) - } - -} - -func TestDecodePayload_Truncated(t *testing.T) { - payload := []byte{ - // header - 0x77, 0xa0, 0x83, 0x80, 0x00, 0x01, 0x00, 0x23, - 0x00, 0x00, 0x00, 0x00, - // query - 0x02, 0x41, 0x64, 0x0d, - 0x6e, 0x6e, 0x6e, 0x6e, - 0x6e, 0x6e, 0x6e, 0x6e, - 0x6e, 0x6e, 0x6e, 0x6e, - 0x6e, 0x02, 0x46, 0x52, - 0x00, 0x00, 0x01, 0x00, 0x01, - // answer 1 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x01, - // answer 2 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x02, - // answer 3 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x03, - // answer 4 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x04, - // answer 5 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x05, - // answer 6 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x06, - // answer 7 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x07, - // answer 8 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x08, - // answer 9 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x09, - // answer 10 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x0a, - // answer 11 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x0b, - // answer 12 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x0c, - // answer 13 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x0d, - // answer 14 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x0e, - // answer 15 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x0f, - // answer 16 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x10, - // answer 17 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x11, - // answer 18 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x12, - // answer 19 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x13, - // answer 20 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x14, - // answer 21 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x15, - // answer 22 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x16, - // answer 23 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x17, - // answer 24 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x18, - // answer 25 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x19, - // answer 26 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x1a, - // answer 27 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x1b, - // answer 28 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x1c, - // answer 29 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, 0x04, 0x0a, 0x01, - 0x01, 0x1d, - // answer 30 - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6b, 0x00, - } - - dm := DNSMessage{} - dm.DNS.Payload = payload - dm.DNS.Length = len(payload) - - header, err := DecodeDNS(payload) - if err != nil { - t.Errorf("unexpected error when decoding header: %v", err) - } - - if err = DecodePayload(&dm, &header, pkgconfig.GetDefaultConfig()); err != nil { - t.Error("expected no error on decode") - } - - if dm.DNS.Flags.TC == false { - t.Error("truncated answer expected") - } - - if dm.DNS.MalformedPacket != true { - t.Errorf("expected packet to be malformed") - } - -} diff --git a/dnsutils/message.go b/dnsutils/dnsmessage.go similarity index 74% rename from dnsutils/message.go rename to dnsutils/dnsmessage.go index 648293f4..974f7aa5 100644 --- a/dnsutils/message.go +++ b/dnsutils/dnsmessage.go @@ -16,13 +16,11 @@ import ( "strings" "time" - "github.com/dmachard/go-dnscollector/pkgconfig" "github.com/dmachard/go-dnstap-protobuf" "github.com/dmachard/go-netutils" "github.com/flosch/pongo2/v6" "github.com/google/gopacket" "github.com/google/gopacket/layers" - "github.com/miekg/dns" "google.golang.org/protobuf/proto" ) @@ -43,37 +41,6 @@ var ( ATagsDirectives = regexp.MustCompile(`^atags*`) ) -func GetFakeDNS() ([]byte, error) { - dnsmsg := new(dns.Msg) - dnsmsg.SetQuestion("dns.collector.", dns.TypeA) - return dnsmsg.Pack() -} - -func GetIPPort(dm *DNSMessage) (string, int, string, int) { - srcIP, srcPort := "0.0.0.0", 53 - dstIP, dstPort := "0.0.0.0", 53 - if dm.NetworkInfo.Family == "INET6" { - srcIP, dstIP = "::", "::" - } - - if dm.NetworkInfo.QueryIP != "-" { - srcIP = dm.NetworkInfo.QueryIP - srcPort, _ = strconv.Atoi(dm.NetworkInfo.QueryPort) - } - if dm.NetworkInfo.ResponseIP != "-" { - dstIP = dm.NetworkInfo.ResponseIP - dstPort, _ = strconv.Atoi(dm.NetworkInfo.ResponsePort) - } - - // reverse destination and source - if dm.DNS.Type == DNSReply { - srcIPTmp, srcPortTmp := srcIP, srcPort - srcIP, srcPort = dstIP, dstPort - dstIP, dstPort = srcIPTmp, srcPortTmp - } - return srcIP, srcPort, dstIP, dstPort -} - type DNSAnswer struct { Name string `json:"name"` Rdatatype string `json:"rdatatype"` @@ -283,19 +250,17 @@ func (dm *DNSMessage) Init() { Identity: "-", Version: "-", TimestampRFC3339: "-", - // LatencySec: "-", - Extra: "-", - PolicyRule: "-", - PolicyType: "-", - PolicyMatch: "-", - PolicyAction: "-", - PolicyValue: "-", - PeerName: "-", - QueryZone: "-", + Extra: "-", + PolicyRule: "-", + PolicyType: "-", + PolicyMatch: "-", + PolicyAction: "-", + PolicyValue: "-", + PeerName: "-", + QueryZone: "-", } dm.DNS = DNS{ - ID: 0, Type: "-", MalformedPacket: false, Rcode: "-", @@ -306,12 +271,7 @@ func (dm *DNSMessage) Init() { } dm.EDNS = DNSExtended{ - UDPSize: 0, - ExtendedRcode: 0, - Version: 0, - Do: 0, - Z: 0, - Options: []DNSOption{}, + Options: []DNSOption{}, } } @@ -672,25 +632,25 @@ func (dm *DNSMessage) ToTextLine(format []string, fieldDelimiter string, fieldBo if len(qname) == 0 { s.WriteString(".") } else { - quoteStringAndWrite(&s, qname, fieldDelimiter, fieldBoundary) + QuoteStringAndWrite(&s, qname, fieldDelimiter, fieldBoundary) } case directive == "identity": if len(dm.DNSTap.Identity) == 0 { s.WriteString("-") } else { - quoteStringAndWrite(&s, dm.DNSTap.Identity, fieldDelimiter, fieldBoundary) + QuoteStringAndWrite(&s, dm.DNSTap.Identity, fieldDelimiter, fieldBoundary) } case directive == "peer-name": if len(dm.DNSTap.PeerName) == 0 { s.WriteString("-") } else { - quoteStringAndWrite(&s, dm.DNSTap.PeerName, fieldDelimiter, fieldBoundary) + QuoteStringAndWrite(&s, dm.DNSTap.PeerName, fieldDelimiter, fieldBoundary) } case directive == "version": if len(dm.DNSTap.Version) == 0 { s.WriteString("-") } else { - quoteStringAndWrite(&s, dm.DNSTap.Version, fieldDelimiter, fieldBoundary) + QuoteStringAndWrite(&s, dm.DNSTap.Version, fieldDelimiter, fieldBoundary) } case directive == "extra": s.WriteString(dm.DNSTap.Extra) @@ -1367,12 +1327,12 @@ func (dm *DNSMessage) ApplyRelabeling(dnsFields map[string]interface{}) error { if value, exists := dnsFields[replacement]; exists { switch v := value.(type) { case []string: - dnsFields[replacement] = append(v, convertToString(dnsFields[key])) + dnsFields[replacement] = append(v, ConvertToString(dnsFields[key])) default: - dnsFields[replacement] = []string{convertToString(v), convertToString(dnsFields[key])} + dnsFields[replacement] = []string{ConvertToString(v), ConvertToString(dnsFields[key])} } } else { - dnsFields[replacement] = convertToString(dnsFields[key]) + dnsFields[replacement] = ConvertToString(dnsFields[key]) } } @@ -1408,31 +1368,51 @@ func (dm *DNSMessage) Matching(matching map[string]interface{}) (error, bool) { switch expectedValue.Kind() { // integer case reflect.Int: - if match, _ := matchUserInteger(realValue, expectedValue); !match { + match, err := matchUserInteger(realValue, expectedValue) + if err != nil { + return err, false + } + if !match { return nil, false } // string case reflect.String: - if match, _ := matchUserPattern(realValue, expectedValue); !match { + match, err := matchUserPattern(realValue, expectedValue) + if err != nil { + return err, false + } + if !match { return nil, false } // bool case reflect.Bool: - if match, _ := matchUserBoolean(realValue, expectedValue); !match { + match, err := matchUserBoolean(realValue, expectedValue) + if err != nil { + return err, false + } + if !match { return nil, false } // map case reflect.Map: - if match, _ := matchUserMap(realValue, expectedValue); !match { + match, err := matchUserMap(realValue, expectedValue) + if err != nil { + return err, false + } + if !match { return nil, false } // list/slice case reflect.Slice: - if match, _ := matchUserSlice(realValue, expectedValue); !match { + match, err := matchUserSlice(realValue, expectedValue) + if err != nil { + return err, false + } + if !match { return nil, false } @@ -1445,501 +1425,3 @@ func (dm *DNSMessage) Matching(matching map[string]interface{}) (error, bool) { return nil, isMatch } - -// map can be provided by user in the config -// dns.qname: -// match-source: "file://./tests/testsdata/filtering_keep_domains_regex.txt" -// source-kind: "regexp_list" -func matchUserMap(realValue, expectedValue reflect.Value) (bool, error) { - for _, opKey := range expectedValue.MapKeys() { - opValue := expectedValue.MapIndex(opKey) - opName := opKey.Interface().(string) - - switch opName { - // Integer great than ? - case MatchingOpGreaterThan: - isFloat, isInt := false, false - if _, ok := opValue.Interface().(float64); ok { - isFloat = true - } - if _, ok := opValue.Interface().(int); !ok { - isInt = true - } - - if !isFloat && !isInt { - return false, fmt.Errorf("integer is expected for greater-than operator") - } - - // If realValue is a slice - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len(); i++ { - elemValue := realValue.Index(i) - - // Check if the element is a int - if _, ok := elemValue.Interface().(int); !ok { - continue - } - - // Check for match - if elemValue.Interface().(int) > opValue.Interface().(int) { - return true, nil - } - } - return false, nil - } - - if realValue.Kind() == reflect.Float64 { - if realValue.Interface().(float64) > opValue.Interface().(float64) { - return true, nil - } - } - - if realValue.Kind() == reflect.Int { - if realValue.Interface().(int) > opValue.Interface().(int) { - return true, nil - } - } - - return false, nil - - // Integer lower than ? - case MatchingOpLowerThan: - if _, ok := opValue.Interface().(int); !ok { - return false, fmt.Errorf("integer is expected for lower-than operator") - } - - // If realValue is a slice - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len(); i++ { - elemValue := realValue.Index(i) - - // Check if the element is a int - if _, ok := elemValue.Interface().(int); !ok { - continue - } - - // Check for match - if elemValue.Interface().(int) < opValue.Interface().(int) { - return true, nil - } - } - return false, nil - } - - if realValue.Kind() != reflect.Int { - return false, nil - } - if realValue.Interface().(int) < opValue.Interface().(int) { - return true, nil - } - return false, nil - - // Ignore these operators - case MatchingOpSource, MatchingOpSourceKind: - continue - - // List of pattern - case MatchingKindRegexp: - patternList := opValue.Interface().([]*regexp.Regexp) - - // If realValue is a slice - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len(); i++ { - elemValue := realValue.Index(i) - - // Check if the element is a string - if _, ok := elemValue.Interface().(string); !ok { - continue - } - - // Check for a match with the regex pattern - for _, pattern := range patternList { - if pattern.MatchString(elemValue.Interface().(string)) { - return true, nil - } - } - } - // No match found in the slice - return false, nil - } - - if realValue.Kind() != reflect.String { - return false, nil - } - for _, pattern := range patternList { - if pattern.MatchString(realValue.Interface().(string)) { - return true, nil - } - } - // No match found in pattern list - return false, nil - - // List of string - case MatchingKindString: - stringList := opValue.Interface().([]string) - - // If realValue is a slice - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len(); i++ { - elemValue := realValue.Index(i) - - // Check if the element is a string - if _, ok := elemValue.Interface().(string); !ok { - continue - } - - // Check for a match with the text - for _, textItem := range stringList { - if textItem == realValue.Interface().(string) { - return true, nil - } - } - } - // No match found in the slice - return false, nil - } - - if realValue.Kind() != reflect.String { - return false, nil - } - for _, textItem := range stringList { - if textItem == realValue.Interface().(string) { - return true, nil - } - } - - // No match found in string list - return false, nil - - default: - return false, fmt.Errorf("invalid operator '%s', ignore it", opKey.Interface().(string)) - } - } - return true, nil -} - -// list can be provided by user in the config -// dns.qname: -// - ".*\\.github\\.com$" -// - "^www\\.google\\.com$" -func matchUserSlice(realValue, expectedValue reflect.Value) (bool, error) { - match := false - for i := 0; i < expectedValue.Len() && !match; i++ { - reflectedSub := reflect.ValueOf(expectedValue.Index(i).Interface()) - - switch reflectedSub.Kind() { - case reflect.Int: - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len(); i++ { - elemValue := realValue.Index(i) - if _, ok := elemValue.Interface().(int); !ok { - continue - } - if reflectedSub.Interface().(int) == elemValue.Interface().(int) { - return true, nil - } - } - } - - if realValue.Kind() != reflect.Int || realValue.Interface().(int) != reflectedSub.Interface().(int) { - continue - } - match = true - case reflect.String: - pattern := regexp.MustCompile(reflectedSub.Interface().(string)) - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len() && !match; i++ { - elemValue := realValue.Index(i) - if _, ok := elemValue.Interface().(string); !ok { - continue - } - // Check for a match with the regex pattern - if pattern.MatchString(elemValue.Interface().(string)) { - match = true - } - } - } - - if realValue.Kind() != reflect.String { - continue - } - - if pattern.MatchString(realValue.Interface().(string)) { - match = true - } - } - } - return match, nil -} - -// boolean can be provided by user in the config -// dns.flags.qr: true -func matchUserBoolean(realValue, expectedValue reflect.Value) (bool, error) { - // If realValue is a slice - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len(); i++ { - elemValue := realValue.Index(i) - - // Check if the element is a int - if _, ok := elemValue.Interface().(bool); !ok { - continue - } - - // Check for match - if expectedValue.Interface().(bool) == elemValue.Interface().(bool) { - return true, nil - } - } - } - - if realValue.Kind() != reflect.Bool { - return false, nil - } - - if expectedValue.Interface().(bool) != realValue.Interface().(bool) { - return false, nil - } - return true, nil -} - -// integer can be provided by user in the config -// dns.opcode: 0 -func matchUserInteger(realValue, expectedValue reflect.Value) (bool, error) { - // If realValue is a slice - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len(); i++ { - elemValue := realValue.Index(i) - - // Check if the element is a int - if _, ok := elemValue.Interface().(int); !ok { - continue - } - - // Check for match - if expectedValue.Interface().(int) == elemValue.Interface().(int) { - return true, nil - } - } - } - - if realValue.Kind() != reflect.Int { - return false, nil - } - if expectedValue.Interface().(int) != realValue.Interface().(int) { - return false, nil - } - - return true, nil -} - -// regexp can be provided by user in the config -// dns.qname: "^.*\\.github\\.com$" -func matchUserPattern(realValue, expectedValue reflect.Value) (bool, error) { - pattern := regexp.MustCompile(expectedValue.Interface().(string)) - - // If realValue is a slice - if realValue.Kind() == reflect.Slice { - for i := 0; i < realValue.Len(); i++ { - elemValue := realValue.Index(i) - - // Check if the element is a string - if _, ok := elemValue.Interface().(string); !ok { - continue - } - - // Check for a match with the regex pattern - if pattern.MatchString(elemValue.Interface().(string)) { - return true, nil - } - } - // No match found in the slice - return false, nil - } - - // If realValue is not a string - if realValue.Kind() != reflect.String { - return false, nil - } - - // Check for a match with the regex pattern - if !pattern.MatchString(realValue.String()) { - return false, nil - } - - // Match found for a single value - return true, nil -} - -func getFieldByJSONTag(value reflect.Value, nestedKeys string) (reflect.Value, bool) { - listKeys := strings.SplitN(nestedKeys, ".", 2) - - for j, jsonKey := range listKeys { - // Iterate over the fields of the structure - for i := 0; i < value.NumField(); i++ { - field := value.Type().Field(i) - - // Get JSON tag - tag := field.Tag.Get("json") - tagClean := strings.TrimSuffix(tag, ",omitempty") - - // Check if the JSON tag matches - if tagClean == jsonKey { - // ptr - switch field.Type.Kind() { - // integer - case reflect.Ptr: - if fieldValue, found := getFieldByJSONTag(value.Field(i).Elem(), listKeys[j+1]); found { - return fieldValue, true - } - - // struct - case reflect.Struct: - if fieldValue, found := getFieldByJSONTag(value.Field(i), listKeys[j+1]); found { - return fieldValue, true - } - - // slice if a list - case reflect.Slice: - if fieldValue, leftKey, found := getSliceElement(value.Field(i), listKeys[j+1]); found { - switch field.Type.Kind() { - case reflect.Struct: - if fieldValue, found := getFieldByJSONTag(fieldValue, leftKey); found { - return fieldValue, true - } - case reflect.Slice: - var result []interface{} - for i := 0; i < fieldValue.Len(); i++ { - - if fieldValue.Index(i).Kind() == reflect.String || fieldValue.Index(i).Kind() == reflect.Int { - result = append(result, fieldValue.Index(i).Interface()) - } else { - if sliceValue, found := getFieldByJSONTag(fieldValue.Index(i), leftKey); found { - result = append(result, sliceValue.Interface()) - } - } - } - // If the list is not empty, return the list - if len(result) > 0 { - return reflect.ValueOf(result), true - } - default: - return value.Field(i), true - } - } - - // int, string - default: - return value.Field(i), true - } - } - } - } - - return reflect.Value{}, false -} - -func getSliceElement(sliceValue reflect.Value, nestedKeys string) (reflect.Value, string, bool) { - listKeys := strings.SplitN(nestedKeys, ".", 2) - leftKeys := "" - if len(listKeys) > 1 { - leftKeys = listKeys[1] - } - sliceIndex := listKeys[0] - - if sliceIndex == "*" { - return sliceValue, leftKeys, true - } - - // Convert the slice index from string to int - index, err := strconv.Atoi(sliceIndex) - if err != nil { - // Handle the error (e.g., invalid index format) - return reflect.Value{}, leftKeys, false - } - - for i := 0; i < sliceValue.Len(); i++ { - if index == i { - return sliceValue.Index(i), leftKeys, true - } - } - // If no match is found, return an empty reflect.Value - return reflect.Value{}, leftKeys, false -} - -func GetFakeDNSMessage() DNSMessage { - dm := DNSMessage{} - dm.Init() - dm.DNSTap.Identity = "collector" - dm.DNSTap.Version = "dnscollector 1.0.0" - dm.DNSTap.Operation = "CLIENT_QUERY" - dm.DNSTap.PeerName = "localhost (127.0.0.1)" - dm.DNS.Type = DNSQuery - dm.DNS.Qname = pkgconfig.ProgQname - dm.NetworkInfo.QueryIP = "1.2.3.4" - dm.NetworkInfo.QueryPort = "1234" - dm.NetworkInfo.ResponseIP = "4.3.2.1" - dm.NetworkInfo.ResponsePort = "4321" - dm.DNS.Rcode = "NOERROR" - dm.DNS.Qtype = "A" - return dm -} - -func GetFakeDNSMessageWithPayload() DNSMessage { - // fake dns query payload - dnsmsg := new(dns.Msg) - dnsmsg.SetQuestion("dnscollector.dev.", dns.TypeAAAA) - dnsquestion, _ := dnsmsg.Pack() - - dm := GetFakeDNSMessage() - dm.NetworkInfo.Family = netutils.ProtoIPv4 - dm.NetworkInfo.Protocol = netutils.ProtoUDP - dm.DNS.Payload = dnsquestion - dm.DNS.Length = len(dnsquestion) - return dm -} - -func GetFlatDNSMessage() (ret map[string]interface{}, err error) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - ret, err = dm.Flatten() - return -} - -func GetReferenceDNSMessage() DNSMessage { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - return dm -} - -func convertToString(value interface{}) string { - switch v := value.(type) { - case int: - return strconv.Itoa(v) - case bool: - return strconv.FormatBool(v) - case float64: - return strconv.FormatFloat(v, 'f', -1, 64) - case string: - return v - default: - return fmt.Sprintf("%v", v) - } -} - -func quoteStringAndWrite(s *strings.Builder, fieldString, fieldDelimiter, fieldBoundary string) { - if len(fieldDelimiter) > 0 { - if strings.Contains(fieldString, fieldDelimiter) { - fieldEscaped := fieldString - if strings.Contains(fieldString, fieldBoundary) { - fieldEscaped = strings.ReplaceAll(fieldEscaped, fieldBoundary, "\\"+fieldBoundary) - } - s.WriteString(fmt.Sprintf(fieldBoundary+"%s"+fieldBoundary, fieldEscaped)) - } else { - s.WriteString(fieldString) - } - } else { - s.WriteString(fieldString) - } -} diff --git a/dnsutils/dnsmessage_dnstap_test.go b/dnsutils/dnsmessage_dnstap_test.go new file mode 100644 index 00000000..8dd60195 --- /dev/null +++ b/dnsutils/dnsmessage_dnstap_test.go @@ -0,0 +1,172 @@ +package dnsutils + +import ( + "testing" + + "github.com/dmachard/go-dnstap-protobuf" + "google.golang.org/protobuf/proto" +) + +// Tests for DNSTap format +func encodeToDNSTap(dm DNSMessage, t *testing.T) *ExtendedDnstap { + // encode to extended dnstap + tapMsg, err := dm.ToDNSTap(true) + if err != nil { + t.Fatalf("could not encode to extended dnstap: %v\n", err) + } + + // decode dnstap message + dt := &dnstap.Dnstap{} + err = proto.Unmarshal(tapMsg, dt) + if err != nil { + t.Fatalf("error to decode dnstap: %v", err) + } + + // decode extended part + edt := &ExtendedDnstap{} + err = proto.Unmarshal(dt.GetExtra(), edt) + if err != nil { + t.Fatalf("error to decode extended dnstap: %v", err) + } + return edt +} + +func TestDnsMessage_ToDNSTap(t *testing.T) { + dm := GetFakeDNSMessageWithPayload() + dm.DNSTap.Extra = "extra:value" + + // encode to dnstap + tapMsg, err := dm.ToDNSTap(false) + if err != nil { + t.Fatalf("could not encode to dnstap: %v\n", err) + } + + // decode dnstap message + dt := &dnstap.Dnstap{} + err = proto.Unmarshal(tapMsg, dt) + if err != nil { + t.Fatalf("error to decode dnstap: %v", err) + } + + if string(dt.GetIdentity()) != dm.DNSTap.Identity { + t.Errorf("identify field should be equal got=%s", string(dt.GetIdentity())) + } + + if string(dt.GetExtra()) != dm.DNSTap.Extra { + t.Errorf("extra field should be equal got=%s", string(dt.GetExtra())) + } +} + +func BenchmarkDnsMessage_ToDNSTap(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := dm.ToDNSTap(false) + if err != nil { + b.Fatalf("could not encode to dnstap: %v\n", err) + } + } +} + +// Tests for Extended DNSTap format +func TestDnsMessage_ToExtendedDNSTap_GetOriginalDnstapExtra(t *testing.T) { + dm := GetFakeDNSMessageWithPayload() + dm.DNSTap.Extra = "tag0:value0" + + // encode to DNSTap and decode extended + edt := encodeToDNSTap(dm, t) + + // check + if string(edt.GetOriginalDnstapExtra()) != dm.DNSTap.Extra { + t.Errorf("extra field should be equal to the original value") + } +} + +func TestDnsMessage_ToExtendedDNSTap_TransformAtags(t *testing.T) { + dm := GetFakeDNSMessageWithPayload() + dm.ATags = &TransformATags{ + Tags: []string{"tag1:value1"}, + } + + // encode to DNSTap and decode extended + edt := encodeToDNSTap(dm, t) + + // check + if edt.GetAtags().Tags[0] != "tag1:value1" { + t.Errorf("invalid value on atags") + } +} + +func TestDnsMessage_ToExtendedDNSTap_TransformNormalize(t *testing.T) { + dm := GetFakeDNSMessageWithPayload() + dm.PublicSuffix = &TransformPublicSuffix{ + QnamePublicSuffix: "com", + QnameEffectiveTLDPlusOne: "dnscollector.com", + } + + // encode to DNSTap and decode extended + edt := encodeToDNSTap(dm, t) + + // checks + if edt.GetNormalize().GetTld() != "com" { + t.Errorf("invalid value on tld") + } + + if edt.GetNormalize().GetEtldPlusOne() != "dnscollector.com" { + t.Errorf("invalid value on etld+1") + } +} + +func TestDnsMessage_ToExtendedDNSTap_TransformFiltering(t *testing.T) { + dm := GetFakeDNSMessageWithPayload() + dm.Filtering = &TransformFiltering{ + SampleRate: 20, + } + + // encode to DNSTap and decode extended + edt := encodeToDNSTap(dm, t) + + // checks + if edt.GetFiltering().GetSampleRate() != 20 { + t.Errorf("invalid value sample rate") + } +} + +func TestDnsMessage_ToExtendedDNSTap_TransformGeo(t *testing.T) { + dm := GetFakeDNSMessageWithPayload() + dm.Geo = &TransformDNSGeo{ + City: "France", + Continent: "Europe", + CountryIsoCode: "44444", + AutonomousSystemNumber: "3333", + AutonomousSystemOrg: "Test", + } + + // encode to DNSTap and decode extended + edt := encodeToDNSTap(dm, t) + + // checks + if edt.GetGeo().GetCity() != "France" { + t.Errorf("invalid value for city") + } + if edt.GetGeo().GetContinent() != "Europe" { + t.Errorf("invalid value for continent") + } +} + +func BenchmarkDnsMessage_ToExtendedDNSTap(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := dm.ToDNSTap(true) + if err != nil { + b.Fatalf("could not encode to extended dnstap: %v\n", err) + } + } +} diff --git a/dnsutils/dnsmessage_json_test.go b/dnsutils/dnsmessage_json_test.go new file mode 100644 index 00000000..d3cfbfe5 --- /dev/null +++ b/dnsutils/dnsmessage_json_test.go @@ -0,0 +1,616 @@ +package dnsutils + +import ( + "encoding/json" + "reflect" + "testing" +) + +// Tests for JSON format +func TestDnsMessage_Json_Reference(t *testing.T) { + dm := DNSMessage{} + dm.Init() + + refJSON := ` + { + "network": { + "family": "-", + "protocol": "-", + "query-ip": "-", + "query-port": "-", + "response-ip": "-", + "response-port": "-", + "ip-defragmented": false, + "tcp-reassembled": false + }, + "dns": { + "id": 0, + "length": 0, + "opcode": 0, + "rcode": "-", + "qname": "-", + "qtype": "-", + "qclass": "-", + "questions-count": 0, + "flags": { + "qr": false, + "tc": false, + "aa": false, + "ra": false, + "ad": false, + "rd": false, + "cd": false + }, + "resource-records": { + "an": [], + "ns": [], + "ar": [] + }, + "malformed-packet": false + }, + "edns": { + "udp-size": 0, + "rcode": 0, + "version": 0, + "dnssec-ok": 0, + "options": [] + }, + "dnstap": { + "operation": "-", + "identity": "-", + "version": "-", + "timestamp-rfc3339ns": "-", + "latency": 0, + "extra": "-", + "policy-type": "-", + "policy-action": "-", + "policy-match": "-", + "policy-value": "-", + "policy-rule": "-", + "peer-name": "-", + "query-zone": "-" + } + } + ` + + var dmMap map[string]interface{} + err := json.Unmarshal([]byte(dm.ToJSON()), &dmMap) + if err != nil { + t.Fatalf("could not unmarshal dm json: %s\n", err) + } + + var refMap map[string]interface{} + err = json.Unmarshal([]byte(refJSON), &refMap) + if err != nil { + t.Fatalf("could not unmarshal ref json: %s\n", err) + } + + if !reflect.DeepEqual(dmMap, refMap) { + t.Errorf("json format different from reference %v", dmMap) + } +} + +func TestDnsMessage_Json_Collectors_Reference(t *testing.T) { + testcases := []struct { + collector string + dmRef DNSMessage + jsonRef string + }{ + { + collector: "powerdns", + dmRef: DNSMessage{PowerDNS: &PowerDNS{ + OriginalRequestSubnet: "subnet", + AppliedPolicy: "basicrpz", + AppliedPolicyHit: "hit", + AppliedPolicyKind: "kind", + AppliedPolicyTrigger: "trigger", + AppliedPolicyType: "type", + Tags: []string{"tag1"}, + Metadata: map[string]string{"stream_id": "collector"}, + HTTPVersion: "http3", + }}, + + jsonRef: `{ + "powerdns": { + "original-request-subnet": "subnet", + "applied-policy": "basicrpz", + "applied-policy-hit": "hit", + "applied-policy-kind": "kind", + "applied-policy-trigger": "trigger", + "applied-policy-type": "type", + "tags": ["tag1"], + "metadata": { + "stream_id": "collector" + }, + "http-version": "http3" + } + }`, + }, + } + for _, tc := range testcases { + t.Run(tc.collector, func(t *testing.T) { + + tc.dmRef.Init() + + var dmMap map[string]interface{} + err := json.Unmarshal([]byte(tc.dmRef.ToJSON()), &dmMap) + if err != nil { + t.Fatalf("could not unmarshal dm json: %s\n", err) + } + + var refMap map[string]interface{} + err = json.Unmarshal([]byte(tc.jsonRef), &refMap) + if err != nil { + t.Fatalf("could not unmarshal ref json: %s\n", err) + } + + if !reflect.DeepEqual(dmMap[tc.collector], refMap[tc.collector]) { + t.Errorf("json format different from reference, Get=%s Want=%s", dmMap[tc.collector], refMap[tc.collector]) + } + }) + } +} + +func TestDnsMessage_Json_Transforms_Reference(t *testing.T) { + + testcases := []struct { + transform string + dmRef DNSMessage + jsonRef string + }{ + { + transform: "filtering", + dmRef: DNSMessage{Filtering: &TransformFiltering{SampleRate: 22}}, + jsonRef: `{ + "filtering": { + "sample-rate": 22 + } + }`, + }, + { + transform: "reducer", + dmRef: DNSMessage{Reducer: &TransformReducer{Occurrences: 10, CumulativeLength: 47}}, + jsonRef: `{ + "reducer": { + "occurrences": 10, + "cumulative-length": 47 + } + }`, + }, + { + transform: "normalize", + dmRef: DNSMessage{ + PublicSuffix: &TransformPublicSuffix{ + QnamePublicSuffix: "com", + QnameEffectiveTLDPlusOne: "hello.com", + ManagedByICANN: true, + }, + }, + jsonRef: `{ + "publicsuffix": { + "tld": "com", + "etld+1": "hello.com", + "managed-icann": true + } + }`, + }, + { + transform: "geoip", + dmRef: DNSMessage{ + Geo: &TransformDNSGeo{ + City: "Paris", + Continent: "Europe", + CountryIsoCode: "FR", + AutonomousSystemNumber: "1234", + AutonomousSystemOrg: "Internet", + }, + }, + jsonRef: `{ + "geoip": { + "city": "Paris", + "continent": "Europe", + "country-isocode": "FR", + "as-number": "1234", + "as-owner": "Internet" + } + }`, + }, + { + transform: "atags", + dmRef: DNSMessage{ATags: &TransformATags{Tags: []string{"test0", "test1"}}}, + jsonRef: `{ + "atags": { + "tags": [ "test0", "test1" ] + } + }`, + }, + } + + for _, tc := range testcases { + t.Run(tc.transform, func(t *testing.T) { + + tc.dmRef.Init() + + var dmMap map[string]interface{} + err := json.Unmarshal([]byte(tc.dmRef.ToJSON()), &dmMap) + if err != nil { + t.Fatalf("could not unmarshal dm json: %s\n", err) + } + + var refMap map[string]interface{} + err = json.Unmarshal([]byte(tc.jsonRef), &refMap) + if err != nil { + t.Fatalf("could not unmarshal ref json: %s\n", err) + } + + if !reflect.DeepEqual(dmMap[tc.transform], refMap[tc.transform]) { + t.Errorf("json format different from reference, Get=%s Want=%s", dmMap[tc.transform], refMap[tc.transform]) + } + }) + } +} + +func BenchmarkDnsMessage_ToJSON(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dm.ToJSON() + } +} + +// Tests for Flat JSON format +func TestDnsMessage_JsonFlatten_Reference(t *testing.T) { + dm := DNSMessage{} + dm.Init() + + // add some items in slices field + dm.DNS.DNSRRs.Answers = append(dm.DNS.DNSRRs.Answers, DNSAnswer{Name: "google.nl", Rdata: "142.251.39.99", Rdatatype: "A", TTL: 300, Class: "IN"}) + dm.EDNS.Options = append(dm.EDNS.Options, DNSOption{Code: 10, Data: "aaaabbbbcccc", Name: "COOKIE"}) + + refJSON := ` + { + "dns.flags.aa": false, + "dns.flags.ad": false, + "dns.flags.qr": false, + "dns.flags.ra": false, + "dns.flags.tc": false, + "dns.flags.rd": false, + "dns.flags.cd": false, + "dns.length": 0, + "dns.malformed-packet": false, + "dns.id": 0, + "dns.opcode": 0, + "dns.qname": "-", + "dns.qtype": "-", + "dns.rcode": "-", + "dns.qclass": "-", + "dns.questions-count": 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", + "dns.resource-records.an.0.ttl": 300, + "dns.resource-records.an.0.class": "IN", + "dns.resource-records.ar": "-", + "dns.resource-records.ns": "-", + "dnstap.identity": "-", + "dnstap.latency": 0, + "dnstap.operation": "-", + "dnstap.timestamp-rfc3339ns": "-", + "dnstap.version": "-", + "dnstap.extra": "-", + "dnstap.policy-rule": "-", + "dnstap.policy-type": "-", + "dnstap.policy-action": "-", + "dnstap.policy-match": "-", + "dnstap.policy-value": "-", + "dnstap.peer-name": "-", + "dnstap.query-zone": "-", + "edns.dnssec-ok": 0, + "edns.options.0.code": 10, + "edns.options.0.data": "aaaabbbbcccc", + "edns.options.0.name": "COOKIE", + "edns.rcode": 0, + "edns.udp-size": 0, + "edns.version": 0, + "network.family": "-", + "network.ip-defragmented": false, + "network.protocol": "-", + "network.query-ip": "-", + "network.query-port": "-", + "network.response-ip": "-", + "network.response-port": "-", + "network.tcp-reassembled": false + } + ` + + var dmFlat map[string]interface{} + dmJSON, err := dm.ToFlatJSON() + if err != nil { + t.Fatalf("could not convert dm to flat json: %s\n", err) + } + err = json.Unmarshal([]byte(dmJSON), &dmFlat) + if err != nil { + t.Fatalf("could not unmarshal dm json: %s\n", err) + } + + var refMap map[string]interface{} + err = json.Unmarshal([]byte(refJSON), &refMap) + if err != nil { + t.Fatalf("could not unmarshal ref json: %s\n", err) + } + + for k, vRef := range refMap { + vFlat, ok := dmFlat[k] + if !ok { + t.Fatalf("Missing key %s in flatten message according to reference", k) + } + if vRef != vFlat { + t.Errorf("Invalid value for key=%s get=%v expected=%v", k, vFlat, vRef) + } + } + + for k := range dmFlat { + _, ok := refMap[k] + if !ok { + t.Errorf("This key %s should not be in the flat message", k) + } + } +} + +func TestDnsMessage_JsonFlatten_Transforms_Reference(t *testing.T) { + + testcases := []struct { + transform string + dm DNSMessage + jsonRef string + }{ + { + transform: "filtering", + dm: DNSMessage{Filtering: &TransformFiltering{SampleRate: 22}}, + jsonRef: `{ + "filtering.sample-rate": 22 + }`, + }, + { + transform: "reducer", + dm: DNSMessage{Reducer: &TransformReducer{Occurrences: 10, CumulativeLength: 47}}, + jsonRef: `{ + "reducer.occurrences": 10, + "reducer.cumulative-length": 47 + }`, + }, + { + transform: "publixsuffix", + dm: DNSMessage{ + PublicSuffix: &TransformPublicSuffix{ + QnamePublicSuffix: "com", + QnameEffectiveTLDPlusOne: "hello.com", + }, + }, + jsonRef: `{ + "publicsuffix.tld": "com", + "publicsuffix.etld+1": "hello.com" + }`, + }, + { + transform: "geoip", + dm: DNSMessage{ + Geo: &TransformDNSGeo{ + City: "Paris", + Continent: "Europe", + CountryIsoCode: "FR", + AutonomousSystemNumber: "1234", + AutonomousSystemOrg: "Internet", + }, + }, + jsonRef: `{ + "geoip.city": "Paris", + "geoip.continent": "Europe", + "geoip.country-isocode": "FR", + "geoip.as-number": "1234", + "geoip.as-owner": "Internet" + }`, + }, + { + transform: "suspicious", + dm: DNSMessage{Suspicious: &TransformSuspicious{Score: 1.0, + MalformedPacket: false, + LargePacket: true, + LongDomain: true, + SlowDomain: false, + UnallowedChars: true, + UncommonQtypes: false, + ExcessiveNumberLabels: true, + Domain: "gogle.co", + }}, + jsonRef: `{ + "suspicious.score": 1.0, + "suspicious.malformed-pkt": false, + "suspicious.large-pkt": true, + "suspicious.long-domain": true, + "suspicious.slow-domain": false, + "suspicious.unallowed-chars": true, + "suspicious.uncommon-qtypes": false, + "suspicious.excessive-number-labels": true, + "suspicious.domain": "gogle.co" + }`, + }, + { + transform: "extracted", + dm: DNSMessage{Extracted: &TransformExtracted{Base64Payload: []byte{}}}, + jsonRef: `{ + "extracted.dns_payload": "" + }`, + }, + { + transform: "machinelearning", + dm: DNSMessage{MachineLearning: &TransformML{ + Entropy: 10.0, + Length: 2, + Labels: 2, + Digits: 1, + Lowers: 35, + Uppers: 23, + Specials: 2, + Others: 1, + RatioDigits: 1.0, + RatioLetters: 1.0, + RatioSpecials: 1.0, + RatioOthers: 1.0, + ConsecutiveChars: 10, + ConsecutiveVowels: 10, + ConsecutiveDigits: 10, + ConsecutiveConsonants: 10, + Size: 11, + Occurrences: 10, + UncommonQtypes: 1, + }}, + jsonRef: `{ + "ml.entropy": 10.0, + "ml.length": 2, + "ml.labels": 2, + "ml.digits": 1, + "ml.lowers": 35, + "ml.uppers": 23, + "ml.specials": 2, + "ml.others": 1, + "ml.ratio-digits": 1.0, + "ml.ratio-letters": 1.0, + "ml.ratio-specials": 1.0, + "ml.ratio-others": 1.0, + "ml.consecutive-chars": 10, + "ml.consecutive-vowels": 10, + "ml.consecutive-digits": 10, + "ml.consecutive-consonants": 10, + "ml.size": 11, + "ml.occurrences": 10, + "ml.uncommon-qtypes": 1 + }`, + }, + { + transform: "atags", + dm: DNSMessage{ATags: &TransformATags{Tags: []string{"test0", "test1"}}}, + jsonRef: `{ + "atags.tags.0": "test0", + "atags.tags.1": "test1" + }`, + }, + } + + for _, tc := range testcases { + t.Run(tc.transform, func(t *testing.T) { + + tc.dm.Init() + + var dmFlat map[string]interface{} + dmJSON, err := tc.dm.ToFlatJSON() + if err != nil { + t.Fatalf("could not convert dm to flat json: %s\n", err) + } + err = json.Unmarshal([]byte(dmJSON), &dmFlat) + if err != nil { + t.Fatalf("could not unmarshal dm json: %s\n", err) + } + + var refMap map[string]interface{} + err = json.Unmarshal([]byte(tc.jsonRef), &refMap) + if err != nil { + t.Fatalf("could not unmarshal ref json: %s\n", err) + } + + for k, vRef := range refMap { + vFlat, ok := dmFlat[k] + if !ok { + t.Fatalf("Missing key %s in flatten message according to reference", k) + } + if vRef != vFlat { + t.Errorf("Invalid value for key=%s get=%v expected=%v", k, vFlat, vRef) + } + } + }) + } +} + +func TestDnsMessage_JsonFlatten_Collectors_Reference(t *testing.T) { + testcases := []struct { + collector string + dm DNSMessage + jsonRef string + }{ + { + collector: "powerdns", + dm: DNSMessage{PowerDNS: &PowerDNS{ + OriginalRequestSubnet: "subnet", + AppliedPolicy: "basicrpz", + AppliedPolicyHit: "hit", + AppliedPolicyKind: "kind", + AppliedPolicyTrigger: "trigger", + AppliedPolicyType: "type", + Tags: []string{"tag1"}, + Metadata: map[string]string{"stream_id": "collector"}, + HTTPVersion: "http3", + }}, + + jsonRef: `{ + "powerdns.original-request-subnet": "subnet", + "powerdns.applied-policy": "basicrpz", + "powerdns.applied-policy-hit": "hit", + "powerdns.applied-policy-kind": "kind", + "powerdns.applied-policy-trigger": "trigger", + "powerdns.applied-policy-type": "type", + "powerdns.tags.0": "tag1", + "powerdns.metadata.stream_id": "collector", + "powerdns.http-version": "http3" + }`, + }, + } + for _, tc := range testcases { + t.Run(tc.collector, func(t *testing.T) { + + tc.dm.Init() + + var dmFlat map[string]interface{} + dmJSON, err := tc.dm.ToFlatJSON() + if err != nil { + t.Fatalf("could not convert dm to flat json: %s\n", err) + } + err = json.Unmarshal([]byte(dmJSON), &dmFlat) + if err != nil { + t.Fatalf("could not unmarshal dm json: %s\n", err) + } + + var refMap map[string]interface{} + err = json.Unmarshal([]byte(tc.jsonRef), &refMap) + if err != nil { + t.Fatalf("could not unmarshal ref json: %s\n", err) + } + + for k, vRef := range refMap { + vFlat, ok := dmFlat[k] + if !ok { + t.Fatalf("Missing key %s in flatten message according to reference", k) + } + if vRef != vFlat { + t.Errorf("Invalid value for key=%s get=%v expected=%v", k, vFlat, vRef) + } + } + }) + } +} + +func BenchmarkDnsMessage_ToFlatJSON(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := dm.ToFlatJSON() + if err != nil { + b.Fatalf("could not encode to flat json: %v\n", err) + } + } +} diff --git a/dnsutils/dnsmessage_matchers.go b/dnsutils/dnsmessage_matchers.go new file mode 100644 index 00000000..a94e7803 --- /dev/null +++ b/dnsutils/dnsmessage_matchers.go @@ -0,0 +1,441 @@ +package dnsutils + +import ( + "fmt" + "reflect" + "regexp" + "strconv" + "strings" +) + +// matchUserMap matches a map based on user-provided conditions. +// dns.qname: +// match-source: "file://./tests/testsdata/filtering_keep_domains_regex.txt" +// source-kind: "regexp_list" +func matchUserMap(realValue, expectedValue reflect.Value) (bool, error) { + for _, opKey := range expectedValue.MapKeys() { + opValue := expectedValue.MapIndex(opKey) + opName := opKey.Interface().(string) + + switch opName { + // Integer great than ? + case MatchingOpGreaterThan: + + isFloat, isInt := false, false + if _, ok := opValue.Interface().(float64); ok { + isFloat = true + } + if _, ok := opValue.Interface().(int); ok { + isInt = true + } + + if !isFloat && !isInt { + return false, fmt.Errorf("integer or float is expected for greater-than operator, not %s", reflect.TypeOf(opValue.Interface())) + } + + // If realValue is a slice + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len(); i++ { + elemValue := realValue.Index(i) + + // Check if the element is a int + if _, ok := elemValue.Interface().(int); !ok { + continue + } + + // Check for match + if elemValue.Interface().(int) > opValue.Interface().(int) { + return true, nil + } + } + return false, nil + } + + if isFloat && realValue.Kind() == reflect.Float64 { + if realValue.Interface().(float64) > opValue.Interface().(float64) { + return true, nil + } + } + + if isInt && realValue.Kind() == reflect.Int { + if realValue.Interface().(int) > opValue.Interface().(int) { + return true, nil + } + } + + return false, nil + + // Integer lower than ? + case MatchingOpLowerThan: + isFloat, isInt := false, false + if _, ok := opValue.Interface().(float64); ok { + isFloat = true + } + if _, ok := opValue.Interface().(int); ok { + isInt = true + } + + if !isFloat && !isInt { + return false, fmt.Errorf("integer or float is expected for lower-than operator, not %s", reflect.TypeOf(opValue.Interface())) + } + + // If realValue is a slice + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len(); i++ { + elemValue := realValue.Index(i) + + // Check if the element is a int + if _, ok := elemValue.Interface().(int); !ok { + continue + } + + // Check for match + if elemValue.Interface().(int) < opValue.Interface().(int) { + return true, nil + } + } + return false, nil + } + + if isFloat && realValue.Kind() == reflect.Float64 { + if realValue.Interface().(float64) < opValue.Interface().(float64) { + return true, nil + } + } + + if isInt && realValue.Kind() == reflect.Int { + if realValue.Interface().(int) < opValue.Interface().(int) { + return true, nil + } + } + + return false, nil + + // Ignore these operators + case MatchingOpSource, MatchingOpSourceKind: + continue + + // List of pattern + case MatchingKindRegexp: + patternList := opValue.Interface().([]*regexp.Regexp) + + // If realValue is a slice + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len(); i++ { + elemValue := realValue.Index(i) + + // Check if the element is a string + if _, ok := elemValue.Interface().(string); !ok { + continue + } + + // Check for a match with the regex pattern + for _, pattern := range patternList { + if pattern.MatchString(elemValue.Interface().(string)) { + return true, nil + } + } + } + // No match found in the slice + return false, nil + } + + if realValue.Kind() != reflect.String { + return false, nil + } + for _, pattern := range patternList { + if pattern.MatchString(realValue.Interface().(string)) { + return true, nil + } + } + // No match found in pattern list + return false, nil + + // List of string + case MatchingKindString: + stringList := opValue.Interface().([]string) + + // If realValue is a slice + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len(); i++ { + elemValue := realValue.Index(i) + + // Check if the element is a string + if _, ok := elemValue.Interface().(string); !ok { + continue + } + + // Check for a match with the text + for _, textItem := range stringList { + if textItem == realValue.Interface().(string) { + return true, nil + } + } + } + // No match found in the slice + return false, nil + } + + if realValue.Kind() != reflect.String { + return false, nil + } + for _, textItem := range stringList { + if textItem == realValue.Interface().(string) { + return true, nil + } + } + + // No match found in string list + return false, nil + + default: + return false, fmt.Errorf("invalid operator '%s', ignore it", opKey.Interface().(string)) + } + } + return true, nil +} + +// matchUserSlice matches a slice based on user-provided conditions. +// dns.qname: +// - ".*\\.github\\.com$" +// - "^www\\.google\\.com$" +func matchUserSlice(realValue, expectedValue reflect.Value) (bool, error) { + match := false + for i := 0; i < expectedValue.Len() && !match; i++ { + reflectedSub := reflect.ValueOf(expectedValue.Index(i).Interface()) + + switch reflectedSub.Kind() { + case reflect.Int: + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len(); i++ { + elemValue := realValue.Index(i) + if _, ok := elemValue.Interface().(int); !ok { + continue + } + if reflectedSub.Interface().(int) == elemValue.Interface().(int) { + return true, nil + } + } + } + + if realValue.Kind() != reflect.Int || realValue.Interface().(int) != reflectedSub.Interface().(int) { + continue + } + match = true + case reflect.String: + pattern := regexp.MustCompile(reflectedSub.Interface().(string)) + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len() && !match; i++ { + elemValue := realValue.Index(i) + if _, ok := elemValue.Interface().(string); !ok { + continue + } + // Check for a match with the regex pattern + if pattern.MatchString(elemValue.Interface().(string)) { + match = true + } + } + } + + if realValue.Kind() != reflect.String { + continue + } + + if pattern.MatchString(realValue.Interface().(string)) { + match = true + } + } + } + return match, nil +} + +// matchUserBoolean matches a boolean based on user-provided conditions. +// dns.flags.qr: true +func matchUserBoolean(realValue, expectedValue reflect.Value) (bool, error) { + // If realValue is a slice + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len(); i++ { + elemValue := realValue.Index(i) + + // Check if the element is a int + if _, ok := elemValue.Interface().(bool); !ok { + continue + } + + // Check for match + if expectedValue.Interface().(bool) == elemValue.Interface().(bool) { + return true, nil + } + } + } + + if realValue.Kind() != reflect.Bool { + return false, nil + } + + if expectedValue.Interface().(bool) != realValue.Interface().(bool) { + return false, nil + } + return true, nil +} + +// matchUserInteger matches an integer based on user-provided conditions. +// dns.opcode: 0 +func matchUserInteger(realValue, expectedValue reflect.Value) (bool, error) { + // If realValue is a slice + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len(); i++ { + elemValue := realValue.Index(i) + + // Check if the element is a int + if _, ok := elemValue.Interface().(int); !ok { + continue + } + + // Check for match + if expectedValue.Interface().(int) == elemValue.Interface().(int) { + return true, nil + } + } + } + + if realValue.Kind() != reflect.Int { + return false, nil + } + if expectedValue.Interface().(int) != realValue.Interface().(int) { + return false, nil + } + + return true, nil +} + +// matchUserPattern matches a pattern based on user-provided conditions. +// dns.qname: "^.*\\.github\\.com$" +func matchUserPattern(realValue, expectedValue reflect.Value) (bool, error) { + pattern := regexp.MustCompile(expectedValue.Interface().(string)) + + // If realValue is a slice + if realValue.Kind() == reflect.Slice { + for i := 0; i < realValue.Len(); i++ { + elemValue := realValue.Index(i) + + // Check if the element is a string + if _, ok := elemValue.Interface().(string); !ok { + continue + } + + // Check for a match with the regex pattern + if pattern.MatchString(elemValue.Interface().(string)) { + return true, nil + } + } + // No match found in the slice + return false, nil + } + + // If realValue is not a string + if realValue.Kind() != reflect.String { + return false, nil + } + + // Check for a match with the regex pattern + if !pattern.MatchString(realValue.String()) { + return false, nil + } + + // Match found for a single value + return true, nil +} + +// getFieldByJSONTag retrieves a field value from a struct based on JSON tags. +func getFieldByJSONTag(value reflect.Value, nestedKeys string) (reflect.Value, bool) { + listKeys := strings.SplitN(nestedKeys, ".", 2) + jsonKey := listKeys[0] + var remainingKeys string + if len(listKeys) > 1 { + remainingKeys = listKeys[1] + } + + for i := 0; i < value.NumField(); i++ { + field := value.Type().Field(i) + + // Get JSON tag + tag := field.Tag.Get("json") + tagClean := strings.TrimSuffix(tag, ",omitempty") + + // Check if the JSON tag matches + if tagClean == jsonKey { + fieldValue := value.Field(i) + + // Handle pointers safely + if fieldValue.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + return reflect.Value{}, false + } + fieldValue = fieldValue.Elem() + } + + if remainingKeys == "" { + // Base case: return the field value if no more keys are left + return fieldValue, true + } + + // Recurse into structs or handle slices + switch fieldValue.Kind() { + case reflect.Struct: + return getFieldByJSONTag(fieldValue, remainingKeys) + case reflect.Slice: + if sliceElem, leftKey, found := getSliceElement(fieldValue, remainingKeys); found { + // Handle the slice element based on its kind + switch sliceElem.Kind() { + case reflect.Struct: + return getFieldByJSONTag(sliceElem, leftKey) + case reflect.Slice, reflect.Array: + var result []interface{} + for i := 0; i < sliceElem.Len(); i++ { + if subElem := sliceElem.Index(i); subElem.Kind() == reflect.Struct { + if nestedValue, found := getFieldByJSONTag(subElem, leftKey); found { + result = append(result, nestedValue.Interface()) + } + } else { + result = append(result, subElem.Interface()) + } + } + if len(result) > 0 { + return reflect.ValueOf(result), true + } + default: + return sliceElem, true + } + } + default: + return fieldValue, true + } + } + } + + return reflect.Value{}, false +} + +// getSliceElement retrieves an element from a slice based on the provided keys. +func getSliceElement(sliceValue reflect.Value, nestedKeys string) (reflect.Value, string, bool) { + listKeys := strings.SplitN(nestedKeys, ".", 2) + leftKeys := "" + if len(listKeys) > 1 { + leftKeys = listKeys[1] + } + sliceIndex := listKeys[0] + + if sliceIndex == "*" { + return sliceValue, leftKeys, true + } + + // Convert the slice index from string to int + index, err := strconv.Atoi(sliceIndex) + if err != nil || index < 0 || index >= sliceValue.Len() { + // Handle the error (e.g., invalid index format or out of range) + return reflect.Value{}, leftKeys, false + } + + return sliceValue.Index(index), leftKeys, true +} diff --git a/dnsutils/dnsmessage_matching_test.go b/dnsutils/dnsmessage_matching_test.go new file mode 100644 index 00000000..ef0fcead --- /dev/null +++ b/dnsutils/dnsmessage_matching_test.go @@ -0,0 +1,339 @@ +package dnsutils + +import "testing" + +// Matching +func TestDNSMessage_Matching(t *testing.T) { + tests := []struct { + name string + dm *DNSMessage + matching map[string]interface{} + wantError bool + wantMatch bool + }{ + { + name: "Test integer matching", + dm: &DNSMessage{DNS: DNS{Opcode: 1}}, + matching: map[string]interface{}{"dns.opcode": 1}, + wantError: false, + wantMatch: true, + }, + { + name: "Test no match with incorrect integer", + dm: &DNSMessage{DNS: DNS{Opcode: 2}}, + matching: map[string]interface{}{"dns.opcode": 1}, + wantError: false, + wantMatch: false, + }, + { + name: "Test string matching", + dm: &DNSMessage{DNS: DNS{Qname: "www.example.com"}}, + matching: map[string]interface{}{"dns.qname": "www.example.com"}, + wantError: false, + wantMatch: true, + }, + { + name: "Test no match with incorrect string", + dm: &DNSMessage{DNS: DNS{Qname: "www.notexample.com"}}, + matching: map[string]interface{}{"dns.qname": "www.example.com"}, + wantError: false, + wantMatch: false, + }, + { + name: "Test boolean matching", + dm: &DNSMessage{DNS: DNS{Flags: DNSFlags{QR: true}}}, + matching: map[string]interface{}{"dns.flags.qr": true}, + wantError: false, + wantMatch: true, + }, + { + name: "Test no match with incorrect boolean", + dm: &DNSMessage{DNS: DNS{Flags: DNSFlags{QR: false}}}, + matching: map[string]interface{}{"dns.flags.qr": true}, + wantError: false, + wantMatch: false, + }, + { + name: "Test regex with match", + dm: &DNSMessage{DNS: DNS{Qname: "www.github.com"}}, + matching: map[string]interface{}{ + "dns.qname": "^.*\\.github\\.com$", + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test regex with no match", + dm: &DNSMessage{DNS: DNS{Qname: "www.google.com"}}, + matching: map[string]interface{}{ + "dns.qname": "^.*\\.github\\.com$", + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test matching with multiple conditions", + dm: &DNSMessage{DNS: DNS{Opcode: 1, Qname: "www.example.com", Flags: DNSFlags{QR: true}}}, + matching: map[string]interface{}{ + "dns.flags.qr": true, + "dns.opcode": 1, + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test integer greater than operator matching", + dm: &DNSMessage{DNS: DNS{Opcode: 5}}, + matching: map[string]interface{}{ + "dns.opcode": map[string]interface{}{ + "greater-than": 3, + }, + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test integer with invalid greater than operator", + dm: &DNSMessage{DNS: DNS{Opcode: 1}}, + matching: map[string]interface{}{ + "dns.opcode": map[string]interface{}{ + "greater-than": "0", + }, + }, + wantError: true, + wantMatch: false, + }, + { + name: "Test float greater than operator matching", + dm: &DNSMessage{DNSTap: DNSTap{Latency: 0.5}}, + matching: map[string]interface{}{ + "dnstap.latency": map[string]interface{}{ + "greater-than": 0.3, + }, + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test lower than operator matching", + dm: &DNSMessage{DNS: DNS{Opcode: 9}}, + matching: map[string]interface{}{ + "dns.opcode": map[string]interface{}{ + "lower-than": 10, + }, + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test lower than operator no match", + dm: &DNSMessage{DNS: DNS{Opcode: 1}}, + matching: map[string]interface{}{ + "dns.opcode": map[string]interface{}{ + "lower-than": 1, + }, + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test no match invalid lower than operator", + dm: &DNSMessage{DNS: DNS{Opcode: 1}}, + matching: map[string]interface{}{ + "dns.opcode": map[string]interface{}{ + "lower-than": "1", + }, + }, + wantError: true, + wantMatch: false, + }, + { + name: "Test match with list of string", + dm: &DNSMessage{DNS: DNS{Qname: "www.example.com"}}, + matching: map[string]interface{}{ + "dns.qname": []interface{}{"www.test.com", "www.example.com"}, + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test no match with list of string", + dm: &DNSMessage{DNS: DNS{Qname: "www.notexample.com"}}, + matching: map[string]interface{}{ + "dns.qname": []interface{}{"www.test.com", "www.example.com"}, + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test non-existent key", + dm: &DNSMessage{DNS: DNS{Opcode: 1}}, + matching: map[string]interface{}{ + "dns.nonexistent": 1, + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test nested non-existent key", + dm: &DNSMessage{DNS: DNS{Opcode: 1}}, + matching: map[string]interface{}{ + "dns.flags.nonexistent": true, + }, + wantError: false, + wantMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, isMatch := tt.dm.Matching(tt.matching) + if (err != nil) != tt.wantError { + t.Errorf("DNSMessage.Matching() error = %v, wantError %v", err, tt.wantError) + return + } + if isMatch != tt.wantMatch { + t.Errorf("DNSMessage.Matching() = %v, want %v", isMatch, tt.wantMatch) + } + }) + } +} + +func TestDNSMessage_Matching_Arrays(t *testing.T) { + tests := []struct { + name string + dm *DNSMessage + matching map[string]interface{} + wantError bool + wantMatch bool + }{ + { + name: "Test wilcard match with operator", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{TTL: 300}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.*.ttl": map[string]interface{}{ + "greater-than": 10, + }, + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test wilcard no match and operator", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{TTL: 300}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.*.ttl": map[string]interface{}{ + "greater-than": 400, + }, + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test wilcard no match and invalid operator", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{TTL: 300}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.*.ttl": map[string]interface{}{ + "greater-than-invalid": 400, + }, + }, + wantError: true, + wantMatch: false, + }, + { + name: "Test array match with index", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{TTL: 300}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.0.ttl": 300, + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test array match with invalid index", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{TTL: 300}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.1.ttl": 300, + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test array match with index and multiple conditions", + dm: &DNSMessage{ + DNSTap: DNSTap{Operation: "CLIENT_RESPONSE"}, + DNS: DNS{Rcode: "NOERROR", Qtype: "A", DNSRRs: DNSRRs{Answers: []DNSAnswer{{Rdata: "0.0.0.0"}}}}, + }, + matching: map[string]interface{}{ + "dnstap.operation": "CLIENT_RESPONSE", + "dns.qtype": "A", + "dns.rcode": "NOERROR", + "dns.resource-records.an.0.rdata": "0.0.0.0", + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test array match with index and missing key", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{TTL: 300}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.0.missing-key": 300, + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test array match with bad index", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{TTL: 300}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.badindex.ttl": 300, + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test array no match with index and invalid data type", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{TTL: 300}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.0.ttl": "not-a-number", + }, + wantError: false, + wantMatch: false, + }, + { + name: "Test IP address match with regex", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{Rdata: "1.2.3.4"}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.0.rdata": []interface{}{ + "^1\\.2\\.3\\.(4|5)$", + }, + }, + wantError: false, + wantMatch: true, + }, + { + name: "Test IP address no match with regex", + dm: &DNSMessage{DNS: DNS{DNSRRs: DNSRRs{Answers: []DNSAnswer{{Rdata: "1.2.3.4"}}}}}, + matching: map[string]interface{}{ + "dns.resource-records.an.0.rdata": []interface{}{ + "^5\\.4\\.3\\.(4|5)$", + }, + }, + wantError: false, + wantMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, isMatch := tt.dm.Matching(tt.matching) + if (err != nil) != tt.wantError { + t.Errorf("DNSMessage.Matching() error = %v, wantError %v", err, tt.wantError) + return + } + if isMatch != tt.wantMatch { + t.Errorf("DNSMessage.Matching() = %v, want %v", isMatch, tt.wantMatch) + } + }) + } +} diff --git a/dnsutils/dnsmessage_pcap_test.go b/dnsutils/dnsmessage_pcap_test.go new file mode 100644 index 00000000..061cbd7f --- /dev/null +++ b/dnsutils/dnsmessage_pcap_test.go @@ -0,0 +1,32 @@ +package dnsutils + +import ( + "testing" + + "github.com/dmachard/go-netutils" + "github.com/miekg/dns" +) + +// Tests for PCAP serialization +func BenchmarkDnsMessage_ToPacketLayer(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + dnsmsg := new(dns.Msg) + dnsmsg.SetQuestion("dnscollector.dev.", dns.TypeAAAA) + dnsquestion, _ := dnsmsg.Pack() + + dm.NetworkInfo.Family = netutils.ProtoIPv4 + dm.NetworkInfo.Protocol = netutils.ProtoUDP + dm.DNS.Payload = dnsquestion + dm.DNS.Length = len(dnsquestion) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := dm.ToPacketLayer() + if err != nil { + b.Fatalf("could not encode to pcap: %v\n", err) + } + } +} diff --git a/dnsutils/dnsmessage_relabelling_test.go b/dnsutils/dnsmessage_relabelling_test.go new file mode 100644 index 00000000..3b37e2f4 --- /dev/null +++ b/dnsutils/dnsmessage_relabelling_test.go @@ -0,0 +1,80 @@ +package dnsutils + +import ( + "reflect" + "regexp" + "testing" +) + +// Flatten and relabeling +func TestDnsMessage_ApplyRelabeling(t *testing.T) { + // Créer un DNSMessage avec des règles de relabeling pour le test + dm := &DNSMessage{ + Relabeling: &TransformRelabeling{ + Rules: []RelabelingRule{ + {Regex: regexp.MustCompile("^old_"), Replacement: "new_field", Action: "rename"}, + {Regex: regexp.MustCompile("^foo_"), Action: "remove"}, + }, + }, + } + + // test map + dnsFields := map[string]interface{}{ + "old_field": "value1", + "foo_field": "value2", + "other_field": "value3", + } + + // apply relabeling + err := dm.ApplyRelabeling(dnsFields) + if err != nil { + t.Errorf("ApplyRelabeling() return an error: %v", err) + } + + // check + expectedDNSFields := map[string]interface{}{ + "new_field": "value1", + "other_field": "value3", + } + if !reflect.DeepEqual(dnsFields, expectedDNSFields) { + t.Errorf("Want: %v, Get: %v", expectedDNSFields, dnsFields) + } +} + +func BenchmarkDnsMessage_ToFlatten_Relabelling(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + dm.Relabeling.Rules = append(dm.Relabeling.Rules, RelabelingRule{ + Regex: regexp.MustCompile(`dns.qname`), + Action: "remove", + }) + dm.Relabeling.Rules = append(dm.Relabeling.Rules, RelabelingRule{ + Regex: regexp.MustCompile(`dns.qtype`), + Replacement: "qtype", + Action: "rename", + }) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := dm.Flatten() + if err != nil { + b.Fatalf("could not flat: %v\n", err) + } + } +} + +func BenchmarkDnsMessage_ToFlatten(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := dm.Flatten() + if err != nil { + b.Fatalf("could not flat: %v\n", err) + } + } +} diff --git a/dnsutils/dnsmessage_template_test.go b/dnsutils/dnsmessage_template_test.go new file mode 100644 index 00000000..4b8821cf --- /dev/null +++ b/dnsutils/dnsmessage_template_test.go @@ -0,0 +1,65 @@ +package dnsutils + +import ( + "strings" + "testing" +) + +// To jinja templating +func TestDnsMessage_ToJinjaFormat(t *testing.T) { + dm := DNSMessage{} + dm.Init() + + dm.DNS.Qname = "qname_for_test" + + 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 }} + +;; QUESTION SECTION: +;{{ dm.DNS.Qname }} {{ dm.DNS.Qclass }} {{ dm.DNS.Qtype }} + +;; ANSWER SECTION: {% for rr in dm.DNS.DNSRRs.Answers %} +{{ rr.Name }} {{ rr.TTL }} {{ rr.Class }} {{ rr.Rdatatype }} {{ rr.Rdata }}{% endfor %} + +;; WHEN: {{ dm.DNSTap.Timestamp }} +;; MSG SIZE rcvd: {{ dm.DNS.Length }}` + + text, err := dm.ToTextTemplate(template) + if err != nil { + t.Errorf("Want no error, got: %s", err) + } + + if !strings.Contains(text, dm.DNS.Qname) { + t.Errorf("Want qname in template, got: %s", text) + } +} + +func BenchmarkDnsMessage_ToJinjaFormat(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + 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 }} + +;; QUESTION SECTION: +;{{ dm.DNS.Qname }} {{ dm.DNS.Qclass }} {{ dm.DNS.Qtype }} + +;; ANSWER SECTION: {% for rr in dm.DNS.DNSRRs.Answers %} +{{ rr.Name }} {{ rr.TTL }} {{ rr.Class }} {{ rr.Rdatatype }} {{ rr.Rdata }}{% endfor %} + +;; WHEN: {{ dm.DNSTap.Timestamp }} +;; MSG SIZE rcvd: {{ dm.DNS.Length }}` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := dm.ToTextTemplate(template) + if err != nil { + b.Fatalf("could not encode to template: %v\n", err) + } + } +} diff --git a/dnsutils/dnsmessage_test.go b/dnsutils/dnsmessage_test.go new file mode 100644 index 00000000..52646056 --- /dev/null +++ b/dnsutils/dnsmessage_test.go @@ -0,0 +1,15 @@ +package dnsutils + +import ( + "testing" +) + +// Bench to init DNS message +func BenchmarkDnsMessage_Init(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + } +} diff --git a/dnsutils/dnsmessage_text_test.go b/dnsutils/dnsmessage_text_test.go new file mode 100644 index 00000000..7ba3a6b8 --- /dev/null +++ b/dnsutils/dnsmessage_text_test.go @@ -0,0 +1,708 @@ +package dnsutils + +import ( + "strings" + "testing" + + "github.com/dmachard/go-dnscollector/pkgconfig" +) + +// Tests for TEXT format +func TestDnsMessage_TextFormat_ToString(t *testing.T) { + + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + delimiter string + boundary string + format string + qname string + identity string + expected string + }{ + { + name: "default", + delimiter: config.Global.TextFormatDelimiter, + boundary: config.Global.TextFormatBoundary, + format: config.Global.TextFormat, + qname: "dnscollector.fr", + identity: "collector", + expected: "- collector CLIENT_QUERY NOERROR 1.2.3.4 1234 - - 0b dnscollector.fr A 0.000000000", + }, + { + name: "custom_delimiter", + delimiter: ";", + boundary: config.Global.TextFormatBoundary, + format: config.Global.TextFormat, + qname: "dnscollector.fr", + identity: "collector", + expected: "-;collector;CLIENT_QUERY;NOERROR;1.2.3.4;1234;-;-;0b;dnscollector.fr;A;0.000000000", + }, + { + name: "empty_delimiter", + delimiter: "", + boundary: config.Global.TextFormatBoundary, + format: config.Global.TextFormat, + qname: "dnscollector.fr", + identity: "collector", + expected: "-collectorCLIENT_QUERYNOERROR1.2.3.41234--0bdnscollector.frA0.000000000", + }, + { + name: "qname_quote", + delimiter: config.Global.TextFormatDelimiter, + boundary: config.Global.TextFormatBoundary, + format: config.Global.TextFormat, + qname: "dns collector.fr", + identity: "collector", + expected: "- collector CLIENT_QUERY NOERROR 1.2.3.4 1234 - - 0b \"dns collector.fr\" A 0.000000000", + }, + { + name: "default_boundary", + delimiter: config.Global.TextFormatDelimiter, + boundary: config.Global.TextFormatBoundary, + format: config.Global.TextFormat, + qname: "dns\"coll tor\".fr", + identity: "collector", + expected: "- collector CLIENT_QUERY NOERROR 1.2.3.4 1234 - - 0b \"dns\\\"coll tor\\\".fr\" A 0.000000000", + }, + { + name: "custom_boundary", + delimiter: config.Global.TextFormatDelimiter, + boundary: "!", + format: config.Global.TextFormat, + qname: "dnscoll tor.fr", + identity: "collector", + expected: "- collector CLIENT_QUERY NOERROR 1.2.3.4 1234 - - 0b !dnscoll tor.fr! A 0.000000000", + }, + { + name: "custom_text", + delimiter: config.Global.TextFormatDelimiter, + boundary: config.Global.TextFormatBoundary, + format: "qname {IN} qtype", + qname: "dnscollector.fr", + identity: "", + expected: "dnscollector.fr IN A", + }, + { + name: "quote_dnstap_version", + delimiter: config.Global.TextFormatDelimiter, + boundary: config.Global.TextFormatBoundary, + format: "identity version qname", + qname: "dnscollector.fr", + identity: "collector", + expected: "collector \"dnscollector 1.0.0\" dnscollector.fr", + }, + { + name: "quote_dnstap_identity", + delimiter: config.Global.TextFormatDelimiter, + boundary: config.Global.TextFormatBoundary, + format: "identity qname", + qname: "dnscollector.fr", + identity: "dns collector", + expected: "\"dns collector\" dnscollector.fr", + }, + { + name: "quote_dnstap_peername", + delimiter: config.Global.TextFormatDelimiter, + boundary: config.Global.TextFormatBoundary, + format: "peer-name qname", + qname: "dnscollector.fr", + identity: "", + expected: "\"localhost (127.0.0.1)\" dnscollector.fr", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + dm := GetFakeDNSMessage() + + dm.DNS.Qname = tc.qname + dm.DNSTap.Identity = tc.identity + + line := dm.String(strings.Fields(tc.format), tc.delimiter, tc.boundary) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_DefaultDirectives(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + format: "timestamp-rfc3339ns timestamp", + dm: DNSMessage{DNSTap: DNSTap{TimestampRFC3339: "2023-04-22T09:17:02.906922231Z"}}, + expected: "2023-04-22T09:17:02.906922231Z 2023-04-22T09:17:02.906922231Z", + }, + { + format: "timestamp-unixns timestamp-unixus timestamp-unixms", + dm: DNSMessage{DNSTap: DNSTap{Timestamp: 1682152174001850960}}, + expected: "1682152174001850960 1682152174001850 1682152174001", + }, + { + format: "latency", + dm: DNSMessage{DNSTap: DNSTap{Latency: 0.00001}}, + expected: "0.000010000", + }, + { + format: "qname qtype opcode", + dm: DNSMessage{DNS: DNS{Qname: "dnscollector.fr", Qtype: "AAAA", Opcode: 42}}, + expected: "dnscollector.fr AAAA 42", + }, + { + format: "qclass", + dm: DNSMessage{DNS: DNS{Qclass: "CH"}}, + expected: "CH", + }, + { + format: "operation", + dm: DNSMessage{DNSTap: DNSTap{Operation: "CLIENT_QUERY"}}, + expected: "CLIENT_QUERY", + }, + { + format: "family protocol", + dm: DNSMessage{NetworkInfo: DNSNetInfo{Family: "IPv4", Protocol: "UDP"}}, + expected: "IPv4 UDP", + }, + { + format: "length", + dm: DNSMessage{DNS: DNS{Length: 42}}, + expected: "42", + }, + { + format: "length-unit", + dm: DNSMessage{DNS: DNS{Length: 42}}, + expected: "42b", + }, + { + format: "malformed", + dm: DNSMessage{DNS: DNS{MalformedPacket: true}}, + expected: "PKTERR", + }, + { + format: "tc aa ra ad", + dm: DNSMessage{DNS: DNS{Flags: DNSFlags{TC: true, AA: true, RA: true, AD: true}}}, + expected: "TC AA RA AD", + }, + { + format: "df tr", + dm: DNSMessage{NetworkInfo: DNSNetInfo{IPDefragmented: true, TCPReassembled: true}}, + expected: "DF TR", + }, + { + format: "queryip queryport", + dm: DNSMessage{NetworkInfo: DNSNetInfo{QueryIP: "1.2.3.4", QueryPort: "4200"}}, + expected: "1.2.3.4 4200", + }, + { + format: "responseip responseport", + dm: DNSMessage{NetworkInfo: DNSNetInfo{ResponseIP: "1.2.3.4", ResponsePort: "4200"}}, + expected: "1.2.3.4 4200", + }, + { + format: "policy-rule policy-type policy-action policy-match policy-value", + dm: DNSMessage{DNSTap: DNSTap{PolicyRule: "rule", PolicyType: "type", + PolicyAction: "action", PolicyMatch: "match", + PolicyValue: "value"}}, + expected: "rule type action match value", + }, + { + format: "peer-name", + dm: DNSMessage{DNSTap: DNSTap{PeerName: "testpeer"}}, + expected: "testpeer", + }, + { + format: "query-zone", + dm: DNSMessage{DNSTap: DNSTap{QueryZone: "queryzone.test"}}, + expected: "queryzone.test", + }, + } + + for _, tc := range testcases { + t.Run(tc.format, func(t *testing.T) { + line := tc.dm.String(strings.Fields(tc.format), config.Global.TextFormatDelimiter, config.Global.TextFormatBoundary) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_InvalidDirectives(t *testing.T) { + testcases := []struct { + name string + dm DNSMessage + format string + }{ + { + name: "default", + dm: DNSMessage{}, + format: "invalid", + }, + { + name: "publicsuffix", + dm: DNSMessage{PublicSuffix: &TransformPublicSuffix{}}, + format: "publixsuffix-invalid", + }, + { + name: "powerdns", + dm: DNSMessage{PowerDNS: &PowerDNS{}}, + format: "powerdns-invalid", + }, + { + name: "geoip", + dm: DNSMessage{Geo: &TransformDNSGeo{}}, + format: "geoip-invalid", + }, + { + name: "suspicious", + dm: DNSMessage{Suspicious: &TransformSuspicious{}}, + format: "suspicious-invalid", + }, + { + name: "extracted", + dm: DNSMessage{Extracted: &TransformExtracted{}}, + format: "extracted-invalid", + }, + { + name: "filtering", + dm: DNSMessage{Filtering: &TransformFiltering{}}, + format: "filtering-invalid", + }, + { + name: "reducer", + dm: DNSMessage{Reducer: &TransformReducer{}}, + format: "reducer-invalid", + }, + { + name: "ml", + dm: DNSMessage{MachineLearning: &TransformML{}}, + format: "ml-invalid", + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.dm.ToTextLine(strings.Fields(tc.format), " ", "") + if err == nil { + t.Errorf("Want err, got nil") + } else if err.Error() != ErrorUnexpectedDirective+tc.format { + t.Errorf("Unexpected error: %s", err.Error()) + } + }) + } +} + +func TestDnsMessage_TextFormat_Directives_PublicSuffix(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + name: "undefined", + format: "publixsuffix-tld", + dm: DNSMessage{}, + expected: "-", + }, + { + name: "default", + format: "publixsuffix-tld publixsuffix-etld+1", + dm: DNSMessage{PublicSuffix: &TransformPublicSuffix{QnamePublicSuffix: "com", QnameEffectiveTLDPlusOne: "google.com"}}, + expected: "com google.com", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + line := tc.dm.String( + strings.Fields(tc.format), + config.Global.TextFormatDelimiter, + config.Global.TextFormatBoundary, + ) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_Directives_Geo(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + name: "undefined", + format: "geoip-continent", + dm: DNSMessage{}, + expected: "-", + }, + { + name: "default", + format: "geoip-continent geoip-country geoip-city geoip-as-number geoip-as-owner", + dm: DNSMessage{Geo: &TransformDNSGeo{City: "Paris", Continent: "Europe", + CountryIsoCode: "FR", AutonomousSystemNumber: "AS1", AutonomousSystemOrg: "Google"}}, + expected: "Europe FR Paris AS1 Google", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + line := tc.dm.String( + strings.Fields(tc.format), + config.Global.TextFormatDelimiter, + config.Global.TextFormatBoundary, + ) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_Directives_Pdns(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + name: "undefined", + format: "powerdns-tags", + dm: DNSMessage{}, + expected: "-", + }, + { + name: "empty_attributes", + format: "powerdns-tags powerdns-applied-policy powerdns-original-request-subnet powerdns-metadata", + dm: DNSMessage{PowerDNS: &PowerDNS{}}, + expected: "- - - -", + }, + { + name: "applied_policy", + format: "powerdns-applied-policy powerdns-applied-policy-hit powerdns-applied-policy-kind powerdns-applied-policy-trigger powerdns-applied-policy-type", + dm: DNSMessage{PowerDNS: &PowerDNS{ + AppliedPolicy: "policy", + AppliedPolicyHit: "hit", + AppliedPolicyKind: "kind", + AppliedPolicyTrigger: "trigger", + AppliedPolicyType: "type", + }}, + expected: "policy hit kind trigger type", + }, + { + name: "original_request_subnet", + format: "powerdns-original-request-subnet", + dm: DNSMessage{PowerDNS: &PowerDNS{OriginalRequestSubnet: "test"}}, + expected: "test", + }, + { + name: "metadata_badsyntax", + format: "powerdns-metadata", + dm: DNSMessage{PowerDNS: &PowerDNS{Metadata: map[string]string{"test_key1": "test_value1"}}}, + expected: "-", + }, + { + name: "metadata", + format: "powerdns-metadata:test_key1", + dm: DNSMessage{PowerDNS: &PowerDNS{Metadata: map[string]string{"test_key1": "test_value1"}}}, + expected: "test_value1", + }, + { + name: "metadata_invalid", + format: "powerdns-metadata:test_key2", + dm: DNSMessage{PowerDNS: &PowerDNS{Metadata: map[string]string{"test_key1": "test_value1"}}}, + expected: "-", + }, + { + name: "tags_all", + format: "powerdns-tags", + dm: DNSMessage{PowerDNS: &PowerDNS{Tags: []string{"tag1", "tag2"}}}, + expected: "tag1,tag2", + }, + { + name: "tags_index", + format: "powerdns-tags:1", + dm: DNSMessage{PowerDNS: &PowerDNS{Tags: []string{"tag1", "tag2"}}}, + expected: "tag2", + }, + { + name: "tags_invalid_index", + format: "powerdns-tags:3", + dm: DNSMessage{PowerDNS: &PowerDNS{Tags: []string{"tag1", "tag2"}}}, + expected: "-", + }, + { + name: "http_version", + format: "powerdns-http-version", + dm: DNSMessage{PowerDNS: &PowerDNS{HTTPVersion: "HTTP2"}}, + expected: "HTTP2", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + line := tc.dm.String( + strings.Fields(tc.format), + config.Global.TextFormatDelimiter, + config.Global.TextFormatBoundary, + ) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_Directives_ATags(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + name: "undefined", + format: "atags", + dm: DNSMessage{}, + expected: "-", + }, + { + name: "empty_attributes", + format: "atags", + dm: DNSMessage{ATags: &TransformATags{}}, + expected: "-", + }, + { + name: "tags_all", + format: "atags", + dm: DNSMessage{ATags: &TransformATags{Tags: []string{"tag1", "tag2"}}}, + expected: "tag1,tag2", + }, + { + name: "tags_index", + format: "atags:1", + dm: DNSMessage{ATags: &TransformATags{Tags: []string{"tag1", "tag2"}}}, + expected: "tag2", + }, + { + name: "tags_invalid_index", + format: "atags:3", + dm: DNSMessage{ATags: &TransformATags{Tags: []string{"tag1", "tag2"}}}, + expected: "-", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + line := tc.dm.String( + strings.Fields(tc.format), + config.Global.TextFormatDelimiter, + config.Global.TextFormatBoundary, + ) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_Directives_Suspicious(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + name: "undefined", + format: "suspicious-score", + dm: DNSMessage{}, + expected: "-", + }, + { + name: "default", + format: "suspicious-score", + dm: DNSMessage{Suspicious: &TransformSuspicious{Score: 4.0}}, + expected: "4", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + line := tc.dm.String( + strings.Fields(tc.format), + config.Global.TextFormatDelimiter, + config.Global.TextFormatBoundary, + ) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_Directives_Reducer(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + name: "undefined", + format: "reducer-occurrences", + dm: DNSMessage{}, + expected: "-", + }, + { + name: "default", + format: "reducer-occurrences", + dm: DNSMessage{Reducer: &TransformReducer{Occurrences: 1}}, + expected: "1", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + line := tc.dm.String( + strings.Fields(tc.format), + config.Global.TextFormatDelimiter, + config.Global.TextFormatBoundary, + ) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_Directives_Extracted(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + name: "undefined", + format: "extracted-dns-payload", + dm: DNSMessage{}, + expected: "-", + }, + { + name: "default", + format: "extracted-dns-payload", + dm: DNSMessage{Extracted: &TransformExtracted{}, DNS: DNS{Payload: []byte{ + 0x9e, 0x84, 0x01, 0x20, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // query 1 + 0x01, 0x61, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // query 2 + 0x01, 0x62, 0x00, + // type A, class IN + 0x00, 0x01, 0x00, 0x01, + // query 3 + 0x01, 0x63, 0x00, + // type AAAA, class IN + 0x00, 0x1c, 0x00, 0x01, + }}}, + expected: "noQBIAADAAAAAAAAAWEAAAEAAQFiAAABAAEBYwAAHAAB", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + line := tc.dm.String( + strings.Fields(tc.format), + config.Global.TextFormatDelimiter, + config.Global.TextFormatBoundary, + ) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func TestDnsMessage_TextFormat_Directives_Filtering(t *testing.T) { + config := pkgconfig.GetDefaultConfig() + + testcases := []struct { + name string + format string + dm DNSMessage + expected string + }{ + { + name: "undefined", + format: "filtering-sample-rate", + dm: DNSMessage{}, + expected: "-", + }, + { + name: "default", + format: "filtering-sample-rate", + dm: DNSMessage{Filtering: &TransformFiltering{SampleRate: 22}}, + expected: "22", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + line := tc.dm.String( + strings.Fields(tc.format), + config.Global.TextFormatDelimiter, + config.Global.TextFormatBoundary, + ) + if line != tc.expected { + t.Errorf("Want: %s, got: %s", tc.expected, line) + } + }) + } +} + +func BenchmarkDnsMessage_ToTextFormat(b *testing.B) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + + textFormat := []string{"timestamp-rfc3339ns", "identity", + "operation", "rcode", "queryip", "queryport", "family", + "protocol", "length-unit", "qname", "qtype", "latency"} + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := dm.ToTextLine(textFormat, " ", "\"") + if err != nil { + b.Fatalf("could not encode to text format: %v\n", err) + } + } +} diff --git a/dnsutils/edns_parser_error_test.go b/dnsutils/edns_parser_error_test.go new file mode 100644 index 00000000..c5a13d2d --- /dev/null +++ b/dnsutils/edns_parser_error_test.go @@ -0,0 +1,159 @@ +package dnsutils + +import ( + "errors" + "testing" +) + +func TestDecodeAnswer_EdnsError(t *testing.T) { + payload := []byte{ + // header + 0xe9, 0x9d, 0x81, 0x82, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + // Additional records section + // empty name + 0x00, + // type OPT + 0x00, 0x29, + // class / UDP Payload size + 0x04, 0xd0, + // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 + 0x00, 0x00, 0x80, 0x00, + // RDLENGTH + 0x00, 0x06, + // RDATA + // CODE - Extended error + 0x00, 0x0f, + // Length + 0x00, 0x02, + // Option data + // Error code + 0x00, 0x17, + } + + _, _, _, offset, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + + edns, _, erre := DecodeEDNS(1, offset, payload) + if erre != nil { + t.Errorf("unexpected error while decoding edns: %v", erre) + } + + if edns.Do != 1 || edns.Z != 0 || edns.Version != 0 || edns.UDPSize != 1232 || edns.ExtendedRcode != 0 { + t.Errorf("invalid data in parsed EDNS header: %v", edns) + } + + if len(edns.Options) != 1 { + t.Errorf("expected one edns option to be parsed, got %d", len(edns.Options)) + } + expected := DNSOption{Code: 0x000f, Name: OptCodeToString(0x000f), Data: "23 Network Error -"} + if edns.Options[0] != expected { + t.Errorf("bad edns option, expected %v, got %v", expected, edns.Options[0]) + } + +} +func TestDecodeAnswer_EdnsErrorText(t *testing.T) { + payload := []byte{ + // header + 0xe9, 0x9d, 0x81, 0x82, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + // Additional records section + // empty name + 0x00, + // type OPT + 0x00, 0x29, + // class / UDP Payload size + 0x04, 0xd0, + // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 + 0x00, 0x00, 0x80, 0x00, + // RDLENGTH + 0x00, 0x0c, + // RDATA + // CODE - Extended error + 0x00, 0x0f, + // Length + 0x00, 0x08, + // Option data + // Error code + 0x00, 0x17, + // Error text + 0x62, 0x30, 0x72, 0x6b, 0x65, 0x6e, + } + + _, _, _, offset, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + + edns, _, erre := DecodeEDNS(1, offset, payload) + if erre != nil { + t.Errorf("unexpected error while decoding edns: %v", erre) + } + + if edns.Do != 1 || edns.Z != 0 || edns.Version != 0 || edns.UDPSize != 1232 || edns.ExtendedRcode != 0 { + t.Errorf("invalid data in parsed EDNS header: %v", edns) + } + + if len(edns.Options) != 1 { + t.Errorf("expected one edns option to be parsed, got %d", len(edns.Options)) + } + expected := DNSOption{Code: 0x000f, Name: OptCodeToString(0x000f), Data: "23 Network Error b0rken"} + if edns.Options[0] != expected { + t.Errorf("bad edns option, expected %v, got %v", expected, edns.Options[0]) + } + +} + +func TestDecodeAnswer_EdnsErrorShort(t *testing.T) { + payload := []byte{ + // header + 0xe9, 0x9d, 0x81, 0x82, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + // Additional records section + // empty name + 0x00, + // type OPT + 0x00, 0x29, + // class / UDP Payload size + 0x04, 0xd0, + // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 + 0x00, 0x00, 0x80, 0x00, + // RDLENGTH + 0x00, 0x05, + // RDATA + // CODE - Extended error + 0x00, 0x0f, + // Length + 0x00, 0x01, + // Option data + // Error code, missing byte + 0x00, + } + + _, _, _, offset, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + + _, _, erre := DecodeEDNS(1, offset, payload) + if !errors.Is(erre, ErrDecodeEdnsOptionTooShort) { + t.Errorf("bad error returned: %v", erre) + } +} diff --git a/dnsutils/edns_parser_subnet_test.go b/dnsutils/edns_parser_subnet_test.go new file mode 100644 index 00000000..33215932 --- /dev/null +++ b/dnsutils/edns_parser_subnet_test.go @@ -0,0 +1,332 @@ +package dnsutils + +import ( + "errors" + "testing" +) + +func TestDecodeQuery_EdnsSubnet(t *testing.T) { + payload := []byte{ + // header + 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + // Additional records section + // empty name + 0x00, + // type OPT + 0x00, 0x29, + // class / UDP Payload size + 0x04, 0xd0, + // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 + 0x00, 0x00, 0x80, 0x00, + // RDLENGTH + 0x00, 0x0b, + // RDATA + // CODE - Client subnet + 0x00, 0x08, + // Length + 0x00, 0x07, + // Option data + // family + 0x00, 0x01, + // prefix-len + 0x18, + // scope prefix-len + 0x00, + // address + 0xc0, 0xa8, 0x01, + } + + _, _, _, offset, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + + answer, _, erra := DecodeAnswer(1, offset, payload) + if erra != nil { + t.Errorf("unexpected error while decoding answer: %v", erra) + } + // parsing answers should skip the OPT type and not return anything from + // the additional section + if len(answer) > 0 { + t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) + } + edns, _, erre := DecodeEDNS(1, offset, payload) + if erre != nil { + t.Errorf("unexpected error when decoding EDNS: %v", erre) + } + if edns.Do != 1 || edns.Z != 0 || edns.Version != 0 || edns.UDPSize != 1232 || edns.ExtendedRcode != 0 { + t.Errorf("invalid data in parsed EDNS header: %v", edns) + } + + if len(edns.Options) != 1 { + t.Errorf("expected 1 EDNS option to be parsed, got %v", len(edns.Options)) + } + + expectedOption := DNSOption{Code: 0x0008, Name: OptCodeToString(0x0008), Data: "192.168.1.0/24"} + + if edns.Options[0] != expectedOption { + t.Errorf("bad option parsed, expected %v, got %v", expectedOption, edns.Options[0]) + } + +} +func TestDecodeQuery_EdnsSubnetV6(t *testing.T) { + payload := []byte{ + // header + 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + // Additional records section + // empty name + 0x00, + // type OPT + 0x00, 0x29, + // class / UDP Payload size + 0x04, 0xd0, + // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 + 0x00, 0x00, 0x80, 0x00, + // RDLENGTH + 0x00, 0x0b, + // RDATA + // CODE - Client subnet + 0x00, 0x08, + // Length + 0x00, 0x07, + // Option data + // family + 0x00, 0x02, + // prefix-len + 0x18, + // scope prefix-len + 0x00, + // address + 0xfe, 0x80, 0x01, + } + + _, _, _, offset, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + + answer, _, erra := DecodeAnswer(1, offset, payload) + if erra != nil { + t.Errorf("unexpected error while decoding answer: %v", erra) + } + // parsing answers should skip the OPT type and not return anything from + // the additional section + if len(answer) > 0 { + t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) + } + edns, _, erre := DecodeEDNS(1, offset, payload) + if erre != nil { + t.Errorf("unexpected error when decoding EDNS: %v", erre) + } + if edns.Do != 1 || edns.Z != 0 || edns.Version != 0 || edns.UDPSize != 1232 || edns.ExtendedRcode != 0 { + t.Errorf("invalid data in parsed EDNS header: %v", edns) + } + + if len(edns.Options) != 1 { + t.Errorf("expected 1 EDNS option to be parsed, got %v", len(edns.Options)) + } + + expectedOption := DNSOption{Code: 0x0008, Name: OptCodeToString(0x0008), Data: "[fe80:100::]/24"} + + if edns.Options[0] != expectedOption { + t.Errorf("bad option parsed, expected %v, got %v", expectedOption, edns.Options[0]) + } + +} + +func TestDecodeQuery_EdnsSubnet_invalidFam(t *testing.T) { + payload := []byte{ + // header + 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + // Additional records section + // empty name + 0x00, + // type OPT + 0x00, 0x29, + // class / UDP Payload size + 0x04, 0xd0, + // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 + 0x00, 0x00, 0x80, 0x00, + // RDLENGTH + 0x00, 0x0b, + // RDATA + // CODE - Client subnet + 0x00, 0x08, + // Length + 0x00, 0x07, + // Option data + // family + 0x00, 0xff, + // prefix-len + 0x18, + // scope prefix-len + 0x00, + // address + 0xfe, 0x80, 0x01, + } + + _, _, _, offset, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + + answer, _, erra := DecodeAnswer(1, offset, payload) + if erra != nil { + t.Errorf("unexpected error while decoding answer: %v", erra) + } + // parsing answers should skip the OPT type and not return anything from + // the additional section + if len(answer) > 0 { + t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) + } + _, _, erre := DecodeEDNS(1, offset, payload) + + if !errors.Is(erre, ErrDecodeEdnsOptionCsubnetBadFamily) { + t.Errorf("bad error returned: %v", erre) + } +} + +func TestDecodeQuery_EdnsSubnet_Short(t *testing.T) { + payload := []byte{ + // header + 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + // Additional records section + // empty name + 0x00, + // type OPT + 0x00, 0x29, + // class / UDP Payload size + 0x04, 0xd0, + // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 + 0x00, 0x00, 0x80, 0x00, + // RDLENGTH + 0x00, 0x06, + // RDATA + // CODE - Client subnet + 0x00, 0x08, + // Length + 0x00, 0x02, + // Option data + // family + 0x00, 0x01, + // prefix-len + // 0x18, + // // scope prefix-len + // 0x00, + // // address + // 0xfe, 0x80, 0x01, + } + + _, _, _, offset, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + + answer, _, erra := DecodeAnswer(1, offset, payload) + if erra != nil { + t.Errorf("unexpected error while decoding answer: %v", erra) + } + // parsing answers should skip the OPT type and not return anything from + // the additional section + if len(answer) > 0 { + t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) + } + _, _, erre := DecodeEDNS(1, offset, payload) + + if !errors.Is(erre, ErrDecodeEdnsOptionTooShort) { + t.Errorf("bad error returned: %v", erre) + } +} + +func TestDecodeQuery_EdnsSubnet_NoAddr(t *testing.T) { + payload := []byte{ + // header + 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + // query section + 0x0b, 0x73, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + // Additional records section + // empty name + 0x00, + // type OPT + 0x00, 0x29, + // class / UDP Payload size + 0x04, 0xd0, + // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 + 0x00, 0x00, 0x80, 0x00, + // RDLENGTH + 0x00, 0x08, + // RDATA + // CODE - Client subnet + 0x00, 0x08, + // Length + 0x00, 0x04, + // Option data + // family + 0x00, 0x01, + // prefix-len + 0x18, + // scope prefix-len + 0x00, + // // address + // 0xfe, 0x80, 0x01, + } + + _, _, _, offset, err := DecodeQuestion(1, payload) + if err != nil { + t.Errorf("unexpected error while decoding question: %v", err) + } + + answer, _, erra := DecodeAnswer(1, offset, payload) + if erra != nil { + t.Errorf("unexpected error while decoding answer: %v", erra) + } + // parsing answers should skip the OPT type and not return anything from + // the additional section + if len(answer) > 0 { + t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) + } + + edns, _, erre := DecodeEDNS(1, offset, payload) + if erre != nil { + t.Errorf("unexpected error while decoding EDNS: %v", erre) + } + + expected := DNSOption{Code: 0x0008, Name: OptCodeToString(0x0008), Data: "0.0.0.0/24"} + + if len(edns.Options) != 1 { + t.Errorf("expected one EDNS option, got %d", len(edns.Options)) + } + + if edns.Options[0] != expected { + t.Errorf("unexpected option parsed from EDNS, expected %v got %v", expected, edns.Options[0]) + } + +} diff --git a/dnsutils/edns_parser_test.go b/dnsutils/edns_parser_test.go index a8b57fdc..7dec3891 100644 --- a/dnsutils/edns_parser_test.go +++ b/dnsutils/edns_parser_test.go @@ -72,485 +72,6 @@ func TestDecodeReply_EDNS(t *testing.T) { } } -func TestDecodeQuery_EdnsSubnet(t *testing.T) { - payload := []byte{ - // header - 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Additional records section - // empty name - 0x00, - // type OPT - 0x00, 0x29, - // class / UDP Payload size - 0x04, 0xd0, - // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 - 0x00, 0x00, 0x80, 0x00, - // RDLENGTH - 0x00, 0x0b, - // RDATA - // CODE - Client subnet - 0x00, 0x08, - // Length - 0x00, 0x07, - // Option data - // family - 0x00, 0x01, - // prefix-len - 0x18, - // scope prefix-len - 0x00, - // address - 0xc0, 0xa8, 0x01, - } - - _, _, _, offset, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - - answer, _, erra := DecodeAnswer(1, offset, payload) - if erra != nil { - t.Errorf("unexpected error while decoding answer: %v", erra) - } - // parsing answers should skip the OPT type and not return anything from - // the additional section - if len(answer) > 0 { - t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) - } - edns, _, erre := DecodeEDNS(1, offset, payload) - if erre != nil { - t.Errorf("unexpected error when decoding EDNS: %v", erre) - } - if edns.Do != 1 || edns.Z != 0 || edns.Version != 0 || edns.UDPSize != 1232 || edns.ExtendedRcode != 0 { - t.Errorf("invalid data in parsed EDNS header: %v", edns) - } - - if len(edns.Options) != 1 { - t.Errorf("expected 1 EDNS option to be parsed, got %v", len(edns.Options)) - } - - expectedOption := DNSOption{Code: 0x0008, Name: OptCodeToString(0x0008), Data: "192.168.1.0/24"} - - if edns.Options[0] != expectedOption { - t.Errorf("bad option parsed, expected %v, got %v", expectedOption, edns.Options[0]) - } - -} -func TestDecodeQuery_EdnsSubnetV6(t *testing.T) { - payload := []byte{ - // header - 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Additional records section - // empty name - 0x00, - // type OPT - 0x00, 0x29, - // class / UDP Payload size - 0x04, 0xd0, - // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 - 0x00, 0x00, 0x80, 0x00, - // RDLENGTH - 0x00, 0x0b, - // RDATA - // CODE - Client subnet - 0x00, 0x08, - // Length - 0x00, 0x07, - // Option data - // family - 0x00, 0x02, - // prefix-len - 0x18, - // scope prefix-len - 0x00, - // address - 0xfe, 0x80, 0x01, - } - - _, _, _, offset, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - - answer, _, erra := DecodeAnswer(1, offset, payload) - if erra != nil { - t.Errorf("unexpected error while decoding answer: %v", erra) - } - // parsing answers should skip the OPT type and not return anything from - // the additional section - if len(answer) > 0 { - t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) - } - edns, _, erre := DecodeEDNS(1, offset, payload) - if erre != nil { - t.Errorf("unexpected error when decoding EDNS: %v", erre) - } - if edns.Do != 1 || edns.Z != 0 || edns.Version != 0 || edns.UDPSize != 1232 || edns.ExtendedRcode != 0 { - t.Errorf("invalid data in parsed EDNS header: %v", edns) - } - - if len(edns.Options) != 1 { - t.Errorf("expected 1 EDNS option to be parsed, got %v", len(edns.Options)) - } - - expectedOption := DNSOption{Code: 0x0008, Name: OptCodeToString(0x0008), Data: "[fe80:100::]/24"} - - if edns.Options[0] != expectedOption { - t.Errorf("bad option parsed, expected %v, got %v", expectedOption, edns.Options[0]) - } - -} - -func TestDecodeQuery_EdnsSubnet_invalidFam(t *testing.T) { - payload := []byte{ - // header - 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Additional records section - // empty name - 0x00, - // type OPT - 0x00, 0x29, - // class / UDP Payload size - 0x04, 0xd0, - // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 - 0x00, 0x00, 0x80, 0x00, - // RDLENGTH - 0x00, 0x0b, - // RDATA - // CODE - Client subnet - 0x00, 0x08, - // Length - 0x00, 0x07, - // Option data - // family - 0x00, 0xff, - // prefix-len - 0x18, - // scope prefix-len - 0x00, - // address - 0xfe, 0x80, 0x01, - } - - _, _, _, offset, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - - answer, _, erra := DecodeAnswer(1, offset, payload) - if erra != nil { - t.Errorf("unexpected error while decoding answer: %v", erra) - } - // parsing answers should skip the OPT type and not return anything from - // the additional section - if len(answer) > 0 { - t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) - } - _, _, erre := DecodeEDNS(1, offset, payload) - - if !errors.Is(erre, ErrDecodeEdnsOptionCsubnetBadFamily) { - t.Errorf("bad error returned: %v", erre) - } -} - -func TestDecodeQuery_EdnsSubnet_Short(t *testing.T) { - payload := []byte{ - // header - 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Additional records section - // empty name - 0x00, - // type OPT - 0x00, 0x29, - // class / UDP Payload size - 0x04, 0xd0, - // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 - 0x00, 0x00, 0x80, 0x00, - // RDLENGTH - 0x00, 0x06, - // RDATA - // CODE - Client subnet - 0x00, 0x08, - // Length - 0x00, 0x02, - // Option data - // family - 0x00, 0x01, - // prefix-len - // 0x18, - // // scope prefix-len - // 0x00, - // // address - // 0xfe, 0x80, 0x01, - } - - _, _, _, offset, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - - answer, _, erra := DecodeAnswer(1, offset, payload) - if erra != nil { - t.Errorf("unexpected error while decoding answer: %v", erra) - } - // parsing answers should skip the OPT type and not return anything from - // the additional section - if len(answer) > 0 { - t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) - } - _, _, erre := DecodeEDNS(1, offset, payload) - - if !errors.Is(erre, ErrDecodeEdnsOptionTooShort) { - t.Errorf("bad error returned: %v", erre) - } -} - -func TestDecodeQuery_EdnsSubnet_NoAddr(t *testing.T) { - payload := []byte{ - // header - 0xe9, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Additional records section - // empty name - 0x00, - // type OPT - 0x00, 0x29, - // class / UDP Payload size - 0x04, 0xd0, - // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 - 0x00, 0x00, 0x80, 0x00, - // RDLENGTH - 0x00, 0x08, - // RDATA - // CODE - Client subnet - 0x00, 0x08, - // Length - 0x00, 0x04, - // Option data - // family - 0x00, 0x01, - // prefix-len - 0x18, - // scope prefix-len - 0x00, - // // address - // 0xfe, 0x80, 0x01, - } - - _, _, _, offset, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - - answer, _, erra := DecodeAnswer(1, offset, payload) - if erra != nil { - t.Errorf("unexpected error while decoding answer: %v", erra) - } - // parsing answers should skip the OPT type and not return anything from - // the additional section - if len(answer) > 0 { - t.Errorf("did not expect any answers to be parsed, got %d", len(answer)) - } - - edns, _, erre := DecodeEDNS(1, offset, payload) - if erre != nil { - t.Errorf("unexpected error while decoding EDNS: %v", erre) - } - - expected := DNSOption{Code: 0x0008, Name: OptCodeToString(0x0008), Data: "0.0.0.0/24"} - - if len(edns.Options) != 1 { - t.Errorf("expected one EDNS option, got %d", len(edns.Options)) - } - - if edns.Options[0] != expected { - t.Errorf("unexpected option parsed from EDNS, expected %v got %v", expected, edns.Options[0]) - } - -} - -func TestDecodeAnswer_EdnsError(t *testing.T) { - payload := []byte{ - // header - 0xe9, 0x9d, 0x81, 0x82, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Additional records section - // empty name - 0x00, - // type OPT - 0x00, 0x29, - // class / UDP Payload size - 0x04, 0xd0, - // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 - 0x00, 0x00, 0x80, 0x00, - // RDLENGTH - 0x00, 0x06, - // RDATA - // CODE - Extended error - 0x00, 0x0f, - // Length - 0x00, 0x02, - // Option data - // Error code - 0x00, 0x17, - } - - _, _, _, offset, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - - edns, _, erre := DecodeEDNS(1, offset, payload) - if erre != nil { - t.Errorf("unexpected error while decoding edns: %v", erre) - } - - if edns.Do != 1 || edns.Z != 0 || edns.Version != 0 || edns.UDPSize != 1232 || edns.ExtendedRcode != 0 { - t.Errorf("invalid data in parsed EDNS header: %v", edns) - } - - if len(edns.Options) != 1 { - t.Errorf("expected one edns option to be parsed, got %d", len(edns.Options)) - } - expected := DNSOption{Code: 0x000f, Name: OptCodeToString(0x000f), Data: "23 Network Error -"} - if edns.Options[0] != expected { - t.Errorf("bad edns option, expected %v, got %v", expected, edns.Options[0]) - } - -} -func TestDecodeAnswer_EdnsErrorText(t *testing.T) { - payload := []byte{ - // header - 0xe9, 0x9d, 0x81, 0x82, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Additional records section - // empty name - 0x00, - // type OPT - 0x00, 0x29, - // class / UDP Payload size - 0x04, 0xd0, - // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 - 0x00, 0x00, 0x80, 0x00, - // RDLENGTH - 0x00, 0x0c, - // RDATA - // CODE - Extended error - 0x00, 0x0f, - // Length - 0x00, 0x08, - // Option data - // Error code - 0x00, 0x17, - // Error text - 0x62, 0x30, 0x72, 0x6b, 0x65, 0x6e, - } - - _, _, _, offset, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - - edns, _, erre := DecodeEDNS(1, offset, payload) - if erre != nil { - t.Errorf("unexpected error while decoding edns: %v", erre) - } - - if edns.Do != 1 || edns.Z != 0 || edns.Version != 0 || edns.UDPSize != 1232 || edns.ExtendedRcode != 0 { - t.Errorf("invalid data in parsed EDNS header: %v", edns) - } - - if len(edns.Options) != 1 { - t.Errorf("expected one edns option to be parsed, got %d", len(edns.Options)) - } - expected := DNSOption{Code: 0x000f, Name: OptCodeToString(0x000f), Data: "23 Network Error b0rken"} - if edns.Options[0] != expected { - t.Errorf("bad edns option, expected %v, got %v", expected, edns.Options[0]) - } - -} - -func TestDecodeAnswer_EdnsErrorShort(t *testing.T) { - payload := []byte{ - // header - 0xe9, 0x9d, 0x81, 0x82, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, - // query section - 0x0b, 0x73, 0x65, 0x6e, - 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x03, 0x63, 0x6f, 0x6d, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Additional records section - // empty name - 0x00, - // type OPT - 0x00, 0x29, - // class / UDP Payload size - 0x04, 0xd0, - // TTL / EXT-RCODE=0, VERSION=0, DO=1, Z=0 - 0x00, 0x00, 0x80, 0x00, - // RDLENGTH - 0x00, 0x05, - // RDATA - // CODE - Extended error - 0x00, 0x0f, - // Length - 0x00, 0x01, - // Option data - // Error code, missing byte - 0x00, - } - - _, _, _, offset, err := DecodeQuestion(1, payload) - if err != nil { - t.Errorf("unexpected error while decoding question: %v", err) - } - - _, _, erre := DecodeEDNS(1, offset, payload) - if !errors.Is(erre, ErrDecodeEdnsOptionTooShort) { - t.Errorf("bad error returned: %v", erre) - } -} - func TestDecodeEdns_Short(t *testing.T) { testData := []struct { name string diff --git a/dnsutils/helper.go b/dnsutils/helper.go new file mode 100644 index 00000000..5a929401 --- /dev/null +++ b/dnsutils/helper.go @@ -0,0 +1,131 @@ +package dnsutils + +import ( + "fmt" + "strconv" + "strings" + + "github.com/dmachard/go-dnscollector/pkgconfig" + "github.com/dmachard/go-netutils" + "github.com/miekg/dns" +) + +func GetFakeDNS() ([]byte, error) { + dnsmsg := new(dns.Msg) + dnsmsg.SetQuestion("dns.collector.", dns.TypeA) + return dnsmsg.Pack() +} + +func GetFakeDNSMessage() DNSMessage { + dm := DNSMessage{} + dm.Init() + dm.DNSTap.Identity = "collector" + dm.DNSTap.Version = "dnscollector 1.0.0" + dm.DNSTap.Operation = "CLIENT_QUERY" + dm.DNSTap.PeerName = "localhost (127.0.0.1)" + dm.DNS.Type = DNSQuery + dm.DNS.Qname = pkgconfig.ProgQname + dm.NetworkInfo.QueryIP = "1.2.3.4" + dm.NetworkInfo.QueryPort = "1234" + dm.NetworkInfo.ResponseIP = "4.3.2.1" + dm.NetworkInfo.ResponsePort = "4321" + dm.DNS.Rcode = "NOERROR" + dm.DNS.Qtype = "A" + return dm +} + +func GetFakeDNSMessageWithPayload() DNSMessage { + // fake dns query payload + dnsmsg := new(dns.Msg) + dnsmsg.SetQuestion("dnscollector.dev.", dns.TypeAAAA) + dnsquestion, _ := dnsmsg.Pack() + + dm := GetFakeDNSMessage() + dm.NetworkInfo.Family = netutils.ProtoIPv4 + dm.NetworkInfo.Protocol = netutils.ProtoUDP + dm.DNS.Payload = dnsquestion + dm.DNS.Length = len(dnsquestion) + return dm +} + +func GetFlatDNSMessage() (ret map[string]interface{}, err error) { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + ret, err = dm.Flatten() + return +} + +func GetReferenceDNSMessage() DNSMessage { + dm := DNSMessage{} + dm.Init() + dm.InitTransforms() + return dm +} + +func GetIPPort(dm *DNSMessage) (string, int, string, int) { + srcIP, srcPort := "0.0.0.0", 53 + dstIP, dstPort := "0.0.0.0", 53 + if dm.NetworkInfo.Family == "INET6" { + srcIP, dstIP = "::", "::" + } + + if dm.NetworkInfo.QueryIP != "-" { + srcIP = dm.NetworkInfo.QueryIP + srcPort, _ = strconv.Atoi(dm.NetworkInfo.QueryPort) + } + if dm.NetworkInfo.ResponseIP != "-" { + dstIP = dm.NetworkInfo.ResponseIP + dstPort, _ = strconv.Atoi(dm.NetworkInfo.ResponsePort) + } + + // reverse destination and source + if dm.DNS.Type == DNSReply { + srcIPTmp, srcPortTmp := srcIP, srcPort + srcIP, srcPort = dstIP, dstPort + dstIP, dstPort = srcIPTmp, srcPortTmp + } + return srcIP, srcPort, dstIP, dstPort +} + +func ConvertToString(value interface{}) string { + switch v := value.(type) { + case int: + return strconv.Itoa(v) + case bool: + return strconv.FormatBool(v) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case string: + return v + default: + return fmt.Sprintf("%v", v) + } +} + +func QuoteStringAndWrite(s *strings.Builder, fieldString, fieldDelimiter, fieldBoundary string) { + // Handle the case where the field string is empty and boundaries are specified + if fieldString == "" && len(fieldBoundary) > 0 { + s.WriteString(fmt.Sprintf("%s%s%s", fieldBoundary, fieldString, fieldBoundary)) + return + } + + switch { + case len(fieldDelimiter) > 0 && strings.Contains(fieldString, fieldDelimiter): + // Case where the field string contains the delimiter + fieldEscaped := fieldString + if len(fieldBoundary) > 0 && strings.Contains(fieldEscaped, fieldBoundary) { + fieldEscaped = strings.ReplaceAll(fieldEscaped, fieldBoundary, "\\"+fieldBoundary) + } + s.WriteString(fmt.Sprintf("%s%s%s", fieldBoundary, fieldEscaped, fieldBoundary)) + + case len(fieldBoundary) > 0 && strings.Contains(fieldString, fieldBoundary): + // Case where the field string contains the boundary character + fieldEscaped := strings.ReplaceAll(fieldString, fieldBoundary, "\\"+fieldBoundary) + s.WriteString(fmt.Sprintf("%s%s%s", fieldBoundary, fieldEscaped, fieldBoundary)) + + default: + // Default case: simply write the field string as is + s.WriteString(fieldString) + } +} diff --git a/dnsutils/helper_test.go b/dnsutils/helper_test.go new file mode 100644 index 00000000..d4e2c728 --- /dev/null +++ b/dnsutils/helper_test.go @@ -0,0 +1,221 @@ +package dnsutils + +import ( + "strings" + "testing" +) + +func TestHelper_GetIPPort(t *testing.T) { + tests := []struct { + name string + dm *DNSMessage + wantSrcIP string + wantSrcPort int + wantDstIP string + wantDstPort int + }{ + { + name: "Test IPv4 source and destination", + dm: &DNSMessage{ + NetworkInfo: DNSNetInfo{ + Family: "INET", + QueryIP: "192.168.1.1", QueryPort: "1234", + ResponseIP: "192.168.1.2", ResponsePort: "5678", + }, + DNS: DNS{Type: DNSQuery}, + }, + wantSrcIP: "192.168.1.1", + wantSrcPort: 1234, + wantDstIP: "192.168.1.2", + wantDstPort: 5678, + }, + { + name: "Test IPv6 source and destination", + dm: &DNSMessage{ + NetworkInfo: DNSNetInfo{ + Family: "INET6", + QueryIP: "::1", QueryPort: "1234", + ResponseIP: "::2", ResponsePort: "5678", + }, + DNS: DNS{Type: DNSQuery}, + }, + wantSrcIP: "::1", + wantSrcPort: 1234, + wantDstIP: "::2", + wantDstPort: 5678, + }, + { + name: "Test DNSReply type", + dm: &DNSMessage{ + NetworkInfo: DNSNetInfo{ + Family: "INET", + QueryIP: "192.168.1.1", QueryPort: "1234", + ResponseIP: "192.168.1.2", ResponsePort: "5678", + }, + DNS: DNS{Type: DNSReply}, + }, + wantSrcIP: "192.168.1.2", + wantSrcPort: 5678, + wantDstIP: "192.168.1.1", + wantDstPort: 1234, + }, + { + name: "Test missing QueryIP and ResponseIP", + dm: &DNSMessage{ + NetworkInfo: DNSNetInfo{ + Family: "INET", + QueryIP: "-", QueryPort: "-", + ResponseIP: "-", ResponsePort: "-", + }, + DNS: DNS{Type: DNSQuery}, + }, + wantSrcIP: "0.0.0.0", + wantSrcPort: 53, + wantDstIP: "0.0.0.0", + wantDstPort: 53, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srcIP, srcPort, dstIP, dstPort := GetIPPort(tt.dm) + if srcIP != tt.wantSrcIP { + t.Errorf("GetIPPort() srcIP = %v, want %v", srcIP, tt.wantSrcIP) + } + if srcPort != tt.wantSrcPort { + t.Errorf("GetIPPort() srcPort = %v, want %v", srcPort, tt.wantSrcPort) + } + if dstIP != tt.wantDstIP { + t.Errorf("GetIPPort() dstIP = %v, want %v", dstIP, tt.wantDstIP) + } + if dstPort != tt.wantDstPort { + t.Errorf("GetIPPort() dstPort = %v, want %v", dstPort, tt.wantDstPort) + } + }) + } +} + +func TestHelper_ConvertToString(t *testing.T) { + tests := []struct { + name string + input interface{} + expected string + }{ + { + name: "Test int", + input: 42, + expected: "42", + }, + { + name: "Test bool true", + input: true, + expected: "true", + }, + { + name: "Test bool false", + input: false, + expected: "false", + }, + { + name: "Test float64", + input: 3.14159, + expected: "3.14159", + }, + { + name: "Test string", + input: "hello", + expected: "hello", + }, + { + name: "Test unknown type", + input: struct{ Name string }{Name: "example"}, + expected: "{example}", + }, + { + name: "Test nil", + input: nil, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConvertToString(tt.input) + if result != tt.expected { + t.Errorf("convertToString() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestHelper_QuoteStringAndWrite(t *testing.T) { + tests := []struct { + name string + fieldString string + fieldDelimiter string + fieldBoundary string + expected string + }{ + { + name: "No delimiter, no boundary", + fieldString: "simpleString", + fieldDelimiter: "", + fieldBoundary: "", + expected: "simpleString", + }, + { + name: "Contains delimiter, no boundary", + fieldString: "string,with,commas", + fieldDelimiter: ",", + fieldBoundary: "", + expected: "string,with,commas", + }, + { + name: "Contains delimiter, with boundary", + fieldString: "string,with,commas", + fieldDelimiter: ",", + fieldBoundary: "\"", + expected: "\"string,with,commas\"", + }, + { + name: "Contains boundary, with delimiter", + fieldString: "string \"with\" quotes", + fieldDelimiter: ",", + fieldBoundary: "\"", + expected: "\"string \\\"with\\\" quotes\"", + }, + { + name: "Contains both delimiter and boundary", + fieldString: "string, \"with\" everything", + fieldDelimiter: ",", + fieldBoundary: "\"", + expected: "\"string, \\\"with\\\" everything\"", + }, + { + name: "Empty string with delimiter and boundary", + fieldString: "", + fieldDelimiter: ",", + fieldBoundary: "\"", + expected: "\"\"", + }, + { + name: "No delimiter, with boundary", + fieldString: "simpleString", + fieldDelimiter: "", + fieldBoundary: "\"", + expected: "simpleString", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var builder strings.Builder + QuoteStringAndWrite(&builder, tt.fieldString, tt.fieldDelimiter, tt.fieldBoundary) + result := builder.String() + + if result != tt.expected { + t.Errorf("quoteStringAndWrite() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/dnsutils/message_test.go b/dnsutils/message_test.go deleted file mode 100644 index 8d9e72ad..00000000 --- a/dnsutils/message_test.go +++ /dev/null @@ -1,1654 +0,0 @@ -package dnsutils - -import ( - "encoding/json" - "reflect" - "regexp" - "strings" - "testing" - - "github.com/dmachard/go-dnscollector/pkgconfig" - "github.com/dmachard/go-dnstap-protobuf" - "github.com/dmachard/go-netutils" - "github.com/miekg/dns" - "google.golang.org/protobuf/proto" -) - -// Bench to init DNS message -func BenchmarkDnsMessage_Init(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - } -} - -// Tests for DNSTap format -func encodeToDNSTap(dm DNSMessage, t *testing.T) *ExtendedDnstap { - // encode to extended dnstap - tapMsg, err := dm.ToDNSTap(true) - if err != nil { - t.Fatalf("could not encode to extended dnstap: %v\n", err) - } - - // decode dnstap message - dt := &dnstap.Dnstap{} - err = proto.Unmarshal(tapMsg, dt) - if err != nil { - t.Fatalf("error to decode dnstap: %v", err) - } - - // decode extended part - edt := &ExtendedDnstap{} - err = proto.Unmarshal(dt.GetExtra(), edt) - if err != nil { - t.Fatalf("error to decode extended dnstap: %v", err) - } - return edt -} - -func TestDnsMessage_ToDNSTap(t *testing.T) { - dm := GetFakeDNSMessageWithPayload() - dm.DNSTap.Extra = "extra:value" - - // encode to dnstap - tapMsg, err := dm.ToDNSTap(false) - if err != nil { - t.Fatalf("could not encode to dnstap: %v\n", err) - } - - // decode dnstap message - dt := &dnstap.Dnstap{} - err = proto.Unmarshal(tapMsg, dt) - if err != nil { - t.Fatalf("error to decode dnstap: %v", err) - } - - if string(dt.GetIdentity()) != dm.DNSTap.Identity { - t.Errorf("identify field should be equal got=%s", string(dt.GetIdentity())) - } - - if string(dt.GetExtra()) != dm.DNSTap.Extra { - t.Errorf("extra field should be equal got=%s", string(dt.GetExtra())) - } -} - -func BenchmarkDnsMessage_ToDNSTap(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := dm.ToDNSTap(false) - if err != nil { - b.Fatalf("could not encode to dnstap: %v\n", err) - } - } -} - -// Tests for Extended DNSTap format -func TestDnsMessage_ToExtendedDNSTap_GetOriginalDnstapExtra(t *testing.T) { - dm := GetFakeDNSMessageWithPayload() - dm.DNSTap.Extra = "tag0:value0" - - // encode to DNSTap and decode extended - edt := encodeToDNSTap(dm, t) - - // check - if string(edt.GetOriginalDnstapExtra()) != dm.DNSTap.Extra { - t.Errorf("extra field should be equal to the original value") - } -} - -func TestDnsMessage_ToExtendedDNSTap_TransformAtags(t *testing.T) { - dm := GetFakeDNSMessageWithPayload() - dm.ATags = &TransformATags{ - Tags: []string{"tag1:value1"}, - } - - // encode to DNSTap and decode extended - edt := encodeToDNSTap(dm, t) - - // check - if edt.GetAtags().Tags[0] != "tag1:value1" { - t.Errorf("invalid value on atags") - } -} - -func TestDnsMessage_ToExtendedDNSTap_TransformNormalize(t *testing.T) { - dm := GetFakeDNSMessageWithPayload() - dm.PublicSuffix = &TransformPublicSuffix{ - QnamePublicSuffix: "com", - QnameEffectiveTLDPlusOne: "dnscollector.com", - } - - // encode to DNSTap and decode extended - edt := encodeToDNSTap(dm, t) - - // checks - if edt.GetNormalize().GetTld() != "com" { - t.Errorf("invalid value on tld") - } - - if edt.GetNormalize().GetEtldPlusOne() != "dnscollector.com" { - t.Errorf("invalid value on etld+1") - } -} - -func TestDnsMessage_ToExtendedDNSTap_TransformFiltering(t *testing.T) { - dm := GetFakeDNSMessageWithPayload() - dm.Filtering = &TransformFiltering{ - SampleRate: 20, - } - - // encode to DNSTap and decode extended - edt := encodeToDNSTap(dm, t) - - // checks - if edt.GetFiltering().GetSampleRate() != 20 { - t.Errorf("invalid value sample rate") - } -} - -func TestDnsMessage_ToExtendedDNSTap_TransformGeo(t *testing.T) { - dm := GetFakeDNSMessageWithPayload() - dm.Geo = &TransformDNSGeo{ - City: "France", - Continent: "Europe", - CountryIsoCode: "44444", - AutonomousSystemNumber: "3333", - AutonomousSystemOrg: "Test", - } - - // encode to DNSTap and decode extended - edt := encodeToDNSTap(dm, t) - - // checks - if edt.GetGeo().GetCity() != "France" { - t.Errorf("invalid value for city") - } - if edt.GetGeo().GetContinent() != "Europe" { - t.Errorf("invalid value for continent") - } -} - -func BenchmarkDnsMessage_ToExtendedDNSTap(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := dm.ToDNSTap(true) - if err != nil { - b.Fatalf("could not encode to extended dnstap: %v\n", err) - } - } -} - -// Tests for JSON format -func TestDnsMessage_Json_Reference(t *testing.T) { - dm := DNSMessage{} - dm.Init() - - refJSON := ` - { - "network": { - "family": "-", - "protocol": "-", - "query-ip": "-", - "query-port": "-", - "response-ip": "-", - "response-port": "-", - "ip-defragmented": false, - "tcp-reassembled": false - }, - "dns": { - "id": 0, - "length": 0, - "opcode": 0, - "rcode": "-", - "qname": "-", - "qtype": "-", - "qclass": "-", - "questions-count": 0, - "flags": { - "qr": false, - "tc": false, - "aa": false, - "ra": false, - "ad": false, - "rd": false, - "cd": false - }, - "resource-records": { - "an": [], - "ns": [], - "ar": [] - }, - "malformed-packet": false - }, - "edns": { - "udp-size": 0, - "rcode": 0, - "version": 0, - "dnssec-ok": 0, - "options": [] - }, - "dnstap": { - "operation": "-", - "identity": "-", - "version": "-", - "timestamp-rfc3339ns": "-", - "latency": 0, - "extra": "-", - "policy-type": "-", - "policy-action": "-", - "policy-match": "-", - "policy-value": "-", - "policy-rule": "-", - "peer-name": "-", - "query-zone": "-" - } - } - ` - - var dmMap map[string]interface{} - err := json.Unmarshal([]byte(dm.ToJSON()), &dmMap) - if err != nil { - t.Fatalf("could not unmarshal dm json: %s\n", err) - } - - var refMap map[string]interface{} - err = json.Unmarshal([]byte(refJSON), &refMap) - if err != nil { - t.Fatalf("could not unmarshal ref json: %s\n", err) - } - - if !reflect.DeepEqual(dmMap, refMap) { - t.Errorf("json format different from reference %v", dmMap) - } -} - -func TestDnsMessage_Json_Collectors_Reference(t *testing.T) { - testcases := []struct { - collector string - dmRef DNSMessage - jsonRef string - }{ - { - collector: "powerdns", - dmRef: DNSMessage{PowerDNS: &PowerDNS{ - OriginalRequestSubnet: "subnet", - AppliedPolicy: "basicrpz", - AppliedPolicyHit: "hit", - AppliedPolicyKind: "kind", - AppliedPolicyTrigger: "trigger", - AppliedPolicyType: "type", - Tags: []string{"tag1"}, - Metadata: map[string]string{"stream_id": "collector"}, - HTTPVersion: "http3", - }}, - - jsonRef: `{ - "powerdns": { - "original-request-subnet": "subnet", - "applied-policy": "basicrpz", - "applied-policy-hit": "hit", - "applied-policy-kind": "kind", - "applied-policy-trigger": "trigger", - "applied-policy-type": "type", - "tags": ["tag1"], - "metadata": { - "stream_id": "collector" - }, - "http-version": "http3" - } - }`, - }, - } - for _, tc := range testcases { - t.Run(tc.collector, func(t *testing.T) { - - tc.dmRef.Init() - - var dmMap map[string]interface{} - err := json.Unmarshal([]byte(tc.dmRef.ToJSON()), &dmMap) - if err != nil { - t.Fatalf("could not unmarshal dm json: %s\n", err) - } - - var refMap map[string]interface{} - err = json.Unmarshal([]byte(tc.jsonRef), &refMap) - if err != nil { - t.Fatalf("could not unmarshal ref json: %s\n", err) - } - - if !reflect.DeepEqual(dmMap[tc.collector], refMap[tc.collector]) { - t.Errorf("json format different from reference, Get=%s Want=%s", dmMap[tc.collector], refMap[tc.collector]) - } - }) - } -} - -func TestDnsMessage_Json_Transforms_Reference(t *testing.T) { - - testcases := []struct { - transform string - dmRef DNSMessage - jsonRef string - }{ - { - transform: "filtering", - dmRef: DNSMessage{Filtering: &TransformFiltering{SampleRate: 22}}, - jsonRef: `{ - "filtering": { - "sample-rate": 22 - } - }`, - }, - { - transform: "reducer", - dmRef: DNSMessage{Reducer: &TransformReducer{Occurrences: 10, CumulativeLength: 47}}, - jsonRef: `{ - "reducer": { - "occurrences": 10, - "cumulative-length": 47 - } - }`, - }, - { - transform: "normalize", - dmRef: DNSMessage{ - PublicSuffix: &TransformPublicSuffix{ - QnamePublicSuffix: "com", - QnameEffectiveTLDPlusOne: "hello.com", - ManagedByICANN: true, - }, - }, - jsonRef: `{ - "publicsuffix": { - "tld": "com", - "etld+1": "hello.com", - "managed-icann": true - } - }`, - }, - { - transform: "geoip", - dmRef: DNSMessage{ - Geo: &TransformDNSGeo{ - City: "Paris", - Continent: "Europe", - CountryIsoCode: "FR", - AutonomousSystemNumber: "1234", - AutonomousSystemOrg: "Internet", - }, - }, - jsonRef: `{ - "geoip": { - "city": "Paris", - "continent": "Europe", - "country-isocode": "FR", - "as-number": "1234", - "as-owner": "Internet" - } - }`, - }, - { - transform: "atags", - dmRef: DNSMessage{ATags: &TransformATags{Tags: []string{"test0", "test1"}}}, - jsonRef: `{ - "atags": { - "tags": [ "test0", "test1" ] - } - }`, - }, - } - - for _, tc := range testcases { - t.Run(tc.transform, func(t *testing.T) { - - tc.dmRef.Init() - - var dmMap map[string]interface{} - err := json.Unmarshal([]byte(tc.dmRef.ToJSON()), &dmMap) - if err != nil { - t.Fatalf("could not unmarshal dm json: %s\n", err) - } - - var refMap map[string]interface{} - err = json.Unmarshal([]byte(tc.jsonRef), &refMap) - if err != nil { - t.Fatalf("could not unmarshal ref json: %s\n", err) - } - - if !reflect.DeepEqual(dmMap[tc.transform], refMap[tc.transform]) { - t.Errorf("json format different from reference, Get=%s Want=%s", dmMap[tc.transform], refMap[tc.transform]) - } - }) - } -} - -func BenchmarkDnsMessage_ToJSON(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - dm.ToJSON() - } -} - -// Tests for Flat JSON format -func TestDnsMessage_JsonFlatten_Reference(t *testing.T) { - dm := DNSMessage{} - dm.Init() - - // add some items in slices field - dm.DNS.DNSRRs.Answers = append(dm.DNS.DNSRRs.Answers, DNSAnswer{Name: "google.nl", Rdata: "142.251.39.99", Rdatatype: "A", TTL: 300, Class: "IN"}) - dm.EDNS.Options = append(dm.EDNS.Options, DNSOption{Code: 10, Data: "aaaabbbbcccc", Name: "COOKIE"}) - - refJSON := ` - { - "dns.flags.aa": false, - "dns.flags.ad": false, - "dns.flags.qr": false, - "dns.flags.ra": false, - "dns.flags.tc": false, - "dns.flags.rd": false, - "dns.flags.cd": false, - "dns.length": 0, - "dns.malformed-packet": false, - "dns.id": 0, - "dns.opcode": 0, - "dns.qname": "-", - "dns.qtype": "-", - "dns.rcode": "-", - "dns.qclass": "-", - "dns.questions-count": 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", - "dns.resource-records.an.0.ttl": 300, - "dns.resource-records.an.0.class": "IN", - "dns.resource-records.ar": "-", - "dns.resource-records.ns": "-", - "dnstap.identity": "-", - "dnstap.latency": 0, - "dnstap.operation": "-", - "dnstap.timestamp-rfc3339ns": "-", - "dnstap.version": "-", - "dnstap.extra": "-", - "dnstap.policy-rule": "-", - "dnstap.policy-type": "-", - "dnstap.policy-action": "-", - "dnstap.policy-match": "-", - "dnstap.policy-value": "-", - "dnstap.peer-name": "-", - "dnstap.query-zone": "-", - "edns.dnssec-ok": 0, - "edns.options.0.code": 10, - "edns.options.0.data": "aaaabbbbcccc", - "edns.options.0.name": "COOKIE", - "edns.rcode": 0, - "edns.udp-size": 0, - "edns.version": 0, - "network.family": "-", - "network.ip-defragmented": false, - "network.protocol": "-", - "network.query-ip": "-", - "network.query-port": "-", - "network.response-ip": "-", - "network.response-port": "-", - "network.tcp-reassembled": false - } - ` - - var dmFlat map[string]interface{} - dmJSON, err := dm.ToFlatJSON() - if err != nil { - t.Fatalf("could not convert dm to flat json: %s\n", err) - } - err = json.Unmarshal([]byte(dmJSON), &dmFlat) - if err != nil { - t.Fatalf("could not unmarshal dm json: %s\n", err) - } - - var refMap map[string]interface{} - err = json.Unmarshal([]byte(refJSON), &refMap) - if err != nil { - t.Fatalf("could not unmarshal ref json: %s\n", err) - } - - for k, vRef := range refMap { - vFlat, ok := dmFlat[k] - if !ok { - t.Fatalf("Missing key %s in flatten message according to reference", k) - } - if vRef != vFlat { - t.Errorf("Invalid value for key=%s get=%v expected=%v", k, vFlat, vRef) - } - } - - for k := range dmFlat { - _, ok := refMap[k] - if !ok { - t.Errorf("This key %s should not be in the flat message", k) - } - } -} - -func TestDnsMessage_JsonFlatten_Transforms_Reference(t *testing.T) { - - testcases := []struct { - transform string - dm DNSMessage - jsonRef string - }{ - { - transform: "filtering", - dm: DNSMessage{Filtering: &TransformFiltering{SampleRate: 22}}, - jsonRef: `{ - "filtering.sample-rate": 22 - }`, - }, - { - transform: "reducer", - dm: DNSMessage{Reducer: &TransformReducer{Occurrences: 10, CumulativeLength: 47}}, - jsonRef: `{ - "reducer.occurrences": 10, - "reducer.cumulative-length": 47 - }`, - }, - { - transform: "publixsuffix", - dm: DNSMessage{ - PublicSuffix: &TransformPublicSuffix{ - QnamePublicSuffix: "com", - QnameEffectiveTLDPlusOne: "hello.com", - }, - }, - jsonRef: `{ - "publicsuffix.tld": "com", - "publicsuffix.etld+1": "hello.com" - }`, - }, - { - transform: "geoip", - dm: DNSMessage{ - Geo: &TransformDNSGeo{ - City: "Paris", - Continent: "Europe", - CountryIsoCode: "FR", - AutonomousSystemNumber: "1234", - AutonomousSystemOrg: "Internet", - }, - }, - jsonRef: `{ - "geoip.city": "Paris", - "geoip.continent": "Europe", - "geoip.country-isocode": "FR", - "geoip.as-number": "1234", - "geoip.as-owner": "Internet" - }`, - }, - { - transform: "suspicious", - dm: DNSMessage{Suspicious: &TransformSuspicious{Score: 1.0, - MalformedPacket: false, - LargePacket: true, - LongDomain: true, - SlowDomain: false, - UnallowedChars: true, - UncommonQtypes: false, - ExcessiveNumberLabels: true, - Domain: "gogle.co", - }}, - jsonRef: `{ - "suspicious.score": 1.0, - "suspicious.malformed-pkt": false, - "suspicious.large-pkt": true, - "suspicious.long-domain": true, - "suspicious.slow-domain": false, - "suspicious.unallowed-chars": true, - "suspicious.uncommon-qtypes": false, - "suspicious.excessive-number-labels": true, - "suspicious.domain": "gogle.co" - }`, - }, - { - transform: "extracted", - dm: DNSMessage{Extracted: &TransformExtracted{Base64Payload: []byte{}}}, - jsonRef: `{ - "extracted.dns_payload": "" - }`, - }, - { - transform: "machinelearning", - dm: DNSMessage{MachineLearning: &TransformML{ - Entropy: 10.0, - Length: 2, - Labels: 2, - Digits: 1, - Lowers: 35, - Uppers: 23, - Specials: 2, - Others: 1, - RatioDigits: 1.0, - RatioLetters: 1.0, - RatioSpecials: 1.0, - RatioOthers: 1.0, - ConsecutiveChars: 10, - ConsecutiveVowels: 10, - ConsecutiveDigits: 10, - ConsecutiveConsonants: 10, - Size: 11, - Occurrences: 10, - UncommonQtypes: 1, - }}, - jsonRef: `{ - "ml.entropy": 10.0, - "ml.length": 2, - "ml.labels": 2, - "ml.digits": 1, - "ml.lowers": 35, - "ml.uppers": 23, - "ml.specials": 2, - "ml.others": 1, - "ml.ratio-digits": 1.0, - "ml.ratio-letters": 1.0, - "ml.ratio-specials": 1.0, - "ml.ratio-others": 1.0, - "ml.consecutive-chars": 10, - "ml.consecutive-vowels": 10, - "ml.consecutive-digits": 10, - "ml.consecutive-consonants": 10, - "ml.size": 11, - "ml.occurrences": 10, - "ml.uncommon-qtypes": 1 - }`, - }, - { - transform: "atags", - dm: DNSMessage{ATags: &TransformATags{Tags: []string{"test0", "test1"}}}, - jsonRef: `{ - "atags.tags.0": "test0", - "atags.tags.1": "test1" - }`, - }, - } - - for _, tc := range testcases { - t.Run(tc.transform, func(t *testing.T) { - - tc.dm.Init() - - var dmFlat map[string]interface{} - dmJSON, err := tc.dm.ToFlatJSON() - if err != nil { - t.Fatalf("could not convert dm to flat json: %s\n", err) - } - err = json.Unmarshal([]byte(dmJSON), &dmFlat) - if err != nil { - t.Fatalf("could not unmarshal dm json: %s\n", err) - } - - var refMap map[string]interface{} - err = json.Unmarshal([]byte(tc.jsonRef), &refMap) - if err != nil { - t.Fatalf("could not unmarshal ref json: %s\n", err) - } - - for k, vRef := range refMap { - vFlat, ok := dmFlat[k] - if !ok { - t.Fatalf("Missing key %s in flatten message according to reference", k) - } - if vRef != vFlat { - t.Errorf("Invalid value for key=%s get=%v expected=%v", k, vFlat, vRef) - } - } - }) - } -} - -func TestDnsMessage_JsonFlatten_Collectors_Reference(t *testing.T) { - testcases := []struct { - collector string - dm DNSMessage - jsonRef string - }{ - { - collector: "powerdns", - dm: DNSMessage{PowerDNS: &PowerDNS{ - OriginalRequestSubnet: "subnet", - AppliedPolicy: "basicrpz", - AppliedPolicyHit: "hit", - AppliedPolicyKind: "kind", - AppliedPolicyTrigger: "trigger", - AppliedPolicyType: "type", - Tags: []string{"tag1"}, - Metadata: map[string]string{"stream_id": "collector"}, - HTTPVersion: "http3", - }}, - - jsonRef: `{ - "powerdns.original-request-subnet": "subnet", - "powerdns.applied-policy": "basicrpz", - "powerdns.applied-policy-hit": "hit", - "powerdns.applied-policy-kind": "kind", - "powerdns.applied-policy-trigger": "trigger", - "powerdns.applied-policy-type": "type", - "powerdns.tags.0": "tag1", - "powerdns.metadata.stream_id": "collector", - "powerdns.http-version": "http3" - }`, - }, - } - for _, tc := range testcases { - t.Run(tc.collector, func(t *testing.T) { - - tc.dm.Init() - - var dmFlat map[string]interface{} - dmJSON, err := tc.dm.ToFlatJSON() - if err != nil { - t.Fatalf("could not convert dm to flat json: %s\n", err) - } - err = json.Unmarshal([]byte(dmJSON), &dmFlat) - if err != nil { - t.Fatalf("could not unmarshal dm json: %s\n", err) - } - - var refMap map[string]interface{} - err = json.Unmarshal([]byte(tc.jsonRef), &refMap) - if err != nil { - t.Fatalf("could not unmarshal ref json: %s\n", err) - } - - for k, vRef := range refMap { - vFlat, ok := dmFlat[k] - if !ok { - t.Fatalf("Missing key %s in flatten message according to reference", k) - } - if vRef != vFlat { - t.Errorf("Invalid value for key=%s get=%v expected=%v", k, vFlat, vRef) - } - } - }) - } -} - -func BenchmarkDnsMessage_ToFlatJSON(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := dm.ToFlatJSON() - if err != nil { - b.Fatalf("could not encode to flat json: %v\n", err) - } - } -} - -// Tests for TEXT format -func TestDnsMessage_TextFormat_ToString(t *testing.T) { - - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - delimiter string - boundary string - format string - qname string - identity string - expected string - }{ - { - name: "default", - delimiter: config.Global.TextFormatDelimiter, - boundary: config.Global.TextFormatBoundary, - format: config.Global.TextFormat, - qname: "dnscollector.fr", - identity: "collector", - expected: "- collector CLIENT_QUERY NOERROR 1.2.3.4 1234 - - 0b dnscollector.fr A 0.000000000", - }, - { - name: "custom_delimiter", - delimiter: ";", - boundary: config.Global.TextFormatBoundary, - format: config.Global.TextFormat, - qname: "dnscollector.fr", - identity: "collector", - expected: "-;collector;CLIENT_QUERY;NOERROR;1.2.3.4;1234;-;-;0b;dnscollector.fr;A;0.000000000", - }, - { - name: "empty_delimiter", - delimiter: "", - boundary: config.Global.TextFormatBoundary, - format: config.Global.TextFormat, - qname: "dnscollector.fr", - identity: "collector", - expected: "-collectorCLIENT_QUERYNOERROR1.2.3.41234--0bdnscollector.frA0.000000000", - }, - { - name: "qname_quote", - delimiter: config.Global.TextFormatDelimiter, - boundary: config.Global.TextFormatBoundary, - format: config.Global.TextFormat, - qname: "dns collector.fr", - identity: "collector", - expected: "- collector CLIENT_QUERY NOERROR 1.2.3.4 1234 - - 0b \"dns collector.fr\" A 0.000000000", - }, - { - name: "default_boundary", - delimiter: config.Global.TextFormatDelimiter, - boundary: config.Global.TextFormatBoundary, - format: config.Global.TextFormat, - qname: "dns\"coll tor\".fr", - identity: "collector", - expected: "- collector CLIENT_QUERY NOERROR 1.2.3.4 1234 - - 0b \"dns\\\"coll tor\\\".fr\" A 0.000000000", - }, - { - name: "custom_boundary", - delimiter: config.Global.TextFormatDelimiter, - boundary: "!", - format: config.Global.TextFormat, - qname: "dnscoll tor.fr", - identity: "collector", - expected: "- collector CLIENT_QUERY NOERROR 1.2.3.4 1234 - - 0b !dnscoll tor.fr! A 0.000000000", - }, - { - name: "custom_text", - delimiter: config.Global.TextFormatDelimiter, - boundary: config.Global.TextFormatBoundary, - format: "qname {IN} qtype", - qname: "dnscollector.fr", - identity: "", - expected: "dnscollector.fr IN A", - }, - { - name: "quote_dnstap_version", - delimiter: config.Global.TextFormatDelimiter, - boundary: config.Global.TextFormatBoundary, - format: "identity version qname", - qname: "dnscollector.fr", - identity: "collector", - expected: "collector \"dnscollector 1.0.0\" dnscollector.fr", - }, - { - name: "quote_dnstap_identity", - delimiter: config.Global.TextFormatDelimiter, - boundary: config.Global.TextFormatBoundary, - format: "identity qname", - qname: "dnscollector.fr", - identity: "dns collector", - expected: "\"dns collector\" dnscollector.fr", - }, - { - name: "quote_dnstap_peername", - delimiter: config.Global.TextFormatDelimiter, - boundary: config.Global.TextFormatBoundary, - format: "peer-name qname", - qname: "dnscollector.fr", - identity: "", - expected: "\"localhost (127.0.0.1)\" dnscollector.fr", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - dm := GetFakeDNSMessage() - - dm.DNS.Qname = tc.qname - dm.DNSTap.Identity = tc.identity - - line := dm.String(strings.Fields(tc.format), tc.delimiter, tc.boundary) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_DefaultDirectives(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - format: "timestamp-rfc3339ns timestamp", - dm: DNSMessage{DNSTap: DNSTap{TimestampRFC3339: "2023-04-22T09:17:02.906922231Z"}}, - expected: "2023-04-22T09:17:02.906922231Z 2023-04-22T09:17:02.906922231Z", - }, - { - format: "timestamp-unixns timestamp-unixus timestamp-unixms", - dm: DNSMessage{DNSTap: DNSTap{Timestamp: 1682152174001850960}}, - expected: "1682152174001850960 1682152174001850 1682152174001", - }, - { - format: "latency", - dm: DNSMessage{DNSTap: DNSTap{Latency: 0.00001}}, - expected: "0.000010000", - }, - { - format: "qname qtype opcode", - dm: DNSMessage{DNS: DNS{Qname: "dnscollector.fr", Qtype: "AAAA", Opcode: 42}}, - expected: "dnscollector.fr AAAA 42", - }, - { - format: "qclass", - dm: DNSMessage{DNS: DNS{Qclass: "CH"}}, - expected: "CH", - }, - { - format: "operation", - dm: DNSMessage{DNSTap: DNSTap{Operation: "CLIENT_QUERY"}}, - expected: "CLIENT_QUERY", - }, - { - format: "family protocol", - dm: DNSMessage{NetworkInfo: DNSNetInfo{Family: "IPv4", Protocol: "UDP"}}, - expected: "IPv4 UDP", - }, - { - format: "length", - dm: DNSMessage{DNS: DNS{Length: 42}}, - expected: "42", - }, - { - format: "length-unit", - dm: DNSMessage{DNS: DNS{Length: 42}}, - expected: "42b", - }, - { - format: "malformed", - dm: DNSMessage{DNS: DNS{MalformedPacket: true}}, - expected: "PKTERR", - }, - { - format: "tc aa ra ad", - dm: DNSMessage{DNS: DNS{Flags: DNSFlags{TC: true, AA: true, RA: true, AD: true}}}, - expected: "TC AA RA AD", - }, - { - format: "df tr", - dm: DNSMessage{NetworkInfo: DNSNetInfo{IPDefragmented: true, TCPReassembled: true}}, - expected: "DF TR", - }, - { - format: "queryip queryport", - dm: DNSMessage{NetworkInfo: DNSNetInfo{QueryIP: "1.2.3.4", QueryPort: "4200"}}, - expected: "1.2.3.4 4200", - }, - { - format: "responseip responseport", - dm: DNSMessage{NetworkInfo: DNSNetInfo{ResponseIP: "1.2.3.4", ResponsePort: "4200"}}, - expected: "1.2.3.4 4200", - }, - { - format: "policy-rule policy-type policy-action policy-match policy-value", - dm: DNSMessage{DNSTap: DNSTap{PolicyRule: "rule", PolicyType: "type", - PolicyAction: "action", PolicyMatch: "match", - PolicyValue: "value"}}, - expected: "rule type action match value", - }, - { - format: "peer-name", - dm: DNSMessage{DNSTap: DNSTap{PeerName: "testpeer"}}, - expected: "testpeer", - }, - { - format: "query-zone", - dm: DNSMessage{DNSTap: DNSTap{QueryZone: "queryzone.test"}}, - expected: "queryzone.test", - }, - } - - for _, tc := range testcases { - t.Run(tc.format, func(t *testing.T) { - line := tc.dm.String(strings.Fields(tc.format), config.Global.TextFormatDelimiter, config.Global.TextFormatBoundary) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_InvalidDirectives(t *testing.T) { - testcases := []struct { - name string - dm DNSMessage - format string - }{ - { - name: "default", - dm: DNSMessage{}, - format: "invalid", - }, - { - name: "publicsuffix", - dm: DNSMessage{PublicSuffix: &TransformPublicSuffix{}}, - format: "publixsuffix-invalid", - }, - { - name: "powerdns", - dm: DNSMessage{PowerDNS: &PowerDNS{}}, - format: "powerdns-invalid", - }, - { - name: "geoip", - dm: DNSMessage{Geo: &TransformDNSGeo{}}, - format: "geoip-invalid", - }, - { - name: "suspicious", - dm: DNSMessage{Suspicious: &TransformSuspicious{}}, - format: "suspicious-invalid", - }, - { - name: "extracted", - dm: DNSMessage{Extracted: &TransformExtracted{}}, - format: "extracted-invalid", - }, - { - name: "filtering", - dm: DNSMessage{Filtering: &TransformFiltering{}}, - format: "filtering-invalid", - }, - { - name: "reducer", - dm: DNSMessage{Reducer: &TransformReducer{}}, - format: "reducer-invalid", - }, - { - name: "ml", - dm: DNSMessage{MachineLearning: &TransformML{}}, - format: "ml-invalid", - }, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - _, err := tc.dm.ToTextLine(strings.Fields(tc.format), " ", "") - if err == nil { - t.Errorf("Want err, got nil") - } else if err.Error() != ErrorUnexpectedDirective+tc.format { - t.Errorf("Unexpected error: %s", err.Error()) - } - }) - } -} - -func TestDnsMessage_TextFormat_Directives_PublicSuffix(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - name: "undefined", - format: "publixsuffix-tld", - dm: DNSMessage{}, - expected: "-", - }, - { - name: "default", - format: "publixsuffix-tld publixsuffix-etld+1", - dm: DNSMessage{PublicSuffix: &TransformPublicSuffix{QnamePublicSuffix: "com", QnameEffectiveTLDPlusOne: "google.com"}}, - expected: "com google.com", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - line := tc.dm.String( - strings.Fields(tc.format), - config.Global.TextFormatDelimiter, - config.Global.TextFormatBoundary, - ) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_Directives_Geo(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - name: "undefined", - format: "geoip-continent", - dm: DNSMessage{}, - expected: "-", - }, - { - name: "default", - format: "geoip-continent geoip-country geoip-city geoip-as-number geoip-as-owner", - dm: DNSMessage{Geo: &TransformDNSGeo{City: "Paris", Continent: "Europe", - CountryIsoCode: "FR", AutonomousSystemNumber: "AS1", AutonomousSystemOrg: "Google"}}, - expected: "Europe FR Paris AS1 Google", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - line := tc.dm.String( - strings.Fields(tc.format), - config.Global.TextFormatDelimiter, - config.Global.TextFormatBoundary, - ) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_Directives_Pdns(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - name: "undefined", - format: "powerdns-tags", - dm: DNSMessage{}, - expected: "-", - }, - { - name: "empty_attributes", - format: "powerdns-tags powerdns-applied-policy powerdns-original-request-subnet powerdns-metadata", - dm: DNSMessage{PowerDNS: &PowerDNS{}}, - expected: "- - - -", - }, - { - name: "applied_policy", - format: "powerdns-applied-policy powerdns-applied-policy-hit powerdns-applied-policy-kind powerdns-applied-policy-trigger powerdns-applied-policy-type", - dm: DNSMessage{PowerDNS: &PowerDNS{ - AppliedPolicy: "policy", - AppliedPolicyHit: "hit", - AppliedPolicyKind: "kind", - AppliedPolicyTrigger: "trigger", - AppliedPolicyType: "type", - }}, - expected: "policy hit kind trigger type", - }, - { - name: "original_request_subnet", - format: "powerdns-original-request-subnet", - dm: DNSMessage{PowerDNS: &PowerDNS{OriginalRequestSubnet: "test"}}, - expected: "test", - }, - { - name: "metadata_badsyntax", - format: "powerdns-metadata", - dm: DNSMessage{PowerDNS: &PowerDNS{Metadata: map[string]string{"test_key1": "test_value1"}}}, - expected: "-", - }, - { - name: "metadata", - format: "powerdns-metadata:test_key1", - dm: DNSMessage{PowerDNS: &PowerDNS{Metadata: map[string]string{"test_key1": "test_value1"}}}, - expected: "test_value1", - }, - { - name: "metadata_invalid", - format: "powerdns-metadata:test_key2", - dm: DNSMessage{PowerDNS: &PowerDNS{Metadata: map[string]string{"test_key1": "test_value1"}}}, - expected: "-", - }, - { - name: "tags_all", - format: "powerdns-tags", - dm: DNSMessage{PowerDNS: &PowerDNS{Tags: []string{"tag1", "tag2"}}}, - expected: "tag1,tag2", - }, - { - name: "tags_index", - format: "powerdns-tags:1", - dm: DNSMessage{PowerDNS: &PowerDNS{Tags: []string{"tag1", "tag2"}}}, - expected: "tag2", - }, - { - name: "tags_invalid_index", - format: "powerdns-tags:3", - dm: DNSMessage{PowerDNS: &PowerDNS{Tags: []string{"tag1", "tag2"}}}, - expected: "-", - }, - { - name: "http_version", - format: "powerdns-http-version", - dm: DNSMessage{PowerDNS: &PowerDNS{HTTPVersion: "HTTP2"}}, - expected: "HTTP2", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - line := tc.dm.String( - strings.Fields(tc.format), - config.Global.TextFormatDelimiter, - config.Global.TextFormatBoundary, - ) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_Directives_ATags(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - name: "undefined", - format: "atags", - dm: DNSMessage{}, - expected: "-", - }, - { - name: "empty_attributes", - format: "atags", - dm: DNSMessage{ATags: &TransformATags{}}, - expected: "-", - }, - { - name: "tags_all", - format: "atags", - dm: DNSMessage{ATags: &TransformATags{Tags: []string{"tag1", "tag2"}}}, - expected: "tag1,tag2", - }, - { - name: "tags_index", - format: "atags:1", - dm: DNSMessage{ATags: &TransformATags{Tags: []string{"tag1", "tag2"}}}, - expected: "tag2", - }, - { - name: "tags_invalid_index", - format: "atags:3", - dm: DNSMessage{ATags: &TransformATags{Tags: []string{"tag1", "tag2"}}}, - expected: "-", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - line := tc.dm.String( - strings.Fields(tc.format), - config.Global.TextFormatDelimiter, - config.Global.TextFormatBoundary, - ) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_Directives_Suspicious(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - name: "undefined", - format: "suspicious-score", - dm: DNSMessage{}, - expected: "-", - }, - { - name: "default", - format: "suspicious-score", - dm: DNSMessage{Suspicious: &TransformSuspicious{Score: 4.0}}, - expected: "4", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - line := tc.dm.String( - strings.Fields(tc.format), - config.Global.TextFormatDelimiter, - config.Global.TextFormatBoundary, - ) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_Directives_Reducer(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - name: "undefined", - format: "reducer-occurrences", - dm: DNSMessage{}, - expected: "-", - }, - { - name: "default", - format: "reducer-occurrences", - dm: DNSMessage{Reducer: &TransformReducer{Occurrences: 1}}, - expected: "1", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - line := tc.dm.String( - strings.Fields(tc.format), - config.Global.TextFormatDelimiter, - config.Global.TextFormatBoundary, - ) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_Directives_Extracted(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - name: "undefined", - format: "extracted-dns-payload", - dm: DNSMessage{}, - expected: "-", - }, - { - name: "default", - format: "extracted-dns-payload", - dm: DNSMessage{Extracted: &TransformExtracted{}, DNS: DNS{Payload: []byte{ - 0x9e, 0x84, 0x01, 0x20, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - // query 1 - 0x01, 0x61, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // query 2 - 0x01, 0x62, 0x00, - // type A, class IN - 0x00, 0x01, 0x00, 0x01, - // query 3 - 0x01, 0x63, 0x00, - // type AAAA, class IN - 0x00, 0x1c, 0x00, 0x01, - }}}, - expected: "noQBIAADAAAAAAAAAWEAAAEAAQFiAAABAAEBYwAAHAAB", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - line := tc.dm.String( - strings.Fields(tc.format), - config.Global.TextFormatDelimiter, - config.Global.TextFormatBoundary, - ) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func TestDnsMessage_TextFormat_Directives_Filtering(t *testing.T) { - config := pkgconfig.GetDefaultConfig() - - testcases := []struct { - name string - format string - dm DNSMessage - expected string - }{ - { - name: "undefined", - format: "filtering-sample-rate", - dm: DNSMessage{}, - expected: "-", - }, - { - name: "default", - format: "filtering-sample-rate", - dm: DNSMessage{Filtering: &TransformFiltering{SampleRate: 22}}, - expected: "22", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - line := tc.dm.String( - strings.Fields(tc.format), - config.Global.TextFormatDelimiter, - config.Global.TextFormatBoundary, - ) - if line != tc.expected { - t.Errorf("Want: %s, got: %s", tc.expected, line) - } - }) - } -} - -func BenchmarkDnsMessage_ToTextFormat(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - textFormat := []string{"timestamp-rfc3339ns", "identity", - "operation", "rcode", "queryip", "queryport", "family", - "protocol", "length-unit", "qname", "qtype", "latency"} - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := dm.ToTextLine(textFormat, " ", "\"") - if err != nil { - b.Fatalf("could not encode to text format: %v\n", err) - } - } -} - -// Tests for PCAP serialization -func BenchmarkDnsMessage_ToPacketLayer(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - dnsmsg := new(dns.Msg) - dnsmsg.SetQuestion("dnscollector.dev.", dns.TypeAAAA) - dnsquestion, _ := dnsmsg.Pack() - - dm.NetworkInfo.Family = netutils.ProtoIPv4 - dm.NetworkInfo.Protocol = netutils.ProtoUDP - dm.DNS.Payload = dnsquestion - dm.DNS.Length = len(dnsquestion) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := dm.ToPacketLayer() - if err != nil { - b.Fatalf("could not encode to pcap: %v\n", err) - } - } -} - -// Flatten and relabeling -func TestDnsMessage_ApplyRelabeling(t *testing.T) { - // Créer un DNSMessage avec des règles de relabeling pour le test - dm := &DNSMessage{ - Relabeling: &TransformRelabeling{ - Rules: []RelabelingRule{ - {Regex: regexp.MustCompile("^old_"), Replacement: "new_field", Action: "rename"}, - {Regex: regexp.MustCompile("^foo_"), Action: "remove"}, - }, - }, - } - - // test map - dnsFields := map[string]interface{}{ - "old_field": "value1", - "foo_field": "value2", - "other_field": "value3", - } - - // apply relabeling - err := dm.ApplyRelabeling(dnsFields) - if err != nil { - t.Errorf("ApplyRelabeling() return an error: %v", err) - } - - // check - expectedDNSFields := map[string]interface{}{ - "new_field": "value1", - "other_field": "value3", - } - if !reflect.DeepEqual(dnsFields, expectedDNSFields) { - t.Errorf("Want: %v, Get: %v", expectedDNSFields, dnsFields) - } -} - -func BenchmarkDnsMessage_ToFlatten_Relabelling(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - dm.Relabeling.Rules = append(dm.Relabeling.Rules, RelabelingRule{ - Regex: regexp.MustCompile(`dns.qname`), - Action: "remove", - }) - dm.Relabeling.Rules = append(dm.Relabeling.Rules, RelabelingRule{ - Regex: regexp.MustCompile(`dns.qtype`), - Replacement: "qtype", - Action: "rename", - }) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := dm.Flatten() - if err != nil { - b.Fatalf("could not flat: %v\n", err) - } - } -} - -func BenchmarkDnsMessage_ToFlatten(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := dm.Flatten() - if err != nil { - b.Fatalf("could not flat: %v\n", err) - } - } -} - -// To jinja templating -func TestDnsMessage_ToJinjaFormat(t *testing.T) { - dm := DNSMessage{} - dm.Init() - - dm.DNS.Qname = "qname_for_test" - - 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 }} - -;; QUESTION SECTION: -;{{ dm.DNS.Qname }} {{ dm.DNS.Qclass }} {{ dm.DNS.Qtype }} - -;; ANSWER SECTION: {% for rr in dm.DNS.DNSRRs.Answers %} -{{ rr.Name }} {{ rr.TTL }} {{ rr.Class }} {{ rr.Rdatatype }} {{ rr.Rdata }}{% endfor %} - -;; WHEN: {{ dm.DNSTap.Timestamp }} -;; MSG SIZE rcvd: {{ dm.DNS.Length }}` - - text, err := dm.ToTextTemplate(template) - if err != nil { - t.Errorf("Want no error, got: %s", err) - } - - if !strings.Contains(text, dm.DNS.Qname) { - t.Errorf("Want qname in template, got: %s", text) - } -} - -func BenchmarkDnsMessage_ToJinjaFormat(b *testing.B) { - dm := DNSMessage{} - dm.Init() - dm.InitTransforms() - - 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 }} - -;; QUESTION SECTION: -;{{ dm.DNS.Qname }} {{ dm.DNS.Qclass }} {{ dm.DNS.Qtype }} - -;; ANSWER SECTION: {% for rr in dm.DNS.DNSRRs.Answers %} -{{ rr.Name }} {{ rr.TTL }} {{ rr.Class }} {{ rr.Rdatatype }} {{ rr.Rdata }}{% endfor %} - -;; WHEN: {{ dm.DNSTap.Timestamp }} -;; MSG SIZE rcvd: {{ dm.DNS.Length }}` - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := dm.ToTextTemplate(template) - if err != nil { - b.Fatalf("could not encode to template: %v\n", err) - } - } -} diff --git a/docs/_examples/use-case-26.yml b/docs/_examples/use-case-26.yml new file mode 100644 index 00000000..c82e9e12 --- /dev/null +++ b/docs/_examples/use-case-26.yml @@ -0,0 +1,25 @@ +global: + trace: + verbose: true + +pipelines: + - name: tap_unix + dnstap: + sock-path: /var/run/named/dnstap.sock + routing-policy: + forward: [ filter-messages ] + + - name: filter-messages + dnsmessage: + matching: + include: + dnstap.operation: "CLIENT_RESPONSE" + dns.qtype: "A" + dns.rcode: "NOERROR" + dns.resource-records.an.0.rdata: "0.0.0.0" + routing-policy: + forward: [console] + + - name: console + stdout: + mode: flat-json \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index 9bbf25fb..9cecfa8d 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -3,9 +3,10 @@ You will find below some examples of configurations to manage your DNS logs. -- Pipelines running mode +- **Pipelines running mode with DNS Message filters** - [x] [Advanced example with DNSmessage collector](./_examples/use-case-24.yml) - [x] [How can I log only slow responses and errors?"](./_examples/use-case-25.yml) + - [x] [Filter DNStap messages where the response ip address is 0.0.0.0](./_examples/use-case-26.yml) - Capture DNS traffic from incoming DNSTap streams - [x] [Read from UNIX DNSTap socket and forward it to TLS stream](./_examples/use-case-5.yml)