aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
Diffstat (limited to 'plugin')
-rw-r--r--plugin/route53/README.md46
-rw-r--r--plugin/route53/route53.go97
-rw-r--r--plugin/route53/route53_test.go81
-rw-r--r--plugin/route53/setup.go89
-rw-r--r--plugin/route53/setup_test.go37
5 files changed, 350 insertions, 0 deletions
diff --git a/plugin/route53/README.md b/plugin/route53/README.md
new file mode 100644
index 000000000..21bab8be6
--- /dev/null
+++ b/plugin/route53/README.md
@@ -0,0 +1,46 @@
+# route53
+
+## Name
+
+*route53* - enables serving zone data from AWS route53.
+
+## Description
+
+The hosts plugin is useful for serving zones from resource record sets in AWS route53.
+This plugin only supports A and AAAA records. The route53 plugin can be used when
+coredns is deployed on AWS.
+
+## Syntax
+
+~~~ txt
+route53 [ZONE:HOSTED_ZONE_ID...] {
+ [aws_access_key AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY]
+}
+~~~
+
+* **ZONE** the name of the domain 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
+ to be used when query AWS (optional). If they are not provided, then coredns tries to access
+ AWS credentials the same way as AWS CLI, e.g., environmental variables, AWS credentials file,
+ instance profile credentials, etc.
+
+## Examples
+
+Enable route53, with implicit aws credentials:
+
+~~~ txt
+. {
+ route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7
+}
+~~~
+
+Enable route53, with explicit aws credentials:
+
+~~~ txt
+. {
+ route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 {
+ aws_access_key AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
+ }
+}
+~~~
diff --git a/plugin/route53/route53.go b/plugin/route53/route53.go
new file mode 100644
index 000000000..0554887a6
--- /dev/null
+++ b/plugin/route53/route53.go
@@ -0,0 +1,97 @@
+// Package route53 implements a plugin that returns resource records
+// from AWS route53
+package route53
+
+import (
+ "net"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/service/route53"
+ "github.com/aws/aws-sdk-go/service/route53/route53iface"
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Route53 is a plugin that returns RR from AWS route53
+type Route53 struct {
+ Next plugin.Handler
+
+ zones []string
+ keys map[string]string
+ client route53iface.Route53API
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (rr Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ state := request.Request{W: w, Req: r}
+ qname := state.Name()
+
+ zone := plugin.Zones(rr.zones).Matches(qname)
+ if zone == "" {
+ return plugin.NextOrFailure(rr.Name(), rr.Next, ctx, w, r)
+ }
+
+ output, err := rr.client.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{
+ HostedZoneId: aws.String(rr.keys[zone]),
+ StartRecordName: aws.String(qname),
+ StartRecordType: aws.String(state.Type()),
+ MaxItems: aws.String("1"),
+ })
+ if err != nil {
+ return dns.RcodeServerFailure, err
+ }
+
+ answers := []dns.RR{}
+ switch state.QType() {
+ case dns.TypeA:
+ answers = a(qname, output.ResourceRecordSets)
+ case dns.TypeAAAA:
+ answers = aaaa(qname, output.ResourceRecordSets)
+ }
+
+ if len(answers) == 0 {
+ return plugin.NextOrFailure(rr.Name(), rr.Next, ctx, w, r)
+ }
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
+ m.Answer = answers
+
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+func a(zone string, rrss []*route53.ResourceRecordSet) []dns.RR {
+ answers := []dns.RR{}
+ for _, rrs := range rrss {
+ for _, rr := range rrs.ResourceRecords {
+ r := new(dns.A)
+ r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: uint32(aws.Int64Value(rrs.TTL))}
+ r.A = net.ParseIP(aws.StringValue(rr.Value)).To4()
+ answers = append(answers, r)
+ }
+ }
+ return answers
+}
+
+func aaaa(zone string, rrss []*route53.ResourceRecordSet) []dns.RR {
+ answers := []dns.RR{}
+ for _, rrs := range rrss {
+ for _, rr := range rrs.ResourceRecords {
+ r := new(dns.AAAA)
+ r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: uint32(aws.Int64Value(rrs.TTL))}
+ r.AAAA = net.ParseIP(aws.StringValue(rr.Value)).To16()
+ answers = append(answers, r)
+ }
+ }
+ return answers
+}
+
+// Name implements the Handler interface.
+func (rr Route53) Name() string { return "route53" }
diff --git a/plugin/route53/route53_test.go b/plugin/route53/route53_test.go
new file mode 100644
index 000000000..50f3c0c8f
--- /dev/null
+++ b/plugin/route53/route53_test.go
@@ -0,0 +1,81 @@
+package route53
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnstest"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/service/route53"
+ "github.com/aws/aws-sdk-go/service/route53/route53iface"
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+type mockedRoute53 struct {
+ route53iface.Route53API
+}
+
+func (mockedRoute53) ListResourceRecordSets(input *route53.ListResourceRecordSetsInput) (*route53.ListResourceRecordSetsOutput, error) {
+ return &route53.ListResourceRecordSetsOutput{
+ ResourceRecordSets: []*route53.ResourceRecordSet{
+ {
+ ResourceRecords: []*route53.ResourceRecord{
+ {
+ Value: aws.String("10.2.3.4"),
+ },
+ },
+ },
+ },
+ }, nil
+}
+
+func TestRoute53(t *testing.T) {
+ r := Route53{
+ zones: []string{"example.org."},
+ keys: map[string]string{"example.org.": "1234567890"},
+ client: mockedRoute53{},
+ }
+
+ tests := []struct {
+ qname string
+ qtype uint16
+ expectedCode int
+ expectedReply []string // ownernames for the records in the additional section.
+ expectedErr error
+ }{
+ {
+ qname: "example.org",
+ qtype: dns.TypeA,
+ expectedCode: dns.RcodeSuccess,
+ expectedReply: []string{"example.org."},
+ expectedErr: nil,
+ },
+ }
+
+ ctx := context.TODO()
+
+ for i, tc := range tests {
+ req := new(dns.Msg)
+ req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype)
+
+ rec := dnstest.NewRecorder(&test.ResponseWriter{})
+ code, err := r.ServeDNS(ctx, rec, req)
+
+ if err != tc.expectedErr {
+ t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err)
+ }
+ if code != int(tc.expectedCode) {
+ t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code)
+ }
+ if len(tc.expectedReply) != 0 {
+ for i, expected := range tc.expectedReply {
+ actual := rec.Msg.Answer[i].Header().Name
+ if actual != expected {
+ t.Errorf("Test %d: Expected answer %s, but got %s", i, expected, actual)
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/route53/setup.go b/plugin/route53/setup.go
new file mode 100644
index 000000000..92e25a738
--- /dev/null
+++ b/plugin/route53/setup.go
@@ -0,0 +1,89 @@
+package route53
+
+import (
+ "strings"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/route53"
+ "github.com/aws/aws-sdk-go/service/route53/route53iface"
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("route53", caddy.Plugin{
+ ServerType: "dns",
+ Action: func(c *caddy.Controller) error {
+ f := func(credential *credentials.Credentials) route53iface.Route53API {
+ return route53.New(session.Must(session.NewSession(&aws.Config{
+ Credentials: credential,
+ })))
+ }
+ return setup(c, f)
+ },
+ })
+}
+
+func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Route53API) error {
+ keys := map[string]string{}
+ var credential *credentials.Credentials
+ for c.Next() {
+ args := c.RemainingArgs()
+
+ for i := 0; i < len(args); i++ {
+ parts := strings.SplitN(args[i], ":", 2)
+ if len(parts) != 2 {
+ return c.Errf("invalid zone '%s'", args[i])
+ }
+ if parts[0] == "" || parts[1] == "" {
+ 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])
+ }
+ keys[zone] = parts[1]
+ }
+
+ for c.NextBlock() {
+ switch c.Val() {
+ case "aws_access_key":
+ v := c.RemainingArgs()
+ if len(v) < 2 {
+ return c.Errf("invalid access key '%v'", v)
+ }
+ credential = credentials.NewStaticCredentials(v[0], v[1], "")
+ default:
+ return c.Errf("unknown property '%s'", c.Val())
+ }
+ }
+ }
+ client := f(credential)
+ zones := []string{}
+ for zone, v := range keys {
+ // Make sure enough credentials is needed
+ if _, err := client.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{
+ HostedZoneId: aws.String(v),
+ MaxItems: aws.String("1"),
+ }); err != nil {
+ return c.Errf("aws error: '%s'", err)
+ }
+
+ zones = append(zones, zone)
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ return Route53{
+ Next: next,
+ keys: keys,
+ zones: zones,
+ client: client,
+ }
+ })
+
+ return nil
+}
diff --git a/plugin/route53/setup_test.go b/plugin/route53/setup_test.go
new file mode 100644
index 000000000..8e90e9965
--- /dev/null
+++ b/plugin/route53/setup_test.go
@@ -0,0 +1,37 @@
+package route53
+
+import (
+ "testing"
+
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/service/route53/route53iface"
+ "github.com/mholt/caddy"
+)
+
+func TestSetupRoute53(t *testing.T) {
+ f := func(credential *credentials.Credentials) route53iface.Route53API {
+ return mockedRoute53{}
+ }
+
+ c := caddy.NewTestController("dns", `route53`)
+ if err := setup(c, f); err != nil {
+ t.Fatalf("Expected no errors, but got: %v", err)
+ }
+
+ c = caddy.NewTestController("dns", `route53 :`)
+ if err := setup(c, f); err == nil {
+ t.Fatalf("Expected errors, but got: %v", err)
+ }
+
+ c = caddy.NewTestController("dns", `route53 example.org:12345678`)
+ if err := setup(c, f); err != nil {
+ t.Fatalf("Expected no errors, but got: %v", err)
+ }
+
+ c = caddy.NewTestController("dns", `route53 example.org:12345678 {
+ aws_access_key
+}`)
+ if err := setup(c, f); err == nil {
+ t.Fatalf("Expected errors, but got: %v", err)
+ }
+}