mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 08:14:18 -04:00
Remove the word middleware (#1067)
* Rename middleware to plugin first pass; mostly used 'sed', few spots where I manually changed text. This still builds a coredns binary. * fmt error * Rename AddMiddleware to AddPlugin * Readd AddMiddleware to remain backwards compat
This commit is contained in:
109
plugin/etcd/README.md
Normal file
109
plugin/etcd/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# etcd
|
||||
|
||||
*etcd* enables reading zone data from an etcd instance. The data in etcd has to be encoded as
|
||||
a [message](https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26)
|
||||
like [SkyDNS](https://github.com/skynetservices/skydns). It should also work just like SkyDNS.
|
||||
|
||||
The etcd plugin makes extensive use of the proxy plugin to forward and query other servers
|
||||
in the network.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
etcd [ZONES...]
|
||||
~~~
|
||||
|
||||
* **ZONES** zones etcd should be authoritative for.
|
||||
|
||||
The path will default to `/skydns` the local etcd proxy (http://localhost:2379).
|
||||
If no zones are specified the block's zone will be used as the zone.
|
||||
|
||||
If you want to `round robin` A and AAAA responses look at the `loadbalance` plugin.
|
||||
|
||||
~~~
|
||||
etcd [ZONES...] {
|
||||
stubzones
|
||||
fallthrough
|
||||
path PATH
|
||||
endpoint ENDPOINT...
|
||||
upstream ADDRESS...
|
||||
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.
|
||||
* **PATH** the path inside etcd. Defaults to "/skydns".
|
||||
* **ENDPOINT** the etcd endpoints. Defaults to "http://localhost:2397".
|
||||
* `upstream` upstream resolvers to be used resolve external names found in etcd (think CNAMEs)
|
||||
pointing to external names. If you want CoreDNS to act as a proxy for clients, you'll need to add
|
||||
the proxy plugin. **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:
|
||||
* no arguments, if the server certificate is signed by a system-installed CA and no client cert is needed
|
||||
* a single argument that is the CA PEM file, if the server cert is not signed by a system CA and no client cert is needed
|
||||
* two arguments - path to cert PEM file, the path to private key PEM file - if the server certificate is signed by a system-installed CA and a client certificate is needed
|
||||
* three arguments - path to cert PEM file, path to client private key PEM file, path to CA PEM file - if the server certificate is not signed by a system-installed CA and client certificate is needed
|
||||
|
||||
## Examples
|
||||
|
||||
This is the default SkyDNS setup, with everying specified in full:
|
||||
|
||||
~~~
|
||||
.:53 {
|
||||
etcd skydns.local {
|
||||
stubzones
|
||||
path /skydns
|
||||
endpoint http://localhost:2379
|
||||
upstream 8.8.8.8:53 8.8.4.4:53
|
||||
}
|
||||
prometheus
|
||||
cache 160 skydns.local
|
||||
loadbalance
|
||||
proxy . 8.8.8.8:53 8.8.4.4:53
|
||||
}
|
||||
~~~
|
||||
|
||||
Or a setup where we use `/etc/resolv.conf` as the basis for the proxy and the upstream
|
||||
when resolving external pointing CNAMEs.
|
||||
|
||||
~~~
|
||||
.:53 {
|
||||
etcd skydns.local {
|
||||
path /skydns
|
||||
upstream /etc/resolv.conf
|
||||
}
|
||||
cache 160 skydns.local
|
||||
proxy . /etc/resolv.conf
|
||||
}
|
||||
~~~
|
||||
|
||||
|
||||
### Reverse zones
|
||||
|
||||
Reverse zones are supported. You need to make CoreDNS aware of the fact that you are also
|
||||
authoritative for the reverse. For instance if you want to add the reverse for 10.0.0.0/24, you'll
|
||||
need to add the zone `0.0.10.in-addr.arpa` to the list of zones. (The fun starts with IPv6 reverse zones
|
||||
in the ip6.arpa domain.) Showing a snippet of a Corefile:
|
||||
|
||||
~~~
|
||||
etcd skydns.local 0.0.10.in-addr.arpa {
|
||||
stubzones
|
||||
...
|
||||
~~~
|
||||
|
||||
Next you'll need to populate the zone with reverse records, here we add a reverse for
|
||||
10.0.0.127 pointing to reverse.skydns.local.
|
||||
|
||||
~~~
|
||||
% curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/arpa/in-addr/10/0/0/127 \
|
||||
-d value='{"host":"reverse.skydns.local."}'
|
||||
~~~
|
||||
|
||||
Querying with dig:
|
||||
|
||||
~~~
|
||||
% dig @localhost -x 10.0.0.127 +short
|
||||
reverse.atoom.net.
|
||||
~~~
|
||||
79
plugin/etcd/cname_test.go
Normal file
79
plugin/etcd/cname_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
// etcd needs to be running on http://localhost:2379
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Check the ordering of returned cname.
|
||||
func TestCnameLookup(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
|
||||
for _, serv := range servicesCname {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
for _, tc := range dnsTestCasesCname {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := etc.ServeDNS(ctxt, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
if !test.Header(t, tc, resp) {
|
||||
t.Logf("%v\n", resp)
|
||||
continue
|
||||
}
|
||||
if !test.Section(t, tc, test.Answer, resp.Answer) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Ns, resp.Ns) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Extra, resp.Extra) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var servicesCname = []*msg.Service{
|
||||
{Host: "cname1.region2.skydns.test", Key: "a.server1.dev.region1.skydns.test."},
|
||||
{Host: "cname2.region2.skydns.test", Key: "cname1.region2.skydns.test."},
|
||||
{Host: "cname3.region2.skydns.test", Key: "cname2.region2.skydns.test."},
|
||||
{Host: "cname4.region2.skydns.test", Key: "cname3.region2.skydns.test."},
|
||||
{Host: "cname5.region2.skydns.test", Key: "cname4.region2.skydns.test."},
|
||||
{Host: "cname6.region2.skydns.test", Key: "cname5.region2.skydns.test."},
|
||||
{Host: "endpoint.region2.skydns.test", Key: "cname6.region2.skydns.test."},
|
||||
{Host: "10.240.0.1", Key: "endpoint.region2.skydns.test."},
|
||||
}
|
||||
|
||||
var dnsTestCasesCname = []test.Case{
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("a.server1.dev.region1.skydns.test. 300 IN SRV 10 100 0 cname1.region2.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.CNAME("cname1.region2.skydns.test. 300 IN CNAME cname2.region2.skydns.test."),
|
||||
test.CNAME("cname2.region2.skydns.test. 300 IN CNAME cname3.region2.skydns.test."),
|
||||
test.CNAME("cname3.region2.skydns.test. 300 IN CNAME cname4.region2.skydns.test."),
|
||||
test.CNAME("cname4.region2.skydns.test. 300 IN CNAME cname5.region2.skydns.test."),
|
||||
test.CNAME("cname5.region2.skydns.test. 300 IN CNAME cname6.region2.skydns.test."),
|
||||
test.CNAME("cname6.region2.skydns.test. 300 IN CNAME endpoint.region2.skydns.test."),
|
||||
test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
188
plugin/etcd/etcd.go
Normal file
188
plugin/etcd/etcd.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// Package etcd provides the etcd backend plugin.
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/pkg/cache"
|
||||
"github.com/coredns/coredns/plugin/pkg/singleflight"
|
||||
"github.com/coredns/coredns/plugin/proxy"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
etcdc "github.com/coreos/etcd/client"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Etcd is a plugin talks to an etcd cluster.
|
||||
type Etcd struct {
|
||||
Next plugin.Handler
|
||||
Fallthrough bool
|
||||
Zones []string
|
||||
PathPrefix string
|
||||
Proxy proxy.Proxy // Proxy for looking up names during the resolution process
|
||||
Client etcdc.KeysAPI
|
||||
Ctx context.Context
|
||||
Inflight *singleflight.Group
|
||||
Stubmap *map[string]proxy.Proxy // list of proxies for stub resolving.
|
||||
|
||||
endpoints []string // Stored here as well, to aid in testing.
|
||||
}
|
||||
|
||||
// Services implements the ServiceBackend interface.
|
||||
func (e *Etcd) Services(state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) {
|
||||
services, err = e.Records(state, exact)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
services = msg.Group(services)
|
||||
return
|
||||
}
|
||||
|
||||
// Reverse implements the ServiceBackend interface.
|
||||
func (e *Etcd) Reverse(state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) {
|
||||
return e.Services(state, exact, opt)
|
||||
}
|
||||
|
||||
// Lookup implements the ServiceBackend interface.
|
||||
func (e *Etcd) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) {
|
||||
return e.Proxy.Lookup(state, name, typ)
|
||||
}
|
||||
|
||||
// IsNameError implements the ServiceBackend interface.
|
||||
func (e *Etcd) IsNameError(err error) bool {
|
||||
if ee, ok := err.(etcdc.Error); ok && ee.Code == etcdc.ErrorCodeKeyNotFound {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Records looks up records in etcd. If exact is true, it will lookup just this
|
||||
// name. This is used when find matches when completing SRV lookups for instance.
|
||||
func (e *Etcd) Records(state request.Request, exact bool) ([]msg.Service, error) {
|
||||
name := state.Name()
|
||||
|
||||
path, star := msg.PathWithWildcard(name, e.PathPrefix)
|
||||
r, err := e.get(path, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
segments := strings.Split(msg.Path(name, e.PathPrefix), "/")
|
||||
switch {
|
||||
case exact && r.Node.Dir:
|
||||
return nil, nil
|
||||
case r.Node.Dir:
|
||||
return e.loopNodes(r.Node.Nodes, segments, star, nil)
|
||||
default:
|
||||
return e.loopNodes([]*etcdc.Node{r.Node}, segments, false, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// get is a wrapper for client.Get that uses SingleInflight to suppress multiple outstanding queries.
|
||||
func (e *Etcd) get(path string, recursive bool) (*etcdc.Response, error) {
|
||||
|
||||
hash := cache.Hash([]byte(path))
|
||||
|
||||
resp, err := e.Inflight.Do(hash, func() (interface{}, error) {
|
||||
ctx, cancel := context.WithTimeout(e.Ctx, etcdTimeout)
|
||||
defer cancel()
|
||||
r, e := e.Client.Get(ctx, path, &etcdc.GetOptions{Sort: false, Recursive: recursive})
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return r, e
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.(*etcdc.Response), err
|
||||
}
|
||||
|
||||
// skydns/local/skydns/east/staging/web
|
||||
// skydns/local/skydns/west/production/web
|
||||
//
|
||||
// skydns/local/skydns/*/*/web
|
||||
// skydns/local/skydns/*/web
|
||||
|
||||
// loopNodes recursively loops through the nodes and returns all the values. The nodes' keyname
|
||||
// will be match against any wildcards when star is true.
|
||||
func (e *Etcd) loopNodes(ns []*etcdc.Node, nameParts []string, star bool, bx map[msg.Service]bool) (sx []msg.Service, err error) {
|
||||
if bx == nil {
|
||||
bx = make(map[msg.Service]bool)
|
||||
}
|
||||
Nodes:
|
||||
for _, n := range ns {
|
||||
if n.Dir {
|
||||
nodes, err := e.loopNodes(n.Nodes, nameParts, star, bx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sx = append(sx, nodes...)
|
||||
continue
|
||||
}
|
||||
if star {
|
||||
keyParts := strings.Split(n.Key, "/")
|
||||
for i, n := range nameParts {
|
||||
if i > len(keyParts)-1 {
|
||||
// name is longer than key
|
||||
continue Nodes
|
||||
}
|
||||
if n == "*" || n == "any" {
|
||||
continue
|
||||
}
|
||||
if keyParts[i] != n {
|
||||
continue Nodes
|
||||
}
|
||||
}
|
||||
}
|
||||
serv := new(msg.Service)
|
||||
if err := json.Unmarshal([]byte(n.Value), serv); err != nil {
|
||||
return nil, fmt.Errorf("%s: %s", n.Key, err.Error())
|
||||
}
|
||||
b := msg.Service{Host: serv.Host, Port: serv.Port, Priority: serv.Priority, Weight: serv.Weight, Text: serv.Text, Key: n.Key}
|
||||
if _, ok := bx[b]; ok {
|
||||
continue
|
||||
}
|
||||
bx[b] = true
|
||||
|
||||
serv.Key = n.Key
|
||||
serv.TTL = e.TTL(n, serv)
|
||||
if serv.Priority == 0 {
|
||||
serv.Priority = priority
|
||||
}
|
||||
sx = append(sx, *serv)
|
||||
}
|
||||
return sx, nil
|
||||
}
|
||||
|
||||
// TTL returns the smaller of the etcd TTL and the service's
|
||||
// TTL. If neither of these are set (have a zero value), a default is used.
|
||||
func (e *Etcd) TTL(node *etcdc.Node, serv *msg.Service) uint32 {
|
||||
etcdTTL := uint32(node.TTL)
|
||||
|
||||
if etcdTTL == 0 && serv.TTL == 0 {
|
||||
return ttl
|
||||
}
|
||||
if etcdTTL == 0 {
|
||||
return serv.TTL
|
||||
}
|
||||
if serv.TTL == 0 {
|
||||
return etcdTTL
|
||||
}
|
||||
if etcdTTL < serv.TTL {
|
||||
return etcdTTL
|
||||
}
|
||||
return serv.TTL
|
||||
}
|
||||
|
||||
const (
|
||||
priority = 10 // default priority when nothing is set
|
||||
ttl = 300 // default ttl when nothing is set
|
||||
etcdTimeout = 5 * time.Second
|
||||
)
|
||||
74
plugin/etcd/group_test.go
Normal file
74
plugin/etcd/group_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestGroupLookup(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
|
||||
for _, serv := range servicesGroup {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
for _, tc := range dnsTestCasesGroup {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := etc.ServeDNS(ctxt, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
test.SortAndCheck(t, resp, tc)
|
||||
}
|
||||
}
|
||||
|
||||
// Note the key is encoded as DNS name, while in "reality" it is a etcd path.
|
||||
var servicesGroup = []*msg.Service{
|
||||
{Host: "127.0.0.1", Key: "a.dom.skydns.test.", Group: "g1"},
|
||||
{Host: "127.0.0.2", Key: "b.sub.dom.skydns.test.", Group: "g1"},
|
||||
|
||||
{Host: "127.0.0.1", Key: "a.dom2.skydns.test.", Group: "g1"},
|
||||
{Host: "127.0.0.2", Key: "b.sub.dom2.skydns.test.", Group: ""},
|
||||
|
||||
{Host: "127.0.0.1", Key: "a.dom1.skydns.test.", Group: "g1"},
|
||||
{Host: "127.0.0.2", Key: "b.sub.dom1.skydns.test.", Group: "g2"},
|
||||
}
|
||||
|
||||
var dnsTestCasesGroup = []test.Case{
|
||||
// Groups
|
||||
{
|
||||
// hits the group 'g1' and only includes those records
|
||||
Qname: "dom.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("dom.skydns.test. 300 IN A 127.0.0.1"),
|
||||
test.A("dom.skydns.test. 300 IN A 127.0.0.2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// One has group, the other has not... Include the non-group always.
|
||||
Qname: "dom2.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("dom2.skydns.test. 300 IN A 127.0.0.1"),
|
||||
test.A("dom2.skydns.test. 300 IN A 127.0.0.2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// The groups differ.
|
||||
Qname: "dom1.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("dom1.skydns.test. 300 IN A 127.0.0.1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
97
plugin/etcd/handler.go
Normal file
97
plugin/etcd/handler.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ServeDNS implements the plugin.Handler interface.
|
||||
func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
opt := plugin.Options{}
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
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())
|
||||
if zone == "" {
|
||||
return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
var (
|
||||
records, extra []dns.RR
|
||||
err error
|
||||
)
|
||||
switch state.Type() {
|
||||
case "A":
|
||||
records, err = plugin.A(e, zone, state, nil, opt)
|
||||
case "AAAA":
|
||||
records, err = plugin.AAAA(e, zone, state, nil, opt)
|
||||
case "TXT":
|
||||
records, err = plugin.TXT(e, zone, state, opt)
|
||||
case "CNAME":
|
||||
records, err = plugin.CNAME(e, zone, state, opt)
|
||||
case "PTR":
|
||||
records, err = plugin.PTR(e, zone, state, opt)
|
||||
case "MX":
|
||||
records, extra, err = plugin.MX(e, zone, state, opt)
|
||||
case "SRV":
|
||||
records, extra, err = plugin.SRV(e, zone, state, opt)
|
||||
case "SOA":
|
||||
records, err = plugin.SOA(e, zone, state, opt)
|
||||
case "NS":
|
||||
if state.Name() == zone {
|
||||
records, extra, err = plugin.NS(e, zone, state, opt)
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
// Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN
|
||||
_, err = plugin.A(e, zone, state, nil, opt)
|
||||
}
|
||||
|
||||
if e.IsNameError(err) {
|
||||
if e.Fallthrough {
|
||||
return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r)
|
||||
}
|
||||
// Make err nil when returning here, so we don't log spam for NXDOMAIN.
|
||||
return plugin.BackendError(e, zone, dns.RcodeNameError, state, nil /* err */, opt)
|
||||
}
|
||||
if err != nil {
|
||||
return plugin.BackendError(e, zone, dns.RcodeServerFailure, state, err, opt)
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return plugin.BackendError(e, zone, dns.RcodeSuccess, state, err, opt)
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
|
||||
m.Answer = append(m.Answer, records...)
|
||||
m.Extra = append(m.Extra, extra...)
|
||||
|
||||
m = dnsutil.Dedup(m)
|
||||
state.SizeAndDo(m)
|
||||
m, _ = state.Scrub(m)
|
||||
w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (e *Etcd) Name() string { return "etcd" }
|
||||
273
plugin/etcd/lookup_test.go
Normal file
273
plugin/etcd/lookup_test.go
Normal file
@@ -0,0 +1,273 @@
|
||||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/plugin/pkg/singleflight"
|
||||
"github.com/coredns/coredns/plugin/pkg/tls"
|
||||
"github.com/coredns/coredns/plugin/proxy"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
etcdc "github.com/coreos/etcd/client"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ctxt = context.TODO()
|
||||
}
|
||||
|
||||
// Note the key is encoded as DNS name, while in "reality" it is a etcd path.
|
||||
var services = []*msg.Service{
|
||||
{Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.skydns.test."},
|
||||
{Host: "10.0.0.1", Port: 8080, Key: "a.server1.prod.region1.skydns.test."},
|
||||
{Host: "10.0.0.2", Port: 8080, Key: "b.server1.prod.region1.skydns.test."},
|
||||
{Host: "::1", Port: 8080, Key: "b.server6.prod.region1.skydns.test."},
|
||||
// Unresolvable internal name.
|
||||
{Host: "unresolvable.skydns.test", Key: "cname.prod.region1.skydns.test."},
|
||||
// Priority.
|
||||
{Host: "priority.server1", Priority: 333, Port: 8080, Key: "priority.skydns.test."},
|
||||
// Subdomain.
|
||||
{Host: "sub.server1", Port: 0, Key: "a.sub.region1.skydns.test."},
|
||||
{Host: "sub.server2", Port: 80, Key: "b.sub.region1.skydns.test."},
|
||||
{Host: "10.0.0.1", Port: 8080, Key: "c.sub.region1.skydns.test."},
|
||||
// Cname loop.
|
||||
{Host: "a.cname.skydns.test", Key: "b.cname.skydns.test."},
|
||||
{Host: "b.cname.skydns.test", Key: "a.cname.skydns.test."},
|
||||
// Nameservers.
|
||||
{Host: "10.0.0.2", Key: "a.ns.dns.skydns.test."},
|
||||
{Host: "10.0.0.3", Key: "b.ns.dns.skydns.test."},
|
||||
// Reverse.
|
||||
{Host: "reverse.example.com", Key: "1.0.0.10.in-addr.arpa."}, // 10.0.0.1
|
||||
}
|
||||
|
||||
var dnsTestCases = []test.Case{
|
||||
// SRV Test
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("a.server1.dev.region1.skydns.test. 300 SRV 10 100 8080 dev.server1.")},
|
||||
},
|
||||
// SRV Test (case test)
|
||||
{
|
||||
Qname: "a.SERVer1.dEv.region1.skydns.tEst.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("a.SERVer1.dEv.region1.skydns.tEst. 300 SRV 10 100 8080 dev.server1.")},
|
||||
},
|
||||
// NXDOMAIN Test
|
||||
{
|
||||
Qname: "doesnotexist.skydns.test.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"),
|
||||
},
|
||||
},
|
||||
// A Test
|
||||
{
|
||||
Qname: "a.server1.prod.region1.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{test.A("a.server1.prod.region1.skydns.test. 300 A 10.0.0.1")},
|
||||
},
|
||||
// SRV Test where target is IP address
|
||||
{
|
||||
Qname: "a.server1.prod.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("a.server1.prod.region1.skydns.test. 300 SRV 10 100 8080 a.server1.prod.region1.skydns.test.")},
|
||||
Extra: []dns.RR{test.A("a.server1.prod.region1.skydns.test. 300 A 10.0.0.1")},
|
||||
},
|
||||
// AAAA Test
|
||||
{
|
||||
Qname: "b.server6.prod.region1.skydns.test.", Qtype: dns.TypeAAAA,
|
||||
Answer: []dns.RR{test.AAAA("b.server6.prod.region1.skydns.test. 300 AAAA ::1")},
|
||||
},
|
||||
// Multiple A Record Test
|
||||
{
|
||||
Qname: "server1.prod.region1.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("server1.prod.region1.skydns.test. 300 A 10.0.0.1"),
|
||||
test.A("server1.prod.region1.skydns.test. 300 A 10.0.0.2"),
|
||||
},
|
||||
},
|
||||
// Priority Test
|
||||
{
|
||||
Qname: "priority.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("priority.skydns.test. 300 SRV 333 100 8080 priority.server1.")},
|
||||
},
|
||||
// Subdomain Test
|
||||
{
|
||||
Qname: "sub.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 0 sub.server1."),
|
||||
test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 80 sub.server2."),
|
||||
test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 8080 c.sub.region1.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{test.A("c.sub.region1.skydns.test. 300 IN A 10.0.0.1")},
|
||||
},
|
||||
// CNAME (unresolvable internal name)
|
||||
{
|
||||
Qname: "cname.prod.region1.skydns.test.", Qtype: dns.TypeA,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
|
||||
},
|
||||
// Wildcard Test
|
||||
{
|
||||
Qname: "*.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 0 sub.server1."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 0 unresolvable.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 80 sub.server2."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 a.server1.prod.region1.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 b.server1.prod.region1.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 b.server6.prod.region1.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 c.sub.region1.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 dev.server1."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"),
|
||||
test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"),
|
||||
test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"),
|
||||
test.A("c.sub.region1.skydns.test. 300 IN A 10.0.0.1"),
|
||||
},
|
||||
},
|
||||
// Wildcard Test
|
||||
{
|
||||
Qname: "prod.*.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
|
||||
test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 0 unresolvable.skydns.test."),
|
||||
test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 a.server1.prod.region1.skydns.test."),
|
||||
test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 b.server1.prod.region1.skydns.test."),
|
||||
test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 b.server6.prod.region1.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"),
|
||||
test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"),
|
||||
test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"),
|
||||
},
|
||||
},
|
||||
// Wildcard Test
|
||||
{
|
||||
Qname: "prod.any.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 0 unresolvable.skydns.test."),
|
||||
test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 a.server1.prod.region1.skydns.test."),
|
||||
test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 b.server1.prod.region1.skydns.test."),
|
||||
test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 b.server6.prod.region1.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"),
|
||||
test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"),
|
||||
test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"),
|
||||
},
|
||||
},
|
||||
// CNAME loop detection
|
||||
{
|
||||
Qname: "a.cname.skydns.test.", Qtype: dns.TypeA,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 1407441600 28800 7200 604800 60")},
|
||||
},
|
||||
// NODATA Test
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
|
||||
},
|
||||
// NODATA Test
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeHINFO,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
|
||||
},
|
||||
// NXDOMAIN Test
|
||||
{
|
||||
Qname: "a.server1.nonexistent.region1.skydns.test.", Qtype: dns.TypeHINFO, Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
|
||||
},
|
||||
{
|
||||
Qname: "skydns.test.", Qtype: dns.TypeSOA,
|
||||
Answer: []dns.RR{test.SOA("skydns.test. 300 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1460498836 14400 3600 604800 60")},
|
||||
},
|
||||
// NS Record Test
|
||||
{
|
||||
Qname: "skydns.test.", Qtype: dns.TypeNS,
|
||||
Answer: []dns.RR{
|
||||
test.NS("skydns.test. 300 NS a.ns.dns.skydns.test."),
|
||||
test.NS("skydns.test. 300 NS b.ns.dns.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.ns.dns.skydns.test. 300 A 10.0.0.2"),
|
||||
test.A("b.ns.dns.skydns.test. 300 A 10.0.0.3"),
|
||||
},
|
||||
},
|
||||
// NS Record Test
|
||||
{
|
||||
Qname: "a.skydns.test.", Qtype: dns.TypeNS, Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1460498836 14400 3600 604800 60")},
|
||||
},
|
||||
// A Record For NS Record Test
|
||||
{
|
||||
Qname: "ns.dns.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("ns.dns.skydns.test. 300 A 10.0.0.2"),
|
||||
test.A("ns.dns.skydns.test. 300 A 10.0.0.3"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "skydns_extra.test.", Qtype: dns.TypeSOA,
|
||||
Answer: []dns.RR{test.SOA("skydns_extra.test. 300 IN SOA ns.dns.skydns_extra.test. hostmaster.skydns_extra.test. 1460498836 14400 3600 604800 60")},
|
||||
},
|
||||
// Reverse lookup
|
||||
{
|
||||
Qname: "1.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
|
||||
Answer: []dns.RR{test.PTR("1.0.0.10.in-addr.arpa. 300 PTR reverse.example.com.")},
|
||||
},
|
||||
}
|
||||
|
||||
func newEtcdMiddleware() *Etcd {
|
||||
ctxt = context.TODO()
|
||||
|
||||
endpoints := []string{"http://localhost:2379"}
|
||||
tlsc, _ := tls.NewTLSConfigFromArgs()
|
||||
client, _ := newEtcdClient(endpoints, tlsc)
|
||||
|
||||
return &Etcd{
|
||||
Proxy: proxy.NewLookup([]string{"8.8.8.8:53"}),
|
||||
PathPrefix: "skydns",
|
||||
Ctx: context.Background(),
|
||||
Inflight: &singleflight.Group{},
|
||||
Zones: []string{"skydns.test.", "skydns_extra.test.", "in-addr.arpa."},
|
||||
Client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func set(t *testing.T, e *Etcd, k string, ttl time.Duration, m *msg.Service) {
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path, _ := msg.PathWithWildcard(k, e.PathPrefix)
|
||||
e.Client.Set(ctxt, path, string(b), &etcdc.SetOptions{TTL: ttl})
|
||||
}
|
||||
|
||||
func delete(t *testing.T, e *Etcd, k string) {
|
||||
path, _ := msg.PathWithWildcard(k, e.PathPrefix)
|
||||
e.Client.Delete(ctxt, path, &etcdc.DeleteOptions{Recursive: false})
|
||||
}
|
||||
|
||||
func TestLookup(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
for _, serv := range services {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
|
||||
for _, tc := range dnsTestCases {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
etc.ServeDNS(ctxt, rec, m)
|
||||
|
||||
resp := rec.Msg
|
||||
test.SortAndCheck(t, resp, tc)
|
||||
}
|
||||
}
|
||||
|
||||
var ctxt context.Context
|
||||
48
plugin/etcd/msg/path.go
Normal file
48
plugin/etcd/msg/path.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Path converts a domainname to an etcd path. If s looks like service.staging.skydns.local.,
|
||||
// the resulting key will be /skydns/local/skydns/staging/service .
|
||||
func Path(s, prefix string) string {
|
||||
l := dns.SplitDomainName(s)
|
||||
for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l...)...)
|
||||
}
|
||||
|
||||
// Domain is the opposite of Path.
|
||||
func Domain(s string) string {
|
||||
l := strings.Split(s, "/")
|
||||
// start with 1, to strip /skydns
|
||||
for i, j := 1, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
return dnsutil.Join(l[1 : len(l)-1])
|
||||
}
|
||||
|
||||
// PathWithWildcard ascts as Path, but if a name contains wildcards (* or any), the name will be
|
||||
// chopped of before the (first) wildcard, and we do a highler evel search and
|
||||
// later find the matching names. So service.*.skydns.local, will look for all
|
||||
// services under skydns.local and will later check for names that match
|
||||
// service.*.skydns.local. If a wildcard is found the returned bool is true.
|
||||
func PathWithWildcard(s, prefix string) (string, bool) {
|
||||
l := dns.SplitDomainName(s)
|
||||
for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
for i, k := range l {
|
||||
if k == "*" || k == "any" {
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l[:i]...)...), true
|
||||
}
|
||||
}
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l...)...), false
|
||||
}
|
||||
12
plugin/etcd/msg/path_test.go
Normal file
12
plugin/etcd/msg/path_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package msg
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
for _, path := range []string{"mydns", "skydns"} {
|
||||
result := Path("service.staging.skydns.local.", path)
|
||||
if result != "/"+path+"/local/skydns/staging/service" {
|
||||
t.Errorf("Failure to get domain's path with prefix: %s", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
203
plugin/etcd/msg/service.go
Normal file
203
plugin/etcd/msg/service.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// Package msg defines the Service structure which is used for service discovery.
|
||||
package msg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Service defines a discoverable service in etcd. It is the rdata from a SRV
|
||||
// record, but with a twist. Host (Target in SRV) must be a domain name, but
|
||||
// if it looks like an IP address (4/6), we will treat it like an IP address.
|
||||
type Service struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Mail bool `json:"mail,omitempty"` // Be an MX record. Priority becomes Preference.
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
|
||||
// When a SRV record with a "Host: IP-address" is added, we synthesize
|
||||
// a srv.Target domain name. Normally we convert the full Key where
|
||||
// the record lives to a DNS name and use this as the srv.Target. When
|
||||
// TargetStrip > 0 we strip the left most TargetStrip labels from the
|
||||
// DNS name.
|
||||
TargetStrip int `json:"targetstrip,omitempty"`
|
||||
|
||||
// Group is used to group (or *not* to group) different services
|
||||
// together. Services with an identical Group are returned in the same
|
||||
// answer.
|
||||
Group string `json:"group,omitempty"`
|
||||
|
||||
// Etcd key where we found this service and ignored from json un-/marshalling
|
||||
Key string `json:"-"`
|
||||
}
|
||||
|
||||
// RR returns an RR representation of s. It is in a condensed form to minimize space
|
||||
// when this is returned in a DNS message.
|
||||
// The RR will look like:
|
||||
// 1.rails.production.east.skydns.local. 300 CH TXT "service1.example.com:8080(10,0,,false)[0,]"
|
||||
// etcd Key Ttl Host:Port < see below >
|
||||
// between parens: (Priority, Weight, Text (only first 200 bytes!), Mail)
|
||||
// between blockquotes: [TargetStrip,Group]
|
||||
// If the record is synthesised by CoreDNS (i.e. no lookup in etcd happened):
|
||||
//
|
||||
// TODO(miek): what to put here?
|
||||
//
|
||||
func (s *Service) RR() *dns.TXT {
|
||||
l := len(s.Text)
|
||||
if l > 200 {
|
||||
l = 200
|
||||
}
|
||||
t := new(dns.TXT)
|
||||
t.Hdr.Class = dns.ClassCHAOS
|
||||
t.Hdr.Ttl = s.TTL
|
||||
t.Hdr.Rrtype = dns.TypeTXT
|
||||
t.Hdr.Name = Domain(s.Key)
|
||||
|
||||
t.Txt = make([]string, 1)
|
||||
t.Txt[0] = fmt.Sprintf("%s:%d(%d,%d,%s,%t)[%d,%s]",
|
||||
s.Host, s.Port,
|
||||
s.Priority, s.Weight, s.Text[:l], s.Mail,
|
||||
s.TargetStrip, s.Group)
|
||||
return t
|
||||
}
|
||||
|
||||
// NewSRV returns a new SRV record based on the Service.
|
||||
func (s *Service) NewSRV(name string, weight uint16) *dns.SRV {
|
||||
host := targetStrip(dns.Fqdn(s.Host), s.TargetStrip)
|
||||
|
||||
return &dns.SRV{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: s.TTL},
|
||||
Priority: uint16(s.Priority), Weight: weight, Port: uint16(s.Port), Target: dns.Fqdn(host)}
|
||||
}
|
||||
|
||||
// NewMX returns a new MX record based on the Service.
|
||||
func (s *Service) NewMX(name string) *dns.MX {
|
||||
host := targetStrip(dns.Fqdn(s.Host), s.TargetStrip)
|
||||
|
||||
return &dns.MX{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: s.TTL},
|
||||
Preference: uint16(s.Priority), Mx: host}
|
||||
}
|
||||
|
||||
// NewA returns a new A record based on the Service.
|
||||
func (s *Service) NewA(name string, ip net.IP) *dns.A {
|
||||
return &dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: s.TTL}, A: ip}
|
||||
}
|
||||
|
||||
// NewAAAA returns a new AAAA record based on the Service.
|
||||
func (s *Service) NewAAAA(name string, ip net.IP) *dns.AAAA {
|
||||
return &dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: s.TTL}, AAAA: ip}
|
||||
}
|
||||
|
||||
// NewCNAME returns a new CNAME record based on the Service.
|
||||
func (s *Service) NewCNAME(name string, target string) *dns.CNAME {
|
||||
return &dns.CNAME{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: s.TTL}, Target: dns.Fqdn(target)}
|
||||
}
|
||||
|
||||
// NewTXT returns a new TXT record based on the Service.
|
||||
func (s *Service) NewTXT(name string) *dns.TXT {
|
||||
return &dns.TXT{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: s.TTL}, Txt: split255(s.Text)}
|
||||
}
|
||||
|
||||
// NewPTR returns a new PTR record based on the Service.
|
||||
func (s *Service) NewPTR(name string, target string) *dns.PTR {
|
||||
return &dns.PTR{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: s.TTL}, Ptr: dns.Fqdn(target)}
|
||||
}
|
||||
|
||||
// NewNS returns a new NS record based on the Service.
|
||||
func (s *Service) NewNS(name string) *dns.NS {
|
||||
host := targetStrip(dns.Fqdn(s.Host), s.TargetStrip)
|
||||
return &dns.NS{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: s.TTL}, Ns: host}
|
||||
}
|
||||
|
||||
// Group checks the services in sx, it looks for a Group attribute on the shortest
|
||||
// keys. If there are multiple shortest keys *and* the group attribute disagrees (and
|
||||
// is not empty), we don't consider it a group.
|
||||
// If a group is found, only services with *that* group (or no group) will be returned.
|
||||
func Group(sx []Service) []Service {
|
||||
if len(sx) == 0 {
|
||||
return sx
|
||||
}
|
||||
|
||||
// Shortest key with group attribute sets the group for this set.
|
||||
group := sx[0].Group
|
||||
slashes := strings.Count(sx[0].Key, "/")
|
||||
length := make([]int, len(sx))
|
||||
for i, s := range sx {
|
||||
x := strings.Count(s.Key, "/")
|
||||
length[i] = x
|
||||
if x < slashes {
|
||||
if s.Group == "" {
|
||||
break
|
||||
}
|
||||
slashes = x
|
||||
group = s.Group
|
||||
}
|
||||
}
|
||||
|
||||
if group == "" {
|
||||
return sx
|
||||
}
|
||||
|
||||
ret := []Service{} // with slice-tricks in sx we can prolly save this allocation (TODO)
|
||||
|
||||
for i, s := range sx {
|
||||
if s.Group == "" {
|
||||
ret = append(ret, s)
|
||||
continue
|
||||
}
|
||||
|
||||
// Disagreement on the same level
|
||||
if length[i] == slashes && s.Group != group {
|
||||
return sx
|
||||
}
|
||||
|
||||
if s.Group == group {
|
||||
ret = append(ret, s)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Split255 splits a string into 255 byte chunks.
|
||||
func split255(s string) []string {
|
||||
if len(s) < 255 {
|
||||
return []string{s}
|
||||
}
|
||||
sx := []string{}
|
||||
p, i := 0, 255
|
||||
for {
|
||||
if i <= len(s) {
|
||||
sx = append(sx, s[p:i])
|
||||
} else {
|
||||
sx = append(sx, s[p:])
|
||||
break
|
||||
|
||||
}
|
||||
p, i = p+255, i+255
|
||||
}
|
||||
|
||||
return sx
|
||||
}
|
||||
|
||||
// targetStrip strips "targetstrip" labels from the left side of the fully qualified name.
|
||||
func targetStrip(name string, targetStrip int) string {
|
||||
if targetStrip == 0 {
|
||||
return name
|
||||
}
|
||||
|
||||
offset, end := 0, false
|
||||
for i := 0; i < targetStrip; i++ {
|
||||
offset, end = dns.NextLabel(name, offset)
|
||||
}
|
||||
if end {
|
||||
// We overshot the name, use the orignal one.
|
||||
offset = 0
|
||||
}
|
||||
name = name[offset:]
|
||||
return name
|
||||
}
|
||||
125
plugin/etcd/msg/service_test.go
Normal file
125
plugin/etcd/msg/service_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package msg
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSplit255(t *testing.T) {
|
||||
xs := split255("abc")
|
||||
if len(xs) != 1 && xs[0] != "abc" {
|
||||
t.Errorf("Failure to split abc")
|
||||
}
|
||||
s := ""
|
||||
for i := 0; i < 255; i++ {
|
||||
s += "a"
|
||||
}
|
||||
xs = split255(s)
|
||||
if len(xs) != 1 && xs[0] != s {
|
||||
t.Errorf("failure to split 255 char long string")
|
||||
}
|
||||
s += "b"
|
||||
xs = split255(s)
|
||||
if len(xs) != 2 || xs[1] != "b" {
|
||||
t.Errorf("failure to split 256 char long string: %d", len(xs))
|
||||
}
|
||||
for i := 0; i < 255; i++ {
|
||||
s += "a"
|
||||
}
|
||||
xs = split255(s)
|
||||
if len(xs) != 3 || xs[2] != "a" {
|
||||
t.Errorf("failure to split 510 char long string: %d", len(xs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
// Key are in the wrong order, but for this test it does not matter.
|
||||
sx := Group(
|
||||
[]Service{
|
||||
{Host: "127.0.0.1", Group: "g1", Key: "b/sub/dom1/skydns/test"},
|
||||
{Host: "127.0.0.2", Group: "g2", Key: "a/dom1/skydns/test"},
|
||||
},
|
||||
)
|
||||
// Expecting to return the shortest key with a Group attribute.
|
||||
if len(sx) != 1 {
|
||||
t.Fatalf("failure to group zeroth set: %v", sx)
|
||||
}
|
||||
if sx[0].Key != "a/dom1/skydns/test" {
|
||||
t.Fatalf("failure to group zeroth set: %v, wrong Key", sx)
|
||||
}
|
||||
|
||||
// Groups disagree, so we will not do anything.
|
||||
sx = Group(
|
||||
[]Service{
|
||||
{Host: "server1", Group: "g1", Key: "region1/skydns/test"},
|
||||
{Host: "server2", Group: "g2", Key: "region1/skydns/test"},
|
||||
},
|
||||
)
|
||||
if len(sx) != 2 {
|
||||
t.Fatalf("failure to group first set: %v", sx)
|
||||
}
|
||||
|
||||
// Group is g1, include only the top-level one.
|
||||
sx = Group(
|
||||
[]Service{
|
||||
{Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"},
|
||||
{Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"},
|
||||
},
|
||||
)
|
||||
if len(sx) != 1 {
|
||||
t.Fatalf("failure to group second set: %v", sx)
|
||||
}
|
||||
|
||||
// Groupless services must be included.
|
||||
sx = Group(
|
||||
[]Service{
|
||||
{Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"},
|
||||
{Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"},
|
||||
{Host: "server2", Group: "", Key: "b/subdom/dom/region1/skydns/test"},
|
||||
},
|
||||
)
|
||||
if len(sx) != 2 {
|
||||
t.Fatalf("failure to group third set: %v", sx)
|
||||
}
|
||||
|
||||
// Empty group on the highest level: include that one also.
|
||||
sx = Group(
|
||||
[]Service{
|
||||
{Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"},
|
||||
{Host: "server1", Group: "", Key: "b/dom/region1/skydns/test"},
|
||||
{Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"},
|
||||
},
|
||||
)
|
||||
if len(sx) != 2 {
|
||||
t.Fatalf("failure to group fourth set: %v", sx)
|
||||
}
|
||||
|
||||
// Empty group on the highest level: include that one also, and the rest.
|
||||
sx = Group(
|
||||
[]Service{
|
||||
{Host: "server1", Group: "g5", Key: "a/dom/region1/skydns/test"},
|
||||
{Host: "server1", Group: "", Key: "b/dom/region1/skydns/test"},
|
||||
{Host: "server2", Group: "g5", Key: "a/subdom/dom/region1/skydns/test"},
|
||||
},
|
||||
)
|
||||
if len(sx) != 3 {
|
||||
t.Fatalf("failure to group fith set: %v", sx)
|
||||
}
|
||||
|
||||
// One group.
|
||||
sx = Group(
|
||||
[]Service{
|
||||
{Host: "server1", Group: "g6", Key: "a/dom/region1/skydns/test"},
|
||||
},
|
||||
)
|
||||
if len(sx) != 1 {
|
||||
t.Fatalf("failure to group sixth set: %v", sx)
|
||||
}
|
||||
|
||||
// No group, once service
|
||||
sx = Group(
|
||||
[]Service{
|
||||
{Host: "server1", Key: "a/dom/region1/skydns/test"},
|
||||
},
|
||||
)
|
||||
if len(sx) != 1 {
|
||||
t.Fatalf("failure to group seventh set: %v", sx)
|
||||
}
|
||||
}
|
||||
33
plugin/etcd/msg/type.go
Normal file
33
plugin/etcd/msg/type.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// HostType returns the DNS type of what is encoded in the Service Host field. We're reusing
|
||||
// dns.TypeXXX to not reinvent a new set of identifiers.
|
||||
//
|
||||
// dns.TypeA: the service's Host field contains an A record.
|
||||
// dns.TypeAAAA: the service's Host field contains an AAAA record.
|
||||
// dns.TypeCNAME: the service's Host field contains a name.
|
||||
//
|
||||
// Note that a service can double/triple as a TXT record or MX record.
|
||||
func (s *Service) HostType() (what uint16, normalized net.IP) {
|
||||
|
||||
ip := net.ParseIP(s.Host)
|
||||
|
||||
switch {
|
||||
case ip == nil:
|
||||
return dns.TypeCNAME, nil
|
||||
|
||||
case ip.To4() != nil:
|
||||
return dns.TypeA, ip.To4()
|
||||
|
||||
case ip.To4() == nil:
|
||||
return dns.TypeAAAA, ip.To16()
|
||||
}
|
||||
// This should never be reached.
|
||||
return dns.TypeNone, nil
|
||||
}
|
||||
31
plugin/etcd/msg/type_test.go
Normal file
31
plugin/etcd/msg/type_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
tests := []struct {
|
||||
serv Service
|
||||
expectedType uint16
|
||||
}{
|
||||
{Service{Host: "example.org"}, dns.TypeCNAME},
|
||||
{Service{Host: "127.0.0.1"}, dns.TypeA},
|
||||
{Service{Host: "2000::3"}, dns.TypeAAAA},
|
||||
{Service{Host: "2000..3"}, dns.TypeCNAME},
|
||||
{Service{Host: "127.0.0.257"}, dns.TypeCNAME},
|
||||
{Service{Host: "127.0.0.252", Mail: true}, dns.TypeA},
|
||||
{Service{Host: "127.0.0.252", Mail: true, Text: "a"}, dns.TypeA},
|
||||
{Service{Host: "127.0.0.254", Mail: false, Text: "a"}, dns.TypeA},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
what, _ := tc.serv.HostType()
|
||||
if what != tc.expectedType {
|
||||
t.Errorf("Test %d: Expected what %v, but got %v", i, tc.expectedType, what)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
59
plugin/etcd/multi_test.go
Normal file
59
plugin/etcd/multi_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestMultiLookup(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
etc.Zones = []string{"skydns.test.", "miek.nl."}
|
||||
etc.Fallthrough = true
|
||||
etc.Next = test.ErrorHandler()
|
||||
|
||||
for _, serv := range servicesMulti {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
for _, tc := range dnsTestCasesMulti {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := etc.ServeDNS(ctxt, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
test.SortAndCheck(t, resp, tc)
|
||||
}
|
||||
}
|
||||
|
||||
// Note the key is encoded as DNS name, while in "reality" it is a etcd path.
|
||||
var servicesMulti = []*msg.Service{
|
||||
{Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.skydns.test."},
|
||||
{Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.miek.nl."},
|
||||
{Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.example.org."},
|
||||
}
|
||||
|
||||
var dnsTestCasesMulti = []test.Case{
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("a.server1.dev.region1.skydns.test. 300 SRV 10 100 8080 dev.server1.")},
|
||||
},
|
||||
{
|
||||
Qname: "a.server1.dev.region1.miek.nl.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("a.server1.dev.region1.miek.nl. 300 SRV 10 100 8080 dev.server1.")},
|
||||
},
|
||||
{
|
||||
Qname: "a.server1.dev.region1.example.org.", Qtype: dns.TypeSRV, Rcode: dns.RcodeServerFailure,
|
||||
},
|
||||
}
|
||||
150
plugin/etcd/other_test.go
Normal file
150
plugin/etcd/other_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// +build etcd
|
||||
|
||||
// tests mx and txt records
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestOtherLookup(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
|
||||
for _, serv := range servicesOther {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
for _, tc := range dnsTestCasesOther {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := etc.ServeDNS(ctxt, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
test.SortAndCheck(t, resp, tc)
|
||||
}
|
||||
}
|
||||
|
||||
// Note the key is encoded as DNS name, while in "reality" it is a etcd path.
|
||||
var servicesOther = []*msg.Service{
|
||||
{Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.skydns.test."},
|
||||
|
||||
// mx
|
||||
{Host: "mx.skydns.test", Priority: 50, Mail: true, Key: "a.mail.skydns.test."},
|
||||
{Host: "mx.miek.nl", Priority: 50, Mail: true, Key: "b.mail.skydns.test."},
|
||||
{Host: "a.ipaddr.skydns.test", Priority: 30, Mail: true, Key: "a.mx.skydns.test."},
|
||||
|
||||
{Host: "a.ipaddr.skydns.test", Mail: true, Key: "a.mx2.skydns.test."},
|
||||
{Host: "b.ipaddr.skydns.test", Mail: true, Key: "b.mx2.skydns.test."},
|
||||
|
||||
{Host: "a.ipaddr.skydns.test", Priority: 20, Mail: true, Key: "a.mx3.skydns.test."},
|
||||
{Host: "a.ipaddr.skydns.test", Priority: 30, Mail: true, Key: "b.mx3.skydns.test."},
|
||||
|
||||
{Host: "172.16.1.1", Key: "a.ipaddr.skydns.test."},
|
||||
{Host: "172.16.1.2", Key: "b.ipaddr.skydns.test."},
|
||||
|
||||
// txt
|
||||
{Text: "abc", Key: "a1.txt.skydns.test."},
|
||||
{Text: "abc abc", Key: "a2.txt.skydns.test."},
|
||||
// txt sizes
|
||||
{Text: strings.Repeat("0", 400), Key: "large400.skydns.test."},
|
||||
{Text: strings.Repeat("0", 600), Key: "large600.skydns.test."},
|
||||
{Text: strings.Repeat("0", 2000), Key: "large2000.skydns.test."},
|
||||
|
||||
// duplicate ip address
|
||||
{Host: "10.11.11.10", Key: "http.multiport.http.skydns.test.", Port: 80},
|
||||
{Host: "10.11.11.10", Key: "https.multiport.http.skydns.test.", Port: 443},
|
||||
}
|
||||
|
||||
var dnsTestCasesOther = []test.Case{
|
||||
// MX Tests
|
||||
{
|
||||
// NODATA as this is not an Mail: true record.
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeMX,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "a.mail.skydns.test.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{test.MX("a.mail.skydns.test. 300 IN MX 50 mx.skydns.test.")},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.ipaddr.skydns.test. 300 IN A 172.16.1.1"),
|
||||
test.CNAME("mx.skydns.test. 300 IN CNAME a.ipaddr.skydns.test."),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "mx2.skydns.test.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.MX("mx2.skydns.test. 300 IN MX 10 a.ipaddr.skydns.test."),
|
||||
test.MX("mx2.skydns.test. 300 IN MX 10 b.ipaddr.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.ipaddr.skydns.test. 300 A 172.16.1.1"),
|
||||
test.A("b.ipaddr.skydns.test. 300 A 172.16.1.2"),
|
||||
},
|
||||
},
|
||||
// different priority, same host
|
||||
{
|
||||
Qname: "mx3.skydns.test.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.MX("mx3.skydns.test. 300 IN MX 20 a.ipaddr.skydns.test."),
|
||||
test.MX("mx3.skydns.test. 300 IN MX 30 a.ipaddr.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.ipaddr.skydns.test. 300 A 172.16.1.1"),
|
||||
},
|
||||
},
|
||||
// Txt
|
||||
{
|
||||
Qname: "a1.txt.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Answer: []dns.RR{
|
||||
test.TXT("a1.txt.skydns.test. 300 IN TXT \"abc\""),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "a2.txt.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Answer: []dns.RR{
|
||||
test.TXT("a2.txt.skydns.test. 300 IN TXT \"abc abc\""),
|
||||
},
|
||||
},
|
||||
// Large txt less than 512
|
||||
{
|
||||
Qname: "large400.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Answer: []dns.RR{
|
||||
test.TXT(fmt.Sprintf("large400.skydns.test. 300 IN TXT \"%s\"", strings.Repeat("0", 400))),
|
||||
},
|
||||
},
|
||||
// Large txt greater than 512 (UDP)
|
||||
{
|
||||
Qname: "large600.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Answer: []dns.RR{
|
||||
test.TXT(fmt.Sprintf("large600.skydns.test. 300 IN TXT \"%s\"", strings.Repeat("0", 600))),
|
||||
},
|
||||
},
|
||||
// Large txt greater than 1500 (typical Ethernet)
|
||||
{
|
||||
Qname: "large2000.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Answer: []dns.RR{
|
||||
test.TXT(fmt.Sprintf("large2000.skydns.test. 300 IN TXT \"%s\"", strings.Repeat("0", 2000))),
|
||||
},
|
||||
},
|
||||
// Duplicate IP address test
|
||||
{
|
||||
Qname: "multiport.http.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{test.A("multiport.http.skydns.test. 300 IN A 10.11.11.10")},
|
||||
},
|
||||
}
|
||||
144
plugin/etcd/setup.go
Normal file
144
plugin/etcd/setup.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/plugin/pkg/singleflight"
|
||||
mwtls "github.com/coredns/coredns/plugin/pkg/tls"
|
||||
"github.com/coredns/coredns/plugin/proxy"
|
||||
|
||||
etcdc "github.com/coreos/etcd/client"
|
||||
"github.com/mholt/caddy"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("etcd", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
e, stubzones, err := etcdParse(c)
|
||||
if err != nil {
|
||||
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 {
|
||||
e.Next = next
|
||||
return e
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func etcdParse(c *caddy.Controller) (*Etcd, bool, error) {
|
||||
stub := make(map[string]proxy.Proxy)
|
||||
etc := Etcd{
|
||||
// Don't default to a proxy for lookups.
|
||||
// Proxy: proxy.NewLookup([]string{"8.8.8.8:53", "8.8.4.4:53"}),
|
||||
PathPrefix: "skydns",
|
||||
Ctx: context.Background(),
|
||||
Inflight: &singleflight.Group{},
|
||||
Stubmap: &stub,
|
||||
}
|
||||
var (
|
||||
tlsConfig *tls.Config
|
||||
err error
|
||||
endpoints = []string{defaultEndpoint}
|
||||
stubzones = false
|
||||
)
|
||||
for c.Next() {
|
||||
etc.Zones = c.RemainingArgs()
|
||||
if len(etc.Zones) == 0 {
|
||||
etc.Zones = make([]string, len(c.ServerBlockKeys))
|
||||
copy(etc.Zones, c.ServerBlockKeys)
|
||||
}
|
||||
for i, str := range etc.Zones {
|
||||
etc.Zones[i] = plugin.Host(str).Normalize()
|
||||
}
|
||||
|
||||
if c.NextBlock() {
|
||||
for {
|
||||
switch c.Val() {
|
||||
case "stubzones":
|
||||
stubzones = true
|
||||
case "fallthrough":
|
||||
etc.Fallthrough = true
|
||||
case "debug":
|
||||
/* it is a noop now */
|
||||
case "path":
|
||||
if !c.NextArg() {
|
||||
return &Etcd{}, false, c.ArgErr()
|
||||
}
|
||||
etc.PathPrefix = c.Val()
|
||||
case "endpoint":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return &Etcd{}, false, c.ArgErr()
|
||||
}
|
||||
endpoints = args
|
||||
case "upstream":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return &Etcd{}, false, c.ArgErr()
|
||||
}
|
||||
ups, err := dnsutil.ParseHostPortOrFile(args...)
|
||||
if err != nil {
|
||||
return &Etcd{}, false, err
|
||||
}
|
||||
etc.Proxy = proxy.NewLookup(ups)
|
||||
case "tls": // cert key cacertfile
|
||||
args := c.RemainingArgs()
|
||||
tlsConfig, err = mwtls.NewTLSConfigFromArgs(args...)
|
||||
if err != nil {
|
||||
return &Etcd{}, false, err
|
||||
}
|
||||
default:
|
||||
if c.Val() != "}" {
|
||||
return &Etcd{}, false, c.Errf("unknown property '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
|
||||
if !c.Next() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
client, err := newEtcdClient(endpoints, tlsConfig)
|
||||
if err != nil {
|
||||
return &Etcd{}, false, err
|
||||
}
|
||||
etc.Client = client
|
||||
etc.endpoints = endpoints
|
||||
|
||||
return &etc, stubzones, nil
|
||||
}
|
||||
return &Etcd{}, false, nil
|
||||
}
|
||||
|
||||
func newEtcdClient(endpoints []string, cc *tls.Config) (etcdc.KeysAPI, error) {
|
||||
etcdCfg := etcdc.Config{
|
||||
Endpoints: endpoints,
|
||||
Transport: mwtls.NewHTTPSTransport(cc),
|
||||
}
|
||||
cli, err := etcdc.New(etcdCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return etcdc.NewKeysAPI(cli), nil
|
||||
}
|
||||
|
||||
const defaultEndpoint = "http://localhost:2379"
|
||||
64
plugin/etcd/setup_test.go
Normal file
64
plugin/etcd/setup_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetupEtcd(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedPath string
|
||||
expectedEndpoint string
|
||||
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
||||
}{
|
||||
// positive
|
||||
{
|
||||
`etcd`, false, "skydns", "http://localhost:2379", "",
|
||||
},
|
||||
{
|
||||
`etcd skydns.local {
|
||||
endpoint localhost:300
|
||||
}
|
||||
`, false, "skydns", "localhost:300", "",
|
||||
},
|
||||
// negative
|
||||
{
|
||||
`etcd {
|
||||
endpoints localhost:300
|
||||
}
|
||||
`, true, "", "", "unknown property 'endpoints'",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
etcd, _ /*stubzones*/, err := etcdParse(c)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if !test.shouldErr {
|
||||
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), test.expectedErrContent) {
|
||||
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !test.shouldErr && etcd.PathPrefix != test.expectedPath {
|
||||
t.Errorf("Etcd not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedPath, etcd.PathPrefix)
|
||||
}
|
||||
if !test.shouldErr && etcd.endpoints[0] != test.expectedEndpoint { // only checks the first
|
||||
t.Errorf("Etcd not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, test.expectedEndpoint, etcd.endpoints[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
82
plugin/etcd/stub.go
Normal file
82
plugin/etcd/stub.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"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.Printf("[WARNING] 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.Printf("[WARNING] 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
|
||||
}
|
||||
86
plugin/etcd/stub_handler.go
Normal file
86
plugin/etcd/stub_handler.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// 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.Printf("[WARNING] 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
|
||||
}
|
||||
m.RecursionAvailable, m.Compress = true, true
|
||||
state.SizeAndDo(m)
|
||||
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
|
||||
}()
|
||||
88
plugin/etcd/stub_test.go
Normal file
88
plugin/etcd/stub_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
|
||||
"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 := newEtcdMiddleware()
|
||||
|
||||
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 := dnsrecorder.New(&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")},
|
||||
Extra: []dns.RR{test.OPT(4096, false)}, // This will have an EDNS0 section, because *we* added our local stub forward to detect loops.
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user