mirror of
https://github.com/soypat/lneto.git
synced 2026-08-05 07:27:54 +00:00
198db3789b
* 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
257 lines
7.6 KiB
Go
257 lines
7.6 KiB
Go
package pcap
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"math"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
_ "time"
|
|
"unsafe"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/ethernet"
|
|
"github.com/soypat/lneto/ipv4"
|
|
"github.com/soypat/lneto/ipv6"
|
|
"github.com/soypat/lneto/ntp"
|
|
"github.com/soypat/lneto/tcp"
|
|
)
|
|
|
|
type Formatter struct {
|
|
FrameSep string
|
|
FieldSep string
|
|
SubfieldSep string
|
|
FilterClasses []FieldClass
|
|
// SubfieldLimit limits the amount of subfields formatted.
|
|
SubfieldLimit int
|
|
// Formatter by default filters out printing legacy fields such as DHCP BOOTP field which
|
|
// may be very large in size but meaningless to the actual network functioning.
|
|
// Enabling DisableLegacyFilter means these fields will be printed as is.
|
|
DisableLegacyFilter bool
|
|
mubuf sync.Mutex
|
|
buf []byte
|
|
uintBuf [8]byte // scratch buffer for fieldAsUint to avoid TinyGo heap escape.
|
|
}
|
|
|
|
// FormatFrames appends the formatted frame data to the destination buffer according the Formatter state.
|
|
// Is equivalent to [Formatter.FormatFrame] called on each Frame with the FrameSep inserted between frames.
|
|
func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte, err error) {
|
|
debuglog("pcap:fmt:start")
|
|
sep := f.frameSep()
|
|
for ifrm := range frms {
|
|
if ifrm != 0 {
|
|
dst = append(dst, sep...)
|
|
}
|
|
dst, err = f.FormatFrame(dst, frms[ifrm], pkt)
|
|
if err != nil {
|
|
return dst, err
|
|
}
|
|
}
|
|
debuglog("pcap:fmt:done")
|
|
return dst, nil
|
|
}
|
|
|
|
// FormatFrame formats a single frame's protocol, fields, and errors into dst.
|
|
func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) {
|
|
debuglog("pcap:fmtframe:start")
|
|
sep := f.fieldSep()
|
|
bitlen := frm.LenBits()
|
|
dst = append(dst, frm.Protocol...)
|
|
if bitlen%8 == 0 {
|
|
dst = append(dst, " len="...)
|
|
dst = strconv.AppendInt(dst, int64(bitlen/8), 10)
|
|
} else {
|
|
dst = append(dst, " bitlen="...)
|
|
dst = strconv.AppendInt(dst, int64(bitlen), 10)
|
|
}
|
|
debuglog("pcap:FormatFrame:int")
|
|
|
|
for ifield := range frm.Fields {
|
|
field := frm.Fields[ifield]
|
|
if f.filterField(field) {
|
|
continue
|
|
}
|
|
dst = append(dst, sep...)
|
|
if field.Class == FieldClassFlags && frm.Protocol == "TCP" {
|
|
// TCP flags pretty print special case.
|
|
dst = append(dst, "flags="...)
|
|
v, err := f.fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
|
|
if err != nil {
|
|
return dst, err
|
|
}
|
|
dst = tcp.Flags(v).AppendFormat(dst)
|
|
continue
|
|
}
|
|
dst, err = f.FormatField(dst, frm.PacketBitOffset, field, pkt)
|
|
if err != nil {
|
|
return dst, err
|
|
}
|
|
}
|
|
if len(frm.Errors) > 0 {
|
|
dst = append(dst, " errs=("...)
|
|
for i, err := range frm.Errors {
|
|
if i != 0 {
|
|
dst = append(dst, ';')
|
|
}
|
|
dst = append(dst, err.Error()...)
|
|
debuglog("pcap:fmtframe:error")
|
|
}
|
|
dst = append(dst, ')')
|
|
}
|
|
debuglog("pcap:fmtframe:done")
|
|
return dst, nil
|
|
}
|
|
|
|
func (f *Formatter) filterField(field FrameField) bool {
|
|
return f.FilterClasses != nil && field.Flags&FlagContainer == 0 && !slices.Contains(f.FilterClasses, field.Class) ||
|
|
(field.Flags.IsLegacy() && !f.DisableLegacyFilter)
|
|
}
|
|
|
|
func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) {
|
|
printOnlySubfields := (field.Flags&FlagContainer != 0 || field.Class == FieldClassOptions) && len(field.SubFields) > 0
|
|
if !printOnlySubfields {
|
|
dst, err = f.formatField(dst, pktStartOff, field, pkt)
|
|
} else {
|
|
dst = append(dst, field.Name...)
|
|
}
|
|
if f.SubfieldLimit > 0 && len(field.SubFields) > 0 {
|
|
sep := f.subfieldSep()
|
|
lim := min(len(field.SubFields), f.SubfieldLimit)
|
|
for i := 0; err == nil && i < lim; i++ {
|
|
if f.filterField(field.SubFields[i]) {
|
|
continue
|
|
}
|
|
dst = append(dst, sep...)
|
|
// Notice we only format subfields one level low
|
|
dst, err = f.formatField(dst, pktStartOff, field.SubFields[i], pkt)
|
|
}
|
|
}
|
|
debuglog("pcap:FormatField:end")
|
|
return dst, err
|
|
}
|
|
|
|
func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) {
|
|
name := field.Name
|
|
if name == "" {
|
|
name = field.Class.String()
|
|
}
|
|
hasSpaces := strings.IndexByte(name, ' ') >= 0
|
|
if hasSpaces {
|
|
dst = append(dst, '(')
|
|
}
|
|
dst = append(dst, name...)
|
|
if hasSpaces {
|
|
dst = append(dst, ')')
|
|
}
|
|
if field.BitLength == 0 {
|
|
// We do not print empty nor container fields.
|
|
return dst, nil
|
|
}
|
|
dst = append(dst, '=')
|
|
if field.Flags&FlagContainer != 0 {
|
|
return dst, nil
|
|
}
|
|
f.mubuf.Lock()
|
|
defer f.mubuf.Unlock()
|
|
f.buf, err = appendField(f.buf[:0], pkt, field.FrameBitOffset+pktStartOff, field.BitLength, field.Flags.IsRightAligned())
|
|
if err != nil {
|
|
return dst, err
|
|
}
|
|
debuglog("pcap:fmtfield:append-done")
|
|
fieldBitStart := pktStartOff + field.FrameBitOffset
|
|
switch field.Class {
|
|
default:
|
|
fallthrough
|
|
case FieldClassChecksum, FieldClassID, FieldClassFlags, FieldClassOptions:
|
|
// Binary data to be printed as hexadecimal.
|
|
dst = append(dst, "0x"...)
|
|
dst = hex.AppendEncode(dst, f.buf)
|
|
case FieldClassTimestamp:
|
|
debuglog("pcap:fmtfield:timestamp")
|
|
// inspired by [time.RFC3339]
|
|
const littlerfc3339 = "2006-01-02T15:04:05.9999"
|
|
if len(f.buf) != 8 {
|
|
return dst, lneto.ErrUnsupported
|
|
}
|
|
ts := ntp.TimestampFromUint64(binary.BigEndian.Uint64(f.buf))
|
|
dst = ts.Time().AppendFormat(dst, littlerfc3339)
|
|
debuglog("pcap:fmtfield:timestamp-done")
|
|
case FieldClassText:
|
|
debuglog("pcap:fmtfield:text")
|
|
if len(f.buf) > 0 {
|
|
dst = strconv.AppendQuote(dst, unsafe.String(&f.buf[0], len(f.buf)))
|
|
}
|
|
debuglog("pcap:fmtfield:text-done")
|
|
case FieldClassDNSName:
|
|
// Walk the DNS message from pktStartOff to resolve the name, following
|
|
// compression pointers that reference earlier bytes in the DNS message.
|
|
dnsMsg := pkt[pktStartOff/8:]
|
|
nameOff := field.FrameBitOffset / 8
|
|
dst = dnsAppendDottedName(dst, dnsMsg, nameOff)
|
|
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
|
|
// IP, MAC addresses and ports.
|
|
if field.BitLength <= 16 {
|
|
v, err := f.fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
|
|
if err != nil {
|
|
return dst, err
|
|
}
|
|
dst = strconv.AppendUint(dst, v, 10)
|
|
debuglog("pcap:fmtfield:uint")
|
|
} else if field.BitLength == 4*8 {
|
|
dst = ipv4.AppendFormatAddr(dst, [4]byte(f.buf))
|
|
debuglog("pcap:fmtfield:addr4")
|
|
} else if field.BitLength == 6*8 {
|
|
dst = ethernet.AppendAddr(dst, [6]byte(f.buf))
|
|
debuglog("pcap:fmtfield:addr6")
|
|
} else if field.BitLength == 16*8 {
|
|
dst = ipv6.AppendFormatAddr(dst, [16]byte(f.buf))
|
|
debuglog("pcap:fmtfield:addr16")
|
|
} else {
|
|
dst = append(dst, "0x"...)
|
|
dst = hex.AppendEncode(dst, f.buf)
|
|
debuglog("pcap:fmtfield:addrN")
|
|
}
|
|
}
|
|
return dst, err
|
|
}
|
|
|
|
func (f *Formatter) frameSep() string {
|
|
sep := f.FrameSep
|
|
if sep == "" {
|
|
sep = " | "
|
|
}
|
|
return sep
|
|
}
|
|
func (f *Formatter) fieldSep() string {
|
|
sep := f.FieldSep
|
|
if sep == "" {
|
|
sep = "; " // default field separator
|
|
}
|
|
return sep
|
|
}
|
|
func (f *Formatter) subfieldSep() string {
|
|
sep := f.SubfieldSep
|
|
if sep == "" {
|
|
sep = "," // default sub-field separator
|
|
}
|
|
return sep
|
|
}
|
|
|
|
// fieldAsUint evaluates a packet field as a uint64 using the Formatter's
|
|
// scratch buffer to avoid a TinyGo heap escape from a local [8]byte.
|
|
func (f *Formatter) fieldAsUint(pkt []byte, fieldBitStart, bitlen int, rightAligned bool) (uint64, error) {
|
|
const badUint64 = math.MaxUint64
|
|
octets := (bitlen + 7) / 8
|
|
if octets > 8 {
|
|
return badUint64, lneto.ErrUnsupported
|
|
}
|
|
f.uintBuf = [8]byte{}
|
|
_, err := appendField(f.uintBuf[8-octets:8-octets], pkt, fieldBitStart, bitlen, rightAligned)
|
|
if err != nil {
|
|
return badUint64, err
|
|
}
|
|
return binary.BigEndian.Uint64(f.uintBuf[:]), nil
|
|
}
|