aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--core/dnsserver/zdirectives.go1
-rw-r--r--core/plugin/zplugin.go1
-rw-r--r--man/coredns-dns64.7105
-rw-r--r--plugin.cfg1
-rw-r--r--plugin/dns64/README.md64
-rw-r--r--plugin/dns64/dns64.go204
-rw-r--r--plugin/dns64/dns64_test.go450
-rw-r--r--plugin/dns64/metrics.go17
-rw-r--r--plugin/dns64/setup.go99
-rw-r--r--plugin/dns64/setup_test.go126
11 files changed, 1069 insertions, 0 deletions
diff --git a/README.md b/README.md
index 956e0dcb4..10dc9f2ba 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ Currently CoreDNS is able to:
* Profiling support (*pprof*).
* Rewrite queries (qtype, qclass and qname) (*rewrite* and *template*).
* Block ANY queries (*any*).
+* Provide DNS64 IPv6 Translation (*dns64*).
And more. Each of the plugins is documented. See [coredns.io/plugins](https://coredns.io/plugins)
for all in-tree plugins, and [coredns.io/explugins](https://coredns.io/explugins) for all
diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go
index 61d96c633..65a51a22b 100644
--- a/core/dnsserver/zdirectives.go
+++ b/core/dnsserver/zdirectives.go
@@ -27,6 +27,7 @@ var Directives = []string{
"errors",
"log",
"dnstap",
+ "dns64",
"acl",
"any",
"chaos",
diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go
index 90267f29b..316f49634 100644
--- a/core/plugin/zplugin.go
+++ b/core/plugin/zplugin.go
@@ -17,6 +17,7 @@ import (
_ "github.com/coredns/coredns/plugin/chaos"
_ "github.com/coredns/coredns/plugin/clouddns"
_ "github.com/coredns/coredns/plugin/debug"
+ _ "github.com/coredns/coredns/plugin/dns64"
_ "github.com/coredns/coredns/plugin/dnssec"
_ "github.com/coredns/coredns/plugin/dnstap"
_ "github.com/coredns/coredns/plugin/erratic"
diff --git a/man/coredns-dns64.7 b/man/coredns-dns64.7
new file mode 100644
index 000000000..0454ba8a3
--- /dev/null
+++ b/man/coredns-dns64.7
@@ -0,0 +1,105 @@
+.\" Generated by Mmark Markdown Processer - mmark.miek.nl
+.TH "COREDNS-DNS64" 7 "January 2020" "CoreDNS" "CoreDNS Plugins"
+
+.SH "NAME"
+.PP
+\fIdns64\fP - enables DNS64 IPv6 transition mechanism.
+
+.SH "DESCRIPTION"
+.PP
+From Wikipedia:
+
+.PP
+.RS
+
+.PP
+DNS64 describes a DNS server that when asked for a domain's AAAA records, but only finds
+A records, synthesizes the AAAA records from the A records.
+
+.RE
+
+.PP
+The synthesis in only performed if the query came in via IPv6.
+
+.PP
+See RFC 6147
+\[la]https://tools.ietf.org/html/rfc6147\[ra] for more information.
+
+.SH "SYNTAX"
+.PP
+.RS
+
+.nf
+dns64 [PREFIX] {
+ [translate\\\_all]
+}
+
+.fi
+.RE
+
+.IP \(bu 4
+[PREFIX] defines a custom prefix instead of the default \fB\fC64:ff9b::/96\fR
+.IP \(bu 4
+\fB\fCtranslate_all\fR translates all queries, including respones that have AAAA results.
+
+
+.SH "EXAMPLES"
+.PP
+Translate with the default well known prefix. Applies to all queries
+
+.PP
+.RS
+
+.nf
+dns64
+
+.fi
+.RE
+
+.PP
+Use a custom prefix
+
+.PP
+.RS
+
+.nf
+dns64 64:1337::/96
+dns64 {
+ prefix 64:1337::/96
+}
+
+.fi
+.RE
+
+.PP
+Enable translation even if an existing AAAA record is present
+
+.PP
+.RS
+
+.nf
+dns64 {
+ translate\_all
+}
+
+.fi
+.RE
+
+.IP \(bu 4
+\fB\fCprefix\fR specifies any local IPv6 prefix to use, instead of the well known prefix (64:ff9b::/96)
+
+
+.SH "BUGS"
+.PP
+Not all features required by DNS64 are implemented, only basic AAAA synthesis.
+
+.IP \(bu 4
+Support "mapping of separate IPv4 ranges to separate IPv6 prefixes"
+.IP \(bu 4
+Resolve PTR records
+.IP \(bu 4
+Follow CNAME records
+.IP \(bu 4
+Make resolver DNSSEC aware
+
+
diff --git a/plugin.cfg b/plugin.cfg
index 3f3dd85fd..76c97cbcb 100644
--- a/plugin.cfg
+++ b/plugin.cfg
@@ -36,6 +36,7 @@ prometheus:metrics
errors:errors
log:log
dnstap:dnstap
+dns64:dns64
acl:acl
any:any
chaos:chaos
diff --git a/plugin/dns64/README.md b/plugin/dns64/README.md
new file mode 100644
index 000000000..6b9c0c54b
--- /dev/null
+++ b/plugin/dns64/README.md
@@ -0,0 +1,64 @@
+# dns64
+
+## Name
+
+*dns64* - enables DNS64 IPv6 transition mechanism.
+
+## Description
+
+From Wikipedia:
+
+> DNS64 describes a DNS server that when asked for a domain's AAAA records, but only finds
+> A records, synthesizes the AAAA records from the A records.
+
+The synthesis in only performed if the query came in via IPv6.
+
+See [RFC 6147](https://tools.ietf.org/html/rfc6147) for more information.
+
+## Syntax
+
+~~~
+dns64 [PREFIX] {
+ [translate\_all]
+}
+~~~
+
+* [PREFIX] defines a custom prefix instead of the default `64:ff9b::/96`
+* `translate_all` translates all queries, including respones that have AAAA results.
+
+## Examples
+
+Translate with the default well known prefix. Applies to all queries
+
+~~~
+dns64
+~~~
+
+Use a custom prefix
+
+~~~
+dns64 64:1337::/96
+# Or
+dns64 {
+ prefix 64:1337::/96
+}
+~~~
+
+Enable translation even if an existing AAAA record is present
+
+~~~
+dns64 {
+ translate_all
+}
+~~~
+
+* `prefix` specifies any local IPv6 prefix to use, instead of the well known prefix (64:ff9b::/96)
+
+## Bugs
+
+Not all features required by DNS64 are implemented, only basic AAAA synthesis.
+
+* Support "mapping of separate IPv4 ranges to separate IPv6 prefixes"
+* Resolve PTR records
+* Follow CNAME records
+* Make resolver DNSSEC aware. See: [RFC 6147 Section 3](https://tools.ietf.org/html/rfc6147#section-3)
diff --git a/plugin/dns64/dns64.go b/plugin/dns64/dns64.go
new file mode 100644
index 000000000..b06b0bdb9
--- /dev/null
+++ b/plugin/dns64/dns64.go
@@ -0,0 +1,204 @@
+// Package dns64 implements a plugin that performs DNS64.
+//
+// See: RFC 6147 (https://tools.ietf.org/html/rfc6147)
+package dns64
+
+import (
+ "context"
+ "errors"
+ "net"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/metrics"
+ "github.com/coredns/coredns/plugin/pkg/nonwriter"
+ "github.com/coredns/coredns/plugin/pkg/response"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// UpstreamInt wraps the Upstream API for dependency injection during testing
+type UpstreamInt interface {
+ Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error)
+}
+
+// DNS64 performs DNS64.
+type DNS64 struct {
+ Next plugin.Handler
+ Prefix *net.IPNet
+ TranslateAll bool // Not comply with 5.1.1
+ Upstream UpstreamInt
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (d *DNS64) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ // Don't proxy if we don't need to.
+ if !requestShouldIntercept(&request.Request{W: w, Req: r}) {
+ return d.Next.ServeDNS(ctx, w, r)
+ }
+
+ // Pass the request to the next plugin in the chain, but intercept the response.
+ nw := nonwriter.New(w)
+ origRc, origErr := d.Next.ServeDNS(ctx, nw, r)
+ if nw.Msg == nil { // somehow we didn't get a response (or raw bytes were written)
+ return origRc, origErr
+ }
+
+ // If the response doesn't need DNS64, short-circuit.
+ if !d.responseShouldDNS64(nw.Msg) {
+ w.WriteMsg(nw.Msg)
+ return origRc, origErr
+ }
+
+ // otherwise do the actual DNS64 request and response synthesis
+ msg, err := d.DoDNS64(ctx, w, r, nw.Msg)
+ if err != nil {
+ // err means we weren't able to even issue the A request
+ // to CoreDNS upstream
+ return dns.RcodeServerFailure, err
+ }
+
+ RequestsTranslatedCount.WithLabelValues(metrics.WithServer(ctx)).Inc()
+ w.WriteMsg(msg)
+ return msg.MsgHdr.Rcode, nil
+}
+
+// Name implements the Handler interface.
+func (d *DNS64) Name() string { return "dns64" }
+
+// requestShouldIntercept returns true if the request represents one that is eligible
+// for DNS64 rewriting:
+// 1. The request came in over IPv6 (not in RFC)
+// 2. The request is of type AAAA
+// 3. The request is of class INET
+func requestShouldIntercept(req *request.Request) bool {
+ // Only intercept with this when the request came in over IPv6. This is not mentioned in the RFC.
+ // File an issue if you think we should translate even requests made using IPv4, or have a configuration flag
+ if req.Family() == 1 { // If it came in over v4, don't do anything.
+ return false
+ }
+
+ // Do not modify if question is not AAAA or not of class IN. See RFC 6147 5.1
+ return req.QType() == dns.TypeAAAA && req.QClass() == dns.ClassINET
+}
+
+// responseShouldDNS64 returns true if the response indicates we should attempt
+// DNS64 rewriting:
+// 1. The response has no valid (RFC 5.1.4) AAAA records (RFC 5.1.1)
+// 2. The response code (RCODE) is not 3 (Name Error) (RFC 5.1.2)
+//
+// Note that requestShouldIntercept must also have been true, so the request
+// is known to be of type AAAA.
+func (d *DNS64) responseShouldDNS64(origResponse *dns.Msg) bool {
+ ty, _ := response.Typify(origResponse, time.Now().UTC())
+
+ // Handle NameError normally. See RFC 6147 5.1.2
+ // All other error types are "equivalent" to empty response
+ if ty == response.NameError {
+ return false
+ }
+
+ // If we've configured to always translate, well, then always translate.
+ if d.TranslateAll {
+ return true
+ }
+
+ // if response includes AAAA record, no need to rewrite
+ for _, rr := range origResponse.Answer {
+ if rr.Header().Rrtype == dns.TypeAAAA {
+ return false
+ }
+ }
+ return true
+}
+
+// DoDNS64 takes an (empty) response to an AAAA question, issues the A request,
+// and synthesizes the answer. Returns the response message, or error on internal failure.
+func (d *DNS64) DoDNS64(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, origResponse *dns.Msg) (*dns.Msg, error) {
+ req := request.Request{W: w, Req: r} // req is unused
+ resp, err := d.Upstream.Lookup(ctx, req, req.Name(), dns.TypeA)
+ if err != nil {
+ return nil, err
+ }
+ out := d.Synthesize(r, origResponse, resp)
+ return out, nil
+}
+
+// Synthesize merges the AAAA response and the records from the A response
+func (d *DNS64) Synthesize(origReq, origResponse, resp *dns.Msg) *dns.Msg {
+ ret := dns.Msg{}
+ ret.SetReply(origReq)
+
+ // 5.3.2: DNS64 MUST pass the additional section unchanged
+ ret.Extra = resp.Extra
+ ret.Ns = resp.Ns
+
+ // 5.1.7: The TTL is the minimum of the A RR and the SOA RR. If SOA is
+ // unknown, then the TTL is the minimum of A TTL and 600
+ SOATtl := uint32(600) // Default NS record TTL
+ for _, ns := range origResponse.Ns {
+ if ns.Header().Rrtype == dns.TypeSOA {
+ SOATtl = ns.Header().Ttl
+ }
+ }
+
+ ret.Answer = make([]dns.RR, 0, len(resp.Answer))
+ // convert A records to AAAA records
+ for _, rr := range resp.Answer {
+ header := rr.Header()
+ // 5.3.3: All other RR's MUST be returned unchanged
+ if header.Rrtype != dns.TypeA {
+ ret.Answer = append(ret.Answer, rr)
+ continue
+ }
+
+ aaaa, _ := to6(d.Prefix, rr.(*dns.A).A)
+
+ // ttl is min of SOA TTL and A TTL
+ ttl := SOATtl
+ if rr.Header().Ttl < ttl {
+ ttl = rr.Header().Ttl
+ }
+
+ // Replace A answer with a DNS64 AAAA answer
+ ret.Answer = append(ret.Answer, &dns.AAAA{
+ Hdr: dns.RR_Header{
+ Name: header.Name,
+ Rrtype: dns.TypeAAAA,
+ Class: header.Class,
+ Ttl: ttl,
+ },
+ AAAA: aaaa,
+ })
+ }
+ return &ret
+}
+
+// to6 takes a prefix and IPv4 address and returns an IPv6 address according to RFC 6052.
+func to6(prefix *net.IPNet, addr net.IP) (net.IP, error) {
+ addr = addr.To4()
+ if addr == nil {
+ return nil, errors.New("not a valid IPv4 address")
+ }
+
+ n, _ := prefix.Mask.Size()
+ // Assumes prefix has been validated during setup
+ v6 := make([]byte, 16)
+ i, j := 0, 0
+
+ for ; i < n/8; i++ {
+ v6[i] = prefix.IP[i]
+ }
+ for ; i < 8; i, j = i+1, j+1 {
+ v6[i] = addr[j]
+ }
+ if i == 8 {
+ i++
+ }
+ for ; j < 4; i, j = i+1, j+1 {
+ v6[i] = addr[j]
+ }
+
+ return v6, nil
+}
diff --git a/plugin/dns64/dns64_test.go b/plugin/dns64/dns64_test.go
new file mode 100644
index 000000000..fe8b77ec6
--- /dev/null
+++ b/plugin/dns64/dns64_test.go
@@ -0,0 +1,450 @@
+package dns64
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "reflect"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnstest"
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+func To6(prefix, address string) (net.IP, error) {
+ _, pref, _ := net.ParseCIDR(prefix)
+ addr := net.ParseIP(address)
+
+ return to6(pref, addr)
+}
+
+func TestTo6(t *testing.T) {
+
+ v6, err := To6("64:ff9b::/96", "64.64.64.64")
+ if err != nil {
+ t.Error(err)
+ }
+ if v6.String() != "64:ff9b::4040:4040" {
+ t.Errorf("%d", v6)
+ }
+
+ v6, err = To6("64:ff9b::/64", "64.64.64.64")
+ if err != nil {
+ t.Error(err)
+ }
+ if v6.String() != "64:ff9b::40:4040:4000:0" {
+ t.Errorf("%d", v6)
+ }
+
+ v6, err = To6("64:ff9b::/56", "64.64.64.64")
+ if err != nil {
+ t.Error(err)
+ }
+ if v6.String() != "64:ff9b:0:40:40:4040::" {
+ t.Errorf("%d", v6)
+ }
+
+ v6, err = To6("64::/32", "64.64.64.64")
+ if err != nil {
+ t.Error(err)
+ }
+ if v6.String() != "64:0:4040:4040::" {
+ t.Errorf("%d", v6)
+ }
+}
+
+func TestResponseShould(t *testing.T) {
+ var tests = []struct {
+ resp dns.Msg
+ translateAll bool
+ expected bool
+ }{
+ // If there's an AAAA record, then no
+ {
+ resp: dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Rcode: dns.RcodeSuccess,
+ },
+ Answer: []dns.RR{
+ test.AAAA("example.com. IN AAAA ::1"),
+ },
+ },
+ expected: false,
+ },
+ // If there's no AAAA, then true
+ {
+ resp: dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Rcode: dns.RcodeSuccess,
+ },
+ Ns: []dns.RR{
+ test.SOA("example.com. IN SOA foo bar 1 1 1 1 1"),
+ },
+ },
+ expected: true,
+ },
+ // Failure, except NameError, should be true
+ {
+ resp: dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Rcode: dns.RcodeNotImplemented,
+ },
+ Ns: []dns.RR{
+ test.SOA("example.com. IN SOA foo bar 1 1 1 1 1"),
+ },
+ },
+ expected: true,
+ },
+ // NameError should be false
+ {
+ resp: dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Rcode: dns.RcodeNameError,
+ },
+ Ns: []dns.RR{
+ test.SOA("example.com. IN SOA foo bar 1 1 1 1 1"),
+ },
+ },
+ expected: false,
+ },
+ // If there's an AAAA record, but translate_all is configured, then yes
+ {
+ resp: dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Rcode: dns.RcodeSuccess,
+ },
+ Answer: []dns.RR{
+ test.AAAA("example.com. IN AAAA ::1"),
+ },
+ },
+ translateAll: true,
+ expected: true,
+ },
+ }
+
+ d := DNS64{}
+
+ for idx, tc := range tests {
+ t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
+ d.TranslateAll = tc.translateAll
+ actual := d.responseShouldDNS64(&tc.resp)
+ if actual != tc.expected {
+ t.Fatalf("Expected %v got %v", tc.expected, actual)
+ }
+ })
+ }
+}
+
+func TestDNS64(t *testing.T) {
+ var cases = []struct {
+ // a brief summary of the test case
+ name string
+
+ // the request
+ req *dns.Msg
+
+ // the initial response from the "downstream" server
+ initResp *dns.Msg
+
+ // A response to provide
+ aResp *dns.Msg
+
+ // the expected ultimate result
+ resp *dns.Msg
+ }{
+ {
+ // no AAAA record, yes A record. Do DNS64
+ name: "standard flow",
+ req: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ RecursionDesired: true,
+ Opcode: dns.OpcodeQuery,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ },
+ initResp: &dns.Msg{ //success, no answers
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Ns: []dns.RR{test.SOA("example.com. 70 IN SOA foo bar 1 1 1 1 1")},
+ },
+ aResp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 43,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeA, dns.ClassINET}},
+ Answer: []dns.RR{
+ test.A("example.com. 60 IN A 192.0.2.42"),
+ test.A("example.com. 5000 IN A 192.0.2.43"),
+ },
+ },
+
+ resp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Answer: []dns.RR{
+ test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"),
+ // override RR ttl to SOA ttl, since it's lower
+ test.AAAA("example.com. 70 IN AAAA 64:ff9b::192.0.2.43"),
+ },
+ },
+ },
+ {
+ // name exists, but has neither A nor AAAA record
+ name: "a empty",
+ req: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ RecursionDesired: true,
+ Opcode: dns.OpcodeQuery,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ },
+ initResp: &dns.Msg{ //success, no answers
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")},
+ },
+ aResp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 43,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeA, dns.ClassINET}},
+ Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")},
+ },
+
+ resp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")},
+ Answer: []dns.RR{}, // just to make comparison happy
+ },
+ },
+ {
+ // Query error other than NameError
+ name: "non-nxdomain error",
+ req: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ RecursionDesired: true,
+ Opcode: dns.OpcodeQuery,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ },
+ initResp: &dns.Msg{ // failure
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeRefused,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ },
+ aResp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 43,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeA, dns.ClassINET}},
+ Answer: []dns.RR{
+ test.A("example.com. 60 IN A 192.0.2.42"),
+ test.A("example.com. 5000 IN A 192.0.2.43"),
+ },
+ },
+
+ resp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Answer: []dns.RR{
+ test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"),
+ test.AAAA("example.com. 600 IN AAAA 64:ff9b::192.0.2.43"),
+ },
+ },
+ },
+ {
+ // nxdomain (NameError): don't even try an A request.
+ name: "nxdomain",
+ req: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ RecursionDesired: true,
+ Opcode: dns.OpcodeQuery,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ },
+ initResp: &dns.Msg{ // failure
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeNameError,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")},
+ },
+ resp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeNameError,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")},
+ },
+ },
+ {
+ // AAAA record exists
+ name: "AAAA record",
+ req: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ RecursionDesired: true,
+ Opcode: dns.OpcodeQuery,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ },
+
+ initResp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Answer: []dns.RR{
+ test.AAAA("example.com. 60 IN AAAA ::1"),
+ test.AAAA("example.com. 5000 IN AAAA ::2"),
+ },
+ },
+
+ resp: &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: 42,
+ Opcode: dns.OpcodeQuery,
+ RecursionDesired: true,
+ Rcode: dns.RcodeSuccess,
+ Response: true,
+ },
+ Question: []dns.Question{dns.Question{"example.com.", dns.TypeAAAA, dns.ClassINET}},
+ Answer: []dns.RR{
+ test.AAAA("example.com. 60 IN AAAA ::1"),
+ test.AAAA("example.com. 5000 IN AAAA ::2"),
+ },
+ },
+ },
+ }
+
+ _, pfx, _ := net.ParseCIDR("64:ff9b::/96")
+
+ for idx, tc := range cases {
+ t.Run(fmt.Sprintf("%d_%s", idx, tc.name), func(t *testing.T) {
+ d := DNS64{
+ Next: &fakeHandler{t, tc.initResp},
+ Prefix: pfx,
+ Upstream: &fakeUpstream{t, tc.req.Question[0].Name, tc.aResp},
+ }
+
+ rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: "::1"})
+ rc, err := d.ServeDNS(context.Background(), rec, tc.req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ actual := rec.Msg
+ if actual.Rcode != rc {
+ t.Fatalf("ServeDNS should return real result code %q != %q", actual.Rcode, rc)
+ }
+
+ if !reflect.DeepEqual(actual, tc.resp) {
+ t.Fatalf("Final answer should match expected %q != %q", actual, tc.resp)
+ }
+ })
+ }
+}
+
+type fakeHandler struct {
+ t *testing.T
+ reply *dns.Msg
+}
+
+func (fh *fakeHandler) ServeDNS(_ context.Context, w dns.ResponseWriter, _ *dns.Msg) (int, error) {
+ if fh.reply == nil {
+ panic("fakeHandler ServeDNS with nil reply")
+ }
+ w.WriteMsg(fh.reply)
+
+ return fh.reply.Rcode, nil
+}
+func (fh *fakeHandler) Name() string {
+ return "fake"
+}
+
+type fakeUpstream struct {
+ t *testing.T
+ qname string
+ resp *dns.Msg
+}
+
+func (fu *fakeUpstream) Lookup(_ context.Context, _ request.Request, name string, typ uint16) (*dns.Msg, error) {
+ if fu.qname == "" {
+ fu.t.Fatalf("Unexpected A lookup for %s", name)
+ }
+ if name != fu.qname {
+ fu.t.Fatalf("Wrong A lookup for %s, expected %s", name, fu.qname)
+ }
+
+ if typ != dns.TypeA {
+ fu.t.Fatalf("Wrong lookup type %d, expected %d", typ, dns.TypeA)
+ }
+
+ return fu.resp, nil
+}
diff --git a/plugin/dns64/metrics.go b/plugin/dns64/metrics.go
new file mode 100644
index 000000000..892455adb
--- /dev/null
+++ b/plugin/dns64/metrics.go
@@ -0,0 +1,17 @@
+package dns64
+
+import (
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+var (
+ // RequestsTranslatedCount is the number of DNS requests translated by dns64.
+ RequestsTranslatedCount = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: "dns",
+ Name: "requests_dns64_translated_total",
+ Help: "Counter of DNS requests translated by dns64.",
+ }, []string{"server"})
+)
diff --git a/plugin/dns64/setup.go b/plugin/dns64/setup.go
new file mode 100644
index 000000000..7d1737229
--- /dev/null
+++ b/plugin/dns64/setup.go
@@ -0,0 +1,99 @@
+package dns64
+
+import (
+ "net"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/metrics"
+ clog "github.com/coredns/coredns/plugin/pkg/log"
+ "github.com/coredns/coredns/plugin/pkg/upstream"
+
+ "github.com/caddyserver/caddy"
+)
+
+var log = clog.NewWithPlugin("dns64")
+
+func init() { plugin.Register("dns64", setup) }
+
+func setup(c *caddy.Controller) error {
+ dns64, err := dns64Parse(c)
+ if err != nil {
+ return plugin.Error("dns64", err)
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ dns64.Next = next
+ return dns64
+ })
+
+ // Register all metrics.
+ c.OnStartup(func() error {
+ metrics.MustRegister(c, RequestsTranslatedCount)
+ return nil
+ })
+
+ return nil
+}
+
+func dns64Parse(c *caddy.Controller) (*DNS64, error) {
+ _, defaultPref, _ := net.ParseCIDR("64:ff9b::/96")
+ dns64 := &DNS64{
+ Upstream: upstream.New(),
+ Prefix: defaultPref,
+ }
+
+ for c.Next() {
+ args := c.RemainingArgs()
+ if len(args) == 1 {
+ pref, err := parsePrefix(c, args[0])
+
+ if err != nil {
+ return nil, err
+ }
+ dns64.Prefix = pref
+ continue
+ }
+ if len(args) > 0 {
+ return nil, c.ArgErr()
+ }
+
+ for c.NextBlock() {
+ switch c.Val() {
+ case "prefix":
+ if !c.NextArg() {
+ return nil, c.ArgErr()
+ }
+ pref, err := parsePrefix(c, c.Val())
+
+ if err != nil {
+ return nil, err
+ }
+ dns64.Prefix = pref
+ case "translate_all":
+ dns64.TranslateAll = true
+ default:
+ return nil, c.Errf("unknown property '%s'", c.Val())
+ }
+ }
+ }
+ return dns64, nil
+}
+
+func parsePrefix(c *caddy.Controller, addr string) (*net.IPNet, error) {
+ _, pref, err := net.ParseCIDR(addr)
+ if err != nil {
+ return nil, err
+ }
+
+ // Test for valid prefix
+ n, total := pref.Mask.Size()
+ if total != 128 {
+ return nil, c.Errf("invalid netmask %d IPv6 address: %q", total, pref)
+ }
+ if n%8 != 0 || n < 32 || n > 96 {
+ return nil, c.Errf("invalid prefix length %q", pref)
+ }
+
+ return pref, nil
+}
diff --git a/plugin/dns64/setup_test.go b/plugin/dns64/setup_test.go
new file mode 100644
index 000000000..13a18a0e1
--- /dev/null
+++ b/plugin/dns64/setup_test.go
@@ -0,0 +1,126 @@
+package dns64
+
+import (
+ "testing"
+
+ "github.com/caddyserver/caddy"
+)
+
+func TestSetupDns64(t *testing.T) {
+ tests := []struct {
+ inputUpstreams string
+ shouldErr bool
+ prefix string
+ }{
+ {
+ `dns64`,
+ false,
+ "64:ff9b::/96",
+ },
+ {
+ `dns64 64:dead::/96`,
+ false,
+ "64:dead::/96",
+ },
+ {
+ `dns64 {
+ translate_all
+ }`,
+ false,
+ "64:ff9b::/96",
+ },
+ {
+ `dns64`,
+ false,
+ "64:ff9b::/96",
+ },
+ {
+ `dns64 {
+ prefix 64:ff9b::/96
+ }`,
+ false,
+ "64:ff9b::/96",
+ },
+ {
+ `dns64 {
+ prefix 64:ff9b::/32
+ }`,
+ false,
+ "64:ff9b::/32",
+ },
+ {
+ `dns64 {
+ prefix 64:ff9b::/52
+ }`,
+ true,
+ "64:ff9b::/52",
+ },
+ {
+ `dns64 {
+ prefix 64:ff9b::/104
+ }`,
+ true,
+ "64:ff9b::/104",
+ },
+ {
+ `dns64 {
+ prefix 8.8.8.8/24
+ }`,
+ true,
+ "8.8.9.9/24",
+ },
+ {
+ `dns64 {
+ prefix 64:ff9b::/96
+ }`,
+ false,
+ "64:ff9b::/96",
+ },
+ {
+ `dns64 {
+ prefix 2002:ac12:b083::/96
+ }`,
+ false,
+ "2002:ac12:b083::/96",
+ },
+ {
+ `dns64 {
+ prefix 2002:c0a8:a88a::/48
+ }`,
+ false,
+ "2002:c0a8:a88a::/48",
+ },
+ {
+ `dns64 foobar {
+ prefix 64:ff9b::/96
+ }`,
+ true,
+ "64:ff9b::/96",
+ },
+ {
+ `dns64 foobar`,
+ true,
+ "64:ff9b::/96",
+ },
+ {
+ `dns64 {
+ foobar
+ }`,
+ true,
+ "64:ff9b::/96",
+ },
+ }
+
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.inputUpstreams)
+ dns64, err := dns64Parse(c)
+ if (err != nil) != test.shouldErr {
+ t.Errorf("Test %d expected %v error, got %v for %s", i+1, test.shouldErr, err, test.inputUpstreams)
+ }
+ if err == nil {
+ if dns64.Prefix.String() != test.prefix {
+ t.Errorf("Test %d expected prefix %s, got %v", i+1, test.prefix, dns64.Prefix.String())
+ }
+ }
+ }
+}