diff --git a/netdev.go b/netdev.go index c228f20..f824806 100644 --- a/netdev.go +++ b/netdev.go @@ -10,6 +10,7 @@ import ( const ( _AF_INET = 0x2 + _AF_INET6 = 0xA _SOCK_STREAM = 0x1 _SOCK_DGRAM = 0x2 _SOL_SOCKET = 0x1 diff --git a/netdev_wasip2.go b/netdev_wasip2.go new file mode 100644 index 0000000..a83530a --- /dev/null +++ b/netdev_wasip2.go @@ -0,0 +1,230 @@ +//go:build wasip2 + +// L3/L4 network/transport layer implementation using WASI preview 2 sockets + +package net + +import ( + "errors" + "fmt" + "net/netip" + "time" + + instancenetwork "internal/wasi/sockets/v0.2.0/instance-network" + ipnamelookup "internal/wasi/sockets/v0.2.0/ip-name-lookup" + "internal/wasi/sockets/v0.2.0/network" +) + +var errInvalidSocketFD = errors.New("wasip2: invalid socket fd") + +func tinygoToWasiAddr(ip netip.AddrPort) network.IPSocketAddress { + if ip.Addr().Is4() { + return network.IPSocketAddressIPv4(network.IPv4SocketAddress{ + Port: ip.Port(), + Address: ip.Addr().As4(), + }) + } + + if ip.Addr().Is6() { + as16 := ip.Addr().As16() + var as8uint16 [8]uint16 + for i := 0; i < 8; i++ { + as8uint16[i] = uint16(as16[i*2])<<8 | uint16(as16[i*2+1]) + } + return network.IPSocketAddressIPv6(network.IPv6SocketAddress{ + Port: ip.Port(), + Address: as8uint16, + }) + } + + return network.IPSocketAddressIPv4(network.IPv4SocketAddress{ + Port: ip.Port(), + }) +} + +func wasiAddrToTinygo(addr network.IPSocketAddress) netip.AddrPort { + if addr4 := addr.IPv4(); addr4 != nil { + return netip.AddrPortFrom(netip.AddrFrom4(addr4.Address), addr4.Port) + } + + if addr6 := addr.IPv6(); addr6 != nil { + var as16 [16]byte + for i := 0; i < 8; i++ { + as16[i*2] = byte(addr6.Address[i] >> 8) + as16[i*2+1] = byte(addr6.Address[i] & 0xFF) + } + return netip.AddrPortFrom(netip.AddrFrom16(as16), addr6.Port) + } + + return netip.AddrPort{} +} + +type wasip2Socket interface { + Recv(buf []byte, flags int, deadline time.Time) (int, error) + Send(buf []byte, flags int, deadline time.Time) (int, error) + Close() error + Listen(backlog int) error + Bind(globalNetwork instancenetwork.Network, ip network.IPSocketAddress) error + Connect(globalNetwork instancenetwork.Network, host string, ip network.IPSocketAddress) error + Accept() (wasip2Socket, *network.IPSocketAddress, error) +} + +// wasip2Netdev is a netdev that uses WASI preview 2 sockets +type wasip2Netdev struct { + fds map[int]wasip2Socket + nextFd int + net instancenetwork.Network +} + +func init() { + useNetdev(&wasip2Netdev{ + fds: make(map[int]wasip2Socket), + nextFd: 0, + net: instancenetwork.InstanceNetwork(), + }) +} + +func (n *wasip2Netdev) GetHostByName(name string) (netip.Addr, error) { + res := ipnamelookup.ResolveAddresses(n.net, name) + + if res.IsErr() { + return netip.Addr{}, fmt.Errorf("failed to resolve address: %s", res.Err().String()) + } + + stream := res.OK() + pollable := stream.Subscribe() + + for { + pollable.Block() + res := stream.ResolveNextAddress() + + if res.IsErr() { + return netip.Addr{}, fmt.Errorf("failed to get resolved address: %s", res.Err().String()) + } + + if res.OK().None() { + return netip.Addr{}, errors.New("no addresses found") + } + + // TODO: handle IPv6 + if addr4 := res.OK().Some().IPv4(); addr4 != nil { + return netip.AddrFrom4(*addr4), nil + } + } +} + +func (n *wasip2Netdev) Addr() (netip.Addr, error) { + return netip.Addr{}, errors.New("wasip2 TODO Addr") +} + +func (n *wasip2Netdev) getNextFD() int { + n.nextFd++ + return n.nextFd - 1 +} + +func (n *wasip2Netdev) Socket(domain int, stype int, protocol int) (sockfd int, _ error) { + af := network.IPAddressFamilyIPv4 + if domain == _AF_INET6 { + af = network.IPAddressFamilyIPv6 + } + + var sock wasip2Socket + var err error + + switch stype { + case _SOCK_STREAM: + sock, err = createTCPSocket(af) + if err != nil { + return -1, err + } + case _SOCK_DGRAM: + sock, err = createUDPSocket(af) + if err != nil { + return -1, err + } + default: + return -1, fmt.Errorf("wasip2: unsupported socket type %d", stype) + } + + fd := n.getNextFD() + n.fds[fd] = sock + + return fd, nil +} + +func (n *wasip2Netdev) Bind(sockfd int, ip netip.AddrPort) error { + sock, ok := n.fds[sockfd] + if !ok { + return errInvalidSocketFD + } + + return sock.Bind(n.net, tinygoToWasiAddr(ip)) +} + +func (n *wasip2Netdev) Connect(sockfd int, host string, ip netip.AddrPort) error { + sock, ok := n.fds[sockfd] + if !ok { + return errInvalidSocketFD + } + + return sock.Connect(n.net, host, tinygoToWasiAddr(ip)) +} + +func (n *wasip2Netdev) Listen(sockfd int, backlog int) error { + sock, ok := n.fds[sockfd] + if !ok { + return errInvalidSocketFD + } + + return sock.Listen(backlog) +} + +func (n *wasip2Netdev) Accept(sockfd int) (int, netip.AddrPort, error) { + sock, ok := n.fds[sockfd] + if !ok { + return -1, netip.AddrPort{}, errInvalidSocketFD + } + + newSock, raddr, err := sock.Accept() + if err != nil { + return -1, netip.AddrPort{}, fmt.Errorf("failed to accept connection: %s", err.Error()) + } + + fd := n.getNextFD() + n.fds[fd] = newSock + + return fd, wasiAddrToTinygo(*raddr), nil +} + +func (n *wasip2Netdev) Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + sock, ok := n.fds[sockfd] + if !ok { + return -1, errInvalidSocketFD + } + + return sock.Send(buf, flags, deadline) +} + +func (n *wasip2Netdev) Recv(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + sock, ok := n.fds[sockfd] + if !ok { + return -1, errInvalidSocketFD + } + + return sock.Recv(buf, flags, deadline) +} + +func (n *wasip2Netdev) Close(sockfd int) error { + sock, ok := n.fds[sockfd] + if !ok { + return errInvalidSocketFD + } + + delete(n.fds, sockfd) + + return sock.Close() +} + +func (n *wasip2Netdev) SetSockOpt(sockfd int, level int, opt int, value interface{}) error { + return errors.New("wasip2 TODO set socket option") +} diff --git a/tcp_wasip2.go b/tcp_wasip2.go new file mode 100644 index 0000000..e85ed22 --- /dev/null +++ b/tcp_wasip2.go @@ -0,0 +1,190 @@ +//go:build wasip2 + +// WASI preview 2 TCP + +package net + +import ( + "fmt" + "time" + + "internal/cm" + "internal/wasi/io/v0.2.0/streams" + instancenetwork "internal/wasi/sockets/v0.2.0/instance-network" + "internal/wasi/sockets/v0.2.0/network" + "internal/wasi/sockets/v0.2.0/tcp" + tcpcreatesocket "internal/wasi/sockets/v0.2.0/tcp-create-socket" +) + +type wasip2TcpSocket struct { + tcpcreatesocket.TCPSocket + tcp.Pollable + *streams.InputStream + *streams.OutputStream +} + +func createTCPSocket(af network.IPAddressFamily) (wasip2Socket, error) { + res := tcpcreatesocket.CreateTCPSocket(af) + if res.IsErr() { + return nil, fmt.Errorf("failed to create TCP socket: %s", res.Err().String()) + } + + sock := res.OK() + return &wasip2TcpSocket{ + TCPSocket: *sock, + Pollable: sock.Subscribe(), + }, nil +} + +func (s *wasip2TcpSocket) Bind(globalNetwork instancenetwork.Network, addr network.IPSocketAddress) error { + res := s.StartBind(globalNetwork, addr) + if res.IsErr() { + return fmt.Errorf("failed to start binding socket: %s", res.Err().String()) + } + + res = s.FinishBind() + if res.IsErr() { + return fmt.Errorf("failed to finish binding socket: %s", res.Err().String()) + } + + return nil +} + +func (s *wasip2TcpSocket) Listen(backlog int) error { + res := s.StartListen() + if res.IsErr() { + return fmt.Errorf("failed to start listening on socket: %s", res.Err().String()) + } + + res = s.FinishListen() + if res.IsErr() { + return fmt.Errorf("failed to finish listening on socket: %s", res.Err().String()) + } + + return nil +} + +func (s *wasip2TcpSocket) Accept() (wasip2Socket, *network.IPSocketAddress, error) { + var clientSocket *tcpcreatesocket.TCPSocket + var inStream *streams.InputStream + var outStream *streams.OutputStream + + for { + res := s.TCPSocket.Accept() + if res.IsOK() { + clientSocket, inStream, outStream = &res.OK().F0, &res.OK().F1, &res.OK().F2 + break + } + + if *res.Err() == network.ErrorCodeWouldBlock { + // FIXME: a proper way is to use Pollable.Block() + // But this seems to cause the single threaded runtime to block indefinitely + for { + if s.Pollable.Ready() { + break + } + + // HACK: Make sure to yield the execution to other goroutines + time.Sleep(100 * time.Millisecond) + } + continue + } + + return nil, nil, fmt.Errorf("failed to accept connection: %s", res.Err().String()) + + } + + raddrRes := clientSocket.RemoteAddress() + if raddrRes.IsErr() { + return nil, nil, fmt.Errorf("failed to get remote address: %s", raddrRes.Err().String()) + } + + return &wasip2TcpSocket{ + TCPSocket: *clientSocket, + Pollable: clientSocket.Subscribe(), + InputStream: inStream, + OutputStream: outStream, + }, raddrRes.OK(), nil +} + +func (s *wasip2TcpSocket) Connect(globalNetwork instancenetwork.Network, host string, ip network.IPSocketAddress) error { + res := s.StartConnect(globalNetwork, ip) + if res.IsErr() { + return fmt.Errorf("failed to start connecting socket: %s", res.Err().String()) + } + + for { + connRes := s.FinishConnect() + if connRes.IsOK() { + s.InputStream, s.OutputStream = &connRes.OK().F0, &connRes.OK().F1 + return nil + } + + if *connRes.Err() == network.ErrorCodeWouldBlock { + s.Block() + continue + } + + return fmt.Errorf("failed to finish connecting socket: %s", connRes.Err().String()) + } +} + +func (c *wasip2TcpSocket) Send(buf []byte, flags int, deadline time.Time) (int, error) { + if flags != 0 { + return -1, fmt.Errorf("wasip2 TCP send flags TODO:", flags) + } + + if c.OutputStream == nil { + return -1, fmt.Errorf("send called on a socket without open streams") + } + + cw, err, iserr := c.CheckWrite().Result() + if iserr { + return -1, fmt.Errorf("failed to do check-write on the output stream: %s", err.String()) + } + if cw < uint64(len(buf)) { + return -1, fmt.Errorf("failed to send: writeable %d < length %d", cw, len(buf)) + } + + res := c.Write(cm.ToList([]uint8(buf))) + if res.IsErr() { + return -1, fmt.Errorf("failed to write to output stream: %s", res.Err().String()) + } + + return len(buf), nil +} + +func (c *wasip2TcpSocket) Recv(buf []byte, flags int, deadline time.Time) (int, error) { + if flags != 0 { + return -1, fmt.Errorf("wasip2 TCP recv flags TODO:", flags) + } + + if c.InputStream == nil { + return -1, fmt.Errorf("recv called on a socket without open streams") + } + + res := c.BlockingRead(uint64(len(buf))) + if res.IsErr() { + return -1, fmt.Errorf("failed to read from input stream: %s", res.Err().String()) + } + + return copy(buf, res.OK().Slice()), nil +} + +func (c *wasip2TcpSocket) Close() error { + res := c.TCPSocket.Shutdown(tcp.ShutdownTypeBoth) + if res.IsErr() { + return fmt.Errorf("failed to shutdown client socket: %s", res.Err().String()) + } + + if c.InputStream != nil { + c.InputStream.ResourceDrop() + } + if c.OutputStream != nil { + c.OutputStream.ResourceDrop() + } + c.Pollable.ResourceDrop() + c.TCPSocket.ResourceDrop() + + return nil +} diff --git a/udp_wasip2.go b/udp_wasip2.go new file mode 100644 index 0000000..af78f51 --- /dev/null +++ b/udp_wasip2.go @@ -0,0 +1,135 @@ +//go:build wasip2 + +// WASI preview 2 UDP + +package net + +import ( + "fmt" + "time" + + "internal/cm" + instancenetwork "internal/wasi/sockets/v0.2.0/instance-network" + "internal/wasi/sockets/v0.2.0/network" + "internal/wasi/sockets/v0.2.0/udp" + udpcreatesocket "internal/wasi/sockets/v0.2.0/udp-create-socket" +) + +type wasip2UdpSocket struct { + udpcreatesocket.UDPSocket + udp.Pollable + *udp.IncomingDatagramStream + *udp.OutgoingDatagramStream +} + +func createUDPSocket(af network.IPAddressFamily) (wasip2Socket, error) { + res := udpcreatesocket.CreateUDPSocket(af) + if res.IsErr() { + return nil, fmt.Errorf("failed to create UDP socket: %s", res.Err().String()) + } + + sock := res.OK() + return &wasip2UdpSocket{ + UDPSocket: *sock, + Pollable: sock.Subscribe(), + }, nil +} + +func (s *wasip2UdpSocket) Bind(globalNetwork instancenetwork.Network, addr network.IPSocketAddress) error { + res := s.StartBind(globalNetwork, addr) + if res.IsErr() { + return fmt.Errorf("failed to start binding socket: %s", res.Err().String()) + } + + res = s.FinishBind() + if res.IsErr() { + return fmt.Errorf("failed to finish binding socket: %s", res.Err().String()) + } + + return nil +} + +func (s *wasip2UdpSocket) Listen(backlog int) error { + fmt.Println("wasip2 UDP listen TODO:", backlog) /// + return nil +} + +func (s *wasip2UdpSocket) Accept() (wasip2Socket, *network.IPSocketAddress, error) { + return nil, nil, fmt.Errorf("wasip2 UDP sockets do not support Accept") +} + +func (s *wasip2UdpSocket) Connect(globalNetwork instancenetwork.Network, host string, ip network.IPSocketAddress) error { + res := s.UDPSocket.Stream(cm.Some(ip)) + + if res.IsErr() { + return fmt.Errorf("failed to connect UDP socket: %s", res.Err().String()) + } + + s.IncomingDatagramStream, s.OutgoingDatagramStream = &res.OK().F0, &res.OK().F1 + + return nil +} + +func (c wasip2UdpSocket) Send(buf []byte, flags int, deadline time.Time) (int, error) { + if flags != 0 { + fmt.Println("wasip2 UDP send flags TODO:", flags) /// + } + + if c.OutgoingDatagramStream == nil { + return -1, fmt.Errorf("send called on a socket without open streams") + } + + res := c.OutgoingDatagramStream.CheckSend() + if res.IsErr() { + return -1, fmt.Errorf("failed to write to output stream: %s", res.Err().String()) + } + + if *res.OK() == 0 { + c.Block() + } + + a := []udp.OutgoingDatagram{ + { + Data: cm.NewList(&buf[0], len(buf)), + // RemoteAddress: cm.Some(c.raddr), + }, + } + + c.OutgoingDatagramStream.Send(cm.NewList(&a[0], 1)) + + return len(buf), nil +} + +func (c wasip2UdpSocket) Recv(buf []byte, flags int, deadline time.Time) (int, error) { + if flags != 0 { + fmt.Println("wasip2 UDP recv flags TODO:", flags) /// + } + + if c.IncomingDatagramStream == nil { + return -1, fmt.Errorf("recv called on a socket without open streams") + } + + res := c.Receive(1) + if res.IsErr() { + return -1, fmt.Errorf("failed to read from input stream: %s", res.Err().String()) + } + + if res.OK().Len() != 1 { + return -1, fmt.Errorf("expected 1 datagram, got %d", res.OK().Len()) + } + + return copy(buf, res.OK().Data().Data.Slice()), nil +} + +func (c wasip2UdpSocket) Close() error { + if c.IncomingDatagramStream != nil { + c.IncomingDatagramStream.ResourceDrop() + } + if c.OutgoingDatagramStream != nil { + c.OutgoingDatagramStream.ResourceDrop() + } + c.Pollable.ResourceDrop() + c.UDPSocket.ResourceDrop() + + return nil +}