package server import ( "context" "net" "reflect" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/abh/geodns/v3/appconfig" "github.com/abh/geodns/v3/monitor" "github.com/abh/geodns/v3/zones" "github.com/miekg/dns" ) const ( PORT = ":8853" ) func TestServe(t *testing.T) { serverInfo := &monitor.ServerInfo{} srv := NewServer(appconfig.Config, serverInfo) ctx, cancel := context.WithCancel(context.Background()) mm, err := zones.NewMuxManager("../dns", srv) if err != nil { t.Fatalf("Loading test zones: %s", err) } go mm.Run(ctx) go func() { srv.ListenAndServe(ctx, PORT) }() // ensure service has properly started before we query it time.Sleep(500 * time.Millisecond) t.Run("Serving", testServing) t.Run("QueryLog", testQueryLog(srv)) // todo: run test queries? cancel() srv.Shutdown() } func testServing(t *testing.T) { r := exchange(t, "_status.pgeodns.", dns.TypeTXT) require.Len(t, r.Answer, 1, "1 txt record for _status.pgeodns") txt := r.Answer[0].(*dns.TXT).Txt[0] if !strings.HasPrefix(txt, "{") { t.Log("Unexpected result for _status.pgeodns", txt) t.Fail() } // Allow _country and _status queries as long as the first label is that r = exchange(t, "_country.foo.pgeodns.", dns.TypeTXT) txt = r.Answer[0].(*dns.TXT).Txt[0] // Got appropriate response for _country txt query if !strings.HasPrefix(txt, "127.0.0.1:") { t.Log("Unexpected result for _country.foo.pgeodns", txt) t.Fail() } // Make sure A requests for _status doesn't NXDOMAIN r = exchange(t, "_status.pgeodns.", dns.TypeA) if len(r.Answer) != 0 { t.Log("got A record for _status.pgeodns") t.Fail() } if len(r.Ns) != 1 { t.Logf("Expected 1 SOA record, got %d", len(r.Ns)) t.Fail() } // NOERROR for A request checkRcode(t, r.Rcode, dns.RcodeSuccess, "_status.pgeodns") // bar is an alias r = exchange(t, "bar.test.example.com.", dns.TypeA) ip := r.Answer[0].(*dns.A).A if ip.String() != "192.168.1.2" { t.Logf("unexpected A record for bar.test.example.com: %s", ip.String()) t.Fail() } // bar is an alias to test, the SOA record should be for test r = exchange(t, "_.root-alias.test.example.com.", dns.TypeA) if len(r.Answer) > 0 { t.Errorf("got answers for _.root-alias.test.example.com") } if len(r.Ns) == 0 { t.Fatalf("_.root-alias.test didn't return auth section") } if n := r.Ns[0].(*dns.SOA).Header().Name; n != "test.example.com." { t.Fatalf("_.root-alias.test didn't have test.example.com soa: %s", n) } // root-alias is an alias to test (apex), but the NS records shouldn't be on root-alias r = exchange(t, "root-alias.test.example.com.", dns.TypeNS) if len(r.Answer) > 0 { t.Errorf("got unexpected answers for root-alias.test.example.com NS") } if len(r.Ns) == 0 { t.Fatalf("root-alias.test NS didn't return auth section") } r = exchange(t, "test.example.com.", dns.TypeSOA) soa := r.Answer[0].(*dns.SOA) serial := soa.Serial assert.Equal(t, 3, int(serial)) // no AAAA records for 'bar', so check we get a soa record back r = exchange(t, "bar.test.example.com.", dns.TypeAAAA) soa2 := r.Ns[0].(*dns.SOA) if !reflect.DeepEqual(soa, soa2) { t.Errorf("AAAA empty NOERROR soa record different from SOA request") } // CNAMEs r = exchange(t, "www.test.example.com.", dns.TypeA) // c.Check(r.Answer[0].(*dns.CNAME).Target, Equals, "geo.bitnames.com.") if int(r.Answer[0].Header().Ttl) != 1800 { t.Logf("unexpected ttl '%d' for geo.bitnames.com (expected %d)", int(r.Answer[0].Header().Ttl), 1800) t.Fail() } //SPF r = exchange(t, "test.example.com.", dns.TypeSPF) assert.Equal(t, r.Answer[0].(*dns.SPF).Txt[0], "v=spf1 ~all") //SRV r = exchange(t, "_sip._tcp.test.example.com.", dns.TypeSRV) assert.Equal(t, r.Answer[0].(*dns.SRV).Target, "sipserver.example.com.") assert.Equal(t, r.Answer[0].(*dns.SRV).Port, uint16(5060)) assert.Equal(t, r.Answer[0].(*dns.SRV).Priority, uint16(10)) assert.Equal(t, r.Answer[0].(*dns.SRV).Weight, uint16(100)) // MX r = exchange(t, "test.example.com.", dns.TypeMX) assert.Equal(t, r.Answer[0].(*dns.MX).Mx, "mx.example.net.") assert.Equal(t, r.Answer[1].(*dns.MX).Mx, "mx2.example.net.") assert.Equal(t, r.Answer[1].(*dns.MX).Preference, uint16(20)) // Verify the first A record was created r = exchange(t, "a.b.c.test.example.com.", dns.TypeA) ip = r.Answer[0].(*dns.A).A assert.Equal(t, ip.String(), "192.168.1.7") // Verify sub-labels are created r = exchange(t, "b.c.test.example.com.", dns.TypeA) assert.Len(t, r.Answer, 0, "expect 0 answer records for b.c.test.example.com") checkRcode(t, r.Rcode, dns.RcodeSuccess, "b.c.test.example.com") r = exchange(t, "c.test.example.com.", dns.TypeA) assert.Len(t, r.Answer, 0, "expect 0 answer records for c.test.example.com") checkRcode(t, r.Rcode, dns.RcodeSuccess, "c.test.example.com") // Verify the first A record was created r = exchange(t, "three.two.one.test.example.com.", dns.TypeA) ip = r.Answer[0].(*dns.A).A assert.Equal(t, ip.String(), "192.168.1.5", "three.two.one.test.example.com A record") // Verify single sub-labels is created and no record is returned r = exchange(t, "two.one.test.example.com.", dns.TypeA) assert.Len(t, r.Answer, 0, "expect 0 answer records for two.one.test.example.com") checkRcode(t, r.Rcode, dns.RcodeSuccess, "two.one.test.example.com") // Verify the A record wasn't over written r = exchange(t, "one.test.example.com.", dns.TypeA) ip = r.Answer[0].(*dns.A).A assert.Equal(t, ip.String(), "192.168.1.6", "one.test.example.com A record") // PTR r = exchange(t, "2.1.168.192.IN-ADDR.ARPA.", dns.TypePTR) assert.Len(t, r.Answer, 1, "expect 1 answer records for 2.1.168.192.IN-ADDR.ARPA") checkRcode(t, r.Rcode, dns.RcodeSuccess, "2.1.168.192.IN-ADDR.ARPA") name := r.Answer[0].(*dns.PTR).Ptr assert.Equal(t, name, "bar.example.com.", "PTR record") } // func TestServingMixedCase(t *testing.T) { // r := exchange(c, "_sTaTUs.pGEOdns.", dns.TypeTXT) // checkRcode(t, r.Rcode, dns.RcodeSuccess, "_sTaTUs.pGEOdns.") // txt := r.Answer[0].(*dns.TXT).Txt[0] // if !strings.HasPrefix(txt, "{") { // t.Log("Unexpected result for _status.pgeodns", txt) // t.Fail() // } // n := "baR.test.eXAmPLe.cOM." // r = exchange(c, n, dns.TypeA) // ip := r.Answer[0].(*dns.A).A // c.Check(ip.String(), Equals, "192.168.1.2") // c.Check(r.Answer[0].Header().Name, Equals, n) // } // func TestCname(t *testing.T) { // // Cname, two possible results // results := make(map[string]int) // for i := 0; i < 10; i++ { // r := exchange(c, "www.se.test.example.com.", dns.TypeA) // // only return one CNAME even if there are multiple options // c.Check(r.Answer, HasLen, 1) // target := r.Answer[0].(*dns.CNAME).Target // results[target]++ // } // // Two possible results from this cname // c.Check(results, HasLen, 2) // } // func testUnknownDomain(t *testing.T) { // r := exchange(t, "no.such.domain.", dns.TypeAAAA) // c.Assert(r.Rcode, Equals, dns.RcodeRefused) // } // func testServingAliases(t *testing.T) { // // Alias, no geo matches // r := exchange(c, "bar-alias.test.example.com.", dns.TypeA) // ip := r.Answer[0].(*dns.A).A // c.Check(ip.String(), Equals, "192.168.1.2") // // Alias to a cname record // r = exchange(c, "www-alias.test.example.com.", dns.TypeA) // c.Check(r.Answer[0].(*dns.CNAME).Target, Equals, "geo.bitnames.com.") // // Alias returning a cname, with geo overrides // r = exchangeSubnet(c, "www-alias.test.example.com.", dns.TypeA, "194.239.134.1") // c.Check(r.Answer, HasLen, 1) // if len(r.Answer) > 0 { // c.Check(r.Answer[0].(*dns.CNAME).Target, Equals, "geo-europe.bitnames.com.") // } // // Alias to Ns records // r = exchange(c, "sub-alias.test.example.org.", dns.TypeNS) // c.Check(r.Answer[0].(*dns.NS).Ns, Equals, "ns1.example.com.") // } // func testServingEDNS(t *testing.T) { // // MX test // r := exchangeSubnet(t, "test.example.com.", dns.TypeMX, "194.239.134.1") // c.Check(r.Answer, HasLen, 1) // if len(r.Answer) > 0 { // c.Check(r.Answer[0].(*dns.MX).Mx, Equals, "mx-eu.example.net.") // } // c.Log("Testing www.test.example.com from .dk, should match www.europe (a cname)") // r = exchangeSubnet(c, "www.test.example.com.", dns.TypeA, "194.239.134.0") // // www.test from .dk IP address gets at least one answer // c.Check(r.Answer, HasLen, 1) // if len(r.Answer) > 0 { // // EDNS-SUBNET test (request A, respond CNAME) // c.Check(r.Answer[0].(*dns.CNAME).Target, Equals, "geo-europe.bitnames.com.") // } // } // func TestServeRace(t *testing.T) { // wg := sync.WaitGroup{} // for i := 0; i < 5; i++ { // wg.Add(1) // go func() { // s.TestServing(t) // wg.Done() // }() // } // wg.Wait() // } // func BenchmarkServingCountryDebug(b *testing.B) { // for i := 0; i < b.N; i++ { // exchange(b, "_country.foo.pgeodns.", dns.TypeTXT) // } // } // func BenchmarkServing(b *testing.B) { // // a deterministic seed is the default anyway, but let's be explicit we want it here. // rnd := rand.NewSource(1) // testNames := []string{"foo.test.example.com.", "one.test.example.com.", // "weight.test.example.com.", "three.two.one.test.example.com.", // "bar.test.example.com.", "0-alias.test.example.com.", // } // for i := 0; i < c.N; i++ { // name := testNames[rnd.Int63()%int64(len(testNames))] // exchange(t, name, dns.TypeA) // } // } func checkRcode(t *testing.T, rcode int, expected int, name string) { if rcode != expected { t.Logf("'%s': rcode!=%s: %s", name, dns.RcodeToString[expected], dns.RcodeToString[rcode]) t.Fail() } } func exchangeSubnet(t *testing.T, name string, dnstype uint16, ip string) *dns.Msg { msg := new(dns.Msg) msg.SetQuestion(name, dnstype) o := new(dns.OPT) o.Hdr.Name = "." o.Hdr.Rrtype = dns.TypeOPT e := new(dns.EDNS0_SUBNET) e.Code = dns.EDNS0SUBNET e.SourceScope = 0 e.Address = net.ParseIP(ip) e.Family = 1 // IP4 e.SourceNetmask = net.IPv4len * 8 o.Option = append(o.Option, e) msg.Extra = append(msg.Extra, o) t.Log("msg", msg) return dorequest(t, msg) } func exchange(t *testing.T, name string, dnstype uint16) *dns.Msg { msg := new(dns.Msg) msg.SetQuestion(name, dnstype) return dorequest(t, msg) } func dorequest(t *testing.T, msg *dns.Msg) *dns.Msg { cli := new(dns.Client) // cli.ReadTimeout = 2 * time.Second r, _, err := cli.Exchange(msg, "127.0.0.1"+PORT) if err != nil { t.Logf("request err '%s': %s", msg.String(), err) t.Fail() } return r }