diff --git a/ethernet/crc.go b/ethernet/crc.go new file mode 100644 index 0000000..75b2972 --- /dev/null +++ b/ethernet/crc.go @@ -0,0 +1,47 @@ +package ethernet + +import ( + "encoding/binary" + "hash/crc32" +) + +// +// CRC API. +// + +// crcTable is the IEEE CRC-32 table used for Ethernet FCS calculation. +var crcTable = crc32.MakeTable(crc32.IEEE) + +// CRC32 calculates the Ethernet Frame Check Sequence (FCS) for the given data. +// The CRC is computed using the IEEE 802.3 CRC-32 polynomial. +// The input should be the frame data from destination MAC through payload, +// excluding any existing FCS. +func CRC32(data []byte) uint32 { + return crc32.Checksum(data, crcTable) +} + +// CRC32Search searches for a valid CRC32 in data starting from minOffCRC. +// It computes the CRC incrementally from minOffCRC, checking at each position +// if the CRC matches the next 4 bytes (little-endian FCS). +// Returns the offset where a valid CRC was found, or -1 if no valid CRC exists. +// This is useful when the exact frame length is unknown but bounded by minOffCRC. +func CRC32Search(data []byte, minOffCRC int) (foundOffOrNegative int) { + if minOffCRC < 0 { + minOffCRC = 0 + } + if len(data) < minOffCRC+4 { + return -1 + } + // Calculate CRC up to minOffCRC + crc := crc32.Checksum(data[:minOffCRC], crcTable) + // Incrementally extend and check at each position + for off := minOffCRC; off <= len(data)-4; off++ { + got := binary.LittleEndian.Uint32(data[off:]) + if crc == got { + return off + } + // Extend CRC by one byte for next iteration + crc = crc32.Update(crc, crcTable, data[off:off+1]) + } + return -1 +} diff --git a/ethernet/crc_test.go b/ethernet/crc_test.go new file mode 100644 index 0000000..3eec98b --- /dev/null +++ b/ethernet/crc_test.go @@ -0,0 +1,100 @@ +package ethernet + +import ( + "encoding/binary" + "testing" +) + +func TestCRC32Search(t *testing.T) { + // Helper to create test data with CRC appended at a specific position. + makeDataWithCRC := func(payloadLen int) []byte { + data := make([]byte, payloadLen+4) + for i := range data[:payloadLen] { + data[i] = byte(i) + } + crc := CRC32(data[:payloadLen]) + binary.LittleEndian.PutUint32(data[payloadLen:], crc) + return data + } + + t.Run("finds CRC at end", func(t *testing.T) { + data := makeDataWithCRC(100) + off := CRC32Search(data, 0) + if off != 100 { + t.Errorf("expected offset 100, got %d", off) + } + }) + + t.Run("finds CRC with minOff before CRC", func(t *testing.T) { + data := makeDataWithCRC(100) + off := CRC32Search(data, 50) + if off != 100 { + t.Errorf("expected offset 100, got %d", off) + } + }) + + t.Run("finds CRC with minOff exactly at CRC", func(t *testing.T) { + data := makeDataWithCRC(100) + off := CRC32Search(data, 100) + if off != 100 { + t.Errorf("expected offset 100, got %d", off) + } + }) + + t.Run("returns -1 when minOff past CRC", func(t *testing.T) { + data := makeDataWithCRC(100) + off := CRC32Search(data, 101) + if off != -1 { + t.Errorf("expected -1, got %d", off) + } + }) + + t.Run("returns -1 when no valid CRC", func(t *testing.T) { + data := make([]byte, 100) + for i := range data { + data[i] = byte(i) + } + off := CRC32Search(data, 0) + if off != -1 { + t.Errorf("expected -1, got %d", off) + } + }) + + t.Run("returns -1 when data too short", func(t *testing.T) { + data := []byte{1, 2, 3} + off := CRC32Search(data, 0) + if off != -1 { + t.Errorf("expected -1, got %d", off) + } + }) + + t.Run("handles negative minOff", func(t *testing.T) { + data := makeDataWithCRC(20) + off := CRC32Search(data, -5) + if off != 20 { + t.Errorf("expected offset 20, got %d", off) + } + }) + + t.Run("finds CRC at position 0", func(t *testing.T) { + // Empty payload, just CRC + data := make([]byte, 4) + crc := CRC32(nil) + binary.LittleEndian.PutUint32(data, crc) + off := CRC32Search(data, 0) + if off != 0 { + t.Errorf("expected offset 0, got %d", off) + } + }) + + t.Run("finds first valid CRC when multiple could match", func(t *testing.T) { + // Create data where CRC is at position 50 + data := makeDataWithCRC(50) + // Extend with more bytes (the search should still find first match) + data = append(data, make([]byte, 50)...) + off := CRC32Search(data, 0) + if off != 50 { + t.Errorf("expected offset 50 (first match), got %d", off) + } + }) +} diff --git a/ethernet/definitions.go b/ethernet/definitions.go index 63c9c57..e131f6a 100644 --- a/ethernet/definitions.go +++ b/ethernet/definitions.go @@ -5,6 +5,12 @@ import ( ) const ( + // MaxOverheadSize is the maximum overhead a packet can incur + // from transmitting an ethernet frame. Includes: + // - 14 bytes of Ethernet header containing MAC addresses and ethernet type, always present + // - 4 bytes of VLAN tag, if present. + // - 4 bytes of the 32 bit trailing CRC, if required by PHY. + MaxOverheadSize = 14 + 4 + 4 sizeHeaderNoVLAN = 14 ) diff --git a/internal/ltesto/httptap.go b/internal/ltesto/httptap.go index 0cca450..33a15ef 100644 --- a/internal/ltesto/httptap.go +++ b/internal/ltesto/httptap.go @@ -13,6 +13,8 @@ import ( "net/url" "sync" "time" + + "github.com/soypat/lneto/ethernet" ) const minMTU = 256 @@ -58,7 +60,7 @@ func (h *HTTPTapClient) IPMask() (netip.Prefix, error) { func (h *HTTPTapClient) MTU() (int, error) { err := h.ensureMTU() - return len(h.buf), err + return len(h.buf) - ethernet.MaxOverheadSize, err // Return payload MTU, not buffer size } func (h *HTTPTapClient) HardwareAddress6() ([6]byte, error) { @@ -85,7 +87,7 @@ func (h *HTTPTapClient) ensureMTU() (err error) { if err != nil { return err } - h.buf = make([]byte, info.MTU) + h.buf = make([]byte, info.MTU+ethernet.MaxOverheadSize) hw, err := net.ParseMAC(info.HardwareAddr) if err == nil { copy(h.hwaddr[:], hw) @@ -215,7 +217,7 @@ func NewHTTPTapServer(iface Interface, minMTU, queueOut, queueIn int) (*HTTPTapS if err != nil { return nil, err } - bufferSize := max(mtu, minMTU) + bufferSize := max(mtu, minMTU) + ethernet.MaxOverheadSize netmask, err := iface.IPMask() if err != nil { return nil, err diff --git a/internet/stack-ethernet.go b/internet/stack-ethernet.go index 499c641..3c0f4f3 100644 --- a/internet/stack-ethernet.go +++ b/internet/stack-ethernet.go @@ -1,6 +1,7 @@ package internet import ( + "encoding/binary" "errors" "io" "log/slog" @@ -11,12 +12,35 @@ import ( "github.com/soypat/lneto/ethernet" ) +// StackEthernetConfig contains configuration parameters for [StackEthernet]. +type StackEthernetConfig struct { + // MTU is the Maximum Transmission Unit, representing the maximum ethernet + // payload size in bytes (excluding the 14-byte ethernet header). + // Standard ethernet MTU is 1500. Must be between 256 and 65535. + MTU int + // MaxNodes is the maximum number of protocol handlers (e.g., IP, ARP) + // that can be registered with the stack. Must be greater than 0. + MaxNodes int + // MAC is the local hardware (MAC) address for this ethernet interface. + MAC [6]byte + // Gateway is the hardware (MAC) address of the default gateway. + // Outgoing frames will be addressed to this MAC. + Gateway [6]byte + // AppendCRC32, if true, appends a 32-bit CRC to outgoing frames. + // Only needed if the PHY does not handle CRC generation. + AppendCRC32 bool + // CRC32Update is a IEEE CRC32 implementation that should be provided if AppendCRC32 is set. + CRC32Update func(crc uint32, p []byte) uint32 +} + type StackEthernet struct { connID uint64 handlers handlers mac [6]byte gwmac [6]byte mtu uint16 + // crcupdate set when crc32 has been configured to be appended. + crcupdate func(crc uint32, p []byte) uint32 } func (ls *StackEthernet) SetGateway6(gw [6]byte) { @@ -35,19 +59,39 @@ func (ls *StackEthernet) HardwareAddr6() [6]byte { return ls.mac } +// Reset6 resets the stack with the given parameters. +// +// Deprecated: Use [StackEthernet.Configure] instead. func (ls *StackEthernet) Reset6(mac, gateway [6]byte, mtu, maxNodes int) error { - if mtu > math.MaxUint16 || mtu < 256 { + return ls.Configure(StackEthernetConfig{ + MTU: mtu, + MaxNodes: maxNodes, + MAC: mac, + Gateway: gateway, + }) +} + +// Configure resets and configures the ethernet stack with the given configuration. +// It validates the configuration parameters and resets internal state. +// The connection ID is incremented on each call to invalidate existing connections. +func (ls *StackEthernet) Configure(cfg StackEthernetConfig) error { + if cfg.MTU > (math.MaxUint16-ethernet.MaxOverheadSize) || cfg.MTU < 256 { return errors.New("invalid MTU") - } else if maxNodes <= 0 { + } else if cfg.MaxNodes <= 0 { return errZeroMaxNodesArg + } else if cfg.AppendCRC32 && cfg.CRC32Update == nil { + return errors.New("need CRC32Update to append ethernet CRC") } - ls.handlers.reset("StackEthernet", maxNodes) + ls.handlers.reset("StackEthernet", cfg.MaxNodes) *ls = StackEthernet{ connID: ls.connID + 1, handlers: ls.handlers, - mac: mac, - gwmac: gateway, - mtu: uint16(mtu), + mac: cfg.MAC, + gwmac: cfg.Gateway, + mtu: uint16(cfg.MTU), + } + if cfg.AppendCRC32 { + ls.crcupdate = cfg.CRC32Update } return nil } @@ -95,7 +139,11 @@ DROP: func (ls *StackEthernet) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) { mtu := ls.mtu dst := carrierData[offsetToFrame:] - if len(dst) < int(mtu) { + requiredSize := int(mtu) + 14 + if ls.crcupdate != nil { + requiredSize += 4 + } + if len(dst) < requiredSize { return 0, io.ErrShortBuffer } efrm, err := ethernet.NewFrame(dst) @@ -108,9 +156,9 @@ func (ls *StackEthernet) Encapsulate(carrierData []byte, offsetToIP, offsetToFra // For IP: offsetToIP=14, offsetToFrame=14 // For ARP: offsetToIP=-1, offsetToFrame=14 (but ARP ignores offsetToIP) // Clip carrierData to MTU to prevent writes beyond MTU limit. - offsetToFrame += 14 - mtuLimit := offsetToFrame + int(mtu) - h, n, err = ls.handlers.encapsulateAny(carrierData[:mtuLimit], offsetToFrame, offsetToFrame) + payloadOffset := offsetToFrame + 14 + mtuLimit := payloadOffset + int(mtu) + h, n, err = ls.handlers.encapsulateAny(carrierData[:mtuLimit], payloadOffset, payloadOffset) if n == 0 { return n, err } @@ -118,5 +166,10 @@ func (ls *StackEthernet) Encapsulate(carrierData []byte, offsetToIP, offsetToFra *efrm.SourceHardwareAddr() = ls.mac efrm.SetEtherType(ethernet.Type(h.proto)) n += 14 + if ls.crcupdate != nil { + crc := ls.crcupdate(0, carrierData[offsetToFrame:offsetToFrame+n]) + binary.LittleEndian.PutUint32(carrierData[offsetToFrame+n:], crc) + n += 4 + } return n, err } diff --git a/x/xnet/stack-async.go b/x/xnet/stack-async.go index 3847055..64545ec 100644 --- a/x/xnet/stack-async.go +++ b/x/xnet/stack-async.go @@ -325,6 +325,8 @@ func (s *StackAsync) StartLookupIP(host string) error { return err } + // EDNS0 buffer size: MTU minus overhead for IP+UDP headers and safety margin. + // 100 bytes covers IPv4 max header (60) + UDP (8) + 32 byte margin. s.ednsopt.SetEDNS0(uint16(s.link.MTU())-100, 0, 0, nil) rand := s.prand32() err = s.dns.StartResolve(uint16(rand>>1)+1024, uint16(rand), dns.ResolveConfig{ diff --git a/x/xnet/xnet_concurrent_test.go b/x/xnet/xnet_concurrent_test.go index ca5e64f..d73920e 100644 --- a/x/xnet/xnet_concurrent_test.go +++ b/x/xnet/xnet_concurrent_test.go @@ -11,15 +11,18 @@ import ( "testing" "time" + "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/tcp" ) func TestTCPListener_ConcurrentEcho(t *testing.T) { const ( - numClients = 10 - serverPort = 8080 - MTU = 1500 - seed = 1 + numClients = 10 + serverPort = 8080 + MTU = 1500 + carrierSize = MTU + ethernet.MaxOverheadSize + tcpBufSize = MTU + seed = 1 ) // 1. Setup server stack with tcp.Listener. @@ -63,7 +66,7 @@ func TestTCPListener_ConcurrentEcho(t *testing.T) { // 2. Setup client stacks (one per client). clientStacks := make([]StackAsync, numClients) clientConns := make([]tcp.Conn, numClients) - connBufs := make([]byte, numClients*MTU*2) // RX+TX buffer space for all clients + connBufs := make([]byte, numClients*tcpBufSize*2) // RX+TX buffer space for all clients for i := range clientStacks { clientMAC := [6]byte{0xaa, 0xbb, 0xcc, 0x00, 0x01, byte(i + 1)} @@ -83,10 +86,10 @@ func TestTCPListener_ConcurrentEcho(t *testing.T) { clientStacks[i].SetGateway6(serverMAC) // Configure client connection buffers. - bufOff := i * MTU * 2 + bufOff := i * tcpBufSize * 2 err = clientConns[i].Configure(tcp.ConnConfig{ - RxBuf: connBufs[bufOff : bufOff+MTU], - TxBuf: connBufs[bufOff+MTU : bufOff+2*MTU], + RxBuf: connBufs[bufOff : bufOff+tcpBufSize], + TxBuf: connBufs[bufOff+tcpBufSize : bufOff+2*tcpBufSize], TxPacketQueueSize: 4, }) if err != nil { @@ -140,7 +143,7 @@ func TestTCPListener_ConcurrentEcho(t *testing.T) { func kernelLoop(ctx context.Context, server *StackAsync, clients []StackAsync) { const MTU = 1500 - const carrierDataSize = MTU + 14 + const carrierDataSize = MTU + ethernet.MaxOverheadSize buf := make([]byte, carrierDataSize) rng := rand.New(rand.NewSource(1)) // Seed 1 for deterministic but randomized order order := make([]int, len(clients)) @@ -174,7 +177,7 @@ func kernelLoop(ctx context.Context, server *StackAsync, clients []StackAsync) { func routePacketToClient(pkt []byte, clients []StackAsync) { // Extract destination IP from IPv4 header (offset 16-19 in IP header, after 14 byte Ethernet header). - if len(pkt) < 34 { // 14 ethernet + 20 min IP header + if len(pkt) < 20+ethernet.MaxOverheadSize { // 20 min IP header return } dstIP := netip.AddrFrom4([4]byte{pkt[30], pkt[31], pkt[32], pkt[33]}) diff --git a/x/xnet/xnet_dns_test.go b/x/xnet/xnet_dns_test.go index 7d1f739..4820cac 100644 --- a/x/xnet/xnet_dns_test.go +++ b/x/xnet/xnet_dns_test.go @@ -47,7 +47,7 @@ func TestDNS_QueryReceivesAnswer(t *testing.T) { } // Client sends DNS query. - const carrierDataSize = MTU + 14 + const carrierDataSize = MTU + ethernet.MaxOverheadSize var buf [carrierDataSize]byte n, err := client.Encapsulate(buf[:], -1, 0) if err != nil { diff --git a/x/xnet/xnet_listener_test.go b/x/xnet/xnet_listener_test.go index a7a1a42..e6036f4 100644 --- a/x/xnet/xnet_listener_test.go +++ b/x/xnet/xnet_listener_test.go @@ -5,12 +5,14 @@ import ( "testing" "time" + "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/tcp" ) func TestStackAsyncListener_SingleConnection(t *testing.T) { const seed int64 = 1234 const MTU = 1500 + const carrierSize = MTU + ethernet.MaxOverheadSize const svPort = 80 const clPort = 1337 @@ -121,6 +123,7 @@ func TestStackAsyncListener_SingleConnection(t *testing.T) { func TestStackAsyncListener_MultiSequentialConn(t *testing.T) { const seed int64 = 1234 const MTU = 1500 + const carrierSize = MTU + ethernet.MaxOverheadSize const svPort = 80 const clPort = 1337 const poolsize = 10 diff --git a/x/xnet/xnet_test.go b/x/xnet/xnet_test.go index a5625cf..104f863 100644 --- a/x/xnet/xnet_test.go +++ b/x/xnet/xnet_test.go @@ -217,7 +217,7 @@ func newTCPStacks(t *testing.T, randSeed int64, mtu int) (s1, s2 *StackAsync, c1 } func testerFrom(t *testing.T, mtu int) *tester { - carrierDataSize := mtu + 14 + carrierDataSize := mtu + ethernet.MaxOverheadSize return &tester{ t: t, buf: make([]byte, carrierDataSize),