Lay out device/stack interfaces - Netdev/Netlink (#92)

* good code today

* add netdev.Runner

* round off APIs more

* better interface method documentation

* improve DHCP netstack API

* add pico w netdev example

* remove temp file

* keep thinking about this. this is hard :/

* fix ci

* small fixer

* working dhcp

* fix rebase API mismatches

* begin adding espradio example

* keep working on espradio, icmp not working

* fix icmp by fixing dhcp
This commit is contained in:
Pat Whittingslow
2026-06-17 14:18:08 -03:00
committed by GitHub
parent 5f8ca45859
commit 813b7b5e57
16 changed files with 925 additions and 0 deletions
+2
View File
@@ -9,6 +9,8 @@ profiles/
# Dependency directories after running `go mod vendor`
vendor/
# Wifi credentials
*.credentials
# Binaries for programs and plugins
*.elf
+1
View File
@@ -28,6 +28,7 @@ const (
// based on one or two encountered use cases, example use case included.
/*
- ErrUnregistered/ErrAborted // connection unregistered. i.e: ICMP client aborted during active ping, ping process returns this.
- ErrInvalidArgs // invalid func arguments i.e: different from Config since this refers to non-config arguments. usually nil values.
*/
)
@@ -0,0 +1,14 @@
module espradio-netdev
go 1.25.7
// These replace directives point to local checkouts during development.
// Remove them when using as a standalone program with published releases.
replace github.com/soypat/lneto => ../../../.
replace tinygo.org/x/espradio => ../../../../espradio
require (
github.com/soypat/lneto v0.1.1-0.20260425023453-aa77403a2b32
tinygo.org/x/espradio v0.1.0
)
@@ -0,0 +1,213 @@
// Example showing how to use the ESP32 WiFi radio through lneto's netdev
// package. This mirrors the picow-netdev example and demonstrates the
// standardised DevEthernet+Netlink interface that works across hardware targets.
//
// Build and flash:
//
// tinygo flash -target xiao-esp32c3 \
// -ldflags="-X main.ssid=YourSSID -X main.password=YourPassword" \
// -monitor ./examples/esp32-netdev
package main
import (
"context"
_ "embed"
"net/netip"
"strings"
"time"
"github.com/soypat/lneto"
"github.com/soypat/lneto/x/netdev"
"github.com/soypat/lneto/x/xnet"
"tinygo.org/x/espradio"
)
var (
//go:embed wifi.credentials
credentials string
// remove windows CRLF "\r\n" and trailing newline.
credentialsNormalized = strings.TrimSuffix(strings.ReplaceAll(credentials, "\r\n", "\n"), "\n")
globConnectParams espradio.STAConfig
poolCfg = xnet.TCPPoolConfig{
PoolSize: 4,
QueueSize: 4,
TxBufSize: 2048,
RxBufSize: 512,
NewBackoff: func() lneto.BackoffStrategy { return backoff },
NanoTime: func() int64 { return time.Now().UnixNano() },
EstablishedTimeout: 5 * time.Second,
ClosingTimeout: 3 * time.Second,
}
)
func main() {
time.Sleep(time.Second)
ssid, password, ok := strings.Cut(credentialsNormalized, "\n")
if !ok {
fail("must write newline separated ssid/password in wifi.credentials", nil)
}
globConnectParams.SSID = ssid
globConnectParams.Password = password
var dev EspDev
dev.radioConfig = espradio.Config{Logging: espradio.LogLevelError}
// LinkConnect runs Enable+Start+Connect+StartNetDev. It must complete
// before HardwareAddr6 is called because the MAC is only readable after
// Enable() initialises the WiFi hardware.
err := dev.LinkConnect(globConnectParams)
failIfErr("wifi connect", err)
hw, err := dev.HardwareAddr6()
failIfErr("hardware addr", err)
var stack xnet.Netstack
err = stack.Reset(xnet.StackConfig{
RandSeed: time.Now().UnixNano() | 1,
Hostname: "esp32-lneto",
MaxActiveTCPPorts: 4,
MaxActiveUDPPorts: 4,
ICMPQueueLimit: 1,
MTU: 1500,
PassivePeers: 4,
HardwareAddress: hw,
}, backoff, poolCfg)
failIfErr("stack reset", err)
err = stack.EnableICMP(true)
failIfErr("icmp enable", err)
dev.LinkNotify(userNotify)
var iface netdev.Interface[espradio.STAConfig]
err = iface.Init(&dev, &dev, netdev.InterfaceConfig{})
failIfErr("init iface", err)
var runner netdev.Runner[espradio.STAConfig]
go func() {
if err := runner.Run(context.Background(), iface, &stack, backoff); err != nil {
failIfErr("runner", err)
}
}()
assigned, gatewayRt, subnetBits, err := stack.EnableDHCP(context.Background(), true, netip.Addr{})
failIfErr("enable dhcp", err)
println("assigned=", assigned.String(), "gateway=", gatewayRt.String(), "subnet=", subnetBits)
select {}
}
// compile-time interface checks.
var _ lneto.BackoffStrategy = backoff
var _ netdev.Stack = (*xnet.Netstack)(nil)
var _ netdev.DevEthernet = (*EspDev)(nil)
var _ netdev.Netlink[espradio.STAConfig] = (*EspDev)(nil)
func backoff(consecutiveBackoffs uint) time.Duration {
return 5 * time.Millisecond
}
func userNotify(connected bool) (retries int, reconnectParams espradio.STAConfig) {
if !connected {
return 1, globConnectParams
}
return 0, espradio.STAConfig{}
}
// EspDev adapts [espradio.NetDev] to [netdev.DevEthernet] and wraps the
// ESP32 WiFi bring-up sequence (Enable/Start/Connect/StartNetDev) as [netdev.Netlink].
//
// The same struct implements both interfaces so a single pointer can be passed
// to [netdev.Interface.Init] for both the netlink and device arguments, matching
// the pattern used by the picow-netdev example.
type EspDev struct {
nd *espradio.NetDev
radioConfig espradio.Config
notifyCb netdev.NotifyCallback[espradio.STAConfig]
}
// LinkConnect implements [netdev.Netlink].
// Runs Enable→Start→Connect→StartNetDev. nd is nil until this returns successfully.
func (d *EspDev) LinkConnect(cfg espradio.STAConfig) error {
if err := espradio.Enable(d.radioConfig); err != nil {
return err
}
if err := espradio.Start(); err != nil {
return err
}
if err := espradio.Connect(cfg); err != nil {
return err
}
nd, err := espradio.StartNetDev()
if err != nil {
return err
}
d.nd = nd
return nil
}
// LinkDisconnect implements [netdev.Netlink].
func (d *EspDev) LinkDisconnect() {}
// LinkNotify implements [netdev.Netlink].
func (d *EspDev) LinkNotify(cb netdev.NotifyCallback[espradio.STAConfig]) {
d.notifyCb = cb
}
// HardwareAddr6 implements [netdev.DevEthernet].
func (d *EspDev) HardwareAddr6() ([6]byte, error) {
return d.nd.HardwareAddr6()
}
// SendOffsetEthFrame implements [netdev.DevEthernet].
// ESP32 has no frame prefix offset, so buf is passed directly to [espradio.NetDev.SendEthFrame].
func (d *EspDev) SendOffsetEthFrame(buf []byte) error {
return d.nd.SendEthFrame(buf)
}
// SetEthRecvHandler implements [netdev.DevEthernet].
// Adapts the error-less netdev handler signature to espradio's error-returning one.
func (d *EspDev) SetEthRecvHandler(handler func(rxEthframe []byte)) {
if handler == nil {
d.nd.SetEthRecvHandler(nil)
return
}
d.nd.SetEthRecvHandler(func(pkt []byte) error {
handler(pkt)
return nil
})
}
// EthPoll implements [netdev.DevEthernet].
//
// espradio.NetDev.EthPoll both pops a frame from the C ring buffer into buf
// AND synchronously calls the registered rxHandler with that frame. Returning
// the non-zero byte count here as well would trigger the Runner's "device uses
// both paths" guard. We therefore drain the ring (calling the handler) and
// return (0, 0, err) so the Runner sees only the handler-based receive path.
func (d *EspDev) EthPoll(buf []byte) (ethFrameOff, ethernetBytes int, err error) {
_, err = d.nd.EthPoll(buf)
return 0, 0, err
}
// MaxFrameSizeAndOffset implements [netdev.DevEthernet].
// ESP32 transmits frames with no prefix offset.
func (d *EspDev) MaxFrameSizeAndOffset() (maxFrameSize int, frameOff int) {
return d.nd.MaxFrameSize(), 0
}
func failIfErr(msg string, err error) {
if err != nil {
fail(msg, err)
}
println(msg, "PASS")
}
func fail(msg string, err error) {
var errstr string
if err != nil {
errstr = err.Error()
}
for {
println("FAIL:", msg, errstr)
time.Sleep(time.Second)
}
}
@@ -0,0 +1,16 @@
module piconetdev
go 1.25.7
require github.com/soypat/cyw43439 v0.1.1
require (
github.com/soypat/lneto v0.1.0 // indirect
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect
github.com/tinygo-org/pio v0.2.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
)
// This is an example taken grom github.com/soypat/lneto
// Remove this replace directive when using as own program.
replace github.com/soypat/lneto => ../../../.
@@ -0,0 +1,10 @@
github.com/soypat/cyw43439 v0.1.1 h1:vcaTiVzfuz3keK7lJpVxStZ6tV8HCw7Ugzsh1k4mneE=
github.com/soypat/cyw43439 v0.1.1/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc=
github.com/soypat/lneto v0.1.0 h1:VAHCJ33hvC3wDqhM0Vm7w0k6vwNsOCAsQ8XTrXJpS7I=
github.com/soypat/lneto v0.1.0/go.mod h1:g/8Lk+hIsMZydyWDJjK2YfsCuG6jA5mWCO6U+4S7w1U=
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA=
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/tinygo-org/pio v0.2.0 h1:vo3xa6xDZ2rVtxrks/KcTZHF3qq4lyWOntvEvl2pOhU=
github.com/tinygo-org/pio v0.2.0/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
@@ -0,0 +1,177 @@
package main
import (
"context"
_ "embed"
"net"
"net/netip"
"strings"
"time"
"github.com/soypat/cyw43439"
"github.com/soypat/lneto"
"github.com/soypat/lneto/x/netdev"
"github.com/soypat/lneto/x/xnet"
)
var (
//go:embed wifi.credentials
credentials string
// remove windows CRLF "\r\n" and trailing newline.
credentialsNormalized = strings.TrimSuffix(strings.ReplaceAll(credentials, "\r\n", "\n"), "\n")
globConnectParams ConnectParams
poolCfg = xnet.TCPPoolConfig{
PoolSize: 5,
QueueSize: 5,
TxBufSize: 2048,
RxBufSize: 2048,
NewBackoff: func() lneto.BackoffStrategy { return backoff },
NanoTime: func() int64 { return time.Now().UnixNano() },
EstablishedTimeout: 5 * time.Second,
ClosingTimeout: 3 * time.Second,
}
)
func main() {
time.Sleep(time.Second)
ssid, password, ok := strings.Cut(credentialsNormalized, "\n")
if !ok {
fail("must write newline separated ssid/password in wifi.credentials", nil)
}
globConnectParams.SSID = ssid
globConnectParams.Passphrase = password
dev := Netdev{
dev: cyw43439.NewPicoWDevice(),
}
err := dev.dev.Init(cyw43439.DefaultWifiConfig())
failIfErr("init cyw43439", err)
hw, _ := dev.HardwareAddr6()
var stack xnet.Netstack
err = stack.Reset(xnet.StackConfig{
RandSeed: time.Now().UnixNano() | 1,
Hostname: "lneto-pico",
MaxActiveTCPPorts: 4,
MaxActiveUDPPorts: 4,
ICMPQueueLimit: 1,
MTU: 1500,
HardwareAddress: hw,
}, backoff, poolCfg)
failIfErr("stack reset", err)
err = stack.EnableICMP(true)
failIfErr("icmp enable", err)
err = dev.LinkConnect(globConnectParams)
failIfErr("wifi join", err)
var iface netdev.Interface[ConnectParams]
var runner netdev.Runner[ConnectParams]
dev.LinkNotify(userNotify)
err = iface.Init(&dev, &dev, netdev.InterfaceConfig{})
failIfErr("init iface", err)
go func() {
if err := runner.Run(context.Background(), iface, &stack, backoff); err != nil {
failIfErr("runner", err)
}
}()
assigned, gatewayRt, subnetBits, err := stack.EnableDHCP(context.Background(), true, netip.Addr{})
failIfErr("enable dhcp", err)
println("assigned=", assigned.String(), "gateway=", gatewayRt.String(), "subnet", subnetBits)
select {}
}
// compile-time guarantee of interface implementation.
var _ lneto.BackoffStrategy = backoff
var _ netdev.Stack = (*xnet.Netstack)(nil)
func backoff(consecutiveBackoffs uint) (sleepOrFlag time.Duration) {
return 5 * time.Millisecond
}
func userNotify(connected bool) (retries int, connectParams ConnectParams) {
if !connected {
return 1, globConnectParams
}
return 0, ConnectParams{}
}
var _ netdev.DevEthernet = (*Netdev)(nil)
var _ netdev.Netlink[ConnectParams] = (*Netdev)(nil)
type Netdev struct {
dev *cyw43439.Device
notifyCb netdev.NotifyCallback[ConnectParams]
}
type ConnectParams struct {
SSID string
cyw43439.JoinOptions
}
func (nl *Netdev) Netflags() (flags net.Flags) {
flags |= net.FlagUp
if nl.dev.IsLinkUp() {
flags |= net.FlagRunning
}
return flags
}
// LinkConnect implements [netdev.Netlink].
func (nl *Netdev) LinkConnect(connectParams ConnectParams) error {
return nl.dev.Join(connectParams.SSID, connectParams.JoinOptions)
}
// LinkDisconnect implements [netdev.Netlink].
func (nl *Netdev) LinkDisconnect() {
// Not implemented by cyw43439 package.
}
// LinkNotify implements [netdev.Netlink].
func (nl *Netdev) LinkNotify(cb netdev.NotifyCallback[ConnectParams]) {
nl.notifyCb = cb
}
// HardwareAddr6 implements [netdev.DevEthernet].
func (d *Netdev) HardwareAddr6() ([6]byte, error) {
return d.dev.HardwareAddr6()
}
// SendEthFrameOffset implements [netdev.DevEthernet].
func (d *Netdev) SendOffsetEthFrame(offsetTxEthFrame []byte) error {
return d.dev.SendEth(offsetTxEthFrame)
}
// SetRecvHandler implements [netdev.DevEthernet].
func (d *Netdev) SetEthRecvHandler(handler func(rxEthframe []byte)) {
d.dev.RecvEthHandle(func(pkt []byte) error {
handler(pkt)
return nil
})
}
// EthPoll implements [netdev.DevEthernet].
func (d *Netdev) EthPoll(buf []byte) (ethFrameOff, ethernetBytes int, err error) {
_, err = d.dev.PollOne()
return 0, 0, err
}
// MaxFrameSizeAndOffset implements [netdev.DevEthernet].
func (d *Netdev) MaxFrameSizeAndOffset() (maxFrameSize int, frameOff int) {
return cyw43439.MaxFrameSize, 0
}
func failIfErr(msg string, err error) {
if err != nil {
fail(msg, err)
}
println("PASS", msg)
}
func fail(msg string, err error) {
var errstr string
if err != nil {
errstr = err.Error()
}
for {
println(msg, errstr)
time.Sleep(time.Second)
}
}
+12
View File
@@ -0,0 +1,12 @@
//go:build tinygo
package netdev
import _ "unsafe" // needed for go:linkname usage.
// UseNetdev is the dynamic linker function
// for inserting a networking stack into the
// standard library implementation in the TinyGo compiler.
//
//go:linkname UseNetdev net.useNetdev
func UseNetdev(dev GoNet)
+30
View File
@@ -0,0 +1,30 @@
package netdev
import (
"net/netip"
"time"
)
// GoNet is the networking interface expected by TinyGo compiler/standard library.
// The methods below define a networking stack as expected by the Go standard library
// when using the TinyGo compiler+stdlib.
type GoNet interface {
// GetHostByName returns the IP address of either a hostname or IPv4
// address in standard dot notation
GetHostByName(name string) (netip.Addr, error)
// Addr returns IP address assigned to the interface, either by
// DHCP or statically
Addr() (netip.Addr, error)
// Berkely Sockets-like interface, Go-ified. See man page for socket(2), etc.
Socket(domain int, stype int, protocol int) (int, error)
Bind(sockfd int, ip netip.AddrPort) error
Connect(sockfd int, host string, ip netip.AddrPort) error
Listen(sockfd int, backlog int) error
Accept(sockfd int) (int, netip.AddrPort, error)
Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error)
Recv(sockfd int, buf []byte, flags int, deadline time.Time) (int, error)
Close(sockfd int) error
SetSockOpt(sockfd int, level int, opt int, value any) error
}
+172
View File
@@ -0,0 +1,172 @@
package netdev
import (
"context"
"errors"
"net/netip"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
)
type Interface[C any] struct {
dev DevEthernet
netlink Netlink[C]
ip netip.Prefix
// Below are values calculated from [DevEthernet] return values to avoid recalculation.
frameSize int
frameOff int
mtu int
mac [6]byte
}
// DevEthernet is an L2 capable device HAL. It is an abstraction
// for devices that send actively and are not polled by an external host.
//
// DevEthernet-specific initialization (WiFi join, PHY auto-negotiation,
// firmware loading) must complete BEFORE the device is used as a stack endpoint.
type DevEthernet interface {
// HardwareAddr6 returns the device's 6-byte MAC address.
// For PHY-only devices, returns the MAC provided at configuration.
HardwareAddr6() ([6]byte, error)
// SendEthFrameOffset transmits a complete Ethernet frame at offset given by [DevEthernet.MaxFrameSizeAndOffset].
// The frame includes the Ethernet header but NOT the FCS/CRC
// trailer (device or stack handles CRC as appropriate).
// SendEthFrameOffset blocks until the transmission is queued succesfully
// or finished sending. Should not be called concurrently
// unless user is sure the driver supports it.
SendOffsetEthFrame(offsetTxEthFrame []byte) error
// SetRecvHandler registers the function called when an Ethernet
// frame is received. Buffers needed by the device to operate efficiently
// should be allocated on its side. This function is mutually exclusive with EthPoll:
// use on or the other to receive data.
SetEthRecvHandler(handler func(rxEthframe []byte))
// EthPoll services the device. For poll-based devices (e.g. CYW43439
// over SPI), reads from the bus and invokes the handler for each
// received frame. This method is mutually exclusive with SetEthRecvHandler:
// use one or the other to receive data but not return data via both channels.
EthPoll(buf []byte) (ethFrameOff, ethernetBytes int, err error)
// MaxFrameSizeAndOffset returns the max complete device frame size
// (including headers and any overhead) for buffer allocation.
// The second value returned is the offset at which the ethernet frame
// should be stored when being passed to [DevEthernet.SendOffsetEthFrame].
// Buffers allocated should be maxEthernetFrameSize+frameOff where maxEthernetFrameSize
// is usually 1500 but less or equal to maxFrameSize-frameOff.
// MTU can be calculated doing:
// // mfu-(14+4+4) for:
// // ethernet header+ethernet CRC if present+ethernet VLAN overhead for VLAN support.
// mtu := dev.MaxFrameSizeAndOffset() - ethernet.MaxOverheadSize
MaxFrameSizeAndOffset() (maxFrameSize int, frameOff int)
}
// Stack is an abstraction for a networking stack.
type Stack interface {
// Configure configures this Stack with the argument mac, ip and gateway addresses.
// The Stack must resolve the gateway hardware address if set.
// Configure(mac net.HardwareAddr, ip netip.Prefix, gw netip.Addr) error
// EnableICMP enables responding/sending ICMP echo frames.
EnableICMP(enabled bool) error
// EnableDHCP enables DHCP on the device if enabled=true and performs a DHCP request.
EnableDHCP(ctx context.Context, enabled bool, reqIP netip.Addr) (assigned netip.Addr, routerGW netip.Addr, subnetBits int, _ error)
// Socket is a berkeley socket abstraction. Returns an [net.Listener] or [net.Conn] depending on laddr/raddr combination.
Socket(ctx context.Context, network string, family, sotype int, laddr, raddr netip.AddrPort) (c any, err error)
// EgressPackets instructs Stack to write outgoing packets into bufs and writing the sizes into sizes.
// offset can be used to tell the stack to start writing after an offset for each buffer.
// The size written into sizes includes only Ethernet frame size and so is independent of offset value.
EgressPackets(bufs [][]byte, sizes []int, offset int) error
// IngressPackets is called on incoming packets so the Stack can direct packets to
// their respective node/connection and update internal state.
// offset instructs the Stack to start reading packets at an offset.
IngressPackets(bufs [][]byte, offset int) error
}
// Netlink represents the physical part of a network device which can connect/disconnect.
// One netlink may correspond to many network devices for interconnected systems.
type Netlink[C any] interface {
// LinkConnect attempts to connect the Netlink if it was not already connected.
// It will block until it succeeds/fails and not retry after returning.
LinkConnect(connectParams C) error
// LinkDisconnect disconnects the Netlink immediately.
LinkDisconnect()
// Link notify sets the callback to be executed after connection state
// changes for the Netlink. The callback can signal an immediate reconnect is desired
// by setting reconnectNowRetries to a positive integer. The netlink should then retry connection
// immediately with the given reconnectParams. reconnectParams should not be nil if reconnectNowRetries is positive.
LinkNotify(cb NotifyCallback[C])
}
// NotifyCallback is a convenience type alias that serves mostly as semantic code documentation.
// NotifyCallback is called when a [Netlink] connects or disconnects from a network. The callback returns:
// - reconnectNowRetries: Amount of times to attempt reconnection before giving up.
// - reconnectParams: parameters to use in reconnection.
type NotifyCallback[C any] = func(connected bool) (reconnectNowRetries int, reconnectParams C)
// InterfaceConfig mostly optional configuration.
type InterfaceConfig struct {
// NetworkIP sets the network's IP range and this interface's IP address. See [netip.Prefix].
// This field is optional if not using a networking stack.
NetworkIP netip.Prefix
// HardwareAddr6 overrides the device hardware address during Init.
// This field is optional if [DevEthernet.HardwareAddr6] returns valid MAC.
HardwareAddr6 [6]byte
// MTU is the maximum ethernet payload size. Does not include ethernet header(14b) and FCS(4b).
// If MTU is zero the default ipv4.MTU value of 1500 is used.
MTU uint16
}
// Init initializes the interface from scratch with a netlink and device. If Init fails all methods on Interface are unsafe to call (panic).
func (iface *Interface[C]) Init(netlink Netlink[C], dev DevEthernet, cfg InterfaceConfig) (err error) {
if netlink == nil || dev == nil {
return lneto.ErrInvalidConfig
}
maxFrameSize, frameOff := dev.MaxFrameSizeAndOffset()
maxEthFrame := maxFrameSize - frameOff
maxEthPayload := maxEthFrame - 14
mtu := int(cfg.MTU)
if mtu == 0 {
mtu = min(1500, maxEthPayload)
}
if mtu > maxEthPayload {
return errors.New("MTU exceeds max frame size")
} else if mtu < ethernet.MinimumMTU || mtu > ethernet.MaxMTU {
return errors.New("bad DevEthernet max frame size and/or frame offset. typical is 1500,0")
}
var mac [6]byte
if internal.IsZeroed(cfg.HardwareAddr6) {
mac, err = dev.HardwareAddr6()
if err != nil {
return err
} else if internal.IsZeroed(mac) {
return lneto.ErrInvalidAddr
}
} else {
mac = cfg.HardwareAddr6
}
*iface = Interface[C]{
dev: dev,
netlink: netlink,
ip: cfg.NetworkIP,
frameSize: maxFrameSize,
frameOff: frameOff,
mtu: mtu,
mac: mac,
}
return nil
}
// HardwareAddr6 returns the hardware address the [Interface] was configured with.
func (iface *Interface[C]) HardwareAddr6() [6]byte {
return iface.mac
}
// NetworkAddr returns the IP and subnet of the network behind the interface. See [netip.Prefix].
// May be unset/invalid.
func (iface *Interface[C]) NetworkAddr() netip.Prefix {
return iface.ip
}
func (iface *Interface[C]) bufsize() int {
return iface.frameOff + 14 + iface.mtu
}
+141
View File
@@ -0,0 +1,141 @@
package netdev
import (
"context"
"errors"
"sync/atomic"
"github.com/soypat/lneto"
)
// Runner orchestrates an Interface and a Stack asynchronously.
type Runner[C any] struct {
running atomic.Uint32
// buflen stores the length of data inside buf. It is used as a buffer acquisition synchronizing primitive.
buflen atomic.Uint32
// pktlost is incremented each time an incoming packet is lost due to insufficient buffer size.
pktlost atomic.Uint64
tx, rx atomic.Uint64
// buf stores actual data.
buf []byte
// bufsaux is used as ana argument to stack processing so that no allocations are performed
bufsaux [1][]byte
sizesaux [1]int
handlerTriggered bool
deviceIsPollOnly bool
}
func (r *Runner[C]) Run(ctx context.Context, iface Interface[C], stack Stack, backoff lneto.BackoffStrategy) error {
if stack == nil || backoff == nil {
return errors.New("nil arguments to Run")
}
if !r.acquire() {
return errors.New("runner currently running.")
}
defer func() {
iface.dev.SetEthRecvHandler(nil)
r.release()
}()
r.rx.Store(0)
r.tx.Store(0)
r.buflen.Store(0)
r.pktlost.Store(0)
r.handlerTriggered = false
r.deviceIsPollOnly = false
bufsize := iface.bufsize()
if cap(r.buf) < bufsize {
r.buf = make([]byte, bufsize)
}
r.buf = r.buf[:bufsize]
iface.dev.SetEthRecvHandler(r.recvEthHandler)
// backoffs stores number of consecutive times no data was sent/received.
var backoffs uint
for ctx.Err() == nil {
n1, _ := r.processRx(stack, 0)
eoff, efrm, err := iface.dev.EthPoll(r.buf)
n2, _ := r.processRx(stack, 0)
if efrm > 0 && n2 == 0 {
r.deviceIsPollOnly = true
r.buflen.Store(uint32(eoff + efrm))
r.processRx(stack, eoff)
} else if efrm > 0 && n2 > 0 {
return errors.New("device both returns nonzero poll read and calls, choose one")
} else if err != nil {
println("err EthPoll:", err.Error())
}
// Now do Tx, but first acquire buffer.
if !r.buflen.CompareAndSwap(0, 1) {
continue // Oh no, async data received, go back to Rx processing.
}
r.bufsaux = [1][]byte{r.buf}
err = stack.EgressPackets(r.bufsaux[:], r.sizesaux[:], iface.frameOff)
n := r.sizesaux[0]
if err != nil {
println("err EgressPackets:", err.Error())
} else if n > 0 {
if n+iface.frameOff > len(r.buf) {
return errors.New("EgressPackets returned invalid written data given frameOffset and argument buffer size")
}
err = iface.dev.SendOffsetEthFrame(r.bufsaux[0][:n+iface.frameOff])
r.tx.Add(uint64(n + iface.frameOff))
if err != nil {
println("err SendOffsetEthFrame:", err.Error())
}
}
r.buflen.Store(0) // Release buffer.
if n1 > 0 || n2 > 0 || efrm > 0 || n > 0 {
backoffs = 0
} else {
backoff.Do(backoffs)
backoffs++
}
}
return ctx.Err()
}
// PrintDebug
//
// Deprecated: Might be given other shape in future, but this is not how we do debugging. use freely meanwhile.
func (r *Runner[C]) PrintDebug() {
print("RUNNER: tx|rx:", r.tx.Load(), "|", r.rx.Load(),
" devpollonly:", r.deviceIsPollOnly, " pktlost:", r.pktlost.Load(),
" handles:", r.handlerTriggered, " bufsize:", len(r.buf),
"\n")
}
func (r *Runner[C]) acquire() bool {
return r.running.CompareAndSwap(0, 1)
}
func (r *Runner[C]) release() {
if r.running.Load()&1 == 0 {
panic("release of unacquired resource")
}
r.running.Store(0)
}
// recvEthHandler is called asynchronously. Should be as fast as possible. Do not block inside.
func (r *Runner[C]) recvEthHandler(incomingEthernet []byte) {
if !r.buflen.CompareAndSwap(0, uint32(len(incomingEthernet))) {
// Failed to acquire buffer, packet dropped.
r.pktlost.Add(1)
return
}
copy(r.buf, incomingEthernet)
}
// processRx is called after a packet is received asynchronously and compied to buffer via recvEthHandler
func (r *Runner[C]) processRx(stack Stack, ethFrameOff int) (int, error) {
r.handlerTriggered = true
n := r.buflen.Load()
if n == 0 {
return 0, nil
}
r.rx.Add(uint64(n))
defer r.buflen.Store(0)
r.bufsaux = [1][]byte{r.buf[:n]}
return int(n), stack.IngressPackets(r.bufsaux[:], ethFrameOff)
}
+111
View File
@@ -0,0 +1,111 @@
package xnet
import (
"context"
"net/netip"
"time"
"github.com/soypat/lneto"
"github.com/soypat/lneto/x/netdev"
)
// Netstack is a more modern outward facing API wrapper on StackAsync.
type Netstack struct {
stack StackAsync
//gstack references stack above.
gstack StackGo
awaitDHCP bool
}
var _ netdev.Stack = (*Netstack)(nil)
// Configure configures this Stack with the argument mac, ip and gateway addresses.
// The Stack must resolve the gateway hardware address if set.
func (netstack *Netstack) Reset(cfg StackConfig, stackBackoff lneto.BackoffStrategy, poolcfg TCPPoolConfig) error {
err := netstack.stack.Reset(cfg)
if err != nil {
return err
}
netstack.gstack = netstack.stack.StackGo(stackBackoff, StackGoConfig{
ListenerPoolConfig: poolcfg,
})
return nil
}
func (netstack *Netstack) IPAddr4() [4]byte {
return netstack.stack.Addr4()
}
// EnableICMP enables responding/sending ICMP echo frames.
func (netstack *Netstack) EnableICMP(enabled bool) error {
return netstack.stack.EnableICMP(enabled)
}
// EnableDHCP performs a DHCPv4 request, writes the assigned address into the
// stack, and resolves the gateway hardware address via ARP.
func (netstack *Netstack) EnableDHCP(ctx context.Context, enabled bool, requestAddr netip.Addr) (assigned netip.Addr, routerGW netip.Addr, subnetBits int, err error) {
var addr [4]byte
if !enabled {
netstack.stack.dhcp.Reset()
return netip.Addr{}, netip.Addr{}, 0, nil
} else if requestAddr.Is4() {
addr = requestAddr.As4()
} else if requestAddr.IsValid() {
return netip.Addr{}, netip.Addr{}, 0, lneto.ErrUnsupported
}
timeout := 4 * time.Second
deadline, ok := ctx.Deadline()
if ok {
timeout = time.Until(deadline)
}
results, err := netstack.gstack.blk.DoDHCPv4(addr, timeout)
if err != nil {
return netip.Addr{}, netip.Addr{}, 0, err
}
// Write IP, subnet and DNS into the stack.
if err = netstack.stack.AssimilateDHCPResults(results); err != nil {
return netip.AddrFrom4(results.AssignedAddr4), results.Router, results.Subnet.Bits(), err
}
// Resolve gateway hardware address so egress packets are unicasted to the
// router rather than broadcast. Failure is non-fatal.
if results.Router.IsValid() {
gwHW, gwErr := netstack.gstack.blk.DoResolveHardwareAddress6(results.Router, 2*time.Second)
if gwErr == nil {
netstack.stack.SetGatewayHardwareAddr(gwHW)
}
}
return netip.AddrFrom4(results.AssignedAddr4), results.Router, results.Subnet.Bits(), nil
}
// Socket is a berkeley socket abstraction. Returns an [net.Listener] or [net.Conn] depending on laddr/raddr combination.
func (netstack *Netstack) Socket(ctx context.Context, network string, family, sotype int, laddr, raddr netip.AddrPort) (c any, err error) {
return netstack.gstack.SocketNetip(ctx, network, family, sotype, laddr, raddr)
}
// EgressPackets instructs Stack to write outgoing packets into bufs and writing the sizes into sizes not including initial offset.
// offset can be used to tell the stack to start writing after an offset for each buffer.
func (netstack *Netstack) EgressPackets(bufs [][]byte, sizes []int, offset int) (err error) {
var err0 error
for i := range bufs {
sizes[i], err0 = netstack.stack.EgressEthernet(bufs[i][offset:])
if sizes[i] == 0 {
return err
} else if err0 != nil {
err = err0
}
}
return err
}
// IngressPackets is called on incoming packets so the Stack can direct packets to
// their respective node/connection and update internal state.
// offset instructs the Stack to start reading packets at an offset.
func (netstack *Netstack) IngressPackets(bufs [][]byte, offset int) (err error) {
for _, buf := range bufs {
err0 := netstack.stack.IngressEthernet(buf[offset:])
if err0 != nil {
err = err0
}
}
return err
}
+4
View File
@@ -119,6 +119,7 @@ func (s *StackAsync) IngressEthernet(ethernetFrame []byte) error {
defer s.mu.Unlock()
s.stats.TotalReceived += uint64(len(ethernetFrame))
err := s.link.Demux(ethernetFrame, 0)
debugPacket("IN ", ethernetFrame)
if err == nil {
s.arpt.learnFromIngressEthernet(ethernetFrame)
}
@@ -132,6 +133,9 @@ func (s *StackAsync) EgressEthernet(dstEthernetFrame []byte) (int, error) {
defer s.mu.Unlock()
n, err := s.link.Encapsulate(dstEthernetFrame, -1, 0)
s.stats.TotalSent += uint64(n)
if n > 0 {
debugPacket("OUT", dstEthernetFrame[:n])
}
return n, err
}
+5
View File
@@ -0,0 +1,5 @@
//go:build !xnetdebug
package xnet
func debugPacket(msg string, b []byte) {}
+17
View File
@@ -0,0 +1,17 @@
//go:build xnetdebug
package xnet
import "os"
var _pcap CapturePrinter
func init() {
_pcap.Configure(os.Stdout, CapturePrinterConfig{
NamespaceWidth: 3,
})
}
func debugPacket(msg string, b []byte) {
_pcap.PrintPacket(msg, b)
}