From f9748da94f767cf27bb5cbb0f6b7e35605d590a9 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Fri, 27 Feb 2026 16:38:33 +0100 Subject: [PATCH] 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 --- dhcpv4/client.go | 4 +- dns/dns.go | 9 +- errors.go | 1 + internal/slices.go | 19 ++++ internal/slogattr.go | 22 +++++ internet/pcap/capture.go | 196 +++++++++++++++++-------------------- internet/pcap/format.go | 48 ++++++++- internet/stack-ethernet.go | 4 +- internet/stack-ip.go | 2 +- stringers.go | 13 +-- x/xnet/stack-async.go | 10 +- 11 files changed, 198 insertions(+), 130 deletions(-) create mode 100644 internal/slogattr.go diff --git a/dhcpv4/client.go b/dhcpv4/client.go index 3d6ca68..27c6869 100644 --- a/dhcpv4/client.go +++ b/dhcpv4/client.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "log/slog" "math" "math/bits" "net" @@ -220,7 +221,8 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error { msgOK := msgType == MsgOffer || msgType == MsgAck 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) if err != nil { diff --git a/dns/dns.go b/dns/dns.go index 1521e4b..025ff00 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -209,8 +209,9 @@ func (m *Message) AppendTo(buf []byte, txid uint16, flags HeaderFlags) (_ []byte nans := uint16(len(m.Answers)) nauth := uint16(len(m.Authorities)) nadd := uint16(len(m.Additionals)) - var hdr [SizeHeader]byte - f, err := NewFrame(hdr[:]) + buf = slices.Grow(buf, int(m.Len())) + // Set the buffer directly with header fields. + f, err := NewFrame(buf[len(buf) : len(buf)+SizeHeader]) if err != nil { return buf, err } @@ -220,9 +221,7 @@ func (m *Message) AppendTo(buf []byte, txid uint16, flags HeaderFlags) (_ []byte f.SetANCount(nans) f.SetNSCount(nauth) f.SetARCount(nadd) - - buf = slices.Grow(buf, int(m.Len())) - buf = append(buf, hdr[:]...) + buf = buf[:len(buf)+SizeHeader] for _, q := range m.Questions { buf, err = q.appendTo(buf) if err != nil { diff --git a/errors.go b/errors.go index a00b818..e20fca2 100644 --- a/errors.go +++ b/errors.go @@ -20,6 +20,7 @@ type errGeneric uint8 // Generic errors common to internet functioning. const ( _ errGeneric = iota // non-initialized err + ErrBug // lneto-bug(use build tag "debugheaplog") ErrPacketDrop // packet dropped ErrBadCRC // incorrect checksum ErrZeroSource // zero source(port/addr) diff --git a/internal/slices.go b/internal/slices.go index f1b11fa..42ab151 100644 --- a/internal/slices.go +++ b/internal/slices.go @@ -46,3 +46,22 @@ func SliceReuse[T any](buf *[]T, n int) { *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] +} diff --git a/internal/slogattr.go b/internal/slogattr.go new file mode 100644 index 0000000..a138495 --- /dev/null +++ b/internal/slogattr.go @@ -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) +} diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index e8d89f8..7319242 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -14,6 +14,7 @@ import ( "github.com/soypat/lneto/dns" "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" @@ -43,7 +44,31 @@ type PacketBreakdown struct { 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, 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() } - finfo := Frame{ - Protocol: ProtoEthernet, - PacketBitOffset: bitOffset, - } - finfo.Fields = append(finfo.Fields, baseEthernetFields[:]...) + finfo := reclaimFrame(&dst, ProtoEthernet, bitOffset, baseEthernetFields[:]) etype := efrm.EtherTypeOrSize() end := 14*octet + bitOffset if etype.IsSize() { finfo.Fields[len(finfo.Fields)-1].Class = FieldClassSize - dst = append(dst, finfo) - dst = append(dst, remainingFrameInfo("Ethernet payload", FieldClassPayload, end, octet*len(pkt))) + reclaimRemainingFrame(&dst, "Ethernet payload", FieldClassPayload, end, octet*len(pkt)) return dst, nil } - dst = append(dst, finfo) if efrm.IsVLAN() { 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 } switch etype { @@ -83,7 +102,7 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in case ethernet.TypeIPv6: dst, err = pc.CaptureIPv6(dst, pkt, end) default: - dst = append(dst, remainingFrameInfo(etype, FieldClassPayload, end, octet*len(pkt))) + reclaimRemainingFrame(&dst, etype, FieldClassPayload, end, octet*len(pkt)) } return dst, err } @@ -101,13 +120,8 @@ func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([ return dst, pc.validator().ErrPop() } - finfo := Frame{ - Protocol: ethernet.TypeARP, - PacketBitOffset: bitOffset, - } - + finfo := reclaimFrame(&dst, ethernet.TypeARP, bitOffset, baseARPFields[:]) const varstart = 8 * octet - finfo.Fields = append(finfo.Fields, baseARPFields[:]...) _, hlen := afrm.Hardware() _, plen := afrm.Protocol() finfo.Fields = append(finfo.Fields, @@ -136,7 +150,6 @@ func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([ BitLength: int(plen) * octet, }, ) - dst = append(dst, finfo) return dst, nil } @@ -152,12 +165,7 @@ func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) ( if pc.validator().HasError() { return dst, pc.validator().ErrPop() } - finfo := Frame{ - Protocol: ethernet.TypeIPv6, - PacketBitOffset: bitOffset, - } - finfo.Fields = append(finfo.Fields, baseIPv6Fields[:]...) - dst = append(dst, finfo) + reclaimFrame(&dst, ethernet.TypeIPv6, bitOffset, baseIPv6Fields[:]) proto := ifrm6.NextHeader() end := bitOffset + 40*octet 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 pkt = pkt[:bitOffset/8+int(ifrm4.TotalLength())] - finfo := Frame{ - Protocol: ethernet.TypeIPv4, - PacketBitOffset: bitOffset, - } - finfo.Fields = append(finfo.Fields, baseIPv4Fields[:]...) + finfo := reclaimFrame(&dst, ethernet.TypeIPv4, bitOffset, baseIPv4Fields[:]) options := ifrm4.Options() if len(options) > 0 { finfo.Fields = append(finfo.Fields, FrameField{ @@ -217,7 +221,6 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( if ifrm4.CalculateHeaderCRC() != 0 { finfo.Errors = append(finfo.Errors, lneto.ErrBadCRC) } - dst = append(dst, finfo) proto := ifrm4.Protocol() end := bitOffset + octet*ifrm4.HeaderLength() var protoErrs []error @@ -277,7 +280,7 @@ func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt case lneto.IPProtoICMP: dst, err = pc.CaptureICMPv4(dst, pkt, bitOffset) 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 { 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() } end := bitOffset + octet*tfrm.HeaderLength() - finfo := Frame{ - Protocol: lneto.IPProtoTCP, - PacketBitOffset: bitOffset, - } - finfo.Fields = append(finfo.Fields, baseTCPFields[:]...) + finfo := reclaimFrame(&dst, lneto.IPProtoTCP, bitOffset, baseTCPFields[:]) options := tfrm.Options() if len(options) > 0 { finfo.Fields = append(finfo.Fields, FrameField{ @@ -311,20 +310,16 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([ BitLength: octet * len(options), }) } - dst = append(dst, finfo) payload := tfrm.Payload() if len(payload) > 0 { dst, err = pc.CaptureHTTP(dst, pkt, end) if err != nil { - dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))) + reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)) } } 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) { if bitOffset%8 != 0 { 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() { return dst, pc.validator().ErrPop() } - finfo := Frame{ - Protocol: lneto.IPProtoUDP, - PacketBitOffset: bitOffset, - } - finfo.Fields = append(finfo.Fields, baseUDPFields[:]...) - dst = append(dst, finfo) + reclaimFrame(&dst, lneto.IPProtoUDP, bitOffset, baseUDPFields[:]) end := bitOffset + 8*octet payload := ufrm.Payload() dstport := ufrm.DestinationPort() @@ -355,7 +345,7 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([ dst, err = pc.CaptureNTP(dst, pkt, end) } if err != nil { - dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))) + reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)) } return dst, nil } @@ -370,11 +360,7 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int) return dst, err } - finfo := Frame{ - Protocol: lneto.IPProtoICMP, - PacketBitOffset: bitOffset, - } - finfo.Fields = append(finfo.Fields, baseICMPv4Fields[:]...) + finfo := reclaimFrame(&dst, lneto.IPProtoICMP, bitOffset, baseICMPv4Fields[:]) // Add type-specific fields. switch ifrm.Type() { @@ -421,13 +407,12 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int) }) } } - dst = append(dst, finfo) return dst, nil } func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { 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:] 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 { return dst, err } - finfo := Frame{ - Protocol: "DNS", - PacketBitOffset: bitOffset, - } + 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")) } @@ -447,52 +429,46 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([ FrameBitOffset: 0, BitLength: int(off) * octet, }) - dst = append(dst, finfo) return dst, nil } func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { 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:] _, err := ntp.NewFrame(ntpData) if err != nil { return dst, err } - finfo := Frame{ - Protocol: "NTP", - PacketBitOffset: bitOffset, - } - finfo.Fields = append(finfo.Fields, baseNTPFields[:]...) - dst = append(dst, finfo) + reclaimFrame(&dst, "NTP", bitOffset, baseNTPFields[:]) return dst, nil } func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { 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:] dfrm, err := dhcpv4.NewFrame(dhcpData) if err != nil { - return nil, err - } - finfo := Frame{ - Protocol: "DHCPv4", - PacketBitOffset: bitOffset, + return dst, err } + 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.Fields = append(finfo.Fields, baseDHCPv4Fields[:]...) options := dfrm.OptionsPayload() if len(options) > 0 && pc.SubfieldLimit > 0 { - var optfield FrameField - optfield.Class = FieldClassOptions - optfield.Name = "options" + // 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", + } 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") @@ -544,12 +520,11 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) optfield.SubFields = append(optfield.SubFields, field) return nil }) - finfo.Fields = append(finfo.Fields, optfield) + // optfield already in finfo.Fields via SliceReclaim, no append needed. if err != nil { finfo.Errors = append(finfo.Errors, err) } } - dst = append(dst, finfo) 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) { const httpProtocol = "HTTP" 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 asRequest = false @@ -606,25 +581,22 @@ func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) ( hdrLen := pc.hdr.BufferParsed() body, _ := pc.hdr.Body() bodyClass := httpBodyClass(pc.hdr.Get("Content-Type"), body) - dst = append(dst, Frame{ - Protocol: httpProtocol, - PacketBitOffset: bitOffset, - Fields: []FrameField{ - { - Name: "HTTP Header", - Class: FieldClassText, - FrameBitOffset: 0, - BitLength: hdrLen * octet, - }, - { - Name: "HTTP Body", - Class: bodyClass, - FrameBitOffset: hdrLen * octet, - BitLength: len(body) * octet, - }, + finfo := reclaimFrame(&dst, httpProtocol, bitOffset, nil) + finfo.Fields = append(finfo.Fields, + FrameField{ + Name: "HTTP Header", + Class: FieldClassText, + FrameBitOffset: 0, + BitLength: hdrLen * octet, }, - }) - return dst, err + FrameField{ + Name: "HTTP Body", + Class: bodyClass, + FrameBitOffset: hdrLen * octet, + BitLength: len(body) * octet, + }, + ) + return dst, nil } 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 } type Frame struct { + PacketBitOffset int Protocol any Fields []FrameField - PacketBitOffset int Errors []error } @@ -1331,14 +1303,26 @@ var baseNTPFields = [...]FrameField{ }, } -func remainingFrameInfo(proto any, class FieldClass, pktBitOffset, pktBitLen int) Frame { - return Frame{ +// 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 any, bitOffset int, baseFields []FrameField) *Frame { + finfo := internal.SliceReclaim(dst) + *finfo = Frame{ + PacketBitOffset: bitOffset, Protocol: proto, - PacketBitOffset: pktBitOffset, - Fields: []FrameField{ - { - Class: class, - BitLength: pktBitLen - pktBitOffset, - }}, + Fields: append(finfo.Fields[:0], baseFields...), + Errors: finfo.Errors[:0], } + 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, + }) } diff --git a/internet/pcap/format.go b/internet/pcap/format.go index 4696d37..637060f 100644 --- a/internet/pcap/format.go +++ b/internet/pcap/format.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "math" "net/netip" "slices" "strconv" @@ -31,6 +32,7 @@ type Formatter struct { DisableLegacyFilter bool mubuf sync.Mutex 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. @@ -49,14 +51,17 @@ func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte 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) { sep := f.fieldSep() bitlen := frm.LenBits() + dst = appendProtocol(dst, frm.Protocol) 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 { - 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 { @@ -68,7 +73,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP { // TCP flags pretty print special case. 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 { return dst, err } @@ -158,7 +163,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation: // IP, MAC addresses and ports. 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 { return dst, err } @@ -198,3 +203,36 @@ func (f *Formatter) subfieldSep() string { } 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) + } +} diff --git a/internet/stack-ethernet.go b/internet/stack-ethernet.go index 6e1dad6..71e0e5c 100644 --- a/internet/stack-ethernet.go +++ b/internet/stack-ethernet.go @@ -6,10 +6,10 @@ import ( "io" "log/slog" "math" - "net" "github.com/soypat/lneto" "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/internal" ) // StackEthernetConfig contains configuration parameters for [StackEthernet]. @@ -132,7 +132,7 @@ func (ls *StackEthernet) Demux(carrierData []byte, frameOffset int) (err error) return err } 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 } diff --git a/internet/stack-ip.go b/internet/stack-ip.go index 804c941..d0bb87e 100644 --- a/internet/stack-ip.go +++ b/internet/stack-ip.go @@ -104,7 +104,7 @@ func (sb *StackIP) Demux(carrierData []byte, offset int) error { // nodeIdx := getNodeByProto(sb.handlers, uint16(proto)) if node == nil { // 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 } // Incoming CRC Validation of common IP Protocols. diff --git a/stringers.go b/stringers.go index c6e4db1..a2799ec 100644 --- a/stringers.go +++ b/stringers.go @@ -194,15 +194,16 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[ErrPacketDrop-1] - _ = x[ErrBadCRC-2] - _ = x[ErrZeroSource-3] - _ = x[ErrZeroDestination-4] + _ = x[ErrBug-1] + _ = x[ErrPacketDrop-2] + _ = x[ErrBadCRC-3] + _ = 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 { i -= 1 diff --git a/x/xnet/stack-async.go b/x/xnet/stack-async.go index 5e7469e..28ac584 100644 --- a/x/xnet/stack-async.go +++ b/x/xnet/stack-async.go @@ -48,6 +48,8 @@ type StackAsync struct { prng uint32 + addrBuf [6]byte // Temporary buffer for As4()/HardwareAddr6() results to avoid heap escapes. + totalsent uint64 totalrecv uint64 } @@ -353,8 +355,8 @@ func (s *StackAsync) StartLookupIP(host string) error { if err != nil { return err } - dns4 := s.dnssv.As4() - s.dnsUDP.SetStackNode(&s.dns, dns4[:], dns.ServerPort) + *(*[4]byte)(s.addrBuf[:4]) = s.dnssv.As4() + s.dnsUDP.SetStackNode(&s.dns, s.addrBuf[:4], dns.ServerPort) err = s.udps.Register(&s.dnsUDP) return err } @@ -417,8 +419,8 @@ func (s *StackAsync) StartNTP(addr netip.Addr) error { defer s.mu.Unlock() s.ntp.Reset(s.sysprec, time.Now) - addr4 := addr.As4() - s.ntpUDP.SetStackNode(&s.ntp, addr4[:], ntp.ServerPort) + *(*[4]byte)(s.addrBuf[:4]) = addr.As4() + s.ntpUDP.SetStackNode(&s.ntp, s.addrBuf[:4], ntp.ServerPort) err := s.udps.Register(&s.ntpUDP) return err }