mirror of
https://github.com/tinygo-org/net.git
synced 2026-09-09 22:19:26 +00:00
2be2e34090
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.
556 lines
16 KiB
Go
556 lines
16 KiB
Go
//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
|
|
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
|
|
}
|
|
|
|
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 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.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)}
|
|
if 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 {
|
|
return addr.Unmap(), 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 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]
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
addr, err := netip.ParseAddr(fields[0])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
addr = addr.Unmap()
|
|
for _, h := range fields[1:] {
|
|
if strings.EqualFold(h, name) {
|
|
if addr.Is4() {
|
|
return addr, true
|
|
}
|
|
if !haveV6 {
|
|
v6, haveV6 = addr, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return v6, haveV6
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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) {
|
|
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, qtype)
|
|
if err == nil {
|
|
return addr, nil
|
|
}
|
|
lastErr = err
|
|
}
|
|
if lastErr == nil {
|
|
lastErr = &DNSError{Err: "no answer", Name: name}
|
|
}
|
|
return netip.Addr{}, lastErr
|
|
}
|
|
|
|
// 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 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
|
|
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,
|
|
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 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
|
|
}
|
|
|
|
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, qtype)
|
|
}
|
|
|
|
// 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"}
|
|
}
|
|
// 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 == 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 matching 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
|
|
}
|
|
}
|
|
}
|