ICMP tests and various fixes (#68)

* various icmp fixes

* improve existing tests
This commit is contained in:
Pat Whittingslow
2026-04-09 12:31:31 -03:00
committed by GitHub
parent 80e4c59ae7
commit 8813ccef82
7 changed files with 212 additions and 43 deletions
-3
View File
@@ -105,9 +105,6 @@ func (sb *StackIP) Demux(carrierData []byte, offset int) error {
off := ifrm.HeaderLength() off := ifrm.HeaderLength()
totalLen := ifrm.TotalLength() totalLen := ifrm.TotalLength()
proto := ifrm.Protocol() proto := ifrm.Protocol()
if proto == lneto.IPProtoICMP {
return sb.recvicmp(ifrm.Payload())
}
node := sb.handlers.nodeByProto(uint16(proto)) node := sb.handlers.nodeByProto(uint16(proto))
// nodeIdx := getNodeByProto(sb.handlers, uint16(proto)) // nodeIdx := getNodeByProto(sb.handlers, uint16(proto))
if node == nil { if node == nil {
+3 -2
View File
@@ -188,6 +188,7 @@ func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int)
} }
copy(data[written:written+size%len(pattern)], pattern) copy(data[written:written+size%len(pattern)], pattern)
n = sizeHeader + size n = sizeHeader + size
out.key |= keyHashSentBit
raddr = out.raddr raddr = out.raddr
} else { } else {
return 0, nil return 0, nil
@@ -198,8 +199,8 @@ func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int)
var crc lneto.CRC791 var crc lneto.CRC791
sum := crc.PayloadSum16(carrierData[frameOffset : frameOffset+n]) sum := crc.PayloadSum16(carrierData[frameOffset : frameOffset+n])
ifrm.SetCRC(sum) ifrm.SetCRC(sum)
if frameOffset >= 20 { if ipOffset >= 0 {
err = internal.SetIPAddrs(carrierData, 0, nil, raddr[:]) err = internal.SetIPAddrs(carrierData[ipOffset:], 0, nil, raddr[:])
} }
return n, err return n, err
} }
+40 -7
View File
@@ -18,6 +18,7 @@ func TestClients(t *testing.T) {
ResponseQueueBuffer: make([]byte, sizebuffer), ResponseQueueBuffer: make([]byte, sizebuffer),
ResponseQueueLimit: queuesize, ResponseQueueLimit: queuesize,
HashSeed: testHashSeed, HashSeed: testHashSeed,
ID: 1,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -26,6 +27,7 @@ func TestClients(t *testing.T) {
ResponseQueueBuffer: make([]byte, sizebuffer), ResponseQueueBuffer: make([]byte, sizebuffer),
ResponseQueueLimit: queuesize, ResponseQueueLimit: queuesize,
HashSeed: testHashSeed, HashSeed: testHashSeed,
ID: 2,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -38,13 +40,30 @@ func TestClients(t *testing.T) {
if !completed || !ok { if !completed || !ok {
t.Fatal("ping did not complete or not exist") t.Fatal("ping did not complete or not exist")
} }
n, err := sender.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Error(err)
} else if n > 0 {
t.Error("sender: expected no more data to be sent")
}
n, err = responder.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Error(err)
} else if n > 0 {
t.Error("responder: expected no more data to be sent")
}
} }
func testSingleExchange(t *testing.T, sender, responder *Client, buf []byte, pattern []byte, size uint16) (senderKey uint32) { func testSingleExchange(t *testing.T, sender, responder *Client, buf []byte, pattern []byte, size uint16) (senderKey uint32) {
var n int
senderKey, n = testSendEcho(t, sender, buf, pattern, size)
const frameOff = 0 const frameOff = 0
const ipOff = 0 const ipOff = -1
n, err := responder.Encapsulate(buf, ipOff, frameOff)
if err != nil {
t.Fatal(err)
} else if n > 0 {
t.Fatal("expected no data pending to be sent from responder")
}
senderKey, n = testSendEcho(t, sender, buf, pattern, size)
completed, ok := sender.PingPeek(senderKey) completed, ok := sender.PingPeek(senderKey)
if !ok { if !ok {
t.Error("ping key not exist") t.Error("ping key not exist")
@@ -58,7 +77,7 @@ func testSingleExchange(t *testing.T, sender, responder *Client, buf []byte, pat
if err1 != nil { if err1 != nil {
t.Error("responder demux during single", err1) t.Error("responder demux during single", err1)
} }
n, err := responder.Encapsulate(buf, ipOff, frameOff) n, err = responder.Encapsulate(buf, ipOff, frameOff)
if err != nil { if err != nil {
t.Error("responder encaps during single", err) t.Error("responder encaps during single", err)
return return
@@ -96,17 +115,31 @@ func testSingleExchange(t *testing.T, sender, responder *Client, buf []byte, pat
if completed2, ok2 := sender.PingPeek(senderKey); completed != completed2 || ok != ok2 { if completed2, ok2 := sender.PingPeek(senderKey); completed != completed2 || ok != ok2 {
t.Error("change in status after peek") t.Error("change in status after peek")
} }
n, err = sender.Encapsulate(buf, ipOff, frameOff)
if err != nil {
t.Error("error after done")
}
if n > 0 {
t.Error("expected no data to be sent after ping completion", n)
}
return senderKey return senderKey
} }
func testSendEcho(t *testing.T, sender *Client, buf []byte, pattern []byte, size uint16) (key uint32, n int) { func testSendEcho(t *testing.T, sender *Client, buf []byte, pattern []byte, size uint16) (key uint32, n int) {
t.Helper() t.Helper()
key, err := sender.PingStart([4]byte{1}, pattern, size) const frameOff = 0
const ipOff = -1
n, err := sender.Encapsulate(buf, ipOff, frameOff)
if err != nil {
t.Fatal(err)
} else if n > 0 {
t.Fatal("expected no data pending to send on testSendEcho start")
}
key, err = sender.PingStart([4]byte{1}, pattern, size)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
const frameOff = 0
const ipOff = 0
n, err = sender.Encapsulate(buf[:], ipOff, frameOff) n, err = sender.Encapsulate(buf[:], ipOff, frameOff)
if err != nil { if err != nil {
t.Errorf("sender encapsulate: %v", err) t.Errorf("sender encapsulate: %v", err)
+9 -13
View File
@@ -5,7 +5,6 @@ import (
"encoding/binary" "encoding/binary"
"math" "math"
"math/bits" "math/bits"
"sync"
"time" "time"
"github.com/soypat/lneto" "github.com/soypat/lneto"
@@ -265,24 +264,21 @@ func (d Date) Time() (time.Time, error) {
return baseTime.Add(off), nil return baseTime.Add(off), nil
} }
var (
ntpOnceSystemClock sync.Once
sysPrec int8
)
// CalculateSystemPrecision calculates the NTP system precision for a time source. // CalculateSystemPrecision calculates the NTP system precision for a time source.
// If the time source is nil the default static call to [time.Now] is used. // If the time source is nil the default static call to [time.Now]->[time.Time.UnixNano] is used.
func CalculateSystemPrecision(now func() time.Time, iters []time.Time) int8 { func CalculateSystemPrecision(nowNano func() int64, iters []int64) int8 {
maxIter := len(iters) maxIter := len(iters)
if now == nil { if nowNano == nil {
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
iters[i] = time.Now() iters[i] = time.Now().UnixNano()
} }
} else { } else {
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
iters[i] = now() iters[i] = nowNano()
} }
} }
avg := iters[maxIter-1].Sub(iters[0]) / time.Duration(maxIter) const seconds = 1_000_000_000 // nanoseconds
return int8(math.Log2(avg.Seconds())) avg := (iters[maxIter-1] - iters[0]) / int64(maxIter)
avgSeconds := float64(avg) / seconds
return int8(math.Log2(avgSeconds))
} }
+7 -8
View File
@@ -132,7 +132,7 @@ func (s *StackAsync) MTU() int {
} }
func (s *StackAsync) Reset(cfg StackConfig) error { func (s *StackAsync) Reset(cfg StackConfig) error {
if cfg.RandSeed == 0 { if cfg.RandSeed == 0 || cfg.Hostname == "" {
return lneto.ErrInvalidConfig return lneto.ErrInvalidConfig
} }
mac := cfg.HardwareAddress mac := cfg.HardwareAddress
@@ -203,14 +203,15 @@ func (s *StackAsync) Reset(cfg StackConfig) error {
err = s.icmp.Configure(icmpv4.ClientConfig{ err = s.icmp.Configure(icmpv4.ClientConfig{
ResponseQueueBuffer: make([]byte, cfg.ICMPQueueLimit*64), ResponseQueueBuffer: make([]byte, cfg.ICMPQueueLimit*64),
ResponseQueueLimit: cfg.ICMPQueueLimit, ResponseQueueLimit: cfg.ICMPQueueLimit,
HashSeed: s.Prand32(), HashSeed: s.prand32(),
ID: uint16(cfg.Hostname[len(cfg.Hostname)-1]) - '0', // Treat last character of hostname as number.
}) })
if err != nil { if err != nil {
return err return err
} }
} }
var timebuf [32]time.Time var timebuf [4]int64
s.sysprec = ntp.CalculateSystemPrecision(time.Now, timebuf[:]) s.sysprec = ntp.CalculateSystemPrecision(nil, timebuf[:])
if s.clientID == "" { if s.clientID == "" {
s.clientID = "lneto-" + s.hostname s.clientID = "lneto-" + s.hostname
} }
@@ -335,11 +336,9 @@ func (s *StackAsync) Gateway6() [6]byte {
func (s *StackAsync) EnableICMP(enabled bool) (err error) { func (s *StackAsync) EnableICMP(enabled bool) (err error) {
if enabled { if enabled {
if s.ip.IsRegistered(lneto.IPProtoICMP) { if s.ip.IsRegistered(lneto.IPProtoICMP) {
err = lneto.ErrAlreadyRegistered return nil
} else {
err = s.ip.Register(&s.icmp)
} }
err = s.ip.Register(&s.icmp)
} else { } else {
s.icmp.Abort() s.icmp.Abort()
} }
+123
View File
@@ -0,0 +1,123 @@
package xnet
import (
"net/netip"
"testing"
)
func TestStackAsync_ICMPEcho(t *testing.T) {
tests := []struct {
name string
pattern []byte
size uint16
}{
{"1 EchoRequestReply", []byte("icmp-test"), 56},
{"2 ReplyToManualRequest", []byte("manual-echo"), 32},
}
sender, receiver := newICMPStacks(t, 42, 1500) // seed 42 is arbitrary, stacks get unique IPs
if err := sender.EnableICMP(true); err != nil {
t.Fatal(err)
}
if err := receiver.EnableICMP(true); err != nil {
t.Fatal(err)
}
buf := make([]byte, 2048)
n, err := sender.EgressEthernet(buf)
if n > 0 || err != nil {
t.Fatal("sender unexpected data sent or error", n, err)
}
n, err = receiver.EgressEthernet(buf)
if n > 0 || err != nil {
t.Fatal("receiver unexpected data sent or error", n, err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sender.SetGateway6(receiver.HardwareAddress())
receiver.SetGateway6(sender.HardwareAddress())
key, err := sender.icmp.PingStart(receiver.Addr().As4(), tt.pattern, tt.size)
if err != nil {
t.Fatal(err)
}
echoSent := exchangeEthernetOnce(t, sender, receiver, buf)
if !echoSent {
t.Error("ECHO not sent")
}
echoReplySent := exchangeEthernetOnce(t, receiver, sender, buf)
if !echoReplySent {
t.Error("ECHOREPLY not sent")
}
n, err = sender.EgressEthernet(buf)
if n > 0 || err != nil {
t.Error("sender excess data sent or error", n, err)
}
n, err = receiver.EgressEthernet(buf)
if n > 0 || err != nil {
t.Error("receiver excess data sent or error", n, err)
}
completed, ok := sender.icmp.PingPop(key)
if !ok {
t.Fatal("ping key not found")
}
if !completed {
t.Fatal("expected ping to complete")
}
})
}
}
// exchangeEthernetOnce sends one Ethernet frame from src to dst if available.
func exchangeEthernetOnce(t *testing.T, src, dst *StackAsync, buf []byte) bool {
t.Helper()
n, err := src.EgressEthernet(buf)
if err != nil {
t.Fatal(err)
}
if n == 0 {
return false
}
if err := dst.IngressEthernet(buf[:n]); err != nil {
t.Fatal(err)
}
return true
}
// newICMPStacks creates two test stacks with distinct static addresses and hardware addresses.
func newICMPStacks(t testing.TB, randSeed int64, mtu int) (*StackAsync, *StackAsync) {
const icmpQueue = 4
s1, s2 := new(StackAsync), new(StackAsync)
// Use the seed to generate two adjacent IPs (10.0.0.x) and MACs.
base := byte(randSeed & 0x7F) // keep in safe range 0..127
addr1 := netip.AddrFrom4([4]byte{10, 0, 0, base})
addr2 := netip.AddrFrom4([4]byte{10, 0, 0, base + 1})
mac1 := [6]byte{0xbe, 0xef, 0, 0, 0, base}
mac2 := [6]byte{0xbe, 0xef, 0, 0, 0, base + 1}
if err := s1.Reset(StackConfig{
Hostname: "icmp-stack-1",
RandSeed: randSeed,
StaticAddress: addr1,
HardwareAddress: mac1,
MTU: uint16(mtu),
ICMPQueueLimit: icmpQueue,
}); err != nil {
t.Fatal(err)
}
if err := s2.Reset(StackConfig{
Hostname: "icmp-stack-2",
RandSeed: ^randSeed,
StaticAddress: addr2,
HardwareAddress: mac2,
MTU: uint16(mtu),
ICMPQueueLimit: icmpQueue,
}); err != nil {
t.Fatal(err)
}
return s1, s2
}
+30 -10
View File
@@ -11,9 +11,11 @@ import (
"github.com/soypat/lneto/arp" "github.com/soypat/lneto/arp"
"github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/internal/ltesto" "github.com/soypat/lneto/internal/ltesto"
"github.com/soypat/lneto/internet/pcap" "github.com/soypat/lneto/internet/pcap"
"github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv4/icmpv4"
"github.com/soypat/lneto/tcp" "github.com/soypat/lneto/tcp"
) )
@@ -913,11 +915,12 @@ func TestTCPConn_BufferNotClearedOnPassiveClose(t *testing.T) {
func TestStackAsync_ICMPEchoChecksum(t *testing.T) { func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
const MTU = 1500 const MTU = 1500
const MaxFrameLength = MTU + 14 + 4 // Ethernet header+FCS.
stackAddr := netip.AddrFrom4([4]byte{192, 168, 1, 99}) stackAddr := netip.AddrFrom4([4]byte{192, 168, 1, 99})
stackMAC := [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff} stackMAC := [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
routerAddr := [4]byte{192, 168, 1, 1} routerAddr := [4]byte{192, 168, 1, 1}
routerMAC := [6]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55} routerMAC := [6]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}
var rawbuf [MaxFrameLength]byte
stack := new(StackAsync) stack := new(StackAsync)
err := stack.Reset(StackConfig{ err := stack.Reset(StackConfig{
Hostname: "ICMPTest", Hostname: "ICMPTest",
@@ -925,11 +928,15 @@ func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
StaticAddress: stackAddr, StaticAddress: stackAddr,
HardwareAddress: stackMAC, HardwareAddress: stackMAC,
MTU: MTU, MTU: MTU,
ICMPQueueLimit: 2,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = stack.EnableICMP(true)
if err != nil {
t.Error("enabling ICMP:", err)
}
gen := ltesto.PacketGen{ gen := ltesto.PacketGen{
SrcMAC: routerMAC, SrcMAC: routerMAC,
DstMAC: stackMAC, DstMAC: stackMAC,
@@ -937,22 +944,35 @@ func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
DstIPv4: stackAddr.As4(), DstIPv4: stackAddr.As4(),
} }
icmpPayload := []byte("abcdefghijklmnopqrstuvwxyz012345") // 32 bytes, typical ping payload. icmpPayload := []byte("abcdefghijklmnopqrstuvwxyz012345") // 32 bytes, typical ping payload.
const (
id = 0x1234
seq = 1
)
// Test 1: Valid ICMP echo request should be accepted. // Test 1: Valid ICMP echo request should be accepted.
pkt := gen.AppendIPv4ICMPEcho(nil, ltesto.ICMPEchoConfig{ pkt := gen.AppendIPv4ICMPEcho(rawbuf[:0], ltesto.ICMPEchoConfig{
Identifier: 0x1234, Identifier: id,
SequenceNumber: 1, SequenceNumber: seq,
Payload: icmpPayload, Payload: icmpPayload,
}) })
err = stack.IngressEthernet(pkt) err = stack.IngressEthernet(pkt)
if err != nil { if err != nil {
t.Fatalf("valid ICMP echo rejected: %v", err) t.Fatalf("valid ICMP echo rejected: %v", err)
} }
n, err := stack.EgressEthernet(rawbuf[:])
if err != nil || n == 0 {
t.Error("expected ICMP response:", n, err)
}
ifrm, err := icmpv4.NewFrame(rawbuf[14+20 : n])
efrm := icmpv4.FrameEcho{Frame: ifrm}
if err != nil {
t.Fatal(err)
} else if efrm.Identifier() != id || efrm.SequenceNumber() != seq || !internal.BytesEqual(icmpPayload, efrm.Data()) {
t.Errorf("id want %d, got %d; seq want %d, got %d, payload want %q, got %q", id, efrm.Identifier(), seq, efrm.SequenceNumber(), icmpPayload, efrm.Data())
}
// Test 2: Valid ICMP with trailing FCS bytes (simulates real PIO hardware capture). // Test 2: Valid ICMP with trailing FCS bytes (simulates real PIO hardware capture).
// This is a regression test for the bug where recvicmp checksummed ifrm.RawData() // This is a regression test for the bug where recvicmp checksummed ifrm.RawData()
// instead of ifrm.Payload(), causing the 4 trailing FCS bytes to corrupt the checksum. // instead of ifrm.Payload(), causing the 4 trailing FCS bytes to corrupt the checksum.
pkt = gen.AppendIPv4ICMPEcho(nil, ltesto.ICMPEchoConfig{ pkt = gen.AppendIPv4ICMPEcho(rawbuf[:0], ltesto.ICMPEchoConfig{
Identifier: 0x1234, Identifier: 0x1234,
SequenceNumber: 2, SequenceNumber: 2,
Payload: icmpPayload, Payload: icmpPayload,
@@ -964,7 +984,7 @@ func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
} }
// Test 3: Corrupted ICMP checksum should be rejected. // Test 3: Corrupted ICMP checksum should be rejected.
pkt = gen.AppendIPv4ICMPEcho(nil, ltesto.ICMPEchoConfig{ pkt = gen.AppendIPv4ICMPEcho(rawbuf[:0], ltesto.ICMPEchoConfig{
Identifier: 0x1234, Identifier: 0x1234,
SequenceNumber: 3, SequenceNumber: 3,
Payload: icmpPayload, Payload: icmpPayload,
@@ -976,7 +996,7 @@ func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
} }
// Test 4: Corrupted ICMP with trailing FCS should also be rejected. // Test 4: Corrupted ICMP with trailing FCS should also be rejected.
pkt = gen.AppendIPv4ICMPEcho(nil, ltesto.ICMPEchoConfig{ pkt = gen.AppendIPv4ICMPEcho(rawbuf[:0], ltesto.ICMPEchoConfig{
Identifier: 0x1234, Identifier: 0x1234,
SequenceNumber: 4, SequenceNumber: 4,
Payload: icmpPayload, Payload: icmpPayload,