More IPv6 support and race condition fixes (#123)

* changes in preparation of wireguard impl

* suggestions by @MDr164
This commit is contained in:
Pat Whittingslow
2026-06-17 15:10:25 -03:00
committed by GitHub
parent 813b7b5e57
commit 60098ad8d0
6 changed files with 130 additions and 36 deletions
+2 -2
View File
@@ -35,8 +35,8 @@ go run ./examples/gen/gen-binary-bench # generate table
| Program | Extra Protocols | Packet capture printing | amd64 Go | WASM Go | amd64 TinyGo | WASM TinyGo | Pico TinyGo |
|---|:---:|:---:|---|---|---|---|---|
| [Lneto MWE](./examples/min-working-example/) | DNS,NTP,DHCP | ✅ | 3.8MB | 4.3MB | 1.6MB | 1.2MB | 185kB |
| [Gvisor MWE w/ go-net](./examples/_import_examples/gvisor-mwe/) | None | ❌ | 6.6MB | 7.4MB | DNC | DNC | DNC |
| [Lneto MWE](./examples/min-working-example/) | DNS,NTP,DHCP | ✅ | 3.9MB | 4.4MB | 1.6MB | 1.2MB | 189kB |
| [Gvisor MWE w/ go-net](./examples/_import_examples/gvisor-mwe/) | None | ❌ | 6.6MB | 7.5MB | DNC | DNC | DNC |
## `xcurl` example
You may try lneto out on linux with the [xcurl example](./examples/xcurl/) which gets an HTTP page by doing all the low-level networking part using absolutely no standard library.
+10
View File
@@ -103,6 +103,16 @@ func (conn *Conn) State() State {
return conn.h.State()
}
// AwaitingSynSend reports whether the connection has been opened actively but has
// not yet emitted its SYN. It locks the connection so it is safe to poll from a
// goroutine other than the one driving the stack (unlike reaching through
// [Conn.InternalHandler]).
func (conn *Conn) AwaitingSynSend() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
return conn.h.AwaitingSynSend()
}
// BufferedInput returns the number of bytes in the socket's receive(input) buffer
// and available to read via a [Conn.Read] call.
func (conn *Conn) BufferedInput() int {
+59 -15
View File
@@ -364,18 +364,18 @@ func (s *StackAsync) Addr4() [4]byte {
}
func (s *StackAsync) SetAddr6(addr [16]byte) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.ipv6enabled {
s.mu.Lock()
defer s.mu.Unlock()
return s.stack6.SetAddr6(addr)
}
return lneto.ErrUnsupported
}
func (s *StackAsync) Addr6() [16]byte {
s.mu.Lock()
defer s.mu.Unlock()
if s.ipv6enabled {
s.mu.Lock()
defer s.mu.Unlock()
return s.stack6.Addr6()
}
return [16]byte{}
@@ -412,6 +412,13 @@ func (s *StackAsync) GatewayHardwareAddr() [6]byte {
return s.link.Gateway6()
}
func (s *StackAsync) IsIPv6Enabled() bool {
s.mu.Lock()
enabled := s.ipv6enabled
s.mu.Unlock()
return enabled
}
// 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) {
@@ -437,25 +444,32 @@ func (s *StackAsync) EnableICMP(enabled bool) (err error) {
func (s *StackAsync) DialUDP(conn *udp.Conn, localPort uint16, addrp netip.AddrPort) (err error) {
addr := addrp.Addr()
if addr.Is4() {
err = s.DialUDP4(conn, localPort, addrp.Addr().As4(), addrp.Port())
return 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
// stack6 is guarded by s.mu (the single stack lock), just like the IPv4
// path locks inside DialUDP4. Hold it here so the port-handler mutation is
// serialized against the Ingress/Egress demux.
s.mu.Lock()
defer s.mu.Unlock()
return s.stack6.DialUDP6(conn, localPort, addr.As16(), addrp.Port())
}
return err
return lneto.ErrInvalidAddr
}
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())
return 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
// stack6 is guarded by s.mu (the single stack lock), just like the IPv4
// path locks inside DialTCP4. Hold it here so the port-handler mutation is
// serialized against the Ingress/Egress demux. Use the unlocked prand32
// since we already hold s.mu (Prand32 would deadlock).
s.mu.Lock()
defer s.mu.Unlock()
return s.stack6.DialTCP6(conn, localPort, addr.As16(), addrp.Port(), tcp.Value(s.prand32()))
}
return err
return lneto.ErrInvalidAddr
}
func (s *StackAsync) DialUDP4(conn *udp.Conn, localPort uint16, raddr [4]byte, rport uint16) (err error) {
@@ -540,14 +554,44 @@ func (s *StackAsync) RegisterListenerUDP(pktconn *udp.PacketConn) (err error) {
return s.udps.RegisterMACFiltered(pktconn, nil)
}
func (s *StackAsync) RegisterListenerTCP6(listener *tcp.Listener) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.ipv6enabled {
return lneto.ErrUnsupported
}
return s.stack6.RegisterListenerTCP6(listener)
}
func (s *StackAsync) RegisterListenerUDP6(pktconn *udp.PacketConn) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.ipv6enabled {
return lneto.ErrUnsupported
}
return s.stack6.RegisterListenerUDP6(pktconn)
}
var errNoDNSServer = errors.New("no DNS server- did DHCP complete? You can set a predetermined DNS server in Stack configuration")
var errDNSv6Transport = errors.New("DNS query over IPv6 transport not supported; configure an IPv4 DNS server")
func (s *StackAsync) StartLookupIP(host string) error {
return s.StartLookupIPType(host, dns.TypeA)
}
// StartLookupIPType begins resolving host for the given record type (e.g. dns.TypeA
// or dns.TypeAAAA). The DNS query is always carried over IPv4 to the configured DNS
// server; resolving over an IPv6 DNS transport is not yet supported.
func (s *StackAsync) StartLookupIPType(host string, qtype dns.Type) error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.dnssv.IsValid() {
return errNoDNSServer
}
if !s.dnssv.Is4() {
return errDNSv6Transport
}
name, err := dns.NewName(host)
if err != nil {
return err
@@ -561,7 +605,7 @@ func (s *StackAsync) StartLookupIP(host string) error {
Questions: []dns.Question{
{
Name: name,
Type: dns.TypeA,
Type: qtype,
Class: dns.ClassINET,
},
},
+9 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/soypat/lneto"
"github.com/soypat/lneto/dhcp/dhcpv4"
"github.com/soypat/lneto/dns"
"github.com/soypat/lneto/tcp"
)
@@ -147,7 +148,13 @@ func (s StackBlocking) DoResolveHardwareAddress6(addr netip.Addr, timeout time.D
}
func (s StackBlocking) DoLookupIP(host string, timeout time.Duration) (addrs []netip.Addr, err error) {
err = s.async.StartLookupIP(host)
return s.DoLookupIPType(host, timeout, dns.TypeA)
}
// DoLookupIPType resolves host for the given record type (dns.TypeA or dns.TypeAAAA),
// blocking until a response arrives or the timeout elapses.
func (s StackBlocking) DoLookupIPType(host string, timeout time.Duration, qtype dns.Type) (addrs []netip.Addr, err error) {
err = s.async.StartLookupIPType(host, qtype)
if err != nil {
return nil, err
}
@@ -180,7 +187,7 @@ func (s StackBlocking) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.A
state := conn.State()
if state == tcp.StateEstablished {
return nil
} else if state == tcp.StateSynSent || state == tcp.StateSynRcvd || conn.InternalHandler().AwaitingSynSend() {
} else if state == tcp.StateSynSent || state == tcp.StateSynRcvd || conn.AwaitingSynSend() {
if err = s.checkDeadline(deadline); err != nil {
conn.Abort()
return err
+36 -17
View File
@@ -47,6 +47,10 @@ type StackGo struct {
func (s StackGo) Socket(ctx context.Context, network string, family, sotype int, laddr, raddr net.Addr) (c any, err error) {
switch family {
case syscall.AF_INET:
case syscall.AF_INET6:
if !s.blk.async.IsIPv6Enabled() {
return nil, errors.ErrUnsupported
}
default:
return nil, lneto.ErrUnsupported
}
@@ -67,31 +71,38 @@ func (s StackGo) Socket(ctx context.Context, network string, family, sotype int,
}
func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype int, laddr, raddr netip.AddrPort) (c any, err error) {
var isV6 bool
switch family {
case syscall.AF_INET:
case syscall.AF_INET6:
if !s.blk.async.IsIPv6Enabled() {
return nil, errors.ErrUnsupported
}
isV6 = true
default:
return nil, lneto.ErrUnsupported
}
// A dial targets a specified (non-unspecified) remote; a listen does not.
isDial := raddr.IsValid() && !raddr.Addr().IsUnspecified()
if laddr.Port() == 0 {
if raddr.IsValid() && raddr.Addr() != netip.IPv4Unspecified() {
// Outbound (dial) connection: auto-assign ephemeral port.
laddr = netip.AddrPortFrom(laddr.Addr(), uint16(49152+s.blk.async.Prand32()%16384))
// Auto-assign an ephemeral port for both outbound dials and for listeners
// that did not request a fixed port.
laddr = netip.AddrPortFrom(laddr.Addr(), uint16(49152+s.blk.async.Prand32()%16384))
}
if laddr.Addr().IsUnspecified() {
// Fill in the stack's configured address for the requested family.
if isV6 {
laddr = netip.AddrPortFrom(netip.AddrFrom16(s.blk.async.Addr6()), laddr.Port())
} else {
return nil, lneto.ErrZeroSource
laddr = netip.AddrPortFrom(netip.AddrFrom4(s.blk.async.ip4.Addr4()), laddr.Port())
}
}
if laddr.Addr() == netip.IPv4Unspecified() {
// Specify address.
laddr = netip.AddrPortFrom(netip.AddrFrom4(s.blk.async.ip4.Addr4()), laddr.Port())
} else if laddr.Addr().Is6() {
return nil, lneto.ErrUnsupported
}
switch network {
case "udp", "udp4":
case "udp", "udp4", "udp6":
if sotype != sockDGRAM {
return nil, lneto.ErrUnsupported
}
if !raddr.IsValid() || raddr.Addr() == netip.IPv4Unspecified() {
if !isDial {
// LISTEN UDP: no fixed remote → PacketConn.
var pc udppktconn
err = pc.c.Configure(udp.PacketConnConfig{
@@ -109,7 +120,11 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype
return nil, err
}
pc.laddr = net.UDPAddr{IP: laddr.Addr().AsSlice(), Port: int(laddr.Port())}
err = s.blk.async.RegisterListenerUDP(&pc.c)
if isV6 {
err = s.blk.async.RegisterListenerUDP6(&pc.c)
} else {
err = s.blk.async.RegisterListenerUDP(&pc.c)
}
if err != nil {
return nil, err
}
@@ -138,12 +153,12 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype
raddr: udpaddr(raddr),
}
return uc, nil
case "tcp", "tcp4":
case "tcp", "tcp4", "tcp6":
if sotype != sockSTREAM {
return nil, lneto.ErrUnsupported
}
if raddr.IsValid() && raddr.Addr() != netip.IPv4Unspecified() {
if isDial {
var conn tcp.Conn
// DIAL TCP: active connection a.k.a TCP Client branch.
err = conn.Configure(tcp.ConnConfig{
@@ -171,7 +186,7 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype
localAddr: net.TCPAddrFromAddrPort(laddr),
}
return tc, nil
} else if state == tcp.StateSynSent || state == tcp.StateSynRcvd || conn.InternalHandler().AwaitingSynSend() {
} else if state == tcp.StateSynSent || state == tcp.StateSynRcvd || conn.AwaitingSynSend() {
if err = ctx.Err(); err != nil {
conn.Abort()
return nil, err
@@ -195,7 +210,11 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype
if err != nil {
return nil, err
}
err = s.blk.async.RegisterListenerTCP(&l.l)
if isV6 {
err = s.blk.async.RegisterListenerTCP6(&l.l)
} else {
err = s.blk.async.RegisterListenerTCP(&l.l)
}
if err != nil {
return nil, err
}
+14
View File
@@ -26,6 +26,8 @@ type Stack6 interface {
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
RegisterListenerTCP6(listener *tcp.Listener) error
RegisterListenerUDP6(pktconn *udp.PacketConn) error
IngressIPv6(ipframe []byte) error
EgressIPv6(ipframe []byte) (int, error)
IPv6Stack() lneto.StackNode
@@ -119,6 +121,18 @@ func (s *stack6) EnableICMP6(enabled bool) (err error) {
return err
}
// RegisterListenerTCP6 registers a passive TCP listener on the IPv6 stack so it
// receives inbound IPv6 segments. Mirrors StackAsync.RegisterListenerTCP for IPv4.
func (s *stack6) RegisterListenerTCP6(listener *tcp.Listener) error {
return s.tcps6.RegisterMACFiltered(listener, nil)
}
// RegisterListenerUDP6 registers a UDP packet connection on the IPv6 stack so it
// receives inbound IPv6 datagrams. Mirrors StackAsync.RegisterListenerUDP for IPv4.
func (s *stack6) RegisterListenerUDP6(pktconn *udp.PacketConn) error {
return s.udps6.RegisterMACFiltered(pktconn, nil)
}
func (s *stack6) IngressIPv6(ipFrame []byte) error {
return s.ip6.Demux(ipFrame, 0)
}