diff --git a/dial.go b/dial.go index 876a1d8..9c78ed4 100644 --- a/dial.go +++ b/dial.go @@ -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) +} diff --git a/dnserror.go b/dnserror.go new file mode 100644 index 0000000..a39349e --- /dev/null +++ b/dnserror.go @@ -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 "" + } + 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 } diff --git a/interface.go b/interface.go index 1a7b5ba..25bfb1f 100644 --- a/interface.go +++ b/interface.go @@ -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 ( diff --git a/lookup.go b/lookup.go index 4eabf77..7b77c26 100644 --- a/lookup.go +++ b/lookup.go @@ -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") diff --git a/tcpsock.go b/tcpsock.go index 6415c48..17e02a6 100644 --- a/tcpsock.go +++ b/tcpsock.go @@ -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 +} diff --git a/udpsock.go b/udpsock.go index ef683b7..11df8b6 100644 --- a/udpsock.go +++ b/udpsock.go @@ -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")