Added dns detector based on miekg/dns

This commit is contained in:
2025-10-09 15:37:30 +02:00
parent fd55412020
commit 13dc5a5d50
4 changed files with 301 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
package dns
import (
"encoding/hex"
"log"
"math"
"strings"
"github.com/miekg/dns"
"git.maze.io/go/dpi/protocol"
)
// Name is the DNS protocol name.
const Name = "dns"
var (
classTypeScoreUnknown = -.15
classTypeScore = map[uint16]float64{
dns.ClassINET: .1,
dns.ClassCHAOS: .05,
}
recordTypeScoreUnknown = -.15
recordTypeScore = map[uint16]float64{
// Common types
dns.TypeA: .2,
dns.TypeAAAA: .2,
dns.TypeCNAME: .2,
dns.TypeMX: .2,
dns.TypeNS: .2,
dns.TypePTR: .2,
dns.TypeSOA: .15,
dns.TypeTXT: .15,
dns.TypeSRV: .15,
dns.TypeCAA: .15,
// Common types related to DNSSEC
dns.TypeDNSKEY: .1,
dns.TypeDS: .1,
dns.TypeNSEC: .1,
dns.TypeNSEC3: .1,
// Rare/obsolete
dns.TypeHINFO: -.1,
dns.TypeNULL: -.1,
}
)
func init() {
// Every DNS packet (query or answer) has a 12-byte header.
log.Println("register DetectDNS")
protocol.Register(protocol.Both, "????????????", DetectDNS)
}
func DetectDNS(dir protocol.Direction, data []byte, srcPort, dstPort int) (proto *protocol.Protocol, confidence float64) {
log.Printf("detect dns: %q", hex.EncodeToString(data))
// Parsing using miekg/dns
msg := new(dns.Msg)
if msg.Unpack(data) != nil {
return nil, 0
}
if srcPort == 53 || dstPort == 53 {
confidence = .1
}
// Base confidence for a DNS packet; a lot of things may look like DNS, so our initial
// confidence isn't very high.
confidence += .45
if msg.Opcode == dns.OpcodeQuery {
confidence += .1
} else {
confidence -= .2
}
switch msg.Rcode {
case dns.RcodeSuccess: // NOERROR
confidence += .1
case dns.RcodeNameError: // NXDOMAIN
confidence += .05
default:
confidence -= .05
}
if len(msg.Question) == 1 {
// Contains exactly one question, this is most common.
confidence += .2
if score, ok := classTypeScore[msg.Question[0].Qclass]; ok {
confidence += score
} else {
confidence += classTypeScoreUnknown
}
if score, ok := recordTypeScore[msg.Question[0].Qtype]; ok {
confidence += score
} else {
confidence += recordTypeScoreUnknown
}
} else {
// Contains zero or more than one question, this is very uncommon.
confidence -= .2
}
if msg.Response {
if msg.Authoritative {
confidence += .05
}
if len(msg.Question) == 1 && len(msg.Answer) > 0 {
var (
question = msg.Question[0].Name
answer = msg.Answer[0].Header().Name
)
if strings.EqualFold(question, answer) {
confidence += .1
} else {
var isCNAME bool
for _, rr := range msg.Answer {
if isCNAME = rr.Header().Rrtype == dns.TypeCNAME; isCNAME {
break
}
}
if isCNAME {
confidence += .05
} else {
confidence -= .1
}
}
}
} else {
if len(msg.Answer) > 0 || len(msg.Extra) > 0 {
// This wasn't a reply but we have an answer section anyway
confidence -= .2
} else {
confidence += .1
}
}
// Clip the confidence between [0 .. 0.99].
confidence = math.Max(confidence, 0)
confidence = math.Min(confidence, .99)
// We don't have a lower threshold for confidence capping in this function, because
// that is really up to the caller to determine if this wasn't some kind of attempt
// to exfiltrate data using malicious queries, etc.
return &protocol.Protocol{
Name: Name,
}, confidence
}

View File

