mirror of
https://github.com/tinygo-org/net.git
synced 2026-09-09 22:19:26 +00:00
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.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+74
-38
@@ -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
|
||||
|
||||
+6
-6
@@ -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
|
||||
}
|
||||
|
||||
+5
-3
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user