aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugin/route53/README.md12
-rw-r--r--plugin/route53/route53.go97
-rw-r--r--plugin/route53/route53_test.go42
-rw-r--r--plugin/route53/setup.go15
-rw-r--r--plugin/route53/setup_test.go17
5 files changed, 123 insertions, 60 deletions
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)
+ }
}