diff --git a/dhcpv4/frame.go b/dhcpv4/frame.go index c9be31f..8778498 100644 --- a/dhcpv4/frame.go +++ b/dhcpv4/frame.go @@ -122,7 +122,7 @@ func (frm Frame) ClearHeader() { // ForEachOption iterates over all DHCPv4 options returning an error on a malformed option or when user provided callback returns an error. // If the user provided callback is nil then only option buffer validation is performed. -func (frm Frame) ForEachOption(fn func(off int, op OptNum, data []byte) error) error { +func (frm Frame) ForEachOption(fn func(off int, opt OptNum, data []byte) error) error { // Parse DHCP options. ptr := OptionsOffset if ptr > len(frm.buf) { diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 26236f7..eab36a3 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -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, }, } diff --git a/internet/pcap/capture_test.go b/internet/pcap/capture_test.go index f3d7bf1..bbc6ba9 100644 --- a/internet/pcap/capture_test.go +++ b/internet/pcap/capture_test.go @@ -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) +} diff --git a/internet/pcap/format.go b/internet/pcap/format.go index b5b55fe..e1630be 100644 --- a/internet/pcap/format.go +++ b/internet/pcap/format.go @@ -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 +}