Files
lneto/internet/pcap/format.go
T
Pat Whittingslow 7d323aae19 Tcp rst handling (#40)
* claude suggests a way forward

* add timing to capture printer

* add pcap.Flags

* fix ICMP CRC calculation and add test

* bugfix: still send data on half-close state(close-wait)

* fix pcap test
2026-02-23 09:50:13 -03:00

201 lines
5.6 KiB
Go

package pcap
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"net/netip"
"slices"
"strconv"
"strings"
"sync"
_ "time"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"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
}
// 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) {
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
}
}
return dst, nil
}
// FormatFrame
func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) {
sep := f.fieldSep()
bitlen := frm.LenBits()
if bitlen%8 == 0 {
dst = fmt.Appendf(dst, "%s len=%d", frm.Protocol, bitlen/8)
} else {
dst = fmt.Appendf(dst, "%s bitlen=%d", frm.Protocol, bitlen)
}
for ifield := range frm.Fields {
field := frm.Fields[ifield]
if f.filterField(field) {
continue
}
dst = append(dst, sep...)
if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP {
// TCP flags pretty print special case.
dst = append(dst, "flags="...)
v, err := 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()...)
}
dst = append(dst, ')')
}
return dst, nil
}
func (f *Formatter) filterField(field FrameField) bool {
return f.FilterClasses != nil && !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.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 && !f.filterField(field.SubFields[i]); i++ {
dst = append(dst, sep...)
// Notice we only format subfields one level low
dst, err = f.formatField(dst, pktStartOff, field.SubFields[i], pkt)
}
}
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, ')')
}
dst = append(dst, '=')
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
}
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:
// inspired by [time.RFC3339]
const littlerfc3339 = "2006-01-02T15:04:05.9999"
if len(f.buf) != 8 {
return dst, errors.New("only timestamp8 supported")
}
ts := ntp.TimestampFromUint64(binary.BigEndian.Uint64(f.buf))
dst = ts.Time().AppendFormat(dst, littlerfc3339)
case FieldClassText:
dst = strconv.AppendQuote(dst, string(f.buf))
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
// IP, MAC addresses and ports.
if field.BitLength <= 16 {
v, err := fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
if err != nil {
return dst, err
}
dst = strconv.AppendUint(dst, v, 10)
} else if field.BitLength == 4*8 {
dst = netip.AddrFrom4([4]byte(f.buf)).AppendTo(dst)
} else if field.BitLength == 6*8 {
dst = ethernet.AppendAddr(dst, [6]byte(f.buf))
} else if field.BitLength == 16*8 {
dst = netip.AddrFrom16([16]byte(f.buf)).AppendTo(dst)
} else {
dst = append(dst, "0x"...)
dst = hex.AppendEncode(dst, f.buf)
}
}
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
}