mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
ethernet: MTU semantic meaning corrections, add FCS logic (#28)
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+63
-10
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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]})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user