Files
lneto/x/xnet/stack-async.go
T
Pat Whittingslow bdbd38ab44 ipv6: StackIP and StackAsync.Addr refactor (#105)
* apply StackIP changes and internet package test passing

* fix tests and examples

* remove old Reset method on StackIP

* use encapsulate for ipv6

* add TCP over IPv6 tests
2026-05-09 16:11:31 -03:00

732 lines
19 KiB
Go

package xnet
import (
"encoding/binary"
"errors"
"log/slog"
"net/netip"
"sync"
"time"
"github.com/soypat/lneto"
"github.com/soypat/lneto/arp"
"github.com/soypat/lneto/dhcpv4"
"github.com/soypat/lneto/dns"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/internet"
"github.com/soypat/lneto/ipv4/icmpv4"
"github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp"
"github.com/soypat/lneto/udp"
)
const (
minTCPBuffer = 256
)
type StackAsync struct {
mu sync.Mutex
hostname string
clientID string
link internet.StackEthernet
ip internet.StackIP
arp arp.Handler
icmp icmpv4.Client
udps internet.StackPortsMACFiltered
tcps internet.StackPortsMACFiltered
defaultValidator lneto.Validator
dhcpUDP internet.StackUDPPort
dhcp dhcpv4.Client
dhcpResults DHCPResults
arpt subnetTable
dnsUDP internet.StackUDPPort
dns dns.Client
ednsopt dns.Resource
lookup dns.Message
dnssv netip.Addr
ntpUDP internet.StackUDPPort
ntp ntp.Client
userUDPs []internet.StackUDPPort
sysprec int8 // NTP system precision.
prng uint32
addrBuf [6]byte // Temporary buffer for As4()/HardwareAddr6() results to avoid heap escapes.
totalsent uint64
totalrecv uint64
}
type StackConfig struct {
// StaticAddress6 [16]byte
StaticAddress4 [4]byte
DNSServer netip.Addr
NTPServer netip.Addr
RandSeed int64
Hostname string
// MaxActiveTCPPorts and MaxActiveUDPPorts are a memory guardrail to limit
// number of simultaneous open TCP/UDP ports. The memory impact at the stack level
// of a port corresponds to ~64 bytes excluding the registered StackNode i.e: [tcp.Conn] or [udp.Conn].
MaxActiveTCPPorts, MaxActiveUDPPorts uint16
EthernetTxCRC32Update func(crc uint32, b []byte) uint32
HardwareAddress [6]byte
MTU uint16
// Accept multicast ethernet and IP packets. Needed for MDNS.
AcceptMulticast bool
// ICMPQueueLimit sets maximum number of input/output packets queued for processing.
// If set to zero ICMP cannot be enabled on the stack.
ICMPQueueLimit int
// PassivePeers limits how many subnet peers the stack passively learns MAC addresses for.
// Passively learned entries skip ARP round-trips on the first DialTCP/DialUDP to that peer.
PassivePeers int
}
func (s *StackAsync) Hostname() string {
return s.hostname
}
// IngressEthernet receives an Ethernet frame from the network and processes it through the stack. The frame should include the Ethernet header and payload and CRC if enabled.
func (s *StackAsync) IngressEthernet(ethernetFrame []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
s.totalrecv += uint64(len(ethernetFrame))
err := s.link.Demux(ethernetFrame, 0)
if err == nil {
s.arpt.learnFromIngressEthernet(ethernetFrame)
}
return err
}
// EgressEthernet writes the next ethernet frame to send into dstEthernetFrame from the stack.
// The length of dstEthernetFrame should be at least MTU + Ethernet header (14) + CRC (4 if enabled).
func (s *StackAsync) EgressEthernet(dstEthernetFrame []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
n, err := s.link.Encapsulate(dstEthernetFrame, -1, 0)
s.totalsent += uint64(n)
return n, err
}
// IngressIP processes an incoming IP frame through the stack and omits ethernet header processing.
func (s *StackAsync) IngressIP(ipFrame []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
s.totalrecv += uint64(len(ipFrame))
return s.ip.Demux(ipFrame, 0)
}
// EgressIP writes the next IP frame to send into dstIPFrame from the stack. The length of dstIPFrame should be at least MTU.
func (s *StackAsync) EgressIP(dstIPFrame []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if len(dstIPFrame) < s.link.MTU() {
return 0, lneto.ErrShortBuffer
}
n, err := s.ip.Encapsulate(dstIPFrame, 0, 0)
s.totalsent += uint64(n)
return n, err
}
// MTU is the Maximum Transmission Unit of the stack corresponding
// to the maximum payload size of an ethernet frame that can be sent through the stack.
// Important to note that the actual ethernet frame size is MTU + Ethernet header (14) + CRC (4 if enabled), this is known as the Maximum Frame Length.
func (s *StackAsync) MTU() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.link.MTU()
}
func (s *StackAsync) Reset(cfg StackConfig) error {
if cfg.RandSeed == 0 || cfg.Hostname == "" || cfg.PassivePeers > 255 {
return lneto.ErrInvalidConfig
}
mac := cfg.HardwareAddress
s.mu.Lock()
defer s.mu.Unlock()
s.prng = uint32(cfg.RandSeed)
s.hostname = cfg.Hostname
const linkNodes = 2 // ARP and IP nodes
ecfg := internet.StackEthernetConfig{
MTU: int(cfg.MTU),
MaxNodes: linkNodes,
MAC: mac,
Gateway: ethernet.BroadcastAddr(),
AppendCRC32: cfg.EthernetTxCRC32Update != nil,
CRC32Update: cfg.EthernetTxCRC32Update,
}
err := s.link.Configure(ecfg)
if err != nil {
return err
}
s.link.SetAcceptMulticast(cfg.AcceptMulticast)
if cfg.PassivePeers == 0 {
s.link.OnEncapsulate(nil)
} else {
s.link.OnEncapsulate(s.arpt.patchEgressMAC)
}
const ipNodes = 3 // 3 IP protocols possible: UDP, TCP, ICMP.
err = s.ip.Reset(&s.defaultValidator, ipNodes, 0)
if err != nil {
return err
}
s.ip.SetAddr4(cfg.StaticAddress4)
s.ip.SetAcceptMulticast4(cfg.AcceptMulticast)
s.arpt.passivePeers = uint8(cfg.PassivePeers)
err = s.resetARP()
if err != nil {
return err
}
udpConns := 3 + cfg.MaxActiveUDPPorts // DHCP, DNS, NTP + user-registered.
err = s.udps.ResetUDP(udpConns)
if err != nil {
return err
}
internal.SliceReuse(&s.userUDPs, int(cfg.MaxActiveUDPPorts))
// Enable TCP if connections present.
if cfg.MaxActiveTCPPorts > 0 {
err = s.tcps.ResetTCP(cfg.MaxActiveTCPPorts)
if err != nil {
return err
}
err = s.ip.Register4(&s.tcps)
if err != nil {
return err
}
}
// Now setup stacks.
// ARP registered in resetARP.
err = s.link.Register(&s.ip) // IPv4 | IPv6
if err != nil {
return err
}
err = s.ip.Register4(&s.udps)
if err != nil {
return err
}
if cfg.ICMPQueueLimit > 0 {
err = s.icmp.Configure(icmpv4.ClientConfig{
ResponseQueueBuffer: make([]byte, cfg.ICMPQueueLimit*64),
ResponseQueueLimit: cfg.ICMPQueueLimit,
HashSeed: s.prand32(),
ID: uint16(cfg.Hostname[len(cfg.Hostname)-1]) - '0', // Treat last character of hostname as number.
})
if err != nil {
return err
}
}
var timebuf [4]int64
s.sysprec = ntp.CalculateSystemPrecision(nil, timebuf[:])
if s.clientID == "" {
s.clientID = "lneto-" + s.hostname
}
s.totalrecv = 0
s.totalsent = 0
if cfg.DNSServer.IsValid() {
s.dnssv = cfg.DNSServer
}
return nil
}
func (s *StackAsync) resetARP() error {
mac := s.link.HardwareAddr6()
addr := s.ip.Addr4()
proto := ethernet.TypeIPv4
err := s.arp.Reset(arp.HandlerConfig{
HardwareAddr: mac[:],
ProtocolAddr: addr[:],
MaxQueries: 5,
MaxPending: 5,
HardwareType: 1,
ProtocolType: proto,
})
if err != nil {
return err
}
s.arpt.reset(10, s.arpt.passivePeers)
s.arp.SetOnResolveCallback(s.arpt.onResolve)
err = s.link.Register(&s.arp)
if err != nil {
return err
}
return nil
}
func (s *StackAsync) prandRead(buf []byte) {
i := 0
for ; i+3 < len(buf); i += 4 {
binary.LittleEndian.PutUint32(buf[i:], s.prand32())
}
v := s.prand32()
for i < len(buf) {
buf[i] = byte(v >> (8 * (i % 4)))
i++
}
}
// Prand32 generates a pseudo random 32-bit unsigned integer from the internal state and advances the seed.
func (s *StackAsync) Prand32() (randval uint32) {
s.mu.Lock()
randval = s.prand32()
s.mu.Unlock()
return randval
}
func (s *StackAsync) prand32() uint32 {
/* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */
seed := internal.Prand32(s.prng)
s.prng = seed
return seed
}
func (s *StackAsync) SetAddr4(addr [4]byte) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.setIPAddr4(addr)
}
func (s *StackAsync) setIPAddr4(addr [4]byte) error {
s.ip.SetAddr4(addr)
return s.arp.UpdateProtoAddr(addr[:])
}
func (s *StackAsync) Addr4() [4]byte {
s.mu.Lock()
defer s.mu.Unlock()
return s.ip.Addr4()
}
func (s *StackAsync) SetSubnet(subnetMask netip.Prefix) {
s.mu.Lock()
defer s.mu.Unlock()
s.arpt.subnet = subnetMask
}
func (s *StackAsync) SetHardwareAddress(hw [6]byte) error {
s.mu.Lock()
defer s.mu.Unlock()
s.link.SetHardwareAddr6(hw)
return s.resetARP()
}
func (s *StackAsync) HardwareAddress() (hw [6]byte) {
s.mu.Lock()
defer s.mu.Unlock()
return s.link.HardwareAddr6()
}
func (s *StackAsync) SetGateway6(gwhw [6]byte) {
s.mu.Lock()
defer s.mu.Unlock()
s.link.SetGateway6(gwhw)
}
func (s *StackAsync) Gateway6() [6]byte {
s.mu.Lock()
defer s.mu.Unlock()
return s.link.Gateway6()
}
// EnableICMP registers an ICMP handler to the stack when enabled is true.
// If enabled=false the currently registered ICMP handler is unregistered and state reset.
func (s *StackAsync) EnableICMP(enabled bool) (err error) {
if s.icmp.IncomingEchoCapacity() == 0 {
err = lneto.ErrInvalidConfig
enabled = false // ensure aborted.
}
if enabled {
if s.ip.IsRegistered4(lneto.IPProtoICMP) {
return nil
}
err = s.ip.Register4(&s.icmp)
} else {
s.icmp.Abort()
}
return err
}
func (s *StackAsync) DialUDP(conn *udp.Conn, localPort uint16, addrp netip.AddrPort) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
var mac []byte
if s.arpt.subnet.Contains(addrp.Addr()) {
mac = make([]byte, 6)
ip := addrp.Addr().As4()
hw, err := s.arp.CacheLookup(ip[:])
if err == nil {
// MAC already contained in results.
copy(mac, hw)
} else {
// StartQuery starts an ARP query for addresses in this network.
// On finishing query MAC is set and thus the StackPort will allow encapsulating
// data on that connection.
err = s.arpt.startQuery(mac, ip[:], &s.arp)
if err != nil {
return err
}
}
}
err = conn.Open(localPort, addrp)
if err != nil {
return err
}
err = s.udps.Register(conn, mac)
return nil
}
func (s *StackAsync) DialTCP(conn *tcp.Conn, localPort uint16, addrp netip.AddrPort) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
var mac []byte
if s.arpt.subnet.Contains(addrp.Addr()) {
ip := addrp.Addr().As4()
hw, err := s.arp.CacheLookup(ip[:])
mac = make([]byte, 6)
if err == nil {
// Query exists, use pre-existing result.
copy(mac, hw)
} else {
// StartQuery starts an ARP query for addresses in this network.
// On finishing query MAC is set and thus the StackPort will allow encapsulating
// data on that connection.
err = s.arpt.startQuery(mac, ip[:], &s.arp)
if err != nil {
return err
}
}
}
err = conn.OpenActive(localPort, addrp, tcp.Value(s.prand32()))
if err != nil {
return err
}
err = s.tcps.Register(conn, mac) // MAC is set later on by ARP response arriving to our network.
if err != nil {
conn.Abort()
return err
}
return nil
}
func (s *StackAsync) ListenTCP(conn *tcp.Conn, localPort uint16) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
err = conn.OpenListen(localPort, tcp.Value(s.prand32()))
if err != nil {
return err
}
err = s.tcps.Register(conn, nil)
if err != nil {
conn.Abort()
return err
}
return nil
}
func (s *StackAsync) RegisterListener(listener *tcp.Listener) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
lport := listener.LocalPort()
if lport == 0 {
return lneto.ErrZeroSource
}
return s.tcps.Register(listener, nil)
}
// RegisterUDP registers a StackNode on a UDP port with the given remote address and port.
// The StackUDPPort wrapping is handled internally. The number of user-registered UDP ports
// is limited by [StackConfig.MaxUDPConns].
func (s *StackAsync) RegisterUDP(node lneto.StackNode, remoteAddr []byte, remotePort uint16) error {
s.mu.Lock()
defer s.mu.Unlock()
idx := len(s.userUDPs)
if idx >= cap(s.userUDPs) {
return lneto.ErrExhausted
}
s.userUDPs = s.userUDPs[:idx+1]
s.userUDPs[idx].SetStackNode(node, remoteAddr, remotePort)
return s.udps.Register(&s.userUDPs[idx], nil)
}
var errNoDNSServer = errors.New("no DNS server- did DHCP complete? You can set a predetermined DNS server in Stack configuration")
func (s *StackAsync) StartLookupIP(host string) error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.dnssv.IsValid() {
return errNoDNSServer
}
name, err := dns.NewName(host)
if err != nil {
return err
}
// EDNS0 buffer size: MTU minus overhead for IP+UDP headers and safety margin.
// 100 bytes covers IPv4 max header (60) + UDP (8) + 32 byte margin.
s.ednsopt.SetEDNS0(uint16(s.link.MTU())-100, 0, 0, nil)
rand := s.prand32()
err = s.dns.StartResolve(uint16(rand>>1)+1024, uint16(rand), dns.ResolveConfig{
Questions: []dns.Question{
{
Name: name,
Type: dns.TypeA,
Class: dns.ClassINET,
},
},
Additional: []dns.Resource{
s.ednsopt,
},
EnableRecursion: true,
})
if err != nil {
return err
}
*(*[4]byte)(s.addrBuf[:4]) = s.dnssv.As4()
s.dnsUDP.SetStackNode(&s.dns, s.addrBuf[:4], dns.ServerPort)
err = s.udps.Register(&s.dnsUDP, nil)
return err
}
var errDNSNotDone = errors.New("DNS not done")
func (s *StackAsync) ResultLookupIP(host string) ([]netip.Addr, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
done, err := s.dns.MessageCopyTo(&s.lookup)
if err != nil {
return nil, done, err
} else if !done {
return nil, done, errDNSNotDone
}
var addrs []netip.Addr
ans := s.lookup.Answers
for i := range ans {
data := ans[i].RawData()
if len(data) == 4 {
addrs = append(addrs, netip.AddrFrom4([4]byte(data)))
} else if len(data) == 16 {
addrs = append(addrs, netip.AddrFrom16([16]byte(data)))
} else {
err = lneto.ErrInvalidAddr
}
}
if err == nil && len(addrs) == 0 {
err = errors.New("no address in DNS answer")
}
return addrs, done, err
}
func (s *StackAsync) StartDHCPv4Request(request [4]byte) error {
s.mu.Lock()
defer s.mu.Unlock()
s.dhcp.Reset()
xid := s.prand32()
err := s.dhcp.BeginRequest(xid, dhcpv4.RequestConfig{
RequestedAddr: request,
ClientHardwareAddr: s.link.HardwareAddr6(),
Hostname: s.hostname,
ClientID: s.clientID,
})
if err != nil {
return err
}
s.dhcpUDP.SetStackNode(&s.dhcp, nil, dhcpv4.DefaultServerPort)
err = s.udps.Register(&s.dhcpUDP, nil)
if err != nil {
return err
}
return err
}
func (s *StackAsync) StartNTP(addr netip.Addr) error {
s.mu.Lock()
defer s.mu.Unlock()
s.ntp.Reset(s.sysprec, time.Now)
*(*[4]byte)(s.addrBuf[:4]) = addr.As4()
s.ntpUDP.SetStackNode(&s.ntp, s.addrBuf[:4], ntp.ServerPort)
err := s.udps.Register(&s.ntpUDP, nil)
return err
}
// ResultNTPOffset returns the result of the NTP protocol such that the following code returns the corrected time.
// If the bool is false then the NTP has not yet completed.
//
// nowCorrected := time.Now().Add(resultNTP)
func (s *StackAsync) ResultNTPOffset() (time.Duration, bool) {
s.mu.Lock()
defer s.mu.Unlock()
return s.ntp.Offset(), s.ntp.IsDone()
}
func (s *StackAsync) StartResolveHardwareAddress6(ip netip.Addr) error {
s.mu.Lock()
defer s.mu.Unlock()
if !ip.Is4() {
return lneto.ErrUnsupported
}
addr := ip.As4()
return s.arp.StartQuery(addr[:], false)
}
// ResultResolveHardwareAddress6
func (s *StackAsync) ResultResolveHardwareAddress6(ip netip.Addr) (hw [6]byte, err error) {
s.mu.Lock()
defer s.mu.Unlock()
if !ip.Is4() {
return hw, lneto.ErrUnsupported
}
addr := ip.As4()
hwslice, err := s.arp.CacheLookup(addr[:])
if err != nil {
return hw, err
} else if len(hwslice) != 6 {
panic("unreachable slice hw length")
}
return [6]byte(hwslice), nil
}
// DiscardResolveHardwareAddress6 discards a pending ARP query for the given IP address.
func (s *StackAsync) DiscardResolveHardwareAddress6(ip netip.Addr) error {
s.mu.Lock()
defer s.mu.Unlock()
if !ip.Is4() {
return lneto.ErrUnsupported
}
addr := ip.As4()
return s.arp.CacheRemove(addr[:])
}
type DHCPResults struct {
DNSServers []netip.Addr
Router netip.Addr
AssignedAddr4 [4]byte
ServerAddr netip.Addr
BroadcastAddr netip.Addr
Gateway netip.Addr
Subnet netip.Prefix
TRebind uint32 // [seconds]
TRenewal uint32
TLease uint32 // IP lease time [seconds].
}
func (s *StackAsync) ResultDHCP() (*DHCPResults, error) {
err := s.populateDHCPResults()
if err != nil {
return nil, err
}
return &s.dhcpResults, nil
}
type Statistics struct {
// Total amount of bytes sent over encapsulate.
TotalSent uint64
// Total amount of bytes received over demux.
TotalReceived uint64
}
func (s *StackAsync) ReadStatistics(stats *Statistics) {
stats.TotalReceived = s.totalrecv
stats.TotalSent = s.totalsent
}
// AssimilateDHCPResults sets the stack's following parameters:
// - IPv4 address.
// - DNS server.
// - Subnet (for ARP resolution of local addresses).
func (stack *StackAsync) AssimilateDHCPResults(results *DHCPResults) error {
stack.mu.Lock()
defer stack.mu.Unlock()
if results.Subnet.IsValid() {
stack.arpt.subnet = results.Subnet
}
if !internal.IsZeroed(results.AssignedAddr4) {
err := stack.setIPAddr4(results.AssignedAddr4)
if err != nil {
return err
}
}
if len(results.DNSServers) > 0 {
if !results.DNSServers[0].IsValid() || !results.DNSServers[0].Is4() {
return lneto.ErrInvalidAddr
}
stack.dnssv = results.DNSServers[0]
}
return nil
}
func (s *StackAsync) populateDHCPResults() error {
if !s.dhcp.State().HasIP() {
return errors.New("DHCP not completed")
}
router4, ok := s.dhcp.RouterAddr()
if !ok {
return errors.New("no DHCP router address")
}
assigned4, ok := s.dhcp.AssignedAddr()
if !ok {
return errors.New("no DHCP assigned address")
}
router := netip.AddrFrom4(router4)
s.dhcpResults = DHCPResults{
Router: router,
Subnet: s.dhcp.SubnetPrefix(),
AssignedAddr4: assigned4,
ServerAddr: addr4(s.dhcp.ServerAddr()),
BroadcastAddr: addr4(s.dhcp.BroadcastAddr()),
Gateway: addr4(s.dhcp.GatewayAddr()),
TRebind: s.dhcp.RebindingSeconds(),
TRenewal: s.dhcp.RenewalSeconds(),
TLease: s.dhcp.IPLeaseSeconds(),
DNSServers: s.dhcpResults.DNSServers[:0], // reuse field capacity.
}
s.dhcpResults.DNSServers = s.dhcp.AppendDNSServers(s.dhcpResults.DNSServers)
return nil
}
func addr4(addr [4]byte, ok bool) netip.Addr {
if !ok {
return netip.Addr{}
}
return netip.AddrFrom4(addr)
}
// Debug prints debugging information. Very useful for users when coupled with
// the debugheaplog build tag. See [internal.LogAttrs] debugheaplog version.
//
// go build -tags=debugheaplog ./yourprogram
func (s *StackAsync) Debug(msg string) {
internal.LogAttrs(slog.Default(), slog.LevelDebug, "stackasync",
slog.String("umsg", msg),
slog.Uint64("sent", s.totalsent),
slog.Uint64("recv", s.totalrecv),
)
}
// DebugErr prints debugging and error info. Very useful for users when coupled with
// the debugheaplog build tag. See [internal.LogAttrs] debugheaplog version.
//
// go build -tags=debugheaplog ./yourprogram
func (s *StackAsync) DebugErr(msg, err string) {
internal.LogAttrs(slog.Default(), slog.LevelError, "stackasync",
slog.String("umsg", msg),
slog.String("err", err),
slog.Uint64("sent", s.totalsent),
slog.Uint64("recv", s.totalrecv),
)
}