Add ICMPv4 and lneto.StackNode and deprecate internet.StackNode (#65)

* begin adding icmp client

* rely on anon structs

* add tests

* tests passing

* icmp fleshed out

* rework icmp to include ip addr

* rename StackAsync.Demux/Encapsulate to RecvEthernet and SendEthernet

* remove legacy unreachable TCP tests

* remove uses of deprecated internet.StackNode in preference of lneto.StackNode

* documentation

* rename methods to signal no I/O happening
This commit is contained in:
Pat Whittingslow
2026-04-09 09:07:16 -03:00
committed by GitHub
parent 22c7e6c9d8
commit ec44889896
26 changed files with 679 additions and 209 deletions
+264
View File
@@ -0,0 +1,264 @@
package icmpv4
import (
"slices"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
)
var _ lneto.StackNode = (*Client)(nil) // Compile-time guarantee of interface implementation.
const (
keyHashCompletedBit = 1 << 31
keyHashSentBit = 1 << 30
keyHashBits = (1 << 30) - 1
)
type Client struct {
connid uint64
magic uint32
_seq uint16
id uint16
outgoingEcho []struct {
// For every ping we send out stores hashes of the data (should include IP likely).
pattern []byte
key uint32
size uint16
raddr [4]byte
}
// responseLengths stores the length of responses received.
// together they should add up to the written length of responseRing.
incomingEcho []struct {
length uint16
id uint16
seq uint16
raddr [4]byte
}
responseRing internal.Ring
}
type ClientConfig struct {
ResponseQueueBuffer []byte
ResponseQueueLimit int
HashSeed uint32
// ID is used for Echo (ping) ID field setting.
ID uint16
}
func (client *Client) Configure(cfg ClientConfig) error {
if cfg.HashSeed == 0 || len(cfg.ResponseQueueBuffer) < 16 || cfg.ResponseQueueLimit <= 0 {
return lneto.ErrInvalidConfig
}
client.connid++
internal.SliceReuse(&client.outgoingEcho, cfg.ResponseQueueLimit)
client.responseRing = internal.Ring{Buf: cfg.ResponseQueueBuffer}
client.magic = cfg.HashSeed
client.id = cfg.ID
return nil
}
func (client *Client) Protocol() uint64 { return uint64(lneto.IPProtoICMP) }
func (client *Client) LocalPort() uint16 { return 0 }
func (client *Client) ConnectionID() *uint64 { return &client.connid }
func (client *Client) Abort() {
client.Reset()
client.connid++
}
func (client *Client) Reset() {
client.incomingEcho = client.incomingEcho[:0]
client.outgoingEcho = client.outgoingEcho[:0]
client.responseRing.Reset()
}
func (client *Client) Demux(carrierData []byte, frameOffset int) error {
rawdata := carrierData[frameOffset:]
ifrm, err := NewFrame(rawdata)
if err != nil {
return err
}
tp := ifrm.Type()
if tp != TypeEcho && tp != TypeEchoReply {
return lneto.ErrPacketDrop
}
var crc lneto.CRC791
if crc.PayloadSum16(rawdata) != 0 {
return lneto.ErrBadCRC
}
var raddr [4]byte
ipEnabled := frameOffset >= 20
if ipEnabled {
src, _, _, _, _ := internal.GetIPAddr(carrierData)
if len(src) == 4 {
raddr = [4]byte(src)
}
}
switch tp {
case TypeEcho:
// We received a ping request; not handled client-side.
efrm := FrameEcho{Frame: ifrm}
data := efrm.Data()
n, werr := client.responseRing.Write(data)
if werr != nil {
err = werr
break
}
v := internal.SliceReclaim(&client.incomingEcho)
v.length = uint16(n)
v.id = efrm.Identifier()
v.seq = efrm.SequenceNumber()
v.raddr = raddr
case TypeEchoReply:
efrm := FrameEcho{Frame: ifrm}
data := efrm.Data()
hash := client.magichash(data, len(data)) & keyHashBits
idx := client.pingidx(hash)
if idx < 0 || (ipEnabled && client.outgoingEcho[idx].raddr != raddr) {
err = lneto.ErrPacketDrop
break
}
client.outgoingEcho[idx].key |= keyHashCompletedBit
default:
err = lneto.ErrPacketDrop
}
return err
}
func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int) (int, error) {
ifrm, err := NewFrame(carrierData[frameOffset:])
if err != nil {
return 0, err
}
// Put n bytes of ICMP data.
var n int
var raddr [4]byte
if len(client.incomingEcho) > 0 {
// Priority: send echo reply.1
inc := client.incomingEcho[0]
efrm := FrameEcho{Frame: ifrm}
efrm.SetType(TypeEchoReply)
efrm.SetIdentifier(inc.id)
efrm.SetSequenceNumber(inc.seq)
dataLen := int(inc.length)
_, rerr := client.responseRing.Read(efrm.Data()[:dataLen])
if rerr != nil {
return 0, rerr
}
client.incomingEcho = slices.Delete(client.incomingEcho, 0, 1)
n = sizeHeader + dataLen
raddr = inc.raddr
} else if len(client.outgoingEcho) > 0 {
idx := 0
for idx < len(client.outgoingEcho) {
out := &client.outgoingEcho[idx]
if out.key&keyHashSentBit == 0 {
break
}
idx++
}
if idx >= len(client.outgoingEcho) {
return 0, nil // No pending to send packet.
}
out := &client.outgoingEcho[idx]
efrm := FrameEcho{Frame: ifrm}
efrm.SetType(TypeEcho)
efrm.SetIdentifier(client.id)
efrm.SetSequenceNumber(client.seq())
pattern := out.pattern
data := efrm.Data()
size := int(out.size)
written := 0
for written+len(pattern) <= size && written+len(pattern) <= len(data) {
copy(data[written:], pattern)
written += len(pattern)
}
copy(data[written:written+size%len(pattern)], pattern)
n = sizeHeader + size
raddr = out.raddr
} else {
return 0, nil
}
ifrm.buf = carrierData[frameOffset : frameOffset+n] // Raw buffer set.
ifrm.SetCode(0)
ifrm.SetCRC(0)
var crc lneto.CRC791
sum := crc.PayloadSum16(carrierData[frameOffset : frameOffset+n])
ifrm.SetCRC(sum)
if frameOffset >= 20 {
err = internal.SetIPAddrs(carrierData, 0, nil, raddr[:])
}
return n, err
}
func (client *Client) seq() uint16 {
client._seq++
return client._seq
}
func (client *Client) magichash(pattern []byte, size int) (hash uint32) {
hash = client.magic
i := 0
n := size / len(pattern)
for i < n {
for _, b := range pattern {
hash = hash*31 + uint32(b)
}
i++
}
n = size % len(pattern)
for i = 0; i < n; i++ {
hash = hash*31 + uint32(pattern[i])
}
return hash
}
func (client *Client) PingStart(remoteAddr [4]byte, pattern []byte, size uint16) (key uint32, err error) {
if int(size) < len(pattern) {
return 0, lneto.ErrInvalidConfig
} else if remoteAddr == [4]byte{} {
return 0, lneto.ErrZeroDestination
}
key = client.magichash(pattern, int(size)) & keyHashBits
v := internal.SliceReclaim(&client.outgoingEcho)
v.key = key
v.size = size
v.pattern = append(v.pattern[:0], pattern...)
v.raddr = remoteAddr
return key, nil
}
func (client *Client) pingidx(key uint32) int {
for i := range client.outgoingEcho {
if client.outgoingEcho[i].key&keyHashBits == key {
return i
}
}
return -1
}
func (client *Client) PingPeek(key uint32) (completed, ok bool) {
idx := client.pingidx(key)
if idx >= 0 {
return client.outgoingEcho[idx].key&keyHashCompletedBit != 0, true
}
return false, false
}
func (client *Client) PingPop(key uint32) (completed, ok bool) {
idx := client.pingidx(key)
if idx >= 0 {
completed := client.outgoingEcho[idx].key&keyHashCompletedBit != 0
client.outgoingEcho = slices.Delete(client.outgoingEcho, idx, idx+1)
return completed, true
}
return false, false
}
+139
View File
@@ -0,0 +1,139 @@
package icmpv4
import (
"testing"
"github.com/soypat/lneto/internal"
)
const (
testHashSeed = 0xdeadbeef
)
func TestClients(t *testing.T) {
const sizebuffer = 64
const queuesize = 2
var sender, responder Client
err := sender.Configure(ClientConfig{
ResponseQueueBuffer: make([]byte, sizebuffer),
ResponseQueueLimit: queuesize,
HashSeed: testHashSeed,
})
if err != nil {
t.Fatal(err)
}
err = responder.Configure(ClientConfig{
ResponseQueueBuffer: make([]byte, sizebuffer),
ResponseQueueLimit: queuesize,
HashSeed: testHashSeed,
})
if err != nil {
t.Fatal(err)
}
pattern := []byte("ab12")
size := 8
var buf [64]byte
key1 := testSingleExchange(t, &sender, &responder, buf[:], pattern, uint16(size))
completed, ok := sender.PingPop(key1)
if !completed || !ok {
t.Fatal("ping did not complete or not exist")
}
}
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 ipOff = 0
completed, ok := sender.PingPeek(senderKey)
if !ok {
t.Error("ping key not exist")
} else if completed {
t.Error("ping completed before response")
}
ifrm, _ := NewFrame(buf[frameOff : frameOff+n])
efrm := FrameEcho{Frame: ifrm}
id, seq := efrm.Identifier(), efrm.SequenceNumber()
err1 := responder.Demux(buf[:frameOff+n], frameOff)
if err1 != nil {
t.Error("responder demux during single", err1)
}
n, err := responder.Encapsulate(buf, ipOff, frameOff)
if err != nil {
t.Error("responder encaps during single", err)
return
} else if n == 0 && err1 == nil {
t.Error("responder wrote no data")
return
}
ifrm, err = NewFrame(buf[frameOff : frameOff+n])
if err != nil {
t.Fatal(err)
}
if ifrm.Type() != TypeEchoReply {
t.Fatalf("expected echo reply %d", ifrm.Type())
}
efrm = FrameEcho{Frame: ifrm}
if efrm.Identifier() != id {
t.Error("mismatched identifier want/got:", id, efrm.Identifier())
}
if efrm.SequenceNumber() != seq {
t.Error("mismatched sequence number want/got:", seq, efrm.SequenceNumber())
}
data := efrm.Data()
testPatternMatch(t, data, pattern, int(size))
err = sender.Demux(buf[:frameOff+n], frameOff)
if err != nil {
t.Error("sender demuxed response", err)
}
completed, ok = sender.PingPeek(senderKey)
if !completed {
t.Error("expected ping to have completed")
}
if !ok {
t.Error("ping key not exist after completion")
}
if completed2, ok2 := sender.PingPeek(senderKey); completed != completed2 || ok != ok2 {
t.Error("change in status after peek")
}
return senderKey
}
func testSendEcho(t *testing.T, sender *Client, buf []byte, pattern []byte, size uint16) (key uint32, n int) {
t.Helper()
key, err := sender.PingStart([4]byte{1}, pattern, size)
if err != nil {
t.Fatal(err)
}
const frameOff = 0
const ipOff = 0
n, err = sender.Encapsulate(buf[:], ipOff, frameOff)
if err != nil {
t.Errorf("sender encapsulate: %v", err)
}
ifrm, err := NewFrame(buf[:n])
if err != nil {
t.Fatal(err) // only fails in short frame case.
}
if ifrm.Type() != TypeEcho {
t.Errorf("not echo type on send: %d", ifrm.Type())
}
efrm := FrameEcho{Frame: ifrm}
data := efrm.Data()
testPatternMatch(t, data, pattern, int(size))
return key, n
}
func testPatternMatch(t *testing.T, data []byte, pattern []byte, size int) {
t.Helper()
if len(data) != size {
t.Errorf("pattern size mismatch, want %d, got %d", size, len(data))
}
for i := 0; i < size; i += len(pattern) {
got := data[i:min(len(data), i+len(pattern))]
want := pattern[:len(got)]
if !internal.BytesEqual(got, want) {
t.Errorf("pattern data mismatch at %d, got %s, want %s", i, got, want)
}
}
}
+5 -1
View File
@@ -6,6 +6,10 @@ import (
"github.com/soypat/lneto"
)
const (
sizeHeader = 8
)
type Type uint8
const (
@@ -54,7 +58,7 @@ const (
)
func NewFrame(buf []byte) (Frame, error) {
if len(buf) < 8 {
if len(buf) < sizeHeader {
return Frame{}, lneto.ErrShortBuffer
}
return Frame{buf: buf}, nil