Files
lneto/x/xnet/subnet-table.go
T
Pat Whittingslow a2970b923d add ipv6 to xnet.StackAsync (#107)
* add ipv6 to xnet.StackAsync

* dns improvements

* improve DNS workings of StackAsync

* add tentative ICMPv6

* work on prefixes and fix some small bugs, plan UDP/TCP6

* fix bugs in StackAsync and ipv4.Prefix.Contains

* update arpsubtable

* completely remove legacy internet.StackIP for StackIPv4/v6

* ipv4/ipv6 tcp/udp

* add TCP6/UDP6 dialing APIs

* add xnet.Stack6 interface

* more ipv6 integration into StackAsync; various tweaks to lneto and documentation+TODOs

* add stack6 tests

* replace netip.Prefix with ipv4.Prefix where it makes sense
2026-05-13 15:31:18 -03:00

167 lines
4.4 KiB
Go

package xnet
import (
"encoding/binary"
"github.com/soypat/lneto"
"github.com/soypat/lneto/arp"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4"
)
// subnetTable manages both passively learned peer MAC/IP tuples and in-flight async ARP resolves.
//
// Layout of resolves slice:
//
// [0 : passivePeers] — owned MAC+IP, permanently retained (learned passively from ingress)
// [passivePeers : len] — externally-owned MAC, evicted by age (pending ARP queries)
type subnetTable struct {
subnet4 ipv4.Prefix
resolves4 []struct {
mac []byte // externally owned for pending entries; owned for passive entries.
ip [4]byte
age uint16
}
passivePeers uint8
}
func (a *subnetTable) reset(arpentries int, passivePeers uint8) {
a.passivePeers = passivePeers
if a.resolves4 == nil {
internal.SliceReuse(&a.resolves4, arpentries+int(passivePeers))
a.resolves4 = a.resolves4[:cap(a.resolves4)]
}
}
func (a *subnetTable) hwDynamicResolve(addr [4]byte, arph *arp.Handler) (mac []byte, err error) {
if !a.subnet4.Contains(addr) {
return nil, nil // not in subnet; caller uses gateway/default routing (nil MAC = no filtering)
}
var mac6 [6]byte
hw, err := arph.CacheLookup(addr[:])
if err == nil {
copy(mac6[:], hw)
} else {
err = a.startQuery(mac6[:], addr[:], arph)
if err != nil {
return nil, err
}
}
return mac6[:], nil // mac6 escapes to heap via startQuery or return
}
func (a *subnetTable) learnFromIngressEthernet(ethernetFrame []byte) {
if len(ethernetFrame) > 14+20 &&
binary.BigEndian.Uint16(ethernetFrame[12:14]) == uint16(ethernet.TypeIPv4) {
src, _, _, _, _ := internal.GetIPAddr(ethernetFrame[14:])
a.learnPassive(src, ethernetFrame[6:12])
}
}
// learnPassive stores or updates a passively observed MAC/IP tuple in the reserved slots.
// It is a no-op if passivePeers is zero, src is not in the local subnet, or all slots are taken.
func (a *subnetTable) learnPassive(src, mac []byte) {
if a.passivePeers == 0 || len(src) != 4 {
return
}
addr := [4]byte(src)
if !a.subnet4.Contains(addr) {
return
}
for i := range a.passivePeers {
v := &a.resolves4[i]
if v.ip == addr {
copy(v.mac, mac) // update in case MAC changed (e.g. NIC swap)
return
}
if v.ip == ([4]byte{}) {
v.ip = addr
v.mac = append(v.mac, mac...)
return
}
}
}
// startQuery copies the MAC into mac immediately if the IP was passively learned,
// otherwise issues an ARP query via h and registers mac as the externally-owned destination.
func (a *subnetTable) startQuery(mac, ip []byte, h *arp.Handler) error {
if len(ip) != 4 {
return lneto.ErrUnsupported
}
addr := [4]byte(ip)
for i := range a.passivePeers {
v := &a.resolves4[i]
if v.ip == addr {
copy(mac, v.mac)
return nil
}
}
if err := h.StartQuery(ip, true); err != nil {
return err
}
n := int(a.passivePeers)
oldest := n
for i := n; i < len(a.resolves4); i++ {
v := &a.resolves4[i]
if len(v.mac) == 0 {
oldest = i
break
} else if v.age > a.resolves4[oldest].age {
oldest = i
}
}
for i := n; i < len(a.resolves4); i++ {
a.resolves4[i].age++
}
v := &a.resolves4[oldest]
v.mac = mac
v.ip = addr
v.age = 1
return nil
}
// onResolve is the arp.Handler resolve callback; called when an ARP response arrives.
func (a *subnetTable) onResolve(mac, ip []byte) {
if len(ip) != 4 {
return
}
addr := [4]byte(ip)
for i := int(a.passivePeers); i < len(a.resolves4); i++ {
v := &a.resolves4[i]
if v.ip == addr {
copy(v.mac, mac)
v.mac = nil
v.ip = [4]byte{}
v.age = 0
return
}
}
}
// patchEgressMAC is registered as the OnEncapsulate callback on StackEthernet.
// It runs after the payload is written but before CRC is appended, so the CRC
// covers the corrected destination MAC.
func (a *subnetTable) patchEgressMAC(frame []byte) {
if a.passivePeers == 0 || len(frame) < 14+20 ||
binary.BigEndian.Uint16(frame[12:14]) != uint16(ethernet.TypeIPv4) {
return
}
efrm, _ := ethernet.NewFrame(frame)
if efrm.IsBroadcast() {
return // broadcast stays broadcast (e.g. DHCP discover).
}
// Server-side connections have no registered MAC; fill from passively learned entries.
_, dstIP, _, _, err := internal.GetIPAddr(frame[14:])
if err != nil {
return
}
for i := range a.passivePeers {
v := &a.resolves4[i]
if internal.BytesEqual(v.ip[:], dstIP) {
*efrm.DestinationHardwareAddr() = [6]byte(v.mac)
return
}
}
}