Default to upstream to self (#2436)

* Default to upstream to self

This is a backwards incompatible change.

This is a massive (cleanup) PR where we default to resolving external
names by the coredns process itself, instead of directly forwarding them
to some upstream.

This ignores any arguments `upstream` may have had and makes it depend
on proxy/forward configuration in the Corefile. This allows resolved
upstream names to be cached and we have better healthchecking of the
upstreams. It also means there is only one way to resolve names, by
either using the proxy or forward plugin.

The proxy/forward lookup.go functions have been removed. This also
lessen the dependency on proxy, meaning deprecating proxy will become
easier. Some tests have been removed as well, or moved to the top-level
test directory as they now require a full coredns process instead of
just the plugin.

For the etcd plugin, the entire StubZone resolving is *dropped*! This
was a hacky (but working) solution to say the least. If someone cares
deeply it can be brought back (maybe)?

The pkg/upstream is now very small and almost does nothing. Also the
New() function was changed to return a pointer to upstream.Upstream. It
also returns only one parameter, so any stragglers using it will
encounter a compile error.

All documentation has been adapted. This affected the following plugins:
* etcd
* file
* auto
* secondary
* federation
* template
* route53

A followup PR will make any upstream directives with arguments an error,
right now they are ignored.

Signed-off-by: Miek Gieben <miek@miek.nl>

* Fix etcd build - probably still fails unit test

Signed-off-by: Miek Gieben <miek@miek.nl>

* Slightly smarter lookup check in upstream

Signed-off-by: Miek Gieben <miek@miek.nl>

* Compilez

Signed-off-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
Miek Gieben 2019-01-13 16:54:49 +00:00 committed by GitHub
parent 6b56a9c921
commit 9c16ed1d14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 184 additions and 1349 deletions

View file

@ -18,7 +18,7 @@ auto [ZONES...] {
directory DIR [REGEXP ORIGIN_TEMPLATE [TIMEOUT]] directory DIR [REGEXP ORIGIN_TEMPLATE [TIMEOUT]]
reload DURATION reload DURATION
no_reload no_reload
upstream [ADDRESS...] upstream
} }
~~~ ~~~
@ -37,9 +37,7 @@ are used.
and reloads zone when serial changes. and reloads zone when serial changes.
* `no_reload` deprecated. Sets reload to 0. * `no_reload` deprecated. Sets reload to 0.
* `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs) * `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs)
pointing to external names. **ADDRESS** can be an IP address, an IP:port or a string pointing to pointing to external names. CoreDNS will resolve CNAMEs against itself.
a file that is structured as /etc/resolv.conf. If no **ADDRESS** is given, CoreDNS will resolve CNAMEs
against itself.
All directives from the *file* plugin are supported. Note that *auto* will load all zones found, All directives from the *file* plugin are supported. Note that *auto* will load all zones found,
even though the directive might only receive queries for a specific zone. I.e: even though the directive might only receive queries for a specific zone. I.e:

View file

@ -33,7 +33,7 @@ type (
// In the future this should be something like ZoneMeta that contains all this stuff. // In the future this should be something like ZoneMeta that contains all this stuff.
transferTo []string transferTo []string
ReloadInterval time.Duration ReloadInterval time.Duration
upstream upstream.Upstream // Upstream for looking up names during the resolution process. upstream *upstream.Upstream // Upstream for looking up names during the resolution process.
duration time.Duration duration time.Duration
} }

View file

