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:
parent
1dba31ee7d
commit
4eeaef29ea
11 changed files with 1069 additions and 0 deletions
|
@ -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
|
||||
|
|
|
@ -27,6 +27,7 @@ var Directives = []string{
|
|||
"errors",
|
||||
"log",
|
||||
"dnstap",
|
||||
"dns64",
|
||||
"acl",
|
||||
"any",
|
||||
"chaos",
|
||||
|
|
|
@ -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
105
man/coredns-dns64.7
Normal 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
|
||||
|
||||
|
|
@ -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
64
plugin/dns64/README.md
Normal 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
204
plugin/dns64/dns64.go
Normal 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
450
plugin/dns64/dns64_test.go
Normal 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
17
plugin/dns64/metrics.go
Normal 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
99
plugin/dns64/setup.go
Normal 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
126
plugin/dns64/setup_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue