Error rewrites (#43)

* pcap: reuse Frame memory

* slog: reduce heap allocations of addresses; also prevent heap alloc of dhcp options in pcap

* dns: heapless improvement; add StackAsync buffer for more heapless operation; start thinking of errors

* errors: begin standardise errors in lneto

* errors: finish standardization of errors

* fix merge issues

* add more lneto errors to rest of package

* format errors.go
This commit is contained in:
Pat Whittingslow
2026-02-28 20:18:27 +01:00
committed by GitHub
parent 3d0bc93cbe
commit fa5ba918bb
38 changed files with 272 additions and 322 deletions
+26 -25
View File
@@ -28,6 +28,9 @@ 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 {
@@ -61,13 +64,11 @@ func (pc *PacketBreakdown) initFrames() []Frame {
}
func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:eth:start")
if dst == nil {
dst = pc.initFrames()
debuglog("pcap:eth:initframes")
}
if bitOffset%8 != 0 {
return dst, errors.New("ethernet must be parsed at byte boundary")
return dst, errNotByteAligned
}
efrm, err := ethernet.NewFrame(pkt[bitOffset/8:])
if err != nil {
@@ -110,7 +111,7 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in
func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:arp:start")
if bitOffset%8 != 0 {
return dst, errors.New("ARP must be parsed at byte boundary")
return dst, errNotByteAligned
}
afrm, err := arp.NewFrame(pkt[bitOffset/8:])
if err != nil {
@@ -157,7 +158,7 @@ func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([
func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:ipv6:start")
if bitOffset%8 != 0 {
return dst, errors.New("IPv6 must be parsed at byte boundary")
return dst, errNotByteAligned
}
ifrm6, err := ipv6.NewFrame(pkt[bitOffset/8:])
if err != nil {
@@ -201,7 +202,7 @@ func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) (
func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:ipv4:start")
if bitOffset%8 != 0 {
return dst, errors.New("IPv4 must be parsed at byte boundary")
return dst, errNotByteAligned
}
ifrm4, err := ipv4.NewFrame(pkt[bitOffset/8:])
if err != nil {
@@ -301,7 +302,7 @@ func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt
func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:tcp:start")
if bitOffset%8 != 0 {
return dst, errors.New("TCP must be parsed at byte boundary")
return dst, errNotByteAligned
}
tfrm, err := tcp.NewFrame(pkt[bitOffset/8:])
if err != nil {
@@ -339,7 +340,7 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:udp:start")
if bitOffset%8 != 0 {
return dst, errors.New("UDP must be parsed at byte boundary")
return dst, errNotByteAligned
}
ufrm, err := udp.NewFrame(pkt[bitOffset/8:])
if err != nil {
@@ -372,7 +373,7 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:icmp:start")
if bitOffset%8 != 0 {
return dst, errors.New("ICMPv4 must be parsed at byte boundary")
return dst, errNotByteAligned
}
icmpData := pkt[bitOffset/8:]
ifrm, err := icmpv4.NewFrame(icmpData)
@@ -432,7 +433,7 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int)
func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return dst, errors.New("DNS must be parsed at byte boundary")
return dst, errNotByteAligned
}
dnsData := pkt[bitOffset/8:]
pc.dmsg.LimitResourceDecoding(20, 20, 20, 20)
@@ -442,9 +443,9 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
}
finfo := reclaimFrame(&dst, "DNS", bitOffset, nil)
if incomplete {
finfo.Errors = append(finfo.Errors, errors.New("pcap: could not parse all DNS resources; add higher limit"))
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
}
finfo.Fields = append(finfo.Fields, FrameField{
finfo.Fields = append(finfo.Fields[:0], FrameField{
Name: "Data",
FrameBitOffset: 0,
BitLength: int(off) * octet,
@@ -454,7 +455,7 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return dst, errors.New("NTP must be parsed at byte boundary")
return dst, errNotByteAligned
}
ntpData := pkt[bitOffset/8:]
_, err := ntp.NewFrame(ntpData)
@@ -467,7 +468,7 @@ func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([
func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return dst, errors.New("DHCP must be parsed at byte boundary")
return dst, errNotByteAligned
}
dhcpData := pkt[bitOffset/8:]
dfrm, err := dhcpv4.NewFrame(dhcpData)
@@ -477,7 +478,7 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int)
finfo := reclaimFrame(&dst, "DHCPv4", bitOffset, baseDHCPv4Fields[:])
magic := dfrm.MagicCookie()
if magic != dhcpv4.MagicCookie {
finfo.Errors = append(finfo.Errors, errors.New("incorrect DHCPv4 magic cookie"))
finfo.Errors = append(finfo.Errors, lneto.ErrInvalidField)
}
options := dfrm.OptionsPayload()
@@ -491,7 +492,7 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int)
}
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")
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}
@@ -585,7 +586,7 @@ func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) (
debuglog("pcap:http:start")
const httpProtocol = "HTTP"
if bitOffset%8 != 0 {
return dst, errors.New("HTTP must be parsed at byte boundary")
return dst, errNotByteAligned
}
const asResponse = true
const asRequest = false
@@ -668,7 +669,7 @@ func (frm Frame) FieldByClass(c FieldClass) (int, error) {
}
if field.Name == "" { // Prioritize "canonical" fields with no name.
if selected >= 0 && frm.Fields[selected].Name == "" {
return -1, errors.New("multiple class fields with no name")
return -1, lneto.ErrMismatch
}
selected = i
} else if selected >= 0 {
@@ -681,7 +682,7 @@ func (frm Frame) FieldByClass(c FieldClass) (int, error) {
return -1, ErrFieldByClassNotFound
}
if multiple && frm.Fields[selected].Name != "" {
return -1, errors.New("multiple classes found and none have empty name")
return -1, lneto.ErrMismatch
}
return selected, nil
}
@@ -690,7 +691,7 @@ func (frm Frame) FieldByClass(c FieldClass) (int, error) {
func (frm Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) {
const badUint64 = math.MaxUint64
if fieldIdx < 0 || fieldIdx >= len(frm.Fields) {
return badUint64, errors.New("invalid field index")
return badUint64, errInvalidFieldIdx
}
field := frm.Fields[fieldIdx]
return fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
@@ -699,7 +700,7 @@ func (frm Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) {
// 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, errors.New("invalid field index")
return dst, errInvalidFieldIdx
}
field := frm.Fields[fieldIdx]
return appendField(dst, pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
@@ -709,7 +710,7 @@ func fieldAsUint(pkt []byte, fieldBitStart, bitlen int, rightAligned bool) (uint
const badUint64 = math.MaxUint64
octets := (bitlen + 7) / 8
if octets > 8 {
return badUint64, errors.New("field too long to be represented by uint64")
return badUint64, lneto.ErrUnsupported
}
var buf [8]byte
_, err := appendField(buf[8-octets:8-octets], pkt, fieldBitStart, bitlen, rightAligned)
@@ -725,13 +726,13 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool)
octets := (bitlen + 7) / 8 // total octets needed to represent field.
octetsStart := fieldBitStart / 8
if octets+octetsStart > len(pkt) {
return dst, errors.New("buffer overflow")
return dst, lneto.ErrShortBuffer
}
firstBitOffset := fieldBitStart % 8
lastOctetExcessBits := fieldBitEnd % 8
if firstBitOffset == 0 {
if rightAligned {
return dst, errors.New("invalid right aligned set for fully aligned field")
return dst, lneto.ErrBug
}
// Optimized path: field starts at byte boundary.
dst = append(dst, pkt[octetsStart:octetsStart+octets]...)
@@ -752,7 +753,7 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool)
// Right aligned with trailing bits. i.e: IPv6 Traffic Class.
// Field spans an extra byte, so need octets+1 bytes from packet.
if octets+octetsStart+1 > len(pkt) {
return dst, errors.New("buffer overflow")
return dst, lneto.ErrShortBuffer
}
for i := 0; i < octets; i++ {
b := (pkt[octetsStart+i] & mask) << (8 - firstBitOffset)
+3 -3
View File
@@ -3,7 +3,6 @@ package pcap
import (
"encoding/binary"
"encoding/hex"
"errors"
"math"
"net/netip"
"slices"
@@ -13,6 +12,7 @@ import (
_ "time"
"unsafe"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp"
@@ -162,7 +162,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
// inspired by [time.RFC3339]
const littlerfc3339 = "2006-01-02T15:04:05.9999"
if len(f.buf) != 8 {
return dst, errors.New("only timestamp8 supported")
return dst, lneto.ErrUnsupported
}
ts := ntp.TimestampFromUint64(binary.BigEndian.Uint64(f.buf))
dst = ts.Time().AppendFormat(dst, littlerfc3339)
@@ -223,7 +223,7 @@ func (f *Formatter) fieldAsUint(pkt []byte, fieldBitStart, bitlen int, rightAlig
const badUint64 = math.MaxUint64
octets := (bitlen + 7) / 8
if octets > 8 {
return badUint64, errors.New("field too long to be represented by uint64")
return badUint64, lneto.ErrUnsupported
}
f.uintBuf = [8]byte{}
_, err := appendField(f.uintBuf[8-octets:8-octets], pkt, fieldBitStart, bitlen, rightAligned)