// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package net import ( "cmp" "context" "encoding/json" "errors" "fmt" "internal/testenv" "os/exec" "reflect" "regexp" "slices" "strings" "syscall" "testing" ) var nslookupTestServers = []string{"mail.golang.com", "gmail.com"} var lookupTestIPs = []string{"8.8.8.8", "1.1.1.1"} func toJson(v any) string { data, _ := json.Marshal(v) return string(data) } func testLookup(t *testing.T, fn func(*testing.T, *Resolver, string)) { for _, def := range []bool{true, false} { def := def for _, server := range nslookupTestServers { server := server var name string if def { name = "default/" } else { name = "go/" } t.Run(name+server, func(t *testing.T) { t.Parallel() r := DefaultResolver if !def { r = &Resolver{PreferGo: true} } fn(t, r, server) }) } } } func TestNSLookupMX(t *testing.T) { testenv.MustHaveExternalNetwork(t) testLookup(t, func(t *testing.T, r *Resolver, server string) { mx, err := r.LookupMX(context.Background(), server) if err != nil { t.Fatal(err) } if len(mx) == 0 { t.Fatal("no results") } expected, err := nslookupMX(server) if err != nil { t.Skipf("skipping failed nslookup %s test: %s", server, err) } byPrefAndHost := func(a, b *MX) int { if r := cmp.Compare(a.Pref, b.Pref); r != 0 { return r } return strings.Compare(a.Host, b.Host) } slices.SortFunc(expected, byPrefAndHost) slices.SortFunc(mx, byPrefAndHost) if !reflect.DeepEqual(expected, mx) { t.Errorf("different results %s:\texp:%v\tgot:%v", server, toJson(expected), toJson(mx)) } }) } func TestNSLookupCNAME(t *testing.T) { testenv.MustHaveExternalNetwork(t) testLookup(t, func(t *testing.T, r *Resolver, server string) { cname, err := r.LookupCNAME(context.Background(), server) if err != nil { t.Fatalf("failed %s: %s", server, err) } if cname == "" { t.Fatalf("no result %s", server) } expected, err := nslookupCNAME(server) if err != nil { t.Skipf("skipping failed nslookup %s test: %s", server, err) } if expected != cname { t.Errorf("different results %s:\texp:%v\tgot:%v", server, expected, cname) } }) } func TestNSLookupNS(t *testing.T) { testenv.MustHaveExternalNetwork(t) testLookup(t, func(t *testing.T, r *Resolver, server string) { ns, err := r.LookupNS(context.Background(), server) if err != nil { t.Fatalf("failed %s: %s", server, err) } if len(ns) == 0 { t.Fatal("no results") } expected, err := nslookupNS(server) if err != nil { t.Skipf("skipping failed nslookup %s test: %s", server, err) } byHost := func(a, b *NS) int { return strings.Compare(a.Host, b.Host) } slices.SortFunc(expected, byHost) slices.SortFunc(ns, byHost) if !reflect.DeepEqual(expected, ns) { t.Errorf("different results %s:\texp:%v\tgot:%v", toJson(server), toJson(expected), ns) } }) } func TestNSLookupTXT(t *testing.T) { testenv.MustHaveExternalNetwork(t) testLookup(t, func(t *testing.T, r *Resolver, server string) { txt, err := r.LookupTXT(context.Background(), server) if err != nil { t.Fatalf("failed %s: %s", server, err) } if len(txt) == 0 { t.Fatalf("no results") } expected, err := nslookupTXT(server) if err != nil { t.Skipf("skipping failed nslookup %s test: %s", server, err) } slices.Sort(expected) slices.Sort(txt) if !reflect.DeepEqual(expected, txt) { t.Errorf("different results %s:\texp:%v\tgot:%v", server, toJson(expected), toJson(txt)) } }) } func TestLookupLocalPTR(t *testing.T) { testenv.MustHaveExternalNetwork(t) addr, err := localIP() if err != nil { t.Errorf("failed to get local ip: %s", err) } names, err := LookupAddr(addr.String()) if err != nil { t.Errorf("failed %s: %s", addr, err) } if len(names) == 0 { t.Errorf("no results") } expected, err := lookupPTR(addr.String()) if err != nil { t.Skipf("skipping failed lookup %s test: %s", addr.String(), err) } slices.Sort(expected) slices.Sort(names) if !reflect.DeepEqual(expected, names) { t.Errorf("different results %s:\texp:%v\tgot:%v", addr, toJson(expected), toJson(names)) } } func TestLookupPTR(t *testing.T) { testenv.MustHaveExternalNetwork(t) for _, addr := range lookupTestIPs { names, err := LookupAddr(addr) if err != nil { // The DNSError type stores the error as a string, so it cannot wrap the // original error code and we cannot check for it here. However, we can at // least use its error string to identify the correct localized text for // the error to skip. var DNS_ERROR_RCODE_SERVER_FAILURE syscall.Errno = 9002 if strings.HasSuffix(err.Error(), DNS_ERROR_RCODE_SERVER_FAILURE.Error()) { testenv.SkipFlaky(t, 38111) } t.Errorf("failed %s: %s", addr, err) } if len(names) == 0 { t.Errorf("no results") } expected, err := lookupPTR(addr) if err != nil { t.Logf("skipping failed lookup %s test: %s", addr, err) continue } slices.Sort(expected) slices.Sort(names) if !reflect.DeepEqual(expected, names) { t.Errorf("different results %s:\texp:%v\tgot:%v", addr, toJson(expected), toJson(names)) } } } func nslookup(qtype, name string) (string, error) { var out strings.Builder var err strings.Builder cmd := exec.Command("nslookup", "-querytype="+qtype, name) cmd.Stdout = &out cmd.Stderr = &err if err := cmd.Run(); err != nil { return "", err } r := strings.ReplaceAll(out.String(), "\r\n", "\n") // nslookup stderr output contains also debug information such as // "Non-authoritative answer" and it doesn't return the correct errcode if strings.Contains(err.String(), "can't find") { return r, errors.New(err.String()) } return r, nil } func nslookupMX(name string) (mx []*MX, err error) { var r string if r, err = nslookup("mx", name); err != nil { return } mx = make([]*MX, 0, 10) // linux nslookup syntax // golang.org mail exchanger = 2 alt1.aspmx.l.google.com. rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+mail exchanger\s*=\s*([0-9]+)\s*([a-z0-9.\-]+)$`) for _, ans := range rx.FindAllStringSubmatch(r, -1) { pref, _, _ := dtoi(ans[2]) mx = append(mx, &MX{absDomainName(ans[3]), uint16(pref)}) } // windows nslookup syntax // gmail.com MX preference = 30, mail exchanger = alt3.gmail-smtp-in.l.google.com rx = regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+MX preference\s*=\s*([0-9]+)\s*,\s*mail exchanger\s*=\s*([a-z0-9.\-]+)$`) for _, ans := range rx.FindAllStringSubmatch(r, -1) { pref, _, _ := dtoi(ans[2]) mx = append(mx, &MX{absDomainName(ans[3]), uint16(pref)}) } return } func nslookupNS(name string) (ns []*NS, err error) { var r string if r, err = nslookup("ns", name); err != nil { return } ns = make([]*NS, 0, 10) // golang.org nameserver = ns1.google.com. rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+nameserver\s*=\s*([a-z0-9.\-]+)$`) for _, ans := range rx.FindAllStringSubmatch(r, -1) { ns = append(ns, &NS{absDomainName(ans[2])}) } return } func nslookupCNAME(name string) (cname string, err error) { var r string if r, err = nslookup("cname", name); err != nil { return } // mail.golang.com canonical name = golang.org. rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+canonical name\s*=\s*([a-z0-9.\-]+)$`) // assumes the last CNAME is the correct one last := name for _, ans := range rx.FindAllStringSubmatch(r, -1) { last = ans[2] } return absDomainName(last), nil } func nslookupTXT(name string) (txt []string, err error) { var r string if r, err = nslookup("txt", name); err != nil { return } txt = make([]string, 0, 10) // linux // golang.org text = "v=spf1 redirect=_spf.google.com" // windows // golang.org text = // // "v=spf1 redirect=_spf.google.com" rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+text\s*=\s*"(.*)"$`) for _, ans := range rx.FindAllStringSubmatch(r, -1) { txt = append(txt, ans[2]) } return } func ping(name string) (string, error) { cmd := exec.Command("ping", "-n", "1", "-a", name) stdoutStderr, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("%v: %v", err, string(stdoutStderr)) } r := strings.ReplaceAll(string(stdoutStderr), "\r\n", "\n") return r, nil } func lookupPTR(name string) (ptr []string, err error) { var r string if r, err = ping(name); err != nil { return } ptr = make([]string, 0, 10) rx := regexp.MustCompile(`(?m)^Pinging\s+([a-zA-Z0-9.\-]+)\s+\[.*$`) for _, ans := range rx.FindAllStringSubmatch(r, -1) { ptr = append(ptr, absDomainName(ans[1])) } return } func localIP() (ip IP, err error) { conn, err := Dial("udp", "golang.org:80") if err != nil { return nil, err } defer conn.Close() localAddr := conn.LocalAddr().(*UDPAddr) return localAddr.IP, nil }