mirror of
https://github.com/soypat/lneto.git
synced 2026-08-20 06:29:03 +00:00
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:
@@ -9,6 +9,8 @@ profiles/
|
||||
# Dependency directories after running `go mod vendor`
|
||||
vendor/
|
||||
|
||||
# Wifi credentials
|
||||
*.credentials
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.elf
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build !xnetdebug
|
||||
|
||||
package xnet
|
||||
|
||||
func debugPacket(msg string, b []byte) {}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user