Error rewrites (#43)

* pcap: reuse Frame memory

* slog: reduce heap allocations of addresses; also prevent heap alloc of dhcp options in pcap

* dns: heapless improvement; add StackAsync buffer for more heapless operation; start thinking of errors

* errors: begin standardise errors in lneto

* errors: finish standardization of errors

* fix merge issues

* add more lneto errors to rest of package

* format errors.go
This commit is contained in:
Pat Whittingslow
2026-02-28 20:18:27 +01:00
committed by GitHub
parent 3d0bc93cbe
commit fa5ba918bb
38 changed files with 272 additions and 322 deletions
+6 -10
View File
@@ -15,12 +15,8 @@ import (
)
var (
errDeadlineExceeded = os.ErrDeadlineExceeded
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")
errDeadlineExceeded = os.ErrDeadlineExceeded
errNoRemoteAddr = errors.New("tcp: no remote address established")
)
// Conn builds on the [Handler] abstraction and adds IP header knowledge, time management, and familiar user facing API
@@ -135,7 +131,7 @@ func (conn *Conn) OpenActive(localPort uint16, remote netip.AddrPort, iss Value)
conn.mu.Lock()
defer conn.mu.Unlock()
if !remote.IsValid() {
return errInvalidIP
return lneto.ErrInvalidAddr
}
rport := remote.Port()
err := conn.h.OpenActive(localPort, rport, iss)
@@ -330,14 +326,14 @@ func (conn *Conn) Demux(buf []byte, off int) (err error) {
conn.mu.Lock()
defer conn.mu.Unlock()
if off >= len(buf) {
return errBadDemuxOffset
return lneto.ErrShortBuffer
}
raddr, _, id, _, err := internal.GetIPAddr(buf[:off])
if err != nil {
return err
}
if conn.isRaddrSet() && !internal.BytesEqual(conn.remoteAddr, raddr) {
return errIPAddrMismatch
return lneto.ErrMismatch
}
conn.trace("tcpconn.Recv", slog.Uint64("lport", uint64(conn.h.LocalPort())), slog.Uint64("rport", uint64(conn.h.remotePort)))
err = conn.h.Recv(buf[off:])
@@ -365,7 +361,7 @@ func (conn *Conn) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
if err != nil {
return 0, err
} else if len(raddr) != len(conn.remoteAddr) {
return 0, errMismatchedIPVersion
return 0, lneto.ErrMismatchLen
}
n, err = conn.h.Send(carrierData[offsetToFrame:])
if err != nil || n == 0 {
+12 -12
View File
@@ -6,24 +6,24 @@ import (
"math/bits"
"strconv"
"unsafe"
"github.com/soypat/lneto"
)
//go:generate stringer -type=State,OptionKind -linecomment -output stringers.go .
var (
// errDropSegment is a flag that signals to drop a segment silently.
errDropSegment = errors.New("drop segment")
errWindowTooLarge = errors.New("invalid window size > 2**16")
errDropSegment error = lneto.ErrPacketDrop
errWindowTooLarge = errors.New("invalid window size > 2**16")
errBufferTooSmall = errors.New("tcp buffer too small")
errNeedClosedTCBToOpen = errors.New("need closed TCB to call open")
errInvalidState = errors.New("invalid state")
errConnNotExist = errors.New("connection does not exist")
errConnectionClosing = errors.New("connection closing")
errExpectedSYN = errors.New("seqs:expected SYN")
errBadSegack = errors.New("seqs:bad segack")
errFinwaitExpectedACK = errors.New("seqs:finwait1 expected ACK")
errFinwaitExpectedFinack = errors.New("seqs:finwait2 expected FINACK")
errBufferTooSmall error = lneto.ErrShortBuffer
errNeedClosedTCBToOpen = errors.New("need closed TCB to call open")
errInvalidState = errors.New("invalid state")
errConnNotExist = errors.New("connection does not exist")
errConnectionClosing = errors.New("connection closing")
errExpectedSYN = errors.New("seqs:expected SYN")
errBadSegack = errors.New("seqs:bad segack")
errFinwaitExpectedACK = errors.New("seqs:finwait1 expected ACK")
errWindowOverflow = newRejectErr("wnd > 2**16")
errSeqNotInWindow = newRejectErr("seq not in snd/rcv.wnd")
+5 -13
View File
@@ -2,7 +2,6 @@ package tcp
import (
"encoding/binary"
"errors"
"fmt"
"math"
@@ -19,7 +18,7 @@ const (
// with payload/options of frames to avoid panics.
func NewFrame(buf []byte) (Frame, error) {
if len(buf) < sizeHeaderTCP {
return Frame{buf: nil}, errors.New("TCP packet too short")
return Frame{buf: nil}, lneto.ErrShortBuffer
}
return Frame{buf: buf}, nil
}
@@ -178,13 +177,6 @@ func (tfrm Frame) String() string {
// Validation API
//
var (
errShortTCP = errors.New("TCP offset exceeds frame")
errBadTCPOff = errors.New("TCP offset invalid")
errEvilPacket = errors.New("evil packet")
errZeroDstPort = errors.New("TCP zero destination port")
errZeroSrcPort = errors.New("TCP zero source port")
)
// func (tfrm Frame) Validate(v *lneto.Validator) {
// tfrm.ValidateSize(v)
@@ -196,19 +188,19 @@ var (
func (tfrm Frame) ValidateSize(v *lneto.Validator) {
off := tfrm.HeaderLength()
if off < sizeHeaderTCP {
v.AddBitPosErr(12*8, 4, errBadTCPOff)
v.AddBitPosErr(12*8, 4, lneto.ErrInvalidLengthField)
}
if off > len(tfrm.RawData()) {
v.AddBitPosErr(12*8, 4, errShortTCP)
v.AddBitPosErr(12*8, 4, lneto.ErrInvalidLengthField)
}
}
func (tfrm Frame) ValidateExceptCRC(v *lneto.Validator) {
tfrm.ValidateSize(v)
if tfrm.DestinationPort() == 0 {
v.AddBitPosErr(2*8, 16, errZeroDstPort)
v.AddBitPosErr(2*8, 16, lneto.ErrZeroDestination)
}
if tfrm.SourcePort() == 0 {
v.AddBitPosErr(0, 16, errZeroSrcPort)
v.AddBitPosErr(0, 16, lneto.ErrZeroSource)
}
}
+5 -11
View File
@@ -1,7 +1,6 @@
package tcp
import (
"errors"
"io"
"net"
@@ -11,11 +10,6 @@ import (
"github.com/soypat/lneto/internal"
)
var (
errMismatchedSrcPort = errors.New("source port mismatch")
errMismatchedDstPort = errors.New("destination port mismatch")
)
// Handler is a low level TCP handling data structure. It implements logic
// related to data buffering, frame sequencing and connection state handling.
// Does NOT implement IP related logic, so no CRC calculation/validation or pseudo header logic.
@@ -56,10 +50,10 @@ func (h *Handler) State() State { return h.scb.State() }
// If the argument buffer is nil then the respective currently set buffer will be reused.
func (h *Handler) SetBuffers(txbuf, rxbuf []byte, packets int) error {
if h.bufRx.Buf == nil && (len(rxbuf) < minBufferSize || len(txbuf) < minBufferSize) {
return errors.New("tcp: short buffer")
return lneto.ErrShortBuffer
}
if !h.scb.State().IsClosed() {
return errors.New("tcp.Handler must be closed before setting buffers")
return lneto.ErrInvalidConfig
}
if rxbuf != nil {
h.bufRx.Buf = rxbuf
@@ -156,15 +150,15 @@ func (h *Handler) Recv(incomingPacket []byte) error {
remotePort := tfrm.SourcePort()
if h.remotePort != 0 && remotePort != h.remotePort {
return errMismatchedSrcPort
return lneto.ErrMismatch
}
dstPort := tfrm.DestinationPort()
if h.localPort != dstPort {
return errMismatchedDstPort
return lneto.ErrMismatch
}
payload := tfrm.Payload()
if len(payload) > h.bufRx.Free() {
return errors.New("rx buffer full")
return lneto.ErrBufferFull
}
segIncoming := tfrm.Segment(len(payload))
if h.scb.IncomingIsKeepalive(segIncoming) {
+5 -6
View File
@@ -1,7 +1,6 @@
package tcp
import (
"errors"
"log/slog"
"net"
"sync"
@@ -70,7 +69,7 @@ func (listener *Listener) Close() error {
listener.mu.Lock()
defer listener.mu.Unlock()
if listener.isClosed() {
return errors.New("already closed")
return net.ErrClosed
}
listener.debug("listener:reset", slog.Uint64("port", uint64(listener.port)))
listener.connID++
@@ -80,9 +79,9 @@ func (listener *Listener) Close() error {
func (listener *Listener) Reset(port uint16, pool pool) error {
if port == 0 {
return errZeroDstPort
return lneto.ErrZeroSource
} else if pool == nil {
return errors.New("nil TCP pool")
return lneto.ErrInvalidConfig
}
listener.mu.Lock()
defer listener.mu.Unlock()
@@ -126,7 +125,7 @@ func (listener *Listener) TryAccept() (*Conn, any, error) {
listener.incoming[i] = handler{} // discard from ready.
return conn, userData, nil
}
return nil, nil, errors.New("no conns available")
return nil, nil, lneto.ErrExhausted
}
// Encapsulate implements [StackNode].
@@ -199,7 +198,7 @@ func (listener *Listener) Demux(carrierData []byte, tcpFrameOffset int) error {
}
dst := tfrm.DestinationPort()
if dst != listener.port {
return errors.New("not our port")
return lneto.ErrMismatch
}
src := tfrm.SourcePort()
+8 -8
View File
@@ -1,9 +1,9 @@
package tcp
import (
"errors"
"fmt"
"strings"
"github.com/soypat/lneto"
)
type OptionKind uint8
@@ -88,11 +88,11 @@ func (op OptionCodec) PutOption32(dst []byte, kind OptionKind, v uint32) (int, e
func (op OptionCodec) PutOption(dst []byte, kind OptionKind, data ...byte) (int, error) {
putSize := 2 + len(data)
if len(dst) < putSize {
return -1, errBufferTooSmall
return -1, lneto.ErrShortBuffer
} else if putSize > 255 {
return -1, errors.New("option data too large")
return -1, lneto.ErrInvalidLengthField
} else if kind == OptNop || kind == OptEnd {
return -1, errors.New("cant put Nop or End option type")
return -1, lneto.ErrInvalidField
}
dst[0] = byte(kind)
dst[1] = byte(putSize)
@@ -111,13 +111,13 @@ func (op OptionCodec) ForEachOption(opts []byte, fn func(OptionKind, []byte) err
continue
}
if len(opts[off:]) < 1 {
return errors.New("short TCP options")
return lneto.ErrShortBuffer
}
size := int(opts[off]) // Total option length including kind and length bytes.
off++
dataLen := size - 2 // Data bytes after kind and length.
if dataLen < 0 || len(opts[off:]) < dataLen {
return fmt.Errorf("option %q length %d exceeds buffer size %d", kind.String(), size, len(opts[off:]))
return lneto.ErrShortBuffer
}
if !skipSizeValidation {
@@ -133,7 +133,7 @@ func (op OptionCodec) ForEachOption(opts []byte, fn func(OptionKind, []byte) err
expectSize = 2
}
if expectSize != -1 && size != expectSize {
return fmt.Errorf("bad TCP option %q size want %d got %d", kind.String(), expectSize, size)
return lneto.ErrInvalidLengthField
}
}
if !(skipObsolete && kind.IsObsolete()) {
+4 -5
View File
@@ -2,8 +2,9 @@ package tcp
import (
"encoding/binary"
"errors"
"io"
"github.com/soypat/lneto"
)
// Embed low 5 bits of counter into cookie for efficient validation.
@@ -45,15 +46,13 @@ type SYNCookieConfig struct {
MaxCounterDelta uint32
}
var (
errInvalidCookie = errors.New("tcp: invalid SYN cookie")
)
var errInvalidCookie error = lneto.ErrMismatch
// Reset initializes or reinitializes the SYNCookie with the given configuration.
// The counter is preserved across resets to maintain cookie validity during secret rotation.
func (sc *SYNCookieJar) Reset(config SYNCookieConfig) error {
if config.Rand == nil {
return errors.New("need rand function")
return lneto.ErrInvalidConfig
}
_, err := io.ReadFull(config.Rand, sc.secret[:])
if err != nil {
+9 -16
View File
@@ -1,20 +1,12 @@
package tcp
import (
"errors"
"log/slog"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
)
var (
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 (
// this must be at least 2 for buffer to work.
minBufferSize = 2
@@ -60,9 +52,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 errQueuedPacketsLEZ
return lneto.ErrInvalidConfig
} else if len(buf) < minBufferSize || len(buf) < maxqueuedPackets {
return errInvalidBufSize
return lneto.ErrShortBuffer
}
*rtx = ringTx{
@@ -127,11 +119,12 @@ func (rtx *ringTx) Write(b []byte) (n int, err error) {
func (rtx *ringTx) MakePacket(b []byte, currentSeq Value) (int, error) {
free := rtx.slist.Free()
if free == 0 {
return 0, errPacketQueueFull
return 0, lneto.ErrBufferFull
}
endSeq, ok := rtx.sentEndSeq()
if ok && currentSeq.LessThan(endSeq) {
return 0, errSeqLessThanLast
internal.LogAttrs(nil, slog.LevelError, "txqueue:seq<endseq", slog.Uint64("seq", uint64(currentSeq)), slog.Uint64("endseq", uint64(endSeq)))
return 0, lneto.ErrBug
}
// Reading unsent ring consumes unsent and converts it to "sent".
unsent, _ := rtx.unsentRing()
@@ -321,11 +314,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 errNoPacketToAck
return lneto.ErrPacketDrop
}
endseq := newest.endSeq()
if endseq.LessThan(ack) {
return errAckUnsent
return lneto.ErrPacketDrop
}
// Mark fully acked.
for i := 0; i < len(sl.pkts); i++ {