Reduce heap allocations (#42)

* 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

* reuse function for reclaiming frames among all protocols for lower memory usage

* remove extraneous code
This commit is contained in:
Pat Whittingslow
2026-02-27 16:38:33 +01:00
committed by GitHub
parent 92b54487df
commit f9748da94f
11 changed files with 198 additions and 130 deletions
+3 -1
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"math" "math"
"math/bits" "math/bits"
"net" "net"
@@ -220,7 +221,8 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error {
msgOK := msgType == MsgOffer || msgType == MsgAck msgOK := msgType == MsgOffer || msgType == MsgAck
if !msgOK { if !msgOK {
return fmt.Errorf("invalid DHCP message received or none got=%d", msgType) internal.LogAttrs(nil, slog.LevelError, "invalid DHCP message", slog.Uint64("type", uint64(msgType)))
return lneto.ErrBug
} }
err = c.setOptions(frm) err = c.setOptions(frm)
if err != nil { if err != nil {
+4 -5
View File
@@ -209,8 +209,9 @@ func (m *Message) AppendTo(buf []byte, txid uint16, flags HeaderFlags) (_ []byte
nans := uint16(len(m.Answers)) nans := uint16(len(m.Answers))
nauth := uint16(len(m.Authorities)) nauth := uint16(len(m.Authorities))
nadd := uint16(len(m.Additionals)) nadd := uint16(len(m.Additionals))
var hdr [SizeHeader]byte buf = slices.Grow(buf, int(m.Len()))
f, err := NewFrame(hdr[:]) // Set the buffer directly with header fields.
f, err := NewFrame(buf[len(buf) : len(buf)+SizeHeader])
if err != nil { if err != nil {
return buf, err return buf, err
} }
@@ -220,9 +221,7 @@ func (m *Message) AppendTo(buf []byte, txid uint16, flags HeaderFlags) (_ []byte
f.SetANCount(nans) f.SetANCount(nans)
f.SetNSCount(nauth) f.SetNSCount(nauth)
f.SetARCount(nadd) f.SetARCount(nadd)
buf = buf[:len(buf)+SizeHeader]
buf = slices.Grow(buf, int(m.Len()))
buf = append(buf, hdr[:]...)
for _, q := range m.Questions { for _, q := range m.Questions {
buf, err = q.appendTo(buf) buf, err = q.appendTo(buf)
if err != nil { if err != nil {
+1
View File
@@ -20,6 +20,7 @@ type errGeneric uint8
// Generic errors common to internet functioning. // Generic errors common to internet functioning.
const ( const (
_ errGeneric = iota // non-initialized err _ errGeneric = iota // non-initialized err
ErrBug // lneto-bug(use build tag "debugheaplog")
ErrPacketDrop // packet dropped ErrPacketDrop // packet dropped
ErrBadCRC // incorrect checksum ErrBadCRC // incorrect checksum
ErrZeroSource // zero source(port/addr) ErrZeroSource // zero source(port/addr)
+19
View File
@@ -46,3 +46,22 @@ func SliceReuse[T any](buf *[]T, n int) {
*buf = (*buf)[:0] *buf = (*buf)[:0]
} }
} }
// SliceReclaim extends the slice length by one and returns a pointer to
// the new last element. The returned element is not zeroed, so callers
// can reuse any existing allocations it may hold from prior use.
//
// Beware: as the name implies SliceReclaim reuses previously held memory
// so it is up to caller to ensure the reclaimed data is coherent by zeroing out
// the needed fields and reusing the previously held slices or pointers responsibly.
func SliceReclaim[T any](ptr *[]T) *T {
b := *ptr
n := len(b)
if n == cap(b) {
var z T
*ptr = append(b, z)
} else {
*ptr = b[:n+1]
}
return &(*ptr)[n]
}
+22
View File
@@ -0,0 +1,22 @@
package internal
import (
"encoding/binary"
"log/slog"
)
// SlogAddr4 returns a slog.Attr for a 4-byte IPv4 address
// packed into a uint64 without allocating a string.
func SlogAddr4(key string, addr *[4]byte) slog.Attr {
u64Addr := uint64(binary.BigEndian.Uint32(addr[:]))
return slog.Uint64(key, u64Addr)
}
// SlogAddr6 returns a slog.Attr for a 6-byte hardware (MAC) address
// packed into a uint64 without allocating a string.
func SlogAddr6(key string, addr *[6]byte) slog.Attr {
var buf [8]byte
copy(buf[2:], addr[:])
u64Addr := binary.BigEndian.Uint64(buf[:])
return slog.Uint64(key, u64Addr)
}
+90 -106
View File
@@ -14,6 +14,7 @@ import (
"github.com/soypat/lneto/dns" "github.com/soypat/lneto/dns"
"github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/http/httpraw" "github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv4/icmpv4" "github.com/soypat/lneto/ipv4/icmpv4"
"github.com/soypat/lneto/ipv6" "github.com/soypat/lneto/ipv6"
@@ -43,7 +44,31 @@ type PacketBreakdown struct {
SubfieldLimit int 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) { func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if dst == nil {
dst = pc.initFrames()
}
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return dst, errors.New("ethernet must be parsed at byte boundary") return dst, errors.New("ethernet must be parsed at byte boundary")
} }
@@ -56,23 +81,17 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in
return dst, pc.validator().ErrPop() return dst, pc.validator().ErrPop()
} }
finfo := Frame{ finfo := reclaimFrame(&dst, ProtoEthernet, bitOffset, baseEthernetFields[:])
Protocol: ProtoEthernet,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseEthernetFields[:]...)
etype := efrm.EtherTypeOrSize() etype := efrm.EtherTypeOrSize()
end := 14*octet + bitOffset end := 14*octet + bitOffset
if etype.IsSize() { if etype.IsSize() {
finfo.Fields[len(finfo.Fields)-1].Class = FieldClassSize finfo.Fields[len(finfo.Fields)-1].Class = FieldClassSize
dst = append(dst, finfo) reclaimRemainingFrame(&dst, "Ethernet payload", FieldClassPayload, end, octet*len(pkt))
dst = append(dst, remainingFrameInfo("Ethernet payload", FieldClassPayload, end, octet*len(pkt)))
return dst, nil return dst, nil
} }
dst = append(dst, finfo)
if efrm.IsVLAN() { if efrm.IsVLAN() {
finfo.Fields = append(finfo.Fields, FrameField{Name: "VLAN Tag", Class: FieldClassType, FrameBitOffset: end, BitLength: 2 * octet}) finfo.Fields = append(finfo.Fields, FrameField{Name: "VLAN Tag", Class: FieldClassType, FrameBitOffset: end, BitLength: 2 * octet})
dst = append(dst, remainingFrameInfo("Ethernet VLAN", FieldClassPayload, end+2*octet, octet*len(pkt))) reclaimRemainingFrame(&dst, "Ethernet VLAN", FieldClassPayload, end+2*octet, octet*len(pkt))
return dst, nil return dst, nil
} }
switch etype { switch etype {
@@ -83,7 +102,7 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in
case ethernet.TypeIPv6: case ethernet.TypeIPv6:
dst, err = pc.CaptureIPv6(dst, pkt, end) dst, err = pc.CaptureIPv6(dst, pkt, end)
default: default:
dst = append(dst, remainingFrameInfo(etype, FieldClassPayload, end, octet*len(pkt))) reclaimRemainingFrame(&dst, etype, FieldClassPayload, end, octet*len(pkt))
} }
return dst, err return dst, err
} }
@@ -101,13 +120,8 @@ func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([
return dst, pc.validator().ErrPop() return dst, pc.validator().ErrPop()
} }
finfo := Frame{ finfo := reclaimFrame(&dst, ethernet.TypeARP, bitOffset, baseARPFields[:])
Protocol: ethernet.TypeARP,
PacketBitOffset: bitOffset,
}
const varstart = 8 * octet const varstart = 8 * octet
finfo.Fields = append(finfo.Fields, baseARPFields[:]...)
_, hlen := afrm.Hardware() _, hlen := afrm.Hardware()
_, plen := afrm.Protocol() _, plen := afrm.Protocol()
finfo.Fields = append(finfo.Fields, finfo.Fields = append(finfo.Fields,
@@ -136,7 +150,6 @@ func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([
BitLength: int(plen) * octet, BitLength: int(plen) * octet,
}, },
) )
dst = append(dst, finfo)
return dst, nil return dst, nil
} }
@@ -152,12 +165,7 @@ func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) (
if pc.validator().HasError() { if pc.validator().HasError() {
return dst, pc.validator().ErrPop() return dst, pc.validator().ErrPop()
} }
finfo := Frame{ reclaimFrame(&dst, ethernet.TypeIPv6, bitOffset, baseIPv6Fields[:])
Protocol: ethernet.TypeIPv6,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseIPv6Fields[:]...)
dst = append(dst, finfo)
proto := ifrm6.NextHeader() proto := ifrm6.NextHeader()
end := bitOffset + 40*octet end := bitOffset + 40*octet
var protoErrs []error var protoErrs []error
@@ -201,11 +209,7 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
} }
// limit packet to the actual IPv4 frame size // limit packet to the actual IPv4 frame size
pkt = pkt[:bitOffset/8+int(ifrm4.TotalLength())] pkt = pkt[:bitOffset/8+int(ifrm4.TotalLength())]
finfo := Frame{ finfo := reclaimFrame(&dst, ethernet.TypeIPv4, bitOffset, baseIPv4Fields[:])
Protocol: ethernet.TypeIPv4,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseIPv4Fields[:]...)
options := ifrm4.Options() options := ifrm4.Options()
if len(options) > 0 { if len(options) > 0 {
finfo.Fields = append(finfo.Fields, FrameField{ finfo.Fields = append(finfo.Fields, FrameField{
@@ -217,7 +221,6 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
if ifrm4.CalculateHeaderCRC() != 0 { if ifrm4.CalculateHeaderCRC() != 0 {
finfo.Errors = append(finfo.Errors, lneto.ErrBadCRC) finfo.Errors = append(finfo.Errors, lneto.ErrBadCRC)
} }
dst = append(dst, finfo)
proto := ifrm4.Protocol() proto := ifrm4.Protocol()
end := bitOffset + octet*ifrm4.HeaderLength() end := bitOffset + octet*ifrm4.HeaderLength()
var protoErrs []error var protoErrs []error
@@ -277,7 +280,7 @@ func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt
case lneto.IPProtoICMP: case lneto.IPProtoICMP:
dst, err = pc.CaptureICMPv4(dst, pkt, bitOffset) dst, err = pc.CaptureICMPv4(dst, pkt, bitOffset)
default: default:
dst = append(dst, remainingFrameInfo(proto, 0, bitOffset, octet*len(pkt))) reclaimRemainingFrame(&dst, proto, 0, bitOffset, octet*len(pkt))
} }
if len(ipProtoErrs) > 0 && len(dst) > nextFrame { if len(ipProtoErrs) > 0 && len(dst) > nextFrame {
dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErrs...) dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErrs...)
@@ -298,11 +301,7 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
return dst, pc.validator().ErrPop() return dst, pc.validator().ErrPop()
} }
end := bitOffset + octet*tfrm.HeaderLength() end := bitOffset + octet*tfrm.HeaderLength()
finfo := Frame{ finfo := reclaimFrame(&dst, lneto.IPProtoTCP, bitOffset, baseTCPFields[:])
Protocol: lneto.IPProtoTCP,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseTCPFields[:]...)
options := tfrm.Options() options := tfrm.Options()
if len(options) > 0 { if len(options) > 0 {
finfo.Fields = append(finfo.Fields, FrameField{ finfo.Fields = append(finfo.Fields, FrameField{
@@ -311,20 +310,16 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
BitLength: octet * len(options), BitLength: octet * len(options),
}) })
} }
dst = append(dst, finfo)
payload := tfrm.Payload() payload := tfrm.Payload()
if len(payload) > 0 { if len(payload) > 0 {
dst, err = pc.CaptureHTTP(dst, pkt, end) dst, err = pc.CaptureHTTP(dst, pkt, end)
if err != nil { if err != nil {
dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))) reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
} }
} }
return dst, nil return dst, nil
} }
// func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
// }
func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return dst, errors.New("UDP must be parsed at byte boundary") return dst, errors.New("UDP must be parsed at byte boundary")
@@ -337,12 +332,7 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
if pc.validator().HasError() { if pc.validator().HasError() {
return dst, pc.validator().ErrPop() return dst, pc.validator().ErrPop()
} }
finfo := Frame{ reclaimFrame(&dst, lneto.IPProtoUDP, bitOffset, baseUDPFields[:])
Protocol: lneto.IPProtoUDP,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseUDPFields[:]...)
dst = append(dst, finfo)
end := bitOffset + 8*octet end := bitOffset + 8*octet
payload := ufrm.Payload() payload := ufrm.Payload()
dstport := ufrm.DestinationPort() dstport := ufrm.DestinationPort()
@@ -355,7 +345,7 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
dst, err = pc.CaptureNTP(dst, pkt, end) dst, err = pc.CaptureNTP(dst, pkt, end)
} }
if err != nil { if err != nil {
dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))) reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
} }
return dst, nil return dst, nil
} }
@@ -370,11 +360,7 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int)
return dst, err return dst, err
} }
finfo := Frame{ finfo := reclaimFrame(&dst, lneto.IPProtoICMP, bitOffset, baseICMPv4Fields[:])
Protocol: lneto.IPProtoICMP,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseICMPv4Fields[:]...)
// Add type-specific fields. // Add type-specific fields.
switch ifrm.Type() { switch ifrm.Type() {
@@ -421,13 +407,12 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int)
}) })
} }
} }
dst = append(dst, finfo)
return dst, nil return dst, nil
} }
func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return nil, errors.New("DNS must be parsed at byte boundary") return dst, errors.New("DNS must be parsed at byte boundary")
} }
dnsData := pkt[bitOffset/8:] dnsData := pkt[bitOffset/8:]
pc.dmsg.LimitResourceDecoding(20, 20, 20, 20) pc.dmsg.LimitResourceDecoding(20, 20, 20, 20)
@@ -435,10 +420,7 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
if err != nil && !incomplete { if err != nil && !incomplete {
return dst, err return dst, err
} }
finfo := Frame{ finfo := reclaimFrame(&dst, "DNS", bitOffset, nil)
Protocol: "DNS",
PacketBitOffset: bitOffset,
}
if incomplete { if incomplete {
finfo.Errors = append(finfo.Errors, errors.New("pcap: could not parse all DNS resources; add higher limit")) finfo.Errors = append(finfo.Errors, errors.New("pcap: could not parse all DNS resources; add higher limit"))
} }
@@ -447,52 +429,46 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
FrameBitOffset: 0, FrameBitOffset: 0,
BitLength: int(off) * octet, BitLength: int(off) * octet,
}) })
dst = append(dst, finfo)
return dst, nil return dst, nil
} }
func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return nil, errors.New("NTP must be parsed at byte boundary") return dst, errors.New("NTP must be parsed at byte boundary")
} }
ntpData := pkt[bitOffset/8:] ntpData := pkt[bitOffset/8:]
_, err := ntp.NewFrame(ntpData) _, err := ntp.NewFrame(ntpData)
if err != nil { if err != nil {
return dst, err return dst, err
} }
finfo := Frame{ reclaimFrame(&dst, "NTP", bitOffset, baseNTPFields[:])
Protocol: "NTP",
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseNTPFields[:]...)
dst = append(dst, finfo)
return dst, nil return dst, nil
} }
func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return nil, errors.New("DHCP must be parsed at byte boundary") return dst, errors.New("DHCP must be parsed at byte boundary")
} }
dhcpData := pkt[bitOffset/8:] dhcpData := pkt[bitOffset/8:]
dfrm, err := dhcpv4.NewFrame(dhcpData) dfrm, err := dhcpv4.NewFrame(dhcpData)
if err != nil { if err != nil {
return nil, err return dst, err
}
finfo := Frame{
Protocol: "DHCPv4",
PacketBitOffset: bitOffset,
} }
finfo := reclaimFrame(&dst, "DHCPv4", bitOffset, baseDHCPv4Fields[:])
magic := dfrm.MagicCookie() magic := dfrm.MagicCookie()
if magic != dhcpv4.MagicCookie { if magic != dhcpv4.MagicCookie {
finfo.Errors = append(finfo.Errors, errors.New("incorrect DHCPv4 magic cookie")) finfo.Errors = append(finfo.Errors, errors.New("incorrect DHCPv4 magic cookie"))
} }
finfo.Fields = append(finfo.Fields, baseDHCPv4Fields[:]...)
options := dfrm.OptionsPayload() options := dfrm.OptionsPayload()
if len(options) > 0 && pc.SubfieldLimit > 0 { if len(options) > 0 && pc.SubfieldLimit > 0 {
var optfield FrameField // Reclaim FrameField from Fields backing array to reuse its SubFields backing array.
optfield.Class = FieldClassOptions optfield := internal.SliceReclaim(&finfo.Fields)
optfield.Name = "options" *optfield = FrameField{ // Ensure consistent zeroing of memory, but keep slice reference for reuse.
Class: FieldClassOptions,
SubFields: optfield.SubFields[:0],
Name: "options",
}
err = dfrm.ForEachOption(func(optoff int, opt dhcpv4.OptNum, data []byte) error { err = dfrm.ForEachOption(func(optoff int, opt dhcpv4.OptNum, data []byte) error {
if len(optfield.SubFields) >= pc.SubfieldLimit { if len(optfield.SubFields) >= pc.SubfieldLimit {
return errors.New("option cap limit surpassed for DHCP") return errors.New("option cap limit surpassed for DHCP")
@@ -544,12 +520,11 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int)
optfield.SubFields = append(optfield.SubFields, field) optfield.SubFields = append(optfield.SubFields, field)
return nil return nil
}) })
finfo.Fields = append(finfo.Fields, optfield) // optfield already in finfo.Fields via SliceReclaim, no append needed.
if err != nil { if err != nil {
finfo.Errors = append(finfo.Errors, err) finfo.Errors = append(finfo.Errors, err)
} }
} }
dst = append(dst, finfo)
return dst, nil return dst, nil
} }
@@ -589,7 +564,7 @@ func httpBodyClass(contentType, body []byte) FieldClass {
func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
const httpProtocol = "HTTP" const httpProtocol = "HTTP"
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return nil, errors.New("HTTP must be parsed at byte boundary") return dst, errors.New("HTTP must be parsed at byte boundary")
} }
const asResponse = true const asResponse = true
const asRequest = false const asRequest = false
@@ -606,25 +581,22 @@ func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) (
hdrLen := pc.hdr.BufferParsed() hdrLen := pc.hdr.BufferParsed()
body, _ := pc.hdr.Body() body, _ := pc.hdr.Body()
bodyClass := httpBodyClass(pc.hdr.Get("Content-Type"), body) bodyClass := httpBodyClass(pc.hdr.Get("Content-Type"), body)
dst = append(dst, Frame{ finfo := reclaimFrame(&dst, httpProtocol, bitOffset, nil)
Protocol: httpProtocol, finfo.Fields = append(finfo.Fields,
PacketBitOffset: bitOffset, FrameField{
Fields: []FrameField{ Name: "HTTP Header",
{ Class: FieldClassText,
Name: "HTTP Header", FrameBitOffset: 0,
Class: FieldClassText, BitLength: hdrLen * octet,
FrameBitOffset: 0,
BitLength: hdrLen * octet,
},
{
Name: "HTTP Body",
Class: bodyClass,
FrameBitOffset: hdrLen * octet,
BitLength: len(body) * octet,
},
}, },
}) FrameField{
return dst, err Name: "HTTP Body",
Class: bodyClass,
FrameBitOffset: hdrLen * octet,
BitLength: len(body) * octet,
},
)
return dst, nil
} }
func (pc *PacketBreakdown) validator() *lneto.Validator { func (pc *PacketBreakdown) validator() *lneto.Validator {
@@ -653,9 +625,9 @@ func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 }
func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 } func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 }
type Frame struct { type Frame struct {
PacketBitOffset int
Protocol any Protocol any
Fields []FrameField Fields []FrameField
PacketBitOffset int
Errors []error Errors []error
} }
@@ -1331,14 +1303,26 @@ var baseNTPFields = [...]FrameField{
}, },
} }
func remainingFrameInfo(proto any, class FieldClass, pktBitOffset, pktBitLen int) Frame { // reclaimFrame extends dst via [internal.SliceReclaim], resets the reclaimed Frame
return 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 any, bitOffset int, baseFields []FrameField) *Frame {
finfo := internal.SliceReclaim(dst)
*finfo = Frame{
PacketBitOffset: bitOffset,
Protocol: proto, Protocol: proto,
PacketBitOffset: pktBitOffset, Fields: append(finfo.Fields[:0], baseFields...),
Fields: []FrameField{ Errors: finfo.Errors[:0],
{
Class: class,
BitLength: pktBitLen - pktBitOffset,
}},
} }
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 any, class FieldClass, pktBitOffset, pktBitLen int) {
finfo := reclaimFrame(dst, proto, pktBitOffset, nil)
finfo.Fields = append(finfo.Fields, FrameField{
Class: class,
BitLength: pktBitLen - pktBitOffset,
})
} }
+43 -5
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"math"
"net/netip" "net/netip"
"slices" "slices"
"strconv" "strconv"
@@ -31,6 +32,7 @@ type Formatter struct {
DisableLegacyFilter bool DisableLegacyFilter bool
mubuf sync.Mutex mubuf sync.Mutex
buf []byte buf []byte
uintBuf [8]byte // scratch buffer for fieldAsUint to avoid TinyGo heap escape.
} }
// FormatFrames appends the formatted frame data to the destination buffer according the Formatter state. // FormatFrames appends the formatted frame data to the destination buffer according the Formatter state.
@@ -49,14 +51,17 @@ func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte
return dst, nil return dst, nil
} }
// FormatFrame // FormatFrame formats a single frame's protocol, fields, and errors into dst.
func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) { func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) {
sep := f.fieldSep() sep := f.fieldSep()
bitlen := frm.LenBits() bitlen := frm.LenBits()
dst = appendProtocol(dst, frm.Protocol)
if bitlen%8 == 0 { if bitlen%8 == 0 {
dst = fmt.Appendf(dst, "%s len=%d", frm.Protocol, bitlen/8) dst = append(dst, " len="...)
dst = strconv.AppendInt(dst, int64(bitlen/8), 10)
} else { } else {
dst = fmt.Appendf(dst, "%s bitlen=%d", frm.Protocol, bitlen) dst = append(dst, " bitlen="...)
dst = strconv.AppendInt(dst, int64(bitlen), 10)
} }
for ifield := range frm.Fields { for ifield := range frm.Fields {
@@ -68,7 +73,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP { if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP {
// TCP flags pretty print special case. // TCP flags pretty print special case.
dst = append(dst, "flags="...) dst = append(dst, "flags="...)
v, err := fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned()) v, err := f.fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
if err != nil { if err != nil {
return dst, err return dst, err
} }
@@ -158,7 +163,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation: case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
// IP, MAC addresses and ports. // IP, MAC addresses and ports.
if field.BitLength <= 16 { if field.BitLength <= 16 {
v, err := fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned()) v, err := f.fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
if err != nil { if err != nil {
return dst, err return dst, err
} }
@@ -198,3 +203,36 @@ func (f *Formatter) subfieldSep() string {
} }
return sep return sep
} }
// fieldAsUint evaluates a packet field as a uint64 using the Formatter's
// scratch buffer to avoid a TinyGo heap escape from a local [8]byte.
func (f *Formatter) fieldAsUint(pkt []byte, fieldBitStart, bitlen int, rightAligned bool) (uint64, error) {
const badUint64 = math.MaxUint64
octets := (bitlen + 7) / 8
if octets > 8 {
return badUint64, errors.New("field too long to be represented by uint64")
}
f.uintBuf = [8]byte{}
_, err := appendField(f.uintBuf[8-octets:8-octets], pkt, fieldBitStart, bitlen, rightAligned)
if err != nil {
return badUint64, err
}
return binary.BigEndian.Uint64(f.uintBuf[:]), nil
}
// appendProtocol appends the string representation of a Frame.Protocol value
// to dst without going through fmt.Appendf reflect machinery.
func appendProtocol(dst []byte, p any) []byte {
switch p := p.(type) {
case proto:
return append(dst, string(p)...)
case string:
return append(dst, p...)
case ethernet.Type:
return append(dst, p.String()...)
case lneto.IPProto:
return append(dst, p.String()...)
default:
return fmt.Appendf(dst, "%v", p)
}
}
+2 -2
View File
@@ -6,10 +6,10 @@ import (
"io" "io"
"log/slog" "log/slog"
"math" "math"
"net"
"github.com/soypat/lneto" "github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
) )
// StackEthernetConfig contains configuration parameters for [StackEthernet]. // StackEthernetConfig contains configuration parameters for [StackEthernet].
@@ -132,7 +132,7 @@ func (ls *StackEthernet) Demux(carrierData []byte, frameOffset int) (err error)
return err return err
} }
DROP: DROP:
ls.handlers.info("LinkStack:drop-packet", slog.String("dsthw", net.HardwareAddr(dstaddr[:]).String()), slog.String("ethertype", efrm.EtherTypeOrSize().String())) ls.handlers.info("LinkStack:drop-packet", internal.SlogAddr6("dsthw", dstaddr), slog.String("ethertype", efrm.EtherTypeOrSize().String()))
return lneto.ErrPacketDrop return lneto.ErrPacketDrop
} }
+1 -1
View File
@@ -104,7 +104,7 @@ func (sb *StackIP) Demux(carrierData []byte, offset int) error {
// nodeIdx := getNodeByProto(sb.handlers, uint16(proto)) // nodeIdx := getNodeByProto(sb.handlers, uint16(proto))
if node == nil { if node == nil {
// Drop packet. // Drop packet.
sb.handlers.info("ip:demux.drop", slog.String("dstaddr", netip.AddrFrom4(*ifrm.DestinationAddr()).String()), slog.String("proto", ifrm.Protocol().String())) sb.handlers.info("ip:demux.drop", internal.SlogAddr4("dstaddr", ifrm.DestinationAddr()), slog.String("proto", ifrm.Protocol().String()))
return lneto.ErrPacketDrop return lneto.ErrPacketDrop
} }
// Incoming CRC Validation of common IP Protocols. // Incoming CRC Validation of common IP Protocols.
+7 -6
View File
@@ -194,15 +194,16 @@ func _() {
// An "invalid array index" compiler error signifies that the constant values have changed. // An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again. // Re-run the stringer command to generate them again.
var x [1]struct{} var x [1]struct{}
_ = x[ErrPacketDrop-1] _ = x[ErrBug-1]
_ = x[ErrBadCRC-2] _ = x[ErrPacketDrop-2]
_ = x[ErrZeroSource-3] _ = x[ErrBadCRC-3]
_ = x[ErrZeroDestination-4] _ = x[ErrZeroSource-4]
_ = x[ErrZeroDestination-5]
} }
const _errGeneric_name = "packet droppedincorrect checksumzero source(port/addr)zero destination(port/addr)" const _errGeneric_name = "lneto-bug(use build tag \"debugheaplog\")packet droppedincorrect checksumzero source(port/addr)zero destination(port/addr)"
var _errGeneric_index = [...]uint8{0, 14, 32, 54, 81} var _errGeneric_index = [...]uint8{0, 39, 53, 71, 93, 120}
func (i errGeneric) String() string { func (i errGeneric) String() string {
i -= 1 i -= 1
+6 -4
View File
@@ -48,6 +48,8 @@ type StackAsync struct {
prng uint32 prng uint32
addrBuf [6]byte // Temporary buffer for As4()/HardwareAddr6() results to avoid heap escapes.
totalsent uint64 totalsent uint64
totalrecv uint64 totalrecv uint64
} }
@@ -353,8 +355,8 @@ func (s *StackAsync) StartLookupIP(host string) error {
if err != nil { if err != nil {
return err return err
} }
dns4 := s.dnssv.As4() *(*[4]byte)(s.addrBuf[:4]) = s.dnssv.As4()
s.dnsUDP.SetStackNode(&s.dns, dns4[:], dns.ServerPort) s.dnsUDP.SetStackNode(&s.dns, s.addrBuf[:4], dns.ServerPort)
err = s.udps.Register(&s.dnsUDP) err = s.udps.Register(&s.dnsUDP)
return err return err
} }
@@ -417,8 +419,8 @@ func (s *StackAsync) StartNTP(addr netip.Addr) error {
defer s.mu.Unlock() defer s.mu.Unlock()
s.ntp.Reset(s.sysprec, time.Now) s.ntp.Reset(s.sysprec, time.Now)
addr4 := addr.As4() *(*[4]byte)(s.addrBuf[:4]) = addr.As4()
s.ntpUDP.SetStackNode(&s.ntp, addr4[:], ntp.ServerPort) s.ntpUDP.SetStackNode(&s.ntp, s.addrBuf[:4], ntp.ServerPort)
err := s.udps.Register(&s.ntpUDP) err := s.udps.Register(&s.ntpUDP)
return err return err
} }