From c97a32f3c12e0c59a3fed8e0d769dca41e8361f2 Mon Sep 17 00:00:00 2001 From: soypat Date: Tue, 1 Sep 2026 09:39:10 -0700 Subject: [PATCH] cull fmt package use and prevent aggressive DCE in MWE example with TinyGo --- arp/frame.go | 24 +----- dhcp/dhcpv4/server.go | 5 +- dns/dns.go | 96 +++++++++++++++++++++++- dns/dns_test.go | 30 +------- examples/min-working-example/main-mwe.go | 26 ++++--- examples/min-working-example/nic.go | 37 +++++++++ internal/strconv.go | 10 +++ internet/pcap/capture.go | 9 +-- ipv4/frame.go | 18 ++--- tcp/definitions.go | 17 ++++- tcp/frame.go | 9 ++- udp/handler.go | 7 +- udp/mux.go | 3 +- validation.go | 3 +- 14 files changed, 197 insertions(+), 97 deletions(-) create mode 100644 examples/min-working-example/nic.go diff --git a/arp/frame.go b/arp/frame.go index fac7374..c52113d 100644 --- a/arp/frame.go +++ b/arp/frame.go @@ -2,9 +2,6 @@ package arp import ( "encoding/binary" - "fmt" - "net" - "net/netip" "github.com/soypat/lneto" "github.com/soypat/lneto/ethernet" @@ -147,21 +144,8 @@ func (afrm Frame) ValidateSize(v *lneto.Validator) { 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) + var rawbuf [11]byte + b := append(rawbuf[:0], "ARP "...) + b = append(b, opstr...) + return string(rawbuf[:len(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..028f5c3 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,15 +82,15 @@ 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{ @@ -111,14 +111,14 @@ func run(ctx context.Context, stack *xnet.StackAsync) error { const sockstream = 0x1 c, err := berkstack.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/strconv.go b/internal/strconv.go index d70e0fb..9beb838 100644 --- a/internal/strconv.go +++ b/internal/strconv.go @@ -1,5 +1,15 @@ package internal +import "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) +} + // 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..6d9f038 100644 --- a/ipv4/frame.go +++ b/ipv4/frame.go @@ -2,8 +2,6 @@ package ipv4 import ( "encoding/binary" - "fmt" - "net/netip" "github.com/soypat/lneto" ) @@ -238,14 +236,10 @@ 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, 5+len(proto)) + b = append(b, "IP ("...) + b = append(b, proto...) + b = append(b, ')') + 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 {