package pcap //go:generate stringer -type=FieldClass -linecomment -output stringers.go . import ( "encoding/binary" "errors" "math" "strconv" "strings" "unsafe" "github.com/soypat/lneto" "github.com/soypat/lneto/arp" "github.com/soypat/lneto/dhcp/dhcpv4" "github.com/soypat/lneto/dns" "github.com/soypat/lneto/dns/mdns" "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/http/httpraw" "github.com/soypat/lneto/internal" "github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/ipv4/icmpv4" "github.com/soypat/lneto/ipv6" "github.com/soypat/lneto/ntp" "github.com/soypat/lneto/tcp" "github.com/soypat/lneto/udp" ) const unknownPayloadProto = "payload?" var ( ErrFieldByClassNotFound = errors.New("pcap: field by class not found") ErrLimitExceeded = errors.New("pcap: limit exceeded") errNotByteAligned = errors.New("must be parsed at byte boundary") errInvalidFieldIdx = errors.New("invalid field index") ) 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 } // initFrames pre-allocates a []Frame with per-position Fields capacity // sized for the protocol layers at each position. This avoids heap // allocations on the first packet when using SliceReclaim on subsequent calls. // // Pre-allocated capacities per position: // // 0: Ethernet (3 base + VLAN tag = 4) // 1: L3 max(IPv4=12, ARP=9, IPv6=8) = 12 // 2: L4 max(TCP=10, ICMP=8, UDP=4) = 10 // 3: App max(DHCP=15, NTP=13, HTTP=2, DNS=1) = 16 // 4-5: overflow/remaining = 2 func (pc *PacketBreakdown) initFrames() []Frame { const nframes = 6 var fieldCaps = [nframes]int{4, 12, 10, 16, 2, 2} frames := make([]Frame, nframes) for i := range frames { frames[i].Fields = make([]FrameField, 0, fieldCaps[i]) } return frames[:0] } func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { if dst == nil { dst = pc.initFrames() } if bitOffset%8 != 0 { return dst, errNotByteAligned } efrm, err := ethernet.NewFrame(pkt[bitOffset/8:]) if err != nil { return dst, err } efrm.ValidateSize(pc.validator()) if pc.validator().HasError() { return dst, pc.validator().ErrPop() } debuglog("pcap:eth:validated") finfo := reclaimFrame(&dst, "Ethernet", bitOffset, baseEthernetFields[:]) debuglog("pcap:eth:reclaimed") etype := efrm.EtherTypeOrSize() end := 14*octet + bitOffset if etype.IsSize() { finfo.Fields[len(finfo.Fields)-1].Class = FieldClassSize reclaimRemainingFrame(&dst, "Ethernet payload", FieldClassPayload, end, octet*len(pkt)) return dst, nil } if efrm.IsVLAN() { finfo.Fields = append(finfo.Fields, FrameField{Name: "VLAN Tag", Class: FieldClassType, FrameBitOffset: end, BitLength: 2 * octet}) reclaimRemainingFrame(&dst, "Ethernet VLAN", FieldClassPayload, end+2*octet, octet*len(pkt)) return dst, nil } switch etype { case ethernet.TypeARP: dst, err = pc.CaptureARP(dst, pkt, end) case ethernet.TypeIPv4: dst, err = pc.CaptureIPv4(dst, pkt, end) case ethernet.TypeIPv6: dst, err = pc.CaptureIPv6(dst, pkt, end) default: reclaimRemainingFrame(&dst, "Unknown Ethertype", FieldClassPayload, end, octet*len(pkt)) } debuglog("pcap:eth:done") return dst, err } func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { debuglog("pcap:arp:start") if bitOffset%8 != 0 { return dst, errNotByteAligned } afrm, err := arp.NewFrame(pkt[bitOffset/8:]) if err != nil { return dst, err } afrm.ValidateSize(pc.validator()) if pc.validator().HasError() { return dst, pc.validator().ErrPop() } finfo := reclaimFrame(&dst, "ARP", bitOffset, baseARPFields[:]) const varstart = 8 * octet _, hlen := afrm.Hardware() _, plen := afrm.Protocol() finfo.Fields = append(finfo.Fields, FrameField{ Name: "Sender hardware address", Class: FieldClassSrc, FrameBitOffset: varstart, BitLength: int(hlen) * octet, }, FrameField{ Name: "Sender protocol address", Class: FieldClassSrc, FrameBitOffset: int(hlen)*octet + varstart, BitLength: int(plen) * octet, }, FrameField{ Name: "Target hardware address", Class: FieldClassSrc, FrameBitOffset: int(hlen+plen)*octet + varstart, BitLength: int(hlen) * octet, }, FrameField{ Name: "Target protocol address", Class: FieldClassSrc, FrameBitOffset: (2*int(hlen)+int(plen))*octet + varstart, BitLength: int(plen) * octet, }, ) return dst, nil } func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { debuglog("pcap:ipv6:start") if bitOffset%8 != 0 { return dst, errNotByteAligned } ifrm6, err := ipv6.NewFrame(pkt[bitOffset/8:]) if err != nil { return dst, err } ifrm6.ValidateSize(pc.validator()) if pc.validator().HasError() { return dst, pc.validator().ErrPop() } reclaimFrame(&dst, "IPv6", bitOffset, baseIPv6Fields[:]) debuglog("pcap:ipv6:reclaimed") proto := ifrm6.NextHeader() end := bitOffset + 40*octet var protoErr error var crc lneto.CRC791 ifrm6.CRCWritePseudo(&crc) switch proto { case lneto.IPProtoTCP: if crc.PayloadSum16(ifrm6.Payload()) != 0 { protoErr = lneto.ErrBadCRC } case lneto.IPProtoUDP, lneto.IPProtoUDPLite: ufrm, err := udp.NewFrame(ifrm6.Payload()) if err != nil { protoErr = err break } ufrm.ValidateSize(pc.validator()) if err = pc.validator().ErrPop(); err != nil { protoErr = err break } frameLen := ufrm.Length() if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 { protoErr = lneto.ErrBadCRC } } return pc.captureIPProto(proto, dst, pkt, end, protoErr) } func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { debuglog("pcap:ipv4:start") if bitOffset%8 != 0 { return dst, errNotByteAligned } ifrm4, err := ipv4.NewFrame(pkt[bitOffset/8:]) if err != nil { return dst, err } ifrm4.ValidateSize(pc.validator()) if pc.validator().HasError() { return dst, pc.validator().ErrPop() } debuglog("pcap:ipv4:validated") // limit packet to the actual IPv4 frame size pkt = pkt[:bitOffset/8+int(ifrm4.TotalLength())] finfo := reclaimFrame(&dst, "IPv4", bitOffset, baseIPv4Fields[:]) debuglog("pcap:ipv4:reclaimed") options := ifrm4.Options() if len(options) > 0 { finfo.Fields = append(finfo.Fields, FrameField{ Class: FieldClassOptions, FrameBitOffset: 20 * octet, BitLength: octet * len(options), }) } if ifrm4.CalculateHeaderCRC() != 0 { finfo.Errors = append(finfo.Errors, lneto.ErrBadCRC) } debuglog("pcap:ipv4:crc-done") proto := ifrm4.Protocol() end := bitOffset + octet*ifrm4.HeaderLength() var crc lneto.CRC791 var ipProtoErr error payload := ifrm4.Payload() switch proto { case lneto.IPProtoTCP: tfrm, err := tcp.NewFrame(payload) if err == nil { tfrm.ValidateSize(pc.validator()) if pc.vld.HasError() { return dst, pc.vld.ErrPop() } ifrm4.CRCWriteTCPPseudo(&crc) if crc.PayloadSum16(payload) != 0 { ipProtoErr = lneto.ErrBadCRC } } case lneto.IPProtoUDP: ufrm, err := udp.NewFrame(payload) if err == nil { ufrm.ValidateSize(pc.validator()) if pc.vld.HasError() { return dst, pc.vld.ErrPop() } if ufrm.CRC() != 0 { frameLen := ufrm.Length() ifrm4.CRCWriteUDPPseudo(&crc, frameLen) if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 { ipProtoErr = lneto.ErrBadCRC } } } case lneto.IPProtoICMP: _, err := icmpv4.NewFrame(payload) if err == nil { if crc.PayloadSum16(payload) != 0 { ipProtoErr = lneto.ErrBadCRC } } } debuglog("pcap:ipv4:proto-crc-done") return pc.captureIPProto(proto, dst, pkt, end, ipProtoErr) } func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt []byte, bitOffset int, ipProtoErr error) (_ []Frame, err error) { debuglog("pcap:ipproto:start") nextFrame := len(dst) switch proto { case lneto.IPProtoTCP: dst, err = pc.CaptureTCP(dst, pkt, bitOffset) case lneto.IPProtoUDP: dst, err = pc.CaptureUDP(dst, pkt, bitOffset) case lneto.IPProtoUDPLite: dst, err = pc.CaptureUDP(dst, pkt, bitOffset) if len(dst) > nextFrame { dst[nextFrame].Protocol = "UDPLite" } case lneto.IPProtoICMP: dst, err = pc.CaptureICMPv4(dst, pkt, bitOffset) default: reclaimRemainingFrame(&dst, "unknown proto", 0, bitOffset, octet*len(pkt)) } if ipProtoErr != nil && len(dst) > nextFrame { dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErr) } debuglog("pcap:ipproto:done") return dst, err } func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { debuglog("pcap:tcp:start") if bitOffset%8 != 0 { return dst, errNotByteAligned } tfrm, err := tcp.NewFrame(pkt[bitOffset/8:]) if err != nil { return dst, err } tfrm.ValidateSize(pc.validator()) if pc.validator().HasError() { return dst, pc.validator().ErrPop() } debuglog("pcap:tcp:validated") end := bitOffset + octet*tfrm.HeaderLength() finfo := reclaimFrame(&dst, "TCP", bitOffset, baseTCPFields[:]) debuglog("pcap:tcp:reclaimed") options := tfrm.Options() if len(options) > 0 { finfo.Fields = append(finfo.Fields, FrameField{ Class: FieldClassOptions, FrameBitOffset: 20 * octet, BitLength: octet * len(options), }) } payload := tfrm.Payload() if len(payload) > 0 { debuglog("pcap:tcp:http-start") dst, err = pc.CaptureHTTP(dst, pkt, end) debuglog("pcap:tcp:http-done") if err != nil { reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)) } } debuglog("pcap:tcp:done") return dst, nil } func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { debuglog("pcap:udp:start") if bitOffset%8 != 0 { return dst, errNotByteAligned } ufrm, err := udp.NewFrame(pkt[bitOffset/8:]) if err != nil { return dst, err } ufrm.ValidateSize(pc.validator()) if pc.validator().HasError() { return dst, pc.validator().ErrPop() } reclaimFrame(&dst, "UDP", bitOffset, baseUDPFields[:]) debuglog("pcap:udp:reclaimed") end := bitOffset + 8*octet payload := ufrm.Payload() dstport := ufrm.DestinationPort() srcport := ufrm.SourcePort() if dhcpv4.PayloadIsDHCPv4(payload) { dst, err = pc.CaptureDHCPv4(dst, pkt, end) } else if dstport == dns.ServerPort || srcport == dns.ServerPort || dstport == mdns.Port || srcport == mdns.Port { dst, err = pc.CaptureDNS(dst, pkt, end) } else if dstport == ntp.ServerPort || srcport == ntp.ServerPort { dst, err = pc.CaptureNTP(dst, pkt, end) } if err != nil { reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)) } debuglog("pcap:udp:done") return dst, nil } func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { debuglog("pcap:icmp:start") if bitOffset%8 != 0 { return dst, errNotByteAligned } icmpData := pkt[bitOffset/8:] ifrm, err := icmpv4.NewFrame(icmpData) if err != nil { return dst, err } finfo := reclaimFrame(&dst, "ICMP", bitOffset, baseICMPv4Fields[:]) tp := ifrm.Type() // Add type-specific fields. switch tp { case icmpv4.TypeEcho, icmpv4.TypeEchoReply: finfo.Fields = append(finfo.Fields, icmpv4EchoFields[:]...) if len(icmpData) > 8 { finfo.Fields = append(finfo.Fields, FrameField{ Name: tp.String(), Class: FieldClassPayload, FrameBitOffset: 8 * octet, BitLength: (len(icmpData) - 8) * octet, }) } case icmpv4.TypeRedirect: finfo.Fields = append(finfo.Fields, icmpv4RedirectFields[:]...) if len(icmpData) > 8 { finfo.Fields = append(finfo.Fields, FrameField{ Name: "Original Datagram", Class: FieldClassPayload, FrameBitOffset: 8 * octet, BitLength: (len(icmpData) - 8) * octet, }) } case icmpv4.TypeDestinationUnreachable, icmpv4.TypeTimeExceeded, icmpv4.TypeSourceQuench, icmpv4.TypeParameterProblem: // 4 bytes unused, then original datagram. if len(icmpData) > 8 { finfo.Fields = append(finfo.Fields, FrameField{ Name: "Original Datagram", Class: FieldClassPayload, FrameBitOffset: 8 * octet, BitLength: (len(icmpData) - 8) * octet, }) } case icmpv4.TypeTimestamp, icmpv4.TypeTimestampReply: finfo.Fields = append(finfo.Fields, icmpv4TimestampFields[:]...) default: // Unknown type - add generic payload. if len(icmpData) > 4 { finfo.Fields = append(finfo.Fields, FrameField{ Class: FieldClassPayload, FrameBitOffset: 4 * octet, BitLength: (len(icmpData) - 4) * octet, }) } } return dst, nil } func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { if bitOffset%8 != 0 { return dst, errNotByteAligned } dnsData := pkt[bitOffset/8:] pc.dmsg.LimitResourceDecoding(4, 4, 4, 4) _, incomplete, err := pc.dmsg.Decode(dnsData) if err != nil && !incomplete { return dst, err } debuglog("pcap:dns-decode") hdr, _ := dns.NewFrame(dnsData) finfo := reclaimFrame(&dst, "DNS", bitOffset, baseDNSFields[:]) if incomplete { finfo.Errors = append(finfo.Errors, ErrLimitExceeded) } 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 } ntpData := pkt[bitOffset/8:] _, err := ntp.NewFrame(ntpData) if err != nil { return dst, err } reclaimFrame(&dst, "NTP", bitOffset, baseNTPFields[:]) debuglog("pcap:ntp") return dst, nil } func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { if bitOffset%8 != 0 { return dst, errNotByteAligned } dhcpData := pkt[bitOffset/8:] dfrm, err := dhcpv4.NewFrame(dhcpData) if err != nil { return dst, err } finfo := reclaimFrame(&dst, "DHCPv4", bitOffset, baseDHCPv4Fields[:]) magic := dfrm.MagicCookie() if magic != dhcpv4.MagicCookie { finfo.Errors = append(finfo.Errors, lneto.ErrInvalidField) } options := dfrm.OptionsPayload() if len(options) > 0 && pc.SubfieldLimit > 0 { // Reclaim FrameField from Fields backing array to reuse its SubFields backing array. optfield := internal.SliceReclaim(&finfo.Fields) *optfield = FrameField{ // Ensure consistent zeroing of memory, but keep slice reference for reuse. Class: FieldClassOptions, SubFields: optfield.SubFields[:0], Name: "options", } debuglog("pcap:dhcp-opt0") err = dfrm.ForEachOption(func(optoff int, opt dhcpv4.OptNum, data []byte) error { if len(optfield.SubFields) >= pc.SubfieldLimit { return ErrLimitExceeded } // 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 }) debuglog("pcap:dhcp-opt1") // optfield already in finfo.Fields via SliceReclaim, no append needed. if err != nil { finfo.Errors = append(finfo.Errors, err) } } debuglog("pcap:dhcp-done") return dst, nil } // httpBodyClass returns FieldClassText if the HTTP body appears to be human-readable text // based on the Content-Type header, falling back to byte inspection when Content-Type is absent. func httpBodyClass(contentType, body []byte) FieldClass { if len(contentType) > 0 { // Use unsafe string conversion to avoid []byte("literal") heap allocations in TinyGo. ct := unsafe.String(&contentType[0], len(contentType)) // Strip parameters (e.g. "; charset=utf-8") for media type matching. if i := strings.IndexByte(ct, ';'); i >= 0 { ct = strings.TrimSpace(ct[:i]) } switch { case strings.HasPrefix(ct, "text/"): return FieldClassText case ct == "application/json", ct == "application/javascript", ct == "application/xml": return FieldClassText case strings.HasSuffix(ct, "+json"), strings.HasSuffix(ct, "+xml"): return FieldClassText } return FieldClassPayload } // No Content-Type: inspect bytes for printable ASCII. for _, c := range body { if c >= 32 && c <= 126 || c == '\t' || c == '\n' || c == '\r' { continue } return FieldClassPayload } return FieldClassText } func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { debuglog("pcap:http:start") const httpProtocol = "HTTP" if bitOffset%8 != 0 { return dst, errNotByteAligned } const asResponse = true const asRequest = false httpData := pkt[bitOffset/8:] pc.hdr.Reset(httpData) err := pc.hdr.Parse(asResponse) if err != nil { pc.hdr.Reset(httpData) err = pc.hdr.Parse(asRequest) // try as request. } if err != nil { return dst, err } debuglog("pcap:http:parsed") hdrLen := pc.hdr.BufferParsed() body, _ := pc.hdr.Body() debuglog("pcap:http:body") bodyClass := httpBodyClass(pc.hdr.Get("Content-Type"), body) debuglog("pcap:http:bodyclass") finfo := reclaimFrame(&dst, httpProtocol, bitOffset, nil) debuglog("pcap:http:reclaim") finfo.Fields = append(finfo.Fields, FrameField{ Name: "HTTP Header", Class: FieldClassText, FrameBitOffset: 0, BitLength: hdrLen * octet, }, FrameField{ Name: "HTTP Body", Class: bodyClass, FrameBitOffset: hdrLen * octet, BitLength: len(body) * octet, }, ) debuglog("pcap:http:done") return dst, nil } func (pc *PacketBreakdown) validator() *lneto.Validator { return &pc.vld } type FrameField struct { Name string Class FieldClass FrameBitOffset int BitLength int SubFields []FrameField Flags Flags } // Flags stores frame field interpretation bits. 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 } func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 } type Frame struct { PacketBitOffset int Protocol string Fields []FrameField Errors []error } // FieldByClass gets the frame field index with the argument FieldClass Class field set. // If there are multiple fields with same class it will get the one with empty name. // If there are multiple fields with same class and none have empty name then it will return an error. func (frm Frame) FieldByClass(c FieldClass) (int, error) { Nfields := len(frm.Fields) selected := -1 multiple := false for i := range Nfields { field := &frm.Fields[i] if field.Class != c { continue } if field.Name == "" { // Prioritize "canonical" fields with no name. if selected >= 0 && frm.Fields[selected].Name == "" { return -1, lneto.ErrMismatch } selected = i } else if selected >= 0 { multiple = true } else { selected = i } } if selected < 0 { return -1, ErrFieldByClassNotFound } if multiple && frm.Fields[selected].Name != "" { return -1, lneto.ErrMismatch } return selected, nil } // FieldAsUint evaluates the field as a 64-bit integer. func (frm Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) { const badUint64 = math.MaxUint64 if fieldIdx < 0 || fieldIdx >= len(frm.Fields) { return badUint64, errInvalidFieldIdx } field := frm.Fields[fieldIdx] return fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned()) } // AppendField appends the binary on-the-wire representation of the field and aligns the field so it starts at the first bit of appended data. func (frm Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, error) { if fieldIdx < 0 || fieldIdx >= len(frm.Fields) { return dst, errInvalidFieldIdx } field := frm.Fields[fieldIdx] return appendField(dst, pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned()) } func 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 } var buf [8]byte _, err := appendField(buf[8-octets:8-octets], pkt, fieldBitStart, bitlen, rightAligned) if err != nil { return badUint64, err } v := binary.BigEndian.Uint64(buf[:]) return v, nil } func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool) ([]byte, error) { fieldBitEnd := fieldBitStart + bitlen octets := (bitlen + 7) / 8 // total octets needed to represent field. octetsStart := fieldBitStart / 8 if octets+octetsStart > len(pkt) { return dst, lneto.ErrShortBuffer } firstBitOffset := fieldBitStart % 8 lastOctetExcessBits := fieldBitEnd % 8 if firstBitOffset == 0 { if rightAligned { return dst, lneto.ErrBug } // Optimized path: field starts at byte boundary. dst = append(dst, pkt[octetsStart:octetsStart+octets]...) debuglog("pcap:appendField:optpath") if lastOctetExcessBits != 0 { dst[len(dst)-1] >>= lastOctetExcessBits } return dst, nil } mask := byte(1< len(pkt) { return dst, lneto.ErrShortBuffer } for i := range octets { b := (pkt[octetsStart+i] & mask) << (8 - firstBitOffset) b |= pkt[octetsStart+i+1] >> firstBitOffset dst = append(dst, b) debuglog("pcap:appendField:rightalign3") } return dst, nil } // LEFT ALIGNED: TODO: test this. for i := 0; i < octets-1; i++ { // Append all octets except last one due to excess bits special handling. b := pkt[i+octetsStart] & mask b |= pkt[i+octetsStart+1] >> firstBitOffset dst = append(dst, b) debuglog("pcap:appendField:leftalign1") } lastOctet := pkt[octetsStart+octets-1] & mask lastOctet >>= lastOctetExcessBits dst = append(dst, lastOctet) debuglog("pcap:appendField:leftalign2") return dst, nil } func (frm Frame) String() string { return string(frm.AppendString(nil)) } func (frm Frame) AppendString(b []byte) []byte { bitlen := frm.LenBits() b = append(b, frm.Protocol...) if bitlen%8 == 0 { b = append(b, " len="...) b = strconv.AppendInt(b, int64(bitlen/8), 10) } else { b = append(b, " bits="...) b = strconv.AppendInt(b, int64(bitlen), 10) } iopt, err := frm.FieldByClass(FieldClassOptions) if err == nil { b = append(b, " optlen="...) b = strconv.AppendInt(b, int64((frm.Fields[iopt].BitLength+7)/8), 10) } for _, err := range frm.Errors { b = append(b, ' ') b = append(b, err.Error()...) } return b } func (frm Frame) LenBits() (totalBitlen int) { for i := range frm.Fields { totalBitlen = max(totalBitlen, frm.Fields[i].FrameBitOffset+frm.Fields[i].BitLength) } return totalBitlen } func (ff FrameField) String() string { if ff.Class == FieldClassPayload { return "Payload len=" + strconv.Itoa(ff.BitLength/8) } if ff.Name != "" { return ff.Name + " (" + ff.Class.String() + ")" } return ff.Class.String() } type FieldClass uint8 const ( fieldClassUndefined FieldClass = iota // undefined FieldClassSrc // source FieldClassDst // destination FieldClassProto // protocol FieldClassVersion // version FieldClassType // type FieldClassSize // size FieldClassFlags // flags FieldClassID // identification FieldClassChecksum // checksum FieldClassOptions // options FieldClassPayload // payload FieldClassText // text FieldClassAddress // address // FieldClassBinaryText represents long stretches of binary data such as BOOTP DHCPv4 field. FieldClassBinaryText // binary-text FieldClassOperation // op FieldClassTimestamp // timestamp FieldClassDNSName // dns name ) const octet = 8 const fieldClassDNSResource = fieldClassUndefined var baseEthernetFields = [...]FrameField{ { Class: FieldClassDst, FrameBitOffset: 0, BitLength: 6 * octet, }, { Class: FieldClassSrc, FrameBitOffset: 6 * octet, BitLength: 6 * octet, }, { Class: FieldClassProto, FrameBitOffset: 12 * octet, BitLength: 2 * octet, }, } var baseARPFields = [...]FrameField{ { Name: "Hardware type", Class: FieldClassType, FrameBitOffset: 0, BitLength: 2 * octet, }, { Name: "Protocol type", Class: FieldClassType, FrameBitOffset: 2 * octet, BitLength: 2 * octet, }, { Name: "Hardware size", Class: FieldClassSize, FrameBitOffset: 4 * octet, BitLength: 1 * octet, }, { Name: "Protocol size", Class: FieldClassSize, FrameBitOffset: 5 * octet, BitLength: 1 * octet, }, { Class: FieldClassOperation, FrameBitOffset: 6 * octet, BitLength: 2 * octet, }, } var baseIPv6Fields = [...]FrameField{ { Class: FieldClassVersion, FrameBitOffset: 0, BitLength: 4, }, { Name: "Type of Service", Class: FieldClassFlags, FrameBitOffset: 4, BitLength: 1 * octet, Flags: FlagRightAligned, }, { Name: "Flow Label", Class: FieldClassID, FrameBitOffset: 12, BitLength: 20, Flags: FlagRightAligned, }, { Name: "Total Length", Class: FieldClassSize, FrameBitOffset: 4 * octet, BitLength: 2 * octet, }, { Name: "Next Header", Class: 0, FrameBitOffset: 6 * octet, BitLength: 1 * octet, }, { Name: "Hop Limit", Class: 0, FrameBitOffset: 7 * octet, BitLength: 1 * octet, }, { Class: FieldClassSrc, FrameBitOffset: 8 * octet, BitLength: 16 * octet, }, { Class: FieldClassSrc, FrameBitOffset: 24 * octet, BitLength: 16 * octet, }, } var baseIPv4Fields = [...]FrameField{ { Class: FieldClassVersion, FrameBitOffset: 0, BitLength: 4, }, { Name: "Header Length", Class: FieldClassSize, FrameBitOffset: 4, BitLength: 4, }, { Name: "Type of Service", Class: FieldClassFlags, FrameBitOffset: 1 * octet, BitLength: 1 * octet, }, { Name: "Total Length", Class: FieldClassSize, FrameBitOffset: 2 * octet, BitLength: 2 * octet, }, { Class: FieldClassID, FrameBitOffset: 4 * octet, BitLength: 2 * octet, }, { Class: FieldClassFlags, FrameBitOffset: 6 * octet, BitLength: 2 * octet, }, { Name: "Time to live", FrameBitOffset: 8 * octet, BitLength: 1 * octet, }, { Class: FieldClassProto, FrameBitOffset: 9 * octet, BitLength: 1 * octet, }, { Class: FieldClassChecksum, FrameBitOffset: 10 * octet, BitLength: 2 * octet, }, { Class: FieldClassSrc, FrameBitOffset: 12 * octet, BitLength: 4 * octet, }, { Class: FieldClassDst, FrameBitOffset: 16 * octet, BitLength: 4 * octet, }, } var baseTCPFields = [...]FrameField{ { Name: "Source port", Class: FieldClassSrc, FrameBitOffset: 0, BitLength: 2 * octet, }, { Name: "Destination port", Class: FieldClassDst, FrameBitOffset: 2 * octet, BitLength: 2 * octet, }, { Name: "Sequence number", Class: FieldClassID, FrameBitOffset: 4 * octet, BitLength: 4 * octet, }, { Name: "Acknowledgement number", Class: FieldClassID, FrameBitOffset: 8 * octet, BitLength: 4 * octet, }, { Name: "Header length", Class: FieldClassSize, FrameBitOffset: 12 * octet, BitLength: 4, }, { Class: FieldClassFlags, FrameBitOffset: 12*octet + 4, BitLength: 12, Flags: FlagRightAligned, }, { Name: "Window", Class: 0, FrameBitOffset: 14 * octet, BitLength: 2 * octet, }, { Class: FieldClassChecksum, FrameBitOffset: 16 * octet, BitLength: 2 * octet, }, { Name: "Urgent pointer", Class: 0, FrameBitOffset: 18 * octet, BitLength: 2 * octet, Flags: FlagLegacy, }, } var baseUDPFields = [...]FrameField{ { Name: "Source port", Class: FieldClassSrc, FrameBitOffset: 0, BitLength: 2 * octet, }, { Name: "Destination port", Class: FieldClassDst, FrameBitOffset: 2 * octet, BitLength: 2 * octet, }, { Class: FieldClassSize, FrameBitOffset: 4 * octet, BitLength: 2 * octet, }, { Class: FieldClassChecksum, FrameBitOffset: 6 * octet, BitLength: 2 * octet, }, } var baseICMPv4Fields = [...]FrameField{ { Class: FieldClassType, FrameBitOffset: 0, BitLength: 1 * octet, }, { Name: "Code", Class: fieldClassUndefined, FrameBitOffset: 1 * octet, BitLength: 1 * octet, }, { Class: FieldClassChecksum, FrameBitOffset: 2 * octet, BitLength: 2 * octet, }, } var icmpv4EchoFields = [...]FrameField{ { Name: "Identifier", Class: FieldClassID, FrameBitOffset: 4 * octet, BitLength: 2 * octet, }, { Name: "Sequence Number", Class: FieldClassID, FrameBitOffset: 6 * octet, BitLength: 2 * octet, }, } var icmpv4RedirectFields = [...]FrameField{ { Name: "Gateway Address", Class: FieldClassAddress, FrameBitOffset: 4 * octet, BitLength: 4 * octet, }, } var icmpv4TimestampFields = [...]FrameField{ { Name: "Identifier", Class: FieldClassID, FrameBitOffset: 4 * octet, BitLength: 2 * octet, }, { Name: "Sequence Number", Class: FieldClassID, FrameBitOffset: 6 * octet, BitLength: 2 * octet, }, { Name: "Originate Timestamp", Class: FieldClassTimestamp, FrameBitOffset: 8 * octet, BitLength: 4 * octet, }, { Name: "Receive Timestamp", Class: FieldClassTimestamp, FrameBitOffset: 12 * octet, BitLength: 4 * octet, }, { Name: "Transmit Timestamp", Class: FieldClassTimestamp, FrameBitOffset: 16 * octet, BitLength: 4 * octet, }, } var baseDHCPv4Fields = [...]FrameField{ { Class: FieldClassOperation, FrameBitOffset: 0, BitLength: 1 * octet, }, { Name: "Hardware Address Type", Class: FieldClassProto, FrameBitOffset: 1 * octet, BitLength: 1 * octet, }, { Name: "Hardware Address Length", Class: FieldClassSize, FrameBitOffset: 2 * octet, BitLength: 1 * octet, }, { Name: "Hops", Class: fieldClassUndefined, FrameBitOffset: 3 * octet, BitLength: 1 * octet, }, { Name: "Transaction ID", Class: FieldClassID, FrameBitOffset: 4 * octet, BitLength: 4 * octet, }, { Name: "Start Time", Class: fieldClassUndefined, FrameBitOffset: 8 * octet, BitLength: 2 * octet, }, { Name: "Flags", Class: FieldClassFlags, FrameBitOffset: 10 * octet, BitLength: 2 * octet, }, { Name: "Client Address", Class: FieldClassAddress, FrameBitOffset: 12 * octet, BitLength: 4 * octet, }, { Name: "Offered Address", Class: FieldClassAddress, FrameBitOffset: 16 * octet, BitLength: 4 * octet, }, { Name: "Server Next Address", Class: FieldClassAddress, FrameBitOffset: 20 * octet, BitLength: 4 * octet, }, { Name: "Relay Agent Address", Class: FieldClassAddress, FrameBitOffset: 24 * octet, BitLength: 4 * octet, }, { Name: "Client Hardware Address", Class: FieldClassAddress, FrameBitOffset: 28 * octet, BitLength: 6 * octet, // Ethernet MAC is 6 bytes, remaining 10 in chaddr are padding }, { Name: "Padding", Class: FieldClassBinaryText, FrameBitOffset: (28 + 6) * octet, // Part of Client Hardware Address(16 bytes) but unused. BitLength: 10 * octet, Flags: FlagLegacy, }, { Name: "BOOTP", Class: FieldClassBinaryText, FrameBitOffset: (28 + 16) * octet, BitLength: (dhcpv4.OptionsOffset - (28 + 16)) * octet, Flags: FlagLegacy, }, } var baseDNSFields = [...]FrameField{ { Class: FieldClassID, FrameBitOffset: 0, BitLength: 2 * octet, }, { Class: FieldClassFlags, FrameBitOffset: 2 * octet, BitLength: 2 * octet, }, { Name: "Questions", Class: FieldClassSize, FrameBitOffset: 4 * octet, BitLength: 2 * octet, }, { Name: "Answers", Class: FieldClassSize, FrameBitOffset: 6 * octet, BitLength: 2 * octet, }, { Name: "Authorities", Class: FieldClassSize, FrameBitOffset: 8 * octet, BitLength: 2 * octet, }, { Name: "Additionals", Class: FieldClassSize, FrameBitOffset: 10 * octet, BitLength: 2 * octet, }, } var baseNTPFields = [...]FrameField{ { Name: "Mode", Class: FieldClassType, FrameBitOffset: 0, BitLength: 3, }, { Class: FieldClassVersion, FrameBitOffset: 3, BitLength: 2, }, { Name: "Leap Indicator", Class: fieldClassUndefined, FrameBitOffset: 5, BitLength: 3, }, { Name: "Stratum", Class: fieldClassUndefined, FrameBitOffset: 1 * octet, BitLength: 1 * octet, }, { Name: "Poll", Class: fieldClassUndefined, FrameBitOffset: 2 * octet, BitLength: 1 * octet, }, { Name: "System Precision", Class: fieldClassUndefined, FrameBitOffset: 3 * octet, BitLength: 1 * octet, }, { Name: "Root Delay", Class: fieldClassUndefined, FrameBitOffset: 4 * octet, BitLength: 4 * octet, }, { Name: "Root Dispersion", Class: fieldClassUndefined, FrameBitOffset: 8 * octet, BitLength: 4 * octet, }, { Name: "Reference ID", Class: FieldClassText, FrameBitOffset: 12 * octet, BitLength: 4 * octet, }, { Name: "Reference Time", Class: FieldClassTimestamp, FrameBitOffset: 16 * octet, BitLength: 8 * octet, }, { Name: "Origin Time", Class: FieldClassTimestamp, FrameBitOffset: 24 * octet, BitLength: 8 * octet, }, { Name: "Receive Time", Class: FieldClassTimestamp, FrameBitOffset: 32 * octet, BitLength: 8 * octet, }, { Name: "Transit Time", Class: FieldClassTimestamp, FrameBitOffset: 40 * octet, BitLength: 8 * octet, }, } // reclaimFrame extends dst via [internal.SliceReclaim], resets the reclaimed Frame // with the given protocol and bit offset while preserving Fields and Errors // backing arrays, and appends baseFields. Returns the Frame for further modification. func reclaimFrame(dst *[]Frame, proto string, bitOffset int, baseFields []FrameField) *Frame { finfo := internal.SliceReclaim(dst) *finfo = Frame{ PacketBitOffset: bitOffset, Protocol: proto, Fields: append(finfo.Fields[:0], baseFields...), Errors: finfo.Errors[:0], } debuglog("pcap:reclaim") return finfo } // reclaimRemainingFrame extends dst via SliceReclaim and populates the reclaimed // Frame as a single-field "remaining payload" frame, reusing the old Fields backing array. func reclaimRemainingFrame(dst *[]Frame, proto string, class FieldClass, pktBitOffset, pktBitLen int) { finfo := reclaimFrame(dst, proto, pktBitOffset, nil) finfo.Fields = append(finfo.Fields, FrameField{ Class: class, BitLength: pktBitLen - pktBitOffset, }) debuglog("pcap:reclaim-rem") } const enableDebug = internal.HeapAllocDebugging func debuglog(msg string) { if enableDebug { internal.LogAllocs(msg) } }