diff --git a/dial.go b/dial.go index 9c78ed4..edaddbb 100644 --- a/dial.go +++ b/dial.go @@ -101,8 +101,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) @@ -157,13 +160,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 @@ -271,12 +274,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 17e02a6..4e3c864 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 } @@ -379,7 +379,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 11df8b6..bf69aee 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 }