plugin/route53: add split zone support (#2160)

Automatically submitted.
This commit is contained in:
Can Yucel 2018-10-10 10:55:54 -07:00 committed by corbot[bot]
parent 49c776df4c
commit 8432f14207
5 changed files with 123 additions and 60 deletions

View file

@ -21,7 +21,9 @@ route53 [ZONE:HOSTED_ZONE_ID...] {
} }
~~~ ~~~
* **ZONE** the name of the domain to be accessed. * **ZONE** the name of the domain to be accessed. When there are multiple zones with overlapping domains
(private vs. public hosted zone), CoreDNS does the lookup in the given order here. 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. * **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 * **AWS_ACCESS_KEY_ID** and **AWS_SECRET_ACCESS_KEY** the AWS access key ID and secret access key
to be used when query AWS (optional). If they are not provided, then coredns tries to access to be used when query AWS (optional). If they are not provided, then coredns tries to access
@ -81,3 +83,11 @@ Enable route53 with AWS credentials file:
} }
} }
~~~ ~~~
Enable route53 with multiple hosted zones with the same domain:
~~~ txt
. {
route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 example.org.:Z93A52145678156
}
~~~

View file

@ -30,28 +30,39 @@ type Route53 struct {
upstream *upstream.Upstream upstream *upstream.Upstream
zMu sync.RWMutex zMu sync.RWMutex
zones map[string]*zone zones zones
} }
type zone struct { type zone struct {
id string id string
z *file.Zone z *file.Zone
dns string
} }
// New returns new *Route53. type zones map[string][]*zone
func New(ctx context.Context, c route53iface.Route53API, keys map[string]string, up *upstream.Upstream) (*Route53, error) {
zones := make(map[string]*zone, len(keys)) // New reads from the keys map which uses domain names as its key and hosted
// zone id lists as its values, validates that each domain name/zone id pair does
// exist, and returns a new *Route53. In addition to this, upstream is passed
// for doing recursive queries against CNAMEs.
// Returns error if it cannot verify any given domain name/zone id pair.
func New(ctx context.Context, c route53iface.Route53API, keys map[string][]string, up *upstream.Upstream) (*Route53, error) {
zones := make(map[string][]*zone, len(keys))
zoneNames := make([]string, 0, len(keys)) zoneNames := make([]string, 0, len(keys))
for dns, id := range keys { for dns, hostedZoneIDs := range keys {
_, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{ for _, hostedZoneID := range hostedZoneIDs {
DNSName: aws.String(dns), _, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{
HostedZoneId: aws.String(id), DNSName: aws.String(dns),
}) HostedZoneId: aws.String(hostedZoneID),
if err != nil { })
return nil, err if err != nil {
return nil, err
}
if _, ok := zones[dns]; !ok {
zoneNames = append(zoneNames, dns)
}
zones[dns] = append(zones[dns], &zone{id: hostedZoneID, dns: dns, z: file.NewZone(dns, "")})
} }
zones[dns] = &zone{id: id, z: file.NewZone(dns, "")}
zoneNames = append(zoneNames, dns)
} }
return &Route53{ return &Route53{
client: c, client: c,
@ -101,9 +112,14 @@ func (h *Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
m.SetReply(r) m.SetReply(r)
m.Authoritative, m.RecursionAvailable = true, true m.Authoritative, m.RecursionAvailable = true, true
var result file.Result var result file.Result
h.zMu.RLock() for _, hostedZone := range z {
m.Answer, m.Ns, m.Extra, result = z.z.Lookup(state, qname) h.zMu.RLock()
h.zMu.RUnlock() m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(state, qname)
h.zMu.RUnlock()
if len(m.Answer) != 0 {
break
}
}
if len(m.Answer) == 0 && h.Fall.Through(qname) { if len(m.Answer) == 0 && h.Fall.Through(qname) {
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
@ -146,36 +162,37 @@ func (h *Route53) updateZones(ctx context.Context) error {
errc := make(chan error) errc := make(chan error)
defer close(errc) defer close(errc)
for zName, z := range h.zones { for zName, z := range h.zones {
go func(zName string, z *zone) { go func(zName string, z []*zone) {
var err error var err error
defer func() { defer func() {
errc <- err errc <- err
}() }()
newZ := file.NewZone(zName, "") for i, hostedZone := range z {
newZ.Upstream = *h.upstream newZ := file.NewZone(zName, "")
newZ.Upstream = *h.upstream
in := &route53.ListResourceRecordSetsInput{ in := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(z.id), HostedZoneId: aws.String(hostedZone.id),
} }
err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in, err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in,
func(out *route53.ListResourceRecordSetsOutput, last bool) bool { func(out *route53.ListResourceRecordSetsOutput, last bool) bool {
for _, rrs := range out.ResourceRecordSets { for _, rrs := range out.ResourceRecordSets {
if err := updateZoneFromRRS(rrs, newZ); err != nil { if err := updateZoneFromRRS(rrs, newZ); err != nil {
// Maybe unsupported record type. Log and carry on. // Maybe unsupported record type. Log and carry on.
log.Warningf("Failed to process resource record set: %v", err) log.Warningf("Failed to process resource record set: %v", err)
}
} }
} return true
return true })
}) if err != nil {
if err != nil { err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, hostedZone.id, err)
err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, z.id, err) return
return }
h.zMu.Lock()
(*z[i]).z = newZ
h.zMu.Unlock()
} }
h.zMu.Lock()
z.z = newZ
h.zMu.Unlock()
}(zName, z) }(zName, z)
} }
// Collect errors (if any). This will also sync on all zones updates // Collect errors (if any). This will also sync on all zones updates

View file

@ -31,19 +31,26 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou
if aws.StringValue(in.HostedZoneId) == "0987654321" { if aws.StringValue(in.HostedZoneId) == "0987654321" {
return errors.New("bad. zone is bad") return errors.New("bad. zone is bad")
} }
var rrs []*route53.ResourceRecordSet rrsResponse := map[string][]*route53.ResourceRecordSet{}
for _, r := range []struct { for _, r := range []struct {
rType, name, value string rType, name, value, hostedZoneID string
}{ }{
{"A", "example.org.", "1.2.3.4"}, {"A", "example.org.", "1.2.3.4", "1234567890"},
{"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334"}, {"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334", "1234567890"},
{"CNAME", "sample.example.org.", "example.org"}, {"CNAME", "sample.example.org.", "example.org", "1234567890"},
{"PTR", "example.org.", "ptr.example.org."}, {"PTR", "example.org.", "ptr.example.org.", "1234567890"},
{"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, {"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400", "1234567890"},
{"NS", "com.", "ns-1536.awsdns-00.co.uk."}, {"NS", "com.", "ns-1536.awsdns-00.co.uk.", "1234567890"},
// Unsupported type should be ignored. // Unsupported type should be ignored.
{"YOLO", "swag.", "foobar"}, {"YOLO", "swag.", "foobar", "1234567890"},
// hosted zone with the same name, but a different id
{"A", "other-example.org.", "3.5.7.9", "1357986420"},
{"SOA", "org.", "ns-15.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400", "1357986420"},
} { } {
rrs, ok := rrsResponse[r.hostedZoneID]
if !ok {
rrs = make([]*route53.ResourceRecordSet, 0)
}
rrs = append(rrs, &route53.ResourceRecordSet{Type: aws.String(r.rType), rrs = append(rrs, &route53.ResourceRecordSet{Type: aws.String(r.rType),
Name: aws.String(r.name), Name: aws.String(r.name),
ResourceRecords: []*route53.ResourceRecord{ ResourceRecords: []*route53.ResourceRecord{
@ -53,9 +60,11 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou
}, },
TTL: aws.Int64(300), TTL: aws.Int64(300),
}) })
rrsResponse[r.hostedZoneID] = rrs
} }
if ok := fn(&route53.ListResourceRecordSetsOutput{ if ok := fn(&route53.ListResourceRecordSetsOutput{
ResourceRecordSets: rrs, ResourceRecordSets: rrsResponse[aws.StringValue(in.HostedZoneId)],
}, true); !ok { }, true); !ok {
return errors.New("paging function return false") return errors.New("paging function return false")
} }
@ -65,7 +74,7 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou
func TestRoute53(t *testing.T) { func TestRoute53(t *testing.T) {
ctx := context.Background() ctx := context.Background()
r, err := New(ctx, fakeRoute53{}, map[string]string{"bad.": "0987654321"}, &upstream.Upstream{}) r, err := New(ctx, fakeRoute53{}, map[string][]string{"bad.": []string{"0987654321"}}, &upstream.Upstream{})
if err != nil { if err != nil {
t.Fatalf("Failed to create Route53: %v", err) t.Fatalf("Failed to create Route53: %v", err)
} }
@ -73,7 +82,7 @@ func TestRoute53(t *testing.T) {
t.Fatalf("Expected errors for zone bad.") t.Fatalf("Expected errors for zone bad.")
} }
r, err = New(ctx, fakeRoute53{}, map[string]string{"org.": "1234567890", "gov.": "Z098765432"}, &upstream.Upstream{}) r, err = New(ctx, fakeRoute53{}, map[string][]string{"org.": []string{"1357986420", "1234567890"}, "gov": []string{"Z098765432"}}, &upstream.Upstream{})
if err != nil { if err != nil {
t.Fatalf("Failed to create Route53: %v", err) t.Fatalf("Failed to create Route53: %v", err)
} }
@ -158,7 +167,7 @@ func TestRoute53(t *testing.T) {
qname: "example.org", qname: "example.org",
qtype: dns.TypeSOA, qtype: dns.TypeSOA,
expectedCode: dns.RcodeSuccess, expectedCode: dns.RcodeSuccess,
wantAnswer: []string{"org. 300 IN SOA ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, wantAnswer: []string{"org. 300 IN SOA ns-15.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"},
}, },
// 6. Explicit SOA query for example.org. // 6. Explicit SOA query for example.org.
{ {
@ -187,6 +196,13 @@ func TestRoute53(t *testing.T) {
expectedCode: dns.RcodeSuccess, expectedCode: dns.RcodeSuccess,
wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"}, wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"},
}, },
// 10. other-zone.example.org is stored in a different hosted zone. success
{
qname: "other-example.org",
qtype: dns.TypeA,
expectedCode: dns.RcodeSuccess,
wantAnswer: []string{"other-example.org. 300 IN A 3.5.7.9"},
},
} }
for ti, tc := range tests { for ti, tc := range tests {

View file

@ -35,7 +35,8 @@ func init() {
} }
func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Route53API) error { func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Route53API) error {
keys := map[string]string{} keyPairs := map[string]struct{}{}
keys := map[string][]string{}
// Route53 plugin attempts to find AWS credentials by using ChainCredentials. // Route53 plugin attempts to find AWS credentials by using ChainCredentials.
// And the order of that provider chain is as follows: // And the order of that provider chain is as follows:
@ -56,14 +57,16 @@ func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Ro
if len(parts) != 2 { if len(parts) != 2 {
return c.Errf("invalid zone '%s'", args[i]) return c.Errf("invalid zone '%s'", args[i])
} }
if parts[0] == "" || parts[1] == "" { dns, hostedZoneID := parts[0], parts[1]
if dns == "" || hostedZoneID == "" {
return c.Errf("invalid zone '%s'", args[i]) return c.Errf("invalid zone '%s'", args[i])
} }
zone := plugin.Host(parts[0]).Normalize() if _, ok := keyPairs[args[i]]; ok {
if v, ok := keys[zone]; ok && v != parts[1] { return c.Errf("conflict zone '%s'", args[i])
return c.Errf("conflict zone '%s' ('%s' vs. '%s')", zone, v, parts[1])
} }
keys[zone] = parts[1]
keyPairs[args[i]] = struct{}{}
keys[dns] = append(keys[dns], hostedZoneID)
} }
for c.NextBlock() { for c.NextBlock() {

View file

@ -53,6 +53,10 @@ func TestSetupRoute53(t *testing.T) {
aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY
upstream 1.2.3.4 upstream 1.2.3.4
}`) }`)
if err := setup(c, f); err != nil {
t.Fatalf("Unexpected errors: %v", err)
}
c = caddy.NewTestController("dns", `route53 example.org:12345678 { c = caddy.NewTestController("dns", `route53 example.org:12345678 {
fallthrough fallthrough
}`) }`)
@ -91,4 +95,17 @@ func TestSetupRoute53(t *testing.T) {
if err := setup(c, f); err == nil { if err := setup(c, f); err == nil {
t.Fatalf("Expected errors, but got: %v", err) t.Fatalf("Expected errors, but got: %v", err)
} }
c = caddy.NewTestController("dns", `route53 example.org:12345678 example.org:12345678 {
upstream 1.2.3.4
}`)
if err := setup(c, f); err == nil {
t.Fatalf("Expected errors, but got: %v", err)
}
c = caddy.NewTestController("dns", `route53 example.org {
upstream 1.2.3.4
}`)
if err := setup(c, f); err == nil {
t.Fatalf("Expected errors, but got: %v", err)
}
} }