Skip to content

net: rework the CNAME handling on unix #57093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/internal/syscall/unix/net_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,11 @@ func syscall_syscall6X(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err
//go:linkname syscall_syscall9 syscall.syscall9
func syscall_syscall9(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err syscall.Errno)

// Based on https://opensource.apple.com/source/libresolv/libresolv-65/resolv.h.auto.html
type ResState struct {
unexported [69]uintptr
_ [496]byte
Res_h_errno int32
_ [52]byte
}

//go:cgo_import_dynamic libresolv_res_9_ninit res_9_ninit "/usr/lib/libresolv.9.dylib"
Expand Down
14 changes: 13 additions & 1 deletion src/net/cgo_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,22 @@ func cgoLookupIP(ctx context.Context, network, name string) (addrs []IPAddr, err
panic("cgo stub: cgo not available")
}

func cgoLookupCNAME(ctx context.Context, name string) (cname string, err error, completed bool) {
func cgoLookupCNAME(ctx context.Context, name string) (cname string, err error) {
panic("cgo stub: cgo not available")
}

func cgoLookupPTR(ctx context.Context, addr string) (ptrs []string, err error) {
panic("cgo stub: cgo not available")
}

func cgoLookupCanonicalName(ctx context.Context, network string, name string) (cname string, err error) {
panic("cgo stub: cgo not available")
}

type stubError struct{}

func (stubError) Error() string {
panic("cgo stub: cgo not available")
}

var errCgoDNSLookupFailed = stubError{}
164 changes: 128 additions & 36 deletions src/net/cgo_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func cgoLookupServicePort(hints *_C_struct_addrinfo, network, service string) (p
return 0, &DNSError{Err: "unknown port", Name: network + "/" + service, IsNotFound: true}
}

func cgoLookupHostIP(network, name string) (addrs []IPAddr, err error) {
func cgoLookupHostIP(network, name string) (addrs []IPAddr, cname string, err error) {
acquireThread()
defer releaseThread()

Expand All @@ -162,7 +162,7 @@ func cgoLookupHostIP(network, name string) (addrs []IPAddr, err error) {

h, err := syscall.BytePtrFromString(name)
if err != nil {
return nil, &DNSError{Err: err.Error(), Name: name}
return nil, "", &DNSError{Err: err.Error(), Name: name}
}
var res *_C_struct_addrinfo
gerrno, err := _C_getaddrinfo((*_C_char)(unsafe.Pointer(h)), nil, &hints, &res)
Expand All @@ -189,10 +189,20 @@ func cgoLookupHostIP(network, name string) (addrs []IPAddr, err error) {
isTemporary = addrinfoErrno(gerrno).Temporary()
}

return nil, &DNSError{Err: err.Error(), Name: name, IsNotFound: isErrorNoSuchHost, IsTemporary: isTemporary}
return nil, "", &DNSError{Err: err.Error(), Name: name, IsNotFound: isErrorNoSuchHost, IsTemporary: isTemporary}
}
defer _C_freeaddrinfo(res)

if res != nil {
cname = _C_GoString(*_C_ai_canonname(res))
if cname == "" {
cname = name
}
if len(cname) > 0 && cname[len(cname)-1] != '.' {
cname += "."
}
}

for r := res; r != nil; r = *_C_ai_next(r) {
// We only asked for SOCK_STREAM, but check anyhow.
if *_C_ai_socktype(r) != _C_SOCK_STREAM {
Expand All @@ -209,12 +219,13 @@ func cgoLookupHostIP(network, name string) (addrs []IPAddr, err error) {
addrs = append(addrs, addr)
}
}
return addrs, nil
return addrs, cname, nil
}

func cgoLookupIP(ctx context.Context, network, name string) (addrs []IPAddr, err error) {
return doBlockingWithCtx(ctx, func() ([]IPAddr, error) {
return cgoLookupHostIP(network, name)
addrs, _, err := cgoLookupHostIP(network, name)
return addrs, err
})
}

Expand Down Expand Up @@ -295,45 +306,97 @@ func cgoSockaddr(ip IP, zone string) (*_C_struct_sockaddr, _C_socklen_t) {
return nil, 0
}

func cgoLookupCNAME(ctx context.Context, name string) (cname string, err error, completed bool) {
resources, err := resSearch(ctx, name, int(dnsmessage.TypeCNAME), int(dnsmessage.ClassINET))
// cgoLookupCanonicalName returns the host canonical name.
func cgoLookupCanonicalName(ctx context.Context, network string, name string) (cname string, err error) {
return doBlockingWithCtx(ctx, func() (string, error) {
_, cname, err := cgoLookupHostIP(network, name)
return cname, err
})
}

// cgoLookupCNAME queries the CNAME resource using cgo resSearch.
// It returns the last CNAME found in the entire CNAME chain or the queried name when
// query returns with no answer resources.
func cgoLookupCNAME(ctx context.Context, name string) (cname string, err error) {
msg, err := resSearch(ctx, name, int(dnsmessage.TypeCNAME), int(dnsmessage.ClassINET))

noData := false
if err != nil {
var dnsErr *DNSError
if !errors.As(err, &dnsErr) {
// Not a DNS error.
return "", err
} else if dnsErr.isNoData && msg != nil {
// DNS query succeeded, without error code (like NXDOMAIN),
// but it has zero answer records.
noData = true
} else {
return "", err
}
}

var p dnsmessage.Parser
_, err = p.Start(msg)
if err != nil {
return
return "", &DNSError{Err: errCannotUnmarshalDNSMessage.Error(), Name: name}
}
cname, err = parseCNAMEFromResources(resources)

q, err := p.Question()
if err != nil {
return "", err, false
return "", &DNSError{Err: errCannotUnmarshalDNSMessage.Error(), Name: name}
}

// Multiple questions, this should never happen.
if err := p.SkipQuestion(); err != dnsmessage.ErrSectionDone {
return "", &DNSError{Err: errCannotUnmarshalDNSMessage.Error(), Name: name}
}

if noData {
return q.Name.String(), nil
}
return cname, nil, true

// Using name from question, not the one provided in function arguments,
// because of possible search domain in resolv.conf.
cname, err = lastCNAMEinChain(q.Name, p)
if err != nil {
return "", &DNSError{
Err: err.Error(),
Name: name,
}
}

return cname, nil
}

// errCgoDNSLookupFailed is returned from resSearch on systems with non thread safe h_errno.
var errCgoDNSLookupFailed = errors.New("res_nsearch lookup failed")

// resSearch will make a call to the 'res_nsearch' routine in the C library
// and parse the output as a slice of DNS resources.
func resSearch(ctx context.Context, hostname string, rtype, class int) ([]dnsmessage.Resource, error) {
return doBlockingWithCtx(ctx, func() ([]dnsmessage.Resource, error) {
// In case of an error, the msg might be populated with a raw DNS response (it might
// be partial or with junk after the DNS message).
func resSearch(ctx context.Context, hostname string, rtype, class int) (msg []byte, err error) {
return doBlockingWithCtx(ctx, func() ([]byte, error) {
return cgoResSearch(hostname, rtype, class)
})
}

func cgoResSearch(hostname string, rtype, class int) ([]dnsmessage.Resource, error) {
func cgoResSearch(hostname string, rtype, class int) ([]byte, error) {
acquireThread()
defer releaseThread()

state := (*_C_struct___res_state)(_C_malloc(unsafe.Sizeof(_C_struct___res_state{})))
defer _C_free(unsafe.Pointer(state))
var state *_C_struct___res_state
if unsafe.Sizeof(_C_struct___res_state{}) != 0 {
state = (*_C_struct___res_state)(_C_malloc(unsafe.Sizeof(_C_struct___res_state{})))
defer _C_free(unsafe.Pointer(state))
*state = _C_struct___res_state{}
}

if err := _C_res_ninit(state); err != nil {
return nil, errors.New("res_ninit failure: " + err.Error())
}
defer _C_res_nclose(state)

// Some res_nsearch implementations (like macOS) do not set errno.
// They set h_errno, which is not per-thread and useless to us.
// res_nsearch returns the size of the DNS response packet.
// But if the DNS response packet contains failure-like response codes,
// res_search returns -1 even though it has copied the packet into buf,
// giving us no way to find out how big the packet is.
// For now, we are willing to take res_search's word that there's nothing
// useful in the response, even though there *is* a response.
bufSize := maxDNSPacketSize
buf := (*_C_uchar)(_C_malloc(uintptr(bufSize)))
defer _C_free(unsafe.Pointer(buf))
Expand All @@ -345,10 +408,44 @@ func cgoResSearch(hostname string, rtype, class int) ([]dnsmessage.Resource, err

var size int
for {
size, _ = _C_res_nsearch(state, (*_C_char)(unsafe.Pointer(s)), class, rtype, buf, bufSize)
var herrno int
var err error
size, herrno, err = _C_res_nsearch(state, (*_C_char)(unsafe.Pointer(s)), class, rtype, buf, bufSize)
if size <= 0 || size > 0xffff {
return nil, errors.New("res_nsearch failure")
// Copy from c to go memory.
msgC := unsafe.Slice((*byte)(unsafe.Pointer(buf)), bufSize)
msg := make([]byte, len(msgC))
copy(msg, msgC)

// We use -1 to indicate that h_errno is available, -2 otherwise.
if size == -1 {
if herrno == _C_HOST_NOT_FOUND || herrno == _C_NO_DATA {
return msg, &DNSError{
Err: errNoSuchHost.Error(),
IsNotFound: true,
isNoData: herrno == _C_NO_DATA,
Name: hostname,
}
}

if err != nil {
return msg, &DNSError{
Err: "dns lookup failure: " + err.Error(),
IsTemporary: herrno == _C_TRY_AGAIN,
Name: hostname,
}
}

return msg, &DNSError{
Err: "dns lookup failure",
IsTemporary: herrno == _C_TRY_AGAIN,
Name: hostname,
}
}

return msg, errCgoDNSLookupFailed
}

if size <= bufSize {
break
}
Expand All @@ -359,14 +456,9 @@ func cgoResSearch(hostname string, rtype, class int) ([]dnsmessage.Resource, err
buf = (*_C_uchar)(_C_malloc(uintptr(bufSize)))
}

var p dnsmessage.Parser
if _, err := p.Start(unsafe.Slice((*byte)(unsafe.Pointer(buf)), size)); err != nil {
return nil, err
}
p.SkipAllQuestions()
resources, err := p.AllAnswers()
if err != nil {
return nil, err
}
return resources, nil
// Copy from c to go memory.
msgC := unsafe.Slice((*byte)(unsafe.Pointer(buf)), size)
msg := make([]byte, len(msgC))
copy(msg, msgC)
return msg, nil
}
9 changes: 9 additions & 0 deletions src/net/cgo_unix_cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package net
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <netdb.h>

#ifndef EAI_NODATA
#define EAI_NODATA -5
Expand All @@ -30,6 +31,12 @@ package net
import "C"
import "unsafe"

const (
_C_HOST_NOT_FOUND = C.HOST_NOT_FOUND
_C_TRY_AGAIN = C.TRY_AGAIN
_C_NO_DATA = C.NO_DATA
)

const (
_C_AF_INET = C.AF_INET
_C_AF_INET6 = C.AF_INET6
Expand All @@ -56,10 +63,12 @@ type (
_C_struct_sockaddr = C.struct_sockaddr
)

func _C_GoString(p *_C_char) string { return C.GoString(p) }
func _C_malloc(n uintptr) unsafe.Pointer { return C.malloc(C.size_t(n)) }
func _C_free(p unsafe.Pointer) { C.free(p) }

func _C_ai_addr(ai *_C_struct_addrinfo) **_C_struct_sockaddr { return &ai.ai_addr }
func _C_ai_canonname(ai *_C_struct_addrinfo) **_C_char { return &ai.ai_canonname }
func _C_ai_family(ai *_C_struct_addrinfo) *_C_int { return &ai.ai_family }
func _C_ai_flags(ai *_C_struct_addrinfo) *_C_int { return &ai.ai_flags }
func _C_ai_next(ai *_C_struct_addrinfo) **_C_struct_addrinfo { return &ai.ai_next }
Expand Down
10 changes: 10 additions & 0 deletions src/net/cgo_unix_cgo_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,13 @@ import (
// This will cause a compile error when the size of
// unix.ResState is too small.
type _ [unsafe.Sizeof(unix.ResState{}) - unsafe.Sizeof(C.struct___res_state{})]byte

// This will cause a compile error when:
// unsafe.Sizeof(new(unix.ResState).Res_h_errno) != unsafe.Sizeof(new(C.struct___res_state).res_h_errno)
type _ [unsafe.Sizeof(new(unix.ResState).Res_h_errno) - unsafe.Sizeof(new(C.struct___res_state).res_h_errno)]byte
type _ [unsafe.Sizeof(new(C.struct___res_state).res_h_errno) - unsafe.Sizeof(new(unix.ResState).Res_h_errno)]byte

// This will cause a compile error when:
// unsafe.Offsetof(new(unix.ResState).Res_h_errno) != unsafe.Offsetof(new(C.struct___res_state).res_h_errno)
type _ [unsafe.Offsetof(new(unix.ResState).Res_h_errno) - unsafe.Offsetof(new(C.struct___res_state).res_h_errno)]byte
type _ [unsafe.Offsetof(new(C.struct___res_state).res_h_errno) - unsafe.Offsetof(new(unix.ResState).Res_h_errno)]byte
47 changes: 44 additions & 3 deletions src/net/cgo_unix_cgo_res.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,30 @@ package net
#include <string.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include <netdb.h>

#ifdef __GLIBC__
#define is_glibc 1
#else
#define is_glibc 0
#endif

int c_res_search(const char *dname, int class, int type, unsigned char *answer, int anslen, int *herrno) {
int ret = res_search(dname, class, type, answer, anslen);

if (ret < 0) {
*herrno = h_errno;
}

return ret;
}

#cgo !android,!openbsd LDFLAGS: -lresolv
*/
import "C"

import "runtime"

type _C_struct___res_state = struct{}

func _C_res_ninit(state *_C_struct___res_state) error {
Expand All @@ -32,7 +51,29 @@ func _C_res_nclose(state *_C_struct___res_state) {
return
}

func _C_res_nsearch(state *_C_struct___res_state, dname *_C_char, class, typ int, ans *_C_uchar, anslen int) (int, error) {
x, err := C.res_search(dname, C.int(class), C.int(typ), ans, C.int(anslen))
return int(x), err
const isGlibc = C.is_glibc == 1

func _C_res_nsearch(state *_C_struct___res_state, dname *_C_char, class, typ int, ans *_C_uchar, anslen int) (ret int, herrno int, err error) {
var h C.int
x, err := C.c_res_search(dname, C.int(class), C.int(typ), ans, C.int(anslen), &h)

if x <= 0 {
if runtime.GOOS == "linux" {
// On glibc and musl h_errno is a thread-safe macro:
// https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=ed4d0184a16db455d626e64722daf9ca4b71742a;hp=c0b338331e8eb0979b909479d6aa9fd1cddd63ec
// http://git.etalabs.net/cgit/musl/commit/?id=9d0b8b92a508c328e7eac774847f001f80dfb5ff
if isGlibc {
return -1, int(h), err
}
// musl does not set errno with the cause of the failure.
return -1, int(h), nil
}

// On Openbsd h_errno is not thread-safe.
// Android h_errno is also thread-safe: https://android.googlesource.com/platform/bionic/+/589afca/libc/dns/resolv/res_state.c
// but the h_errno doesn't seem to be set on noSuchHost.
return -2, 0, nil
}

return int(x), 0, nil
}
Loading