Add dns64 plugin (#3534)

* Add dns64 plugin

Add external plugin to core in-tree.
* Pull code from upstream: https://github.com/serverwentdown/dns64
* Update docs.

Signed-off-by: Ben Kochie <superq@gmail.com>

* Make dns64 consistent.

Signed-off-by: Ben Kochie <superq@gmail.com>

* Cleanup README

Signed-off-by: Ben Kochie <superq@gmail.com>

* Cleanup minor issues.

Signed-off-by: Ben Kochie <superq@gmail.com>

* Remove proxy method.

Signed-off-by: Ben Kochie <superq@gmail.com>

* dns64: big cleanup

* Make the code a bit more idiomatic
* Add tests
* use proper Upstream API

Signed-off-by: Casey Callendrello <c1@caseyc.net>
Signed-off-by: Ben Kochie <superq@gmail.com>

* A little more clenaup

* Fix some docs.
* Use the correct plugin register method.
* Cleanup some review items.

Signed-off-by: Ben Kochie <superq@gmail.com>

* Add metrics counter for DNS64 translations

Add a basic counter of how many DNS64 translations have been completed.

Signed-off-by: Ben Kochie <superq@gmail.com>

* Add DNSSEC bug link

Signed-off-by: Ben Kochie <superq@gmail.com>

* Test cleanup

Signed-off-by: Ben Kochie <superq@gmail.com>

* dns64: more test cleanup

Signed-off-by: Casey Callendrello <c1@caseyc.net>

Co-authored-by: Casey Callendrello <c1@caseyc.net>
This commit is contained in:
Ben Kochie 2020-03-26 08:42:23 +01:00 committed by GitHub
parent 1dba31ee7d
commit 4eeaef29ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1069 additions and 0 deletions

View file

@ -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

View file

@ -27,6 +27,7 @@ var Directives = []string{
"errors",
"log",
"dnstap",
"dns64",
"acl",
"any",
"chaos",

View file

@ -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"

105
man/coredns-dns64.7 Normal file
View file

@ -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

View file

@ -36,6 +36,7 @@ prometheus:metrics
errors:errors
log:log
dnstap:dnstap
dns64:dns64
acl:acl
any:any
chaos:chaos

64
plugin/dns64/README.md Normal file
View file

@ -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)

204
plugin/dns64/dns64.go Normal file
View file

@ -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
}

450
plugin/dns64/dns64_test.go Normal file
View file

@ -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
}

17
plugin/dns64/metrics.go Normal file
View file

@ -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"})
)

99
plugin/dns64/setup.go Normal file
View file

@ -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
}

126
plugin/dns64/setup_test.go Normal file
View file

@ -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())
}
}
}
}