net: add IsNotFound field to DNSError

This adds the ability to determine if a lookup error was
due to a non-existent hostname. Previously users needed
to do string matching on the DNSError.Err value.

Fixes #28635

Change-Id: If4bd3ad32cbc2db5614f2c6b72e0a9161d813efa
Reviewed-on: https://go-review.googlesource.com/c/go/+/168597
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/src/net/cgo_unix.go b/src/net/cgo_unix.go
index 6420fd0..2baab5f 100644
--- a/src/net/cgo_unix.go
+++ b/src/net/cgo_unix.go
@@ -158,6 +158,7 @@
 	var res *C.struct_addrinfo
 	gerrno, err := C.getaddrinfo((*C.char)(unsafe.Pointer(&h[0])), nil, &hints, &res)
 	if gerrno != 0 {
+		isErrorNoSuchHost := false
 		switch gerrno {
 		case C.EAI_SYSTEM:
 			if err == nil {
@@ -172,10 +173,12 @@
 			}
 		case C.EAI_NONAME:
 			err = errNoSuchHost
+			isErrorNoSuchHost = true
 		default:
 			err = addrinfoErrno(gerrno)
 		}
-		return nil, "", &DNSError{Err: err.Error(), Name: name}
+
+		return nil, "", &DNSError{Err: err.Error(), Name: name, IsNotFound: isErrorNoSuchHost}
 	}
 	defer C.freeaddrinfo(res)
 
diff --git a/src/net/dnsclient_unix.go b/src/net/dnsclient_unix.go
index 7ed4ea8..478ee51 100644
--- a/src/net/dnsclient_unix.go
+++ b/src/net/dnsclient_unix.go
@@ -284,10 +284,8 @@
 				if err == errNoSuchHost {
 					// The name does not exist, so trying
 					// another server won't help.
-					//
-					// TODO: indicate this in a more
-					// obvious way, such as a field on
-					// DNSError?
+
+					dnsErr.IsNotFound = true
 					return p, server, dnsErr
 				}
 				lastErr = dnsErr
@@ -306,9 +304,8 @@
 			if err == errNoSuchHost {
 				// The name does not exist, so trying another
 				// server won't help.
-				//
-				// TODO: indicate this in a more obvious way,
-				// such as a field on DNSError?
+
+				lastErr.(*DNSError).IsNotFound = true
 				return p, server, lastErr
 			}
 		}
@@ -398,7 +395,7 @@
 		// 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}
+		return dnsmessage.Parser{}, "", &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true}
 	}
 	resolvConf.tryUpdate("/etc/resolv.conf")
 	resolvConf.mu.RLock()
@@ -575,7 +572,7 @@
 	}
 	if !isDomainName(name) {
 		// See comment in func lookup above about use of errNoSuchHost.
-		return nil, dnsmessage.Name{}, &DNSError{Err: errNoSuchHost.Error(), Name: name}
+		return nil, dnsmessage.Name{}, &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true}
 	}
 	resolvConf.tryUpdate("/etc/resolv.conf")
 	resolvConf.mu.RLock()
diff --git a/src/net/dnsclient_unix_test.go b/src/net/dnsclient_unix_test.go
index f1ed58c..1b67494 100644
--- a/src/net/dnsclient_unix_test.go
+++ b/src/net/dnsclient_unix_test.go
@@ -666,7 +666,7 @@
 		wantErr      *DNSError
 	}{
 		{true, &DNSError{Name: fqdn, Err: "server misbehaving", IsTemporary: true}},
-		{false, &DNSError{Name: fqdn, Err: errNoSuchHost.Error()}},
+		{false, &DNSError{Name: fqdn, Err: errNoSuchHost.Error(), IsNotFound: true}},
 	}
 	for _, tt := range cases {
 		r := Resolver{PreferGo: true, StrictErrors: tt.strictErrors, Dial: fake.DialContext}
@@ -1138,9 +1138,10 @@
 	}
 	makeNxDomain := func() error {
 		return &DNSError{
-			Err:    errNoSuchHost.Error(),
-			Name:   name,
-			Server: server,
+			Err:        errNoSuchHost.Error(),
+			Name:       name,
+			Server:     server,
+			IsNotFound: true,
 		}
 	}
 
@@ -1472,6 +1473,32 @@
 	}
 }
 