@ -155,15 +155,8 @@ func autoParse(c *caddy.Controller) (Auto, error) {
a.loader.ReloadInterval = 0 a.loader.ReloadInterval = 0
case "upstream": case "upstream":
args := c.RemainingArgs() c.RemainingArgs() // eat remaining args
if len(args) == 0 { a.loader.upstream = upstream.New()
return a, c.ArgErr()
}
var err error
a.loader.upstream, err = upstream.New(args)
if err != nil {
return a, err
}
default: default:
t, _, e := parse.Transfer(c, false) t, _, e := parse.Transfer(c, false)

View file

@ -28,28 +28,22 @@ If you want to `round robin` A and AAAA responses look at the `loadbalance` plug
~~~ ~~~
etcd [ZONES...] { etcd [ZONES...] {
stubzones
fallthrough [ZONES...] fallthrough [ZONES...]
path PATH path PATH
endpoint ENDPOINT... endpoint ENDPOINT...
upstream [ADDRESS...] upstream
tls CERT KEY CACERT tls CERT KEY CACERT
} }
~~~ ~~~
* `stubzones` enables the stub zones feature. The stubzone is *only* done in the etcd tree located
under the *first* zone specified.
* `fallthrough` If zone matches but no record can be generated, pass request to the next plugin. * `fallthrough` If zone matches but no record can be generated, pass request to the next plugin.
If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin
is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only
queries for those zones will be subject to fallthrough. queries for those zones will be subject to fallthrough.
* **PATH** the path inside etcd. Defaults to "/skydns". * **PATH** the path inside etcd. Defaults to "/skydns".
* **ENDPOINT** the etcd endpoints. Defaults to "http://localhost:2379". * **ENDPOINT** the etcd endpoints. Defaults to "http://localhost:2379".
* `upstream` upstream resolvers to be used resolve external names found in etcd (think CNAMEs) * `upstream` resolve names found in etcd (think CNAMEs) If you want CoreDNS to act as a proxy for clients,
pointing to external names. If you want CoreDNS to act as a proxy for clients, you'll need to add you'll need to add the forward plugin. CoreDNS will resolve CNAMEs against itself.
the proxy plugin. If no **ADDRESS** is given, CoreDNS will resolve CNAMEs against itself.
**ADDRESS** can be an IP address, and IP:port or a string pointing to a file that is structured
as /etc/resolv.conf.
* `tls` followed by: * `tls` followed by:
* no arguments, if the server certificate is signed by a system-installed CA and no client cert is needed * no arguments, if the server certificate is signed by a system-installed CA and no client cert is needed
@ -79,10 +73,9 @@ This is the default SkyDNS setup, with everything specified in full:
~~~ corefile ~~~ corefile
. { . {
etcd skydns.local { etcd skydns.local {
stubzones
path /skydns path /skydns
endpoint http://localhost:2379 endpoint http://localhost:2379
upstream 8.8.8.8:53 8.8.4.4:53 upstream
} }
prometheus prometheus
cache 160 skydns.local cache 160 skydns.local
@ -98,7 +91,7 @@ when resolving external pointing CNAMEs.
. { . {
etcd skydns.local { etcd skydns.local {
path /skydns path /skydns
upstream /etc/resolv.conf upstream
} }
cache 160 skydns.local cache 160 skydns.local
proxy . /etc/resolv.conf proxy . /etc/resolv.conf
@ -125,7 +118,6 @@ need to add the zone `0.0.10.in-addr.arpa` to the list of zones. Showing a snipp
~~~ ~~~
etcd skydns.local 10.0.0.0/24 { etcd skydns.local 10.0.0.0/24 {
stubzones
... ...
~~~ ~~~

View file

@ -12,7 +12,6 @@ import (
"github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/etcd/msg" "github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/pkg/fall"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/request" "github.com/coredns/coredns/request"
"github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/plugin/pkg/upstream"
@ -35,10 +34,9 @@ type Etcd struct {
Fall fall.F Fall fall.F
Zones []string Zones []string
PathPrefix string PathPrefix string
Upstream upstream.Upstream // Proxy for looking up names during the resolution process Upstream *upstream.Upstream
Client *etcdcv3.Client Client *etcdcv3.Client
Ctx context.Context Ctx context.Context
Stubmap *map[string]proxy.Proxy // list of proxies for stub resolving.
endpoints []string // Stored here as well, to aid in testing. endpoints []string // Stored here as well, to aid in testing.
} }

View file

@ -14,20 +14,6 @@ func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (
opt := plugin.Options{} opt := plugin.Options{}
state := request.Request{W: w, Req: r, Context: ctx} state := request.Request{W: w, Req: r, Context: ctx}
name := state.Name()
// We need to check stubzones first, because we may get a request for a zone we
// are not auth. for *but* do have a stubzone forward for. If we do the stubzone
// handler will handle the request.
if e.Stubmap != nil && len(*e.Stubmap) > 0 {
for zone := range *e.Stubmap {
if plugin.Name(zone).Matches(name) {
stub := Stub{Etcd: e, Zone: zone}
return stub.ServeDNS(ctx, w, r)
}
}
}
zone := plugin.Zones(e.Zones).Matches(state.Name()) zone := plugin.Zones(e.Zones).Matches(state.Name())
if zone == "" { if zone == "" {
return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r) return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r)

View file

@ -12,7 +12,6 @@ import (
"github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/tls" "github.com/coredns/coredns/plugin/pkg/tls"
"github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -283,9 +282,8 @@ func newEtcdPlugin() *Etcd {
tlsc, _ := tls.NewTLSConfigFromArgs() tlsc, _ := tls.NewTLSConfigFromArgs()
client, _ := newEtcdClient(endpoints, tlsc) client, _ := newEtcdClient(endpoints, tlsc)
p := proxy.NewLookup([]string{"8.8.8.8:53"})
return &Etcd{ return &Etcd{
Upstream: upstream.Upstream{Forward: &p}, Upstream: upstream.New(),
PathPrefix: "skydns", PathPrefix: "skydns",
Ctx: context.Background(), Ctx: context.Background(),
Zones: []string{"skydns.test.", "skydns_extra.test.", "skydns_zonea.test.", "skydns_zoneb.test.", "skydns_zonec.test.", "skydns_zoned.test.", "in-addr.arpa."}, Zones: []string{"skydns.test.", "skydns_extra.test.", "skydns_zonea.test.", "skydns_zoneb.test.", "skydns_zonec.test.", "skydns_zoned.test.", "in-addr.arpa."},

View file

@ -9,7 +9,6 @@ import (
clog "github.com/coredns/coredns/plugin/pkg/log" clog "github.com/coredns/coredns/plugin/pkg/log"
mwtls "github.com/coredns/coredns/plugin/pkg/tls" mwtls "github.com/coredns/coredns/plugin/pkg/tls"
"github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/coredns/coredns/plugin/proxy"
etcdcv3 "github.com/coreos/etcd/clientv3" etcdcv3 "github.com/coreos/etcd/clientv3"
"github.com/mholt/caddy" "github.com/mholt/caddy"
@ -25,18 +24,11 @@ func init() {
} }
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
e, stubzones, err := etcdParse(c) e, err := etcdParse(c)
if err != nil { if err != nil {
return plugin.Error("etcd", err) return plugin.Error("etcd", err)
} }
if stubzones {
c.OnStartup(func() error {
e.UpdateStubZones()
return nil
})
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
e.Next = next e.Next = next
return e return e
@ -45,20 +37,17 @@ func setup(c *caddy.Controller) error {
return nil return nil
} }
func etcdParse(c *caddy.Controller) (*Etcd, bool, error) { func etcdParse(c *caddy.Controller) (*Etcd, error) {
stub := make(map[string]proxy.Proxy)
etc := Etcd{ etc := Etcd{
// Don't default to a proxy for lookups. // Don't default to a proxy for lookups.
// Proxy: proxy.NewLookup([]string{"8.8.8.8:53", "8.8.4.4:53"}), // Proxy: proxy.NewLookup([]string{"8.8.8.8:53", "8.8.4.4:53"}),
PathPrefix: "skydns", PathPrefix: "skydns",
Ctx: context.Background(), Ctx: context.Background(),
Stubmap: &stub,
} }
var ( var (
tlsConfig *tls.Config tlsConfig *tls.Config
err error err error
endpoints = []string{defaultEndpoint} endpoints = []string{defaultEndpoint}
stubzones = false
) )
for c.Next() { for c.Next() {
etc.Zones = c.RemainingArgs() etc.Zones = c.RemainingArgs()
@ -74,38 +63,35 @@ func etcdParse(c *caddy.Controller) (*Etcd, bool, error) {
for { for {
switch c.Val() { switch c.Val() {
case "stubzones": case "stubzones":
stubzones = true // ignored, remove later.
case "fallthrough": case "fallthrough":
etc.Fall.SetZonesFromArgs(c.RemainingArgs()) etc.Fall.SetZonesFromArgs(c.RemainingArgs())
case "debug": case "debug":
/* it is a noop now */ /* it is a noop now */
case "path": case "path":
if !c.NextArg() { if !c.NextArg() {
return &Etcd{}, false, c.ArgErr() return &Etcd{}, c.ArgErr()
} }
etc.PathPrefix = c.Val() etc.PathPrefix = c.Val()
case "endpoint": case "endpoint":
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) == 0 { if len(args) == 0 {
return &Etcd{}, false, c.ArgErr() return &Etcd{}, c.ArgErr()
} }
endpoints = args endpoints = args
case "upstream": case "upstream":
args := c.RemainingArgs() // check args != 0 and error in the future
u, err := upstream.New(args) c.RemainingArgs() // clear buffer
if err != nil { etc.Upstream = upstream.New()
return nil, false, err
}
etc.Upstream = u
case "tls": // cert key cacertfile case "tls": // cert key cacertfile
args := c.RemainingArgs() args := c.RemainingArgs()
tlsConfig, err = mwtls.NewTLSConfigFromArgs(args...) tlsConfig, err = mwtls.NewTLSConfigFromArgs(args...)
if err != nil { if err != nil {
return &Etcd{}, false, err return &Etcd{}, err
} }
default: default:
if c.Val() != "}" { if c.Val() != "}" {
return &Etcd{}, false, c.Errf("unknown property '%s'", c.Val()) return &Etcd{}, c.Errf("unknown property '%s'", c.Val())
} }
} }
@ -117,14 +103,14 @@ func etcdParse(c *caddy.Controller) (*Etcd, bool, error) {
} }
client, err := newEtcdClient(endpoints, tlsConfig) client, err := newEtcdClient(endpoints, tlsConfig)
if err != nil { if err != nil {
return &Etcd{}, false, err return &Etcd{}, err
} }
etc.Client = client etc.Client = client
etc.endpoints = endpoints etc.endpoints = endpoints
return &etc, stubzones, nil return &etc, nil
} }
return &Etcd{}, false, nil return &Etcd{}, nil
} }
func newEtcdClient(endpoints []string, cc *tls.Config) (*etcdcv3.Client, error) { func newEtcdClient(endpoints []string, cc *tls.Config) (*etcdcv3.Client, error) {

View file

@ -56,7 +56,7 @@ func TestSetupEtcd(t *testing.T) {
for i, test := range tests { for i, test := range tests {
c := caddy.NewTestController("dns", test.input) c := caddy.NewTestController("dns", test.input)
etcd, _ /*stubzones*/, err := etcdParse(c) etcd, err := etcdParse(c)
if test.shouldErr && err == nil { if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)

View file

@ -1,81 +0,0 @@
package etcd
import (
"net"
"strconv"
"time"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/pkg/dnsutil"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// UpdateStubZones checks etcd for an update on the stubzones.
func (e *Etcd) UpdateStubZones() {
go func() {
for {
e.updateStubZones()
time.Sleep(15 * time.Second)
}
}()
}
// Look in .../dns/stub/<zone>/xx for msg.Services. Loop through them
// extract <zone> and add them as forwarders (ip:port-combos) for
// the stub zones. Only numeric (i.e. IP address) hosts are used.
// Only the first zone configured on e is used for the lookup.
func (e *Etcd) updateStubZones() {
zone := e.Zones[0]
fakeState := request.Request{W: nil, Req: new(dns.Msg)}
fakeState.Req.SetQuestion(stubDomain+"."+zone, dns.TypeA)
services, err := e.Records(fakeState, false)
if err != nil {
return
}
stubmap := make(map[string]proxy.Proxy)
// track the nameservers on a per domain basis, but allow a list on the domain.
nameservers := map[string][]string{}
Services:
for _, serv := range services {
if serv.Port == 0 {
serv.Port = 53
}
ip := net.ParseIP(serv.Host)
if ip == nil {
log.Warningf("Non IP address stub nameserver: %s", serv.Host)
continue
}
domain := msg.Domain(serv.Key)
labels := dns.SplitDomainName(domain)
// If the remaining name equals any of the zones we have, we ignore it.
for _, z := range e.Zones {
// Chop of left most label, because that is used as the nameserver place holder
// and drop the right most labels that belong to zone.
// We must *also* chop of dns.stub. which means cutting two more labels.
domain = dnsutil.Join(labels[1 : len(labels)-dns.CountLabel(z)-2]...)
if domain == z {
log.Warningf("Skipping nameserver for domain we are authoritative for: %s", domain)
continue Services
}
}
nameservers[domain] = append(nameservers[domain], net.JoinHostPort(serv.Host, strconv.Itoa(serv.Port)))
}
for domain, nss := range nameservers {
stubmap[domain] = proxy.NewLookup(nss)
}
// atomic swap (at least that's what we hope it is)
if len(stubmap) > 0 {
e.Stubmap = &stubmap
}
return
}

View file

@ -1,83 +0,0 @@
package etcd
import (
"context"
"errors"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// Stub wraps an Etcd. We have this type so that it can have a ServeDNS method.
type Stub struct {
*Etcd
Zone string // for what zone (and thus what nameservers are we called)
}
// ServeDNS implements the plugin.Handler interface.
func (s Stub) ServeDNS(ctx context.Context, w dns.ResponseWriter, req *dns.Msg) (int, error) {
if hasStubEdns0(req) {
log.Warningf("Forwarding cycle detected, refusing msg: %s", req.Question[0].Name)
return dns.RcodeRefused, errors.New("stub forward cycle")
}
req = addStubEdns0(req)
proxy, ok := (*s.Etcd.Stubmap)[s.Zone]
if !ok { // somebody made a mistake..
return dns.RcodeServerFailure, nil
}
state := request.Request{W: w, Req: req}
m, e := proxy.Forward(state)
if e != nil {
return dns.RcodeServerFailure, e
}
w.WriteMsg(m)
return dns.RcodeSuccess, nil
}
// hasStubEdns0 checks if the message is carrying our special edns0 zero option.
func hasStubEdns0(m *dns.Msg) bool {
option := m.IsEdns0()
if option == nil {
return false
}
for _, o := range option.Option {
if o.Option() == ednsStubCode && len(o.(*dns.EDNS0_LOCAL).Data) == 1 &&
o.(*dns.EDNS0_LOCAL).Data[0] == 1 {
return true
}
}
return false
}
// addStubEdns0 adds our special option to the message's OPT record.
func addStubEdns0(m *dns.Msg) *dns.Msg {
option := m.IsEdns0()
// Add a custom EDNS0 option to the packet, so we can detect loops when 2 stubs are forwarding to each other.
if option != nil {
option.Option = append(option.Option, &dns.EDNS0_LOCAL{Code: ednsStubCode, Data: []byte{1}})
return m
}
m.Extra = append(m.Extra, ednsStub)
return m
}
const (
ednsStubCode = dns.EDNS0LOCALSTART + 10
stubDomain = "stub.dns"
)
var ednsStub = func() *dns.OPT {
o := new(dns.OPT)
o.Hdr.Name = "."
o.Hdr.Rrtype = dns.TypeOPT
o.SetUDPSize(4096)
e := new(dns.EDNS0_LOCAL)
e.Code = ednsStubCode
e.Data = []byte{1}
o.Option = append(o.Option, e)
return o
}()

View file

@ -1,87 +0,0 @@
// +build etcd
package etcd
import (
"net"
"strconv"
"testing"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
func fakeStubServerExampleNet(t *testing.T) (*dns.Server, string) {
server, addr, err := test.UDPServer("127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create a UDP server: %s", err)
}
// add handler for example.net
dns.HandleFunc("example.net.", func(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
m.Answer = []dns.RR{test.A("example.net. 86400 IN A 93.184.216.34")}
w.WriteMsg(m)
})
return server, addr
}
func TestStubLookup(t *testing.T) {
server, addr := fakeStubServerExampleNet(t)
defer server.Shutdown()
host, p, _ := net.SplitHostPort(addr)
port, _ := strconv.Atoi(p)
exampleNetStub := &msg.Service{Host: host, Port: port, Key: "a.example.net.stub.dns.skydns.test."}
servicesStub = append(servicesStub, exampleNetStub)
etc := newEtcdPlugin()
for _, serv := range servicesStub {
set(t, etc, serv.Key, 0, serv)
defer delete(t, etc, serv.Key)
}
etc.updateStubZones()
for _, tc := range dnsTestCasesStub {
m := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := etc.ServeDNS(ctxt, rec, m)
if err != nil && m.Question[0].Name == "example.org." {
// This is OK, we expect this backend to *not* work.
continue
}
if err != nil {
t.Errorf("Expected no error, got %v for %s\n", err, m.Question[0].Name)
}
resp := rec.Msg
if resp == nil {
// etcd not running?
continue
}
test.SortAndCheck(t, resp, tc)
}
}
var servicesStub = []*msg.Service{
// Two tests, ask a question that should return servfail because remote it no accessible
// and one with edns0 option added, that should return refused.
{Host: "127.0.0.1", Port: 666, Key: "b.example.org.stub.dns.skydns.test."},
}
var dnsTestCasesStub = []test.Case{
{
Qname: "example.org.", Qtype: dns.TypeA, Rcode: dns.RcodeServerFailure,
},
{
Qname: "example.net.", Qtype: dns.TypeA,
Answer: []dns.RR{test.A("example.net. 86400 IN A 93.184.216.34")},
},
}

View file

@ -17,16 +17,14 @@ Enabling *federation* without also having *kubernetes* is a noop.
~~~ ~~~
federation [ZONES...] { federation [ZONES...] {
NAME DOMAIN NAME DOMAIN
upstream [ADDRESS...] upstream
} }
~~~ ~~~
* Each **NAME** and **DOMAIN** defines federation membership. One entry for each. A duplicate * Each **NAME** and **DOMAIN** defines federation membership. One entry for each. A duplicate
**NAME** will silently overwrite any previous value. **NAME** will silently overwrite any previous value.
* `upstream` [**ADDRESS**...] defines the upstream resolvers used for resolving the `CNAME` target * `upstream` [**ADDRESS**...] resolve the `CNAME` target produced by this plugin. CoreDNS
produced by this plugin. If no **ADDRESS** is given, CoreDNS will resolve External Services against itself.
will resolve External Services against itself. **ADDRESS** can be an IP, an IP:port, or a path
to a file structured like resolv.conf.
## Examples ## Examples

View file

@ -64,12 +64,8 @@ func federationParse(c *caddy.Controller) (*Federation, error) {
x := c.Val() x := c.Val()
switch x { switch x {
case "upstream": case "upstream":
args := c.RemainingArgs() c.RemainingArgs()
u, err := upstream.New(args) fed.Upstream = upstream.New()
if err != nil {
return nil, err
}
fed.Upstream = &u
default: default:
args := c.RemainingArgs() args := c.RemainingArgs()
if x := len(args); x != 1 { if x := len(args); x != 1 {

View file

@ -29,7 +29,7 @@ file DBFILE [ZONES... ] {
transfer to ADDRESS... transfer to ADDRESS...
reload DURATION reload DURATION
no_reload no_reload
upstream [ADDRESS...] upstream
} }
~~~ ~~~
@ -41,11 +41,9 @@ file DBFILE [ZONES... ] {
Value of `0` means to not scan for changes and reload. For example, `30s` checks the zonefile every 30 seconds Value of `0` means to not scan for changes and reload. For example, `30s` checks the zonefile every 30 seconds
and reloads the zone when serial changes. and reloads the zone when serial changes.
* `no_reload` deprecated. Sets reload to 0. * `no_reload` deprecated. Sets reload to 0.
* `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs) * `upstream` resolve external names found (think CNAMEs) pointing to external names. This is only
pointing to external names. This is only really useful when CoreDNS is configured as a proxy; for really useful when CoreDNS is configured as a proxy; for normal authoritative serving you don't
normal authoritative serving you don't need *or* want to use this. **ADDRESS** can be an IP need *or* want to use this. CoreDNS will resolve CNAMEs against itself.
address, an IP:port or a string pointing to a file that is structured as /etc/resolv.conf.
If no **ADDRESS** is given, CoreDNS will resolve CNAMEs against itself.
## Examples ## Examples

View file

@ -1,124 +0,0 @@
package file
import (
"context"
"strings"
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
func TestLookupCNAMEChain(t *testing.T) {
name := "example.org."
zone, err := Parse(strings.NewReader(dbExampleCNAME), name, "stdin", 0)
if err != nil {
t.Fatalf("Expected no error when reading zone, got %q", err)
}
fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}}
ctx := context.TODO()
for _, tc := range cnameTestCases {
m := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := fm.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("Expected no error, got %v\n", err)
return
}
resp := rec.Msg
test.SortAndCheck(t, resp, tc)
}
}
var cnameTestCases = []test.Case{
{
Qname: "a.example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("a.example.org. 1800 IN A 127.0.0.1"),
},
},
{
Qname: "www3.example.org.", Qtype: dns.TypeCNAME,
Answer: []dns.RR{
test.CNAME("www3.example.org. 1800 IN CNAME www2.example.org."),
},
},
{
Qname: "dangling.example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.CNAME("dangling.example.org. 1800 IN CNAME foo.example.org."),
},
},
{
Qname: "www3.example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("a.example.org. 1800 IN A 127.0.0.1"),
test.CNAME("www.example.org. 1800 IN CNAME a.example.org."),
test.CNAME("www1.example.org. 1800 IN CNAME www.example.org."),
test.CNAME("www2.example.org. 1800 IN CNAME www1.example.org."),
test.CNAME("www3.example.org. 1800 IN CNAME www2.example.org."),
},
},
}
func TestLookupCNAMEExternal(t *testing.T) {
name := "example.org."
zone, err := Parse(strings.NewReader(dbExampleCNAME), name, "stdin", 0)
if err != nil {
t.Fatalf("Expected no error when reading zone, got %q", err)
}
zone.Upstream, _ = upstream.New([]string{"8.8.8.8:53"}) // TODO(miek): point to local instance
fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}}
ctx := context.TODO()
for _, tc := range exernalTestCases {
m := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := fm.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("Expected no error, got %v\n", err)
return
}
resp := rec.Msg
test.SortAndCheck(t, resp, tc)
}
}
var exernalTestCases = []test.Case{
{
Qname: "external.example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.CNAME("external.example.org. 1800 CNAME www.example.net."),
// magic 303 TTL that says: don't check TTL.
test.A("www.example.net. 303 IN A 93.184.216.34"),
},
},
}
const dbExampleCNAME = `
$TTL 30M
$ORIGIN example.org.
@ IN SOA linode.atoom.net. miek.miek.nl. (
1282630057 ; Serial
4H ; Refresh
1H ; Retry
7D ; Expire
4H ) ; Negative Cache TTL
a IN A 127.0.0.1
www3 IN CNAME www2
www2 IN CNAME www1
www1 IN CNAME www
www IN CNAME a
dangling IN CNAME foo
external IN CNAME www.example.net.`

View file

@ -1,5 +1,8 @@
package file package file
/*
TODO(miek): move to test/ for full server testing
import ( import (
"context" "context"
"strings" "strings"
@ -294,3 +297,4 @@ ns.example.org. 1800 IN A 127.0.0.1
RXpMdvaE6ZDwalWldLjC3h8QDywDoFdndoRY RXpMdvaE6ZDwalWldLjC3h8QDywDoFdndoRY
eHOsmTvvtWWqtO6Fa5A8gmHT5HA= ) eHOsmTvvtWWqtO6Fa5A8gmHT5HA= )
` `
*/

View file

@ -374,7 +374,6 @@ func cnameForType(targets []dns.RR, origQtype uint16) []dns.RR {
func (z *Zone) externalLookup(state request.Request, target string, qtype uint16) []dns.RR { func (z *Zone) externalLookup(state request.Request, target string, qtype uint16) []dns.RR {
m, e := z.Upstream.Lookup(state, target, qtype) m, e := z.Upstream.Lookup(state, target, qtype)
if e != nil { if e != nil {
// TODO(miek): Log, or return error here?
return nil return nil
} }
if m == nil { if m == nil {

View file

@ -93,7 +93,7 @@ func fileParse(c *caddy.Controller) (Zones, error) {
} }
reload := 1 * time.Minute reload := 1 * time.Minute
upstr := upstream.Upstream{} upstr := upstream.New()
t := []string{} t := []string{}
var e error var e error
@ -116,11 +116,8 @@ func fileParse(c *caddy.Controller) (Zones, error) {
reload = 0 reload = 0
case "upstream": case "upstream":
args := c.RemainingArgs() // ignore args, will be error later.
upstr, err = upstream.New(args) c.RemainingArgs() // clear buffer
if err != nil {
return Zones{}, err
}
default: default:
return Zones{}, c.Errf("unknown property '%s'", c.Val()) return Zones{}, c.Errf("unknown property '%s'", c.Val())

View file

@ -57,8 +57,8 @@ func TestFileParse(t *testing.T) {
`file ` + zoneFileName1 + ` example.net. { `file ` + zoneFileName1 + ` example.net. {
upstream a upstream a
}`, }`,
true, false, // OK for now as we disregard any options for the `upstream`.
Zones{Names: []string{}}, Zones{Names: []string{"example.net."}},
}, },
{ {
`file ` + zoneFileName1 + ` example.net. { `file ` + zoneFileName1 + ` example.net. {

View file

@ -32,7 +32,7 @@ type Zone struct {
LastReloaded time.Time LastReloaded time.Time
reloadMu sync.RWMutex reloadMu sync.RWMutex
reloadShutdown chan bool reloadShutdown chan bool
Upstream upstream.Upstream // Upstream for looking up names during the resolution process Upstream *upstream.Upstream // Upstream for looking up external names during the resolution process
} }
// Apex contains the apex records of a zone: SOA, NS and their potential signatures. // Apex contains the apex records of a zone: SOA, NS and their potential signatures.

View file

@ -1,69 +0,0 @@
package forward
import (
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/transport"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
func TestForward(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Answer = append(ret.Answer, test.A("example.org. IN A 127.0.0.1"))
w.WriteMsg(ret)
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
f := New()
f.SetProxy(p)
defer f.Close()
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
state.Req.SetQuestion("example.org.", dns.TypeA)
resp, err := f.Forward(state)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
// expect answer section with A record in it
if len(resp.Answer) == 0 {
t.Fatalf("Expected to at least one RR in the answer section, got none: %s", resp)
}
if resp.Answer[0].Header().Rrtype != dns.TypeA {
t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype)
}
if resp.Answer[0].(*dns.A).A.String() != "127.0.0.1" {
t.Errorf("Expected 127.0.0.1, got: %s", resp.Answer[0].(*dns.A).A.String())
}
}
func TestForwardRefused(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Rcode = dns.RcodeRefused
w.WriteMsg(ret)
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
f := New()
f.SetProxy(p)
defer f.Close()
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
state.Req.SetQuestion("example.org.", dns.TypeA)
resp, err := f.Forward(state)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
if resp.Rcode != dns.RcodeRefused {
t.Errorf("Expected rcode to be %d, got %d", dns.RcodeRefused, resp.Rcode)
}
}

View file

@ -1,89 +0,0 @@
// Package forward implements a forwarding proxy. It caches an upstream net.Conn for some time, so if the same
// client returns the upstream's Conn will be precached. Depending on how you benchmark this looks to be
// 50% faster than just opening a new connection for every client. It works with UDP and TCP and uses
// inband healthchecking.
package forward
import (
"context"
"github.com/coredns/coredns/plugin/pkg/transport"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// Forward forward the request in state as-is. Unlike Lookup that adds EDNS0 suffix to the message.
// Forward may be called with a nil f, an error is returned in that case.
func (f *Forward) Forward(state request.Request) (*dns.Msg, error) {
if f == nil {
return nil, ErrNoForward
}
fails := 0
var upstreamErr error
for _, proxy := range f.List() {
if proxy.Down(f.maxfails) {
fails++
if fails < len(f.proxies) {
continue
}
// All upstream proxies are dead, assume healtcheck is complete broken and randomly
// select an upstream to connect to.
proxy = f.List()[0]
}
ret, err := proxy.Connect(context.Background(), state, f.opts)
upstreamErr = err
if err != nil {
if fails < len(f.proxies) {
continue
}
break
}
// Check if the reply is correct; if not return FormErr.
if !state.Match(ret) {
return state.ErrorMessage(dns.RcodeFormatError), nil
}
ret = state.Scrub(ret)
return ret, err
}
if upstreamErr != nil {
return nil, upstreamErr
}
return nil, ErrNoHealthy
}
// Lookup will use name and type to forge a new message and will send that upstream. It will
// set any EDNS0 options correctly so that downstream will be able to process the reply.
// Lookup may be called with a nil f, an error is returned in that case.
func (f *Forward) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) {
if f == nil {
return nil, ErrNoForward
}
req := new(dns.Msg)
req.SetQuestion(name, typ)
state.SizeAndDo(req)
state2 := request.Request{W: state.W, Req: req}
return f.Forward(state2)
}
// NewLookup returns a Forward that can be used for plugin that need an upstream to resolve external names.
// Note that the caller MUST run Close on the forward to stop the health checking goroutines.
func NewLookup(addr []string) *Forward {
f := New()
for i := range addr {
p := NewProxy(addr[i], transport.DNS)
f.SetProxy(p)
}
return f
}

View file

@ -1,43 +0,0 @@
package forward
import (
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/transport"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
func TestLookup(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Answer = append(ret.Answer, test.A("example.org. IN A 127.0.0.1"))
w.WriteMsg(ret)
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
f := New()
f.SetProxy(p)
defer f.Close()
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
resp, err := f.Lookup(state, "example.org.", dns.TypeA)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
// expect answer section with A record in it
if len(resp.Answer) == 0 {
t.Fatalf("Expected to at least one RR in the answer section, got none: %s", resp)
}
if resp.Answer[0].Header().Rrtype != dns.TypeA {
t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype)
}
if resp.Answer[0].(*dns.A).A.String() != "127.0.0.1" {
t.Errorf("Expected 127.0.0.1, got: %s", resp.Answer[0].(*dns.A).A.String())
}
}

View file

@ -32,7 +32,7 @@ import (
type Kubernetes struct { type Kubernetes struct {
Next plugin.Handler Next plugin.Handler
Zones []string Zones []string
Upstream upstream.Upstream Upstream *upstream.Upstream
APIServerList []string APIServerList []string
APIProxy *apiProxy APIProxy *apiProxy
APICertAuth string APICertAuth string

View file

@ -241,12 +241,8 @@ func ParseStanza(c *caddy.Controller) (*Kubernetes, error) {
case "fallthrough": case "fallthrough":
k8s.Fall.SetZonesFromArgs(c.RemainingArgs()) k8s.Fall.SetZonesFromArgs(c.RemainingArgs())
case "upstream": case "upstream":
args := c.RemainingArgs() c.RemainingArgs() // eat remaining args
u, err := upstream.New(args) k8s.Upstream = upstream.New()
if err != nil {
return nil, err
}
k8s.Upstream = u
case "ttl": case "ttl":
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) == 0 { if len(args) == 0 {

View file

@ -7,7 +7,6 @@ import (
"github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/pkg/fall"
"github.com/coredns/coredns/plugin/proxy"
"github.com/mholt/caddy" "github.com/mholt/caddy"
meta "k8s.io/apimachinery/pkg/apis/meta/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -23,7 +22,6 @@ func TestKubernetesParse(t *testing.T) {
expectedLabelSelector string // expected label selector value expectedLabelSelector string // expected label selector value
expectedPodMode string expectedPodMode string
expectedFallthrough fall.F expectedFallthrough fall.F
expectedUpstreams []string
}{ }{
// positive // positive
{ {
@ -36,7 +34,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local test.local`, `kubernetes coredns.local test.local`,
@ -48,7 +45,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -61,7 +57,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -75,7 +70,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -89,7 +83,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -103,7 +96,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -117,7 +109,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -131,7 +122,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -145,7 +135,6 @@ func TestKubernetesParse(t *testing.T) {
"environment=prod", "environment=prod",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -159,7 +148,6 @@ func TestKubernetesParse(t *testing.T) {
"application=nginx,environment in (production,qa,staging)", "application=nginx,environment in (production,qa,staging)",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local test.local { `kubernetes coredns.local test.local {
@ -177,7 +165,6 @@ func TestKubernetesParse(t *testing.T) {
"application=nginx,environment in (production,qa,staging)", "application=nginx,environment in (production,qa,staging)",
podModeDisabled, podModeDisabled,
fall.Root, fall.Root,
nil,
}, },
// negative // negative
{ {
@ -192,7 +179,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -206,7 +192,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -220,7 +205,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -234,7 +218,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -248,7 +231,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -262,7 +244,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -276,7 +257,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
// pods disabled // pods disabled
{ {
@ -291,7 +271,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
// pods insecure // pods insecure
{ {
@ -306,7 +285,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeInsecure, podModeInsecure,
fall.Zero, fall.Zero,
nil,
}, },
// pods verified // pods verified
{ {
@ -321,7 +299,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeVerified, podModeVerified,
fall.Zero, fall.Zero,
nil,
}, },
// pods invalid // pods invalid
{ {
@ -336,7 +313,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeVerified, podModeVerified,
fall.Zero, fall.Zero,
nil,
}, },
// fallthrough with zones // fallthrough with zones
{ {
@ -351,12 +327,11 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.F{Zones: []string{"ip6.arpa.", "inaddr.arpa.", "foo.com."}}, fall.F{Zones: []string{"ip6.arpa.", "inaddr.arpa.", "foo.com."}},
nil,
}, },
// Valid upstream // Valid upstream
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
upstream 13.14.15.16:53 upstream
}`, }`,
false, false,
"", "",
@ -366,22 +341,6 @@ func TestKubernetesParse(t *testing.T) {
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
[]string{"13.14.15.16:53"},
},
// Invalid upstream
{
`kubernetes coredns.local {
upstream 13.14.15.16orange
}`,
true,
"not an IP address or file: \"13.14.15.16orange\"",
-1,
0,
defaultResyncPeriod,
"",
podModeDisabled,
fall.Zero,
nil,
}, },
// More than one Kubernetes not allowed // More than one Kubernetes not allowed
{ {
@ -395,7 +354,6 @@ kubernetes cluster.local`,
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -409,7 +367,6 @@ kubernetes cluster.local`,
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -423,7 +380,6 @@ kubernetes cluster.local`,
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -437,7 +393,6 @@ kubernetes cluster.local`,
"", "",
podModeDisabled, podModeDisabled,
fall.Zero, fall.Zero,
nil,
}, },
{ {
`kubernetes coredns.local { `kubernetes coredns.local {
@ -519,31 +474,6 @@ kubernetes cluster.local`,
if !k8sController.Fall.Equal(test.expectedFallthrough) { if !k8sController.Fall.Equal(test.expectedFallthrough) {
t.Errorf("Test %d: Expected kubernetes controller to be initialized with fallthrough '%v'. Instead found fallthrough '%v' for input '%s'", i, test.expectedFallthrough, k8sController.Fall, test.input) t.Errorf("Test %d: Expected kubernetes controller to be initialized with fallthrough '%v'. Instead found fallthrough '%v' for input '%s'", i, test.expectedFallthrough, k8sController.Fall, test.input)
} }
// upstream
var foundUpstreams *[]proxy.Upstream
if k8sController.Upstream.Forward != nil {
foundUpstreams = k8sController.Upstream.Forward.Upstreams
}
if test.expectedUpstreams == nil {
if foundUpstreams != nil {
t.Errorf("Test %d: Expected kubernetes controller to not be initialized with upstreams for input '%s'", i, test.input)
}
} else {
if foundUpstreams == nil {
t.Errorf("Test %d: Expected kubernetes controller to be initialized with upstreams for input '%s'", i, test.input)
} else {
if len(*foundUpstreams) != len(test.expectedUpstreams) {
t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d upstreams. Instead found %d upstreams for input '%s'", i, len(test.expectedUpstreams), len(*foundUpstreams), test.input)
}
for j, want := range test.expectedUpstreams {
got := (*foundUpstreams)[j].Select().Name
if got != want {
t.Errorf("Test %d: Expected kubernetes controller to be initialized with upstream '%s'. Instead found upstream '%s' for input '%s'", i, want, got, test.input)
}
}
}
}
} }
} }

View file

@ -1,58 +1,35 @@
// Package upstream abstracts a upstream lookups so that plugins // Package upstream abstracts a upstream lookups so that plugins can handle them in an unified way.
// can handle them in an unified way.
package upstream package upstream
import ( import (
"fmt"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin/pkg/nonwriter" "github.com/coredns/coredns/plugin/pkg/nonwriter"
"github.com/coredns/coredns/plugin/pkg/parse"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/request" "github.com/coredns/coredns/request"
) )
// Upstream is used to resolve CNAME targets // Upstream is used to resolve CNAME or other external targets via CoreDNS itself.
type Upstream struct { type Upstream struct{}
self bool
Forward *proxy.Proxy
}
// New creates a new Upstream for given destination(s). If dests is empty it default to upstreaming to // New creates a new Upstream to resolve names using the the coredns process.
// the coredns process. func New() *Upstream { return &Upstream{} }
func New(dests []string) (Upstream, error) {
u := Upstream{}
if len(dests) == 0 {
u.self = true
return u, nil
}
u.self = false
ups, err := parse.HostPortOrFile(dests...)
if err != nil {
return u, err
}
p := proxy.NewLookup(ups)
u.Forward = &p
return u, nil
}
// Lookup routes lookups to our selves or forward to a remote. // Lookup routes lookups to our selves or forward to a remote.
func (u Upstream) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) { func (u *Upstream) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) {
if u.self { server, ok := state.Context.Value(dnsserver.Key{}).(*dnsserver.Server)
req := new(dns.Msg) if !ok {
req.SetQuestion(name, typ) return nil, fmt.Errorf("no full server is running")
nw := nonwriter.New(state.W)
server := state.Context.Value(dnsserver.Key{}).(*dnsserver.Server)
server.ServeDNS(state.Context, nw, req)
return nw.Msg, nil
} }
if u.Forward != nil { req := new(dns.Msg)
return u.Forward.Lookup(state, name, typ) req.SetQuestion(name, typ)
}
return nil, nil nw := nonwriter.New(state.W)
server.ServeDNS(state.Context, nw, req)
return nw.Msg, nil
} }

View file

@ -1,60 +0,0 @@
package proxy
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy/caddyfile"
"github.com/miekg/dns"
)
func TestUnhealthy(t *testing.T) {
// High HC interval, we want to test the HC after failed queries.
config := "proxy . %s {\n health_check /healthcheck:%s 10s \nfail_timeout 100ms\n}"
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Body.Close()
w.Write([]byte("OK"))
}))
defer backend.Close()
port := backend.URL[17:] // Remove all crap up to the port
back := backend.URL[7:] // Remove http://
c := caddyfile.NewDispenser("testfile", strings.NewReader(fmt.Sprintf(config, back, port)))
upstreams, err := NewStaticUpstreams(&c)
if err != nil {
t.Errorf("Expected no error. Got: %s", err)
}
p := &Proxy{Upstreams: &upstreams}
m := new(dns.Msg)
m.SetQuestion("example.org.", dns.TypeA)
state := request.Request{W: &test.ResponseWriter{}, Req: m}
// Should all fail.
for j := 0; j < failureCheck; j++ {
if _, err := p.Forward(state); err == nil {
t.Errorf("Expected error. Got: nil")
}
}
fails := atomic.LoadInt32(&upstreams[0].(*staticUpstream).Hosts[0].Fails)
if fails != 3 {
t.Errorf("Expected %d fails, got %d", 3, fails)
}
// HC should be kicked off, and reset the counter to 0
i := 0
for fails != 0 {
fails = atomic.LoadInt32(&upstreams[0].(*staticUpstream).Hosts[0].Fails)
time.Sleep(100 * time.Microsecond)
i++
}
}

View file

@ -1,127 +0,0 @@
package proxy
// functions other plugin might want to use to do lookup in the same style as the proxy.
import (
"context"
"fmt"
"net"
"sync/atomic"
"time"
"github.com/coredns/coredns/plugin/pkg/healthcheck"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// NewLookup create a new proxy with the hosts in host and a Random policy.
func NewLookup(hosts []string) Proxy { return NewLookupWithOption(hosts, Options{}) }
// NewLookupWithOption process creates a simple round robin forward with potentially forced proto for upstream.
func NewLookupWithOption(hosts []string, opts Options) Proxy {
p := Proxy{Next: nil}
// TODO(miek): this needs to be unified with upstream.go's NewStaticUpstreams, caddy uses NewHost
// we should copy/make something similar.
upstream := &staticUpstream{
from: ".",
HealthCheck: healthcheck.HealthCheck{
FailTimeout: 5 * time.Second,
MaxFails: 3,
},
ex: newDNSExWithOption(opts),
}
upstream.Hosts = make([]*healthcheck.UpstreamHost, len(hosts))
for i, host := range hosts {
uh := &healthcheck.UpstreamHost{
Name: host,
FailTimeout: upstream.FailTimeout,
CheckDown: checkDownFunc(upstream),
}
upstream.Hosts[i] = uh
}
p.Upstreams = &[]Upstream{upstream}
return p
}
// Lookup will use name and type to forge a new message and will send that upstream. It will
// set any EDNS0 options correctly so that downstream will be able to process the reply.
func (p Proxy) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) {
req := new(dns.Msg)
req.SetQuestion(name, typ)
state.SizeAndDo(req)
state2 := request.Request{W: state.W, Req: req}
return p.lookup(state2)
}
// Forward forward the request in state as-is. Unlike Lookup that adds EDNS0 suffix to the message.
func (p Proxy) Forward(state request.Request) (*dns.Msg, error) {
return p.lookup(state)
}
func (p Proxy) lookup(state request.Request) (*dns.Msg, error) {
upstream := p.match(state)
if upstream == nil {
return nil, errInvalidDomain
}
for {
start := time.Now()
var reply *dns.Msg
var backendErr error
// Since Select() should give us "up" hosts, keep retrying
// hosts until timeout (or until we get a nil host).
for time.Since(start) < tryDuration {
host := upstream.Select()
if host == nil {
return nil, fmt.Errorf("%s: %s", errUnreachable, "no upstream host")
}
// duplicated from proxy.go, but with a twist, we don't write the
// reply back to the client, we return it and there is no monitoring to update here.
atomic.AddInt64(&host.Conns, 1)
reply, backendErr = upstream.Exchanger().Exchange(context.TODO(), host.Name, state)
atomic.AddInt64(&host.Conns, -1)
if backendErr == nil {
if !state.Match(reply) {
return state.ErrorMessage(dns.RcodeFormatError), nil
}
return reply, nil
}
if oe, ok := backendErr.(*net.OpError); ok {
if oe.Timeout() { // see proxy.go for docs.
continue
}
}
timeout := host.FailTimeout
if timeout == 0 {
timeout = defaultFailTimeout
}
atomic.AddInt32(&host.Fails, 1)
fails := atomic.LoadInt32(&host.Fails)
go func(host *healthcheck.UpstreamHost, timeout time.Duration) {
time.Sleep(timeout)
atomic.AddInt32(&host.Fails, -1)
if fails%failureCheck == 0 { // Kick off healthcheck on eveyry third failure.
host.HealthCheckURL()
}
}(host, timeout)
}
return nil, fmt.Errorf("%s: %s", errUnreachable, backendErr)
}
}

View file

@ -9,12 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyfile"
"github.com/miekg/dns"
) )
func TestStop(t *testing.T) { func TestStop(t *testing.T) {
@ -75,25 +70,3 @@ func TestStop(t *testing.T) {
}) })
} }
} }
func TestProxyRefused(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Rcode = dns.RcodeRefused
w.WriteMsg(ret)
})
defer s.Close()
p := NewLookup([]string{s.Addr})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
state.Req.SetQuestion("example.org.", dns.TypeA)
resp, err := p.Forward(state)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
if resp.Rcode != dns.RcodeRefused {
t.Errorf("Expected rcode to be %d, got %d", dns.RcodeRefused, resp.Rcode)
}
}

View file

@ -6,8 +6,9 @@
## Description ## Description
The route53 plugin is useful for serving zones from resource record sets in AWS route53. This plugin The route53 plugin is useful for serving zones from resource record
supports all Amazon Route 53 records (https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html). sets in AWS route53. This plugin supports all Amazon Route 53 records
([https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html)).
The route53 plugin can be used when coredns is deployed on AWS or elsewhere. The route53 plugin can be used when coredns is deployed on AWS or elsewhere.
## Syntax ## Syntax
@ -15,33 +16,40 @@ The route53 plugin can be used when coredns is deployed on AWS or elsewhere.
~~~ txt ~~~ txt
route53 [ZONE:HOSTED_ZONE_ID...] { route53 [ZONE:HOSTED_ZONE_ID...] {
[aws_access_key AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY] [aws_access_key AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY]
upstream [ADDRESS...] upstream
credentials PROFILE [FILENAME] credentials PROFILE [FILENAME]
fallthrough [ZONES...] fallthrough [ZONES...]
} }
~~~ ~~~
* **ZONE** the name of the domain to be accessed. When there are multiple zones with overlapping domains * **ZONE** the name of the domain to be accessed. When there are multiple zones with overlapping
(private vs. public hosted zone), CoreDNS does the lookup in the given order here. Therefore, for a domains (private vs. public hosted zone), CoreDNS does the lookup in the given order here.
non-existing resource record, SOA response will be from the rightmost zone. Therefore, for a non-existing resource record, SOA response will be from the rightmost zone.
* **HOSTED_ZONE_ID** the ID of the hosted zone that contains the resource record sets to be accessed.
* **AWS_ACCESS_KEY_ID** and **AWS_SECRET_ACCESS_KEY** the AWS access key ID and secret access key * **HOSTED*ZONE*ID** the ID of the hosted zone that contains the resource record sets to be
to be used when query AWS (optional). If they are not provided, then coredns tries to access accessed.
AWS credentials the same way as AWS CLI, e.g., environmental variables, AWS credentials file,
instance profile credentials, etc. * **AWS*ACCESS*KEY_ID** and **AWS*SECRET*ACCESS_KEY** the AWS access key ID and secret access key
* `upstream` [**ADDRESS**...] specifies upstream resolver(s) used for resolving services that point to be used when query AWS (optional). If they are not provided, then coredns tries to access
to external hosts (eg. used to resolve CNAMEs). If no **ADDRESS** is given, CoreDNS will resolve AWS credentials the same way as AWS CLI, e.g., environmental variables, AWS credentials file,
against itself. **ADDRESS** can be an IP, an IP:port or a path to a file structured like instance profile credentials, etc.
resolv.conf.
* `credentials` used for reading the credential file and setting the profile name for a given zone. * `upstream`is used for resolving services that point to external hosts (eg. used to resolve
* **PROFILE** AWS account profile name. Defaults to `default`. CNAMEs). CoreDNS will resolve against itself.
* **FILENAME** AWS credentials filename. Defaults to `~/.aws/credentials`
are used. * `credentials` used for reading the credential file and setting the profile name for a given
* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin. zone.
If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin
is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only * **PROFILE** AWS account profile name. Defaults to `default`.
queries for those zones will be subject to fallthrough.
* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block * **FILENAME** AWS credentials filename. Defaults to `~/.aws/credentials` are used.
* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin.
If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin is
authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then
only queries for those zones will be subject to fallthrough.
* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block
## Examples ## Examples

View file

@ -170,7 +170,7 @@ func (h *Route53) updateZones(ctx context.Context) error {
for i, hostedZone := range z { for i, hostedZone := range z {
newZ := file.NewZone(zName, "") newZ := file.NewZone(zName, "")
newZ.Upstream = *h.upstream newZ.Upstream = h.upstream
in := &route53.ListResourceRecordSetsInput{ in := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(hostedZone.id), HostedZoneId: aws.String(hostedZone.id),
} }

View file

@ -48,7 +48,7 @@ func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Ro
var providers []credentials.Provider var providers []credentials.Provider
var fall fall.F var fall fall.F
up, _ := upstream.New(nil) up := upstream.New()
for c.Next() { for c.Next() {
args := c.RemainingArgs() args := c.RemainingArgs()
@ -83,12 +83,7 @@ func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Ro
}, },
}) })
case "upstream": case "upstream":
args := c.RemainingArgs() c.RemainingArgs() // eats args
var err error
up, err = upstream.New(args)
if err != nil {
return c.Errf("invalid upstream: %v", err)
}
case "credentials": case "credentials":
if c.NextArg() { if c.NextArg() {
sharedProvider.Profile = c.Val() sharedProvider.Profile = c.Val()
@ -109,7 +104,7 @@ func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Ro
client := f(credentials.NewChainCredentials(providers)) client := f(credentials.NewChainCredentials(providers))
ctx := context.Background() ctx := context.Background()
h, err := New(ctx, client, keys, &up) h, err := New(ctx, client, keys, up)
if err != nil { if err != nil {
return c.Errf("failed to create Route53 plugin: %v", err) return c.Errf("failed to create Route53 plugin: %v", err)
} }

View file

@ -23,18 +23,16 @@ A working syntax would be:
secondary [zones...] { secondary [zones...] {
transfer from ADDRESS transfer from ADDRESS
transfer to ADDRESS transfer to ADDRESS
upstream [ADDRESS...] upstream
} }
~~~ ~~~
* `transfer from` specifies from which address to fetch the zone. It can be specified multiple times; * `transfer from` specifies from which address to fetch the zone. It can be specified multiple times;
if one does not work, another will be tried. if one does not work, another will be tried.
* `transfer to` can be enabled to allow this secondary zone to be transferred again. * `transfer to` can be enabled to allow this secondary zone to be transferred again.
* `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs) * `upstream` resolve external names found (think CNAMEs) pointing to external names. This is only
pointing to external names. This is only really useful when CoreDNS is configured as a proxy, for really useful when CoreDNS is configured as a proxy; for normal authoritative serving you don't
normal authoritative serving you don't need *or* want to use this. **ADDRESS** can be an IP need *or* want to use this. CoreDNS will resolve CNAMEs against itself.
address, and IP:port or a string pointing to a file that is structured as /etc/resolv.conf.
If no **ADDRESS** is given, CoreDNS will resolve CNAMEs against itself.
When a zone is due to be refreshed (Refresh timer fires) a random jitter of 5 seconds is When a zone is due to be refreshed (Refresh timer fires) a random jitter of 5 seconds is
applied, before fetching. In the case of retry this will be 2 seconds. If there are any errors applied, before fetching. In the case of retry this will be 2 seconds. If there are any errors

View file

@ -49,7 +49,7 @@ func setup(c *caddy.Controller) error {
func secondaryParse(c *caddy.Controller) (file.Zones, error) { func secondaryParse(c *caddy.Controller) (file.Zones, error) {
z := make(map[string]*file.Zone) z := make(map[string]*file.Zone)
names := []string{} names := []string{}
upstr := upstream.Upstream{} upstr := upstream.New()
for c.Next() { for c.Next() {
if c.Val() == "secondary" { if c.Val() == "secondary" {
@ -78,12 +78,7 @@ func secondaryParse(c *caddy.Controller) (file.Zones, error) {
return file.Zones{}, e return file.Zones{}, e
} }
case "upstream": case "upstream":
args := c.RemainingArgs() c.RemainingArgs() // eat args
var err error
upstr, err = upstream.New(args)
if err != nil {
return file.Zones{}, err
}
default: default:
return file.Zones{}, c.Errf("unknown property '%s'", c.Val()) return file.Zones{}, c.Errf("unknown property '%s'", c.Val())
} }

View file

@ -12,14 +12,13 @@ The *template* plugin allows you to dynamically respond to queries by just writi
~~~ ~~~
template CLASS TYPE [ZONE...] { template CLASS TYPE [ZONE...] {
[match REGEX...] match REGEX...
[answer RR] answer RR
[additional RR] additional RR
[authority RR] authority RR
[...] rcode CODE
[rcode CODE] upstream
[upstream [ADDRESS...]] fallthrough [ZONE...]
[fallthrough [ZONE...]]
} }
~~~ ~~~
@ -30,9 +29,7 @@ template CLASS TYPE [ZONE...] {
* `answer|additional|authority` **RR** A [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) style resource record fragment * `answer|additional|authority` **RR** A [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) style resource record fragment
built by a [Go template](https://golang.org/pkg/text/template/) that contains the reply. built by a [Go template](https://golang.org/pkg/text/template/) that contains the reply.
* `rcode` **CODE** A response code (`NXDOMAIN, SERVFAIL, ...`). The default is `SUCCESS`. * `rcode` **CODE** A response code (`NXDOMAIN, SERVFAIL, ...`). The default is `SUCCESS`.
* `upstream` [**ADDRESS**...] defines the upstream resolvers used for resolving CNAME. * `upstream` defines the upstream resolvers used for resolving CNAMEs. CoreDNS will resolve CNAMEs against itself.
If no **ADDRESS** is given, CoreDNS will resolve CNAMEs against itself. **ADDRESS**
can be an IP, an IP:port, or a path to a file structured like resolv.conf.
* `fallthrough` Continue with the next plugin if the zone matched but no regex matched. * `fallthrough` Continue with the next plugin if the zone matched but no regex matched.
If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only queries for If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only queries for
those zones will be subject to fallthrough. those zones will be subject to fallthrough.

View file

@ -144,12 +144,8 @@ func templateParse(c *caddy.Controller) (handler Handler, err error) {
t.fall.SetZonesFromArgs(c.RemainingArgs()) t.fall.SetZonesFromArgs(c.RemainingArgs())
case "upstream": case "upstream":
args := c.RemainingArgs() c.RemainingArgs() // eat remaining args
u, err := upstream.New(args) t.upstream = upstream.New()
if err != nil {
return handler, err
}
t.upstream = &u
default: default:
return handler, c.ArgErr() return handler, c.ArgErr()
} }

View file

@ -148,13 +148,6 @@ func TestSetupParse(t *testing.T) {
}`, }`,
false, false,
}, },
{
`template ANY ANY up.stream.local {
answer "up.stream.local 5 IN CNAME up.river.local"
upstream invalid-upstream-argument
}`,
true,
},
} }
for i, test := range tests { for i, test := range tests {
c := caddy.NewTestController("dns", test.inputFileRules) c := caddy.NewTestController("dns", test.inputFileRules)

View file

@ -7,10 +7,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -34,10 +30,9 @@ func TestAuto(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("www.example.org.", dns.TypeA)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "www.example.org.", dns.TypeA)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }
@ -52,7 +47,7 @@ func TestAuto(t *testing.T) {
time.Sleep(1500 * time.Millisecond) // wait for it to be picked up time.Sleep(1500 * time.Millisecond) // wait for it to be picked up
resp, err = p.Lookup(state, "www.example.org.", dns.TypeA) resp, err = dns.Exchange(m, udp)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }
@ -64,7 +59,7 @@ func TestAuto(t *testing.T) {
os.Remove(filepath.Join(tmpdir, "db.example.org")) os.Remove(filepath.Join(tmpdir, "db.example.org"))
time.Sleep(1100 * time.Millisecond) // wait for it to be picked up time.Sleep(1100 * time.Millisecond) // wait for it to be picked up
resp, err = p.Lookup(state, "www.example.org.", dns.TypeA) resp, err = dns.Exchange(m, udp)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }
@ -99,10 +94,9 @@ func TestAutoNonExistentZone(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("example.org.", dns.TypeA)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "example.org.", dns.TypeA)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }
@ -145,12 +139,9 @@ func TestAutoAXFR(t *testing.T) {
time.Sleep(1100 * time.Millisecond) // wait for it to be picked up time.Sleep(1100 * time.Millisecond) // wait for it to be picked up
p := proxy.NewLookup([]string{udp})
m := new(dns.Msg) m := new(dns.Msg)
m.SetAxfr("example.org.") m.SetAxfr("example.org.")
state := request.Request{W: &test.ResponseWriter{}, Req: m} resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "example.org.", dns.TypeAXFR)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }

View file

@ -3,9 +3,7 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -40,21 +38,20 @@ func TestLookupCache(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
t.Run("Long TTL", func(t *testing.T) { t.Run("Long TTL", func(t *testing.T) {
testCase(t, state, p, "example.org.", 2, 10) testCase(t, "example.org.", udp, 2, 10)
}) })
t.Run("Short TTL", func(t *testing.T) { t.Run("Short TTL", func(t *testing.T) {
testCase(t, state, p, "short.example.org.", 1, 5) testCase(t, "short.example.org.", udp, 1, 5)
}) })
} }
func testCase(t *testing.T, state request.Request, p proxy.Proxy, name string, expectAnsLen int, expectTTL uint32) { func testCase(t *testing.T, name, addr string, expectAnsLen int, expectTTL uint32) {
resp, err := p.Lookup(state, name, dns.TypeA) m := new(dns.Msg)
m.SetQuestion(name, dns.TypeA)
resp, err := dns.Exchange(m, addr)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }

View file

@ -3,9 +3,7 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
mtest "github.com/coredns/coredns/plugin/test" mtest "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -47,11 +45,10 @@ func TestLookupDS(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp})
state := request.Request{W: &mtest.ResponseWriter{}, Req: new(dns.Msg)}
for _, tc := range dsTestCases { for _, tc := range dsTestCases {
resp, err := p.Lookup(state, tc.Qname, tc.Qtype) m := new(dns.Msg)
m.SetQuestion(tc.Qname, tc.Qtype)
resp, err := dns.Exchange(m, udp)
if err != nil || resp == nil { if err != nil || resp == nil {
t.Fatalf("Expected to receive reply, but didn't for %s %d", tc.Qname, tc.Qtype) t.Fatalf("Expected to receive reply, but didn't for %s %d", tc.Qname, tc.Qtype)
} }

View file

@ -7,9 +7,6 @@ import (
"testing" "testing"
"github.com/coredns/coredns/plugin/etcd/msg" "github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -38,16 +35,15 @@ func TestEtcdCache(t *testing.T) {
defer delete(ctx, t, etc, serv.Key) defer delete(ctx, t, etc, serv.Key)
} }
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("b.example.skydns.test.", dns.TypeA)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "b.example.skydns.test.", dns.TypeA)
if err != nil { if err != nil {
t.Errorf("Expected to receive reply, but didn't: %s", err) t.Errorf("Expected to receive reply, but didn't: %s", err)
} }
checkResponse(t, resp) checkResponse(t, resp)
resp, err = p.Lookup(state, "b.example.skydns.test.", dns.TypeA) resp, err = dns.Exchange(m, udp)
if err != nil { if err != nil {
t.Errorf("Expected to receive reply, but didn't: %s", err) t.Errorf("Expected to receive reply, but didn't: %s", err)
} }

View file

@ -10,9 +10,6 @@ import (
"github.com/coredns/coredns/plugin/etcd" "github.com/coredns/coredns/plugin/etcd"
"github.com/coredns/coredns/plugin/etcd/msg" "github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
etcdcv3 "github.com/coreos/etcd/clientv3" etcdcv3 "github.com/coreos/etcd/clientv3"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -38,7 +35,7 @@ func TestEtcdStubAndProxyLookup(t *testing.T) {
stubzones stubzones
path /skydns path /skydns
endpoint http://localhost:2379 endpoint http://localhost:2379
upstream 8.8.8.8:53 8.8.4.4:53 upstream
fallthrough fallthrough
} }
proxy . 8.8.8.8:53 proxy . 8.8.8.8:53
@ -58,9 +55,9 @@ func TestEtcdStubAndProxyLookup(t *testing.T) {
defer delete(ctx, t, etc, serv.Key) defer delete(ctx, t, etc, serv.Key)
} }
p := proxy.NewLookup([]string{udp}) // use udp port from the server m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("example.com.", dns.TypeA)
resp, err := p.Lookup(state, "example.com.", dns.TypeA) resp, err := dns.Exchange(m, udp)
if err != nil { if err != nil {
t.Fatalf("Expected to receive reply, but didn't: %v", err) t.Fatalf("Expected to receive reply, but didn't: %v", err)
} }

View file

@ -11,6 +11,5 @@ short.example.org. 1 IN A 127.0.0.3
*.w.example.org. IN TXT "Wildcard" *.w.example.org. IN TXT "Wildcard"
a.b.c.w.example.org. IN TXT "Not a wildcard" a.b.c.w.example.org. IN TXT "Not a wildcard"
cname.example.org. IN CNAME www.example.net. cname.example.org. IN CNAME www.example.net.
service.example.org. IN SRV 8080 10 10 example.org. service.example.org. IN SRV 8080 10 10 example.org.
` `

View file

@ -3,10 +3,6 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -30,10 +26,9 @@ func TestZoneExternalCNAMELookupWithoutProxy(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("cname.example.org.", dns.TypeA)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "cname.example.org.", dns.TypeA)
if err != nil { if err != nil {
t.Fatalf("Expected to receive reply, but didn't: %s", err) t.Fatalf("Expected to receive reply, but didn't: %s", err)
} }
@ -52,11 +47,12 @@ func TestZoneExternalCNAMELookupWithProxy(t *testing.T) {
} }
defer rm() defer rm()
// Corefile with for example without proxy section. // Corefile with for example proxy section.
corefile := `example.org:0 { corefile := `.:0 {
file ` + name + ` { file ` + name + ` example.org {
upstream 8.8.8.8 upstream
} }
proxy . 8.8.8.8 8.8.4.4
} }
` `
i, udp, _, err := CoreDNSServerAndPorts(corefile) i, udp, _, err := CoreDNSServerAndPorts(corefile)
@ -65,10 +61,9 @@ func TestZoneExternalCNAMELookupWithProxy(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("cname.example.org.", dns.TypeA)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "cname.example.org.", dns.TypeA)
if err != nil { if err != nil {
t.Fatalf("Expected to receive reply, but didn't: %s", err) t.Fatalf("Expected to receive reply, but didn't: %s", err)
} }

View file

@ -6,9 +6,6 @@ import (
"time" "time"
"github.com/coredns/coredns/plugin/file" "github.com/coredns/coredns/plugin/file"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -39,10 +36,9 @@ example.net:0 {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("example.org.", dns.TypeA)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "example.org.", dns.TypeA)
if err != nil { if err != nil {
t.Fatalf("Expected to receive reply, but didn't: %s", err) t.Fatalf("Expected to receive reply, but didn't: %s", err)
} }
@ -55,7 +51,7 @@ example.net:0 {
time.Sleep(2 * time.Second) // reload time time.Sleep(2 * time.Second) // reload time
resp, err = p.Lookup(state, "example.org.", dns.TypeA) resp, err = dns.Exchange(m, udp)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }

View file

@ -3,10 +3,6 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -30,10 +26,9 @@ func TestZoneSRVAdditional(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("service.example.org.", dns.TypeSRV)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "service.example.org.", dns.TypeSRV)
if err != nil { if err != nil {
t.Fatalf("Expected to receive reply, but didn't: %s", err) t.Fatalf("Expected to receive reply, but didn't: %s", err)
} }

View file

@ -3,10 +3,6 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -24,10 +20,9 @@ func TestHostsInlineLookup(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("example.org.", dns.TypeA)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "example.org.", dns.TypeA)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }

View file

@ -3,36 +3,9 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func TestProxyErratic(t *testing.T) {
corefile := `example.org:0 {
erratic {
drop 2
}
}
`
backend, udp, _, err := CoreDNSServerAndPorts(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
defer backend.Stop()
p := proxy.NewLookup([]string{udp})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
// We do one lookup that should not time out.
// After this the backend is marked unhealthy anyway. So basically this
// tests that it times out.
p.Lookup(state, "example.org.", dns.TypeA)
}
func TestProxyThreeWay(t *testing.T) { func TestProxyThreeWay(t *testing.T) {
// Run 3 CoreDNS server, 2 upstream ones and a proxy. 1 Upstream is unhealthy after 1 query, but after // Run 3 CoreDNS server, 2 upstream ones and a proxy. 1 Upstream is unhealthy after 1 query, but after
// that we should still be able to send to the other one. // that we should still be able to send to the other one.

View file

@ -1,86 +0,0 @@
package test
import (
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
func TestProxyWithHTTPCheckOK(t *testing.T) {
healthCheckServer := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
io.WriteString(w, "OK\n")
}))
defer healthCheckServer.Close()
healthCheckURL, err := url.Parse(healthCheckServer.URL)
if err != nil {
t.Fatal(err)
}
// TODO: use URL.Port() (Go 1.8+) once we've deprecated Go 1.7 support
var healthCheckPort string
if _, healthCheckPort, err = net.SplitHostPort(healthCheckURL.Host); err != nil {
healthCheckPort = "80"
}
name, rm, err := test.TempFile(".", exampleOrg)
if err != nil {
t.Fatalf("Failed to create zone: %s", err)
}
defer rm()
// We have to bind to 127.0.0.1 because the server started by
// httptest.NewServer does, and the IP addresses of the backend
// DNS and HTTP servers must match.
authoritativeCorefile := `example.org:0 {
bind 127.0.0.1
file ` + name + `
}
`
authoritativeInstance, authoritativeAddr, _, err := CoreDNSServerAndPorts(authoritativeCorefile)
if err != nil {
t.Fatalf("Could not get CoreDNS authoritative instance: %s", err)
}
defer authoritativeInstance.Stop()
proxyCorefile := `example.org:0 {
proxy . ` + authoritativeAddr + ` {
health_check /health:` + healthCheckPort + ` 1s
}
}
`
proxyInstance, proxyAddr, _, err := CoreDNSServerAndPorts(proxyCorefile)
if err != nil {
t.Fatalf("Could not get CoreDNS proxy instance: %s", err)
}
defer proxyInstance.Stop()
p := proxy.NewLookup([]string{proxyAddr})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
resp, err := p.Lookup(state, "example.org.", dns.TypeA)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
// expect answer section with A record in it
if len(resp.Answer) == 0 {
t.Fatalf("Expected to at least one RR in the answer section, got none: %s", resp)
}
if resp.Answer[0].Header().Rrtype != dns.TypeA {
t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype)
}
if resp.Answer[0].(*dns.A).A.String() != "127.0.0.1" {
t.Errorf("Expected 127.0.0.1, got: %s", resp.Answer[0].(*dns.A).A.String())
}
}

View file

@ -3,9 +3,7 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -29,46 +27,9 @@ func TestLookupProxy(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("example.org.", dns.TypeA)
resp, err := p.Lookup(state, "example.org.", dns.TypeA) resp, err := dns.Exchange(m, udp)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
// expect answer section with A record in it
if len(resp.Answer) == 0 {
t.Fatalf("Expected to at least one RR in the answer section, got none: %s", resp)
}
if resp.Answer[0].Header().Rrtype != dns.TypeA {
t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype)
}
if resp.Answer[0].(*dns.A).A.String() != "127.0.0.1" {
t.Errorf("Expected 127.0.0.1, got: %s", resp.Answer[0].(*dns.A).A.String())
}
}
func TestLookupDnsWithForcedTcp(t *testing.T) {
t.Parallel()
name, rm, err := test.TempFile(".", exampleOrg)
if err != nil {
t.Fatalf("Failed to create zone: %s", err)
}
defer rm()
corefile := `example.org:0 {
file ` + name + `
}
`
i, _, tcp, err := CoreDNSServerAndPorts(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
defer i.Stop()
p := proxy.NewLookupWithOption([]string{tcp}, proxy.Options{ForceTCP: true})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
resp, err := p.Lookup(state, "example.org.", dns.TypeA)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }
@ -108,13 +69,12 @@ func BenchmarkProxyLookup(b *testing.B) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("example.org.", dns.TypeA)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := p.Lookup(state, "example.org.", dns.TypeA) if _, err := dns.Exchange(m, udp); err != nil {
if err != nil {
b.Fatal("Expected to receive reply, but didn't") b.Fatal("Expected to receive reply, but didn't")
} }
} }

View file

@ -3,10 +3,6 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -26,9 +22,9 @@ func TestReverseCorefile(t *testing.T) {
t.Fatalf("Could not get UDP listening port") t.Fatalf("Could not get UDP listening port")
} }
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("17.0.0.10.in-addr.arpa.", dns.TypePTR)
resp, err := p.Lookup(state, "17.0.0.10.in-addr.arpa.", dns.TypePTR) resp, err := dns.Exchange(m, udp)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }

View file

@ -3,9 +3,7 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -25,10 +23,9 @@ func TestEmptySecondaryZone(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp}) m := new(dns.Msg)
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} m.SetQuestion("www.example.org.", dns.TypeA)
resp, err := dns.Exchange(m, udp)
resp, err := p.Lookup(state, "www.example.org.", dns.TypeA)
if err != nil { if err != nil {
t.Fatal("Expected to receive reply, but didn't") t.Fatal("Expected to receive reply, but didn't")
} }

View file

@ -3,9 +3,7 @@ package test
import ( import (
"testing" "testing"
"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -29,11 +27,10 @@ func TestLookupWildcard(t *testing.T) {
} }
defer i.Stop() defer i.Stop()
p := proxy.NewLookup([]string{udp})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
for _, lookup := range []string{"a.w.example.org.", "a.a.w.example.org."} { for _, lookup := range []string{"a.w.example.org.", "a.a.w.example.org."} {
resp, err := p.Lookup(state, lookup, dns.TypeTXT) m := new(dns.Msg)
m.SetQuestion(lookup, dns.TypeTXT)
resp, err := dns.Exchange(m, udp)
if err != nil || resp == nil { if err != nil || resp == nil {
t.Fatalf("Expected to receive reply, but didn't for %s", lookup) t.Fatalf("Expected to receive reply, but didn't for %s", lookup)
} }
@ -64,7 +61,9 @@ func TestLookupWildcard(t *testing.T) {
} }
for _, lookup := range []string{"w.example.org.", "a.w.example.org.", "a.a.w.example.org."} { for _, lookup := range []string{"w.example.org.", "a.w.example.org.", "a.a.w.example.org."} {
resp, err := p.Lookup(state, lookup, dns.TypeSRV) m := new(dns.Msg)
m.SetQuestion(lookup, dns.TypeSRV)
resp, err := dns.Exchange(m, udp)
if err != nil || resp == nil { if err != nil || resp == nil {
t.Fatal("Expected to receive reply, but didn't", lookup) t.Fatal("Expected to receive reply, but didn't", lookup)
} }