Add DNS support to pcap (#100)

* begin adding dns pcap functionality

* improve question formatting

* work on capturing answers as well

* finalize DNS printing

* increase subfield limits

* add FlagContainer type

* prettier DNS printing
This commit is contained in:
Pat Whittingslow
2026-04-24 19:41:10 -03:00
committed by GitHub
parent b2c3d11c4c
commit 198db3789b
8 changed files with 401 additions and 23 deletions
+193 -11
View File
@@ -384,14 +384,14 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int)
}
finfo := reclaimFrame(&dst, "ICMP", bitOffset, baseICMPv4Fields[:])
tp := ifrm.Type()
// Add type-specific fields.
switch ifrm.Type() {
switch tp {
case icmpv4.TypeEcho, icmpv4.TypeEchoReply:
finfo.Fields = append(finfo.Fields, icmpv4EchoFields[:]...)
if len(icmpData) > 8 {
finfo.Fields = append(finfo.Fields, FrameField{
Name: "Data",
Name: tp.String(),
Class: FieldClassPayload,
FrameBitOffset: 8 * octet,
BitLength: (len(icmpData) - 8) * octet,
@@ -439,26 +439,202 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
}
dnsData := pkt[bitOffset/8:]
pc.dmsg.LimitResourceDecoding(4, 4, 4, 4)
off, incomplete, err := pc.dmsg.Decode(dnsData)
_, incomplete, err := pc.dmsg.Decode(dnsData)
if err != nil && !incomplete {
return dst, err
}
debuglog("pcap:dns-decode")
finfo := reclaimFrame(&dst, "DNS", bitOffset, nil)
hdr, _ := dns.NewFrame(dnsData)
finfo := reclaimFrame(&dst, "DNS", bitOffset, baseDNSFields[:])
if incomplete {
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
}
field := internal.SliceReclaim(&finfo.Fields)
*field = FrameField{
Name: "Data",
FrameBitOffset: 0,
BitLength: int(off) * octet,
SubFields: field.SubFields[:0], // Reuse subfields.
if pc.SubfieldLimit <= 0 {
debuglog("pcap:dns-done")
return dst, nil
}
wireOff := dns.SizeHeader
// Questions section: walk all QDCount wire records to keep wireOff correct,
// but only emit SubFields for the decoded ones.
nq := int(hdr.QDCount())
if nq > 0 {
sectionStart := wireOff
qfield := internal.SliceReclaim(&finfo.Fields)
*qfield = FrameField{Name: "Questions", Class: fieldClassDNSResource, SubFields: qfield.SubFields[:0], Flags: FlagContainer}
decoded := pc.dmsg.Questions
for i := range nq {
nameStart := wireOff
wireOff, err = dnsSkipName(dnsData, wireOff)
if err != nil {
break
}
nameEnd := wireOff
wireOff += 4 // Type(2) + Class(2)
if i < len(decoded) && len(qfield.SubFields)+3 <= pc.SubfieldLimit {
qfield.SubFields = append(qfield.SubFields, FrameField{
Name: "Name",
Class: FieldClassDNSName,
FrameBitOffset: nameStart * octet,
BitLength: (nameEnd - nameStart) * octet,
}, FrameField{
Name: "Type",
Class: FieldClassOperation,
FrameBitOffset: nameEnd * octet,
BitLength: 2 * octet,
}, FrameField{
Name: "Class",
Class: FieldClassOperation,
FrameBitOffset: (nameEnd + 2) * octet,
BitLength: 2 * octet,
})
}
}
qfield.FrameBitOffset = sectionStart * octet
qfield.BitLength = (wireOff - sectionStart) * octet
}
wireOff = pc.appendDNSResources(finfo, "Answers", dnsData, pc.dmsg.Answers, int(hdr.ANCount()), wireOff)
wireOff = pc.appendDNSResources(finfo, "Authorities", dnsData, pc.dmsg.Authorities, int(hdr.NSCount()), wireOff)
wireOff = pc.appendDNSResources(finfo, "Additionals", dnsData, pc.dmsg.Additionals, int(hdr.ARCount()), wireOff)
_ = wireOff
debuglog("pcap:dns-done")
return dst, nil
}
// appendDNSResources adds a section FrameField with per-record SubFields to finfo.
// It walks all `total` wire records to keep wireOff accurate, but only emits SubFields
// for the decoded slice entries while nFields < pc.SubfieldLimit.
func (pc *PacketBreakdown) appendDNSResources(finfo *Frame, name string, dnsData []byte, decoded []dns.Resource, total, wireOff int) int {
if total == 0 {
return wireOff
}
var err error
sectionStart := wireOff
rfield := internal.SliceReclaim(&finfo.Fields)
*rfield = FrameField{Name: name, Class: fieldClassDNSResource, SubFields: rfield.SubFields[:0], Flags: FlagContainer}
for i := range total {
nameStart := wireOff
wireOff, err = dnsSkipName(dnsData, wireOff)
if err != nil || wireOff+10 > len(dnsData) {
break
}
nameEnd := wireOff
dataLen := int(binary.BigEndian.Uint16(dnsData[nameEnd+8:]))
wireOff += 10 + dataLen // Type(2)+Class(2)+TTL(4)+Length(2)+Data
if wireOff > len(dnsData) {
break
}
if i < len(decoded) && len(rfield.SubFields)+6 <= pc.SubfieldLimit {
rfield.SubFields = append(rfield.SubFields, FrameField{
Name: "Name",
Class: FieldClassDNSName,
FrameBitOffset: nameStart * octet,
BitLength: (nameEnd - nameStart) * octet,
}, FrameField{
Name: "Type",
Class: FieldClassOperation,
FrameBitOffset: nameEnd * octet,
BitLength: 2 * octet,
}, FrameField{
Name: "Class",
Class: FieldClassOperation,
FrameBitOffset: (nameEnd + 2) * octet,
BitLength: 2 * octet,
}, FrameField{
Name: "TTL",
Class: FieldClassID,
FrameBitOffset: (nameEnd + 4) * octet,
BitLength: 4 * octet,
}, FrameField{
Name: "Length",
Class: FieldClassSize,
FrameBitOffset: (nameEnd + 8) * octet,
BitLength: 2 * octet,
}, FrameField{
Name: "Data",
Class: FieldClassAddress,
FrameBitOffset: (nameEnd + 10) * octet,
BitLength: dataLen * octet,
})
}
}
rfield.FrameBitOffset = sectionStart * octet
rfield.BitLength = (wireOff - sectionStart) * octet
if err != nil {
finfo.Errors = append(finfo.Errors, err)
}
return wireOff
}
// dnsAppendDottedName walks a DNS name in wire format starting at off within
// dnsMsg, follows compression pointers, and appends the dotted representation
// (e.g. "example.com") to dst. Returns dst unchanged on error.
func dnsAppendDottedName(dst, dnsMsg []byte, off int) []byte {
// TODO: somehow move this to dns package. dns.Name.Append ? dns.Name is the wire format...
first := true
for ptr := 0; off < len(dnsMsg) && ptr <= 10; {
c := dnsMsg[off]
off++
switch c & 0xc0 {
case 0x00:
if c == 0 {
return dst // null terminator: done
}
end := off + int(c)
if end > len(dnsMsg) {
return dst
}
if !first {
dst = append(dst, '.')
}
dst = append(dst, dnsMsg[off:end]...)
off = end
first = false
case 0xc0:
if off >= len(dnsMsg) {
return dst
}
off = int(c&0x3f)<<8 | int(dnsMsg[off])
ptr++ // guard against pointer loops
default:
return dst // reserved label type
}
}
return dst
}
// dnsSkipName advances off past a DNS name in wire format without allocating.
// Compression pointers are followed and consume 2 bytes, terminating the name.
func dnsSkipName(b []byte, off int) (int, error) {
for {
if off >= len(b) {
return off, lneto.ErrTruncatedFrame
}
c := b[off]
off++
switch c & 0xc0 {
case 0x00:
if c == 0 {
return off, nil // null terminator
}
off += int(c) // skip label bytes
if off > len(b) {
return off, lneto.ErrTruncatedFrame
}
case 0xc0:
if off >= len(b) {
return off, lneto.ErrTruncatedFrame
}
return off + 1, nil // compression pointer: 2 bytes total, always terminal
default:
return off, lneto.ErrInvalidField // reserved label type
}
}
}
func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return dst, errNotByteAligned
@@ -656,6 +832,9 @@ type Flags uint32
const (
FlagRightAligned Flags = 1 << iota
FlagLegacy
// FlagContainer is used for [FrameField]s whose SubFields represent
// the entirety of the FrameField's data. i.e: DNS Questions/Answers.
FlagContainer
)
func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 }
@@ -855,10 +1034,13 @@ const (
FieldClassBinaryText // binary-text
FieldClassOperation // op
FieldClassTimestamp // timestamp
FieldClassDNSName // dns name
)
const octet = 8
const fieldClassDNSResource = fieldClassUndefined
var baseEthernetFields = [...]FrameField{
{
Class: FieldClassDst,