Files
lneto/x/netdev/interface.go
T
Pat Whittingslow 99e9d90a60 Netdev revamp (#145)
* begin working on netdev solution

* need to roll back some assumptions in next commit

* dual poll/async mode for netdev.Runner

* add wake on rx semantics

* remove TODO

* more reworking of Runner

* work on applying @MDr164 suggestions and a couple extra revamps

* add newline to end of test file
2026-07-09 14:04:10 -03:00

192 lines
8.4 KiB
Go

package netdev
import (
"context"
"errors"
"net/netip"
"unsafe"
"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)
// SendOffsetEthFrame 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).
// SendOffsetEthFrame 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
// SetEthRecvHandler registers the function called when an Ethernet
// frame is received. Buffers needed by the device to operate efficiently
// should be allocated on its side.
//
// Frames may be delivered via this handler, via EthPoll's buffer, or both:
// - Handler unset: EthPoll writes received frames into its argument buffer.
// - Handler set: received frames are delivered to the handler. EthPoll must
// not write to its argument buffer; it is called with a nil buffer purely
// to pump devices that need explicit servicing to drive the handler.
//
// Quiescence guarantee: SetEthRecvHandler(nil) must not return while a
// previously installed handler is executing on another goroutine, and after
// it returns the old handler must not be invoked again (analogous to Linux
// synchronize_irq semantics). Callers rely on this to safely reuse the
// buffers a handler writes into.
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.
//
// Behavior depends on whether a handler is set via SetEthRecvHandler:
// - No handler: writes a received frame into buf, returning its offset/length.
// - Handler set: buf is nil and must not be written to; EthPoll only pumps
// the device so frames are delivered through the handler. Return values are ignored.
EthPoll(buf []byte) (ethFrameOff, ethernetBytes int, err error)
// MaxFrameSizeAndOffset returns the max complete device frame size
// (including headers and any overhead) for Ethernet Rx 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 for Rx should be maxFrameSize.
MaxFrameSizeAndOffset() (maxFrameSize int, sendEthFrameOff int)
}
// Stack is an abstraction for a networking stack.
type Stack interface {
// 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
}
// RunnerBuffers returns 32-bit aligned contiguous buffers for using with [RunnerConfig].
func (iface *Interface[C]) RunnerBuffers(n int) [][]byte {
frmlen32 := (iface.frameSize + 3) / 4
rawBuf32 := make([]uint32, n*frmlen32) // ensure memory aligned
bufs := make([][]byte, n)
for i := range n {
buf32 := rawBuf32[i*frmlen32 : (i+1)*frmlen32]
bufs[i] = unsafe.Slice((*byte)(unsafe.Pointer(&buf32[0])), iface.frameSize)
}
return bufs
}
// 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
}