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 package arp
import ( import (
"bytes"
"errors" "errors"
"log/slog" "log/slog"
@@ -101,7 +100,7 @@ func (h *Handler) expectSize() int {
func (h *Handler) QueryResult(protoAddr []byte) (hwAddr []byte, err error) { func (h *Handler) QueryResult(protoAddr []byte) (hwAddr []byte, err error) {
for i := range h.queries { 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 { if !h.queries[i].querysent {
return nil, errors.New("query not yet sent") 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 { func (h *Handler) DiscardQuery(protoAddr []byte) error {
for i := range h.queries { for i := range h.queries {
q := &h.queries[i] q := &h.queries[i]
if bytes.Equal(protoAddr, q.protoaddr) { if internal.BytesEqual(protoAddr, q.protoaddr) {
q.destroy() q.destroy()
return nil return nil
} }
@@ -240,7 +239,7 @@ func (h *Handler) Demux(ethFrame []byte, frameOffset int) error {
switch afrm.Operation() { switch afrm.Operation() {
case OpRequest: case OpRequest:
_, protoaddr := afrm.Target() _, protoaddr := afrm.Target()
if !bytes.Equal(protoaddr, h.ourProtoAddr) { if !internal.BytesEqual(protoaddr, h.ourProtoAddr) {
return nil // Not for us. return nil // Not for us.
} }
h.pendingResponse = h.pendingResponse[:len(h.pendingResponse)+1] // Extend pending buffer. 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 { for i := range h.queries {
q := &h.queries[i] q := &h.queries[i]
mac := q.response() 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...) q.hwaddr = append(q.hwaddr, hwaddr...)
if q.dstHw != nil { if q.dstHw != nil {
if !internal.IsZeroed(q.dstHw...) { if !internal.IsZeroed(q.dstHw...) {
+1 -2
View File
@@ -2,7 +2,6 @@ package httpraw
import ( import (
"bytes" "bytes"
"errors"
) )
// Cookie implements cookie key-value parsing. Methods function similarly to eponymous [Header] methods. // 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. // Parse parses the cookie's buffer in place.
func (c *Cookie) Parse() error { func (c *Cookie) Parse() error {
if len(c.kvs) > 0 { if len(c.kvs) > 0 {
return errors.New("cookies already parsed, reset before parsing again") return errCookiesParsed
} }
off := 0 off := 0
for { for {
+20 -5
View File
@@ -2,9 +2,9 @@ package httpraw
import ( import (
"bytes" "bytes"
"errors"
"io" "io"
"slices" "slices"
"unsafe"
) )
const ( 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]. // 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. // It fails if HTTP data is incomplete.
func (h *Header) Parse(asResponse bool) error { func (h *Header) Parse(asResponse bool) error {
debuglog("http:parse:reset")
h.Reset(h.hbuf.buf) h.Reset(h.hbuf.buf)
debuglog("http:parse:start")
return h.parse(asResponse) 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) { func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) {
if h.flags.hasAny(flagDoneParsingHeader) { if h.flags.hasAny(flagDoneParsingHeader) {
return false, errors.New("TryParse called after header parsed") return false, errAlreadyParsed
} else if h.flags.hasAny(flagMangledBuffer) { } else if h.flags.hasAny(flagMangledBuffer) {
return false, errMangledBuffer return false, errMangledBuffer
} }
@@ -225,15 +227,18 @@ func (h *Header) Reset(buf []byte) {
panic("small buffer and flagNoBufferGrow set") panic("small buffer and flagNoBufferGrow set")
} }
const persistentFlags = flagNoBufferGrow const persistentFlags = flagNoBufferGrow
debuglog("http:reset:hbuf")
h.hbuf.reset(buf) h.hbuf.reset(buf)
*h = Header{ *h = Header{
hbuf: h.hbuf, hbuf: h.hbuf,
flags: h.flags & persistentFlags, 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. // 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) { func (h *Header) Body() ([]byte, error) {
debuglog("http:body")
if h.flags.hasAny(flagMangledBuffer) { if h.flags.hasAny(flagMangledBuffer) {
return nil, errMangledBuffer return nil, errMangledBuffer
} else if h.flags.hasAny(flagDoneParsingHeader) { } else if h.flags.hasAny(flagDoneParsingHeader) {
@@ -242,7 +247,14 @@ func (h *Header) Body() ([]byte, error) {
return nil, errUnparsed 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) { func (h *Header) Set(key, value string) {
hb := &h.hbuf hb := &h.hbuf
var useKv *argsKV 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. // 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 { func (h *Header) Get(key string) []byte {
debuglog("http:get:start")
kv := h.peekHeader(key) kv := h.peekHeader(key)
if kv.isValid() { if kv.isValid() {
debuglog("http:get:found")
return h.hbuf.musttoken(kv.value) return h.hbuf.musttoken(kv.value)
} }
debuglog("http:get:notfound")
return nil return nil
} }
@@ -338,7 +353,7 @@ func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
if h.flags.hasAny(flagOOMReached) { if h.flags.hasAny(flagOOMReached) {
return dst, errOOM return dst, errOOM
} else if h.requestURI.len == 0 || h.method.len == 0 { } 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 { } else if len(proto) == 0 {
return dst, errNoProto return dst, errNoProto
} }
@@ -368,7 +383,7 @@ func (h *Header) AppendResponse(dst []byte) ([]byte, error) {
if h.flags.hasAny(flagOOMReached) { if h.flags.hasAny(flagOOMReached) {
return dst, errOOM return dst, errOOM
} else if h.statusCode.len == 0 || h.statusText.len == 0 { } 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 { } else if len(proto) == 0 {
return dst, errNoProto return dst, errNoProto
} }
+52 -21
View File
@@ -3,8 +3,11 @@ package httpraw
import ( import (
"bytes" "bytes"
"errors" "errors"
"log/slog"
"slices" "slices"
"unsafe" "unsafe"
"github.com/soypat/lneto/internal"
) )
var ( var (
@@ -16,8 +19,15 @@ var (
errOOM = errors.New("httpraw: buffer out of memory") errOOM = errors.New("httpraw: buffer out of memory")
// Header.Set and Header.Add mangles the buffer. // 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?). // 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") errMangledBuffer = errors.New("httpraw: mangled buffer")
errNoCookies = errors.New("no cookie found") 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 { type headerBuf struct {
@@ -29,6 +39,20 @@ type headerBuf struct {
headers []argsKV 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 tokint = uint16
type headerSlice struct { type headerSlice struct {
@@ -56,11 +80,16 @@ type scannerState struct {
} }
func (h *Header) parse(asResponse bool) (err error) { func (h *Header) parse(asResponse bool) (err error) {
debuglog("http:firstline:start")
err = h.parseFirstLine(asResponse) err = h.parseFirstLine(asResponse)
if err != nil { if err != nil {
debuglog("http:firstline:err")
return 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) { 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) free() int { return cap(hb.buf) - len(hb.buf) }
func (hb *headerBuf) parseNextHeaders(ss *scannerState) { func (hb *headerBuf) parseNextHeaders(ss *scannerState) {
debuglog("http:nexthdr:loop")
for kv := hb.next(ss); kv.isValid(); kv = hb.next(ss) { 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 { 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) { 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. hb.off = 0 // Parsing first line resets offset.
var b []byte var b []byte
hb.skipLeadingCRLF() hb.skipLeadingCRLF()
@@ -135,6 +167,7 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto
if len(b) < 5 { if len(b) < 5 {
return method, uri, proto, flags, errNeedMore return method, uri, proto, flags, errNeedMore
} }
debuglog("http:req:parse")
methodEnd := max(0, bytes.IndexByte(b, ' ')) methodEnd := max(0, bytes.IndexByte(b, ' '))
reqURIEnd := bytes.IndexByte(b[methodEnd+1:], ' ') reqURIEnd := bytes.IndexByte(b[methodEnd+1:], ' ')
@@ -145,7 +178,7 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto
flags |= flagNoHTTP11 flags |= flagNoHTTP11
} }
} else if reqURIEnd == 0 { } else if reqURIEnd == 0 {
return method, uri, proto, flags, errors.New("empty URI") return method, uri, proto, flags, errEmptyURI
} else { } else {
// No version provided. // No version provided.
reqURIEnd = methodEnd + 1 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) { 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. hb.off = 0 // Parsing first line resets offset.
var b []byte var b []byte
hb.skipLeadingCRLF() hb.skipLeadingCRLF()
@@ -166,23 +200,23 @@ func (hb *headerBuf) parseFirstLineResponse(initFlags flags) (statusCode, status
if len(b) < 5 { if len(b) < 5 {
return statusCode, statusText, flags, errNeedMore return statusCode, statusText, flags, errNeedMore
} }
debuglog("http:resp:parse")
statusCodeEnd := max(0, bytes.IndexByte(b, ' ')) statusCodeEnd := max(0, bytes.IndexByte(b, ' '))
if statusCodeEnd < 0 {
return statusCode, statusText, flags, errors.New("missing status code")
}
code := b[:statusCodeEnd] code := b[:statusCodeEnd]
text := b[statusCodeEnd:] text := b[statusCodeEnd:]
if len(code) > 3 { if len(code) > 3 {
return statusCode, statusText, flags, errors.New("long status code") return statusCode, statusText, flags, errLongStatusCode
} }
for i := range code { for i := range code {
if code[i] > '9' || code[i] < '0' { 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) statusCode = hb.slice(code)
statusText = hb.slice(text) statusText = hb.slice(text)
debuglog("http:resp:done")
return statusCode, statusText, flags, nil return statusCode, statusText, flags, nil
} }
@@ -360,17 +394,6 @@ func (hb *headerBuf) next(ss *scannerState) argsKV {
return resultKV 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. // ConnectionClose returns true if 'Connection: close' header is set or if a invalid header was found.
func (h *Header) ConnectionClose() bool { func (h *Header) ConnectionClose() bool {
closed := h.flags.hasAny(flagConnClose) || closed := h.flags.hasAny(flagConnClose) ||
@@ -402,3 +425,11 @@ func bytes2tok(buf, value []byte) headerSlice {
len: tokint(len(value)), 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 package internal
import "log/slog" import (
"log/slog"
"runtime"
"strconv"
"sync"
"unsafe"
)
const ( const (
LevelTrace slog.Level = slog.LevelDebug - 2 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 ( var (
memstats runtime.MemStats
lastAllocs uint64
timebuf [len(timefmt) * 2]byte 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) { func LogAttrs(_ *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr) {
now := time.Now() now := time.Now()
n := len(now.AppendFormat(timebuf[:0], timefmt)) n := len(now.AppendFormat(timebuf[:0], timefmt))
runtime.ReadMemStats(&memstats) LogAllocs(msg)
if memstats.TotalAlloc != lastAllocs {
print("[ALLOC] inc=", int64(memstats.TotalAlloc)-int64(lastAllocs))
print(" tot=", memstats.TotalAlloc, " seqs")
println()
}
print("time=", unsafe.String(&timebuf[0], n), " ") print("time=", unsafe.String(&timebuf[0], n), " ")
if level == LevelTrace { if level == LevelTrace {
print("TRACE ") print("TRACE ")
@@ -57,8 +49,12 @@ func LogAttrs(_ *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr)
} }
} }
println() println()
allocmu.Lock()
runtime.ReadMemStats(&memstats) runtime.ReadMemStats(&memstats)
if memstats.TotalAlloc != lastAllocs { if lastAllocs != memstats.TotalAlloc {
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 package ltesto
import ( import (
"bytes"
"math" "math"
"math/rand" "math/rand"
"github.com/soypat/lneto" "github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/ethernet"
"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/tcp" "github.com/soypat/lneto/tcp"
@@ -134,9 +134,9 @@ func (gen *PacketGen) AppendRandomIPv4TCPPacket(dst []byte, rng *rand.Rand, seg
switch { switch {
case gen.SrcTCP != tfrm.SourcePort(): case gen.SrcTCP != tfrm.SourcePort():
panic("IP options overwrite TCP header") 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") 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") panic("bad tcp options written, ensure tcp options length is multiple of 4")
case *ifrm.DestinationAddr() != gen.DstIPv4: case *ifrm.DestinationAddr() != gen.DstIPv4:
panic("IP options overwrite own header") panic("IP options overwrite own header")
+14
View File
@@ -1,5 +1,7 @@
package internal package internal
import "unsafe"
// IsZeroed returns true if all arguments are set to their zero value. // IsZeroed returns true if all arguments are set to their zero value.
func IsZeroed[T comparable](a ...T) bool { func IsZeroed[T comparable](a ...T) bool {
var z T var z T
@@ -65,3 +67,15 @@ func SliceReclaim[T any](ptr *[]T) *T {
} }
return &(*ptr)[n] 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 { func (s *cbnode) Demux(carrierData []byte, frameOffset int) error {
debugLog("cbnode:pre-demux")
return s._demux(carrierData, frameOffset) return s._demux(carrierData, frameOffset)
} }
+53 -22
View File
@@ -6,6 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"math" "math"
"github.com/soypat/lneto" "github.com/soypat/lneto"
@@ -29,12 +30,6 @@ var (
ErrFieldByClassNotFound = errors.New("pcap: field by class not found") ErrFieldByClassNotFound = errors.New("pcap: field by class not found")
) )
type proto string
const (
ProtoEthernet proto = "Ethernet"
)
type PacketBreakdown struct { type PacketBreakdown struct {
hdr httpraw.Header hdr httpraw.Header
dmsg dns.Message dmsg dns.Message
@@ -66,8 +61,10 @@ func (pc *PacketBreakdown) initFrames() []Frame {
} }
func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:eth:start")
if dst == nil { if dst == nil {
dst = pc.initFrames() dst = pc.initFrames()
debuglog("pcap:eth: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")
@@ -80,8 +77,10 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in
if pc.validator().HasError() { if pc.validator().HasError() {
return dst, pc.validator().ErrPop() 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() etype := efrm.EtherTypeOrSize()
end := 14*octet + bitOffset end := 14*octet + bitOffset
if etype.IsSize() { if etype.IsSize() {
@@ -102,12 +101,14 @@ 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:
reclaimRemainingFrame(&dst, etype, FieldClassPayload, end, octet*len(pkt)) reclaimRemainingFrame(&dst, "Unknown Ethertype", FieldClassPayload, end, octet*len(pkt))
} }
debuglog("pcap:eth:done")
return dst, err return dst, err
} }
func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:arp:start")
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return dst, errors.New("ARP must be parsed at byte boundary") 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() return dst, pc.validator().ErrPop()
} }
finfo := reclaimFrame(&dst, ethernet.TypeARP, bitOffset, baseARPFields[:]) finfo := reclaimFrame(&dst, "ARP", bitOffset, baseARPFields[:])
const varstart = 8 * octet const varstart = 8 * octet
_, hlen := afrm.Hardware() _, hlen := afrm.Hardware()
_, plen := afrm.Protocol() _, 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) { func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:ipv6:start")
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return dst, errors.New("IPv6 must be parsed at byte boundary") 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() { if pc.validator().HasError() {
return dst, pc.validator().ErrPop() return dst, pc.validator().ErrPop()
} }
reclaimFrame(&dst, ethernet.TypeIPv6, bitOffset, baseIPv6Fields[:]) reclaimFrame(&dst, "IPv6", bitOffset, baseIPv6Fields[:])
debuglog("pcap:ipv6:reclaimed")
proto := ifrm6.NextHeader() proto := ifrm6.NextHeader()
end := bitOffset + 40*octet end := bitOffset + 40*octet
var protoErrs []error 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) { func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:ipv4:start")
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return dst, errors.New("IPv4 must be parsed at byte boundary") 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() { if pc.validator().HasError() {
return dst, pc.validator().ErrPop() return dst, pc.validator().ErrPop()
} }
debuglog("pcap:ipv4:validated")
// 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 := reclaimFrame(&dst, ethernet.TypeIPv4, bitOffset, baseIPv4Fields[:]) finfo := reclaimFrame(&dst, "IPv4", bitOffset, baseIPv4Fields[:])
debuglog("pcap:ipv4:reclaimed")
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{
@@ -221,6 +227,7 @@ 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)
} }
debuglog("pcap:ipv4:crc-done")
proto := ifrm4.Protocol() proto := ifrm4.Protocol()
end := bitOffset + octet*ifrm4.HeaderLength() end := bitOffset + octet*ifrm4.HeaderLength()
var protoErrs []error 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...) 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) { 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) nextFrame := len(dst)
switch proto { switch proto {
case lneto.IPProtoTCP: case lneto.IPProtoTCP:
@@ -275,20 +284,22 @@ func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt
case lneto.IPProtoUDPLite: case lneto.IPProtoUDPLite:
dst, err = pc.CaptureUDP(dst, pkt, bitOffset) dst, err = pc.CaptureUDP(dst, pkt, bitOffset)
if len(dst) > nextFrame { if len(dst) > nextFrame {
dst[nextFrame].Protocol = lneto.IPProtoUDPLite dst[nextFrame].Protocol = "UDPLite"
} }
case lneto.IPProtoICMP: case lneto.IPProtoICMP:
dst, err = pc.CaptureICMPv4(dst, pkt, bitOffset) dst, err = pc.CaptureICMPv4(dst, pkt, bitOffset)
default: 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 { if len(ipProtoErrs) > 0 && len(dst) > nextFrame {
dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErrs...) dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErrs...)
} }
debuglog("pcap:ipproto:done")
return dst, err return dst, err
} }
func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:tcp:start")
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return dst, errors.New("TCP must be parsed at byte boundary") 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() { if pc.validator().HasError() {
return dst, pc.validator().ErrPop() return dst, pc.validator().ErrPop()
} }
debuglog("pcap:tcp:validated")
end := bitOffset + octet*tfrm.HeaderLength() 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() options := tfrm.Options()
if len(options) > 0 { if len(options) > 0 {
finfo.Fields = append(finfo.Fields, FrameField{ finfo.Fields = append(finfo.Fields, FrameField{
@@ -312,15 +325,19 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
} }
payload := tfrm.Payload() payload := tfrm.Payload()
if len(payload) > 0 { if len(payload) > 0 {
debuglog("pcap:tcp:http-start")
dst, err = pc.CaptureHTTP(dst, pkt, end) dst, err = pc.CaptureHTTP(dst, pkt, end)
debuglog("pcap:tcp:http-done")
if err != nil { if err != nil {
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)) reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
} }
} }
debuglog("pcap:tcp:done")
return dst, nil return dst, nil
} }
func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:udp:start")
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")
} }
@@ -332,7 +349,8 @@ 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()
} }
reclaimFrame(&dst, lneto.IPProtoUDP, bitOffset, baseUDPFields[:]) reclaimFrame(&dst, "UDP", bitOffset, baseUDPFields[:])
debuglog("pcap:udp:reclaimed")
end := bitOffset + 8*octet end := bitOffset + 8*octet
payload := ufrm.Payload() payload := ufrm.Payload()
dstport := ufrm.DestinationPort() dstport := ufrm.DestinationPort()
@@ -347,10 +365,12 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
if err != nil { if err != nil {
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)) reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
} }
debuglog("pcap:udp:done")
return dst, nil return dst, nil
} }
func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:icmp:start")
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return dst, errors.New("ICMPv4 must be parsed at byte boundary") 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 return dst, err
} }
finfo := reclaimFrame(&dst, lneto.IPProtoICMP, bitOffset, baseICMPv4Fields[:]) finfo := reclaimFrame(&dst, "ICMP", bitOffset, baseICMPv4Fields[:])
// Add type-specific fields. // Add type-specific fields.
switch ifrm.Type() { switch ifrm.Type() {
@@ -541,9 +561,9 @@ func httpBodyClass(contentType, body []byte) FieldClass {
switch { switch {
case bytes.HasPrefix(mediaType, []byte("text/")): case bytes.HasPrefix(mediaType, []byte("text/")):
return FieldClassText return FieldClassText
case bytes.Equal(mediaType, []byte("application/json")), case internal.BytesEqual(mediaType, []byte("application/json")),
bytes.Equal(mediaType, []byte("application/javascript")), internal.BytesEqual(mediaType, []byte("application/javascript")),
bytes.Equal(mediaType, []byte("application/xml")): internal.BytesEqual(mediaType, []byte("application/xml")):
return FieldClassText return FieldClassText
case bytes.HasSuffix(mediaType, []byte("+json")), case bytes.HasSuffix(mediaType, []byte("+json")),
bytes.HasSuffix(mediaType, []byte("+xml")): 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) { func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:http:start")
const httpProtocol = "HTTP" const httpProtocol = "HTTP"
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return dst, errors.New("HTTP must be parsed at byte boundary") 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 { if err != nil {
return dst, err return dst, err
} }
debuglog("pcap:http:parsed")
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)
@@ -596,6 +618,7 @@ func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) (
BitLength: len(body) * octet, BitLength: len(body) * octet,
}, },
) )
debuglog("pcap:http:done")
return dst, nil return dst, nil
} }
@@ -626,7 +649,7 @@ func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 }
type Frame struct { type Frame struct {
PacketBitOffset int PacketBitOffset int
Protocol any Protocol string
Fields []FrameField Fields []FrameField
Errors []error Errors []error
} }
@@ -1306,7 +1329,7 @@ var baseNTPFields = [...]FrameField{
// reclaimFrame extends dst via [internal.SliceReclaim], resets the reclaimed Frame // reclaimFrame extends dst via [internal.SliceReclaim], resets the reclaimed Frame
// with the given protocol and bit offset while preserving Fields and Errors // with the given protocol and bit offset while preserving Fields and Errors
// backing arrays, and appends baseFields. Returns the Frame for further modification. // 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 := internal.SliceReclaim(dst)
*finfo = Frame{ *finfo = Frame{
PacketBitOffset: bitOffset, 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 // reclaimRemainingFrame extends dst via SliceReclaim and populates the reclaimed
// Frame as a single-field "remaining payload" frame, reusing the old Fields backing array. // 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 := reclaimFrame(dst, proto, pktBitOffset, nil)
finfo.Fields = append(finfo.Fields, FrameField{ finfo.Fields = append(finfo.Fields, FrameField{
Class: class, Class: class,
BitLength: pktBitLen - pktBitOffset, 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. // Find IPv6 frame.
var ipv6Frame *Frame var ipv6Frame *Frame
for i := range frames { for i := range frames {
if frames[i].Protocol == ethernet.TypeIPv6 { if frames[i].Protocol == "IPv6" {
ipv6Frame = &frames[i] ipv6Frame = &frames[i]
break break
} }
@@ -476,15 +476,16 @@ func ExampleFormatter_dhcp() {
// Output: // Output:
// Ethernet len=14; destination=ff:ff:ff:ff:ff:ff; source=de:ad:be:ef:ca:fe; protocol=0x0800 // 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 // 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 // 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 // (DHCP message type.)=1
// (Parameter request list)=0x0102031a1c060f2a // (Parameter request list)=0x0102031a1c060f2a
// (DHCP maximum message size)=558 // (DHCP maximum message size)=558
// (Requested IP address)=192.168.1.100 // (Requested IP address)=192.168.1.100
// (Client identifier)="lneto-test" // (Client identifier)="lneto-test"
// (Hostname string)="myhost" // (Hostname string)="myhost"
} }
func writeOpt(dst []byte, opt dhcpv4.OptNum, data ...byte) int { func writeOpt(dst []byte, opt dhcpv4.OptNum, data ...byte) int {
dst[0] = byte(opt) dst[0] = byte(opt)
dst[1] = byte(len(data)) dst[1] = byte(len(data))
+18 -22
View File
@@ -4,7 +4,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"math" "math"
"net/netip" "net/netip"
"slices" "slices"
@@ -12,8 +11,8 @@ import (
"strings" "strings"
"sync" "sync"
_ "time" _ "time"
"unsafe"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/ntp" "github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp" "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. // 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. // 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) { func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte, err error) {
debuglog("pcap:fmt:start")
sep := f.frameSep() sep := f.frameSep()
for ifrm := range frms { for ifrm := range frms {
if ifrm != 0 { if ifrm != 0 {
@@ -48,14 +48,16 @@ func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte
return dst, err return dst, err
} }
} }
debuglog("pcap:fmt:done")
return dst, nil return dst, nil
} }
// FormatFrame formats a single frame's protocol, fields, and errors into dst. // 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) {
debuglog("pcap:fmtframe:start")
sep := f.fieldSep() sep := f.fieldSep()
bitlen := frm.LenBits() bitlen := frm.LenBits()
dst = appendProtocol(dst, frm.Protocol) dst = append(dst, frm.Protocol...)
if bitlen%8 == 0 { if bitlen%8 == 0 {
dst = append(dst, " len="...) dst = append(dst, " len="...)
dst = strconv.AppendInt(dst, int64(bitlen/8), 10) 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 continue
} }
dst = append(dst, sep...) 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. // TCP flags pretty print special case.
dst = append(dst, "flags="...) dst = append(dst, "flags="...)
v, err := f.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())
@@ -95,6 +97,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
} }
dst = append(dst, ')') dst = append(dst, ')')
} }
debuglog("pcap:fmtframe:done")
return dst, nil return dst, nil
} }
@@ -135,6 +138,9 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
if hasSpaces { if hasSpaces {
dst = append(dst, ')') dst = append(dst, ')')
} }
if field.BitLength == 0 {
return dst, nil
}
dst = append(dst, '=') dst = append(dst, '=')
f.mubuf.Lock() f.mubuf.Lock()
defer f.mubuf.Unlock() defer f.mubuf.Unlock()
@@ -142,6 +148,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
if err != nil { if err != nil {
return dst, err return dst, err
} }
debuglog("pcap:fmtfield:append-done")
fieldBitStart := pktStartOff + field.FrameBitOffset fieldBitStart := pktStartOff + field.FrameBitOffset
switch field.Class { switch field.Class {
default: default:
@@ -151,6 +158,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
dst = append(dst, "0x"...) dst = append(dst, "0x"...)
dst = hex.AppendEncode(dst, f.buf) dst = hex.AppendEncode(dst, f.buf)
case FieldClassTimestamp: case FieldClassTimestamp:
debuglog("pcap:fmtfield:timestamp")
// inspired by [time.RFC3339] // inspired by [time.RFC3339]
const littlerfc3339 = "2006-01-02T15:04:05.9999" const littlerfc3339 = "2006-01-02T15:04:05.9999"
if len(f.buf) != 8 { 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)) ts := ntp.TimestampFromUint64(binary.BigEndian.Uint64(f.buf))
dst = ts.Time().AppendFormat(dst, littlerfc3339) dst = ts.Time().AppendFormat(dst, littlerfc3339)
debuglog("pcap:fmtfield:timestamp-done")
case FieldClassText: 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: case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
// IP, MAC addresses and ports. // IP, MAC addresses and ports.
if field.BitLength <= 16 { 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 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) { func (l logger) trace(msg string, attrs ...slog.Attr) {
internal.LogAttrs(l.log, internal.LevelTrace, msg, attrs...) 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 package tcp
import ( import (
"bytes"
"errors" "errors"
"log/slog" "log/slog"
"net" "net"
@@ -20,6 +19,8 @@ var (
errNoRemoteAddr = errors.New("tcp: no remote address established") errNoRemoteAddr = errors.New("tcp: no remote address established")
errInvalidIP = errors.New("tcp: invalid IP") errInvalidIP = errors.New("tcp: invalid IP")
errMismatchedIPVersion = errors.New("mismatched IP version") 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 // 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() conn.mu.Lock()
defer conn.mu.Unlock() defer conn.mu.Unlock()
if off >= len(buf) { if off >= len(buf) {
return errors.New("bad offset in TCPConn.Recv") return errBadDemuxOffset
} }
raddr, _, id, _, err := internal.GetIPAddr(buf[:off]) raddr, _, id, _, err := internal.GetIPAddr(buf[:off])
if err != nil { if err != nil {
return err return err
} }
if conn.isRaddrSet() && !bytes.Equal(conn.remoteAddr, raddr) { if conn.isRaddrSet() && !internal.BytesEqual(conn.remoteAddr, raddr) {
return errors.New("IP addr mismatch on TCPConn") return errIPAddrMismatch
} }
conn.trace("tcpconn.Recv", slog.Uint64("lport", uint64(conn.h.LocalPort())), slog.Uint64("rport", uint64(conn.h.remotePort))) conn.trace("tcpconn.Recv", slog.Uint64("lport", uint64(conn.h.LocalPort())), slog.Uint64("rport", uint64(conn.h.remotePort)))
err = conn.h.Recv(buf[off:]) 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. // Mask returns the flags with non-flag bits unset.
func (flags Flags) Mask() Flags { return flags & flagMask } 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: // StringFlags returns human readable flag string. i.e:
// //
// "[SYN,ACK]" // "[SYN,ACK]"
@@ -201,6 +203,8 @@ func (flags Flags) String() string {
return "[FIN,ACK]" return "[FIN,ACK]"
case pshack: case pshack:
return "[PSH,ACK]" return "[PSH,ACK]"
case FlagFIN | FlagPSH | FlagACK:
return "[FIN,PSH,ACK]"
case FlagACK: case FlagACK:
return "[ACK]" return "[ACK]"
case FlagSYN: case FlagSYN:
@@ -210,41 +214,48 @@ func (flags Flags) String() string {
case FlagRST: case FlagRST:
return "[RST]" return "[RST]"
} }
if flags&flagMask != flags { if flags.Invalid() {
return strInvalidTCPFlags return strInvalidTCPFlags
} }
buf := make([]byte, 0, 2+3*bits.OnesCount16(uint16(flags))) // Since Go 1.26 this should not allocate if returned string does not escape and is smaller than 32 bytes.
buf = append(buf, '[') // https://go.dev/blog/allocation-optimizations
buf = flags.AppendFormat(buf) var buf [2 + 4*9]byte
buf = append(buf, ']') buf[0] = '['
return string(buf) 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>" const strInvalidTCPFlags = "<invalid TCP flags>"
// AppendFormat appends a human readable flag string to b returning the extended buffer. func (flags Flags) format(buf *[4 * 9]byte) (n int) {
func (flags Flags) AppendFormat(b []byte) []byte {
if flags == 0 { if flags == 0 {
return b return 0
} else if flags&flagMask != flags { } else if flags.Invalid() {
return append(b, strInvalidTCPFlags...) return copy(buf[:], strInvalidTCPFlags)
} }
// String Flag const
const flaglen = 3 const flaglen = 3
const strflags = "FINSYNRSTPSHACKURGECECWRNS " const strflags = "FINSYNRSTPSHACKURGECECWRNS "
var addcommas bool var addcommas bool
for flags != 0 { // written by Github Copilot- looks OK. for flags != 0 { // written by Github Copilot- looks OK.
i := bits.TrailingZeros16(uint16(flags)) i := bits.TrailingZeros16(uint16(flags))
if addcommas { if addcommas {
b = append(b, ',') buf[n] = ','
n++
} else { } else {
addcommas = true addcommas = true
} }
b = append(b, strflags[i*flaglen:i*flaglen+flaglen]...) n += copy(buf[n:], strflags[i*flaglen:i*flaglen+flaglen])
flags &= ^(1 << i) flags &= ^(1 << i)
} }
return b return n
} }
// State enumerates states a TCP connection progresses through during its lifetime as per RFC9293. // 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) { 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 return nil
} }
+1 -2
View File
@@ -1,7 +1,6 @@
package tcp package tcp
import ( import (
"bytes"
"errors" "errors"
"log/slog" "log/slog"
"net" "net"
@@ -293,7 +292,7 @@ func getConn(conns []handler, remotePort uint16, remoteAddr []byte) int {
} }
gotPort := conn.RemotePort() gotPort := conn.RemotePort()
gotaddr := conn.RemoteAddr() gotaddr := conn.RemoteAddr()
if remotePort == gotPort && bytes.Equal(remoteAddr, gotaddr) { if remotePort == gotPort && internal.BytesEqual(remoteAddr, gotaddr) {
return i return i
} }
} }
+96
View File
@@ -788,3 +788,99 @@ func FuzzTCBActions(f *testing.F) {
// hasPanicked = false // 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 ( 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 ( const (
@@ -55,9 +60,9 @@ type ringidx struct {
func (rtx *ringTx) Reset(buf []byte, maxqueuedPackets int, iss Value) error { func (rtx *ringTx) Reset(buf []byte, maxqueuedPackets int, iss Value) error {
buf = buf[:len(buf):len(buf)] // safely omit capacity section. buf = buf[:len(buf):len(buf)] // safely omit capacity section.
if maxqueuedPackets <= 0 { if maxqueuedPackets <= 0 {
return errors.New("queued packets <=0") return errQueuedPacketsLEZ
} else if len(buf) < minBufferSize || len(buf) < maxqueuedPackets { } else if len(buf) < minBufferSize || len(buf) < maxqueuedPackets {
return errors.New("invalid buffer size") return errInvalidBufSize
} }
*rtx = ringTx{ *rtx = ringTx{
@@ -126,7 +131,7 @@ func (rtx *ringTx) MakePacket(b []byte, currentSeq Value) (int, error) {
} }
endSeq, ok := rtx.sentEndSeq() endSeq, ok := rtx.sentEndSeq()
if ok && currentSeq.LessThan(endSeq) { 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". // Reading unsent ring consumes unsent and converts it to "sent".
unsent, _ := rtx.unsentRing() 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 { func (sl *sentlist) RecvAck(ack Value, bufsize int) error {
newest := sl.Newest() newest := sl.Newest()
if newest == nil { if newest == nil {
return errors.New("no packet to ack") return errNoPacketToAck
} }
endseq := newest.endSeq() endseq := newest.endSeq()
if endseq.LessThan(ack) { if endseq.LessThan(ack) {
return errors.New("ack of unsent packet") return errAckUnsent
} }
// Mark fully acked. // Mark fully acked.
for i := 0; i < len(sl.pkts); i++ { 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 err == nil {
if useTimestamps { if useTimestamps {
diff := captime.Sub(stack.origin) 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, ' ')
} }
fmtbuf = append(fmtbuf, prefix...) fmtbuf = append(fmtbuf, prefix...)
+27
View File
@@ -2,6 +2,7 @@ package xnet
import ( import (
"errors" "errors"
"log/slog"
"net/netip" "net/netip"
"sync" "sync"
"time" "time"
@@ -11,6 +12,7 @@ import (
"github.com/soypat/lneto/dhcpv4" "github.com/soypat/lneto/dhcpv4"
"github.com/soypat/lneto/dns" "github.com/soypat/lneto/dns"
"github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/internet" "github.com/soypat/lneto/internet"
"github.com/soypat/lneto/ntp" "github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp" "github.com/soypat/lneto/tcp"
@@ -566,3 +568,28 @@ func addr4(addr [4]byte, ok bool) netip.Addr {
} }
return netip.AddrFrom4(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() srcEth := src.HardwareAddress()
dstEth := dst.HardwareAddress() dstEth := dst.HardwareAddress()
if !bytes.Equal(srcEth[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc)) { if !bytes.Equal(srcEth[:], tst.getData(protoEthernet, pcap.FieldClassSrc)) {
t.Errorf("mismatched ethernet src addr %x", tst.getData(pcap.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)) { if !bytes.Equal(dstEth[:], tst.getData(protoEthernet, pcap.FieldClassDst)) {
t.Errorf("mismatched ethernet dst addr %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst)) t.Errorf("mismatched ethernet dst addr %x", tst.getData(protoEthernet, pcap.FieldClassDst))
} }
if tst.getInt(ethernet.TypeIPv4, pcap.FieldClassVersion) != 4 { if tst.getInt(protoIPv4, pcap.FieldClassVersion) != 4 {
t.Errorf("did not get IP version=4, got=%d", tst.getInt(ethernet.TypeIPv4, pcap.FieldClassVersion)) t.Errorf("did not get IP version=4, got=%d", tst.getInt(protoIPv4, pcap.FieldClassVersion))
} }
srcAddr := src.Addr() srcAddr := src.Addr()
dstAddr := dst.Addr() dstAddr := dst.Addr()
if !bytes.Equal(srcAddr.AsSlice(), 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(ethernet.TypeIPv4, 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)) { if !bytes.Equal(dstAddr.AsSlice(), tst.getData(protoIPv4, pcap.FieldClassDst)) {
t.Errorf("mismatched ip dst addr %d", tst.getData(ethernet.TypeIPv4, pcap.FieldClassDst)) t.Errorf("mismatched ip dst addr %d", tst.getData(protoIPv4, pcap.FieldClassDst))
} }
tfrm := tst.getTCPFrame() tfrm := tst.getTCPFrame()
@@ -502,11 +502,11 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) {
tgtIP := target.Addr() tgtIP := target.Addr()
// Validate Ethernet layer (request is broadcast) // Validate Ethernet layer (request is broadcast)
if !bytes.Equal(qHw[:], 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(pcap.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)) { if !bytes.Equal(broadcast[:], tst.getData(protoEthernet, pcap.FieldClassDst)) {
t.Errorf("request: expected broadcast ethernet dst addr, got %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst)) t.Errorf("request: expected broadcast ethernet dst addr, got %x", tst.getData(protoEthernet, pcap.FieldClassDst))
} }
// Validate ARP request fields // Validate ARP request fields
@@ -515,13 +515,13 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) {
if tst.getARPOperation() != arp.OpRequest { if tst.getARPOperation() != arp.OpRequest {
t.Errorf("request: expected ARP OpRequest, got %d", tst.getARPOperation()) 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") 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") 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") t.Errorf("request: mismatched ARP target proto")
} }
@@ -549,27 +549,27 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) {
tst.buf = tst.buf[:n] tst.buf = tst.buf[:n]
// Validate Ethernet layer (reply is unicast to querying) // Validate Ethernet layer (reply is unicast to querying)
if !bytes.Equal(tgtHw[:], 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(pcap.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)) { if !bytes.Equal(qHw[:], tst.getData(protoEthernet, pcap.FieldClassDst)) {
t.Errorf("reply: expected unicast to querying, got %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst)) t.Errorf("reply: expected unicast to querying, got %x", tst.getData(protoEthernet, pcap.FieldClassDst))
} }
// Validate ARP reply fields // Validate ARP reply fields
if tst.getARPOperation() != arp.OpReply { if tst.getARPOperation() != arp.OpReply {
t.Errorf("reply: expected ARP OpReply, got %d", tst.getARPOperation()) 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)") 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)") 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)") 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)") 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 { func (tst *tester) getTCPFrame() tcp.Frame {
tst.t.Helper() tst.t.Helper()
// Find the IP frame's position in the captured packet buffer. // 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 { if ipFrm == nil {
tst.t.Fatal("no IP frame in capture") tst.t.Fatal("no IP frame in capture")
} }
@@ -615,7 +615,7 @@ func (tst *tester) getTCPFrame() tcp.Frame {
return frame return frame
} }
func (tst *tester) getPayload(proto any) []byte { func (tst *tester) getPayload(proto string) []byte {
tst.t.Helper() tst.t.Helper()
i := 0 i := 0
for i = 0; i < len(tst.frmbuf); i++ { 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) 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() tst.t.Helper()
frm := getProtoFrame(tst.frmbuf, proto) frm := getProtoFrame(tst.frmbuf, proto)
if frm == nil { 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] 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() tst.t.Helper()
frm := getProtoFrame(tst.frmbuf, proto) frm := getProtoFrame(tst.frmbuf, proto)
if frm == nil { if frm == nil {
@@ -671,7 +671,7 @@ func (tst *tester) getInt(proto any, field pcap.FieldClass) uint64 {
return v return v
} }
func getProtoFrame(frms []pcap.Frame, proto any) *pcap.Frame { func getProtoFrame(frms []pcap.Frame, proto string) *pcap.Frame {
for i := range frms { for i := range frms {
if frms[i].Protocol == proto { if frms[i].Protocol == proto {
return &frms[i] 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. // getFieldByClassLen finds a field by protocol, class, and octet length.
// occurrence specifies which match to return (0 = first, 1 = second, etc.) // 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. // 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() tst.t.Helper()
frm := getProtoFrame(tst.frmbuf, proto) frm := getProtoFrame(tst.frmbuf, proto)
if frm == nil { if frm == nil {
@@ -712,7 +712,7 @@ func (tst *tester) getFieldByClassLen(proto any, class pcap.FieldClass, octetLen
func (tst *tester) getARPOperation() arp.Operation { func (tst *tester) getARPOperation() arp.Operation {
tst.t.Helper() 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 // TestTCPConn_BufferNotClearedOnPassiveClose tests that data remains readable after
@@ -981,7 +981,7 @@ func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
SequenceNumber: 4, SequenceNumber: 4,
Payload: icmpPayload, 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. pkt = append(pkt, 0xDE, 0xAD, 0xBE, 0xEF) // Simulate Ethernet FCS.
err = stack.Demux(pkt, 0) err = stack.Demux(pkt, 0)
if err == nil { if err == nil {
@@ -989,3 +989,10 @@ func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
} }
} }
const (
protoEthernet = "Ethernet"
protoARP = "ARP"
protoIPv4 = "IPv4"
protoTCP = "TCP"
)