diff --git a/plugin/route53/README.md b/plugin/route53/README.md index 0363df696..32628d81d 100644 --- a/plugin/route53/README.md +++ b/plugin/route53/README.md @@ -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. * **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 @@ -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 +} +~~~ diff --git a/plugin/route53/route53.go b/plugin/route53/route53.go index 3e1a2ddea..7420e2677 100644 --- a/plugin/route53/route53.go +++ b/plugin/route53/route53.go @@ -30,28 +30,39 @@ type Route53 struct { upstream *upstream.Upstream zMu sync.RWMutex - zones map[string]*zone + zones zones } type zone struct { - id string - z *file.Zone + id string + z *file.Zone + dns string } -// New returns new *Route53. -func New(ctx context.Context, c route53iface.Route53API, keys map[string]string, up *upstream.Upstream) (*Route53, error) { - zones := make(map[string]*zone, len(keys)) +type zones map[string][]*zone + +// 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)) - for dns, id := range keys { - _, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{ - DNSName: aws.String(dns), - HostedZoneId: aws.String(id), - }) - if err != nil { - return nil, err + for dns, hostedZoneIDs := range keys { + for _, hostedZoneID := range hostedZoneIDs { + _, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{ + DNSName: aws.String(dns), + HostedZoneId: aws.String(hostedZoneID), + }) + 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{ client: c, @@ -101,9 +112,14 @@ func (h *Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg m.SetReply(r) m.Authoritative, m.RecursionAvailable = true, true var result file.Result - h.zMu.RLock() - m.Answer, m.Ns, m.Extra, result = z.z.Lookup(state, qname) - h.zMu.RUnlock() + for _, hostedZone := range z { + h.zMu.RLock() + 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) { 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) defer close(errc) for zName, z := range h.zones { - go func(zName string, z *zone) { + go func(zName string, z []*zone) { var err error defer func() { errc <- err }() - newZ := file.NewZone(zName, "") - newZ.Upstream = *h.upstream - - in := &route53.ListResourceRecordSetsInput{ - HostedZoneId: aws.String(z.id), - } - err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in, - func(out *route53.ListResourceRecordSetsOutput, last bool) bool { - for _, rrs := range out.ResourceRecordSets { - if err := updateZoneFromRRS(rrs, newZ); err != nil { - // Maybe unsupported record type. Log and carry on. - log.Warningf("Failed to process resource record set: %v", err) + for i, hostedZone := range z { + newZ := file.NewZone(zName, "") + newZ.Upstream = *h.upstream + in := &route53.ListResourceRecordSetsInput{ + HostedZoneId: aws.String(hostedZone.id), + } + err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in, + func(out *route53.ListResourceRecordSetsOutput, last bool) bool { + for _, rrs := range out.ResourceRecordSets { + if err := updateZoneFromRRS(rrs, newZ); err != nil { + // Maybe unsupported record type. Log and carry on. + log.Warningf("Failed to process resource record set: %v", err) + } } - } - return true - }) - if err != nil { - err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, z.id, err) - return + return true + }) + if err != nil { + err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, hostedZone.id, err) + return + } + h.zMu.Lock() + (*z[i]).z = newZ + h.zMu.Unlock() } - h.zMu.Lock() - z.z = newZ - h.zMu.Unlock() }(zName, z) } // Collect errors (if any). This will also sync on all zones updates diff --git a/plugin/route53/route53_test.go b/plugin/route53/route53_test.go index a0fa38838..12a717f9d 100644 --- a/plugin/route53/route53_test.go +++ b/plugin/route53/route53_test.go @@ -31,19 +31,26 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou if aws.StringValue(in.HostedZoneId) == "0987654321" { return errors.New("bad. zone is bad") } - var rrs []*route53.ResourceRecordSet + rrsResponse := map[string][]*route53.ResourceRecordSet{} for _, r := range []struct { - rType, name, value string + rType, name, value, hostedZoneID string }{ - {"A", "example.org.", "1.2.3.4"}, - {"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334"}, - {"CNAME", "sample.example.org.", "example.org"}, - {"PTR", "example.org.", "ptr.example.org."}, - {"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, - {"NS", "com.", "ns-1536.awsdns-00.co.uk."}, + {"A", "example.org.", "1.2.3.4", "1234567890"}, + {"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334", "1234567890"}, + {"CNAME", "sample.example.org.", "example.org", "1234567890"}, + {"PTR", "example.org.", "ptr.example.org.", "1234567890"}, + {"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.", "1234567890"}, // 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), Name: aws.String(r.name), ResourceRecords: []*route53.ResourceRecord{ @@ -53,9 +60,11 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou }, TTL: aws.Int64(300), }) + rrsResponse[r.hostedZoneID] = rrs } + if ok := fn(&route53.ListResourceRecordSetsOutput{ - ResourceRecordSets: rrs, + ResourceRecordSets: rrsResponse[aws.StringValue(in.HostedZoneId)], }, true); !ok { return errors.New("paging function return false") } @@ -65,7 +74,7 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou func TestRoute53(t *testing.T) { 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 { t.Fatalf("Failed to create Route53: %v", err) } @@ -73,7 +82,7 @@ func TestRoute53(t *testing.T) { 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 { t.Fatalf("Failed to create Route53: %v", err) } @@ -158,7 +167,7 @@ func TestRoute53(t *testing.T) { qname: "example.org", qtype: dns.TypeSOA, 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. { @@ -187,6 +196,13 @@ func TestRoute53(t *testing.T) { expectedCode: dns.RcodeSuccess, 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 { diff --git a/plugin/route53/setup.go b/plugin/route53/setup.go index 494f4f1b6..e016272ca 100644 --- a/plugin/route53/setup.go +++ b/plugin/route53/setup.go @@ -35,7 +35,8 @@ func init() { } 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. // 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 { 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]) } - zone := plugin.Host(parts[0]).Normalize() - if v, ok := keys[zone]; ok && v != parts[1] { - return c.Errf("conflict zone '%s' ('%s' vs. '%s')", zone, v, parts[1]) + if _, ok := keyPairs[args[i]]; ok { + return c.Errf("conflict zone '%s'", args[i]) } - keys[zone] = parts[1] + + keyPairs[args[i]] = struct{}{} + keys[dns] = append(keys[dns], hostedZoneID) } for c.NextBlock() { diff --git a/plugin/route53/setup_test.go b/plugin/route53/setup_test.go index a0b13b3d6..98d7b2bce 100644 --- a/plugin/route53/setup_test.go +++ b/plugin/route53/setup_test.go @@ -53,6 +53,10 @@ func TestSetupRoute53(t *testing.T) { aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY 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 { fallthrough }`) @@ -91,4 +95,17 @@ func TestSetupRoute53(t *testing.T) { if err := setup(c, f); err == nil { 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) + } }