package stub import ( "math/rand" "reflect" "strings" "testing" "time" "unicode" "github.com/caddyserver/caddy/v2/caddytest" "github.com/miekg/dns" ) const dns_address string = "127.0.0.1:53535" const dns_only string = `{ admin localhost:2999 debug dns 127.0.0.1:53535 { record "sub123.example.com. A 127.0.0.1" record "ABC123. AAAA ::" record "example.com. CAA 0 issue ca.example.net" record "example.com. CNAME test123" record "example.com. 3333 IN NS ns1.example.com." record "example.com. MX 42 mx.example.com." record "_caddy._tcp.example.com. SRV 3 33 2999 test123." record "txt123.example.com. TXT Test123" record "whitespace. TXT Test 123 ABC XYZ" } } ` const dns_only_json string = `{ "admin": { "listen": "localhost:2999" }, "logging": { "logs": { "default": { "level": "DEBUG" } } }, "apps": { "dns": { "address": "127.0.0.1:53535", "records": [ "sub123.example.com.\t3600\tIN\tA\t127.0.0.1", "ABC123.\t3600\tIN\tAAAA\t::", "example.com.\t3600\tIN\tCAA\t0 issue \"ca.example.net\"", "example.com.\t3600\tIN\tCNAME\ttest123.", "example.com.\t3333\tIN\tNS\tns1.example.com.", "example.com.\t3600\tIN\tMX\t42 mx.example.com.", "_caddy._tcp.example.com.\t3600\tIN\tSRV\t3 33 2999 test123.", "txt123.example.com.\t3600\tIN\tTXT\t\"Test123\"", "whitespace.\t3600\tIN\tTXT\t\"Test\" \"123\" \"ABC\" \"XYZ\"" ] } } }` const dns_but_empty string = `{ admin localhost:2999 debug dns 127.0.0.1:53535 } ` const dns_but_empty_json string = `{ "admin": { "listen": "localhost:2999" }, "logging": { "logs": { "default": { "level": "debug" } } }, "apps": { "dns": { "address": "127.0.0.1:53535" } } }` func TestDNSConfig(t *testing.T) { caddytest.AssertAdapt(t, dns_only, "caddyfile", dns_only_json) } func check_exists(t *testing.T, record string) { rr, err := dns.NewRR(record) if err != nil { t.Fatal("invalid record; ", record, "\nerror:", err) } in := query_dns(t, rr.Header().Name, rr.Header().Rrtype) if in.MsgHdr.Rcode != dns.RcodeSuccess { t.Fatal("DNS error: ", dns.RcodeToString[in.MsgHdr.Rcode]) } expected := rr.String() received := in.Answer[0].String() if expected != received { t.Fatal( "record mismatch!", "\nexpected: ", expected, "\nreceived: ", received, ) } // Gives different results than the string comparison. unclear why //if !reflect.DeepEqual(in.Answer[0], rr) { // t.Fatal("answer section mismatch!") //} } func query_dns(t *testing.T, name string, qtype uint16) *dns.Msg { m := new(dns.Msg) m.SetQuestion(name, qtype) c := new(dns.Client) c.DialTimeout = 1 * time.Second in, rtt, err := c.Exchange(m, dns_address) _ = rtt if err != nil { t.Fatal(err) } if !reflect.DeepEqual(in.Question, m.Question) { t.Fatal("question section mismatch!") } return in } func wAcKY_casE(input string) string { INPUT := strings.ToUpper(input) InPuT := "" for i := range input { if rand.Intn(2) == 1 { InPuT = InPuT + INPUT[i:i] } else { InPuT = InPuT + input[i:i] } } return InPuT } // https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00 func qUeRY_dNs(t *testing.T, name string, qtype uint16) *dns.Msg { runes := []rune{} for _, c := range name { if rand.Intn(2) == 1 { runes = append(runes, unicode.ToUpper(c)) } else { runes = append(runes, c) } } nAmE := string(runes) println("before: ", name, "\nafter: ", nAmE) m := new(dns.Msg) m.SetQuestion(nAmE, qtype) c := new(dns.Client) c.DialTimeout = 1 * time.Second in, rtt, err := c.Exchange(m, dns_address) _ = rtt if err != nil { t.Fatal(err) } if !reflect.DeepEqual(in.Question, m.Question) { t.Fatal( "question section mismatch!", "\nsent: ", m.Question, "\nreceived: ", in.Question, ) } return in } func cHEcK_eXiSTs(t *testing.T, record string) { rr, err := dns.NewRR(record) if err != nil { t.Fatal("invalid record; ", record, "\nerror:", err) } in := qUeRY_dNs(t, rr.Header().Name, rr.Header().Rrtype) if in.MsgHdr.Rcode != dns.RcodeSuccess { t.Fatal("DNS error: ", dns.RcodeToString[in.MsgHdr.Rcode]) } expected := rr.String() received := in.Answer[0].String() if expected != received { t.Fatal( "record mismatch!", "\nexpected: ", expected, "\nreceived: ", received, ) } } func check_errors(t *testing.T, m *dns.Msg, rcode int) { c := new(dns.Client) c.DialTimeout = 100 * time.Millisecond in, rtt, err := c.Exchange(m, dns_address) _ = rtt if err != nil { t.Fatal(err, "\n", m) } /* if !reflect.DeepEqual(in.Question, m.Question) { t.Fatal( "question section mismatch!", "\nsent: ", m.Question, "\nreceived: ", in.Question, ) } */ if in.Rcode != rcode { t.Fatal( "rcode mismatch:", "\nexpected: ", rcode, "\nreceived: ", in.Rcode, "\nquery:\n", m, ) } } func check_fails(t *testing.T, m *dns.Msg, error_contains string) { c := new(dns.Client) c.DialTimeout = 100 * time.Millisecond in, rtt, err := c.Exchange(m, dns_address) _ = rtt if in != nil { t.Fatal( "query was expected to fail with: \"", error_contains, "\", but returned response!\n", in.String(), ) } if err == nil { t.Fatal( "query was expected to fail with: \"", error_contains, "\", but did not return an error!", ) } if !strings.Contains(err.Error(), error_contains) { // TODO: this might be flaky t.Fatal( "query was expected to fail with: \"", error_contains, "\", but returned error: ", err, ) } } func TestServer(t *testing.T) { // I guess I have to seed my own RNG like a caveman rand.Seed(time.Now().UnixNano()) caddytest.Default.TestRequestTimeout = 1 * time.Second caddytest.Default.LoadRequestTimeout = 1 * time.Second tester := caddytest.NewTester(t) tester.InitServer(dns_only, "caddyfile") records := []string{ "sub123.example.com. A 127.0.0.1", "ABC123 AAAA ::", "example.com. CAA 0 issue ca.example.net", "example.com CNAME test123", "example.com. 3333 IN NS ns1.example.com.", "example.com. MX 42 mx.example.com.", "_caddy._tcp.example.com. SRV 3 33 2999 test123.", "txt123.example.com. TXT Test123", "whitespace. TXT Test 123 ABC XYZ", } for _, record := range records { check_exists(t, record) /* WACKY-CASE QUERIES */ cHEcK_eXiSTs(t, record) } empty := new(dns.Msg) check_errors(t, empty, dns.RcodeFormatError) non_existent := new(dns.Msg) non_existent.SetQuestion("does.not.exist.example.com.", dns.TypeA) check_errors(t, non_existent, dns.RcodeNameError) chaos := new(dns.Msg) chaos.SetQuestion("sub123.example.com.", dns.TypeA) chaos.Question[0].Qclass = dns.ClassCHAOS check_errors(t, chaos, dns.RcodeNotImplemented) /* IGNORE NON-QUESTIONS */ question := new(dns.Msg) question.SetQuestion("sub123.example.com.", dns.TypeA) not_a_question := new(dns.Msg) not_a_question.SetReply(not_a_question) answer, _ := dns.NewRR("sub123.example.com. A 127.0.0.1") not_a_question.Answer = []dns.RR{answer} check_fails(t, not_a_question, "timeout") /* REFUSE MULTI-QUESTIONS */ multi := new(dns.Msg) multi.SetQuestion("example.com.", dns.TypeCNAME) q1 := multi.Question[0] multi.SetQuestion("sub123.example.com.", dns.TypeA) multi.Question = append(multi.Question, q1) check_errors(t, multi, dns.RcodeFormatError) } // The server only listens for DNS queries if there are records to serve func TestEmptyServer(t *testing.T) { caddytest.Default.TestRequestTimeout = 1 * time.Second caddytest.Default.LoadRequestTimeout = 1 * time.Second tester := caddytest.NewTester(t) tester.InitServer(dns_but_empty, "caddyfile") question := new(dns.Msg) question.SetQuestion("sub123.example.com.", dns.TypeA) check_fails(t, question, "refused") }