+func TestIssueNoSuchHostExists(t *testing.T) {
+	err := lookupWithFake(fakeDNSServer{
+		rh: func(n, _ string, q dnsmessage.Message, _ time.Time) (dnsmessage.Message, error) {
+			return dnsmessage.Message{
+				Header: dnsmessage.Header{
+					ID:       q.ID,
+					Response: true,
+					RCode:    dnsmessage.RCodeNameError,
+				},
+				Questions: q.Questions,
+			}, nil
+		},
+	}, "golang.org.", dnsmessage.TypeALL)
+	if err == nil {
+		t.Fatal("expected an error")
+	}
+	if _, ok := err.(Error); !ok {
+		t.Fatalf("err = %#v; wanted something supporting net.Error", err)
+	}
+	if de, ok := err.(*DNSError); !ok {
+		t.Fatalf("err = %#v; wanted a *net.DNSError", err)
+	} else if !de.IsNotFound {
+		t.Fatalf("IsNotFound = false for err = %#v; want IsNotFound == true", err)
+	}
+}
+
 // TestNoSuchHost verifies that tryOneName works correctly when the domain does
 // not exist.
 //
@@ -1541,6 +1568,9 @@
 			if de.Err != errNoSuchHost.Error() {
 				t.Fatalf("Err = %#v; wanted %q", de.Err, errNoSuchHost.Error())
 			}
+			if !de.IsNotFound {
+				t.Fatalf("IsNotFound = %v wanted true", de.IsNotFound)
+			}
 		})
 	}
 }
diff --git a/src/net/lookup.go b/src/net/lookup.go
index 0af1e2c..24d0d25 100644
--- a/src/net/lookup.go
+++ b/src/net/lookup.go
@@ -177,7 +177,7 @@
 	// Make sure that no matter what we do later, host=="" is rejected.
 	// parseIP, for example, does accept empty strings.
 	if host == "" {
-		return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host}
+		return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
 	}
 	if ip, _ := parseIPZone(host); ip != nil {
 		return []string{host}, nil
@@ -238,7 +238,7 @@
 	// Make sure that no matter what we do later, host=="" is rejected.
 	// parseIP, for example, does accept empty strings.
 	if host == "" {
-		return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host}
+		return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
 	}
 	if ip, zone := parseIPZone(host); ip != nil {
 		return []IPAddr{{IP: ip, Zone: zone}}, nil
diff --git a/src/net/lookup_test.go b/src/net/lookup_test.go
index 28a895e..ed477a7 100644
--- a/src/net/lookup_test.go
+++ b/src/net/lookup_test.go
@@ -877,6 +877,9 @@
 	if !strings.HasSuffix(err.Error(), errNoSuchHost.Error()) {
 		t.Fatalf("lookup error = %v, want %v", err, errNoSuchHost)
 	}
+	if !err.(*DNSError).IsNotFound {
+		t.Fatalf("lookup error = %v, want true", err.(*DNSError).IsNotFound)
+	}
 }
 
 func TestLookupContextCancel(t *testing.T) {
diff --git a/src/net/lookup_windows.go b/src/net/lookup_windows.go
index 8a68d18..cd071c5 100644
--- a/src/net/lookup_windows.go
+++ b/src/net/lookup_windows.go
@@ -56,7 +56,12 @@
 			if proto, err := lookupProtocolMap(name); err == nil {
 				return proto, nil
 			}
-			r.err = &DNSError{Err: r.err.Error(), Name: name}
+
+			dnsError := &DNSError{Err: r.err.Error(), Name: name}
+			if r.err == errNoSuchHost {
+				dnsError.IsNotFound = true
+			}
+			r.err = dnsError
 		}
 		return r.proto, r.err
 	case <-ctx.Done():
@@ -98,7 +103,12 @@
 		var result *syscall.AddrinfoW
 		e := syscall.GetAddrInfoW(syscall.StringToUTF16Ptr(name), nil, &hints, &result)
 		if e != nil {
-			return nil, &DNSError{Err: winError("getaddrinfow", e).Error(), Name: name}
+			err := winError("getaddrinfow", e)
+			dnsError := &DNSError{Err: err.Error(), Name: name}
+			if err == errNoSuchHost {
+				dnsError.IsNotFound = true
+			}
+			return nil, dnsError
 		}
 		defer syscall.FreeAddrInfoW(result)
 		addrs := make([]IPAddr, 0, 5)
@@ -176,7 +186,12 @@
 		if port, err := lookupPortMap(network, service); err == nil {
 			return port, nil
 		}
-		return 0, &DNSError{Err: winError("getaddrinfow", e).Error(), Name: network + "/" + service}
+		err := winError("getaddrinfow", e)
+		dnsError := &DNSError{Err: err.Error(), Name: network + "/" + service}
+		if err == errNoSuchHost {
+			dnsError.IsNotFound = true
+		}
+		return 0, dnsError
 	}
 	defer syscall.FreeAddrInfoW(result)
 	if result == nil {
diff --git a/src/net/net.go b/src/net/net.go
index b44ecb6..0e07862 100644
--- a/src/net/net.go
+++ b/src/net/net.go
@@ -579,6 +579,7 @@
 	Server      string // server used
 	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
 }
 
 func (e *DNSError) Error() string {