pcap: add subfield formatting

This commit is contained in:
Patricio Whittingslow
2026-01-26 10:30:45 -03:00
parent 4cffe26c30
commit e8afc09a73
4 changed files with 206 additions and 22 deletions
+60 -8
View File
@@ -36,6 +36,9 @@ type PacketBreakdown struct {
hdr httpraw.Header
dmsg dns.Message
vld lneto.Validator
// SubfieldLimit will limit the number of captured subfields to the value it has.
// Typically this means option fields of DHCP,IPv4,TCP.
SubfieldLimit int
}
func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
@@ -416,16 +419,63 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int)
}
finfo.Fields = append(finfo.Fields, baseDHCPv4Fields[:]...)
options := dfrm.OptionsPayload()
if len(options) > 0 {
err = dfrm.ForEachOption(func(optoff int, op dhcpv4.OptNum, data []byte) error {
finfo.Fields = append(finfo.Fields, FrameField{
Name: op.String(),
Class: FieldClassOptions,
FrameBitOffset: optoff * octet,
BitLength: (2 + len(data)) * octet,
})
if len(options) > 0 && pc.SubfieldLimit > 0 {
var optfield FrameField
optfield.Class = FieldClassOptions
optfield.Name = "options"
err = dfrm.ForEachOption(func(optoff int, opt dhcpv4.OptNum, data []byte) error {
if len(optfield.SubFields) >= pc.SubfieldLimit {
return errors.New("option cap limit surpassed for DHCP")
}
// optoff points to start of length and num bytes, skip over them with FrameBitOffset.
field := FrameField{Name: opt.String(), FrameBitOffset: (optoff + 2) * octet, BitLength: len(data) * octet}
switch opt {
// Text options.
case dhcpv4.OptHostName, dhcpv4.OptDomainName, dhcpv4.OptMessage, dhcpv4.OptRootPath:
field.Class = FieldClassText
// Address options (single or multiple IP addresses).
case dhcpv4.OptSubnetMask, dhcpv4.OptRouter, dhcpv4.OptDNSServers,
dhcpv4.OptBroadcastAddress, dhcpv4.OptServerIdentification,
dhcpv4.OptRequestedIPaddress, dhcpv4.OptNTPServersAddresses,
dhcpv4.OptTimeServers, dhcpv4.OptNameServers, dhcpv4.OptLogServers:
field.Class = FieldClassAddress
// Size options.
case dhcpv4.OptMaximumMessageSize, dhcpv4.OptInterfaceMTUSize, dhcpv4.OptBootFileSize:
field.Class = FieldClassSize
// Time/duration options (seconds).
case dhcpv4.OptIPAddressLeaseTime, dhcpv4.OptRenewTimeValue, dhcpv4.OptRebindingTimeValue,
dhcpv4.OptTimeOffset, dhcpv4.OptARPCacheTimeout, dhcpv4.OptPathMTUAgingTimeout,
dhcpv4.OptTCPKeepaliveInterval, dhcpv4.OptDefaultIPTTL, dhcpv4.OptDefaultTCPTimetoLive:
field.Class = FieldClassTimestamp
// Operation/type options.
case dhcpv4.OptMessageType:
field.Class = FieldClassOperation
// Identifier options.
case dhcpv4.OptClientIdentifier, dhcpv4.OptClientIdentifier1:
field.Class = FieldClassText
for _, c := range data {
if c < 32 || c > 127 { // If clientID is ascii, print as is.
field.Class = FieldClassID
break
}
}
// Parameter list (list of option codes).
case dhcpv4.OptParameterRequestList:
field.Class = FieldClassOptions
default:
field.Class = FieldClassPayload
}
optfield.SubFields = append(optfield.SubFields, field)
return nil
})
finfo.Fields = append(finfo.Fields, optfield)
if err != nil {
finfo.Errors = append(finfo.Errors, err)
}
@@ -484,6 +534,7 @@ type FrameField struct {
BitLength int
SubFields []FrameField
RightAligned bool
Legacy bool
}
type Frame struct {
@@ -1002,6 +1053,7 @@ var baseDHCPv4Fields = [...]FrameField{
Class: FieldClassBinaryText,
FrameBitOffset: (28 + 16) * octet,
BitLength: (dhcpv4.OptionsOffset - (28 + 16)) * octet,
Legacy: true,
},
}
+93
View File
@@ -2,18 +2,21 @@ package pcap
import (
"encoding/binary"
"fmt"
"math"
"math/rand"
"strings"
"testing"
"github.com/soypat/lneto"
"github.com/soypat/lneto/dhcpv4"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal/ltesto"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv6"
"github.com/soypat/lneto/tcp"
"github.com/soypat/lneto/udp"
)
const httpProtocol = "HTTP/1.1"
@@ -390,3 +393,93 @@ func TestFieldAsUintRightAligned(t *testing.T) {
t.Errorf("flow label: got 0x%05x, want 0x%05x", gotFlow, flowLabel)
}
}
func ExampleFormatter_dhcp() {
// Build an Ethernet + IPv4 + UDP + DHCPv4 packet.
const (
ethSize = 14
ipv4Size = 20
udpSize = 8
)
pktSize := ethSize + ipv4Size + udpSize + dhcpv4.OptionsOffset + 32 // room for options
pkt := make([]byte, pktSize)
// Ethernet frame.
efrm, _ := ethernet.NewFrame(pkt)
*efrm.DestinationHardwareAddr() = [6]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} // Broadcast
*efrm.SourceHardwareAddr() = [6]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe}
efrm.SetEtherType(ethernet.TypeIPv4)
// IPv4 frame.
ifrm, _ := ipv4.NewFrame(efrm.Payload())
ifrm.SetVersionAndIHL(4, 5)
ifrm.SetTotalLength(uint16(ipv4Size + udpSize + dhcpv4.OptionsOffset + 32))
ifrm.SetID(0x1234)
ifrm.SetFlags(0x4000) // Don't Fragment
ifrm.SetTTL(64)
ifrm.SetProtocol(lneto.IPProtoUDP)
*ifrm.SourceAddr() = [4]byte{0, 0, 0, 0} // 0.0.0.0 (DHCP client)
*ifrm.DestinationAddr() = [4]byte{255, 255, 255, 255} // Broadcast
ifrm.SetCRC(ifrm.CalculateHeaderCRC())
// UDP frame.
ufrm, _ := udp.NewFrame(ifrm.Payload())
ufrm.SetSourcePort(dhcpv4.DefaultClientPort) // 68
ufrm.SetDestinationPort(dhcpv4.DefaultServerPort) // 67
ufrm.SetLength(uint16(udpSize + dhcpv4.OptionsOffset + 32))
// DHCPv4 frame (DHCP Discover).
dfrm, _ := dhcpv4.NewFrame(ufrm.Payload())
dfrm.ClearHeader()
dfrm.SetOp(dhcpv4.OpRequest)
dfrm.SetHardware(1, 6, 0) // Ethernet, 6 bytes, 0 hops
dfrm.SetXID(0xdeadbeef)
*dfrm.CHAddrAs6() = [6]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe}
dfrm.SetMagicCookie(dhcpv4.MagicCookie)
// Add DHCP options.
opts := dfrm.OptionsPayload()
n := 0
n += writeOpt(opts[n:], dhcpv4.OptMessageType, byte(dhcpv4.MsgDiscover))
n += writeOpt(opts[n:], dhcpv4.OptHostName, []byte("myhost")...)
n += writeOpt(opts[n:], dhcpv4.OptParameterRequestList,
byte(dhcpv4.OptSubnetMask),
byte(dhcpv4.OptRouter),
byte(dhcpv4.OptDNSServers),
)
opts[n] = byte(dhcpv4.OptEnd)
// Capture and format the packet.
var cap PacketBreakdown
cap.SubfieldLimit = 10 // Capture up to 10 DHCP options
frames, err := cap.CaptureEthernet(nil, pkt, 0)
if err != nil {
fmt.Println("capture error:", err)
return
}
var fmtr Formatter
fmtr.SubfieldLimit = cap.SubfieldLimit
fmtr.FrameSep = "\n"
fmtr.FieldSep = "; "
fmtr.SubfieldSep = "\n\t"
out, err := fmtr.FormatFrames(nil, frames, pkt)
if err != nil {
fmt.Println("format error:", err)
return
}
fmt.Println(string(out))
// Output:
// Ethernet len=14; destination=ff:ff:ff:ff:ff:ff; source=de:ad:be:ef:ca:fe; protocol=0x0800
// IPv4 len=20; version=0x04; (Header Length)=5; (Type of Service)=0x00; (Total Length)=300; identification=0x1234; identification=0x1234; flags=0x4000; (Time to live)=0x40; protocol=0x11; checksum=0x278e; source=0.0.0.0; destination=255.255.255.255
// UDP [RFC768] len=8; (Source port)=68; (Destination port)=67; size=280; checksum=0x0000
// DHCPv4 len=240; op=1; (Hardware Address Type)=0x01; (Hardware Address Length)=6; Hops=0x00; (Transaction ID)=0xdeadbeef; (Start Time)=0x0000; Flags=0x0000; (Client Address)=0.0.0.0; (Offered Address)=0.0.0.0; (Server Next Address)=0.0.0.0; (Relay Agent Address)=0.0.0.0; (Client Hardware Address)=dead:beef:cafe::; options((DHCP message type.)=1
// (Hostname string)="myhost"
// (Parameter request list)=0x010306)
}
func writeOpt(dst []byte, opt dhcpv4.OptNum, data ...byte) int {
dst[0] = byte(opt)
dst[1] = byte(len(data))
copy(dst[2:], data)
return 2 + len(data)
}
+52 -13
View File
@@ -18,19 +18,23 @@ import (
)
type Formatter struct {
FieldSep string
FrameSep string
FieldSep string
SubfieldSep string
FilterClasses []FieldClass
buf []byte
// 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
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
if sep == "" {
sep = " | "
}
sep := f.frameSep()
for ifrm := range frms {
if ifrm != 0 {
dst = append(dst, sep...)
@@ -45,10 +49,7 @@ func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte
// FormatFrame
func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) {
sep := f.FieldSep
if sep == "" {
sep = "; " // default field separator
}
sep := f.fieldSep()
bitlen := frm.LenBits()
if bitlen%8 == 0 {
dst = fmt.Appendf(dst, "%s len=%d", frm.Protocol, bitlen/8)
@@ -72,7 +73,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
dst = tcp.Flags(v).AppendFormat(dst)
continue
}
dst, err = f.formatField(dst, frm.PacketBitOffset, field, pkt)
dst, err = f.FormatField(dst, frm.PacketBitOffset, field, pkt)
if err != nil {
return dst, err
}
@@ -81,11 +82,27 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
}
func (f *Formatter) filterField(field FrameField) bool {
return f.FilterClasses != nil && !slices.Contains(f.FilterClasses, field.Class)
return f.FilterClasses != nil && !slices.Contains(f.FilterClasses, field.Class) ||
(field.Legacy && !f.DisableLegacyFilter)
}
func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) {
return f.formatField(dst, pktStartOff, field, pkt)
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) {
@@ -145,3 +162,25 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
}
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
}