diff --git a/README.md b/README.md index dc53e89..261c6d1 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/tcp/conn.go b/tcp/conn.go index c77ec11..190fedd 100644 --- a/tcp/conn.go +++ b/tcp/conn.go @@ -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 { diff --git a/x/xnet/stack-async.go b/x/xnet/stack-async.go index 5d3cb60..d9e61e0 100644 --- a/x/xnet/stack-async.go +++ b/x/xnet/stack-async.go @@ -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, }, }, diff --git a/x/xnet/stack-blocking.go b/x/xnet/stack-blocking.go index ee54a9a..314b3be 100644 --- a/x/xnet/stack-blocking.go +++ b/x/xnet/stack-blocking.go @@ -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 diff --git a/x/xnet/stack-go.go b/x/xnet/stack-go.go index cb2ca63..34861ae 100644 --- a/x/xnet/stack-go.go +++ b/x/xnet/stack-go.go @@ -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 } diff --git a/x/xnet/stack6.go b/x/xnet/stack6.go index 60da6ff..043c4d7 100644 --- a/x/xnet/stack6.go +++ b/x/xnet/stack6.go @@ -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) }