diff --git a/arp/handler.go b/arp/handler.go index 648985a..2547aba 100644 --- a/arp/handler.go +++ b/arp/handler.go @@ -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...) { diff --git a/http/httpraw/cookie.go b/http/httpraw/cookie.go index c4e4742..9f85b6b 100644 --- a/http/httpraw/cookie.go +++ b/http/httpraw/cookie.go @@ -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 { diff --git a/http/httpraw/header.go b/http/httpraw/header.go index 0274869..d991eee 100644 --- a/http/httpraw/header.go +++ b/http/httpraw/header.go @@ -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 } diff --git a/http/httpraw/parse.go b/http/httpraw/parse.go index 3229267..6666e35 100644 --- a/http/httpraw/parse.go +++ b/http/httpraw/parse.go @@ -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) + } +} diff --git a/internal/debug.go b/internal/debug.go index 90ae9f9..eef856d 100644 --- a/internal/debug.go +++ b/internal/debug.go @@ -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)) +} diff --git a/internal/debug_heaplog.go b/internal/debug_heaplog.go index c300d9e..bf4f561 100644 --- a/internal/debug_heaplog.go +++ b/internal/debug_heaplog.go @@ -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() } diff --git a/internal/ltesto/ltesto.go b/internal/ltesto/ltesto.go index 3079ea7..6744d10 100644 --- a/internal/ltesto/ltesto.go +++ b/internal/ltesto/ltesto.go @@ -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") diff --git a/internal/slices.go b/internal/slices.go index 42ab151..5a49172 100644 --- a/internal/slices.go +++ b/internal/slices.go @@ -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)) +} diff --git a/internet/definitions_tinygo.go b/internet/definitions_tinygo.go index 4b4d695..c684924 100644 --- a/internet/definitions_tinygo.go +++ b/internet/definitions_tinygo.go @@ -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) } diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 7319242..4576fd1 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -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) + } +} diff --git a/internet/pcap/capture_test.go b/internet/pcap/capture_test.go index f1ae4ac..f11a62a 100644 --- a/internet/pcap/capture_test.go +++ b/internet/pcap/capture_test.go @@ -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)) diff --git a/internet/pcap/format.go b/internet/pcap/format.go index 637060f..d26e964 100644 --- a/internet/pcap/format.go +++ b/internet/pcap/format.go @@ -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) - } -} diff --git a/internet/stack-ip.go b/internet/stack-ip.go index d0bb87e..c634b9b 100644 --- a/internet/stack-ip.go +++ b/internet/stack-ip.go @@ -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) + } +} diff --git a/tcp/conn.go b/tcp/conn.go index 9ba1347..5cf8c8b 100644 --- a/tcp/conn.go +++ b/tcp/conn.go @@ -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:]) diff --git a/tcp/definitions.go b/tcp/definitions.go index 84a95fa..4ce0a3c 100644 --- a/tcp/definitions.go +++ b/tcp/definitions.go @@ -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 = "" -// 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. diff --git a/tcp/handler.go b/tcp/handler.go index 7f9dcc8..480eea6 100644 --- a/tcp/handler.go +++ b/tcp/handler.go @@ -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 } diff --git a/tcp/listener.go b/tcp/listener.go index cfa822d..e9ac8b2 100644 --- a/tcp/listener.go +++ b/tcp/listener.go @@ -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 } } diff --git a/tcp/tcp_test.go b/tcp/tcp_test.go index c7757de..8452573 100644 --- a/tcp/tcp_test.go +++ b/tcp/tcp_test.go @@ -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: ""}, + } + 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: ""}, + } + 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) + } + } +} diff --git a/tcp/txqueue.go b/tcp/txqueue.go index f4ef732..a345971 100644 --- a/tcp/txqueue.go +++ b/tcp/txqueue.go @@ -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++ { diff --git a/x/xnet/pcap-printer.go b/x/xnet/pcap-printer.go index 5efb218..29a5912 100644 --- a/x/xnet/pcap-printer.go +++ b/x/xnet/pcap-printer.go @@ -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...) diff --git a/x/xnet/stack-async.go b/x/xnet/stack-async.go index 28ac584..22ed8ea 100644 --- a/x/xnet/stack-async.go +++ b/x/xnet/stack-async.go @@ -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), + ) +} diff --git a/x/xnet/xnet_test.go b/x/xnet/xnet_test.go index 2b7e62b..1368e19 100644 --- a/x/xnet/xnet_test.go +++ b/x/xnet/xnet_test.go @@ -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" +)