From d7f39244899160aefe7d5c0354bf7b05a88f2d37 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Sun, 6 Sep 2026 17:17:14 -0300 Subject: [PATCH] prevent aggressive DCE in MWE w/ TinyGo and cull fmt package (#196) * cull fmt package use and prevent aggressive DCE in MWE example with TinyGo * add noslog build tag and small reference section to README * add memci * fix ci * whoops, forgot memci.json to gitignore * whoops x2 * use noslog build tag * don't go ham on removing fmt useful data * remove old benchmarking CI --- .github/workflows/bench-comment.yaml | 90 ---------------------- .github/workflows/ci.yaml | 39 +++------- .github/workflows/memci-comment.yml | 24 ++++++ .github/workflows/memci.json | 13 ++++ .gitignore | 3 + README.md | 8 ++ arp/frame.go | 39 +++++----- dhcp/dhcpv4/server.go | 5 +- dns/dns.go | 96 +++++++++++++++++++++++- dns/dns_test.go | 30 +------- examples/min-working-example/main-mwe.go | 30 ++++---- examples/min-working-example/nic.go | 37 +++++++++ internal/debug_heaplog.go | 2 +- internal/debug_noheaplog.go | 4 +- internal/debug_noslog.go | 5 ++ internal/debug_yesslog.go | 5 ++ internal/strconv.go | 21 ++++++ internet/pcap/capture.go | 9 +-- ipv4/frame.go | 27 ++++--- tcp/definitions.go | 17 ++++- tcp/frame.go | 9 ++- udp/handler.go | 7 +- udp/mux.go | 3 +- validation.go | 3 +- 24 files changed, 306 insertions(+), 220 deletions(-) delete mode 100644 .github/workflows/bench-comment.yaml create mode 100644 .github/workflows/memci-comment.yml create mode 100644 .github/workflows/memci.json create mode 100644 examples/min-working-example/nic.go create mode 100644 internal/debug_noslog.go create mode 100644 internal/debug_yesslog.go diff --git a/.github/workflows/bench-comment.yaml b/.github/workflows/bench-comment.yaml deleted file mode 100644 index 1f6714d..0000000 --- a/.github/workflows/bench-comment.yaml +++ /dev/null @@ -1,90 +0,0 @@ -name: Benchmark Comment - -# Rationale: This more privileged workflow runs in the base repo's -# context (with a write token) only after the 'untrusted' CI workflow finishes. -# It does NOT check out or execute PR code; it only consumes the benchmark -# report artifact as inert, validated data. -on: - workflow_run: - workflows: [CI] - types: [completed] - -permissions: - contents: read - -jobs: - comment: - # Only for PR-triggered CI runs that succeeded to suppress noise - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write # needed to comment - steps: - - name: Download generated benchmark report - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.1 - with: - name: bench-report - path: bench-report - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Validate report and upsert PR comment - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} - HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - HEAD_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} - HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - run: | - set -euo pipefail - - report="bench-report/bench-report.md" - marker='' - - # Treat the artifact as untrusted input: it was produced by a job that - # ran PR code. Basic validation before posting. TODO: Make more robust. - test -f "$report" - size="$(wc -c < "$report")" - if [ "$size" -le 0 ] || [ "$size" -gt 1000000 ]; then - echo "::error::benchmark report has unexpected size: ${size} bytes" - exit 1 - fi - if ! grep -qF "$marker" "$report"; then - echo "::error::benchmark report missing marker ${marker}" - exit 1 - fi - - # Prefer the PR number from the workflow_run payload, which ties this - # privileged workflow to the PR-triggered CI run. Fall back to the - # run's head SHA/branch only when GitHub does not populate it. - pr="$PR_NUMBER" - if [ -z "$pr" ]; then - pr="$(gh api "repos/${REPO}/commits/${HEAD_SHA}/pulls" \ - --jq 'map(select(.state == "open")) | .[0].number // empty')" - fi - if [ -z "$pr" ]; then - pr="$(gh api --method GET "repos/${REPO}/pulls" \ - -f state=open \ - -f head="${HEAD_OWNER}:${HEAD_BRANCH}" \ - --jq '.[0].number // empty')" - fi - if [ -z "$pr" ]; then - echo "No open PR found for ${HEAD_SHA}; nothing to comment." - exit 0 - fi - - comment_id="$( - gh api "repos/${REPO}/issues/${pr}/comments" --paginate \ - --jq ".[] | select(.body | contains(\"${marker}\")) | .id" | head -n1 - )" - if [ -n "$comment_id" ]; then - gh api --method PATCH "repos/${REPO}/issues/comments/${comment_id}" \ - --input - < <(jq -n --rawfile body "$report" '{body: $body}') - else - gh api --method POST "repos/${REPO}/issues/${pr}/comments" \ - --input - < <(jq -n --rawfile body "$report" '{body: $body}') - fi diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c1f2914..415c5df 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -100,38 +100,21 @@ jobs: - name: Test (race + shuffle) run: go test -race -shuffle=on -count=1 -timeout=10m ./... - benchmark: + memci: needs: [test] runs-on: ubuntu-latest - permissions: - contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: go-version: "1.26" - - - name: Run benchmarks - run: go test -bench=. -benchmem -count=5 -shuffle=on -run='^$' -timeout=15m ./... | tee bench-results.txt - - - name: Generate benchmark report - run: | - go run ./internal/benchci -current bench-results.txt -out bench-report.md - cat bench-report.md >> "$GITHUB_STEP_SUMMARY" - - - name: Upload generated benchmark report - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + # Needed because a build command in memci.json names tinygo. Keep the two + # versions in step: TinyGo 0.42 builds with Go 1.25 through 1.27 and + # refuses to run outside that window, in either direction. + - uses: acifani/setup-tinygo@v2 with: - name: bench-report - path: bench-report.md - retention-days: 30 - - - name: Upload raw benchmark results - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + tinygo-version: "0.42.0" + - uses: soypat/memci@main with: - name: bench-results - path: bench-results.txt - retention-days: 30 + args: -kind package + targets: .github/workflows/memci.json \ No newline at end of file diff --git a/.github/workflows/memci-comment.yml b/.github/workflows/memci-comment.yml new file mode 100644 index 0000000..a618e1e --- /dev/null +++ b/.github/workflows/memci-comment.yml @@ -0,0 +1,24 @@ +name: memci comment + +# Runs in the base repo's context with a write token, after the untrusted memci +# workflow finishes. It never checks out or runs PR code; it only consumes the +# report artifact as inert data. + +on: + workflow_run: + workflows: [CI] + types: [completed] + +permissions: read-all + +jobs: + comment: + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: soypat/memci/comment@main + with: + run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/memci.json b/.github/workflows/memci.json new file mode 100644 index 0000000..a20041e --- /dev/null +++ b/.github/workflows/memci.json @@ -0,0 +1,13 @@ +[ + { + "name":"Stack MWE", + "build":"go build -o=mwe.elf -tags=noslog ./examples/min-working-example", + "elf":"mwe.elf" + }, + { + "name":"Stack MWE pico", + "build":"tinygo build -o=mwe-pico.elf -target=pico -tags=noslog -panic=trap ./examples/min-working-example", + "elf":"mwe-pico.elf", + "mem":true + } +] diff --git a/.gitignore b/.gitignore index cec7615..7a8964d 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,6 @@ local # If running a python script. */__pycache__/* + +# memci targets +!.github/workflows/memci.json \ No newline at end of file diff --git a/README.md b/README.md index 352bc77..b613772 100644 --- a/README.md +++ b/README.md @@ -290,3 +290,11 @@ The document has moved success ``` + +## Reference + +### Build tags + +- `debugheaplog`: All logging calls are enabled and all will print out heap information. Warning: Heavy cost on some TinyGo garbage collectors which do not cache the GC statistics +- `noslog`: All slog package logging calls omitted. +- `xnetdebug`: Packet capture printing to standard output enabled on `xnet.StackAsync` Ethernet and IP receive and send methods diff --git a/arp/frame.go b/arp/frame.go index fac7374..1089bad 100644 --- a/arp/frame.go +++ b/arp/frame.go @@ -2,12 +2,10 @@ package arp import ( "encoding/binary" - "fmt" - "net" - "net/netip" "github.com/soypat/lneto" "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/internal" ) // NewFrame returns a Frame with data set to buf. @@ -145,23 +143,24 @@ func (afrm Frame) ValidateSize(v *lneto.Validator) { } } +// String returns a basic human readable represetation of ARP frame. func (afrm Frame) String() string { opstr := afrm.Operation().String() - hwt, _ := afrm.Hardware() - ptt, _ := afrm.Protocol() - sndhw, sndpt := afrm.Sender() - tgthw, tgtpt := afrm.Target() - var sndstr, tgtstr string - if ptt == ethernet.TypeIPv4 || ptt == ethernet.TypeIPv6 { - sender, _ := netip.AddrFromSlice(sndpt) - target, _ := netip.AddrFromSlice(tgtpt) - sndstr = sender.String() - tgtstr = target.String() - } else { - sndstr = net.HardwareAddr(sndpt).String() - tgtstr = net.HardwareAddr(tgtpt).String() - } - return fmt.Sprintf("ARP %s HW=(%d,SENDER=%s,TARGET=%s) PROTO=(%s,SENDER=%s,TARGET=%s)", - opstr, hwt, net.HardwareAddr(sndhw).String(), net.HardwareAddr(tgthw).String(), - ptt.String(), sndstr, tgtstr) + rawbuf := make([]byte, 0, 128) + b := append(rawbuf[:0], "ARP "...) + b = append(b, opstr...) + htyp, hlen := afrm.Hardware() + b = internal.AppendStrDecimal(b, " htyp=", int64(htyp)) + b = internal.AppendStrDecimal(b, " hlen=", int64(hlen)) + ptyp, plen := afrm.Protocol() + b = internal.AppendStrDecimal(b, " plen=", int64(plen)) + b = append(b, " proto="...) + b = append(b, ptyp.String()...) + hw, proto := afrm.Target() + b = internal.AppendStrHexData(b, " htgt=", hw...) + b = internal.AppendStrHexData(b, " ptgt=", proto...) + hw, proto = afrm.Sender() + b = internal.AppendStrHexData(b, " hsnd=", hw...) + b = internal.AppendStrHexData(b, " psnd=", proto...) + return string(b) } diff --git a/dhcp/dhcpv4/server.go b/dhcp/dhcpv4/server.go index 1890368..b5f4e0f 100644 --- a/dhcp/dhcpv4/server.go +++ b/dhcp/dhcpv4/server.go @@ -3,7 +3,6 @@ package dhcpv4 import ( "encoding/binary" "errors" - "fmt" "github.com/soypat/lneto" "github.com/soypat/lneto/internal" @@ -220,10 +219,10 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error { } default: - err = fmt.Errorf("unhandled message type %s", msgType.String()) + err = errors.New("unhandled message type: " + msgType.String()) } if err != nil { - return fmt.Errorf("msgtype=%s client=%+v: %w", msgType.String(), client, err) + return errors.New("dhcpv4 server demux fail on " + msgType.String()) } sv.hosts[clientIDRaw] = client return nil diff --git a/dns/dns.go b/dns/dns.go index 1ebd6ba..fc22d78 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -3,6 +3,7 @@ package dns import ( "bytes" "encoding/binary" + "encoding/hex" "math" "net/netip" "slices" @@ -426,10 +427,64 @@ func (m *Message) Reset() { m.Additionals = m.Additionals[:0] } +// AppendText appends a human readable representation of the Message's resources +// to b and returns the resulting slice. It implements [encoding.TextAppender]. +func (m *Message) AppendText(b []byte) (_ []byte, err error) { + if len(m.Questions) > 0 { + b = append(b, "-- Questions\n"...) + for i := range m.Questions { + b, err = m.Questions[i].AppendText(b) + if err != nil { + return b, err + } + b = append(b, '\n') + } + } + b, err = appendResourcesText(b, "-- Answers\n", m.Answers) + if err != nil { + return b, err + } + b, err = appendResourcesText(b, "-- Authorities\n", m.Authorities) + if err != nil { + return b, err + } + return appendResourcesText(b, "-- Additionals\n", m.Additionals) +} + +func appendResourcesText(b []byte, title string, resources []Resource) (_ []byte, err error) { + if len(resources) == 0 { + return b, nil + } + b = append(b, title...) + for i := range resources { + b, err = resources[i].AppendText(b) + if err != nil { + return b, err + } + b = append(b, '\n') + } + return b, nil +} + // String returns a string representation of the header. func (h *ResourceHeader) String() string { - return h.Name.String() + " " + h.Type.String() + " " + h.Class.String() + - " ttl=" + strconv.FormatUint(uint64(h.TTL), 10) + " len=" + strconv.FormatUint(uint64(h.Length), 10) + b, _ := h.AppendText(make([]byte, 0, 64)) + return string(b) +} + +// AppendText appends a human readable representation of the header to b and +// returns the resulting slice. It implements [encoding.TextAppender]. +func (h *ResourceHeader) AppendText(b []byte) ([]byte, error) { + b = h.Name.AppendDottedTo(b) + b = append(b, ' ') + b = append(b, h.Type.String()...) + b = append(b, ' ') + b = append(b, h.Class.String()...) + b = append(b, " ttl="...) + b = strconv.AppendUint(b, uint64(h.TTL), 10) + b = append(b, " len="...) + b = strconv.AppendUint(b, uint64(h.Length), 10) + return b, nil } func (r *Resource) Reset() { @@ -456,6 +511,29 @@ func (r *Resource) CNAMEView() Name { return Name{data: r.RawData()} } +// String returns a string representation of the Resource: its header followed by +// the record's data. +func (r *Resource) String() string { + b, _ := r.AppendText(make([]byte, 0, 96)) + return string(b) +} + +// AppendText appends a human readable representation of the Resource to b: the +// header followed by the record's data, in dotted format for CNAME records and +// hexadecimal otherwise. It implements [encoding.TextAppender]. +func (r *Resource) AppendText(b []byte) (_ []byte, err error) { + b, err = r.header.AppendText(b) + if err != nil { + return b, err + } + b = append(b, " data="...) + if r.header.Type == TypeCNAME { + cname := r.CNAMEView() + return cname.AppendDottedTo(b), nil + } + return hex.AppendEncode(b, r.RawData()), nil +} + func (q *Question) Reset() { q.Name.Reset() *q = Question{Name: q.Name} // Reuse Name's buffer. @@ -494,7 +572,19 @@ func (q *Question) appendTo(buf []byte) (_ []byte, err error) { // String returns a string representation of the Question with the Name in dotted format. func (q *Question) String() string { - return q.Name.String() + " " + q.Type.String() + " " + q.Class.String() + b, _ := q.AppendText(make([]byte, 0, 32)) + return string(b) +} + +// AppendText appends a human readable representation of the Question to b with +// the Name in dotted format. It implements [encoding.TextAppender]. +func (q *Question) AppendText(b []byte) ([]byte, error) { + b = q.Name.AppendDottedTo(b) + b = append(b, ' ') + b = append(b, q.Type.String()...) + b = append(b, ' ') + b = append(b, q.Class.String()...) + return b, nil } func (r *Resource) Decode(b []byte, off uint16) (uint16, error) { diff --git a/dns/dns_test.go b/dns/dns_test.go index 3eaebc3..166083b 100644 --- a/dns/dns_test.go +++ b/dns/dns_test.go @@ -1,7 +1,6 @@ package dns import ( - "fmt" "net/netip" "strings" "testing" @@ -195,33 +194,8 @@ func TestMessageAppendEncodeIncompleteOK(t *testing.T) { } func (m *Message) String() string { - // s := fmt.Sprintf("Message: %#v\n", &m.Header) - var s strings.Builder - if len(m.Questions) > 0 { - s.WriteString("-- Questions\n") - for _, q := range m.Questions { - s.WriteString(fmt.Sprintf("%#v\n", q)) - } - } - if len(m.Answers) > 0 { - s.WriteString("-- Answers\n") - for _, a := range m.Answers { - s.WriteString(fmt.Sprintf("%#v\n", a)) - } - } - if len(m.Authorities) > 0 { - s.WriteString("-- Authorities\n") - for _, ns := range m.Authorities { - s.WriteString(fmt.Sprintf("%#v\n", ns)) - } - } - if len(m.Additionals) > 0 { - s.WriteString("-- Additionals\n") - for _, e := range m.Additionals { - s.WriteString(fmt.Sprintf("%#v\n", e)) - } - } - return s.String() + b, _ := m.AppendText(nil) + return string(b) } func TestDecodeMessage(t *testing.T) { diff --git a/examples/min-working-example/main-mwe.go b/examples/min-working-example/main-mwe.go index 722e6c6..d67ada2 100644 --- a/examples/min-working-example/main-mwe.go +++ b/examples/min-working-example/main-mwe.go @@ -2,7 +2,7 @@ package main import ( "context" - "fmt" + "errors" "net" "net/netip" "os" @@ -46,7 +46,7 @@ func main() { var stack xnet.StackAsync ctx := context.Background() if err := run(ctx, &stack); err != nil { - fmt.Println(err) + os.Stdout.WriteString(err.Error()) os.Exit(1) } } @@ -71,7 +71,7 @@ func run(ctx context.Context, stack *xnet.StackAsync) error { HardwareAddress: hwaddr, }) if err != nil { - return fmt.Errorf("configuring stack: %w", err) + return makeMsgErr("configuring stack", err) } ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -82,18 +82,18 @@ func run(ctx context.Context, stack *xnet.StackAsync) error { rstack := stack.StackRetrying(stackBackoff) results, err := rstack.DoDHCPv4([4]byte{}, protoTimeout, protoRetries) if err != nil { - return fmt.Errorf("doing DHCP: %w", err) + return makeMsgErr("doing DHCP", err) } err = stack.AssimilateDHCPResults(results) if err != nil { - return fmt.Errorf("assimilating DHCP: %w", err) + return makeMsgErr("assimilating DHCP", err) } gateway, err := rstack.DoResolveHardwareAddress6(results.Router, protoTimeout, protoRetries) if err != nil { - return fmt.Errorf("resolving router MAC: %w", err) + return makeMsgErr("resolving Router MAC", err) } stack.SetGatewayHardwareAddr(gateway) - berkstack := stack.StackBlocking(stackBackoff).StackGo(xnet.StackGoConfig{ + gostack := stack.StackBlocking(stackBackoff).StackGo(xnet.StackGoConfig{ ListenerPoolConfig: xnet.TCPPoolConfig{ PoolSize: tcpConnPoolSize, QueueSize: tcpPacketQueueSize, @@ -109,16 +109,16 @@ func run(ctx context.Context, stack *xnet.StackAsync) error { laddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4(results.AssignedAddr4), 80)) // raddr := net.TCPAddr{} // If active (client) connection then set raddr in which case a net.Conn type is returned. const sockstream = 0x1 - c, err := berkstack.Socket(ctx, "tcp", syscall.AF_INET, sockstream, laddr, nil) + c, err := gostack.Socket(ctx, "tcp", syscall.AF_INET, sockstream, laddr, nil) if err != nil { - return fmt.Errorf("creating AF_INET stream socket: %w", err) + return makeMsgErr("creating AF_INET stream socket", err) } listener := c.(net.Listener) for ctx.Err() == nil { time.Sleep(pollTime) conn, err := listener.Accept() if err != nil { - fmt.Println("conn failed:", err) + return makeMsgErr("listener.Accept failed", err) } go handleConn(conn) } @@ -145,18 +145,18 @@ func stackLoop(ctx context.Context, stack *xnet.StackAsync) { for ctx.Err() == nil { nwrite, err := stack.EgressEthernet(buf[:]) if err != nil { - fmt.Println("encaps err:", err) + os.Stderr.WriteString(err.Error()) } else if nwrite > 0 { network.SendEth(buf[:nwrite]) cap.PrintEthernet("OUT", buf[:nwrite]) } nread, err := network.RecvEth(buf[:]) if err != nil { - fmt.Println("network read err:", err) + os.Stderr.WriteString(err.Error()) } else if nread > 0 { err = stack.IngressEthernet(buf[:nread]) if err != nil && err != lneto.ErrPacketDrop { - fmt.Println("demux err:", err) + os.Stderr.WriteString(err.Error()) } else { cap.PrintEthernet("IN ", buf[:nread]) } @@ -191,3 +191,7 @@ func tcpBackoff(consecutiveBackoffs uint) time.Duration { wait := min(shifted, maxWait) return time.Duration(wait) } + +func makeMsgErr(msg string, err error) error { + return errors.New(msg + ": " + err.Error()) +} diff --git a/examples/min-working-example/nic.go b/examples/min-working-example/nic.go new file mode 100644 index 0000000..a0d40b8 --- /dev/null +++ b/examples/min-working-example/nic.go @@ -0,0 +1,37 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/soypat/lneto/ethernet" +) + +func init() { + mn := &mockNetwork{ + rng: rand.New(rand.NewSource(time.Now().UnixNano())), + } + network = mn +} + +type mockNetwork struct { + rng *rand.Rand +} + +func (m *mockNetwork) SendEth(frame []byte) error { + return nil +} +func (m *mockNetwork) RecvEth(dst []byte) (int, error) { + n := m.rng.Int() % ethernet.MaxFrameLength + if n < ethernet.MinimumFrameLength { + return 0, nil + } + n, _ = m.rng.Read(dst[:min(len(dst), n)]) + return n, nil +} +func (m *mockNetwork) HardwareAddress6() ([6]byte, error) { + return [6]byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x00}, nil +} +func (m *mockNetwork) MaxFrameLength() (int, error) { + return ethernet.MaxFrameLength, nil +} diff --git a/internal/debug_heaplog.go b/internal/debug_heaplog.go index 5815c87..9585d65 100644 --- a/internal/debug_heaplog.go +++ b/internal/debug_heaplog.go @@ -18,7 +18,7 @@ var ( ) func LogEnabled(l *slog.Logger, lvl slog.Level) bool { - return true + return logEnabled } func logAttrsAndAllocs(allocmsg string, l *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr) { diff --git a/internal/debug_noheaplog.go b/internal/debug_noheaplog.go index 7cade96..6534ffa 100644 --- a/internal/debug_noheaplog.go +++ b/internal/debug_noheaplog.go @@ -10,14 +10,14 @@ import ( const HeapAllocDebugging = false func LogEnabled(l *slog.Logger, lvl slog.Level) bool { - return l != nil && l.Handler().Enabled(context.Background(), lvl) + return logEnabled && l != nil && l.Handler().Enabled(context.Background(), lvl) } // LogAttrs is a helper function that is used by all package loggers and that // can be switched out with the `debugheaplog` build tag for a non-allocating // logger that prints out when heap allocations occur. func LogAttrs(l *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr) { - if l != nil { + if logEnabled && l != nil { l.LogAttrs(context.Background(), level, msg, attrs...) } } diff --git a/internal/debug_noslog.go b/internal/debug_noslog.go new file mode 100644 index 0000000..3b83116 --- /dev/null +++ b/internal/debug_noslog.go @@ -0,0 +1,5 @@ +//go:build noslog + +package internal + +const logEnabled = false diff --git a/internal/debug_yesslog.go b/internal/debug_yesslog.go new file mode 100644 index 0000000..2f4c912 --- /dev/null +++ b/internal/debug_yesslog.go @@ -0,0 +1,5 @@ +//go:build !noslog + +package internal + +const logEnabled = true diff --git a/internal/strconv.go b/internal/strconv.go index d70e0fb..4584646 100644 --- a/internal/strconv.go +++ b/internal/strconv.go @@ -1,5 +1,26 @@ package internal +import ( + "encoding/hex" + "strconv" +) + +// AppendStrDecimal appends pfx followed by value in base 10 to dst and returns +// the resulting slice. It condenses the prefixed-number pattern common to +// AppendString/AppendText methods, i.e. `internal.AppendStrDecimal(b, " len=", 4)`. +func AppendStrDecimal(dst []byte, pfx string, value int64) []byte { + dst = append(dst, pfx...) + return strconv.AppendInt(dst, value, 10) +} + +// AppendStrHexData appends pfx followed by data as hexadecimaldata. +// +// dst = internal.AppendStrHexData(dst, "data=0x", data...) // data=0xdeadbeef +func AppendStrHexData(dst []byte, pfx string, data ...byte) []byte { + dst = append(dst, pfx...) + return hex.AppendEncode(dst, data) +} + // IntLen returns the number of bytes [strconv.AppendInt] emits for value in the // given base, including a leading minus sign for negatives. Lets callers size a // buffer, or test whether a value fits an existing slot, before writing a byte. diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 5c8b25e..0f40979 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -987,16 +987,13 @@ func (frm Frame) AppendString(b []byte) []byte { bitlen := frm.LenBits() b = append(b, frm.Protocol...) if bitlen%8 == 0 { - b = append(b, " len="...) - b = strconv.AppendInt(b, int64(bitlen/8), 10) + b = internal.AppendStrDecimal(b, " len=", int64(bitlen/8)) } else { - b = append(b, " bits="...) - b = strconv.AppendInt(b, int64(bitlen), 10) + b = internal.AppendStrDecimal(b, " bits=", int64(bitlen)) } iopt, err := frm.FieldByClass(FieldClassOptions) if err == nil { - b = append(b, " optlen="...) - b = strconv.AppendInt(b, int64((frm.Fields[iopt].BitLength+7)/8), 10) + b = internal.AppendStrDecimal(b, " optlen=", int64((frm.Fields[iopt].BitLength+7)/8)) } for _, err := range frm.Errors { b = append(b, ' ') diff --git a/ipv4/frame.go b/ipv4/frame.go index 7d6a6c9..8730422 100644 --- a/ipv4/frame.go +++ b/ipv4/frame.go @@ -2,10 +2,9 @@ package ipv4 import ( "encoding/binary" - "fmt" - "net/netip" "github.com/soypat/lneto" + "github.com/soypat/lneto/internal" ) // NewFrame returns a new [Frame] with data set to buf. @@ -238,14 +237,18 @@ func (ifrm Frame) ValidateExceptCRC(v *lneto.Validator) { } func (ifrm Frame) String() string { - dst := netip.AddrFrom4(*ifrm.DestinationAddr()) - src := netip.AddrFrom4(*ifrm.SourceAddr()) - - hl := ifrm.HeaderLength() - tl := int(ifrm.TotalLength()) - ttl := ifrm.TTL() - id := ifrm.ID() - proto := ifrm.Protocol() - tos := ifrm.ToS() - return fmt.Sprintf("IP %s SRC=%s DST=%s LEN=%d OPT=%d TTL=%d ID=%d ToS=0x%x", proto.String(), src.String(), dst.String(), tl, tl-hl, ttl, id, tos) + proto := ifrm.Protocol().String() + b := make([]byte, 0, 91+len(proto)) + b = append(b, "IP ("...) + b = append(b, proto...) + b = append(b, ") src="...) + b = AppendFormatAddr(b, *ifrm.SourceAddr()) + b = append(b, " dst="...) + b = AppendFormatAddr(b, *ifrm.DestinationAddr()) + b = internal.AppendStrDecimal(b, " len=", int64(ifrm.TotalLength())) + b = internal.AppendStrDecimal(b, " opt=", int64(ifrm.HeaderLength()-20)) + b = internal.AppendStrDecimal(b, " ttl=", int64(ifrm.TTL())) + b = internal.AppendStrDecimal(b, " id=", int64(ifrm.ID())) + b = internal.AppendStrHexData(b, " tos=0x", byte(ifrm.ToS())) + return string(b) } diff --git a/tcp/definitions.go b/tcp/definitions.go index df50534..42263ef 100644 --- a/tcp/definitions.go +++ b/tcp/definitions.go @@ -2,12 +2,12 @@ package tcp import ( "errors" - "fmt" "math/bits" "strconv" "unsafe" "github.com/soypat/lneto" + "github.com/soypat/lneto/internal" ) //go:generate stringer -type=State,OptionKind -linecomment -output stringers.go . @@ -73,10 +73,19 @@ func (seg Segment) isFirstSYN() bool { } func (seg Segment) String() string { - if seg.DATALEN == 0 { - return fmt.Sprintf("SEG %s ACK=%d SEQ=%d WND=%d", seg.Flags, seg.ACK, seg.SEQ, seg.WND) + return string(seg.AppendString(nil)) +} + +func (seg Segment) AppendString(b []byte) []byte { + b = append(b, "SEG "...) + b = append(b, seg.Flags.String()...) + b = internal.AppendStrDecimal(b, " ACK=", int64(seg.ACK)) + b = internal.AppendStrDecimal(b, " SEQ=", int64(seg.SEQ)) + b = internal.AppendStrDecimal(b, " WND=", int64(seg.WND)) + if seg.DATALEN > 0 { + b = internal.AppendStrDecimal(b, " DATALEN=", int64(seg.DATALEN)) } - return fmt.Sprintf("SEG %s ACK=%d SEQ=%d WND=%d DATALEN=%d", seg.Flags, seg.ACK, seg.SEQ, seg.WND, seg.DATALEN) + return b } // ClientSynSegment is a the first packet sent over a TCP connection to a server. Typically the client diff --git a/tcp/frame.go b/tcp/frame.go index 7604d10..5df1754 100644 --- a/tcp/frame.go +++ b/tcp/frame.go @@ -2,10 +2,10 @@ package tcp import ( "encoding/binary" - "fmt" "math" "github.com/soypat/lneto" + "github.com/soypat/lneto/internal" ) const ( @@ -170,7 +170,12 @@ func (tfrm Frame) String() string { src := tfrm.SourcePort() dst := tfrm.DestinationPort() seg := tfrm.Segment(len(tfrm.Payload())) - return fmt.Sprintf("TCP :%d -> :%d %s", src, dst, seg.String()) + b := make([]byte, 0, 64) + b = append(b, "TCP "...) + b = internal.AppendStrDecimal(b, " src=", int64(src)) + b = internal.AppendStrDecimal(b, " dst=", int64(dst)) + b = append(b, ' ') + return string(seg.AppendString(b)) } // diff --git a/udp/handler.go b/udp/handler.go index 6a1c67d..38fca1e 100644 --- a/udp/handler.go +++ b/udp/handler.go @@ -1,7 +1,6 @@ package udp import ( - "fmt" "net" "github.com/soypat/lneto" @@ -117,7 +116,7 @@ func (h *Handler) Send(buf []byte) (int, error) { dgram := internal.SliceDequeueFront(&h.txDgrams) n, err := h.txRing.Read(buf[8 : 8+dgram.length]) if err != nil || n != int(dgram.length) { - panic(fmt.Sprintf("udp send handler failure %d %s", n, err)) + panic("udp send handler failure") } ufrm.SetSourcePort(h.lport) ufrm.SetDestinationPort(h.rport) @@ -152,13 +151,13 @@ func (h *Handler) ReadNext(b []byte) (int, error) { dgram := internal.SliceDequeueFront(&h.rxDgrams) n, err := h.rxRing.Read(b[:min(len(b), int(dgram.length))]) if err != nil { - panic(fmt.Sprintf("udp read handler failure %d %s", n, err)) + panic("udp readnext rx ring failure") } discard := int(dgram.length) - len(b) if discard > 0 { err = h.rxRing.ReadDiscard(discard) if err != nil { - panic(fmt.Sprintf("udp readdiscard handler failure %d %s", n, err)) + panic("udp readnext discard failure") } } return n, nil diff --git a/udp/mux.go b/udp/mux.go index 489d0b1..ba8f870 100644 --- a/udp/mux.go +++ b/udp/mux.go @@ -1,7 +1,6 @@ package udp import ( - "fmt" "math" "net" "net/netip" @@ -226,7 +225,7 @@ func (mh *muxHandler) Encapsulate(carrierData []byte, ipOffset, frameOffset int) n, err := mh.txRing.Read(buf[8 : 8+dgram.length]) if err != nil || n != int(dgram.length) { - panic(fmt.Sprintf("udp send handler failure %d %s", n, err)) + panic("udp muxh encaps fail txring read") } ufrm.SetSourcePort(dgram.lport) ufrm.SetDestinationPort(dgram.rport) diff --git a/validation.go b/validation.go index 24a6e6f..ca53056 100644 --- a/validation.go +++ b/validation.go @@ -2,7 +2,6 @@ package lneto import ( "errors" - "fmt" "strconv" ) @@ -82,7 +81,7 @@ type BitPosErr struct { } func (bpe *BitPosErr) Error() string { - return fmt.Sprintf("%s at bits %d..%d", bpe.Err.Error(), bpe.BitStart, bpe.BitStart+bpe.BitLen) + return bpe.Err.Error() } func (bpe *BitPosErr) AppendError(dst []byte) []byte {