package main import ( "encoding/binary" "net" "os" "path/filepath" "strings" "testing" "time" ) // respond answers one query: novox.internal A 10.10.0.1 via a compressed name, anything else rcode. func respond(q []byte, rcode byte) []byte { end := 12 for q[end] != 0 { end += int(q[end]) + 1 } question := q[12 : end+5] name := strings.ToLower(string(q[13:end])) r := append([]byte{}, q[:2]...) r = append(r, 0x81, 0x80|rcode, 0, 1) answer := rcode == 0 && strings.Contains(name, "novox") && binary.BigEndian.Uint16(q[end+1:]) == 1 if answer { r = append(r, 0, 1, 0, 0, 0, 0) } else { r = append(r, 0, 0, 0, 0, 0, 0) } r = append(r, question...) if answer { r = append(r, 0xC0, 12, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 10, 10, 0, 1) } return r } func udpResolver(t *testing.T, rcode byte) string { pc, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { t.Fatal(err) } t.Cleanup(func() { pc.Close() }) go func() { buf := make([]byte, 512) for { n, from, err := pc.ReadFrom(buf) if err != nil { return } _, _ = pc.WriteTo(respond(buf[:n], rcode), from) } }() return pc.LocalAddr().String() } func tcpResolver(t *testing.T) string { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } t.Cleanup(func() { l.Close() }) go func() { for { c, err := l.Accept() if err != nil { return } var size [2]byte if _, err := readFull(c, size[:]); err == nil { q := make([]byte, binary.BigEndian.Uint16(size[:])) if _, err := readFull(c, q); err == nil { r := respond(q, 0) _, _ = c.Write(append(binary.BigEndian.AppendUint16(nil, uint16(len(r))), r...)) } } c.Close() } }() return l.Addr().String() } // Issue 348: the question that could not be asked during the outage — does the resolver at one address answer? func TestDNSAsksANamedResolverOverUDPAndTCPAndSaysItsResponseCode(t *testing.T) { got, err := AskDNS("novox.internal", "A", udpResolver(t, 0), false, time.Second) if err != nil || got.Rcode != "NOERROR" || got.Protocol != "udp" || len(got.Answers) != 1 || got.Answers[0] != "10.10.0.1" || got.Error != "" { t.Fatalf("udp: %+v, %v", got, err) } got, err = AskDNS("novox.internal", "A", tcpResolver(t), true, time.Second) if err != nil || got.Rcode != "NOERROR" || got.Protocol != "tcp" || len(got.Answers) != 1 || got.Answers[0] != "10.10.0.1" { t.Fatalf("tcp: %+v, %v", got, err) } got, _ = AskDNS("nope.internal", "A", udpResolver(t, 3), false, time.Second) if got.Rcode != "NXDOMAIN" || got.Error == "" || len(got.Answers) != 0 { t.Fatalf("no such name is a result with its code: %+v", got) } got, _ = AskDNS("novox.internal", "A", udpResolver(t, 5), false, time.Second) if got.Rcode != "REFUSED" || got.Error == "" { t.Fatalf("a refusal: %+v", got) } } func TestDNSSaysARefusedPortAndRefusesANameAsServer(t *testing.T) { l, _ := net.Listen("tcp", "127.0.0.1:0") addr := l.Addr().String() l.Close() got, err := AskDNS("novox.internal", "A", addr, true, time.Second) if err != nil || !strings.Contains(got.Error, "connection refused") { t.Fatalf("nothing listening is a result naming it: %+v, %v", got, err) } if _, err := AskDNS("novox.internal", "A", "resolver.example", false, time.Second); err == nil { t.Fatal("a server given by name was accepted") } if _, err := AskDNS("novox.internal", "A", "", true, time.Second); err == nil { t.Fatal("tcp without a server was accepted") } if a, _ := serverAddress("10.10.0.1"); a != "10.10.0.1:53" { t.Fatalf("default port: %s", a) } for server, want := range map[string]string{"10.10.0.1:5353": "10.10.0.1:5353", " 127.0.0.1 ": "127.0.0.1:53", "[::1]:53": "[::1]:53", "::1": "[::1]:53"} { if a, err := serverAddress(server); err != nil || a != want { t.Errorf("%q: %q, %v; want %q", server, a, err, want) } } for _, server := range []string{"resolver.example:53", "resolver.example", "10.10.0.1:0", "10.10.0.1:65536", "10.10.0.1:dns", ""} { if a, err := serverAddress(server); err == nil { t.Errorf("%q was taken as %q", server, a) } } } func TestListeningReadsTheKernelsTablesAndSaysWhatItCouldNotSee(t *testing.T) { root := t.TempDir() must := func(err error) { if err != nil { t.Fatal(err) } } must(os.MkdirAll(filepath.Join(root, "net"), 0o755)) head := " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n" // 127.0.0.1:53 and 10.10.0.1:53 listening, a connection on 22 (not listening), and an udp 53 bound on loopback only. must(os.WriteFile(filepath.Join(root, "net", "tcp"), []byte(head+ " 0: 0100007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 111 1 0 100 0 0 10 0\n"+ " 1: 01000A0A:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 112 1 0 100 0 0 10 0\n"+ " 2: 0100007F:0016 0200007F:9C40 01 00000000:00000000 00:00000000 00000000 0 0 113 1 0 100 0 0 10 0\n"), 0o644)) must(os.WriteFile(filepath.Join(root, "net", "udp"), []byte(head+ " 0: 0100007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 121 2 0 0\n"), 0o644)) must(os.MkdirAll(filepath.Join(root, "42", "fd"), 0o755)) must(os.WriteFile(filepath.Join(root, "42", "comm"), []byte("dnsmasq\n"), 0o644)) must(os.Symlink("socket:[111]", filepath.Join(root, "42", "fd", "5"))) got := Listening(root, 53) if got.Count != 3 || len(got.Unread) != 0 { t.Fatalf("%+v", got) } var where []string for _, s := range got.Sockets { where = append(where, s.Protocol+" "+s.Address) } if strings.Join(where, ",") != "tcp 10.10.0.1,tcp 127.0.0.1,udp 127.0.0.1" { t.Fatalf("sockets: %v", where) } if got.Sockets[1].Process != "42/dnsmasq" || got.Sockets[0].Owner == "" || !strings.Contains(got.Owners, "2 socket(s)") { t.Fatalf("owners: %+v", got) } if missing := Listening(t.TempDir(), 0); len(missing.Unread) != 2 { t.Fatalf("an unreadable table is said, not left out: %+v", missing) } } func TestListeningOnThisMachineFindsASocketItOpened(t *testing.T) { if _, err := os.Stat("/proc/net/tcp"); err != nil { t.Skip("no /proc here") } l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() port := l.Addr().(*net.TCPAddr).Port got := Listening("/proc", port) if got.Count != 1 || got.Sockets[0].Address != "127.0.0.1" { t.Fatalf("%+v", got) } if got.Sockets[0].Process == "" { t.Fatalf("its own socket's program is not named: %+v", got) } }