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
11 changed files with 1069 additions and 0 deletions

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