From 41bc1fff98f45405977069bfd3af6a5c7f2e18f3 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Tue, 22 Aug 2023 08:08:22 +0200 Subject: [PATCH] net: rework CNAME handling --- src/internal/syscall/unix/net_darwin.go | 5 +- src/net/cgo_stub.go | 14 +- src/net/cgo_unix.go | 164 ++++++++++++++++++------ src/net/cgo_unix_cgo.go | 9 ++ src/net/cgo_unix_cgo_darwin.go | 10 ++ src/net/cgo_unix_cgo_res.go | 47 ++++++- src/net/cgo_unix_cgo_resn.go | 9 +- src/net/cgo_unix_syscall.go | 19 ++- src/net/cgo_unix_test.go | 20 +++ src/net/dnsclient.go | 12 +- src/net/dnsclient_unix.go | 139 +++++++++++++------- src/net/dnsclient_unix_test.go | 2 +- src/net/lookup.go | 26 ++-- src/net/lookup_plan9.go | 6 +- src/net/lookup_test.go | 69 ++++++++++ src/net/lookup_unix.go | 34 ++++- src/net/lookup_windows.go | 4 +- src/net/net.go | 14 ++ 18 files changed, 477 insertions(+), 126 deletions(-) diff --git a/src/internal/syscall/unix/net_darwin.go b/src/internal/syscall/unix/net_darwin.go index bbaa94b0d21d49..18a68f65e13a01 100644 --- a/src/internal/syscall/unix/net_darwin.go +++ b/src/internal/syscall/unix/net_darwin.go @@ -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" diff --git a/src/net/cgo_stub.go b/src/net/cgo_stub.go index a4f6b4b0e8d8b7..eb6d720196df0b 100644 --- a/src/net/cgo_stub.go +++ b/src/net/cgo_stub.go @@ -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{} diff --git a/src/net/cgo_unix.go b/src/net/cgo_unix.go index 0a783d08a9f15b..5476bd2055fa21 100644 --- a/src/net/cgo_unix.go +++ b/src/net/cgo_unix.go @@ -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() @@ -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) @@ -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 { @@ -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 }) } @@ -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)) @@ -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 } @@ -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 } diff --git a/src/net/cgo_unix_cgo.go b/src/net/cgo_unix_cgo.go index 7c609eddbf76cd..ddb200eeb9a32e 100644 --- a/src/net/cgo_unix_cgo.go +++ b/src/net/cgo_unix_cgo.go @@ -17,6 +17,7 @@ package net #include #include #include +#include #ifndef EAI_NODATA #define EAI_NODATA -5 @@ -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 @@ -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 } diff --git a/src/net/cgo_unix_cgo_darwin.go b/src/net/cgo_unix_cgo_darwin.go index 40d5e426f25ce5..a177a1255357ea 100644 --- a/src/net/cgo_unix_cgo_darwin.go +++ b/src/net/cgo_unix_cgo_darwin.go @@ -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 diff --git a/src/net/cgo_unix_cgo_res.go b/src/net/cgo_unix_cgo_res.go index 37bbc9a762d8ae..8683f9982ba0cc 100644 --- a/src/net/cgo_unix_cgo_res.go +++ b/src/net/cgo_unix_cgo_res.go @@ -17,11 +17,30 @@ package net #include #include #include +#include + +#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 { @@ -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 } diff --git a/src/net/cgo_unix_cgo_resn.go b/src/net/cgo_unix_cgo_resn.go index 4a5ff165dfae23..a638ac5d7ca140 100644 --- a/src/net/cgo_unix_cgo_resn.go +++ b/src/net/cgo_unix_cgo_resn.go @@ -33,7 +33,10 @@ func _C_res_nclose(state *_C_struct___res_state) { C.res_nclose(state) } -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_nsearch(state, dname, C.int(class), C.int(typ), ans, C.int(anslen)) - return int(x), err +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) { + x, _ := C.res_nsearch(state, dname, C.int(class), C.int(typ), ans, C.int(anslen)) + if x <= 0 { + return -1, int(state.res_h_errno), nil + } + return int(x), 0, nil } diff --git a/src/net/cgo_unix_syscall.go b/src/net/cgo_unix_syscall.go index ac9aaa78fe7c2b..4f8d1c560a9364 100644 --- a/src/net/cgo_unix_syscall.go +++ b/src/net/cgo_unix_syscall.go @@ -13,6 +13,12 @@ import ( "unsafe" ) +const ( + _C_HOST_NOT_FOUND = 1 + _C_TRY_AGAIN = 2 + _C_NO_DATA = 4 +) + const ( _C_AF_INET = syscall.AF_INET _C_AF_INET6 = syscall.AF_INET6 @@ -40,6 +46,10 @@ type ( _C_struct_sockaddr = syscall.RawSockaddr ) +func _C_GoString(p *_C_char) string { + return unix.GoString(p) +} + func _C_free(p unsafe.Pointer) { runtime.KeepAlive(p) } func _C_malloc(n uintptr) unsafe.Pointer { @@ -50,6 +60,7 @@ func _C_malloc(n uintptr) unsafe.Pointer { } func _C_ai_addr(ai *_C_struct_addrinfo) **_C_struct_sockaddr { return &ai.Addr } +func _C_ai_canonname(ai *_C_struct_addrinfo) **_C_char { return &ai.Canonname } func _C_ai_family(ai *_C_struct_addrinfo) *_C_int { return &ai.Family } func _C_ai_flags(ai *_C_struct_addrinfo) *_C_int { return &ai.Flags } func _C_ai_next(ai *_C_struct_addrinfo) **_C_struct_addrinfo { return &ai.Next } @@ -73,8 +84,12 @@ func _C_res_ninit(state *_C_struct___res_state) error { return nil } -func _C_res_nsearch(state *_C_struct___res_state, dname *_C_char, class, typ int, ans *_C_char, anslen int) (int, error) { - return unix.ResNsearch(state, dname, class, typ, ans, anslen) +func _C_res_nsearch(state *_C_struct___res_state, dname *_C_char, class, typ int, ans *_C_char, anslen int) (ret int, herrno int, err error) { + x, _ := unix.ResNsearch(state, dname, class, typ, ans, anslen) + if x <= 0 { + return -1, int(state.Res_h_errno), nil + } + return x, 0, nil } func _C_res_nclose(state *_C_struct___res_state) { diff --git a/src/net/cgo_unix_test.go b/src/net/cgo_unix_test.go index d8233dfaf22960..4bb6a1d6d6af79 100644 --- a/src/net/cgo_unix_test.go +++ b/src/net/cgo_unix_test.go @@ -8,6 +8,8 @@ package net import ( "context" + "errors" + "runtime" "testing" ) @@ -67,3 +69,21 @@ func TestCgoLookupPTRWithCancel(t *testing.T) { t.Error(err) } } + +func TestCgoLookupCNAMEHErrno(t *testing.T) { + defer dnsWaitGroup.Wait() + _, err := cgoLookupCNAME(context.Background(), "invalid.invalid") + + if runtime.GOOS == "openbsd" || runtime.GOOS == "android" { + if err != errCgoDNSLookupFailed { + t.Fatalf("unexpected error: %v", err) + } + return + } + + var dnsErr *DNSError + errors.As(err, &dnsErr) + if dnsErr == nil || dnsErr.Err != errNoSuchHost.Error() || !dnsErr.IsNotFound { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/src/net/dnsclient.go b/src/net/dnsclient.go index b609dbd468f698..4e81204cefca26 100644 --- a/src/net/dnsclient.go +++ b/src/net/dnsclient.go @@ -49,20 +49,12 @@ func reverseaddr(addr string) (arpa string, err error) { return string(buf), nil } -func equalASCIIName(x, y dnsmessage.Name) bool { +func equalASCIIName(x, y *dnsmessage.Name) bool { if x.Length != y.Length { return false } for i := 0; i < int(x.Length); i++ { - a := x.Data[i] - b := y.Data[i] - if 'A' <= a && a <= 'Z' { - a += 0x20 - } - if 'A' <= b && b <= 'Z' { - b += 0x20 - } - if a != b { + if lowerASCII(x.Data[i]) != lowerASCII(y.Data[i]) { return false } } diff --git a/src/net/dnsclient_unix.go b/src/net/dnsclient_unix.go index c291d5eb4f20e0..d0e9bce00c0d93 100644 --- a/src/net/dnsclient_unix.go +++ b/src/net/dnsclient_unix.go @@ -91,7 +91,7 @@ func checkResponse(reqID uint16, reqQues dnsmessage.Question, respHdr dnsmessage if reqID != respHdr.ID { return false } - if reqQues.Type != respQues.Type || reqQues.Class != respQues.Class || !equalASCIIName(reqQues.Name, respQues.Name) { + if reqQues.Type != respQues.Type || reqQues.Class != respQues.Class || !equalASCIIName(&reqQues.Name, &respQues.Name) { return false } return true @@ -323,7 +323,6 @@ func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string, if err == errNoSuchHost { // The name does not exist, so trying // another server won't help. - dnsErr.IsNotFound = true return p, server, dnsErr } @@ -345,6 +344,7 @@ func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string, // server won't help. lastErr.(*DNSError).IsNotFound = true + lastErr.(*DNSError).isNoData = true return p, server, lastErr } } @@ -439,26 +439,22 @@ func (conf *resolverConfig) releaseSema() { <-conf.ch } -func (r *Resolver) lookup(ctx context.Context, name string, qtype dnsmessage.Type, conf *dnsConfig) (dnsmessage.Parser, string, error) { +func (r *Resolver) lookup(ctx context.Context, name string, qtype dnsmessage.Type, conf *dnsConfig) (p dnsmessage.Parser, server string, queriedName string, err error) { if !isDomainName(name) { // We used to use "invalid domain name" as the error, // but that is a detail of the specific lookup mechanism. // Other lookups might allow broader name syntax // (for example Multicast DNS allows UTF-8; see RFC 6762). // For consistency with libc resolvers, report no such host. - return dnsmessage.Parser{}, "", &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true} + return dnsmessage.Parser{}, "", "", &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true} } if conf == nil { conf = getSystemDNSConfig() } - var ( - p dnsmessage.Parser - server string - err error - ) for _, fqdn := range conf.nameList(name) { + queriedName = fqdn p, server, err = r.tryOneName(ctx, conf, fqdn, qtype) if err == nil { break @@ -470,7 +466,7 @@ func (r *Resolver) lookup(ctx context.Context, name string, qtype dnsmessage.Typ } } if err == nil { - return p, server, nil + return p, server, queriedName, nil } if err, ok := err.(*DNSError); ok { // Show original name passed to lookup, not suffixed one. @@ -478,7 +474,7 @@ func (r *Resolver) lookup(ctx context.Context, name string, qtype dnsmessage.Typ // just one is misleading. See also golang.org/issue/6324. err.Name = name } - return dnsmessage.Parser{}, "", err + return dnsmessage.Parser{}, "", queriedName, err } // avoidDNS reports whether this is a hostname for which we should not @@ -644,9 +640,6 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin lane := make(chan result, 1) qtypes := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA} - if network == "CNAME" { - qtypes = append(qtypes, dnsmessage.TypeCNAME) - } switch ipVersion(network) { case '4': qtypes = []dnsmessage.Type{dnsmessage.TypeA} @@ -736,10 +729,7 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin break loop } addrs = append(addrs, IPAddr{IP: IP(a.A[:])}) - if cname.Length == 0 && h.Name.Length != 0 { - cname = h.Name - } - + cname = h.Name case dnsmessage.TypeAAAA: aaaa, err := result.p.AAAAResource() if err != nil { @@ -751,24 +741,7 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin break loop } addrs = append(addrs, IPAddr{IP: IP(aaaa.AAAA[:])}) - if cname.Length == 0 && h.Name.Length != 0 { - cname = h.Name - } - - case dnsmessage.TypeCNAME: - c, err := result.p.CNAMEResource() - if err != nil { - lastErr = &DNSError{ - Err: errCannotUnmarshalDNSMessage.Error(), - Name: name, - Server: result.server, - } - break loop - } - if cname.Length == 0 && c.CNAME.Length > 0 { - cname = c.CNAME - } - + cname = h.Name default: if err := result.p.SkipAnswer(); err != nil { lastErr = &DNSError{ @@ -778,7 +751,6 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin } break loop } - continue } } } @@ -789,7 +761,7 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin addrs = nil break } - if len(addrs) > 0 || network == "CNAME" && cname.Length > 0 { + if len(addrs) > 0 { break } } @@ -800,7 +772,7 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin lastErr.Name = name } sortByRFC6724(addrs) - if len(addrs) == 0 && !(network == "CNAME" && cname.Length > 0) { + if len(addrs) == 0 { if order == hostLookupDNSFiles { var canonical string addrs, canonical = goLookupIPFiles(name) @@ -820,10 +792,36 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin return addrs, cname, nil } -// goLookupCNAME is the native Go (non-cgo) implementation of LookupCNAME. -func (r *Resolver) goLookupCNAME(ctx context.Context, host string, order hostLookupOrder, conf *dnsConfig) (string, error) { - _, cname, err := r.goLookupIPCNAMEOrder(ctx, "CNAME", host, order, conf) - return cname.String(), err +// goLookupCanonicalName returns the canonical name of the host +func (r *Resolver) goLookupCanonicalName(ctx context.Context, host string, order hostLookupOrder, conf *dnsConfig) (string, error) { + _, cname, err := r.goLookupIPCNAMEOrder(ctx, "ip", host, order, conf) + if err != nil { + return "", err + } + return cname.String(), nil +} + +// goLookupCNAME queries the CNAME record and returns the last CNAME in the CNAME chain or when +// the DNS query returns no answer records it returns the queried name. +func (r *Resolver) goLookupCNAME(ctx context.Context, host string, conf *dnsConfig) (string, error) { + p, server, queriedName, err := r.lookup(ctx, host, dnsmessage.TypeCNAME, conf) + if err != nil { + var dnsErr *DNSError + if errors.As(err, &dnsErr) && dnsErr.isNoData { + return absDomainName(queriedName), nil + } + return "", err + } + + cname, err := lastCNAMEinChain(dnsmessage.MustNewName(queriedName), p) + if err != nil { + return "", &DNSError{ + Err: err.Error(), + Server: server, + } + } + + return cname, nil } // goLookupPTR is the native Go implementation of LookupAddr. @@ -843,7 +841,7 @@ func (r *Resolver) goLookupPTR(ctx context.Context, addr string, order hostLooku if err != nil { return nil, err } - p, server, err := r.lookup(ctx, arpa, dnsmessage.TypePTR, conf) + p, server, _, err := r.lookup(ctx, arpa, dnsmessage.TypePTR, conf) if err != nil { var dnsErr *DNSError if errors.As(err, &dnsErr) && dnsErr.IsNotFound { @@ -894,3 +892,56 @@ func (r *Resolver) goLookupPTR(ctx context.Context, addr string, order hostLooku return ptrs, nil } + +func lowerDNSName(a *dnsmessage.Name) { + lowerASCIIBytes(a.Data[:a.Length]) +} + +// lastCNAMEinChain returns the last CNAME in CNAME chain. +func lastCNAMEinChain(queryName dnsmessage.Name, p dnsmessage.Parser) (string, error) { + curName := queryName + + // All (or most) responses contain the CNAMEs in order + // so this in practice should never be used. + // The key must be in lower case for case insensitive match. + var cnames map[dnsmessage.Name]dnsmessage.Name + + for { + hdr, err := p.AnswerHeader() + if err == dnsmessage.ErrSectionDone { + break + } + if err != nil || hdr.Class != dnsmessage.ClassINET || hdr.Type != dnsmessage.TypeCNAME { + return "", errCannotUnmarshalDNSMessage + } + cname, err := p.CNAMEResource() + if err != nil { + return "", errCannotUnmarshalDNSMessage + } + + if !equalASCIIName(&curName, &hdr.Name) { + // Out of order CNAME record; add it to the map for later processing. + if cnames == nil { + cnames = make(map[dnsmessage.Name]dnsmessage.Name) + } + lowerDNSName(&hdr.Name) + // Not lowering the cname ASCII case here to preserve the DNS case. + cnames[hdr.Name] = cname.CNAME + continue + } + + curName = cname.CNAME + } + + for len(cnames) != 0 { + lowerDNSName(&curName) + cname, ok := cnames[curName] + if !ok { + return "", errCannotUnmarshalDNSMessage + } + delete(cnames, curName) + curName = cname + } + + return curName.String(), nil +} diff --git a/src/net/dnsclient_unix_test.go b/src/net/dnsclient_unix_test.go index 0da36303cc8887..fba4bc772ccd53 100644 --- a/src/net/dnsclient_unix_test.go +++ b/src/net/dnsclient_unix_test.go @@ -1405,7 +1405,7 @@ func TestStrictErrorsLookupTXT(t *testing.T) { for _, strict := range []bool{true, false} { r := Resolver{StrictErrors: strict, Dial: fake.DialContext} - p, _, err := r.lookup(context.Background(), name, dnsmessage.TypeTXT, nil) + p, _, _, err := r.lookup(context.Background(), name, dnsmessage.TypeTXT, nil) var wantErr error var wantRRs int if strict { diff --git a/src/net/lookup.go b/src/net/lookup.go index 15165970b6f38f..cfe55dae4a6a41 100644 --- a/src/net/lookup.go +++ b/src/net/lookup.go @@ -6,7 +6,6 @@ package net import ( "context" - "errors" "internal/nettrace" "internal/singleflight" "net/netip" @@ -453,7 +452,8 @@ func (r *Resolver) LookupPort(ctx context.Context, network, service string) (por // the canonical name as part of the lookup. // // A canonical name is the final name after following zero -// or more CNAME records. +// or more CNAME records or an os-dependent canonical name of +// the given host. // LookupCNAME does not return an error if host does not // contain DNS "CNAME" records, as long as host resolves to // address records. @@ -473,7 +473,8 @@ func LookupCNAME(host string) (cname string, err error) { // the canonical name as part of the lookup. // // A canonical name is the final name after following zero -// or more CNAME records. +// or more CNAME records or an os-dependent canonical name of +// the given host. // LookupCNAME does not return an error if host does not // contain DNS "CNAME" records, as long as host resolves to // address records. @@ -722,7 +723,7 @@ func (r *Resolver) goLookupSRV(ctx context.Context, service, proto, name string) } else { target = "_" + service + "._" + proto + "." + name } - p, server, err := r.lookup(ctx, target, dnsmessage.TypeSRV, nil) + p, server, _, err := r.lookup(ctx, target, dnsmessage.TypeSRV, nil) if err != nil { return "", nil, err } @@ -768,7 +769,7 @@ func (r *Resolver) goLookupSRV(ctx context.Context, service, proto, name string) // goLookupMX returns the MX records for name. func (r *Resolver) goLookupMX(ctx context.Context, name string) ([]*MX, error) { - p, server, err := r.lookup(ctx, name, dnsmessage.TypeMX, nil) + p, server, _, err := r.lookup(ctx, name, dnsmessage.TypeMX, nil) if err != nil { return nil, err } @@ -812,7 +813,7 @@ func (r *Resolver) goLookupMX(ctx context.Context, name string) ([]*MX, error) { // goLookupNS returns the NS records for name. func (r *Resolver) goLookupNS(ctx context.Context, name string) ([]*NS, error) { - p, server, err := r.lookup(ctx, name, dnsmessage.TypeNS, nil) + p, server, _, err := r.lookup(ctx, name, dnsmessage.TypeNS, nil) if err != nil { return nil, err } @@ -854,7 +855,7 @@ func (r *Resolver) goLookupNS(ctx context.Context, name string) ([]*NS, error) { // goLookupTXT returns the TXT records from name. func (r *Resolver) goLookupTXT(ctx context.Context, name string) ([]string, error) { - p, server, err := r.lookup(ctx, name, dnsmessage.TypeTXT, nil) + p, server, _, err := r.lookup(ctx, name, dnsmessage.TypeTXT, nil) if err != nil { return nil, err } @@ -907,14 +908,3 @@ func (r *Resolver) goLookupTXT(ctx context.Context, name string) ([]string, erro } return txts, nil } - -func parseCNAMEFromResources(resources []dnsmessage.Resource) (string, error) { - if len(resources) == 0 { - return "", errors.New("no CNAME record received") - } - c, ok := resources[0].Body.(*dnsmessage.CNAMEResource) - if !ok { - return "", errors.New("could not parse CNAME record") - } - return c.CNAME.String(), nil -} diff --git a/src/net/lookup_plan9.go b/src/net/lookup_plan9.go index 9d2c4cda5bf53d..2ddd66c353992e 100644 --- a/src/net/lookup_plan9.go +++ b/src/net/lookup_plan9.go @@ -245,14 +245,14 @@ func (*Resolver) lookupPortWithNetwork(ctx context.Context, network, errNetwork, } func (r *Resolver) lookupCNAME(ctx context.Context, name string) (cname string, err error) { - if order, conf := systemConf().hostLookupOrder(r, name); order != hostLookupCgo { - return r.goLookupCNAME(ctx, name, order, conf) + if systemConf().mustUseGoResolver(r) { + return r.goLookupCNAME(ctx, name, getSystemDNSConfig()) } lines, err := queryDNS(ctx, name, "cname") if err != nil { if stringsHasSuffix(err.Error(), "dns failure") || stringsHasSuffix(err.Error(), "resource does not exist; negrcode 0") { - cname = name + "." + cname = absDomainName(name) err = nil } return diff --git a/src/net/lookup_test.go b/src/net/lookup_test.go index 1e222763bd0d0d..90a174bdc6b6b1 100644 --- a/src/net/lookup_test.go +++ b/src/net/lookup_test.go @@ -349,6 +349,10 @@ var lookupCNAMETests = []struct { {"www.google.com", "google.com."}, {"google.com", "google.com."}, {"cname-to-txt.go4.org", "test-txt-record.go4.org."}, + + // Domain that doesn't have any A/AAAA RRs, but has different one (in this case a TXT), + // so that it returns an empty response without any error codes (NXDOMAIN). + {"golang.rsc.io.", "golang.rsc.io."}, } func TestLookupCNAME(t *testing.T) { @@ -1524,3 +1528,68 @@ func allResolvers(t *testing.T, f func(t *testing.T)) { } }) } + +func TestLookupCNAMENoSuchHost(t *testing.T) { + if runtime.GOOS == "plan9" { + t.Skip("no supported on plan9") + } + + mustHaveExternalNetwork(t) + + if runtime.GOOS != "windows" { + testLookupCNAMENoSuchHost(t, "default resolver") + } + + func() { + if runtime.GOOS != "windows" { + defer forceCgoDNS()() + testLookupCNAMENoSuchHost(t, "forced cgo resolver") + } + }() + + func() { + defer forceGoDNS()() + testLookupCNAMENoSuchHost(t, "forced go resolver") + }() +} + +func testLookupCNAMENoSuchHost(t *testing.T, resolver string) { + attempts := 0 + for { + ret, err := LookupCNAME("invalid.invalid.") + if err == nil { + t.Errorf("%v unexpected success: %v", resolver, ret) + return + } + + var dnsErr *DNSError + if errors.As(err, &dnsErr) { + succeeded := true + if !dnsErr.IsNotFound { + succeeded = false + t.Logf("%v: IsNotFound is set to false", resolver) + } + + if dnsErr.Err != errNoSuchHost.Error() { + succeeded = false + t.Logf("%v: error message is not equal to: %v", resolver, errNoSuchHost.Error()) + } + + if succeeded { + return + } + } + + testenv.SkipFlakyNet(t) + if attempts < len(backoffDuration) { + dur := backoffDuration[attempts] + t.Logf("%v: backoff %v after failure %v\n", resolver, dur, err) + time.Sleep(dur) + attempts++ + continue + } + + t.Errorf("%v: unexpected error: %v", resolver, err) + return + } +} diff --git a/src/net/lookup_unix.go b/src/net/lookup_unix.go index 382a2d44bb5cfd..c24ac15315f158 100644 --- a/src/net/lookup_unix.go +++ b/src/net/lookup_unix.go @@ -89,11 +89,41 @@ func (r *Resolver) lookupPort(ctx context.Context, network, service string) (int func (r *Resolver) lookupCNAME(ctx context.Context, name string) (string, error) { order, conf := systemConf().hostLookupOrder(r, name) if order == hostLookupCgo { - if cname, err, ok := cgoLookupCNAME(ctx, name); ok { + cname, err := cgoLookupCanonicalName(ctx, "ip", name) + if err == nil || !isErrorNoSuchHost(err) { return cname, err } + + cname, errCgo := cgoLookupCNAME(ctx, name) + if errCgo != nil { + // errCgoDNSLookupFailed is returned from cgoLookupCNAME on systems + // for which we don't detect the concrete error, if so + // return the noSuchHost error returned from cgoLookupCanonicalName. + // In most cases this is going to be the right error cause here too, + // because cgoLookupCNAME is only executed when cgoLookupCanonicalName returns + // errNosuchHost. + if errCgo == errCgoDNSLookupFailed { + return "", err + } + return "", errCgo + } + return cname, nil + } + + if conf == nil { + conf = getSystemDNSConfig() } - return r.goLookupCNAME(ctx, name, order, conf) + + if !isDomainName(name) { + return "", &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true} + } + + cname, err := r.goLookupCanonicalName(ctx, name, order, conf) + if err == nil || !isErrorNoSuchHost(err) { + return cname, err + } + + return r.goLookupCNAME(ctx, name, conf) } func (r *Resolver) lookupSRV(ctx context.Context, service, proto, name string) (string, []*SRV, error) { diff --git a/src/net/lookup_windows.go b/src/net/lookup_windows.go index b6ef6da716edec..244ac642844bfb 100644 --- a/src/net/lookup_windows.go +++ b/src/net/lookup_windows.go @@ -256,8 +256,8 @@ func (r *Resolver) lookupPort(ctx context.Context, network, service string) (int } func (r *Resolver) lookupCNAME(ctx context.Context, name string) (string, error) { - if order, conf := systemConf().hostLookupOrder(r, name); order != hostLookupCgo { - return r.goLookupCNAME(ctx, name, order, conf) + if systemConf().mustUseGoResolver(r) { + return r.goLookupCNAME(ctx, name, getSystemDNSConfig()) } // TODO(bradfitz): finish ctx plumbing. Nothing currently depends on this. diff --git a/src/net/net.go b/src/net/net.go index 5cfc25ffca8fcf..cf9600ecbf3bf0 100644 --- a/src/net/net.go +++ b/src/net/net.go @@ -617,6 +617,16 @@ var ( errNoSuchHost = errors.New("no such host") ) +func isErrorNoSuchHost(err error) bool { + var e *DNSError + if errors.As(err, &e) { + if e.IsNotFound { + return true + } + } + return false +} + // DNSError represents a DNS lookup error. type DNSError struct { Err string // description of the error @@ -625,6 +635,10 @@ type DNSError struct { IsTimeout bool // if true, timed out; not all timeouts set this IsTemporary bool // if true, error is temporary; not all errors set this IsNotFound bool // if true, host could not be found + + // used internally: if true, dns query returned with + // success, but with no answer records + isNoData bool } func (e *DNSError) Error() string {