mirror of
https://github.com/soypat/lneto.git
synced 2026-08-04 23:17:52 +00:00
ef279863a9
* rewrite tinygo failing test to be more real-time * use channels to scheduler stack * fix flakiness by increasing timeout and move implementation to top of file
419 lines
10 KiB
Go
419 lines
10 KiB
Go
package xnet
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math"
|
|
"net"
|
|
"net/netip"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/tcp"
|
|
"github.com/soypat/lneto/udp"
|
|
)
|
|
|
|
// Socket types
|
|
const (
|
|
sockSTREAM = 0x1
|
|
sockDGRAM = 0x2
|
|
|
|
defaultTCPDialTimeout = 2 * time.Second
|
|
defaultTCPDialRetries = 1
|
|
)
|
|
|
|
type StackGoConfig struct {
|
|
ListenerPoolConfig TCPPoolConfig
|
|
TCPDialTimeout time.Duration
|
|
TCPDialRetries int
|
|
}
|
|
|
|
func (s *StackAsync) StackGo(stackProtoBackoff lneto.BackoffStrategy, cfg StackGoConfig) StackGo {
|
|
if stackProtoBackoff == nil || cfg.ListenerPoolConfig.NewBackoff == nil {
|
|
panic("nil backoff to StackGo")
|
|
}
|
|
return s.StackBlocking(stackProtoBackoff).StackGo(cfg)
|
|
}
|
|
|
|
func (s StackBlocking) StackGo(cfg StackGoConfig) StackGo {
|
|
if cfg.TCPDialRetries <= 0 {
|
|
cfg.TCPDialRetries = defaultTCPDialRetries
|
|
}
|
|
if cfg.TCPDialTimeout <= 0 {
|
|
cfg.TCPDialTimeout = defaultTCPDialTimeout
|
|
}
|
|
|
|
sg := StackGo{
|
|
blk: s,
|
|
plcfg: cfg.ListenerPoolConfig,
|
|
tcpDialTimeout: cfg.TCPDialTimeout,
|
|
tcpDialRetries: cfg.TCPDialRetries,
|
|
}
|
|
return sg
|
|
}
|
|
|
|
type StackGo struct {
|
|
blk StackBlocking
|
|
plcfg TCPPoolConfig
|
|
tcpDialTimeout time.Duration
|
|
tcpDialRetries int
|
|
}
|
|
|
|
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
|
|
}
|
|
var local, remote netip.AddrPort
|
|
if laddr != nil {
|
|
local, err = parseNetAddr(laddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if raddr != nil {
|
|
remote, err = parseNetAddr(raddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return s.SocketNetip(ctx, network, family, sotype, local, remote)
|
|
}
|
|
|
|
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 {
|
|
// 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 {
|
|
laddr = netip.AddrPortFrom(netip.AddrFrom4(s.blk.async.ip4.Addr4()), laddr.Port())
|
|
}
|
|
}
|
|
switch network {
|
|
case "udp", "udp4", "udp6":
|
|
if sotype != sockDGRAM {
|
|
return nil, lneto.ErrUnsupported
|
|
}
|
|
if !isDial {
|
|
// LISTEN UDP: no fixed remote → PacketConn.
|
|
var pc udppktconn
|
|
err = pc.c.Configure(udp.PacketConnConfig{
|
|
TxBuf: make([]byte, s.plcfg.TxBufSize),
|
|
RxBuf: make([]byte, s.plcfg.RxBufSize),
|
|
TxQueueSize: s.plcfg.QueueSize,
|
|
RxQueueSize: s.plcfg.QueueSize,
|
|
RWBackoff: s.plcfg.NewBackoff(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = pc.c.Open(laddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pc.laddr = net.UDPAddr{IP: laddr.Addr().AsSlice(), Port: int(laddr.Port())}
|
|
if isV6 {
|
|
err = s.blk.async.RegisterListenerUDP6(&pc.c)
|
|
} else {
|
|
err = s.blk.async.RegisterListenerUDP(&pc.c)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &pc, nil
|
|
}
|
|
var conn udp.Conn
|
|
err = conn.Configure(udp.ConnConfig{
|
|
TxBuf: make([]byte, s.plcfg.TxBufSize),
|
|
RxBuf: make([]byte, s.plcfg.RxBufSize),
|
|
TxQueueSize: s.plcfg.QueueSize,
|
|
RxQueueSize: s.plcfg.QueueSize,
|
|
RWBackoff: s.plcfg.NewBackoff(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = s.blk.async.DialUDP(&conn, laddr.Port(), raddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uc := udpconn{
|
|
Conn: &conn,
|
|
// TODO: use udpaddr until UDPAddrFromAddrPort added to tinygo.
|
|
// https://github.com/tinygo-org/net/issues/45
|
|
localAddr: udpaddr(laddr),
|
|
raddr: udpaddr(raddr),
|
|
}
|
|
return uc, nil
|
|
case "tcp", "tcp4", "tcp6":
|
|
if sotype != sockSTREAM {
|
|
return nil, lneto.ErrUnsupported
|
|
}
|
|
|
|
if isDial {
|
|
var conn tcp.Conn
|
|
// DIAL TCP: active connection a.k.a TCP Client branch.
|
|
err = conn.Configure(tcp.ConnConfig{
|
|
// TODO(pato): Eventually add UDP configuration. we use TCP for now for simplicity's sake.
|
|
TxBuf: make([]byte, s.plcfg.TxBufSize),
|
|
RxBuf: make([]byte, s.plcfg.RxBufSize),
|
|
TxPacketQueueSize: s.plcfg.QueueSize,
|
|
RWBackoff: s.plcfg.NewBackoff(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = s.blk.StackRetrying().DoDialTCP(&conn, laddr.Port(), raddr, s.tcpDialTimeout, s.tcpDialRetries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var backoffs uint
|
|
for {
|
|
s.blk.backoff(backoffs)
|
|
backoffs++
|
|
state := conn.State()
|
|
if state == tcp.StateEstablished {
|
|
tc := tcpconn{
|
|
Conn: &conn,
|
|
localAddr: net.TCPAddrFromAddrPort(laddr),
|
|
}
|
|
return tc, nil
|
|
} else if state == tcp.StateSynSent || state == tcp.StateSynRcvd || conn.AwaitingSynSend() {
|
|
if err = ctx.Err(); err != nil {
|
|
conn.Abort()
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Unexpected state, abort and terminate connection.
|
|
conn.Abort()
|
|
return errTCPFailedToConnect, nil
|
|
}
|
|
}
|
|
} else {
|
|
// LISTEN TCP: passive connection. fulfills net.Listener interface.
|
|
pool, err := NewTCPPool(s.plcfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var l tcplistener
|
|
l.localAddr = net.TCPAddrFromAddrPort(laddr)
|
|
l.sleep = s.blk._backoff
|
|
err = l.l.Reset(laddr.Port(), pool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if isV6 {
|
|
err = s.blk.async.RegisterListenerTCP6(&l.l)
|
|
} else {
|
|
err = s.blk.async.RegisterListenerTCP(&l.l)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &l, nil
|
|
}
|
|
}
|
|
return nil, lneto.ErrUnsupported
|
|
}
|
|
|
|
// udppktconn implements [net.PacketConn] for [udp.PacketConn].
|
|
type udppktconn struct {
|
|
c udp.PacketConn
|
|
laddr net.UDPAddr
|
|
raddr net.UDPAddr
|
|
}
|
|
|
|
var _ net.PacketConn = (*udppktconn)(nil)
|
|
|
|
func (u *udppktconn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
n, ap, err := u.c.ReadFrom(p)
|
|
if err != nil {
|
|
return n, nil, err
|
|
}
|
|
u.raddr.IP, _ = ap.Addr().AppendBinary(u.raddr.IP[:0])
|
|
u.raddr.Port = int(ap.Port())
|
|
u.raddr.Zone = ""
|
|
return n, &u.raddr, nil
|
|
}
|
|
|
|
func (u *udppktconn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|
uaddr, ok := addr.(*net.UDPAddr)
|
|
ip, ok2 := netip.AddrFromSlice(uaddr.IP)
|
|
if !ok || !ok2 || uaddr.Port <= 0 || uaddr.Port > math.MaxUint16 {
|
|
return 0, lneto.ErrInvalidAddr
|
|
}
|
|
ap := netip.AddrPortFrom(ip, uint16(uaddr.Port))
|
|
return u.c.WriteTo(p, ap)
|
|
}
|
|
|
|
func (u *udppktconn) Close() error { return u.c.Close() }
|
|
|
|
func (u *udppktconn) LocalAddr() net.Addr { return &u.laddr }
|
|
|
|
func (u *udppktconn) SetDeadline(t time.Time) error { return u.c.SetDeadline(t) }
|
|
func (u *udppktconn) SetReadDeadline(t time.Time) error { return u.c.SetReadDeadline(t) }
|
|
func (u *udppktconn) SetWriteDeadline(t time.Time) error { return u.c.SetWriteDeadline(t) }
|
|
func (u *udppktconn) LnetoPacketConn() *udp.PacketConn { return &u.c }
|
|
|
|
func (u *udppktconn) Read(b []byte) (int, error) { n, _, err := u.ReadFrom(b); return n, err }
|
|
func (u *udppktconn) Write(b []byte) (int, error) {
|
|
return 0, errors.New("udp: Write requires WriteTo on a packet conn")
|
|
}
|
|
func (u *udppktconn) RemoteAddr() net.Addr { return nil }
|
|
|
|
type tcplistener struct {
|
|
l tcp.Listener
|
|
closed bool
|
|
sleep lneto.BackoffStrategy
|
|
localAddr net.Addr
|
|
}
|
|
|
|
var _ net.Listener = (*tcplistener)(nil)
|
|
|
|
func (l *tcplistener) LnetoListener() *tcp.Listener {
|
|
return &l.l
|
|
}
|
|
func (l *tcplistener) Addr() net.Addr {
|
|
return l.localAddr
|
|
}
|
|
|
|
func (l *tcplistener) Shutdown() { l.Close() }
|
|
|
|
func (l *tcplistener) Accept() (net.Conn, error) {
|
|
if l.closed {
|
|
return nil, net.ErrClosed
|
|
}
|
|
var backoffs uint
|
|
for {
|
|
if l.closed {
|
|
return nil, net.ErrClosed
|
|
}
|
|
n := l.l.NumberOfReadyToAccept()
|
|
if n == 0 {
|
|
backoff(l.sleep, backoffs)
|
|
backoffs++
|
|
continue
|
|
}
|
|
backoffs = 0
|
|
c, _, err := l.l.TryAccept()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cc := tcpconn{
|
|
Conn: c,
|
|
localAddr: l.localAddr,
|
|
}
|
|
return cc, nil
|
|
}
|
|
}
|
|
|
|
func (l *tcplistener) Close() error {
|
|
if l.closed {
|
|
return net.ErrClosed
|
|
}
|
|
err := l.l.Close()
|
|
l.closed = true
|
|
return err
|
|
}
|
|
|
|
type tcpconn struct {
|
|
*tcp.Conn
|
|
localAddr net.Addr
|
|
}
|
|
|
|
var _ net.Conn = tcpconn{}
|
|
|
|
func (c tcpconn) LnetoConn() *tcp.Conn {
|
|
return c.Conn
|
|
}
|
|
|
|
func (c tcpconn) CloseWrite() error { return c.Conn.Close() }
|
|
|
|
func (c tcpconn) LocalAddr() net.Addr {
|
|
return c.localAddr
|
|
}
|
|
|
|
func (c tcpconn) RemoteAddr() net.Addr {
|
|
return &net.TCPAddr{
|
|
IP: c.Conn.RemoteAddr(),
|
|
Port: int(c.Conn.RemotePort()),
|
|
}
|
|
}
|
|
|
|
type udpconn struct {
|
|
*udp.Conn
|
|
localAddr net.Addr
|
|
raddr net.Addr
|
|
}
|
|
|
|
var _ net.Conn = udpconn{}
|
|
|
|
func (c udpconn) LocalAddr() net.Addr { return c.localAddr }
|
|
func (c udpconn) RemoteAddr() net.Addr { return c.raddr }
|
|
func (c udpconn) LnetoConn() *udp.Conn { return c.Conn }
|
|
|
|
func (c udpconn) ReadFrom(b []byte) (int, net.Addr, error) {
|
|
n, err := c.Conn.Read(b)
|
|
return n, c.raddr, err
|
|
}
|
|
|
|
func (c udpconn) WriteTo(b []byte, _ net.Addr) (int, error) {
|
|
return c.Conn.Write(b) // connected UDP: always writes to dialed remote
|
|
}
|
|
func udpaddr(addr netip.AddrPort) net.Addr {
|
|
return &net.UDPAddr{
|
|
IP: addr.Addr().AsSlice(),
|
|
Zone: addr.Addr().Zone(),
|
|
Port: int(addr.Port()),
|
|
}
|
|
}
|
|
|
|
// parseNetAddr converts a [net.Addr] to a [netip.AddrPort]. A nil or empty IP
|
|
// (e.g. ":22" from a listen address with no host) is treated as 0.0.0.0 so
|
|
// that SocketNetip's IPv4Unspecified check can then fill in the stack's
|
|
// configured address. Without this, netip.ParseAddrPort returns "no IP".
|
|
func parseNetAddr(addr net.Addr) (netip.AddrPort, error) {
|
|
var ip net.IP
|
|
var port int
|
|
switch a := addr.(type) {
|
|
case *net.TCPAddr:
|
|
ip, port = a.IP, a.Port
|
|
case *net.UDPAddr:
|
|
ip, port = a.IP, a.Port
|
|
default:
|
|
return netip.AddrPort{}, lneto.ErrUnsupported
|
|
}
|
|
if len(ip) == 0 {
|
|
ip = net.IP{0, 0, 0, 0}
|
|
}
|
|
nip, ok := netip.AddrFromSlice(ip)
|
|
if !ok {
|
|
return netip.AddrPort{}, lneto.ErrInvalidAddr
|
|
}
|
|
return netip.AddrPortFrom(nip.Unmap(), uint16(port)), nil
|
|
}
|