@@ -0,0 +1,115 @@
package dns
import (
"testing"
"git.maze.io/go/dpi/protocol"
)
func TestDetectDNS(t *testing.T) {
// A valid DNS query for "www.google.com" (A record)
dnsQuery := []byte{
0x12, 0x34, // Transaction ID
0x01, 0x00, // Flags (Standard Query, Recursion Desired)
0x00, 0x01, // Questions: 1
0x00, 0x00, // Answer RRs: 0
0x00, 0x00, // Authority RRs: 0
0x00, 0x00, // Additional RRs: 0
0x03, 'w', 'w', 'w', // Question section:
0x06, 'g', 'o', 'o', 'g', 'l', 'e', //
0x03, 'c', 'o', 'm', //
0x00, // Null terminator for the name
0x00, 0x01, // Type: A (Host Address)
0x00, 0x01, // Class: IN (Internet)
}
// A valid DNS reply for the above query
dnsReply := []byte{
0x12, 0x34, // Transaction ID (matches query)
0x81, 0x80, // Flags (Response, Recursion Desired, Recursion Available)
0x00, 0x01, // Questions: 1
0x00, 0x01, // Answer RRs: 1
0x00, 0x00, // Authority RRs: 0
0x00, 0x00, // Additional RRs: 0
0x03, 'w', 'w', 'w', // Question section (same as query)
0x06, 'g', 'o', 'o', 'g', 'l', 'e', //
0x03, 'c', 'o', 'm', //
0x00, //
0x00, 0x01, //
0x00, 0x01, //
0xc0, 0x0c, // Answer section. Name: Pointer to the question name at offset 12 (0x0c)
0x00, 0x01, // Type: A
0x00, 0x01, // Class: IN
0x00, 0x00, 0x01, 0x2c, // Time to Live (TTL): 300 seconds
0x00, 0x04, // Data Length: 4 bytes
0x08, 0x08, 0x08, 0x08, // RDATA: IP Address 8.8.8.8
}
// This is just the first 12 bytes of a DNS query with a single question.
headerWithQuestionPromise := []byte{
0x12, 0x34, // Transaction ID
0x01, 0x00, // Flags (Standard Query)
0x00, 0x01, // Questions: 1 <-- This promises more data!
0x00, 0x00, // Answer RRs: 0
0x00, 0x00, // Authority RRs: 0
0x00, 0x00, // Additional RRs: 0
}
// This is a valid, self-contained "empty" DNS message.
headerWithNoPromise := []byte{
0x56, 0x78, // Transaction ID
0x01, 0x00, // Flags (Standard Query)
0x00, 0x00, // Questions: 0 <-- This promises NO more data.
0x00, 0x00, // Answer RRs: 0
0x00, 0x00, // Authority RRs: 0
0x00, 0x00, // Additional RRs: 0
}
t.Run("query", func(t *testing.T) {
p, c, err := protocol.Detect(protocol.Both, dnsQuery, 1234, 53)
if err != nil {
t.Fatalf("unexpected error: %v", err)
return
}
t.Logf("detected %s confidence %g%%", p.Name, c*100)
if p.Name != Name {
t.Errorf("expected %q protocol, got %q", Name, p.Name)
}
})
t.Run("answer", func(t *testing.T) {
p, c, err := protocol.Detect(protocol.Both, dnsReply, 53, 1234)
if err != nil {
t.Fatalf("unexpected error: %v", err)
return
}
t.Logf("detected %s confidence %g%%", p.Name, c*100)
if p.Name != Name {
t.Errorf("expected %q protocol, got %q", Name, p.Name)
}
})
t.Run("header with no promise", func(t *testing.T) {
p, c, err := protocol.Detect(protocol.Both, headerWithNoPromise, 1234, 53)
if err != nil {
t.Fatalf("unexpected error: %v", err)
return
}
t.Logf("detected %s confidence %g%%", p.Name, c*100)
if p.Name != Name {
t.Errorf("expected %q protocol, got %q", Name, p.Name)
}
})
t.Run("header with question promise", func(t *testing.T) {
p, c, err := protocol.Detect(protocol.Both, headerWithQuestionPromise, 1234, 53)
if err != nil {
t.Fatalf("unexpected error: %v", err)
return
}
t.Logf("detected %s confidence %g%%", p.Name, c*100)
if p.Name != Name {
t.Errorf("expected %q protocol, got %q", Name, p.Name)
}
})
}

View File

@@ -0,0 +1,19 @@
module git.maze.io/go/dpi/protocol/detect/dns
go 1.25.0
replace git.maze.io/go/dpi => ../../..
require (
git.maze.io/go/dpi v0.0.0-20251009103949-31774b961d0d
github.com/miekg/dns v1.1.68
golang.org/x/crypto v0.42.0
)
require (
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/tools v0.33.0 // indirect
)

View File

@@ -0,0 +1,16 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=