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
This commit is contained in:
Pat Whittingslow
2026-05-13 15:31:18 -03:00
committed by GitHub
parent 7d5830d7ab
commit a2970b923d
44 changed files with 1938 additions and 395 deletions
+205 -145
View File
@@ -15,6 +15,7 @@ import (
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/internet"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv4/icmpv4"
"github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp"
@@ -23,6 +24,7 @@ import (
const (
minTCPBuffer = 256
icmpEchoSize = 64
)
type StackAsync struct {
@@ -30,9 +32,13 @@ type StackAsync struct {
hostname string
clientID string
link internet.StackEthernet
ip internet.StackIP
arp arp.Handler
icmp icmpv4.Client
ip4 internet.StackIPv4
// ip6 internet.StackIPv6
arp arp.Handler
icmp icmpv4.Client
// icmp6 icmpv6.Client
icmp6buf []byte
udps internet.StackPortsMACFiltered
tcps internet.StackPortsMACFiltered
@@ -58,38 +64,51 @@ type StackAsync struct {
prng uint32
addrBuf [6]byte // Temporary buffer for As4()/HardwareAddr6() results to avoid heap escapes.
addrBuf [6]byte // Temporary buffer for As4()/HardwareAddr6() results to avoid heap escapes.
addrbufnip [4]netip.Addr
totalsent uint64
totalrecv uint64
stats Statistics
ipv6enabled bool
stack6 Stack6
}
type StackConfig struct {
// StaticAddress6 [16]byte
StaticAddress4 [4]byte
HardwareAddress [6]byte
StaticAddress4 [4]byte
StaticAddress6 [16]byte
IPv6Stack Stack6
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
// Hostname is used for DHCP hostname and ICMP ID.
Hostname string
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
// 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
// MTU sets the maximum transmission unit, which is the maximum size of the Ethernet payload
// not including ethernet header, ethernet CRC. It is determined by the NIC hardware and the route the packets take over the network.
// By far the most common value for MTU is 1500 as specified by IEEE 802.3.
MTU uint16
// Accept multicast ethernet and IP packets. Needed for MDNS.
AcceptMulticast bool
}
func (cfg *StackConfig) id() uint16 {
return uint16(cfg.Hostname[len(cfg.Hostname)-1] - '0')
}
func (s *StackAsync) Hostname() string {
@@ -100,7 +119,7 @@ func (s *StackAsync) Hostname() string {
func (s *StackAsync) IngressEthernet(ethernetFrame []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
s.totalrecv += uint64(len(ethernetFrame))
s.stats.TotalReceived += uint64(len(ethernetFrame))
err := s.link.Demux(ethernetFrame, 0)
if err == nil {
s.arpt.learnFromIngressEthernet(ethernetFrame)
@@ -114,16 +133,28 @@ 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)
s.stats.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 {
if len(ipFrame) < 1 {
return lneto.ErrTruncatedFrame
}
version := ipFrame[0] >> 4
s.mu.Lock()
defer s.mu.Unlock()
s.totalrecv += uint64(len(ipFrame))
return s.ip.Demux(ipFrame, 0)
s.stats.TotalReceived += uint64(len(ipFrame))
switch version {
case 4:
return s.ip4.Demux(ipFrame, 0)
case 6:
if s.ipv6enabled {
return s.stack6.IngressIPv6(ipFrame)
}
}
return lneto.ErrPacketDrop
}
// EgressIP writes the next IP frame to send into dstIPFrame from the stack. The length of dstIPFrame should be at least MTU.
@@ -133,8 +164,11 @@ func (s *StackAsync) EgressIP(dstIPFrame []byte) (int, error) {
if len(dstIPFrame) < s.link.MTU() {
return 0, lneto.ErrShortBuffer
}
n, err := s.ip.Encapsulate(dstIPFrame, 0, 0)
s.totalsent += uint64(n)
n, err := s.ip4.Encapsulate(dstIPFrame, 0, 0)
if s.ipv6enabled && n == 0 {
n, err = s.stack6.EgressIPv6(dstIPFrame)
}
s.stats.TotalSent += uint64(n)
return n, err
}
@@ -147,17 +181,33 @@ func (s *StackAsync) MTU() int {
return s.link.MTU()
}
func (s *StackAsync) Reset(cfg StackConfig) error {
func (s *StackAsync) Reset(cfg StackConfig) (err error) {
ipv6Enabled := cfg.IPv6Stack != nil
if cfg.RandSeed == 0 || cfg.Hostname == "" || cfg.PassivePeers > 255 {
return lneto.ErrInvalidConfig
} else if !internal.IsZeroed(cfg.StaticAddress6) && !ipv6Enabled {
return lneto.ErrBug // Forgot to EnableIPv6 after setting static IPv6 address.
}
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
// Treat last character of hostname as number.
id := cfg.id()
linkNodes := 2 // ARP and IPv4 nodes
s.ipv6enabled = ipv6Enabled
s.stack6 = nil
if s.ipv6enabled {
linkNodes = 3 // IPv6
s.Debug("ipv6 enabled")
err = cfg.IPv6Stack.Reset6(&cfg)
if err != nil {
s.ipv6enabled = false
return err
}
}
s.stack6 = cfg.IPv6Stack
ecfg := internet.StackEthernetConfig{
MTU: int(cfg.MTU),
MaxNodes: linkNodes,
@@ -166,42 +216,37 @@ func (s *StackAsync) Reset(cfg StackConfig) error {
AppendCRC32: cfg.EthernetTxCRC32Update != nil,
CRC32Update: cfg.EthernetTxCRC32Update,
}
err := s.link.Configure(ecfg)
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)
err = s.ip4.Reset(&s.defaultValidator, ipNodes)
if err != nil {
return err
}
s.ip.SetAddr4(cfg.StaticAddress4)
s.ip.SetAcceptMulticast4(cfg.AcceptMulticast)
s.ip4.SetAddr4(cfg.StaticAddress4)
s.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
}
s.udps.ResetUDP(udpConns)
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)
s.tcps.ResetTCP(cfg.MaxActiveTCPPorts)
err = s.ip4.Register4(&s.tcps)
if err != nil {
return err
}
@@ -209,20 +254,21 @@ func (s *StackAsync) Reset(cfg StackConfig) error {
// Now setup stacks.
// ARP registered in resetARP.
err = s.link.Register(&s.ip) // IPv4 | IPv6
err = s.link.RegisterEthernet(&s.ip4) // IPv4
if err != nil {
return err
}
err = s.ip.Register4(&s.udps)
err = s.ip4.Register4(&s.udps)
if err != nil {
return err
}
if cfg.ICMPQueueLimit > 0 {
err = s.icmp.Configure(icmpv4.ClientConfig{
ResponseQueueBuffer: make([]byte, cfg.ICMPQueueLimit*64),
ResponseQueueBuffer: make([]byte, cfg.ICMPQueueLimit*icmpEchoSize),
ResponseQueueLimit: cfg.ICMPQueueLimit,
HashSeed: s.prand32(),
ID: uint16(cfg.Hostname[len(cfg.Hostname)-1]) - '0', // Treat last character of hostname as number.
ID: id,
})
if err != nil {
return err
@@ -233,17 +279,23 @@ func (s *StackAsync) Reset(cfg StackConfig) error {
if s.clientID == "" {
s.clientID = "lneto-" + s.hostname
}
s.totalrecv = 0
s.totalsent = 0
s.stats = Statistics{}
if cfg.DNSServer.IsValid() {
s.dnssv = cfg.DNSServer
}
if s.ipv6enabled {
s.Debug("registering IPv6 to ethernet")
err = s.link.RegisterEthernet(s.stack6.IPv6Stack())
if err != nil {
return err
}
}
return nil
}
func (s *StackAsync) resetARP() error {
mac := s.link.HardwareAddr6()
addr := s.ip.Addr4()
addr := s.ip4.Addr4()
proto := ethernet.TypeIPv4
err := s.arp.Reset(arp.HandlerConfig{
HardwareAddr: mac[:],
@@ -258,7 +310,7 @@ func (s *StackAsync) resetARP() error {
}
s.arpt.reset(10, s.arpt.passivePeers)
s.arp.SetOnResolveCallback(s.arpt.onResolve)
err = s.link.Register(&s.arp)
err = s.link.RegisterEthernet(&s.arp)
if err != nil {
return err
}
@@ -299,42 +351,42 @@ func (s *StackAsync) SetAddr4(addr [4]byte) error {
}
func (s *StackAsync) setIPAddr4(addr [4]byte) error {
s.ip.SetAddr4(addr)
s.ip4.SetAddr4(addr)
return s.arp.UpdateProtoAddr(addr[:])
}
func (s *StackAsync) Addr4() [4]byte {
s.mu.Lock()
defer s.mu.Unlock()
return s.ip.Addr4()
return s.ip4.Addr4()
}
func (s *StackAsync) SetSubnet(subnetMask netip.Prefix) {
func (s *StackAsync) SetSubnet4(addr [4]byte, prefixBits uint8) {
s.mu.Lock()
defer s.mu.Unlock()
s.arpt.subnet = subnetMask
s.arpt.subnet4 = ipv4.PrefixFrom(addr, prefixBits)
}
func (s *StackAsync) SetHardwareAddress(hw [6]byte) error {
func (s *StackAsync) SetHardwareAddr(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) {
func (s *StackAsync) HardwareAddr() (hw [6]byte) {
s.mu.Lock()
defer s.mu.Unlock()
return s.link.HardwareAddr6()
}
func (s *StackAsync) SetGateway6(gwhw [6]byte) {
func (s *StackAsync) SetGatewayHardwareAddr(gwhw [6]byte) {
s.mu.Lock()
defer s.mu.Unlock()
s.link.SetGateway6(gwhw)
}
func (s *StackAsync) Gateway6() [6]byte {
func (s *StackAsync) GatewayHardwareAddr() [6]byte {
s.mu.Lock()
defer s.mu.Unlock()
return s.link.Gateway6()
@@ -348,71 +400,56 @@ func (s *StackAsync) EnableICMP(enabled bool) (err error) {
enabled = false // ensure aborted.
}
if enabled {
if s.ip.IsRegistered4(lneto.IPProtoICMP) {
return nil
if !s.ip4.IsRegistered4(lneto.IPProtoICMP) {
err = s.ip4.Register4(&s.icmp)
}
err = s.ip.Register4(&s.icmp)
} else {
s.icmp.Abort()
}
if s.ipv6enabled {
if err2 := s.stack6.EnableICMP6(enabled); err2 != nil {
err = err2
}
}
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
}
}
addr := addrp.Addr()
if addr.Is4() {
err = s.DialUDP4(conn, localPort, addrp.Addr().As4(), addrp.Port())
} else if s.ipv6enabled && addr.Is6() {
err = s.stack6.DialUDP6(conn, localPort, addr.As16(), addrp.Port())
} else {
err = lneto.ErrInvalidAddr
}
err = conn.Open(localPort, addrp)
if err != nil {
return err
}
err = s.udps.Register(conn, mac)
return nil
return err
}
func (s *StackAsync) DialTCP(conn *tcp.Conn, localPort uint16, addrp netip.AddrPort) (err error) {
addr := addrp.Addr()
if addr.Is4() {
err = s.DialTCP4(conn, localPort, addrp.Addr().As4(), addrp.Port())
} else if s.ipv6enabled && addr.Is6() {
err = s.stack6.DialTCP6(conn, localPort, addr.As16(), addrp.Port(), tcp.Value(s.Prand32()))
} else {
err = lneto.ErrInvalidAddr
}
return err
}
func (s *StackAsync) DialUDP4(conn *udp.Conn, localPort uint16, raddr [4]byte, rport uint16) (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()))
mac, err := s.arpt.hwDynamicResolve(raddr, &s.arp)
if err != nil {
return err
}
err = s.tcps.Register(conn, mac) // MAC is set later on by ARP response arriving to our network.
err = conn.Open(localPort, netip.AddrPortFrom(netip.AddrFrom4(raddr), rport))
if err != nil {
return err
}
err = s.udps.RegisterMACFiltered(conn, mac)
if err != nil {
conn.Abort()
return err
@@ -420,14 +457,33 @@ func (s *StackAsync) DialTCP(conn *tcp.Conn, localPort uint16, addrp netip.AddrP
return nil
}
func (s *StackAsync) ListenTCP(conn *tcp.Conn, localPort uint16) (err error) {
func (s *StackAsync) DialTCP4(conn *tcp.Conn, localPort uint16, raddr [4]byte, rport uint16) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
mac, err := s.arpt.hwDynamicResolve(raddr, &s.arp)
if err != nil {
return err
}
err = conn.OpenActive(localPort, netip.AddrPortFrom(netip.AddrFrom4(raddr), rport), tcp.Value(s.prand32()))
if err != nil {
return err
}
err = s.tcps.RegisterMACFiltered(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) ListenTCP4(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)
err = s.tcps.RegisterMACFiltered(conn, nil)
if err != nil {
conn.Abort()
return err
@@ -436,19 +492,21 @@ func (s *StackAsync) ListenTCP(conn *tcp.Conn, localPort uint16) (err error) {
}
func (s *StackAsync) RegisterListener(listener *tcp.Listener) (err error) {
// TODO(pato): Possible to forward both IPv4 and IPv6 packets to the listener and have it selectively mux out correctly?
// Can try changing listener to inspect carrierData on demux and get the IPversion to know which tcp.Conns match the IP version.
s.mu.Lock()
defer s.mu.Unlock()
lport := listener.LocalPort()
if lport == 0 {
return lneto.ErrZeroSource
}
return s.tcps.Register(listener, nil)
return s.tcps.RegisterMACFiltered(listener, nil)
}
// RegisterUDP registers a StackNode on a UDP port with the given remote address and port.
// RegisterUDP4 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 {
func (s *StackAsync) RegisterUDP4(node lneto.StackNode, remoteAddr []byte, remotePort uint16) error {
s.mu.Lock()
defer s.mu.Unlock()
idx := len(s.userUDPs)
@@ -457,7 +515,7 @@ func (s *StackAsync) RegisterUDP(node lneto.StackNode, remoteAddr []byte, remote
}
s.userUDPs = s.userUDPs[:idx+1]
s.userUDPs[idx].SetStackNode(node, remoteAddr, remotePort)
return s.udps.Register(&s.userUDPs[idx], nil)
return s.udps.RegisterMACFiltered(&s.userUDPs[idx], nil)
}
var errNoDNSServer = errors.New("no DNS server- did DHCP complete? You can set a predetermined DNS server in Stack configuration")
@@ -495,38 +553,27 @@ func (s *StackAsync) StartLookupIP(host string) error {
}
*(*[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)
err = s.udps.RegisterMACFiltered(&s.dnsUDP, nil)
return err
}
var errDNSNotDone = errors.New("DNS not done")
var (
errDNSNotDone = errors.New("DNS not done")
errDNSNoAns = errors.New("no address in DNS answer")
)
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
_, ok := s.dns.ResponseFlags()
if !ok {
return nil, false, 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
}
n, err := s.dns.ResponseAnswerLookup(s.addrbufnip[:], host)
if n == 0 && err == nil {
err = errDNSNoAns
}
if err == nil && len(addrs) == 0 {
err = errors.New("no address in DNS answer")
}
return addrs, done, err
return s.addrbufnip[:n], true, err
}
func (s *StackAsync) StartDHCPv4Request(request [4]byte) error {
@@ -545,7 +592,7 @@ func (s *StackAsync) StartDHCPv4Request(request [4]byte) error {
}
s.dhcpUDP.SetStackNode(&s.dhcp, nil, dhcpv4.DefaultServerPort)
err = s.udps.Register(&s.dhcpUDP, nil)
err = s.udps.RegisterMACFiltered(&s.dhcpUDP, nil)
if err != nil {
return err
}
@@ -559,7 +606,7 @@ func (s *StackAsync) StartNTP(addr netip.Addr) error {
*(*[4]byte)(s.addrBuf[:4]) = addr.As4()
s.ntpUDP.SetStackNode(&s.ntp, s.addrBuf[:4], ntp.ServerPort)
err := s.udps.Register(&s.ntpUDP, nil)
err := s.udps.RegisterMACFiltered(&s.ntpUDP, nil)
return err
}
@@ -611,6 +658,17 @@ func (s *StackAsync) DiscardResolveHardwareAddress6(ip netip.Addr) error {
return s.arp.CacheRemove(addr[:])
}
func (s *StackAsync) SetAcceptMulticast4(enabled bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.setAcceptMulticast4(enabled)
}
func (s *StackAsync) setAcceptMulticast4(enabled bool) {
s.link.SetAcceptMulticast(enabled)
s.ip4.SetAcceptMulticast4(enabled)
}
type DHCPResults struct {
DNSServers []netip.Addr
Router netip.Addr
@@ -640,8 +698,9 @@ type Statistics struct {
}
func (s *StackAsync) ReadStatistics(stats *Statistics) {
stats.TotalReceived = s.totalrecv
stats.TotalSent = s.totalsent
s.mu.Lock()
*stats = s.stats
s.mu.Unlock()
}
// AssimilateDHCPResults sets the stack's following parameters:
@@ -651,8 +710,8 @@ func (s *StackAsync) ReadStatistics(stats *Statistics) {
func (stack *StackAsync) AssimilateDHCPResults(results *DHCPResults) error {
stack.mu.Lock()
defer stack.mu.Unlock()
if results.Subnet.IsValid() {
stack.arpt.subnet = results.Subnet
if results.Subnet.IsValid() && results.Subnet.Addr().Is4() {
stack.arpt.subnet4 = ipv4.PrefixFromNetip(results.Subnet)
}
if !internal.IsZeroed(results.AssignedAddr4) {
err := stack.setIPAddr4(results.AssignedAddr4)
@@ -682,9 +741,10 @@ func (s *StackAsync) populateDHCPResults() error {
return errors.New("no DHCP assigned address")
}
router := netip.AddrFrom4(router4)
subnet := s.dhcp.SubnetPrefix()
s.dhcpResults = DHCPResults{
Router: router,
Subnet: s.dhcp.SubnetPrefix(),
Subnet: subnet.NetipPrefix(),
AssignedAddr4: assigned4,
ServerAddr: addr4(s.dhcp.ServerAddr()),
BroadcastAddr: addr4(s.dhcp.BroadcastAddr()),
@@ -712,8 +772,8 @@ func addr4(addr [4]byte, ok bool) netip.Addr {
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),
slog.Uint64("sent", s.stats.TotalSent),
slog.Uint64("recv", s.stats.TotalReceived),
)
}
@@ -725,7 +785,7 @@ 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),
slog.Uint64("sent", s.stats.TotalSent),
slog.Uint64("recv", s.stats.TotalReceived),
)
}
+1 -1
View File
@@ -76,7 +76,7 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype
}
if laddr.Addr() == netip.IPv4Unspecified() {
// Specify address.
laddr = netip.AddrPortFrom(netip.AddrFrom4(s.blk.async.ip.Addr4()), laddr.Port())
laddr = netip.AddrPortFrom(netip.AddrFrom4(s.blk.async.ip4.Addr4()), laddr.Port())
} else if laddr.Addr().Is6() {
return nil, lneto.ErrUnsupported
}
+212
View File
@@ -0,0 +1,212 @@
package xnet
import (
"net/netip"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/internet"
"github.com/soypat/lneto/ipv6/icmpv6"
"github.com/soypat/lneto/tcp"
"github.com/soypat/lneto/udp"
)
var _ Stack6 = (*stack6)(nil)
func DefaultStack6() Stack6 {
return new(stack6)
}
type Stack6 interface {
Reset6(cfg *StackConfig) error
Addr6() [16]byte
SetAddr6(addr [16]byte)
EnableICMP6(enabled bool) error
Register6(node lneto.StackNode) error
DialUDP6(conn *udp.Conn, localPort uint16, raddr [16]byte, rport uint16) error
DialTCP6(conn *tcp.Conn, localPort uint16, raddr [16]byte, rport uint16, iss tcp.Value) error
IngressIPv6(ipframe []byte) error
EgressIPv6(ipframe []byte) (int, error)
IPv6Stack() lneto.StackNode
}
type stack6 struct {
ip6 internet.StackIPv6
udps6 internet.StackPortsMACFiltered
tcps6 internet.StackPortsMACFiltered
vld lneto.Validator
icmp6buf []byte
icmp6 icmpv6.Client
// ndpPending tracks in-flight NDP MAC resolves for outbound connections.
// macBuf is shared with the registered node so macResolve patches it in place.
ndpPending []struct {
addr [16]byte
macBuf []byte
}
}
func (s *stack6) Register6(node lneto.StackNode) error { return s.ip6.Register6(node) }
func (s *stack6) Addr6() [16]byte { return s.ip6.Addr6() }
func (s *stack6) SetAddr6(addr [16]byte) { s.ip6.SetAddr6(addr) }
func (s *stack6) IPv6Stack() lneto.StackNode { return &s.ip6 }
func (s *stack6) Reset6(cfg *StackConfig) error {
const ipnodes = 3 // ICMP, TCP, UDP.
err := s.ip6.Reset(&s.vld, ipnodes)
if err != nil {
return err
}
s.ip6.SetAddr6(cfg.StaticAddress6)
s.ip6.SetAcceptMulticast6(true) // IPv6 needs multicast to work.
s.tcps6.ResetTCP(cfg.MaxActiveTCPPorts)
if cfg.MaxActiveTCPPorts > 0 {
err = s.ip6.Register6(&s.tcps6)
if err != nil {
return err
}
}
s.udps6.ResetUDP(cfg.MaxActiveUDPPorts)
if cfg.MaxActiveUDPPorts > 0 {
err = s.ip6.Register6(&s.udps6)
if err != nil {
return err
}
}
if cfg.ICMPQueueLimit > 0 {
minSize := cfg.ICMPQueueLimit * icmpEchoSize
internal.SliceReuse(&s.icmp6buf, minSize)
err = s.icmp6.Configure(icmpv6.ClientConfig{
ResponseQueueBuffer: s.icmp6buf[:cap(s.icmp6buf)],
ResponseQueueLimit: cfg.ICMPQueueLimit,
HashSeed: uint32(cfg.RandSeed),
ID: cfg.id(),
OurAddr: cfg.StaticAddress6,
OurMAC: cfg.HardwareAddress,
NDPCache: 16,
})
if err != nil {
return err
}
s.icmp6.SetNDPResolveCallback(s.macResolve)
ndpSlots := int(cfg.MaxActiveTCPPorts) + int(cfg.MaxActiveUDPPorts)
internal.SliceReuse(&s.ndpPending, ndpSlots)
s.ndpPending = s.ndpPending[:cap(s.ndpPending)] // all slots available for scan
}
return nil
}
func (s *stack6) EnableICMP6(enabled bool) (err error) {
if s.icmp6.PingIncomingCapacity() == 0 {
err = lneto.ErrInvalidConfig
enabled = false // ensure aborted.
}
if enabled {
if !s.ip6.IsRegistered6(lneto.IPProtoIPv6ICMP) {
err = s.ip6.Register6(&s.icmp6)
}
} else {
s.icmp6.Abort()
}
return err
}
func (s *stack6) IngressIPv6(ipFrame []byte) error {
return s.ip6.Demux(ipFrame, 0)
}
func (s *stack6) EgressIPv6(ipFrame []byte) (int, error) {
return s.ip6.Encapsulate(ipFrame, 0, 0)
}
// DialTCP6 opens an active TCP connection to raddr:rport. iss is the initial
// sequence number; the caller supplies a random value. NDP MAC resolution is
// attempted immediately; if the peer MAC is not yet cached a Neighbor
// Solicitation is queued and the connection is held until macResolve fires.
func (s *stack6) DialTCP6(conn *tcp.Conn, localPort uint16, raddr [16]byte, rport uint16, iss tcp.Value) error {
mac, err := s.ndpDynamicResolve(raddr)
if err != nil {
return err
}
err = conn.OpenActive(localPort, netip.AddrPortFrom(netip.AddrFrom16(raddr), rport), iss)
if err != nil {
return err
}
err = s.tcps6.RegisterMACFiltered(conn, mac)
if err != nil {
conn.Abort()
return err
}
return nil
}
// DialUDP6 opens a UDP connection to raddr:rport. NDP MAC resolution follows
// the same deferred strategy as DialTCP6.
func (s *stack6) DialUDP6(conn *udp.Conn, localPort uint16, raddr [16]byte, rport uint16) error {
mac, err := s.ndpDynamicResolve(raddr)
if err != nil {
return err
}
err = conn.Open(localPort, netip.AddrPortFrom(netip.AddrFrom16(raddr), rport))
if err != nil {
return err
}
err = s.udps6.RegisterMACFiltered(conn, mac)
if err != nil {
conn.Abort()
return err
}
return nil
}
// macResolve is the NDP resolve callback. It patches the shared macBuf of any
// pending outbound connection to addr so StackPortsMACFiltered begins forwarding.
func (s *stack6) macResolve(mac [6]byte, addr [16]byte) {
for i := range s.ndpPending {
e := &s.ndpPending[i]
if e.addr == addr && e.macBuf != nil {
// macbuf is externally owned and expects it to be written to on resolve.
copy(e.macBuf, mac[:])
e.macBuf = nil // free slot for future NDP resolution.
e.addr = [16]byte{}
}
}
}
// ndpDynamicResolve mirrors hwDynamicResolve for IPv6. It returns a
// heap-allocated MAC slice shared with the ndpPending table so that macResolve
// can patch the destination MAC in place once NDP resolves, exactly as the ARP
// subnetTable does for IPv4. Returns nil (no MAC filtering) when NDP is not
// configured.
func (s *stack6) ndpDynamicResolve(raddr [16]byte) ([]byte, error) {
if !s.ip6.IsRegistered6(lneto.IPProtoIPv6ICMP) {
return nil, nil // NDP unavailable; routing layer handles MAC.
}
mac, err := s.icmp6.NDPCacheLookup(raddr)
macBuf := make([]byte, 6)
if err == nil {
copy(macBuf, mac[:])
return macBuf, nil
}
if err = s.icmp6.NDPStartQuery(raddr, true); err != nil {
return nil, err
}
// Find a freed slot or grow the pending slice.
idx := -1
for i := range s.ndpPending {
if s.ndpPending[i].macBuf == nil {
idx = i
break
}
}
if idx < 0 {
return nil, lneto.ErrExhausted
}
e := &s.ndpPending[idx]
e.addr = raddr
e.macBuf = macBuf
return macBuf, nil
}
+618
View File
@@ -0,0 +1,618 @@
package xnet
import (
"bytes"
"testing"
"github.com/soypat/lneto/tcp"
"github.com/soypat/lneto/udp"
)
const (
ipv6HeaderSize = 40
mtu6Test = 1500
maxFrame6 = ipv6HeaderSize + mtu6Test
)
func stack6PairConfigs(seed int64, maxports, icmpQueue uint16) (cfg1, cfg2 StackConfig) {
var (
testAddr6A = [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} // 2001:db8::1
testAddr6B = [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} // 2001:db8::2
testMAC6A = [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01}
testMAC6B = [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x02}
)
cfg1 = StackConfig{
Hostname: "test-s6-1",
RandSeed: seed,
StaticAddress6: testAddr6A,
HardwareAddress: testMAC6A,
MTU: mtu6Test,
MaxActiveUDPPorts: maxports,
MaxActiveTCPPorts: maxports,
ICMPQueueLimit: int(icmpQueue),
}
cfg2 = StackConfig{
Hostname: "test-s6-2",
RandSeed: ^seed,
StaticAddress6: testAddr6B,
HardwareAddress: testMAC6B,
MTU: mtu6Test,
MaxActiveUDPPorts: maxports,
MaxActiveTCPPorts: maxports,
ICMPQueueLimit: int(icmpQueue),
}
return cfg1, cfg2
}
// newStack6Pair creates two stack6 instances with distinct IPv6 addresses and MACs.
// ICMPQueueLimit is zero so NDP is disabled; DialUDP6/DialTCP6 skip MAC filtering.
func newStack6Pair(t testing.TB, seed int64, maxports, icmpQueue uint16) (s1, s2 Stack6) {
t.Helper()
cfg1, cfg2 := stack6PairConfigs(seed, maxports, icmpQueue)
s1 = DefaultStack6()
s2 = DefaultStack6()
if err := s1.Reset6(&cfg1); err != nil {
t.Fatal("s1 Reset6:", err)
}
if err := s2.Reset6(&cfg2); err != nil {
t.Fatal("s2 Reset6:", err)
}
return s1, s2
}
// newUDPConn6 allocates and configures a udp.Conn for use with stack6.
func newUDPConn6(t testing.TB) *udp.Conn {
t.Helper()
const bufSize = 2048
conn := new(udp.Conn)
if err := conn.Configure(udp.ConnConfig{
RxBuf: make([]byte, bufSize),
TxBuf: make([]byte, bufSize),
RxQueueSize: 4,
TxQueueSize: 4,
}); err != nil {
t.Fatal("UDP Configure:", err)
}
return conn
}
// newTCPConn6 allocates and configures a tcp.Conn for use with stack6.
func newTCPConn6(t testing.TB) *tcp.Conn {
t.Helper()
const bufSize = 2048
conn := new(tcp.Conn)
if err := conn.Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufSize),
TxBuf: make([]byte, bufSize),
TxPacketQueueSize: 4,
}); err != nil {
t.Fatal("TCP Configure:", err)
}
return conn
}
// exchangeIPv6Once encapsulates one IPv6 frame from src and delivers it to dst.
// Returns the number of bytes written (0 if src had nothing to send).
func exchangeIPv6Once(t testing.TB, src, dst Stack6, buf []byte) int {
t.Helper()
n, err := src.EgressIPv6(buf)
if err != nil {
t.Error("EgressIPv6:", err)
return 0
}
if n == 0 {
return 0
}
if err := dst.IngressIPv6(buf[:n]); err != nil {
t.Error("IngressIPv6:", err)
}
return n
}
// listenTCP6 opens a passive TCP connection and registers it with a stack6 directly,
// mirroring what StackAsync.ListenTCP4 does for IPv4.
func listenTCP6(t testing.TB, s Stack6, conn *tcp.Conn, localPort uint16, iss tcp.Value) {
t.Helper()
if err := conn.OpenListen(localPort, iss); err != nil {
t.Fatal("OpenListen:", err)
}
if err := s.(*stack6).tcps6.RegisterMACFiltered(conn, nil); err != nil {
conn.Abort()
t.Fatal("RegisterMACFiltered:", err)
}
}
// ===== Tests =====
func TestStack6Reset(t *testing.T) {
s := DefaultStack6()
testMAC6A := [6]byte{1, 2, 3}
testAddr6A := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
err := s.Reset6(&StackConfig{
Hostname: "reset-test-1",
RandSeed: 42,
StaticAddress6: testAddr6A,
HardwareAddress: testMAC6A,
MTU: mtu6Test,
})
if err != nil {
t.Fatal(err)
}
if got := s.Addr6(); got != testAddr6A {
t.Errorf("Addr6 = %v, want %v", got, testAddr6A)
}
testAddr2 := testAddr6A
testAddr2[15] = 2
// SetAddr6 must update the returned address.
s.SetAddr6(testAddr2)
if got := s.Addr6(); got != testAddr2 {
t.Errorf("after SetAddr6: got %v, want %v", got, testAddr2)
}
}
func TestStack6Reset_ICMPConfigured(t *testing.T) {
s := DefaultStack6()
err := s.Reset6(&StackConfig{
Hostname: "icmp-cfg-1",
RandSeed: 1337,
MTU: mtu6Test,
ICMPQueueLimit: 4,
})
if err != nil {
t.Fatal(err)
}
// EnableICMP6 should succeed since the client was configured.
if err := s.EnableICMP6(true); err != nil {
t.Fatal("EnableICMP6:", err)
}
// Disable should always succeed.
if err := s.EnableICMP6(false); err != nil {
t.Fatal("EnableICMP6(false):", err)
}
}
// TestStack6UDP_DataExchange sends a datagram from stack A to stack B and reads it back.
// Both stacks are configured without ICMP so NDP/MAC-filtering is bypassed.
func TestStack6UDP_DataExchange(t *testing.T) {
const (
rngseed = 100
portA = 5001
portB = 5002
nports = 2
)
s1, s2 := newStack6Pair(t, rngseed, nports, 0)
buf := make([]byte, maxFrame6)
connA := newUDPConn6(t)
connB := newUDPConn6(t)
// Open A -> B direction.
if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil {
t.Fatal("DialUDP6 A:", err)
}
// Open B -> A direction so B's stack accepts datagrams from A.
if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil {
t.Fatal("DialUDP6 B:", err)
}
want := []byte("hello ipv6 udp")
if _, err := connA.Write(want); err != nil {
t.Fatal("Write:", err)
}
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected packet from A to B")
}
var rbuf [256]byte
n, err := connB.Read(rbuf[:])
if err != nil {
t.Fatal("Read:", err)
}
if !bytes.Equal(rbuf[:n], want) {
t.Errorf("got %q, want %q", rbuf[:n], want)
}
}
// TestStack6UDP_BidirectionalExchange verifies that both sides can send and receive.
func TestStack6UDP_BidirectionalExchange(t *testing.T) {
const (
rngseed = 100
nports = 1
portA = 6001
portB = 6002
)
s1, s2 := newStack6Pair(t, rngseed, nports, 0)
buf := make([]byte, maxFrame6)
connA := newUDPConn6(t)
connB := newUDPConn6(t)
if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil {
t.Fatal("DialUDP6 A:", err)
}
if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil {
t.Fatal("DialUDP6 B:", err)
}
// A -> B
msgAtoB := []byte("A->B")
if _, err := connA.Write(msgAtoB); err != nil {
t.Fatal("Write A->B:", err)
}
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected packet A->B")
}
var rbuf [256]byte
n, err := connB.Read(rbuf[:])
if err != nil {
t.Fatal("Read B:", err)
}
if !bytes.Equal(rbuf[:n], msgAtoB) {
t.Errorf("B received %q, want %q", rbuf[:n], msgAtoB)
}
// B -> A
msgBtoA := []byte("B->A reply")
if _, err := connB.Write(msgBtoA); err != nil {
t.Fatal("Write B->A:", err)
}
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected packet B->A")
}
n, err = connA.Read(rbuf[:])
if err != nil {
t.Fatal("Read A:", err)
}
if !bytes.Equal(rbuf[:n], msgBtoA) {
t.Errorf("A received %q, want %q", rbuf[:n], msgBtoA)
}
}
// TestStack6ICMPv6_PingEcho verifies a full ICMPv6 echo request/reply exchange.
func TestStack6ICMPv6_PingEcho(t *testing.T) {
const (
rngSeed = 42
nports = 1
icmpQueue = 2
)
s1, s2 := newStack6Pair(t, rngSeed, nports, icmpQueue)
buf := make([]byte, maxFrame6)
if err := s1.EnableICMP6(true); err != nil {
t.Fatal("s1 EnableICMP6:", err)
}
if err := s2.EnableICMP6(true); err != nil {
t.Fatal("s2 EnableICMP6:", err)
}
// Verify that no packets are pending before the ping.
if n, _ := s1.EgressIPv6(buf); n != 0 {
t.Fatal("unexpected egress from s1 before ping")
}
if n, _ := s2.EgressIPv6(buf); n != 0 {
t.Fatal("unexpected egress from s2 before ping")
}
// Start the ping from s1 to s2.
pattern := []byte("ping6test")
key, err := s1.(*stack6).icmp6.PingStart(s2.Addr6(), pattern, 32)
if err != nil {
t.Fatal("PingStart:", err)
}
// s1 sends echo request to s2.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected ICMPv6 echo request from s1")
}
// s2 sends echo reply back to s1.
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected ICMPv6 echo reply from s2")
}
// No further packets should be needed.
if n, _ := s1.EgressIPv6(buf); n != 0 {
t.Error("unexpected extra egress from s1 after ping")
}
if n, _ := s2.EgressIPv6(buf); n != 0 {
t.Error("unexpected extra egress from s2 after ping")
}
completed, ok := s1.(*stack6).icmp6.PingPop(key)
if !ok {
t.Fatal("ping key not found after exchange")
}
if !completed {
t.Fatal("expected ping to be completed")
}
}
// TestStack6ICMPv6_MultiPing verifies multiple sequential pings work.
func TestStack6ICMPv6_MultiPing(t *testing.T) {
const (
rngseed = 193213
icmpqueue = 2
)
s1, s2 := newStack6Pair(t, rngseed, 0, icmpqueue)
buf := make([]byte, maxFrame6)
if err := s1.EnableICMP6(true); err != nil {
t.Fatal("s1 EnableICMP6:", err)
}
if err := s2.EnableICMP6(true); err != nil {
t.Fatal("s2 EnableICMP6:", err)
}
for i, pattern := range [][]byte{
[]byte("first"),
[]byte("second"),
[]byte("third"),
} {
key, err := s1.(*stack6).icmp6.PingStart(s2.Addr6(), pattern, uint16(len(pattern)+4))
if err != nil {
t.Fatalf("PingStart [%d]: %v", i, err)
}
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatalf("[%d] expected echo request from s1", i)
}
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatalf("[%d] expected echo reply from s2", i)
}
completed, ok := s1.(*stack6).icmp6.PingPop(key)
if !ok {
t.Fatalf("[%d] ping key not found", i)
}
if !completed {
t.Fatalf("[%d] ping not completed", i)
}
}
}
// TestStack6TCP_Handshake verifies that a TCP connection can be established over stack6.
func TestStack6TCP_Handshake(t *testing.T) {
const (
rngseed = 213213
svPort = 8080
clPort = 12345
nports = 1
icmpqueue = 2
)
s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue)
buf := make([]byte, maxFrame6)
svConn := newTCPConn6(t)
clConn := newTCPConn6(t)
// Server listens on s2.
listenTCP6(t, s2, svConn, svPort, 200)
// Client dials from s1 to s2.
if err := s1.DialTCP6(clConn, clPort, s2.Addr6(), svPort, 100); err != nil {
t.Fatal("DialTCP6:", err)
}
// SYN: client -> server.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected SYN from client")
}
if clConn.State() != tcp.StateSynSent {
t.Errorf("client state = %s, want SYN_SENT", clConn.State())
}
if svConn.State() != tcp.StateSynRcvd {
t.Errorf("server state = %s, want SYN_RCVD", svConn.State())
}
// SYNACK: server -> client.
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected SYNACK from server")
}
if clConn.State() != tcp.StateEstablished {
t.Errorf("client state = %s, want ESTABLISHED", clConn.State())
}
// ACK: client -> server.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected ACK from client")
}
if svConn.State() != tcp.StateEstablished {
t.Errorf("server state = %s, want ESTABLISHED", svConn.State())
}
}
// TestStack6TCP_DataExchange establishes a TCP connection and transfers data.
func TestStack6TCP_DataExchange(t *testing.T) {
const (
rngseed = 400
svPort = 9090
clPort = 11111
nports = 1
icmpqueue = 1
)
s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue)
buf := make([]byte, maxFrame6)
svConn := newTCPConn6(t)
clConn := newTCPConn6(t)
listenTCP6(t, s2, svConn, svPort, 300)
if err := s1.DialTCP6(clConn, clPort, s2.Addr6(), svPort, 200); err != nil {
t.Fatal("DialTCP6:", err)
}
// Three-way handshake.
tcp6Handshake(t, s1, s2, buf)
// Send data from client to server.
payload := []byte("hello over tcp6")
if _, err := clConn.Write(payload); err != nil {
t.Fatal("Write:", err)
}
// PSH+ACK: client -> server.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected data packet from client")
}
// ACK: server -> client.
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected ACK from server")
}
// Drain any extra ACKs.
exchangeIPv6Once(t, s1, s2, buf)
exchangeIPv6Once(t, s2, s1, buf)
var rbuf [256]byte
n, err := svConn.Read(rbuf[:])
if err != nil {
t.Fatal("svConn.Read:", err)
}
if !bytes.Equal(rbuf[:n], payload) {
t.Errorf("server received %q, want %q", rbuf[:n], payload)
}
}
// TestStack6_NDP_DialUDP verifies that DialUDP6 with ICMP enabled triggers NDP
// resolution and that seeding the NDP cache allows the connection to proceed.
func TestStack6_NDP_DialUDP(t *testing.T) {
const (
portA = 7001
portB = 7002
rngseed = 132131
nports = 1
icmpqueue = 4
)
cfg1, cfg2 := stack6PairConfigs(rngseed, nports, icmpqueue)
s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue)
buf := make([]byte, maxFrame6)
if err := s1.EnableICMP6(true); err != nil {
t.Fatal("s1 EnableICMP6:", err)
}
if err := s2.EnableICMP6(true); err != nil {
t.Fatal("s2 EnableICMP6:", err)
}
// Seed s2's NDP cache so it knows s1's MAC (needed for NA reply).
if err := s2.(*stack6).icmp6.NDPCacheSeed(cfg1.StaticAddress6, cfg1.HardwareAddress); err != nil {
t.Fatal("NDPCacheSeed s2:", err)
}
connA := newUDPConn6(t)
connB := newUDPConn6(t)
// DialUDP6 on s1 queues an NS since s2's MAC is not yet in s1's NDP cache.
if err := s1.DialUDP6(connA, portA, cfg2.StaticAddress6, portB); err != nil {
t.Fatal("DialUDP6 A:", err)
}
// s2 already has s1's address seeded so DialUDP6 resolves immediately.
if err := s2.DialUDP6(connB, portB, cfg1.StaticAddress6, portA); err != nil {
t.Fatal("DialUDP6 B:", err)
}
// NDP exchange: s1 sends Neighbor Solicitation, s2 replies with Neighbor Advertisement.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected NDP Neighbor Solicitation from s1")
}
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected NDP Neighbor Advertisement from s2")
}
// After NDP resolved, connA's macBuf is patched; verify the cache now has s2's MAC.
mac, err := s1.(*stack6).icmp6.NDPCacheLookup(cfg2.StaticAddress6)
if err != nil {
t.Fatal("NDPCacheLookup after NDP exchange:", err)
}
if mac != cfg2.HardwareAddress {
t.Errorf("NDP resolved MAC = %v, want %v", mac, cfg2.HardwareAddress)
}
// Now that NDP is resolved, data should flow.
want := []byte("ndp resolved udp")
if _, err := connA.Write(want); err != nil {
t.Fatal("Write:", err)
}
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected UDP packet after NDP resolution")
}
var rbuf [256]byte
n, err := connB.Read(rbuf[:])
if err != nil {
t.Fatal("Read:", err)
}
if !bytes.Equal(rbuf[:n], want) {
t.Errorf("got %q, want %q", rbuf[:n], want)
}
}
// tcp6Handshake performs the SYN/SYNACK/ACK exchange between two stacks.
func tcp6Handshake(t testing.TB, client, server Stack6, buf []byte) {
t.Helper()
if n := exchangeIPv6Once(t, client, server, buf); n == 0 {
t.Fatal("handshake: expected SYN")
}
if n := exchangeIPv6Once(t, server, client, buf); n == 0 {
t.Fatal("handshake: expected SYNACK")
}
if n := exchangeIPv6Once(t, client, server, buf); n == 0 {
t.Fatal("handshake: expected ACK")
}
}
// TestStack6_EgressNoData checks that EgressIPv6 returns 0 when there is nothing to send.
func TestStack6_EgressNoData(t *testing.T) {
s, _ := newStack6Pair(t, 13213, 2, 2)
buf := make([]byte, maxFrame6)
n, err := s.EgressIPv6(buf)
if err != nil {
t.Errorf("EgressIPv6 with no traffic: %v", err)
}
if n != 0 {
t.Errorf("expected 0 bytes, got %d", n)
}
}
// TestStack6_IngressDropsWrongDst checks that packets destined for a different address are dropped.
func TestStack6_IngressDropsWrongDst(t *testing.T) {
s1, s2 := newStack6Pair(t, 2133213, 1, 1)
buf := make([]byte, maxFrame6)
connA := newUDPConn6(t)
connB := newUDPConn6(t)
const (
portA = 4001
portB = 4002
)
if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil {
t.Fatal(err)
}
if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil {
t.Fatal(err)
}
// Write and encapsulate from s1 (destination = testAddr6B).
if _, err := connA.Write([]byte("drop me")); err != nil {
t.Fatal(err)
}
n, err := s1.EgressIPv6(buf)
if err != nil || n == 0 {
t.Fatalf("expected encapsulated packet: n=%d err=%v", n, err)
}
// Feed to s1 itself (wrong destination) should be dropped (ErrPacketDrop or similar).
wrongDst := s1
if err := wrongDst.IngressIPv6(buf[:n]); err == nil {
t.Error("expected error when delivering packet to wrong destination stack, got nil")
}
// Deliver correctly to s2 should succeed.
if err := s2.IngressIPv6(buf[:n]); err != nil {
t.Errorf("correct delivery to s2 failed: %v", err)
}
// s2 should have received the datagram.
var rbuf [256]byte
rn, err := connB.Read(rbuf[:])
if err != nil || rn == 0 {
t.Errorf("expected s2 to have received data: n=%d err=%v", rn, err)
}
}
+57 -30
View File
@@ -2,11 +2,12 @@ package xnet
import (
"encoding/binary"
"net/netip"
"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.
@@ -16,10 +17,10 @@ import (
// [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 {
subnet netip.Prefix
resolves []struct {
subnet4 ipv4.Prefix
resolves4 []struct {
mac []byte // externally owned for pending entries; owned for passive entries.
ip []byte // always owned by this struct.
ip [4]byte
age uint16
}
passivePeers uint8
@@ -27,12 +28,29 @@ type subnetTable struct {
func (a *subnetTable) reset(arpentries int, passivePeers uint8) {
a.passivePeers = passivePeers
if a.resolves == nil {
internal.SliceReuse(&a.resolves, arpentries+int(passivePeers))
a.resolves = a.resolves[:cap(a.resolves)]
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) {
@@ -44,21 +62,21 @@ func (a *subnetTable) learnFromIngressEthernet(ethernetFrame []byte) {
// 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 {
if a.passivePeers == 0 || len(src) != 4 {
return
}
addr, _ := netip.AddrFromSlice(src)
if !a.subnet.Contains(addr) {
addr := [4]byte(src)
if !a.subnet4.Contains(addr) {
return
}
for i := range a.passivePeers {
v := &a.resolves[i]
if internal.BytesEqual(v.ip, src) {
v := &a.resolves4[i]
if v.ip == addr {
copy(v.mac, mac) // update in case MAC changed (e.g. NIC swap)
return
}
if len(v.ip) == 0 {
v.ip = append(v.ip, src...)
if v.ip == ([4]byte{}) {
v.ip = addr
v.mac = append(v.mac, mac...)
return
}
@@ -68,9 +86,13 @@ func (a *subnetTable) learnPassive(src, mac []byte) {
// 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.resolves[i]
if internal.BytesEqual(v.ip, ip) {
v := &a.resolves4[i]
if v.ip == addr {
copy(mac, v.mac)
return nil
}
@@ -80,33 +102,38 @@ func (a *subnetTable) startQuery(mac, ip []byte, h *arp.Handler) error {
}
n := int(a.passivePeers)
oldest := n
for i := n; i < len(a.resolves); i++ {
v := &a.resolves[i]
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.resolves[oldest].age {
} else if v.age > a.resolves4[oldest].age {
oldest = i
}
}
for i := n; i < len(a.resolves); i++ {
a.resolves[i].age++
for i := n; i < len(a.resolves4); i++ {
a.resolves4[i].age++
}
v := &a.resolves[oldest]
v := &a.resolves4[oldest]
v.mac = mac
v.ip = append(v.ip[:0], ip...)
v.age = 0
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) {
for i := int(a.passivePeers); i < len(a.resolves); i++ {
v := &a.resolves[i]
if internal.BytesEqual(ip, v.ip) {
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 = v.ip[:0]
v.ip = [4]byte{}
v.age = 0
return
}
}
@@ -130,8 +157,8 @@ func (a *subnetTable) patchEgressMAC(frame []byte) {
return
}
for i := range a.passivePeers {
v := &a.resolves[i]
if internal.BytesEqual(v.ip, dstIP) {
v := &a.resolves4[i]
if internal.BytesEqual(v.ip[:], dstIP) {
*efrm.DestinationHardwareAddr() = [6]byte(v.mac)
return
}
+4 -4
View File
@@ -14,8 +14,8 @@ func TestARPLocal(t *testing.T) {
s1, s2, c1, c2 := newTCPStacks(t, seed, mtu)
routerHw := [6]byte{1, 2, 3, 4, 5, 6}
// Most common case: we have a router in between computers.
s1.SetGateway6(routerHw)
s2.SetGateway6(routerHw)
s1.SetGatewayHardwareAddr(routerHw)
s2.SetGatewayHardwareAddr(routerHw)
addr1 := netip.AddrPortFrom(netip.AddrFrom4(s1.Addr4()), 1024) // dialer, client.
addr2 := netip.AddrPortFrom(netip.AddrFrom4(s2.Addr4()), 80) // listener, server.
err := s1.AssimilateDHCPResults(&DHCPResults{
@@ -30,12 +30,12 @@ func TestARPLocal(t *testing.T) {
if err != nil {
t.Fatal(err)
}
hw2 := s2.HardwareAddress()
hw2 := s2.HardwareAddr()
err = s1.DialTCP(c1, addr1.Port(), addr2) // addr2 MAC address is unknown and must be resolved by stack.
if err != nil {
t.Fatal(err)
}
err = s2.ListenTCP(c2, addr2.Port())
err = s2.ListenTCP4(c2, addr2.Port())
if err != nil {
t.Fatal(err)
}
+5 -5
View File
@@ -35,8 +35,8 @@ func BenchmarkARPExchange(b *testing.B) {
b.Fatal(err)
}
// Set gateways so ethernet frames are properly addressed.
c1.SetGateway6(c2.HardwareAddress())
c2.SetGateway6(c1.HardwareAddress())
c1.SetGatewayHardwareAddr(c2.HardwareAddr())
c2.SetGatewayHardwareAddr(c1.HardwareAddr())
var buf [frameSize]byte
@@ -104,8 +104,8 @@ func BenchmarkTCPHandshake(b *testing.B) {
if err != nil {
b.Fatal(err)
}
sv.SetGateway6(client.HardwareAddress())
client.SetGateway6(sv.HardwareAddress())
sv.SetGatewayHardwareAddr(client.HardwareAddr())
client.SetGatewayHardwareAddr(sv.HardwareAddr())
buf := make([]byte, MTU*4)
err = clconn.Configure(tcp.ConnConfig{
@@ -130,7 +130,7 @@ func BenchmarkTCPHandshake(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Setup connections.
err = sv.ListenTCP(svconn, svPort)
err = sv.ListenTCP4(svconn, svPort)
if err != nil {
b.Fatal(err)
}
+1 -1
View File
@@ -88,7 +88,7 @@ func TestTCPListener_ConcurrentEcho(t *testing.T) {
t.Fatalf("client %d reset: %v", i, err)
}
// Client gateway points to server.
clientStacks[i].SetGateway6(serverMAC)
clientStacks[i].SetGatewayHardwareAddr(serverMAC)
// Configure client connection buffers.
bufOff := i * tcpBufSize * 2
+1 -1
View File
@@ -35,7 +35,7 @@ func TestDNS_QueryReceivesAnswer(t *testing.T) {
if err != nil {
t.Fatal("client Reset failed:", err)
}
client.SetGateway6(dnsServerMAC)
client.SetGatewayHardwareAddr(dnsServerMAC)
// The IP address we expect to receive from the DNS response.
wantAddr := netip.MustParseAddr("93.184.216.34") // example.com's IP
+3 -3
View File
@@ -22,7 +22,7 @@ func FuzzStackPacketHTTP(f *testing.F) {
var buf [ethernet.MaxFrameLength]byte
s1, s2, c1, c2 := newTCPStacks(f, seed, MTU)
var hdr httpraw.Header
err := s1.ListenTCP(c1, 80)
err := s1.ListenTCP4(c1, 80)
if err != nil {
f.Fatal(err)
}
@@ -88,7 +88,7 @@ func FuzzStackPacketHTTP(f *testing.F) {
if err != nil {
t.Fatal(err)
}
err := s1.ListenTCP(c1, 80)
err := s1.ListenTCP4(c1, 80)
if err != nil {
t.Fatal(err)
}
@@ -364,7 +364,7 @@ func testStackSeeded(t *testing.T, seed1, seed2 int64) {
if err != nil {
t.Fatal(i, err)
}
err = s2.ListenTCP(&tcp2, port2)
err = s2.ListenTCP4(&tcp2, port2)
if err != nil {
t.Fatal(i, err)
}
+2 -2
View File
@@ -33,8 +33,8 @@ func TestStackAsync_ICMPEcho(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sender.SetGateway6(receiver.HardwareAddress())
receiver.SetGateway6(sender.HardwareAddress())
sender.SetGatewayHardwareAddr(receiver.HardwareAddr())
receiver.SetGatewayHardwareAddr(sender.HardwareAddr())
key, err := sender.icmp.PingStart(receiver.Addr4(), tt.pattern, tt.size)
if err != nil {
+4 -4
View File
@@ -40,8 +40,8 @@ func TestStackAsyncListener_SingleConnection(t *testing.T) {
if err != nil {
t.Fatal(err)
}
client.SetGateway6(sv.HardwareAddress())
sv.SetGateway6(client.HardwareAddress())
client.SetGatewayHardwareAddr(sv.HardwareAddr())
sv.SetGatewayHardwareAddr(client.HardwareAddr())
// Create client connection.
var clConn tcp.Conn
@@ -166,7 +166,7 @@ func TestStackAsyncListener_MultiSequentialConn(t *testing.T) {
}
caddr := netip.AddrFrom4([4]byte{10, 0, 0, 1})
chw := [6]byte{0xbe, 0xef, 0, 0, 0, 1}
sv.SetGateway6(chw)
sv.SetGatewayHardwareAddr(chw)
tst := testerFrom(t, MTU)
doRequest := func(caddrp netip.AddrPort, sleep time.Duration, data []byte) {
var client StackAsync
@@ -181,7 +181,7 @@ func TestStackAsyncListener_MultiSequentialConn(t *testing.T) {
if err != nil {
panic(err)
}
client.SetGateway6(sv.HardwareAddress())
client.SetGatewayHardwareAddr(sv.HardwareAddr())
// Create client connection.
var clConn tcp.Conn
err = clConn.Configure(tcp.ConnConfig{
+6 -6
View File
@@ -54,7 +54,7 @@ func TestMDNS_QueryResponse(t *testing.T) {
if err != nil {
t.Fatal("responder reset:", err)
}
responderStack.SetGateway6(querierMAC)
responderStack.SetGatewayHardwareAddr(querierMAC)
var responderClient mdns.Client
err = responderClient.Configure(mdns.ClientConfig{
@@ -65,7 +65,7 @@ func TestMDNS_QueryResponse(t *testing.T) {
if err != nil {
t.Fatal("responder configure:", err)
}
err = responderStack.RegisterUDP(&responderClient, mcastAddr, mdns.Port)
err = responderStack.RegisterUDP4(&responderClient, mcastAddr, mdns.Port)
if err != nil {
t.Fatal("responder register:", err)
}
@@ -84,7 +84,7 @@ func TestMDNS_QueryResponse(t *testing.T) {
if err != nil {
t.Fatal("querier reset:", err)
}
querierStack.SetGateway6(responderMAC)
querierStack.SetGatewayHardwareAddr(responderMAC)
var querierClient mdns.Client
err = querierClient.Configure(mdns.ClientConfig{
@@ -105,7 +105,7 @@ func TestMDNS_QueryResponse(t *testing.T) {
if err != nil {
t.Fatal("start resolve:", err)
}
err = querierStack.RegisterUDP(&querierClient, mcastAddr, mdns.Port)
err = querierStack.RegisterUDP4(&querierClient, mcastAddr, mdns.Port)
if err != nil {
t.Fatal("querier register:", err)
}
@@ -289,7 +289,7 @@ func newMDNSStack(t *testing.T, hostname string, seed int64,
if err != nil {
t.Fatal(hostname, "reset:", err)
}
stack.SetGateway6(gatewayMAC)
stack.SetGatewayHardwareAddr(gatewayMAC)
var client mdns.Client
err = client.Configure(mdnsCfg)
@@ -297,7 +297,7 @@ func newMDNSStack(t *testing.T, hostname string, seed int64,
t.Fatal(hostname, "mdns configure:", err)
}
err = stack.RegisterUDP(&client, mdnsCfg.MulticastAddr, mdns.Port)
err = stack.RegisterUDP4(&client, mdnsCfg.MulticastAddr, mdns.Port)
if err != nil {
t.Fatal(hostname, "register udp:", err)
}
+5 -4
View File
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/tcp"
)
@@ -21,7 +22,7 @@ func TestSubnetTable_PatchEgressMAC_WhenGatewayMAC(t *testing.T) {
var st subnetTable
st.reset(4, 2)
st.subnet = netip.MustParsePrefix("10.0.0.0/24")
st.subnet4 = ipv4.PrefixFrom(clientIP, 24)
// Learn client MAC from a simulated ingress frame (client→server SYN).
ingressFrame := makeMinimalIPv4Frame(serverMAC, clientMAC, clientIP, serverIP)
@@ -62,8 +63,8 @@ func TestStackAsync_ListenerSynAckAddressedToClient(t *testing.T) {
if err != nil {
t.Fatal(err)
}
sv.SetGateway6(routerMAC)
sv.SetSubnet(netip.MustParsePrefix("10.0.0.0/24"))
sv.SetGatewayHardwareAddr(routerMAC)
sv.SetSubnet4(sv.Addr4(), 24)
pool, err := NewTCPPool(TCPPoolConfig{
PoolSize: 1,
@@ -97,7 +98,7 @@ func TestStackAsync_ListenerSynAckAddressedToClient(t *testing.T) {
if err != nil {
t.Fatal(err)
}
client.SetGateway6(serverMAC)
client.SetGatewayHardwareAddr(serverMAC)
var clConn tcp.Conn
if err = clConn.Configure(tcp.ConnConfig{
+7 -7
View File
@@ -201,8 +201,8 @@ func newTCPStacks(t testing.TB, randSeed int64, mtu int) (s1, s2 *StackAsync, c1
if err != nil {
t.Fatal(err)
}
s1.SetGateway6(s2.HardwareAddress())
s2.SetGateway6(s1.HardwareAddress())
s1.SetGatewayHardwareAddr(s2.HardwareAddr())
s2.SetGatewayHardwareAddr(s1.HardwareAddr())
buf := make([]byte, mtu*4)
err = c1.Configure(tcp.ConnConfig{
RxBuf: buf[:mtu],
@@ -253,7 +253,7 @@ func noExchange(source int) tcpExpectExchange {
func (tst *tester) TestTCPSetupAndEstablish(svStack, clStack *StackAsync, svConn, clConn *tcp.Conn, svPort, clPort uint16) {
t := tst.t
// Attach server and client connections to stacks.
err := svStack.ListenTCP(svConn, svPort)
err := svStack.ListenTCP4(svConn, svPort)
if err != nil {
t.Fatal(err)
}
@@ -453,8 +453,8 @@ func (tst *tester) TCPExchange(expect tcpExpectExchange, stack1, stack2 *StackAs
if err != nil {
t.Fatal(err)
}
srcEth := src.HardwareAddress()
dstEth := dst.HardwareAddress()
srcEth := src.HardwareAddr()
dstEth := dst.HardwareAddr()
if !bytes.Equal(srcEth[:], tst.getData(protoEthernet, pcap.FieldClassSrc)) {
t.Errorf("mismatched ethernet src addr %x", tst.getData(protoEthernet, pcap.FieldClassSrc))
}
@@ -511,8 +511,8 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) {
}
tst.buf = tst.buf[:n]
qHw := querying.HardwareAddress()
tgtHw := target.HardwareAddress()
qHw := querying.HardwareAddr()
tgtHw := target.HardwareAddr()
broadcast := ethernet.BroadcastAddr()
qIP := querying.Addr4()
tgtIP := target.Addr4()