Commit 4eeaef29 authored by Ben Kochie's avatar Ben Kochie Committed by GitHub

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: default avatarBen Kochie <superq@gmail.com>

* Make dns64 consistent.
Signed-off-by: default avatarBen Kochie <superq@gmail.com>

* Cleanup README
Signed-off-by: default avatarBen Kochie <superq@gmail.com>

* Cleanup minor issues.
Signed-off-by: default avatarBen Kochie <superq@gmail.com>

* Remove proxy method.
Signed-off-by: default avatarBen Kochie <superq@gmail.com>

* dns64: big cleanup

* Make the code a bit more idiomatic
* Add tests
* use proper Upstream API
Signed-off-by: default avatarCasey Callendrello <c1@caseyc.net>
Signed-off-by: default avatarBen Kochie <superq@gmail.com>

* A little more clenaup

* Fix some docs.
* Use the correct plugin register method.
* Cleanup some review items.
Signed-off-by: default avatarBen 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: default avatarBen Kochie <superq@gmail.com>

* Add DNSSEC bug link
Signed-off-by: default avatarBen Kochie <superq@gmail.com>

* Test cleanup
Signed-off-by: default avatarBen Kochie <superq@gmail.com>

* dns64: more test cleanup
Signed-off-by: default avatarCasey Callendrello <c1@caseyc.net>
Co-authored-by: default avatarCasey Callendrello <c1@caseyc.net>
parent 1dba31ee
......@@ -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"
......
.\" 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
......
# 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)
// 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
}
This diff is collapsed.
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"})
)
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
}
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())
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment