net: add UDP listen, resolver, DNSError, and TCP listener APIs for wasm

TinyGo's net package was missing several symbols that upstream Go's
js/wasm net declares, blocking builds that pull pion/transport, pion/dtls,
and similar (via netbird's WASM client). Add them, backed by the existing
netdev abstraction so they compile for all targets and no-op cleanly under
nopNetdev (matching Go's js runtime behavior):

- DNSError type (dnserror.go), copied from Go 1.26.2.
- Resolver + DefaultResolver with LookupHost/LookupIP/LookupIPAddr/
  LookupNetIP/LookupPort; LookupHost/LookupIP package funcs; Dialer.Resolver
  field for API compatibility.
- UDPConn: ListenUDP, ReadFromUDP(AddrPort), WriteToUDP(AddrPort),
  SetReadBuffer, SetWriteBuffer.
- TCPConn: CloseRead, SetNoDelay, SetReadBuffer, SetWriteBuffer,
  ReadFrom(io.Reader).
- TCPListener: ListenTCP, AcceptTCP, SetDeadline; ListenPacket package func.
- Interface: Addrs, MulticastAddrs stubs.
This commit is contained in:
Patricio Whittingslow
2026-07-01 22:34:14 -03:00
parent 1026408a38
commit 2e825c2b01
6 changed files with 420 additions and 0 deletions
+26
View File
@@ -87,6 +87,13 @@ type Dialer struct {
// If KeepAliveConfig.Enable is false and KeepAlive is negative,
// keep-alive probes are disabled.
KeepAliveConfig KeepAliveConfig
// Resolver optionally specifies an alternate resolver to use.
//
// TINYGO: present for API compatibility; DialContext resolves via the
// netdev-backed ResolveTCPAddr/ResolveUDPAddr and does not consult this
// field.
Resolver *Resolver
}
// Dial connects to the address on the named network.
@@ -281,3 +288,22 @@ func Listen(network, address string) (Listener, error) {
return listenTCP(laddr)
}
// ListenPacket announces on the local network address.
//
// TINYGO: only UDP networks are supported, backed by ListenUDP; the
// returned PacketConn is a *UDPConn.
func ListenPacket(network, address string) (PacketConn, error) {
switch network {
case "udp", "udp4":
default:
return nil, fmt.Errorf("Network %s not supported", network)
}
laddr, err := ResolveUDPAddr(network, address)
if err != nil {
return nil, err
}
return ListenUDP(network, laddr)
}
+47
View File
@@ -0,0 +1,47 @@
// TINYGO: The following is copied and modified from Go 1.26.2 official implementation.
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package net
// DNSError represents a DNS lookup error.
type DNSError struct {
UnwrapErr error // error returned by the [DNSError.Unwrap] method, might be nil
Err string // description of the error
Name string // name looked for
Server string // server used
IsTimeout bool // if true, timed out; not all timeouts set this
IsTemporary bool // if true, error is temporary; not all errors set this
// IsNotFound is set to true when the requested name does not
// contain any records of the requested type (data not found),
// or the name itself was not found (NXDOMAIN).
IsNotFound bool
}
// Unwrap returns e.UnwrapErr.
func (e *DNSError) Unwrap() error { return e.UnwrapErr }
func (e *DNSError) Error() string {
if e == nil {
return "<nil>"
}
s := "lookup " + e.Name
if e.Server != "" {
s += " on " + e.Server
}
s += ": " + e.Err
return s
}
// Timeout reports whether the DNS lookup is known to have timed out.
// This is not always known; a DNS lookup may fail due to a timeout
// and return a [DNSError] for which Timeout returns false.
func (e *DNSError) Timeout() bool { return e.IsTimeout }
// Temporary reports whether the DNS error is known to be temporary.
// This is not always known; a DNS lookup may fail due to a temporary
// error and return a [DNSError] for which Temporary returns false.
func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary }
+16
View File
@@ -35,6 +35,22 @@ type Interface struct {
Flags Flags // e.g., FlagUp, FlagLoopback, FlagMulticast
}
// Addrs returns a list of unicast interface addresses for a specific
// interface.
//
// TINYGO: not implemented; netdev exposes no per-interface address list.
func (ifi *Interface) Addrs() ([]Addr, error) {
return nil, errors.New("Interface.Addrs not implemented")
}
// MulticastAddrs returns a list of multicast, joined group addresses
// for a specific interface.
//
// TINYGO: not implemented; netdev exposes no per-interface address list.
func (ifi *Interface) MulticastAddrs() ([]Addr, error) {
return nil, errors.New("Interface.MulticastAddrs not implemented")
}
type Flags uint
const (
+109
View File
@@ -7,13 +7,122 @@
package net
import (
"context"
"errors"
"net/netip"
)
// A Resolver looks up names and numbers.
//
// A nil *Resolver is equivalent to a zero Resolver.
//
// TINYGO: name resolution is backed by the netdev GetHostByName hook;
// the PreferGo/StrictErrors/Dial fields exist for API compatibility with
// upstream Go and are ignored.
type Resolver struct {
// PreferGo controls whether Go's built-in DNS resolver is preferred
// on platforms where it's available. It is equivalent to setting
// GODEBUG=netdns=go, but scoped to just this resolver.
PreferGo bool
// StrictErrors controls the behavior of temporary errors
// (including timeout, socket errors, and SERVFAIL) when using
// Go's built-in resolver.
StrictErrors bool
// Dial optionally specifies an alternate dialer for use by
// Go's built-in DNS resolver to make TCP and UDP connections
// to DNS services.
Dial func(ctx context.Context, network, address string) (Conn, error)
}
// DefaultResolver is the resolver used by the package-level Lookup
// functions and by Dialers without a specified Resolver.
var DefaultResolver = &Resolver{}
// LookupHost looks up the given host using the local resolver.
// It returns a slice of that host's addresses.
//
// LookupHost uses [context.Background] internally; to specify the context, use
// [Resolver.LookupHost].
func LookupHost(host string) (addrs []string, err error) {
return DefaultResolver.LookupHost(context.Background(), host)
}
// LookupHost looks up the given host using the local resolver.
// It returns a slice of that host's addresses.
//
// TINYGO: resolves via netdev.GetHostByName, returning at most one address.
func (r *Resolver) LookupHost(ctx context.Context, host string) (addrs []string, err error) {
if host == "" {
return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
ip, err := netdev.GetHostByName(host)
if err != nil {
return nil, &DNSError{Err: err.Error(), Name: host}
}
return []string{ip.String()}, nil
}
// LookupIP looks up host using the local resolver.
// It returns a slice of that host's IPv4 and IPv6 addresses.
func LookupIP(host string) ([]IP, error) {
addrs, err := DefaultResolver.LookupIPAddr(context.Background(), host)
if err != nil {
return nil, err
}
ips := make([]IP, 0, len(addrs))
for _, addr := range addrs {
ips = append(ips, addr.IP)
}
return ips, nil
}
// LookupIPAddr looks up host using the local resolver.
// It returns a slice of that host's IPv4 and IPv6 addresses.
//
// TINYGO: resolves via netdev.GetHostByName, returning at most one address.
func (r *Resolver) LookupIPAddr(ctx context.Context, host string) ([]IPAddr, error) {
if host == "" {
return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
ip, err := netdev.GetHostByName(host)
if err != nil {
return nil, &DNSError{Err: err.Error(), Name: host}
}
return []IPAddr{{IP: ip.AsSlice(), Zone: ip.Zone()}}, nil
}
// LookupNetIP looks up host using the local resolver.
// It returns a slice of that host's IP addresses of the type specified by
// network. The network must be one of "ip", "ip4" or "ip6".
//
// TINYGO: resolves via netdev.GetHostByName, returning at most one address.
func (r *Resolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) {
if host == "" {
return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
ip, err := netdev.GetHostByName(host)
if err != nil {
return nil, &DNSError{Err: err.Error(), Name: host}
}
return []netip.Addr{ip}, nil
}
// LookupPort looks up the port for the given network and service.
//
// LookupPort uses [context.Background] internally; to specify the context, use
// [Resolver.LookupPort].
func LookupPort(network, service string) (port int, err error) {
return DefaultResolver.LookupPort(context.Background(), network, service)
}
// LookupPort looks up the port for the given network and service.
//
// TINYGO: not implemented; netdev provides no service database.
func (r *Resolver) LookupPort(ctx context.Context, network, service string) (port int, err error) {
return 0, errors.New("net:LookupPort not implemented")
}
// errNoSuchHost is returned when the host lookup finds no matching records.
var errNoSuchHost = errors.New("no such host")
+115
View File
@@ -296,6 +296,61 @@ func (c *TCPConn) CloseWrite() error {
return fmt.Errorf("CloseWrite not implemented")
}
// CloseRead shuts down the reading side of the TCP connection.
// Most callers should just use Close.
//
// TINYGO: no-op; netdev has no half-close, reads simply stop when Close
// is called.
func (c *TCPConn) CloseRead() error {
return nil
}
// SetNoDelay controls whether the operating system should delay
// packet transmission in hopes of sending fewer packets (Nagle's
// algorithm). The default is true (no delay), meaning that data is
// sent as soon as possible after a Write.
//
// TINYGO: no-op; Nagle behavior is managed by the netdev driver.
func (c *TCPConn) SetNoDelay(noDelay bool) error {
return nil
}
// SetReadBuffer sets the size of the operating system's receive buffer
// associated with the connection.
//
// TINYGO: no-op; buffer sizing is managed by the netdev driver.
func (c *TCPConn) SetReadBuffer(bytes int) error {
return nil
}
// SetWriteBuffer sets the size of the operating system's transmit buffer
// associated with the connection.
//
// TINYGO: no-op; buffer sizing is managed by the netdev driver.
func (c *TCPConn) SetWriteBuffer(bytes int) error {
return nil
}
// ReadFrom implements the io.ReaderFrom ReadFrom method.
//
// TINYGO: generic copy loop over Write; no zero-copy sendfile path.
func (c *TCPConn) ReadFrom(r io.Reader) (int64, error) {
return genericReadFrom(c, r)
}
// genericReadFrom copies from r into w until EOF, returning the number
// of bytes copied.
func genericReadFrom(w io.Writer, r io.Reader) (int64, error) {
// Wrap w to hide its ReadFrom method, so io.Copy uses the generic
// read/write loop instead of recursing back into TCPConn.ReadFrom.
return io.Copy(onlyWriter{w}, r)
}
// onlyWriter hides w's ReadFrom method from io.Copy.
type onlyWriter struct {
io.Writer
}
type listener struct {
fd int
laddr *TCPAddr
@@ -348,3 +403,63 @@ func listenTCP(laddr *TCPAddr) (Listener, error) {
type TCPListener struct {
listener
}
// ListenTCP acts like [Listen] for TCP networks.
//
// The network must be a TCP network name; see func [Dial] for details.
// If the IP field of laddr is nil or an unspecified IP address,
// ListenTCP listens on all available unicast and anycast IP addresses
// of the local system. If the Port field of laddr is 0, a port number
// is automatically chosen.
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error) {
switch network {
case "tcp", "tcp4":
default:
return nil, fmt.Errorf("Network '%s' not supported", network)
}
if laddr == nil {
laddr = &TCPAddr{}
}
fd, err := netdev.Socket(_AF_INET, _SOCK_STREAM, _IPPROTO_TCP)
if err != nil {
return nil, err
}
if err = netdev.Bind(fd, laddr.AddrPort()); err != nil {
netdev.Close(fd)
return nil, err
}
if err = netdev.Listen(fd, 5); err != nil {
netdev.Close(fd)
return nil, err
}
return &TCPListener{listener{fd: fd, laddr: laddr}}, nil
}
// AcceptTCP accepts the next incoming call and returns the new
// connection.
func (l *TCPListener) AcceptTCP() (*TCPConn, error) {
fd, raddr, err := netdev.Accept(l.fd)
if err != nil {
return nil, err
}
return &TCPConn{
fd: fd,
net: "tcp",
laddr: l.laddr,
raddr: TCPAddrFromAddrPort(raddr),
}, nil
}
// SetDeadline sets the deadline associated with the listener.
// A zero time value disables the deadline.
//
// TINYGO: no-op; netdev.Accept has no deadline support.
func (l *TCPListener) SetDeadline(t time.Time) error {
return nil
}
+107
View File
@@ -210,6 +210,53 @@ func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) {
}, nil
}
// ListenUDP acts like [ListenPacket] for UDP networks.
//
// The network must be a UDP network name; see func [Dial] for details.
//
// If the IP field of laddr is nil or an unspecified IP address,
// ListenUDP listens on all available IP addresses of the local system
// except multicast IP addresses.
// If the Port field of laddr is 0, a port number is automatically
// chosen.
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error) {
switch network {
case "udp", "udp4":
default:
return nil, fmt.Errorf("Network '%s' not supported", network)
}
// TINYGO: Use netdev to create UDP socket and bind (no connect)
if laddr == nil {
laddr = &UDPAddr{}
}
// If no port was given, grab an ephemeral port
if laddr.Port == 0 {
laddr.Port = ephemeralPort()
}
fd, err := netdev.Socket(_AF_INET, _SOCK_DGRAM, _IPPROTO_UDP)
if err != nil {
return nil, err
}
lip, _ := netip.AddrFromSlice(laddr.IP)
laddrport := netip.AddrPortFrom(lip, uint16(laddr.Port))
if err = netdev.Bind(fd, laddrport); err != nil {
netdev.Close(fd)
return nil, err
}
return &UDPConn{
fd: fd,
net: network,
laddr: laddr,
}, nil
}
// SyscallConn returns a raw network connection.
// This implements the syscall.Conn interface.
func (c *UDPConn) SyscallConn() (syscall.RawConn, error) {
@@ -242,6 +289,66 @@ func (c *UDPConn) Write(b []byte) (int, error) {
return n, err
}
// ReadFromUDP acts like [UDPConn.ReadFrom] but returns a [UDPAddr].
//
// TINYGO: netdev has no per-datagram source address, so the connected
// remote address (c.raddr) is returned as the source.
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error) {
n, err := netdev.Recv(c.fd, b, 0, c.readDeadline)
if n < 0 {
n = 0
}
if err != nil && err != io.EOF {
err = &OpError{Op: "read", Net: c.net, Source: c.laddr, Addr: c.raddr, Err: err}
}
return n, c.raddr, err
}
// ReadFromUDPAddrPort acts like [UDPConn.ReadFromUDP] but returns a [netip.AddrPort].
func (c *UDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
n, uaddr, err := c.ReadFromUDP(b)
if uaddr != nil {
addr = uaddr.AddrPort()
}
return n, addr, err
}
// WriteToUDP acts like [UDPConn.WriteTo] but takes a [UDPAddr].
//
// TINYGO: the socket is connected via netdev, so writes go to the
// connected remote regardless of addr, mirroring netdev.Send.
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error) {
n, err := netdev.Send(c.fd, b, 0, c.writeDeadline)
if n < 0 {
n = 0
}
if err != nil {
err = &OpError{Op: "write", Net: c.net, Source: c.laddr, Addr: addr.opAddr(), Err: err}
}
return n, err
}
// WriteToUDPAddrPort acts like [UDPConn.WriteToUDP] but takes a [netip.AddrPort].
func (c *UDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
return c.WriteToUDP(b, UDPAddrFromAddrPort(addr))
}
// SetReadBuffer sets the size of the operating system's receive buffer
// associated with the connection.
//
// TINYGO: no-op; buffer sizing is managed by the netdev driver.
func (c *UDPConn) SetReadBuffer(bytes int) error {
return nil
}
// SetWriteBuffer sets the size of the operating system's transmit buffer
// associated with the connection.
//
// TINYGO: no-op; buffer sizing is managed by the netdev driver.
func (c *UDPConn) SetWriteBuffer(bytes int) error {
return nil
}
// ReadFrom implements the PacketConn ReadFrom method.
func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) {
return 0, nil, errors.New("ReadFrom not implemented")