From 6f2563f72246dc59e8b019da04377f72db874dc0 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:43:38 -0500 Subject: [PATCH 1/2] net: add default host netdev backed by raw Linux syscalls On the native linux target the net package defaulted to a NOP netdev that returns "Netdev not set" for every operation, so net.Dial/Listen, DNS and anything built on them (net/http, tls) failed at runtime. Native linux does not override the syscall package, and the TinyGo compiler lowers syscall.Syscall/RawSyscall into real system calls, so the standard library socket functions work directly (musl's omitted network module is not needed). Register a default netdev implementing the netdever interface on top of syscall.Socket/Connect/Bind/Listen/Accept/Send/Recv/SetSockOpt, plus a small UDP DNS resolver (/etc/hosts, /etc/resolv.conf) for GetHostByName. Also restore the net.DNSError type used by resolution errors. Blocking sockets under the threads scheduler; IPv4 only. This avoids the internal/poll netpoller dependency that blocked the upstream-net approach. Refs tinygo-org/net#28 --- lookup.go | 39 ++++ netdev_native.go | 519 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 netdev_native.go diff --git a/lookup.go b/lookup.go index 4eabf77..a85fa5d 100644 --- a/lookup.go +++ b/lookup.go @@ -17,3 +17,42 @@ import ( func LookupPort(network, service string) (port int, err error) { return 0, errors.New("net:LookupPort not implemented") } + +// DNSError represents a DNS lookup error. +// +// TINYGO: Trimmed copy of the standard library's net.DNSError so that name +// resolution errors keep a familiar, type-assertable shape. +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. +func (e *DNSError) Timeout() bool { return e.IsTimeout } + +// Temporary reports whether the DNS error is known to be temporary. +func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary } diff --git a/netdev_native.go b/netdev_native.go new file mode 100644 index 0000000..202072b --- /dev/null +++ b/netdev_native.go @@ -0,0 +1,519 @@ +//go:build linux && !baremetal && !nintendoswitch && !wasm_unknown && !tinygo.wasm + +// TINYGO: Native (host) netdev for the TinyGo "linux" target. +// +// On the native linux target TinyGo does NOT override the "syscall" package, so +// the standard library's syscall.Socket/Connect/Bind/... are available and the +// TinyGo compiler lowers syscall.Syscall/RawSyscall into real inline-asm system +// calls (see compiler/syscall.go). That means we can implement the netdever +// interface directly on top of raw Linux sockets, without needing a network +// driver or musl's (omitted) src/network module. +// +// This file registers that implementation as the default netdev, so that +// net.Dial/Listen/Lookup just work on a regular Linux host. See +// https://github.com/skycoin/skycoin/issues/2902. + +package net + +import ( + "io" + "net/netip" + "os" + "strings" + "syscall" + "time" +) + +// Register the host netdev as the default. A network driver (e.g. on a board +// that also reports GOOS=linux, which doesn't happen today) could still replace +// it by calling useNetdev() from its own init/setup. +func init() { + useNetdev(&hostNetdev{}) +} + +// hostNetdev implements netdever using raw Linux sockets via the syscall +// package. The "sockfd" values it returns are plain OS file descriptors. +// +// Deadlines are implemented with the per-socket SO_RCVTIMEO/SO_SNDTIMEO +// options rather than a runtime poller. As a consequence, issuing concurrent +// reads (or concurrent writes) with different deadlines on the same connection +// is not supported; this matches the typical net.Conn usage of one reader and +// one writer goroutine. +type hostNetdev struct{} + +// timeoutError is returned from Send/Recv when a deadline expires. It satisfies +// the net.Error interface so callers (e.g. net/http) can detect timeouts. +type timeoutError struct{} + +func (timeoutError) Error() string { return "i/o timeout" } +func (timeoutError) Timeout() bool { return true } +func (timeoutError) Temporary() bool { return true } + +func (*hostNetdev) GetHostByName(name string) (netip.Addr, error) { + return hostLookup(name) +} + +func (*hostNetdev) Addr() (netip.Addr, error) { + // Determine the address of the interface that would be used to reach the + // public internet by "connecting" a UDP socket (no packets are sent) and + // reading back the chosen local address. + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0) + if err != nil { + return netip.Addr{}, err + } + defer syscall.Close(fd) + + err = syscall.Connect(fd, &syscall.SockaddrInet4{ + Addr: [4]byte{8, 8, 8, 8}, + Port: 53, + }) + if err != nil { + // No route to the internet; fall back to loopback. + return netip.AddrFrom4([4]byte{127, 0, 0, 1}), nil + } + + sa, err := syscall.Getsockname(fd) + if err != nil { + return netip.Addr{}, err + } + if sa4, ok := sa.(*syscall.SockaddrInet4); ok { + return netip.AddrFrom4(sa4.Addr), nil + } + return netip.AddrFrom4([4]byte{127, 0, 0, 1}), nil +} + +func (*hostNetdev) Socket(domain, stype, protocol int) (int, error) { + // _IPPROTO_TLS is a made-up protocol used by net.DialTLS on devices with an + // offloaded TLS stack. The host has no such offload (TLS is done in Go via + // crypto/tls over a plain TCP conn), so treat it as an ordinary TCP socket. + if protocol == _IPPROTO_TLS { + protocol = syscall.IPPROTO_TCP + } + + fd, err := syscall.Socket(domain, stype, protocol) + if err != nil { + return -1, err + } + + // Allow quick rebind of listening sockets (e.g. restarting a server), + // matching the standard library's behaviour. + if stype == syscall.SOCK_STREAM { + syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + } + + return fd, nil +} + +func (*hostNetdev) Bind(sockfd int, ip netip.AddrPort) error { + return syscall.Bind(sockfd, sockaddr(ip)) +} + +func (n *hostNetdev) Connect(sockfd int, host string, ip netip.AddrPort) error { + addr := ip.Addr() + if !addr.IsValid() || addr.IsUnspecified() { + // net.DialTLS passes the host name with a zero IP; resolve it here. + resolved, err := n.GetHostByName(host) + if err != nil { + return err + } + addr = resolved + } + sa := sockaddrFromParts(addr, ip.Port()) + for { + err := syscall.Connect(sockfd, sa) + if err == syscall.EINTR { + continue + } + return err + } +} + +func (*hostNetdev) Listen(sockfd int, backlog int) error { + return syscall.Listen(sockfd, backlog) +} + +func (*hostNetdev) Accept(sockfd int) (int, netip.AddrPort, error) { + nfd, sa, err := syscall.Accept(sockfd) + if err != nil { + return -1, netip.AddrPort{}, err + } + var raddr netip.AddrPort + if sa4, ok := sa.(*syscall.SockaddrInet4); ok { + raddr = netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), uint16(sa4.Port)) + } + return nfd, raddr, nil +} + +func (*hostNetdev) Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + // net.Conn.Write must send the whole buffer (or report an error), so loop + // over short writes. The send timeout is (re)programmed each iteration so a + // deadline bounds the whole operation, not each individual write. + total := 0 + for total < len(buf) { + if expired(deadline) { + return total, timeoutError{} + } + if err := setSockTimeout(sockfd, syscall.SO_SNDTIMEO, deadline); err != nil { + return total, err + } + n, err := syscall.Write(sockfd, buf[total:]) + if err != nil { + if err == syscall.EINTR { + continue + } + if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { + return total, timeoutError{} + } + return total, err + } + if n <= 0 { + break + } + total += n + } + return total, nil +} + +func (*hostNetdev) Recv(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + if expired(deadline) { + return 0, timeoutError{} + } + if err := setSockTimeout(sockfd, syscall.SO_RCVTIMEO, deadline); err != nil { + return 0, err + } + + for { + n, err := syscall.Read(sockfd, buf) + if err != nil { + if err == syscall.EINTR { + continue + } + if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { + return 0, timeoutError{} + } + return n, err + } + // A read of 0 bytes on a stream socket means the peer closed the + // connection. (A zero-length UDP datagram would also report EOF here; + // the fd's socket type isn't tracked, but the net package only does + // connected-UDP reads where this is not a practical concern.) + if n == 0 && len(buf) > 0 { + return 0, io.EOF + } + return n, nil + } +} + +func (*hostNetdev) Close(sockfd int) error { + return syscall.Close(sockfd) +} + +func (*hostNetdev) SetSockOpt(sockfd int, level int, opt int, value interface{}) error { + // SO_LINGER takes a struct linger; the net package passes it the linger + // seconds as an int. + if level == syscall.SOL_SOCKET && opt == syscall.SO_LINGER { + sec := toInt(value) + l := &syscall.Linger{} + if sec >= 0 { + l.Onoff = 1 + l.Linger = int32(sec) + } + return syscall.SetsockoptLinger(sockfd, level, opt, l) + } + return syscall.SetsockoptInt(sockfd, level, opt, toInt(value)) +} + +// toInt coerces the values the net package passes to SetSockOpt (int, bool, or +// float64 durations) into an int. +func toInt(value interface{}) int { + switch v := value.(type) { + case int: + return v + case bool: + if v { + return 1 + } + return 0 + case float64: + return int(v) + case int64: + return int(v) + default: + return 0 + } +} + +// sockaddr builds a SockaddrInet4 from an AddrPort, mapping an invalid/zero +// address to 0.0.0.0 (the wildcard used when binding). +func sockaddr(ip netip.AddrPort) *syscall.SockaddrInet4 { + return sockaddrFromParts(ip.Addr(), ip.Port()) +} + +func sockaddrFromParts(addr netip.Addr, port uint16) *syscall.SockaddrInet4 { + sa := &syscall.SockaddrInet4{Port: int(port)} + // As4 panics on a non-4-byte address; only an IPv4 address is valid here. + // A zero/unspecified address leaves Addr as 0.0.0.0 (the bind wildcard). + if addr = addr.Unmap(); addr.Is4() { + sa.Addr = addr.As4() + } + return sa +} + +// expired reports whether a non-zero deadline is already in the past. +func expired(deadline time.Time) bool { + return !deadline.IsZero() && !deadline.After(time.Now()) +} + +// setSockTimeout programs SO_RCVTIMEO/SO_SNDTIMEO so a blocking recv/send +// returns EAGAIN at the deadline. A zero deadline disables the timeout. +func setSockTimeout(sockfd int, opt int, deadline time.Time) error { + var tv syscall.Timeval + if !deadline.IsZero() { + d := time.Until(deadline) + if d < time.Microsecond { + d = time.Microsecond + } + tv = syscall.NsecToTimeval(d.Nanoseconds()) + } + return syscall.SetsockoptTimeval(sockfd, syscall.SOL_SOCKET, opt, &tv) +} + +// --- Name resolution -------------------------------------------------------- + +// hostLookup resolves a host name (or IP literal) to a single IPv4 address, +// consulting (in order): IP literals, /etc/hosts, then DNS. +func hostLookup(name string) (netip.Addr, error) { + if name == "" { + return netip.AddrFrom4([4]byte{0, 0, 0, 0}), nil + } + + // IP literal? + if addr, err := netip.ParseAddr(name); err == nil { + addr = addr.Unmap() + if !addr.Is4() { + return netip.Addr{}, &DNSError{Err: "only IPv4 is supported", Name: name} + } + return addr, nil + } + + // /etc/hosts + if addr, ok := lookupStaticHost(name); ok { + return addr, nil + } + + // Well-known fallback in case /etc/hosts is missing. + if strings.EqualFold(name, "localhost") { + return netip.AddrFrom4([4]byte{127, 0, 0, 1}), nil + } + + return dnsLookup(name) +} + +// lookupStaticHost scans /etc/hosts for an IPv4 address matching name. +func lookupStaticHost(name string) (netip.Addr, bool) { + data, err := os.ReadFile("/etc/hosts") + if err != nil { + return netip.Addr{}, false + } + for _, line := range strings.Split(string(data), "\n") { + if i := strings.IndexByte(line, '#'); i >= 0 { + line = line[:i] + } + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + addr, err := netip.ParseAddr(fields[0]) + if err != nil || !addr.Unmap().Is4() { + continue + } + for _, h := range fields[1:] { + if strings.EqualFold(h, name) { + return addr.Unmap(), true + } + } + } + return netip.Addr{}, false +} + +// resolvConfServers returns the nameservers from /etc/resolv.conf as +// "ip:53" strings, defaulting to localhost if the file is missing/empty. +func resolvConfServers() []string { + var servers []string + if data, err := os.ReadFile("/etc/resolv.conf"); err == nil { + for _, line := range strings.Split(string(data), "\n") { + if i := strings.IndexByte(line, '#'); i >= 0 { + line = line[:i] + } + fields := strings.Fields(line) + if len(fields) >= 2 && fields[0] == "nameserver" { + if addr, err := netip.ParseAddr(fields[1]); err == nil && addr.Unmap().Is4() { + servers = append(servers, addr.Unmap().String()) + } + } + } + } + if len(servers) == 0 { + servers = []string{"127.0.0.1"} + } + return servers +} + +// dnsLookup resolves name to an IPv4 address by querying the system +// nameservers over UDP. +func dnsLookup(name string) (netip.Addr, error) { + id, query := buildDNSQuery(name) + var lastErr error + for _, server := range resolvConfServers() { + addr, err := dnsQuery(server, query, id) + if err == nil { + return addr, nil + } + lastErr = err + } + if lastErr == nil { + lastErr = &DNSError{Err: "no answer", Name: name} + } + return netip.Addr{}, &DNSError{Err: lastErr.Error(), Name: name} +} + +// dnsID derives a non-secret 16-bit query ID. It need not be cryptographically +// random for a stub resolver on a trusted link; it just disambiguates replies. +func dnsID() uint16 { + return uint16(time.Now().UnixNano()) +} + +// buildDNSQuery builds a standard recursive A-record query for name and returns +// it together with the query ID, so the reply can be matched against it. +func buildDNSQuery(name string) (uint16, []byte) { + id := dnsID() + msg := []byte{ + byte(id >> 8), byte(id), // ID + 0x01, 0x00, // flags: recursion desired + 0x00, 0x01, // QDCOUNT + 0x00, 0x00, // ANCOUNT + 0x00, 0x00, // NSCOUNT + 0x00, 0x00, // ARCOUNT + } + for _, label := range strings.Split(strings.TrimSuffix(name, "."), ".") { + if len(label) == 0 || len(label) > 63 { + continue + } + msg = append(msg, byte(len(label))) + msg = append(msg, label...) + } + msg = append(msg, 0x00) // root label + msg = append(msg, + 0x00, 0x01, // QTYPE = A + 0x00, 0x01, // QCLASS = IN + ) + return id, msg +} + +// dnsQuery sends query to server (an IPv4 string) on port 53 and returns the +// first A record from the response that matches id. +func dnsQuery(server string, query []byte, id uint16) (netip.Addr, error) { + srv, err := netip.ParseAddr(server) + if err != nil { + return netip.Addr{}, err + } + + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0) + if err != nil { + return netip.Addr{}, err + } + defer syscall.Close(fd) + + if err := syscall.Connect(fd, &syscall.SockaddrInet4{Addr: srv.As4(), Port: 53}); err != nil { + return netip.Addr{}, err + } + + // Bound how long we wait for a reply. + tv := syscall.NsecToTimeval((5 * time.Second).Nanoseconds()) + syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &tv) + + if _, err := syscall.Write(fd, query); err != nil { + return netip.Addr{}, err + } + + resp := make([]byte, 512) + n, err := syscall.Read(fd, resp) + if err != nil { + return netip.Addr{}, err + } + return parseDNSResponse(resp[:n], id) +} + +// parseDNSResponse extracts the first A record from a DNS response message, +// after validating that it is a non-error reply to query id. +func parseDNSResponse(msg []byte, id uint16) (netip.Addr, error) { + if len(msg) < 12 { + return netip.Addr{}, &DNSError{Err: "short DNS response"} + } + // Match the reply to our query and check it is a response, not an error. + if uint16(msg[0])<<8|uint16(msg[1]) != id { + return netip.Addr{}, &DNSError{Err: "DNS response ID mismatch"} + } + if msg[2]&0x80 == 0 { + return netip.Addr{}, &DNSError{Err: "DNS reply is not a response"} + } + switch rcode := msg[3] & 0x0f; rcode { + case 0: // NOERROR + case 3: // NXDOMAIN + return netip.Addr{}, &DNSError{Err: "host not found", IsNotFound: true} + default: + return netip.Addr{}, &DNSError{Err: "DNS server error"} + } + qdcount := int(msg[4])<<8 | int(msg[5]) + ancount := int(msg[6])<<8 | int(msg[7]) + + off := 12 + // Skip the question section. + for i := 0; i < qdcount; i++ { + off = skipName(msg, off) + if off < 0 || off+4 > len(msg) { + return netip.Addr{}, &DNSError{Err: "malformed DNS question"} + } + off += 4 // QTYPE + QCLASS + } + + for i := 0; i < ancount; i++ { + off = skipName(msg, off) + if off < 0 || off+10 > len(msg) { + return netip.Addr{}, &DNSError{Err: "malformed DNS answer"} + } + rrtype := int(msg[off])<<8 | int(msg[off+1]) + rdlength := int(msg[off+8])<<8 | int(msg[off+9]) + off += 10 + if off+rdlength > len(msg) { + return netip.Addr{}, &DNSError{Err: "malformed DNS rdata"} + } + if rrtype == 1 && rdlength == 4 { // A record + return netip.AddrFrom4([4]byte{ + msg[off], msg[off+1], msg[off+2], msg[off+3], + }), nil + } + off += rdlength + } + return netip.Addr{}, &DNSError{Err: "no A record in DNS response", IsNotFound: true} +} + +// skipName advances past a (possibly compressed) DNS name and returns the +// offset just after it, or -1 on malformed input. +func skipName(msg []byte, off int) int { + for { + if off >= len(msg) { + return -1 + } + b := int(msg[off]) + switch { + case b == 0: + return off + 1 + case b&0xc0 == 0xc0: + // Compression pointer ends the name. + return off + 2 + default: + off += 1 + b + } + } +} From e1d3688d987d5df6a822b1ab964964efcbdebc64 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:19:40 -0500 Subject: [PATCH 2/2] net: support IPv6 in the host netdev and TCP/UDP plumbing Make the net package address-family aware instead of IPv4-only: DialTCP, listenTCP and DialUDP now choose AF_INET or AF_INET6 from the target address (socketFamily), the "only ipv4 supported" guard is replaced by a 4-or-16 byte check, and "tcp6"/"udp6" network names are accepted. In the host netdev, sockaddrFromParts builds a SockaddrInet6 for IPv6 addresses (SockaddrInet4 otherwise), Accept decodes both families, GetHostByName and the /etc/hosts lookup accept IPv6, and the stub resolver now queries AAAA after A. Name resolution still prefers IPv4, so the "4"/"6" suffix is advisory for host names; this is documented on Dial/Listen. Link-local IPv6 zones are not mapped to a scope id. Verified on linux/amd64 with tinygo: IPv6 loopback Listen/Accept/Dial over [::1], AAAA fallback for an IPv6-only host name, and IPv4 behaviour unchanged. --- dial.go | 15 ++++--- netdev.go | 11 +++++ netdev_native.go | 112 +++++++++++++++++++++++++++++++---------------- tcpsock.go | 12 ++--- udpsock.go | 8 ++-- 5 files changed, 105 insertions(+), 53 deletions(-) diff --git a/dial.go b/dial.go index 876a1d8..33d241d 100644 --- a/dial.go +++ b/dial.go @@ -94,8 +94,11 @@ type Dialer struct { // See Go "net" package Dial() for more information. // // Note: Tinygo Dial supports a subset of networks supported by Go Dial, -// specifically: "tcp", "tcp4", "udp", and "udp4". IP and unix networks are -// not supported. +// specifically: "tcp", "tcp4", "tcp6", "udp", "udp4", and "udp6". IP and unix +// networks are not supported. IPv6 addresses are supported, but when dialing a +// host name the resolver prefers an IPv4 (A) address and only falls back to +// IPv6 (AAAA), so the "4"/"6" suffix does not force the address family for name +// resolution. func Dial(network, address string) (Conn, error) { var d Dialer return d.Dial(network, address) @@ -150,13 +153,13 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn // TINYGO: Ignoring context switch network { - case "tcp", "tcp4": + case "tcp", "tcp4", "tcp6": raddr, err := ResolveTCPAddr(network, address) if err != nil { return nil, err } return DialTCP(network, nil, raddr) - case "udp", "udp4": + case "udp", "udp4", "udp6": raddr, err := ResolveUDPAddr(network, address) if err != nil { return nil, err @@ -264,12 +267,12 @@ func parseNetwork(ctx context.Context, network string, needsProto bool) (afnet s // See Go "net" package Listen() for more information. // // Note: Tinygo Listen supports a subset of networks supported by Go Listen, -// specifically: "tcp", "tcp4". "tcp6" and unix networks are not supported. +// specifically: "tcp", "tcp4", and "tcp6". unix networks are not supported. func Listen(network, address string) (Listener, error) { // println("Listen", address) switch network { - case "tcp", "tcp4": + case "tcp", "tcp4", "tcp6": default: return nil, fmt.Errorf("Network %s not supported", network) } diff --git a/netdev.go b/netdev.go index c228f20..8822262 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 @@ -36,6 +37,16 @@ func useNetdev(dev netdever) { netdev = dev } +// socketFamily returns the address family (_AF_INET or _AF_INET6) to use for a +// socket targeting ip. A nil/zero-length IP (e.g. a wildcard listen address) +// defaults to IPv4. +func socketFamily(ip IP) int { + if len(ip) == 16 && ip.To4() == nil { + return _AF_INET6 + } + return _AF_INET +} + // netdever is TinyGo's OSI L3/L4 network/transport layer interface. Network // drivers implement the netdever interface, providing a common network L3/L4 // interface to TinyGo's "net" package. net.Conn implementations (TCPConn, diff --git a/netdev_native.go b/netdev_native.go index 202072b..e522a08 100644 --- a/netdev_native.go +++ b/netdev_native.go @@ -138,8 +138,11 @@ func (*hostNetdev) Accept(sockfd int) (int, netip.AddrPort, error) { return -1, netip.AddrPort{}, err } var raddr netip.AddrPort - if sa4, ok := sa.(*syscall.SockaddrInet4); ok { - raddr = netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), uint16(sa4.Port)) + switch s := sa.(type) { + case *syscall.SockaddrInet4: + raddr = netip.AddrPortFrom(netip.AddrFrom4(s.Addr), uint16(s.Port)) + case *syscall.SockaddrInet6: + raddr = netip.AddrPortFrom(netip.AddrFrom16(s.Addr), uint16(s.Port)) } return nfd, raddr, nil } @@ -243,17 +246,21 @@ func toInt(value interface{}) int { } } -// sockaddr builds a SockaddrInet4 from an AddrPort, mapping an invalid/zero -// address to 0.0.0.0 (the wildcard used when binding). -func sockaddr(ip netip.AddrPort) *syscall.SockaddrInet4 { +// sockaddr builds a syscall.Sockaddr from an AddrPort: a SockaddrInet6 for an +// IPv6 address, otherwise a SockaddrInet4 (an invalid/zero address maps to the +// 0.0.0.0 wildcard used when binding). +func sockaddr(ip netip.AddrPort) syscall.Sockaddr { return sockaddrFromParts(ip.Addr(), ip.Port()) } -func sockaddrFromParts(addr netip.Addr, port uint16) *syscall.SockaddrInet4 { +func sockaddrFromParts(addr netip.Addr, port uint16) syscall.Sockaddr { + // As4/As16 panic on a wrongly-sized address, so dispatch on the family. + if addr = addr.Unmap(); addr.Is6() { + // Link-local zones (%zone) are not resolved to a scope id. + return &syscall.SockaddrInet6{Port: int(port), Addr: addr.As16()} + } sa := &syscall.SockaddrInet4{Port: int(port)} - // As4 panics on a non-4-byte address; only an IPv4 address is valid here. - // A zero/unspecified address leaves Addr as 0.0.0.0 (the bind wildcard). - if addr = addr.Unmap(); addr.Is4() { + if addr.Is4() { sa.Addr = addr.As4() } return sa @@ -289,11 +296,7 @@ func hostLookup(name string) (netip.Addr, error) { // IP literal? if addr, err := netip.ParseAddr(name); err == nil { - addr = addr.Unmap() - if !addr.Is4() { - return netip.Addr{}, &DNSError{Err: "only IPv4 is supported", Name: name} - } - return addr, nil + return addr.Unmap(), nil } // /etc/hosts @@ -309,12 +312,15 @@ func hostLookup(name string) (netip.Addr, error) { return dnsLookup(name) } -// lookupStaticHost scans /etc/hosts for an IPv4 address matching name. +// lookupStaticHost scans /etc/hosts for an address matching name, preferring an +// IPv4 match but falling back to IPv6. func lookupStaticHost(name string) (netip.Addr, bool) { data, err := os.ReadFile("/etc/hosts") if err != nil { return netip.Addr{}, false } + var v6 netip.Addr + var haveV6 bool for _, line := range strings.Split(string(data), "\n") { if i := strings.IndexByte(line, '#'); i >= 0 { line = line[:i] @@ -324,16 +330,22 @@ func lookupStaticHost(name string) (netip.Addr, bool) { continue } addr, err := netip.ParseAddr(fields[0]) - if err != nil || !addr.Unmap().Is4() { + if err != nil { continue } + addr = addr.Unmap() for _, h := range fields[1:] { if strings.EqualFold(h, name) { - return addr.Unmap(), true + if addr.Is4() { + return addr, true + } + if !haveV6 { + v6, haveV6 = addr, true + } } } } - return netip.Addr{}, false + return v6, haveV6 } // resolvConfServers returns the nameservers from /etc/resolv.conf as @@ -359,13 +371,29 @@ func resolvConfServers() []string { return servers } -// dnsLookup resolves name to an IPv4 address by querying the system -// nameservers over UDP. +const ( + dnsTypeA = 1 + dnsTypeAAAA = 28 +) + +// dnsLookup resolves name by querying the system nameservers over UDP, +// preferring an IPv4 (A) answer and falling back to IPv6 (AAAA). func dnsLookup(name string) (netip.Addr, error) { - id, query := buildDNSQuery(name) + if addr, err := dnsLookupType(name, dnsTypeA); err == nil { + return addr, nil + } + if addr, err := dnsLookupType(name, dnsTypeAAAA); err == nil { + return addr, nil + } + return netip.Addr{}, &DNSError{Err: "no address found", Name: name} +} + +// dnsLookupType resolves name for a single DNS record type (A or AAAA). +func dnsLookupType(name string, qtype uint16) (netip.Addr, error) { + id, query := buildDNSQuery(name, qtype) var lastErr error for _, server := range resolvConfServers() { - addr, err := dnsQuery(server, query, id) + addr, err := dnsQuery(server, query, id, qtype) if err == nil { return addr, nil } @@ -374,7 +402,7 @@ func dnsLookup(name string) (netip.Addr, error) { if lastErr == nil { lastErr = &DNSError{Err: "no answer", Name: name} } - return netip.Addr{}, &DNSError{Err: lastErr.Error(), Name: name} + return netip.Addr{}, lastErr } // dnsID derives a non-secret 16-bit query ID. It need not be cryptographically @@ -383,9 +411,10 @@ func dnsID() uint16 { return uint16(time.Now().UnixNano()) } -// buildDNSQuery builds a standard recursive A-record query for name and returns -// it together with the query ID, so the reply can be matched against it. -func buildDNSQuery(name string) (uint16, []byte) { +// buildDNSQuery builds a standard recursive query for name of the given record +// type and returns it together with the query ID, so the reply can be matched +// against it. +func buildDNSQuery(name string, qtype uint16) (uint16, []byte) { id := dnsID() msg := []byte{ byte(id >> 8), byte(id), // ID @@ -404,15 +433,15 @@ func buildDNSQuery(name string) (uint16, []byte) { } msg = append(msg, 0x00) // root label msg = append(msg, - 0x00, 0x01, // QTYPE = A + byte(qtype>>8), byte(qtype), // QTYPE 0x00, 0x01, // QCLASS = IN ) return id, msg } // dnsQuery sends query to server (an IPv4 string) on port 53 and returns the -// first A record from the response that matches id. -func dnsQuery(server string, query []byte, id uint16) (netip.Addr, error) { +// first record of type qtype from the response that matches id. +func dnsQuery(server string, query []byte, id uint16, qtype uint16) (netip.Addr, error) { srv, err := netip.ParseAddr(server) if err != nil { return netip.Addr{}, err @@ -441,12 +470,12 @@ func dnsQuery(server string, query []byte, id uint16) (netip.Addr, error) { if err != nil { return netip.Addr{}, err } - return parseDNSResponse(resp[:n], id) + return parseDNSResponse(resp[:n], id, qtype) } -// parseDNSResponse extracts the first A record from a DNS response message, -// after validating that it is a non-error reply to query id. -func parseDNSResponse(msg []byte, id uint16) (netip.Addr, error) { +// parseDNSResponse extracts the first record of type qtype from a DNS response +// message, after validating that it is a non-error reply to query id. +func parseDNSResponse(msg []byte, id uint16, qtype uint16) (netip.Addr, error) { if len(msg) < 12 { return netip.Addr{}, &DNSError{Err: "short DNS response"} } @@ -488,14 +517,21 @@ func parseDNSResponse(msg []byte, id uint16) (netip.Addr, error) { if off+rdlength > len(msg) { return netip.Addr{}, &DNSError{Err: "malformed DNS rdata"} } - if rrtype == 1 && rdlength == 4 { // A record - return netip.AddrFrom4([4]byte{ - msg[off], msg[off+1], msg[off+2], msg[off+3], - }), nil + if rrtype == int(qtype) { + if qtype == dnsTypeA && rdlength == 4 { + var b [4]byte + copy(b[:], msg[off:off+4]) + return netip.AddrFrom4(b), nil + } + if qtype == dnsTypeAAAA && rdlength == 16 { + var b [16]byte + copy(b[:], msg[off:off+16]) + return netip.AddrFrom16(b), nil + } } off += rdlength } - return netip.Addr{}, &DNSError{Err: "no A record in DNS response", IsNotFound: true} + return netip.Addr{}, &DNSError{Err: "no matching record in DNS response", IsNotFound: true} } // skipName advances past a (possibly compressed) DNS name and returns the diff --git a/tcpsock.go b/tcpsock.go index 6415c48..fae02ea 100644 --- a/tcpsock.go +++ b/tcpsock.go @@ -94,7 +94,7 @@ func (a *TCPAddr) opAddr() Addr { func ResolveTCPAddr(network, address string) (*TCPAddr, error) { switch network { - case "tcp", "tcp4": + case "tcp", "tcp4", "tcp6": default: return nil, fmt.Errorf("Network '%s' not supported", network) } @@ -161,7 +161,7 @@ type TCPConn struct { func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error) { switch network { - case "tcp", "tcp4": + case "tcp", "tcp4", "tcp6": default: return nil, errors.New("Network not supported: '" + network + "'") } @@ -174,11 +174,11 @@ func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error) { if raddr.IP.IsUnspecified() { return nil, errors.New("Sorry, localhost isn't available on Tinygo") - } else if len(raddr.IP) != 4 { - return nil, errors.New("only ipv4 supported") + } else if len(raddr.IP) != 4 && len(raddr.IP) != 16 { + return nil, errors.New("invalid IP address") } - fd, err := netdev.Socket(_AF_INET, _SOCK_STREAM, _IPPROTO_TCP) + fd, err := netdev.Socket(socketFamily(raddr.IP), _SOCK_STREAM, _IPPROTO_TCP) if err != nil { return nil, err } @@ -324,7 +324,7 @@ func (l *listener) Addr() Addr { } func listenTCP(laddr *TCPAddr) (Listener, error) { - fd, err := netdev.Socket(_AF_INET, _SOCK_STREAM, _IPPROTO_TCP) + fd, err := netdev.Socket(socketFamily(laddr.IP), _SOCK_STREAM, _IPPROTO_TCP) if err != nil { return nil, err } diff --git a/udpsock.go b/udpsock.go index ef683b7..96a5d96 100644 --- a/udpsock.go +++ b/udpsock.go @@ -83,7 +83,7 @@ func (a *UDPAddr) opAddr() Addr { func ResolveUDPAddr(network, address string) (*UDPAddr, error) { switch network { - case "udp", "udp4": + case "udp", "udp4", "udp6": default: return nil, fmt.Errorf("Network '%s' not supported", network) } @@ -156,7 +156,7 @@ func ephemeralPort() int { // local system is assumed. func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) { switch network { - case "udp", "udp4": + case "udp", "udp4", "udp6": default: return nil, fmt.Errorf("Network '%s' not supported", network) } @@ -173,6 +173,8 @@ func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) { if raddr.IP.IsUnspecified() { return nil, fmt.Errorf("Sorry, localhost isn't available on Tinygo") + } else if len(raddr.IP) != 4 && len(raddr.IP) != 16 { + return nil, fmt.Errorf("invalid IP address") } // If no port was given, grab an ephemeral port @@ -180,7 +182,7 @@ func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) { laddr.Port = ephemeralPort() } - fd, err := netdev.Socket(_AF_INET, _SOCK_DGRAM, _IPPROTO_UDP) + fd, err := netdev.Socket(socketFamily(raddr.IP), _SOCK_DGRAM, _IPPROTO_UDP) if err != nil { return nil, err }