Reduce heap allocs 2 (#46)

* reduce heap allocations in tcp logging; omit use of AppendFloat which allocates a metric sh*tton

* debugheaplog: better heap statistic logging

* heap: use string for pcap.Frame.Protocol

* add potential to eliminate Flags.String heap alloc, remove incorrect HEAP comments

* add StackAsync.DebugErr and httpraw.SetBytes

* many heap alloc reductions and replacement of bytes.Equal with internal.BytesEqual
This commit is contained in:
Pat Whittingslow
2026-02-28 19:20:46 +01:00
committed by GitHub
parent eab43c4653
commit 989bb6a0b9
22 changed files with 511 additions and 163 deletions
+4 -5
View File
@@ -1,7 +1,6 @@
package arp
import (
"bytes"
"errors"
"log/slog"
@@ -101,7 +100,7 @@ func (h *Handler) expectSize() int {
func (h *Handler) QueryResult(protoAddr []byte) (hwAddr []byte, err error) {
for i := range h.queries {
if bytes.Equal(protoAddr, h.queries[i].protoaddr) {
if internal.BytesEqual(protoAddr, h.queries[i].protoaddr) {
if !h.queries[i].querysent {
return nil, errors.New("query not yet sent")
}
@@ -118,7 +117,7 @@ func (h *Handler) QueryResult(protoAddr []byte) (hwAddr []byte, err error) {
func (h *Handler) DiscardQuery(protoAddr []byte) error {
for i := range h.queries {
q := &h.queries[i]
if bytes.Equal(protoAddr, q.protoaddr) {
if internal.BytesEqual(protoAddr, q.protoaddr) {
q.destroy()
return nil
}
@@ -240,7 +239,7 @@ func (h *Handler) Demux(ethFrame []byte, frameOffset int) error {
switch afrm.Operation() {
case OpRequest:
_, protoaddr := afrm.Target()
if !bytes.Equal(protoaddr, h.ourProtoAddr) {
if !internal.BytesEqual(protoaddr, h.ourProtoAddr) {
return nil // Not for us.
}
h.pendingResponse = h.pendingResponse[:len(h.pendingResponse)+1] // Extend pending buffer.
@@ -251,7 +250,7 @@ func (h *Handler) Demux(ethFrame []byte, frameOffset int) error {
for i := range h.queries {
q := &h.queries[i]
mac := q.response()
if mac == nil && bytes.Equal(q.protoaddr, protoaddr) {
if mac == nil && internal.BytesEqual(q.protoaddr, protoaddr) {
q.hwaddr = append(q.hwaddr, hwaddr...)
if q.dstHw != nil {
if !internal.IsZeroed(q.dstHw...) {
+1 -2
View File
@@ -2,7 +2,6 @@ package httpraw
import (
"bytes"
"errors"
)
// Cookie implements cookie key-value parsing. Methods function similarly to eponymous [Header] methods.
@@ -55,7 +54,7 @@ func (dst *Cookie) CopyFrom(c Cookie) {
// Parse parses the cookie's buffer in place.
func (c *Cookie) Parse() error {
if len(c.kvs) > 0 {
return errors.New("cookies already parsed, reset before parsing again")
return errCookiesParsed
}
off := 0
for {
+20 -5
View File
@@ -2,9 +2,9 @@ package httpraw
import (
"bytes"
"errors"
"io"
"slices"
"unsafe"
)
const (
@@ -75,7 +75,9 @@ func (h *Header) ParseBytes(asResponse bool, b []byte) error {
// Parse parses accumulated data in-place with no copying. One can set HTTP header data buffer with [Header.Reset].
// It fails if HTTP data is incomplete.
func (h *Header) Parse(asResponse bool) error {
debuglog("http:parse:reset")
h.Reset(h.hbuf.buf)
debuglog("http:parse:start")
return h.parse(asResponse)
}
@@ -97,7 +99,7 @@ func (h *Header) Parse(asResponse bool) error {
// }
func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) {
if h.flags.hasAny(flagDoneParsingHeader) {
return false, errors.New("TryParse called after header parsed")
return false, errAlreadyParsed
} else if h.flags.hasAny(flagMangledBuffer) {
return false, errMangledBuffer
}
@@ -225,15 +227,18 @@ func (h *Header) Reset(buf []byte) {
panic("small buffer and flagNoBufferGrow set")
}
const persistentFlags = flagNoBufferGrow
debuglog("http:reset:hbuf")
h.hbuf.reset(buf)
*h = Header{
hbuf: h.hbuf,
flags: h.flags & persistentFlags,
}
debuglog("http:reset:done")
}
// Body returns the surplus data following headers. It is only valid as long as Parse* or Reset methods are not called.
func (h *Header) Body() ([]byte, error) {
debuglog("http:body")
if h.flags.hasAny(flagMangledBuffer) {
return nil, errMangledBuffer
} else if h.flags.hasAny(flagDoneParsingHeader) {
@@ -242,7 +247,14 @@ func (h *Header) Body() ([]byte, error) {
return nil, errUnparsed
}
// Set sets a key-value pair in the HTTP header. Calling Set mangles the buffer.
// SetBytes is equivalent to [Header.Set] but with a []byte value. Does not keep reference to value slice.
// Calling SetBytes Mangles the buffer.
func (h *Header) SetBytes(key string, value []byte) {
h.Set(key, unsafe.String(&value[0], len(value)))
}
// Set sets a key-value pair in the HTTP header.
// Calling Set mangles the buffer.
func (h *Header) Set(key, value string) {
hb := &h.hbuf
var useKv *argsKV
@@ -269,10 +281,13 @@ func (h *Header) Set(key, value string) {
// Get gets the first value of a key found in the headers. Use [Header.ForEach] to find multiple values corresponding to same key.
func (h *Header) Get(key string) []byte {
debuglog("http:get:start")
kv := h.peekHeader(key)
if kv.isValid() {
debuglog("http:get:found")
return h.hbuf.musttoken(kv.value)
}
debuglog("http:get:notfound")
return nil
}
@@ -338,7 +353,7 @@ func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
if h.flags.hasAny(flagOOMReached) {
return dst, errOOM
} else if h.requestURI.len == 0 || h.method.len == 0 {
return dst, errors.New("need method/request URI to create request header")
return dst, errNeedMethodURI
} else if len(proto) == 0 {
return dst, errNoProto
}
@@ -368,7 +383,7 @@ func (h *Header) AppendResponse(dst []byte) ([]byte, error) {
if h.flags.hasAny(flagOOMReached) {
return dst, errOOM
} else if h.statusCode.len == 0 || h.statusText.len == 0 {
return dst, errors.New("invalid status code or text")
return dst, errBadStatusCodeTxt
} else if len(proto) == 0 {
return dst, errNoProto
}
+52 -21
View File
@@ -3,8 +3,11 @@ package httpraw
import (
"bytes"
"errors"
"log/slog"
"slices"
"unsafe"
"github.com/soypat/lneto/internal"
)
var (
@@ -16,8 +19,15 @@ var (
errOOM = errors.New("httpraw: buffer out of memory")
// Header.Set and Header.Add mangles the buffer.
// Call them after retrieving the Body. Do not call them before parsing the header (why would you even do that?).
errMangledBuffer = errors.New("httpraw: mangled buffer")
errNoCookies = errors.New("no cookie found")
errMangledBuffer = errors.New("httpraw: mangled buffer")
errNoCookies = errors.New("no cookie found")
errEmptyURI = errors.New("empty URI")
errLongStatusCode = errors.New("long status code")
errBadStatusCode = errors.New("invalid status code")
errAlreadyParsed = errors.New("TryParse called after header parsed")
errNeedMethodURI = errors.New("need method/request URI to create request header")
errBadStatusCodeTxt = errors.New("invalid status code or text")
errCookiesParsed = errors.New("cookies already parsed, reset before parsing again")
)
type headerBuf struct {
@@ -29,6 +39,20 @@ type headerBuf struct {
headers []argsKV
}
// reset sets the buffer data and discards all parsed data.
func (h *headerBuf) reset(buf []byte) {
if buf == nil {
buf = h.buf[:0] // Reuse buffer but discard raw data on nil input.
}
if cap(h.headers) == 0 {
h.headers = make([]argsKV, 16)
}
*h = headerBuf{
buf: buf,
headers: h.headers[:0],
}
}
type tokint = uint16
type headerSlice struct {
@@ -56,11 +80,16 @@ type scannerState struct {
}
func (h *Header) parse(asResponse bool) (err error) {
debuglog("http:firstline:start")
err = h.parseFirstLine(asResponse)
if err != nil {
debuglog("http:firstline:err")
return err
}
return h.parseNextHeaders()
debuglog("http:firstline:done")
err = h.parseNextHeaders()
debuglog("http:headers:done")
return err
}
func (h *Header) parseFirstLine(asResponse bool) (err error) {
@@ -90,9 +119,11 @@ func (hb *headerBuf) readFromBytes(b []byte) {
func (hb *headerBuf) free() int { return cap(hb.buf) - len(hb.buf) }
func (hb *headerBuf) parseNextHeaders(ss *scannerState) {
debuglog("http:nexthdr:loop")
for kv := hb.next(ss); kv.isValid(); kv = hb.next(ss) {
hb.headers = append(hb.headers, kv)
hb.headers = append(hb.headers, kv) // TODO(HEAP): inc=16B slice growth when capacity exceeded
}
debuglog("http:nexthdr:done")
}
func (hb *headerBuf) offBuf() []byte {
@@ -127,6 +158,7 @@ func (hb *headerBuf) scanUntilByte(c byte) []byte {
}
func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto headerSlice, flags flags, err error) {
debuglog("http:req:scan")
hb.off = 0 // Parsing first line resets offset.
var b []byte
hb.skipLeadingCRLF()
@@ -135,6 +167,7 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto
if len(b) < 5 {
return method, uri, proto, flags, errNeedMore
}
debuglog("http:req:parse")
methodEnd := max(0, bytes.IndexByte(b, ' '))
reqURIEnd := bytes.IndexByte(b[methodEnd+1:], ' ')
@@ -145,7 +178,7 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto
flags |= flagNoHTTP11
}
} else if reqURIEnd == 0 {
return method, uri, proto, flags, errors.New("empty URI")
return method, uri, proto, flags, errEmptyURI
} else {
// No version provided.
reqURIEnd = methodEnd + 1
@@ -158,6 +191,7 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto
}
func (hb *headerBuf) parseFirstLineResponse(initFlags flags) (statusCode, statusText headerSlice, flags flags, err error) {
debuglog("http:resp:scan")
hb.off = 0 // Parsing first line resets offset.
var b []byte
hb.skipLeadingCRLF()
@@ -166,23 +200,23 @@ func (hb *headerBuf) parseFirstLineResponse(initFlags flags) (statusCode, status
if len(b) < 5 {
return statusCode, statusText, flags, errNeedMore
}
debuglog("http:resp:parse")
statusCodeEnd := max(0, bytes.IndexByte(b, ' '))
if statusCodeEnd < 0 {
return statusCode, statusText, flags, errors.New("missing status code")
}
code := b[:statusCodeEnd]
text := b[statusCodeEnd:]
if len(code) > 3 {
return statusCode, statusText, flags, errors.New("long status code")
return statusCode, statusText, flags, errLongStatusCode
}
for i := range code {
if code[i] > '9' || code[i] < '0' {
return statusCode, statusText, flags, errors.New("invalid status code")
debuglog("http:resp:invalid-code")
return statusCode, statusText, flags, errBadStatusCode
}
}
statusCode = hb.slice(code)
statusText = hb.slice(text)
debuglog("http:resp:done")
return statusCode, statusText, flags, nil
}
@@ -360,17 +394,6 @@ func (hb *headerBuf) next(ss *scannerState) argsKV {
return resultKV
}
// reset sets the buffer data and discards all parsed data.
func (h *headerBuf) reset(buf []byte) {
if buf == nil {
buf = h.buf[:0] // Reuse buffer but discard raw data on nil input.
}
*h = headerBuf{
buf: buf,
headers: h.headers[:0],
}
}
// ConnectionClose returns true if 'Connection: close' header is set or if a invalid header was found.
func (h *Header) ConnectionClose() bool {
closed := h.flags.hasAny(flagConnClose) ||
@@ -402,3 +425,11 @@ func bytes2tok(buf, value []byte) headerSlice {
len: tokint(len(value)),
}
}
const enableDebug = false
func debuglog(msg string) {
if enableDebug {
internal.LogAttrs(nil, slog.LevelDebug, msg)
}
}
+80 -1
View File
@@ -1,7 +1,86 @@
package internal
import "log/slog"
import (
"log/slog"
"runtime"
"strconv"
"sync"
"unsafe"
)
const (
LevelTrace slog.Level = slog.LevelDebug - 2
usePrintLogAllocs = true
)
var (
memstats runtime.MemStats
lastAllocs uint64
lastMallocs uint64
allocmu sync.Mutex
allocbuf [256]byte
)
func LogAllocs(msg string) {
allocmu.Lock()
runtime.ReadMemStats(&memstats)
if memstats.TotalAlloc == lastAllocs {
allocmu.Unlock()
return
}
if usePrintLogAllocs {
print("[ALLOC] ", msg)
print(" inc=", int64(memstats.TotalAlloc)-int64(lastAllocs))
print(" n=", int64(memstats.Mallocs)-int64(lastMallocs))
print(" heap=", memstats.HeapAlloc)
print(" tot=", memstats.TotalAlloc)
println()
} else {
n := copy(allocbuf[:], "[ALLOC] ")
n += copy(allocbuf[n:], msg)
n += copyValueInt(allocbuf[n:], "inc", int64(memstats.TotalAlloc)-int64(lastAllocs))
n += copyValueInt(allocbuf[n:], "n", int64(memstats.Mallocs)-int64(lastMallocs))
n += copyValueUint(allocbuf[n:], "heap", memstats.HeapAlloc)
n += copyValueUint(allocbuf[n:], "tot", memstats.TotalAlloc)
println(unsafe.String(&allocbuf[0], n))
}
lastAllocs = memstats.TotalAlloc
lastMallocs = memstats.Mallocs
allocmu.Unlock()
}
func copyValueInt(buf []byte, key string, v int64) int {
// ' ' + key + '=' + up to 20 chars for int64
if len(buf) < 2+len(key)+20 {
return 0
}
buf[0] = ' '
n := 1 + copy(buf[1:], key)
buf[n] = '='
n++
return n + copyInt(buf[n:], v)
}
func copyValueUint(buf []byte, key string, v uint64) int {
if len(buf) < 2+len(key)+20 {
return 0
}
buf[0] = ' '
n := 1 + copy(buf[1:], key)
buf[n] = '='
n++
return n + copyUint(buf[n:], v)
}
// copyInt formats v into buf and returns the number of bytes written.
// Caller must ensure cap(buf) >= 20.
func copyInt(buf []byte, v int64) int {
return len(strconv.AppendInt(buf[:0], v, 10))
}
// copyUint formats v into buf and returns the number of bytes written.
// Caller must ensure cap(buf) >= 20.
func copyUint(buf []byte, v uint64) int {
return len(strconv.AppendUint(buf[:0], v, 10))
}
+7 -11
View File
@@ -15,9 +15,6 @@ const (
)
var (
memstats runtime.MemStats
lastAllocs uint64
timebuf [len(timefmt) * 2]byte
)
@@ -28,12 +25,7 @@ func LogEnabled(l *slog.Logger, lvl slog.Level) bool {
func LogAttrs(_ *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr) {
now := time.Now()
n := len(now.AppendFormat(timebuf[:0], timefmt))
runtime.ReadMemStats(&memstats)
if memstats.TotalAlloc != lastAllocs {
print("[ALLOC] inc=", int64(memstats.TotalAlloc)-int64(lastAllocs))
print(" tot=", memstats.TotalAlloc, " seqs")
println()
}
LogAllocs(msg)
print("time=", unsafe.String(&timebuf[0], n), " ")
if level == LevelTrace {
print("TRACE ")
@@ -57,8 +49,12 @@ func LogAttrs(_ *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr)
}
}
println()
allocmu.Lock()
runtime.ReadMemStats(&memstats)
if memstats.TotalAlloc != lastAllocs {
lastAllocs = memstats.TotalAlloc
if lastAllocs != memstats.TotalAlloc {
print("alloc increase in heaplog")
}
lastAllocs = memstats.TotalAlloc
lastMallocs = memstats.Mallocs
allocmu.Unlock()
}
+3 -3
View File
@@ -1,12 +1,12 @@
package ltesto
import (
"bytes"
"math"
"math/rand"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv4/icmpv4"
"github.com/soypat/lneto/tcp"
@@ -134,9 +134,9 @@ func (gen *PacketGen) AppendRandomIPv4TCPPacket(dst []byte, rng *rand.Rand, seg
switch {
case gen.SrcTCP != tfrm.SourcePort():
panic("IP options overwrite TCP header")
case !bytes.Equal(ifrm.Options(), ipOpts):
case !internal.BytesEqual(ifrm.Options(), ipOpts):
panic("bad ip options written, ensure ip options length is multiple of 4")
case !bytes.Equal(tfrm.Options(), tcpOpts):
case !internal.BytesEqual(tfrm.Options(), tcpOpts):
panic("bad tcp options written, ensure tcp options length is multiple of 4")
case *ifrm.DestinationAddr() != gen.DstIPv4:
panic("IP options overwrite own header")
+14
View File
@@ -1,5 +1,7 @@
package internal
import "unsafe"
// IsZeroed returns true if all arguments are set to their zero value.
func IsZeroed[T comparable](a ...T) bool {
var z T
@@ -65,3 +67,15 @@ func SliceReclaim[T any](ptr *[]T) *T {
}
return &(*ptr)[n]
}
// BytesEqual is heapless replacement of [bytes.Equal] since it allocates in tinygo.
// https://github.com/tinygo-org/tinygo/issues/4045
func BytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
if len(a) == 0 {
return true
}
return unsafe.String(&a[0], len(a)) == unsafe.String(&b[0], len(b))
}
+1
View File
@@ -21,6 +21,7 @@ func (s *cbnode) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
}
func (s *cbnode) Demux(carrierData []byte, frameOffset int) error {
debugLog("cbnode:pre-demux")
return s._demux(carrierData, frameOffset)
}
+53 -22
View File
@@ -6,6 +6,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"log/slog"
"math"
"github.com/soypat/lneto"
@@ -29,12 +30,6 @@ var (
ErrFieldByClassNotFound = errors.New("pcap: field by class not found")
)
type proto string
const (
ProtoEthernet proto = "Ethernet"
)
type PacketBreakdown struct {
hdr httpraw.Header
dmsg dns.Message
@@ -66,8 +61,10 @@ 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")
@@ -80,8 +77,10 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in
if pc.validator().HasError() {
return dst, pc.validator().ErrPop()
}
debuglog("pcap:eth:validated")
finfo := reclaimFrame(&dst, ProtoEthernet, bitOffset, baseEthernetFields[:])
finfo := reclaimFrame(&dst, "Ethernet", bitOffset, baseEthernetFields[:])
debuglog("pcap:eth:reclaimed")
etype := efrm.EtherTypeOrSize()
end := 14*octet + bitOffset
if etype.IsSize() {
@@ -102,12 +101,14 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in
case ethernet.TypeIPv6:
dst, err = pc.CaptureIPv6(dst, pkt, end)
default:
reclaimRemainingFrame(&dst, etype, FieldClassPayload, end, octet*len(pkt))
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, errors.New("ARP must be parsed at byte boundary")
}
@@ -120,7 +121,7 @@ func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([
return dst, pc.validator().ErrPop()
}
finfo := reclaimFrame(&dst, ethernet.TypeARP, bitOffset, baseARPFields[:])
finfo := reclaimFrame(&dst, "ARP", bitOffset, baseARPFields[:])
const varstart = 8 * octet
_, hlen := afrm.Hardware()
_, plen := afrm.Protocol()
@@ -154,6 +155,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")
}
@@ -165,7 +167,8 @@ func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) (
if pc.validator().HasError() {
return dst, pc.validator().ErrPop()
}
reclaimFrame(&dst, ethernet.TypeIPv6, bitOffset, baseIPv6Fields[:])
reclaimFrame(&dst, "IPv6", bitOffset, baseIPv6Fields[:])
debuglog("pcap:ipv6:reclaimed")
proto := ifrm6.NextHeader()
end := bitOffset + 40*octet
var protoErrs []error
@@ -196,6 +199,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")
}
@@ -207,9 +211,11 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
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, ethernet.TypeIPv4, bitOffset, baseIPv4Fields[:])
finfo := reclaimFrame(&dst, "IPv4", bitOffset, baseIPv4Fields[:])
debuglog("pcap:ipv4:reclaimed")
options := ifrm4.Options()
if len(options) > 0 {
finfo.Fields = append(finfo.Fields, FrameField{
@@ -221,6 +227,7 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
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 protoErrs []error
@@ -262,10 +269,12 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
}
}
}
debuglog("pcap:ipv4:proto-crc-done")
return pc.captureIPProto(proto, dst, pkt, end, protoErrs...)
}
func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt []byte, bitOffset int, ipProtoErrs ...error) (_ []Frame, err error) {
debuglog("pcap:ipproto:start")
nextFrame := len(dst)
switch proto {
case lneto.IPProtoTCP:
@@ -275,20 +284,22 @@ func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt
case lneto.IPProtoUDPLite:
dst, err = pc.CaptureUDP(dst, pkt, bitOffset)
if len(dst) > nextFrame {
dst[nextFrame].Protocol = lneto.IPProtoUDPLite
dst[nextFrame].Protocol = "UDPLite"
}
case lneto.IPProtoICMP:
dst, err = pc.CaptureICMPv4(dst, pkt, bitOffset)
default:
reclaimRemainingFrame(&dst, proto, 0, bitOffset, octet*len(pkt))
reclaimRemainingFrame(&dst, "unknown proto", 0, bitOffset, octet*len(pkt))
}
if len(ipProtoErrs) > 0 && len(dst) > nextFrame {
dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErrs...)
}
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, errors.New("TCP must be parsed at byte boundary")
}
@@ -300,8 +311,10 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
if pc.validator().HasError() {
return dst, pc.validator().ErrPop()
}
debuglog("pcap:tcp:validated")
end := bitOffset + octet*tfrm.HeaderLength()
finfo := reclaimFrame(&dst, lneto.IPProtoTCP, bitOffset, baseTCPFields[:])
finfo := reclaimFrame(&dst, "TCP", bitOffset, baseTCPFields[:])
debuglog("pcap:tcp:reclaimed")
options := tfrm.Options()
if len(options) > 0 {
finfo.Fields = append(finfo.Fields, FrameField{
@@ -312,15 +325,19 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
}
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, errors.New("UDP must be parsed at byte boundary")
}
@@ -332,7 +349,8 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
if pc.validator().HasError() {
return dst, pc.validator().ErrPop()
}
reclaimFrame(&dst, lneto.IPProtoUDP, bitOffset, baseUDPFields[:])
reclaimFrame(&dst, "UDP", bitOffset, baseUDPFields[:])
debuglog("pcap:udp:reclaimed")
end := bitOffset + 8*octet
payload := ufrm.Payload()
dstport := ufrm.DestinationPort()
@@ -347,10 +365,12 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
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, errors.New("ICMPv4 must be parsed at byte boundary")
}
@@ -360,7 +380,7 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int)
return dst, err
}
finfo := reclaimFrame(&dst, lneto.IPProtoICMP, bitOffset, baseICMPv4Fields[:])
finfo := reclaimFrame(&dst, "ICMP", bitOffset, baseICMPv4Fields[:])
// Add type-specific fields.
switch ifrm.Type() {
@@ -541,9 +561,9 @@ func httpBodyClass(contentType, body []byte) FieldClass {
switch {
case bytes.HasPrefix(mediaType, []byte("text/")):
return FieldClassText
case bytes.Equal(mediaType, []byte("application/json")),
bytes.Equal(mediaType, []byte("application/javascript")),
bytes.Equal(mediaType, []byte("application/xml")):
case internal.BytesEqual(mediaType, []byte("application/json")),
internal.BytesEqual(mediaType, []byte("application/javascript")),
internal.BytesEqual(mediaType, []byte("application/xml")):
return FieldClassText
case bytes.HasSuffix(mediaType, []byte("+json")),
bytes.HasSuffix(mediaType, []byte("+xml")):
@@ -562,6 +582,7 @@ func httpBodyClass(contentType, body []byte) FieldClass {
}
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, errors.New("HTTP must be parsed at byte boundary")
@@ -578,6 +599,7 @@ func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) (
if err != nil {
return dst, err
}
debuglog("pcap:http:parsed")
hdrLen := pc.hdr.BufferParsed()
body, _ := pc.hdr.Body()
bodyClass := httpBodyClass(pc.hdr.Get("Content-Type"), body)
@@ -596,6 +618,7 @@ func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) (
BitLength: len(body) * octet,
},
)
debuglog("pcap:http:done")
return dst, nil
}
@@ -626,7 +649,7 @@ func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 }
type Frame struct {
PacketBitOffset int
Protocol any
Protocol string
Fields []FrameField
Errors []error
}
@@ -1306,7 +1329,7 @@ var baseNTPFields = [...]FrameField{
// 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 {
func reclaimFrame(dst *[]Frame, proto string, bitOffset int, baseFields []FrameField) *Frame {
finfo := internal.SliceReclaim(dst)
*finfo = Frame{
PacketBitOffset: bitOffset,
@@ -1319,10 +1342,18 @@ func reclaimFrame(dst *[]Frame, proto any, bitOffset int, baseFields []FrameFiel
// 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) {
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,
})
}
const enableDebug = false
func debuglog(msg string) {
if enableDebug {
internal.LogAttrs(nil, slog.LevelDebug, msg)
}
}
+9 -8
View File
@@ -209,7 +209,7 @@ func TestRightAlignedFields(t *testing.T) {
// Find IPv6 frame.
var ipv6Frame *Frame
for i := range frames {
if frames[i].Protocol == ethernet.TypeIPv6 {
if frames[i].Protocol == "IPv6" {
ipv6Frame = &frames[i]
break
}
@@ -476,15 +476,16 @@ func ExampleFormatter_dhcp() {
// Output:
// Ethernet len=14; destination=ff:ff:ff:ff:ff:ff; source=de:ad:be:ef:ca:fe; protocol=0x0800
// IPv4 len=20; version=0x04; (Header Length)=5; (Type of Service)=0x00; (Total Length)=312; identification=0x6043; flags=0x4000; (Time to live)=0x40; protocol=0x11; checksum=0xd972; source=0.0.0.0; destination=255.255.255.255
// UDP [RFC768] len=8; (Source port)=68; (Destination port)=67; size=292; checksum=0x0000
// UDP len=8; (Source port)=68; (Destination port)=67; size=292; checksum=0x0000
// DHCPv4 len=240; op=1; (Hardware Address Type)=0x01; (Hardware Address Length)=6; Hops=0x00; (Transaction ID)=0xdeadbeef; (Start Time)=0x0001; Flags=0x0000; (Client Address)=0.0.0.0; (Offered Address)=0.0.0.0; (Server Next Address)=255.255.255.255; (Relay Agent Address)=0.0.0.0; (Client Hardware Address)=de:ad:be:ef:ca:fe; options
// (DHCP message type.)=1
// (Parameter request list)=0x0102031a1c060f2a
// (DHCP maximum message size)=558
// (Requested IP address)=192.168.1.100
// (Client identifier)="lneto-test"
// (Hostname string)="myhost"
// (DHCP message type.)=1
// (Parameter request list)=0x0102031a1c060f2a
// (DHCP maximum message size)=558
// (Requested IP address)=192.168.1.100
// (Client identifier)="lneto-test"
// (Hostname string)="myhost"
}
func writeOpt(dst []byte, opt dhcpv4.OptNum, data ...byte) int {
dst[0] = byte(opt)
dst[1] = byte(len(data))
+18 -22
View File
@@ -4,7 +4,6 @@ import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"math"
"net/netip"
"slices"
@@ -12,8 +11,8 @@ import (
"strings"
"sync"
_ "time"
"unsafe"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp"
@@ -38,6 +37,7 @@ type Formatter struct {
// FormatFrames appends the formatted frame data to the destination buffer according the Formatter state.
// Is equivalent to [Formatter.FormatFrame] called on each Frame with the FrameSep inserted between frames.
func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte, err error) {
debuglog("pcap:fmt:start")
sep := f.frameSep()
for ifrm := range frms {
if ifrm != 0 {
@@ -48,14 +48,16 @@ func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte
return dst, err
}
}
debuglog("pcap:fmt:done")
return dst, nil
}
// 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) {
debuglog("pcap:fmtframe:start")
sep := f.fieldSep()
bitlen := frm.LenBits()
dst = appendProtocol(dst, frm.Protocol)
dst = append(dst, frm.Protocol...)
if bitlen%8 == 0 {
dst = append(dst, " len="...)
dst = strconv.AppendInt(dst, int64(bitlen/8), 10)
@@ -70,7 +72,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
continue
}
dst = append(dst, sep...)
if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP {
if field.Class == FieldClassFlags && frm.Protocol == "TCP" {
// TCP flags pretty print special case.
dst = append(dst, "flags="...)
v, err := f.fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
@@ -95,6 +97,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
}
dst = append(dst, ')')
}
debuglog("pcap:fmtframe:done")
return dst, nil
}
@@ -135,6 +138,9 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
if hasSpaces {
dst = append(dst, ')')
}
if field.BitLength == 0 {
return dst, nil
}
dst = append(dst, '=')
f.mubuf.Lock()
defer f.mubuf.Unlock()
@@ -142,6 +148,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
if err != nil {
return dst, err
}
debuglog("pcap:fmtfield:append-done")
fieldBitStart := pktStartOff + field.FrameBitOffset
switch field.Class {
default:
@@ -151,6 +158,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
dst = append(dst, "0x"...)
dst = hex.AppendEncode(dst, f.buf)
case FieldClassTimestamp:
debuglog("pcap:fmtfield:timestamp")
// inspired by [time.RFC3339]
const littlerfc3339 = "2006-01-02T15:04:05.9999"
if len(f.buf) != 8 {
@@ -158,8 +166,13 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
}
ts := ntp.TimestampFromUint64(binary.BigEndian.Uint64(f.buf))
dst = ts.Time().AppendFormat(dst, littlerfc3339)
debuglog("pcap:fmtfield:timestamp-done")
case FieldClassText:
dst = strconv.AppendQuote(dst, string(f.buf))
debuglog("pcap:fmtfield:text")
if len(f.buf) > 0 {
dst = strconv.AppendQuote(dst, unsafe.String(&f.buf[0], len(f.buf)))
}
debuglog("pcap:fmtfield:text-done")
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
// IP, MAC addresses and ports.
if field.BitLength <= 16 {
@@ -219,20 +232,3 @@ func (f *Formatter) fieldAsUint(pkt []byte, fieldBitStart, bitlen int, rightAlig
}
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)
}
}
+8
View File
@@ -232,3 +232,11 @@ func (l logger) debug(msg string, attrs ...slog.Attr) {
func (l logger) trace(msg string, attrs ...slog.Attr) {
internal.LogAttrs(l.log, internal.LevelTrace, msg, attrs...)
}
const enableAllocLog = false
func debugLog(msg string) {
if enableAllocLog {
internal.LogAllocs(msg)
}
}
+5 -4
View File
@@ -1,7 +1,6 @@
package tcp
import (
"bytes"
"errors"
"log/slog"
"net"
@@ -20,6 +19,8 @@ var (
errNoRemoteAddr = errors.New("tcp: no remote address established")
errInvalidIP = errors.New("tcp: invalid IP")
errMismatchedIPVersion = errors.New("mismatched IP version")
errBadDemuxOffset = errors.New("bad offset in TCPConn.Recv")
errIPAddrMismatch = errors.New("IP addr mismatch on TCPConn")
)
// Conn builds on the [Handler] abstraction and adds IP header knowledge, time management, and familiar user facing API
@@ -329,14 +330,14 @@ func (conn *Conn) Demux(buf []byte, off int) (err error) {
conn.mu.Lock()
defer conn.mu.Unlock()
if off >= len(buf) {
return errors.New("bad offset in TCPConn.Recv")
return errBadDemuxOffset
}
raddr, _, id, _, err := internal.GetIPAddr(buf[:off])
if err != nil {
return err
}
if conn.isRaddrSet() && !bytes.Equal(conn.remoteAddr, raddr) {
return errors.New("IP addr mismatch on TCPConn")
if conn.isRaddrSet() && !internal.BytesEqual(conn.remoteAddr, raddr) {
return errIPAddrMismatch
}
conn.trace("tcpconn.Recv", slog.Uint64("lport", uint64(conn.h.LocalPort())), slog.Uint64("rport", uint64(conn.h.remotePort)))
err = conn.h.Recv(buf[off:])
+27 -16
View File
@@ -183,6 +183,8 @@ func (flags Flags) HasAny(mask Flags) bool { return flags&mask != 0 }
// Mask returns the flags with non-flag bits unset.
func (flags Flags) Mask() Flags { return flags & flagMask }
func (flags Flags) Invalid() bool { return flags&flagMask != flags }
// StringFlags returns human readable flag string. i.e:
//
// "[SYN,ACK]"
@@ -201,6 +203,8 @@ func (flags Flags) String() string {
return "[FIN,ACK]"
case pshack:
return "[PSH,ACK]"
case FlagFIN | FlagPSH | FlagACK:
return "[FIN,PSH,ACK]"
case FlagACK:
return "[ACK]"
case FlagSYN:
@@ -210,41 +214,48 @@ func (flags Flags) String() string {
case FlagRST:
return "[RST]"
}
if flags&flagMask != flags {
if flags.Invalid() {
return strInvalidTCPFlags
}
buf := make([]byte, 0, 2+3*bits.OnesCount16(uint16(flags)))
buf = append(buf, '[')
buf = flags.AppendFormat(buf)
buf = append(buf, ']')
return string(buf)
// Since Go 1.26 this should not allocate if returned string does not escape and is smaller than 32 bytes.
// https://go.dev/blog/allocation-optimizations
var buf [2 + 4*9]byte
buf[0] = '['
n := flags.format((*[36]byte)(buf[1:]))
buf[1+n] = ']'
return string(buf[:2+n])
}
// AppendFormat appends a human readable flag string to b returning the extended buffer.
func (flags Flags) AppendFormat(b []byte) []byte {
var buf [36]byte
n := flags.format(&buf)
return append(b, buf[:n]...)
}
const strInvalidTCPFlags = "<invalid TCP flags>"
// AppendFormat appends a human readable flag string to b returning the extended buffer.
func (flags Flags) AppendFormat(b []byte) []byte {
func (flags Flags) format(buf *[4 * 9]byte) (n int) {
if flags == 0 {
return b
} else if flags&flagMask != flags {
return append(b, strInvalidTCPFlags...)
return 0
} else if flags.Invalid() {
return copy(buf[:], strInvalidTCPFlags)
}
// String Flag const
const flaglen = 3
const strflags = "FINSYNRSTPSHACKURGECECWRNS "
var addcommas bool
for flags != 0 { // written by Github Copilot- looks OK.
i := bits.TrailingZeros16(uint16(flags))
if addcommas {
b = append(b, ',')
buf[n] = ','
n++
} else {
addcommas = true
}
b = append(b, strflags[i*flaglen:i*flaglen+flaglen]...)
n += copy(buf[n:], strflags[i*flaglen:i*flaglen+flaglen])
flags &= ^(1 << i)
}
return b
return n
}
// State enumerates states a TCP connection progresses through during its lifetime as per RFC9293.
+7 -1
View File
@@ -216,7 +216,13 @@ func (h *Handler) Recv(incomingPacket []byte) error {
}
}
if h.logenabled(internal.LevelTrace) {
h.trace("tcp.Handler:rx-done", slog.Uint64("port", uint64(h.localPort)), slog.Uint64("remoteport", uint64(remotePort)), slog.String("seg", segIncoming.String()))
h.trace("tcp.Handler:rx-done",
slog.Uint64("lport", uint64(h.localPort)),
slog.Uint64("rport", uint64(remotePort)),
slog.Uint64("seg.seq", uint64(segIncoming.SEQ)),
slog.Uint64("seg.ack", uint64(segIncoming.ACK)),
slog.Uint64("seg.datalen", uint64(segIncoming.DATALEN)),
)
}
return nil
}
+1 -2
View File
@@ -1,7 +1,6 @@
package tcp
import (
"bytes"
"errors"
"log/slog"
"net"
@@ -293,7 +292,7 @@ func getConn(conns []handler, remotePort uint16, remoteAddr []byte) int {
}
gotPort := conn.RemotePort()
gotaddr := conn.RemoteAddr()
if remotePort == gotPort && bytes.Equal(remoteAddr, gotaddr) {
if remotePort == gotPort && internal.BytesEqual(remoteAddr, gotaddr) {
return i
}
}
+96
View File
@@ -788,3 +788,99 @@ func FuzzTCBActions(f *testing.F) {
// hasPanicked = false
})
}
func TestFlagsString(t *testing.T) {
tests := []struct {
flags tcp.Flags
want string
}{
{flags: 0, want: "[]"},
{flags: tcp.FlagSYN, want: "[SYN]"},
{flags: tcp.FlagACK, want: "[ACK]"},
{flags: tcp.FlagFIN, want: "[FIN]"},
{flags: tcp.FlagRST, want: "[RST]"},
{flags: tcp.FlagSYN | tcp.FlagACK, want: "[SYN,ACK]"},
{flags: tcp.FlagFIN | tcp.FlagACK, want: "[FIN,ACK]"},
{flags: tcp.FlagPSH | tcp.FlagACK, want: "[PSH,ACK]"},
// Multi-flag combinations.
{flags: tcp.FlagFIN | tcp.FlagSYN | tcp.FlagACK, want: "[FIN,SYN,ACK]"},
{flags: tcp.FlagURG | tcp.FlagACK, want: "[ACK,URG]"},
{flags: tcp.FlagECE | tcp.FlagCWR, want: "[ECE,CWR]"},
{flags: tcp.FlagNS | tcp.FlagACK, want: "[ACK,NS ]"},
// All flags set.
{flags: tcp.FlagFIN | tcp.FlagSYN | tcp.FlagRST | tcp.FlagPSH | tcp.FlagACK | tcp.FlagURG | tcp.FlagECE | tcp.FlagCWR | tcp.FlagNS, want: "[FIN,SYN,RST,PSH,ACK,URG,ECE,CWR,NS ]"},
// Invalid flags (bits above mask).
{flags: 0xFFFF, want: "<invalid TCP flags>"},
}
for _, tc := range tests {
got := tc.flags.String()
if got != tc.want {
t.Errorf("Flags(%#x).String() = %q; want %q", uint16(tc.flags), got, tc.want)
}
}
}
func TestFlagsAppendFormat(t *testing.T) {
tests := []struct {
flags tcp.Flags
want string
}{
{flags: 0, want: ""},
{flags: tcp.FlagSYN, want: "SYN"},
{flags: tcp.FlagACK, want: "ACK"},
{flags: tcp.FlagFIN, want: "FIN"},
{flags: tcp.FlagRST, want: "RST"},
{flags: tcp.FlagSYN | tcp.FlagACK, want: "SYN,ACK"},
{flags: tcp.FlagFIN | tcp.FlagACK, want: "FIN,ACK"},
{flags: tcp.FlagPSH | tcp.FlagACK, want: "PSH,ACK"},
{flags: tcp.FlagFIN | tcp.FlagSYN | tcp.FlagACK, want: "FIN,SYN,ACK"},
{flags: tcp.FlagURG | tcp.FlagACK, want: "ACK,URG"},
{flags: tcp.FlagECE | tcp.FlagCWR, want: "ECE,CWR"},
{flags: tcp.FlagNS | tcp.FlagACK, want: "ACK,NS "},
{flags: tcp.FlagFIN | tcp.FlagSYN | tcp.FlagRST | tcp.FlagPSH | tcp.FlagACK | tcp.FlagURG | tcp.FlagECE | tcp.FlagCWR | tcp.FlagNS, want: "FIN,SYN,RST,PSH,ACK,URG,ECE,CWR,NS "},
{flags: 0xFFFF, want: "<invalid TCP flags>"},
}
for _, tc := range tests {
got := string(tc.flags.AppendFormat(nil))
if got != tc.want {
t.Errorf("Flags(%#x).AppendFormat(nil) = %q; want %q", uint16(tc.flags), got, tc.want)
}
}
// Test that AppendFormat appends to existing data.
prefix := []byte("flags=")
got := string(SYNACK.AppendFormat(prefix))
if want := "flags=SYN,ACK"; got != want {
t.Errorf("AppendFormat with prefix = %q; want %q", got, want)
}
}
func TestFlagsStringAppendFormatConsistency(t *testing.T) {
// Verify String() and AppendFormat() produce consistent results
// for all single-flag and common multi-flag values.
allFlags := []tcp.Flags{
0,
tcp.FlagFIN, tcp.FlagSYN, tcp.FlagRST, tcp.FlagPSH,
tcp.FlagACK, tcp.FlagURG, tcp.FlagECE, tcp.FlagCWR, tcp.FlagNS,
SYNACK, FINACK, PSHACK,
tcp.FlagFIN | tcp.FlagSYN | tcp.FlagRST | tcp.FlagPSH | tcp.FlagACK | tcp.FlagURG | tcp.FlagECE | tcp.FlagCWR | tcp.FlagNS,
}
for _, flags := range allFlags {
str := flags.String()
appended := string(flags.AppendFormat(nil))
// String() wraps in brackets, AppendFormat() does not.
if flags == 0 {
if str != "[]" {
t.Errorf("Flags(0).String() = %q; want %q", str, "[]")
}
if appended != "" {
t.Errorf("Flags(0).AppendFormat() = %q; want empty", appended)
}
continue
}
wantStr := "[" + appended + "]"
if str != wantStr {
t.Errorf("Flags(%#x): String()=%q inconsistent with AppendFormat()=%q (want %q)", uint16(flags), str, appended, wantStr)
}
}
}
+11 -6
View File
@@ -7,7 +7,12 @@ import (
)
var (
errPacketQueueFull = errors.New("packet queue full")
errPacketQueueFull = errors.New("packet queue full")
errQueuedPacketsLEZ = errors.New("queued packets <=0")
errInvalidBufSize = errors.New("invalid buffer size")
errSeqLessThanLast = errors.New("sequence number less than last sequence number")
errNoPacketToAck = errors.New("no packet to ack")
errAckUnsent = errors.New("ack of unsent packet")
)
const (
@@ -55,9 +60,9 @@ type ringidx struct {
func (rtx *ringTx) Reset(buf []byte, maxqueuedPackets int, iss Value) error {
buf = buf[:len(buf):len(buf)] // safely omit capacity section.
if maxqueuedPackets <= 0 {
return errors.New("queued packets <=0")
return errQueuedPacketsLEZ
} else if len(buf) < minBufferSize || len(buf) < maxqueuedPackets {
return errors.New("invalid buffer size")
return errInvalidBufSize
}
*rtx = ringTx{
@@ -126,7 +131,7 @@ func (rtx *ringTx) MakePacket(b []byte, currentSeq Value) (int, error) {
}
endSeq, ok := rtx.sentEndSeq()
if ok && currentSeq.LessThan(endSeq) {
return 0, errors.New("sequence number less than last sequence number")
return 0, errSeqLessThanLast
}
// Reading unsent ring consumes unsent and converts it to "sent".
unsent, _ := rtx.unsentRing()
@@ -316,11 +321,11 @@ func (sl *sentlist) AddPacket(datalen, off, bufsize int, seq Value) *ringidx {
func (sl *sentlist) RecvAck(ack Value, bufsize int) error {
newest := sl.Newest()
if newest == nil {
return errors.New("no packet to ack")
return errNoPacketToAck
}
endseq := newest.endSeq()
if endseq.LessThan(ack) {
return errors.New("ack of unsent packet")
return errAckUnsent
}
// Mark fully acked.
for i := 0; i < len(sl.pkts); i++ {
+27 -1
View File
@@ -62,7 +62,33 @@ func (stack *CapturePrinter) PrintPacket(prefix string, pkt []byte) {
if err == nil {
if useTimestamps {
diff := captime.Sub(stack.origin)
fmtbuf = strconv.AppendFloat(fmtbuf, diff.Seconds(), 'f', stack.timeprec, 32)
ms := diff.Milliseconds()
fmtbuf = strconv.AppendInt(fmtbuf, ms/1000, 10)
fmtbuf = append(fmtbuf, '.')
frac := ms % 1000
if frac < 0 {
frac = -frac
}
// Pad fractional part to timeprec digits (up to 3).
switch {
case stack.timeprec >= 3:
if frac < 100 {
fmtbuf = append(fmtbuf, '0')
}
if frac < 10 {
fmtbuf = append(fmtbuf, '0')
}
fmtbuf = strconv.AppendInt(fmtbuf, frac, 10)
case stack.timeprec == 2:
frac /= 10 // truncate to centiseconds
if frac < 10 {
fmtbuf = append(fmtbuf, '0')
}
fmtbuf = strconv.AppendInt(fmtbuf, frac, 10)
case stack.timeprec == 1:
frac /= 100 // truncate to deciseconds
fmtbuf = strconv.AppendInt(fmtbuf, frac, 10)
}
fmtbuf = append(fmtbuf, ' ')
}
fmtbuf = append(fmtbuf, prefix...)
+27
View File
@@ -2,6 +2,7 @@ package xnet
import (
"errors"
"log/slog"
"net/netip"
"sync"
"time"
@@ -11,6 +12,7 @@ import (
"github.com/soypat/lneto/dhcpv4"
"github.com/soypat/lneto/dns"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/internet"
"github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp"
@@ -566,3 +568,28 @@ func addr4(addr [4]byte, ok bool) netip.Addr {
}
return netip.AddrFrom4(addr)
}
// Debug prints debugging information. Very useful for users when coupled with
// the debugheaplog build tag. See [internal.LogAttrs] debugheaplog version.
//
// go build -tags=debugheaplog ./yourprogram
func (s *StackAsync) Debug(msg string) {
internal.LogAttrs(slog.Default(), slog.LevelDebug, "stackasync",
slog.String("umsg", msg),
slog.Uint64("sent", s.totalsent),
slog.Uint64("recv", s.totalrecv),
)
}
// DebugErr prints debugging and error info. Very useful for users when coupled with
// the debugheaplog build tag. See [internal.LogAttrs] debugheaplog version.
//
// go build -tags=debugheaplog ./yourprogram
func (s *StackAsync) DebugErr(msg, err string) {
internal.LogAttrs(slog.Default(), slog.LevelError, "stackasync",
slog.String("umsg", msg),
slog.String("err", err),
slog.Uint64("sent", s.totalsent),
slog.Uint64("recv", s.totalrecv),
)
}
+40 -33
View File
@@ -439,22 +439,22 @@ func (tst *tester) TCPExchange(expect tcpExpectExchange, stack1, stack2 *StackAs
}
srcEth := src.HardwareAddress()
dstEth := dst.HardwareAddress()
if !bytes.Equal(srcEth[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc)) {
t.Errorf("mismatched ethernet src addr %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc))
if !bytes.Equal(srcEth[:], tst.getData(protoEthernet, pcap.FieldClassSrc)) {
t.Errorf("mismatched ethernet src addr %x", tst.getData(protoEthernet, pcap.FieldClassSrc))
}
if !bytes.Equal(dstEth[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst)) {
t.Errorf("mismatched ethernet dst addr %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst))
if !bytes.Equal(dstEth[:], tst.getData(protoEthernet, pcap.FieldClassDst)) {
t.Errorf("mismatched ethernet dst addr %x", tst.getData(protoEthernet, pcap.FieldClassDst))
}
if tst.getInt(ethernet.TypeIPv4, pcap.FieldClassVersion) != 4 {
t.Errorf("did not get IP version=4, got=%d", tst.getInt(ethernet.TypeIPv4, pcap.FieldClassVersion))
if tst.getInt(protoIPv4, pcap.FieldClassVersion) != 4 {
t.Errorf("did not get IP version=4, got=%d", tst.getInt(protoIPv4, pcap.FieldClassVersion))
}
srcAddr := src.Addr()
dstAddr := dst.Addr()
if !bytes.Equal(srcAddr.AsSlice(), tst.getData(ethernet.TypeIPv4, pcap.FieldClassSrc)) {
t.Errorf("mismatched ip src addr %d", tst.getData(ethernet.TypeIPv4, pcap.FieldClassSrc))
if !bytes.Equal(srcAddr.AsSlice(), tst.getData(protoIPv4, pcap.FieldClassSrc)) {
t.Errorf("mismatched ip src addr %d", tst.getData(protoIPv4, pcap.FieldClassSrc))
}
if !bytes.Equal(dstAddr.AsSlice(), tst.getData(ethernet.TypeIPv4, pcap.FieldClassDst)) {
t.Errorf("mismatched ip dst addr %d", tst.getData(ethernet.TypeIPv4, pcap.FieldClassDst))
if !bytes.Equal(dstAddr.AsSlice(), tst.getData(protoIPv4, pcap.FieldClassDst)) {
t.Errorf("mismatched ip dst addr %d", tst.getData(protoIPv4, pcap.FieldClassDst))
}
tfrm := tst.getTCPFrame()
@@ -502,11 +502,11 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) {
tgtIP := target.Addr()
// Validate Ethernet layer (request is broadcast)
if !bytes.Equal(qHw[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc)) {
t.Errorf("request: mismatched ethernet src addr %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc))
if !bytes.Equal(qHw[:], tst.getData(protoEthernet, pcap.FieldClassSrc)) {
t.Errorf("request: mismatched ethernet src addr %x", tst.getData(protoEthernet, pcap.FieldClassSrc))
}
if !bytes.Equal(broadcast[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst)) {
t.Errorf("request: expected broadcast ethernet dst addr, got %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst))
if !bytes.Equal(broadcast[:], tst.getData(protoEthernet, pcap.FieldClassDst)) {
t.Errorf("request: expected broadcast ethernet dst addr, got %x", tst.getData(protoEthernet, pcap.FieldClassDst))
}
// Validate ARP request fields
@@ -515,13 +515,13 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) {
if tst.getARPOperation() != arp.OpRequest {
t.Errorf("request: expected ARP OpRequest, got %d", tst.getARPOperation())
}
if !bytes.Equal(qHw[:], tst.getFieldByClassLen(ethernet.TypeARP, pcap.FieldClassSrc, 6, 0)) {
if !bytes.Equal(qHw[:], tst.getFieldByClassLen(protoARP, pcap.FieldClassSrc, 6, 0)) {
t.Errorf("request: mismatched ARP sender HW")
}
if !bytes.Equal(qIP.AsSlice(), tst.getFieldByClassLen(ethernet.TypeARP, pcap.FieldClassSrc, 4, 0)) {
if !bytes.Equal(qIP.AsSlice(), tst.getFieldByClassLen(protoARP, pcap.FieldClassSrc, 4, 0)) {
t.Errorf("request: mismatched ARP sender proto")
}
if !bytes.Equal(tgtIP.AsSlice(), tst.getFieldByClassLen(ethernet.TypeARP, pcap.FieldClassSrc, 4, 1)) {
if !bytes.Equal(tgtIP.AsSlice(), tst.getFieldByClassLen(protoARP, pcap.FieldClassSrc, 4, 1)) {
t.Errorf("request: mismatched ARP target proto")
}
@@ -549,27 +549,27 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) {
tst.buf = tst.buf[:n]
// Validate Ethernet layer (reply is unicast to querying)
if !bytes.Equal(tgtHw[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc)) {
t.Errorf("reply: mismatched ethernet src addr %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc))
if !bytes.Equal(tgtHw[:], tst.getData(protoEthernet, pcap.FieldClassSrc)) {
t.Errorf("reply: mismatched ethernet src addr %x", tst.getData(protoEthernet, pcap.FieldClassSrc))
}
if !bytes.Equal(qHw[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst)) {
t.Errorf("reply: expected unicast to querying, got %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst))
if !bytes.Equal(qHw[:], tst.getData(protoEthernet, pcap.FieldClassDst)) {
t.Errorf("reply: expected unicast to querying, got %x", tst.getData(protoEthernet, pcap.FieldClassDst))
}
// Validate ARP reply fields
if tst.getARPOperation() != arp.OpReply {
t.Errorf("reply: expected ARP OpReply, got %d", tst.getARPOperation())
}
if !bytes.Equal(tgtHw[:], tst.getFieldByClassLen(ethernet.TypeARP, pcap.FieldClassSrc, 6, 0)) {
if !bytes.Equal(tgtHw[:], tst.getFieldByClassLen(protoARP, pcap.FieldClassSrc, 6, 0)) {
t.Errorf("reply: mismatched ARP sender HW (should be target's MAC)")
}
if !bytes.Equal(tgtIP.AsSlice(), tst.getFieldByClassLen(ethernet.TypeARP, pcap.FieldClassSrc, 4, 0)) {
if !bytes.Equal(tgtIP.AsSlice(), tst.getFieldByClassLen(protoARP, pcap.FieldClassSrc, 4, 0)) {
t.Errorf("reply: mismatched ARP sender proto (should be target's IP)")
}
if !bytes.Equal(qHw[:], tst.getFieldByClassLen(ethernet.TypeARP, pcap.FieldClassSrc, 6, 1)) {
if !bytes.Equal(qHw[:], tst.getFieldByClassLen(protoARP, pcap.FieldClassSrc, 6, 1)) {
t.Errorf("reply: mismatched ARP target HW (should be querying's MAC)")
}
if !bytes.Equal(qIP.AsSlice(), tst.getFieldByClassLen(ethernet.TypeARP, pcap.FieldClassSrc, 4, 1)) {
if !bytes.Equal(qIP.AsSlice(), tst.getFieldByClassLen(protoARP, pcap.FieldClassSrc, 4, 1)) {
t.Errorf("reply: mismatched ARP target proto (should be querying's IP)")
}
@@ -593,7 +593,7 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) {
func (tst *tester) getTCPFrame() tcp.Frame {
tst.t.Helper()
// Find the IP frame's position in the captured packet buffer.
ipFrm := getProtoFrame(tst.frmbuf, ethernet.TypeIPv4)
ipFrm := getProtoFrame(tst.frmbuf, protoIPv4)
if ipFrm == nil {
tst.t.Fatal("no IP frame in capture")
}
@@ -615,7 +615,7 @@ func (tst *tester) getTCPFrame() tcp.Frame {
return frame
}
func (tst *tester) getPayload(proto any) []byte {
func (tst *tester) getPayload(proto string) []byte {
tst.t.Helper()
i := 0
for i = 0; i < len(tst.frmbuf); i++ {
@@ -633,7 +633,7 @@ func (tst *tester) getPayload(proto any) []byte {
return tst.getData(proto, pcap.FieldClassPayload)
}
func (tst *tester) getData(proto any, field pcap.FieldClass) []byte {
func (tst *tester) getData(proto string, field pcap.FieldClass) []byte {
tst.t.Helper()
frm := getProtoFrame(tst.frmbuf, proto)
if frm == nil {
@@ -654,7 +654,7 @@ func (tst *tester) getData(proto any, field pcap.FieldClass) []byte {
return tst.buf[bitoff/8 : bitoff/8+bitlen/8]
}
func (tst *tester) getInt(proto any, field pcap.FieldClass) uint64 {
func (tst *tester) getInt(proto string, field pcap.FieldClass) uint64 {
tst.t.Helper()
frm := getProtoFrame(tst.frmbuf, proto)
if frm == nil {
@@ -671,7 +671,7 @@ func (tst *tester) getInt(proto any, field pcap.FieldClass) uint64 {
return v
}
func getProtoFrame(frms []pcap.Frame, proto any) *pcap.Frame {
func getProtoFrame(frms []pcap.Frame, proto string) *pcap.Frame {
for i := range frms {
if frms[i].Protocol == proto {
return &frms[i]
@@ -690,7 +690,7 @@ func setzero[T ~[]E, E any](s T) {
// getFieldByClassLen finds a field by protocol, class, and octet length.
// occurrence specifies which match to return (0 = first, 1 = second, etc.)
// This is needed for ARP where sender and target fields share the same class.
func (tst *tester) getFieldByClassLen(proto any, class pcap.FieldClass, octetLen, occurrence int) []byte {
func (tst *tester) getFieldByClassLen(proto string, class pcap.FieldClass, octetLen, occurrence int) []byte {
tst.t.Helper()
frm := getProtoFrame(tst.frmbuf, proto)
if frm == nil {
@@ -712,7 +712,7 @@ func (tst *tester) getFieldByClassLen(proto any, class pcap.FieldClass, octetLen
func (tst *tester) getARPOperation() arp.Operation {
tst.t.Helper()
return arp.Operation(tst.getInt(ethernet.TypeARP, pcap.FieldClassOperation))
return arp.Operation(tst.getInt(protoARP, pcap.FieldClassOperation))
}
// TestTCPConn_BufferNotClearedOnPassiveClose tests that data remains readable after
@@ -981,7 +981,7 @@ func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
SequenceNumber: 4,
Payload: icmpPayload,
})
pkt[len(pkt)-1] ^= 0xFF // Corrupt ICMP payload.
pkt[len(pkt)-1] ^= 0xFF // Corrupt ICMP payload.
pkt = append(pkt, 0xDE, 0xAD, 0xBE, 0xEF) // Simulate Ethernet FCS.
err = stack.Demux(pkt, 0)
if err == nil {
@@ -989,3 +989,10 @@ func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
}
}
const (
protoEthernet = "Ethernet"
protoARP = "ARP"
protoIPv4 = "IPv4"
protoTCP = "TCP"
)