Add middleware reverse (#452)
This commit is contained in:
parent
a7c9fd5d6b
commit
61afc6dbad
9 changed files with 804 additions and 0 deletions
|
@ -25,6 +25,7 @@ import (
|
||||||
_ "github.com/miekg/coredns/middleware/metrics"
|
_ "github.com/miekg/coredns/middleware/metrics"
|
||||||
_ "github.com/miekg/coredns/middleware/pprof"
|
_ "github.com/miekg/coredns/middleware/pprof"
|
||||||
_ "github.com/miekg/coredns/middleware/proxy"
|
_ "github.com/miekg/coredns/middleware/proxy"
|
||||||
|
_ "github.com/miekg/coredns/middleware/reverse"
|
||||||
_ "github.com/miekg/coredns/middleware/rewrite"
|
_ "github.com/miekg/coredns/middleware/rewrite"
|
||||||
_ "github.com/miekg/coredns/middleware/root"
|
_ "github.com/miekg/coredns/middleware/root"
|
||||||
_ "github.com/miekg/coredns/middleware/secondary"
|
_ "github.com/miekg/coredns/middleware/secondary"
|
||||||
|
|
|
@ -29,6 +29,7 @@ var directives = []string{
|
||||||
"secondary",
|
"secondary",
|
||||||
"etcd",
|
"etcd",
|
||||||
"kubernetes",
|
"kubernetes",
|
||||||
|
"reverse",
|
||||||
"proxy",
|
"proxy",
|
||||||
"httpprox",
|
"httpprox",
|
||||||
"whoami",
|
"whoami",
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
160:secondary:
|
160:secondary:
|
||||||
170:etcd:
|
170:etcd:
|
||||||
180:kubernetes:
|
180:kubernetes:
|
||||||
|
185:reverse:
|
||||||
190:proxy:
|
190:proxy:
|
||||||
200:httpprox:
|
200:httpprox:
|
||||||
210:whoami:
|
210:whoami:
|
||||||
|
|
97
middleware/reverse/README.md
Normal file
97
middleware/reverse/README.md
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
# reverse
|
||||||
|
|
||||||
|
The *reverse* middleware allows CoreDNS to respond dynamic to an PTR request and the related A/AAAA request.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
~~~
|
||||||
|
reverse NETWORK.. {
|
||||||
|
hostname TEMPLATE
|
||||||
|
[ttl TTL]
|
||||||
|
[fallthrough]
|
||||||
|
~~~
|
||||||
|
|
||||||
|
* **NETWORK** one or more CIDR formatted networks to respond on.
|
||||||
|
* `hostname` inject the ip and zone to an template for the hostname. Defaults to "ip-{ip}.{zone[0]}". See below for template.
|
||||||
|
* `ttl` defaults to 60
|
||||||
|
* `fallthrough` If zone matches and no record can be generated, pass request to the next middleware.
|
||||||
|
|
||||||
|
### Template Syntax
|
||||||
|
The template for the hostname is used for generating the PTR for an reverse lookup and matching the forward lookup back to an ip.
|
||||||
|
|
||||||
|
#### `{ip}`
|
||||||
|
This symbol is **required** to work.
|
||||||
|
V4 network replaces the "." with an "-". 10.1.1.1 results in "10-1-1-1"
|
||||||
|
V6 network removes the ":" and fills the zeros. "ffff::ffff" results in "ffff000000000000000000000000ffff"
|
||||||
|
|
||||||
|
#### `{zone[i]}`
|
||||||
|
This symbol is **optional** to use and can be replaced by a fix zone string.
|
||||||
|
The zone will be matched by the configured listener on the server block key.
|
||||||
|
`i` needs to be replaced to the index of the configured listener zones, starting with 0.
|
||||||
|
|
||||||
|
`arpa.:53 domain.com.:8053` will resolve `zone{0}` to `arpa.` and `zone{1}` to `domain.com.`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
~~~
|
||||||
|
# Serve on port 53
|
||||||
|
# match arpa. and compute.internal. to resolv reverse and forward lookup
|
||||||
|
.arpa.:53 compute.internal.:53 {
|
||||||
|
# proxy unmatched requests
|
||||||
|
proxy . 8.8.8.8
|
||||||
|
|
||||||
|
# answer requests for IPs in this networks
|
||||||
|
# PTR 1.0.32.10.in-addr.arpa. 3600 ip-10-0-32-1.compute.internal.
|
||||||
|
# A ip-10-0-32-1.compute.internal. 3600 10.0.32.1
|
||||||
|
# v6 is also possible
|
||||||
|
# PTR 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.d.f.ip6.arpa. 3600 ip-fd010000000000000000000000000001.compute.internal.
|
||||||
|
# AAAA ip-fd010000000000000000000000000001.compute.internal. 3600 fd01::1
|
||||||
|
reverse 10.32.0.0/16 fd01::/16 {
|
||||||
|
# template of the ip injection to hostname, zone resolved to compute.internal.
|
||||||
|
hostname ip-{ip}.{zone[1]}
|
||||||
|
|
||||||
|
# set time-to-live of the RR
|
||||||
|
ttl 3600
|
||||||
|
|
||||||
|
# forward unanswered or unmatched requests to proxy
|
||||||
|
# without this flag, requesting A/AAAA records on compute.internal. will end here
|
||||||
|
fallthrough
|
||||||
|
}
|
||||||
|
|
||||||
|
# cache with ttl timeout
|
||||||
|
cache
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
~~~
|
||||||
|
# Serve on port 53
|
||||||
|
# listen only on the specific network
|
||||||
|
32.10.in-addr.arpa.arpa.:53 arpa.company.org.:53 {
|
||||||
|
|
||||||
|
reverse 10.32.0.0/16 {
|
||||||
|
# template of the ip injection to hostname, zone resolved to arpa.company.org.
|
||||||
|
hostname "ip-{ip}.v4.{zone[1]}"
|
||||||
|
|
||||||
|
# set time-to-live of the RR
|
||||||
|
ttl 3600
|
||||||
|
|
||||||
|
# fallthrough is not required, v4.arpa.company.org. will be only answered here
|
||||||
|
}
|
||||||
|
|
||||||
|
# cidr closer to the ip wins, so we can overwrite the "default"
|
||||||
|
reverse 10.32.2.0/24 {
|
||||||
|
# its also possible to set fix domain suffix
|
||||||
|
hostname ip-{ip}.fix.arpa.company.org.
|
||||||
|
|
||||||
|
# set time-to-live of the RR
|
||||||
|
ttl 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
# cache with ttl timeout
|
||||||
|
cache
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
|
123
middleware/reverse/network.go
Normal file
123
middleware/reverse/network.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package reverse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type network struct {
|
||||||
|
IPnet *net.IPNet
|
||||||
|
Zone string // forward lookup zone
|
||||||
|
Template string
|
||||||
|
TTL uint32
|
||||||
|
RegexMatchIP *regexp.Regexp
|
||||||
|
Fallthrough bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexDigit = "0123456789abcdef"
|
||||||
|
const templateNameIP = "{ip}"
|
||||||
|
const regexMatchV4 = "((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\-){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))"
|
||||||
|
const regexMatchV6 = "([0-9a-fA-F]{32})"
|
||||||
|
|
||||||
|
// For forward lookup
|
||||||
|
// converts the hostname back to an ip, based on the template
|
||||||
|
// returns nil if there is no ip found
|
||||||
|
func (network *network) hostnameToIP(rname string) net.IP {
|
||||||
|
var matchedIP net.IP
|
||||||
|
|
||||||
|
// use precompiled regex by setup
|
||||||
|
match := network.RegexMatchIP.FindStringSubmatch(rname)
|
||||||
|
// regex did not matched
|
||||||
|
if (len(match) != 2) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.IPnet.IP.To4() != nil {
|
||||||
|
matchedIP = net.ParseIP(strings.Replace(match[1], "-", ".", 4))
|
||||||
|
} else {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
// convert back to an valid ipv6 string with colons
|
||||||
|
for i := 0; i < 8 * 4; i += 4 {
|
||||||
|
buf.WriteString(match[1][i:i + 4])
|
||||||
|
if (i < 28) {
|
||||||
|
buf.WriteString(":")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matchedIP = net.ParseIP(buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// No valid ip or it does not belong to this network
|
||||||
|
if matchedIP == nil || !network.IPnet.Contains(matchedIP) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedIP
|
||||||
|
}
|
||||||
|
|
||||||
|
// For reverse lookup
|
||||||
|
// Converts an Ip to an dns compatible hostname and injects it into the template.domain
|
||||||
|
func (network *network) ipToHostname(ip net.IP) string {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
ipv4 := ip.To4()
|
||||||
|
if ipv4 != nil {
|
||||||
|
// replace . to -
|
||||||
|
name = uitoa(ipv4[0]) + "-" +
|
||||||
|
uitoa(ipv4[1]) + "-" +
|
||||||
|
uitoa(ipv4[2]) + "-" +
|
||||||
|
uitoa(ipv4[3])
|
||||||
|
} else {
|
||||||
|
// assume v6
|
||||||
|
// ensure zeros are present in string
|
||||||
|
buf := make([]byte, 0, len(ip) * 4)
|
||||||
|
for i := 0; i < len(ip); i++ {
|
||||||
|
v := ip[i]
|
||||||
|
buf = append(buf, hexDigit[v >> 4])
|
||||||
|
buf = append(buf, hexDigit[v & 0xF])
|
||||||
|
}
|
||||||
|
name = string(buf)
|
||||||
|
}
|
||||||
|
// inject the converted ip into the fqdn template
|
||||||
|
return strings.Replace(network.Template, templateNameIP, name, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// just the same from net.ip package, but with uint8
|
||||||
|
func uitoa(val uint8) string {
|
||||||
|
if val == 0 {
|
||||||
|
// avoid string allocation
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
var buf [20]byte // big enough for 64bit value base 10
|
||||||
|
i := len(buf) - 1
|
||||||
|
for val >= 10 {
|
||||||
|
q := val / 10
|
||||||
|
buf[i] = byte('0' + val - q * 10)
|
||||||
|
i--
|
||||||
|
val = q
|
||||||
|
}
|
||||||
|
// val < 10
|
||||||
|
buf[i] = byte('0' + val)
|
||||||
|
return string(buf[i:])
|
||||||
|
}
|
||||||
|
|
||||||
|
type networks []network
|
||||||
|
|
||||||
|
// implements the sort interface
|
||||||
|
func (slice networks) Len() int {
|
||||||
|
return len(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements the sort interface
|
||||||
|
// cidr closer to the ip wins (by netmask)
|
||||||
|
func (slice networks) Less(i, j int) bool {
|
||||||
|
isize, _ := slice[i].IPnet.Mask.Size()
|
||||||
|
jsize, _ := slice[j].IPnet.Mask.Size()
|
||||||
|
return isize > jsize
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements the sort interface
|
||||||
|
func (slice networks) Swap(i, j int) {
|
||||||
|
slice[i], slice[j] = slice[j], slice[i]
|
||||||
|
}
|
135
middleware/reverse/network_test.go
Normal file
135
middleware/reverse/network_test.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package reverse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test converting from hostname to IP and back again to hostname
|
||||||
|
func TestNetworkConversion(t *testing.T) {
|
||||||
|
|
||||||
|
_, net4, _ := net.ParseCIDR("10.1.1.0/24")
|
||||||
|
_, net6, _ := net.ParseCIDR("fd01::/64")
|
||||||
|
|
||||||
|
regexIP4, _ := regexp.Compile("^dns-" + regexMatchV4 + "\\.domain\\.internal\\.$")
|
||||||
|
regexIP6, _ := regexp.Compile("^dns-" + regexMatchV6 + "\\.domain\\.internal\\.$")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
network network
|
||||||
|
resultHost string
|
||||||
|
resultIP net.IP
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net4,
|
||||||
|
Template: "dns-{ip}.domain.internal.",
|
||||||
|
RegexMatchIP:regexIP4,
|
||||||
|
},
|
||||||
|
"dns-10-1-1-23.domain.internal.",
|
||||||
|
net.ParseIP("10.1.1.23"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net6,
|
||||||
|
Template: "dns-{ip}.domain.internal.",
|
||||||
|
RegexMatchIP:regexIP6,
|
||||||
|
},
|
||||||
|
"dns-fd01000000000000000000000000a32f.domain.internal.",
|
||||||
|
net.ParseIP("fd01::a32f"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
resultIP := test.network.hostnameToIP(test.resultHost)
|
||||||
|
if !reflect.DeepEqual(test.resultIP, resultIP) {
|
||||||
|
t.Fatalf("Test %d expected %v, got %v", i, test.resultIP, resultIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultHost := test.network.ipToHostname(test.resultIP)
|
||||||
|
if !reflect.DeepEqual(test.resultHost, resultHost) {
|
||||||
|
t.Fatalf("Test %d expected %v, got %v", i, test.resultHost, resultHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkHostnameToIP(t *testing.T) {
|
||||||
|
|
||||||
|
_, net4, _ := net.ParseCIDR("10.1.1.0/24")
|
||||||
|
_, net6, _ := net.ParseCIDR("fd01::/64")
|
||||||
|
|
||||||
|
regexIP4, _ := regexp.Compile("^dns-" + regexMatchV4 + "\\.domain\\.internal\\.$")
|
||||||
|
regexIP6, _ := regexp.Compile("^dns-" + regexMatchV6 + "\\.domain\\.internal\\.$")
|
||||||
|
|
||||||
|
// Test regex does NOT match
|
||||||
|
// All this test should return nil
|
||||||
|
testsNil := []struct {
|
||||||
|
network network
|
||||||
|
hostname string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net4,
|
||||||
|
RegexMatchIP:regexIP4,
|
||||||
|
},
|
||||||
|
// domain does not match
|
||||||
|
"dns-10-1-1-23.domain.internals.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net4,
|
||||||
|
RegexMatchIP:regexIP4,
|
||||||
|
},
|
||||||
|
// IP does match / contain in subnet
|
||||||
|
"dns-200-1-1-23.domain.internals.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net4,
|
||||||
|
RegexMatchIP:regexIP4,
|
||||||
|
},
|
||||||
|
// template does not match
|
||||||
|
"dns-10-1-1-23-x.domain.internal.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net4,
|
||||||
|
RegexMatchIP:regexIP4,
|
||||||
|
},
|
||||||
|
// template does not match
|
||||||
|
"IP-dns-10-1-1-23.domain.internal.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net6,
|
||||||
|
RegexMatchIP:regexIP6,
|
||||||
|
},
|
||||||
|
// template does not match
|
||||||
|
"dnx-fd01000000000000000000000000a32f.domain.internal.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net6,
|
||||||
|
RegexMatchIP:regexIP6,
|
||||||
|
},
|
||||||
|
// no valid v6 (missing one 0, only 31 chars)
|
||||||
|
"dns-fd0100000000000000000000000a32f.domain.internal.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
network{
|
||||||
|
IPnet:net6,
|
||||||
|
RegexMatchIP:regexIP6,
|
||||||
|
},
|
||||||
|
// IP does match / contain in subnet
|
||||||
|
"dns-ed01000000000000000000000000a32f.domain.internal.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range testsNil {
|
||||||
|
resultIP := test.network.hostnameToIP(test.hostname)
|
||||||
|
if resultIP != nil {
|
||||||
|
t.Fatalf("Test %d expected nil, got %v", i, resultIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
middleware/reverse/reverse.go
Normal file
114
middleware/reverse/reverse.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package reverse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/miekg/coredns/request"
|
||||||
|
"github.com/miekg/coredns/middleware"
|
||||||
|
"github.com/miekg/coredns/middleware/pkg/dnsutil"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reverse provides dynamic reverse dns and the related forward rr
|
||||||
|
type Reverse struct {
|
||||||
|
Next middleware.Handler
|
||||||
|
Networks networks
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeDNS implements the middleware.Handler interface.
|
||||||
|
func (reverse Reverse) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||||
|
var rr dns.RR
|
||||||
|
nextHandler := true
|
||||||
|
|
||||||
|
state := request.Request{W: w, Req: r}
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
|
||||||
|
|
||||||
|
switch state.QType(){
|
||||||
|
case dns.TypePTR:
|
||||||
|
address := dnsutil.ExtractAddressFromReverse(state.Name())
|
||||||
|
|
||||||
|
if address == "" {
|
||||||
|
// Not an reverse lookup, but can still be an pointer for an domain
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(address)
|
||||||
|
// loop through the configured networks
|
||||||
|
for _, n := range reverse.Networks {
|
||||||
|
if (n.IPnet.Contains(ip)) {
|
||||||
|
nextHandler = n.Fallthrough
|
||||||
|
rr = &dns.PTR{
|
||||||
|
Hdr: dns.RR_Header{Name: state.QName(), Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: n.TTL},
|
||||||
|
Ptr: n.ipToHostname(ip),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case dns.TypeA:
|
||||||
|
for _, n := range reverse.Networks {
|
||||||
|
if dns.IsSubDomain(n.Zone, state.Name()) {
|
||||||
|
nextHandler = n.Fallthrough
|
||||||
|
|
||||||
|
// skip if requesting an v4 address and network is not v4
|
||||||
|
if n.IPnet.IP.To4() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result := n.hostnameToIP(state.Name())
|
||||||
|
if result != nil {
|
||||||
|
rr = &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: n.TTL},
|
||||||
|
A: result,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
for _, n := range reverse.Networks {
|
||||||
|
if dns.IsSubDomain(n.Zone, state.Name()) {
|
||||||
|
nextHandler = n.Fallthrough
|
||||||
|
|
||||||
|
// Do not use To16 which tries to make v4 in v6
|
||||||
|
if n.IPnet.IP.To4() != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result := n.hostnameToIP(state.Name())
|
||||||
|
if result != nil {
|
||||||
|
rr = &dns.AAAA{
|
||||||
|
Hdr: dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: n.TTL},
|
||||||
|
AAAA: result,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if rr == nil {
|
||||||
|
if reverse.Next == nil || !nextHandler {
|
||||||
|
// could not resolv
|
||||||
|
w.WriteMsg(m)
|
||||||
|
return dns.RcodeNameError, nil
|
||||||
|
}
|
||||||
|
return reverse.Next.ServeDNS(ctx, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Answer = append(m.Answer, rr)
|
||||||
|
state.SizeAndDo(m)
|
||||||
|
w.WriteMsg(m)
|
||||||
|
return dns.RcodeSuccess, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements the Handler interface.
|
||||||
|
func (reverse Reverse) Name() string {
|
||||||
|
return "reverse"
|
||||||
|
}
|
146
middleware/reverse/setup.go
Normal file
146
middleware/reverse/setup.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
package reverse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"strconv"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/miekg/coredns/core/dnsserver"
|
||||||
|
"github.com/miekg/coredns/middleware"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterPlugin("reverse", caddy.Plugin{
|
||||||
|
ServerType: "dns",
|
||||||
|
Action: setupReverse,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupReverse(c *caddy.Controller) error {
|
||||||
|
networks, err := reverseParse(c)
|
||||||
|
if err != nil {
|
||||||
|
return middleware.Error("reverse", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
|
||||||
|
return Reverse{Next: next, Networks:networks}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reverseParse(c *caddy.Controller) (networks, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// normalize zones, validation is almost done by dnsserver
|
||||||
|
zones := make([]string, len(c.ServerBlockKeys))
|
||||||
|
for i, str := range c.ServerBlockKeys {
|
||||||
|
host, _, _ := net.SplitHostPort(str)
|
||||||
|
zones[i] = strings.ToLower(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
networks := networks{}
|
||||||
|
for c.Next() {
|
||||||
|
if c.Val() == "reverse" {
|
||||||
|
|
||||||
|
var cidrs []*net.IPNet
|
||||||
|
|
||||||
|
// parse all networks
|
||||||
|
for _, cidr := range c.RemainingArgs() {
|
||||||
|
if cidr == "{" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, ipnet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.Errf("%v needs to be an CIDR formatted Network\n", cidr)
|
||||||
|
}
|
||||||
|
cidrs = append(cidrs, ipnet)
|
||||||
|
}
|
||||||
|
if len(cidrs) == 0 {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// set defaults
|
||||||
|
var (
|
||||||
|
template = "ip-" + templateNameIP + ".{zone[0]}"
|
||||||
|
ttl = 60
|
||||||
|
fall = false
|
||||||
|
)
|
||||||
|
for c.NextBlock() {
|
||||||
|
switch c.Val() {
|
||||||
|
case "hostname":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
template = c.Val()
|
||||||
|
|
||||||
|
case "ttl":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
ttl, err = strconv.Atoi(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case "fallthrough":
|
||||||
|
fall = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare template
|
||||||
|
// replace {zone[index]} by the listen zone/domain of this config block
|
||||||
|
for i, zone := range zones {
|
||||||
|
template = strings.Replace(template, "{zone[" + string(i + 48) + "]}", zone, 1)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(template, ".") {
|
||||||
|
template += "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract zone from template
|
||||||
|
templateZone := strings.SplitAfterN(template, ".", 2)
|
||||||
|
if len(templateZone) != 2 || templateZone[1] == "" {
|
||||||
|
return nil, c.Errf("Cannot find domain in template '%v'", template)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create for each configured network in this stanza
|
||||||
|
for _, ipnet := range cidrs {
|
||||||
|
// precompile regex for hostname to ip matching
|
||||||
|
regexIP := regexMatchV4
|
||||||
|
if ipnet.IP.To4() == nil {
|
||||||
|
regexIP = regexMatchV6
|
||||||
|
}
|
||||||
|
regex, err := regexp.Compile(
|
||||||
|
"^" + strings.Replace( // inject ip regex into template
|
||||||
|
regexp.QuoteMeta(template), // escape dots
|
||||||
|
regexp.QuoteMeta(templateNameIP),
|
||||||
|
regexIP,
|
||||||
|
1, ) + "$")
|
||||||
|
if err != nil {
|
||||||
|
// invalid regex
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
networks = append(networks, network{
|
||||||
|
IPnet: ipnet,
|
||||||
|
Zone: templateZone[1],
|
||||||
|
Template: template,
|
||||||
|
RegexMatchIP: regex,
|
||||||
|
TTL: uint32(ttl),
|
||||||
|
Fallthrough: fall,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by cidr
|
||||||
|
sort.Sort(networks)
|
||||||
|
return networks, nil
|
||||||
|
}
|
186
middleware/reverse/setup_test.go
Normal file
186
middleware/reverse/setup_test.go
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
package reverse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetupParse(t *testing.T) {
|
||||||
|
|
||||||
|
_, net4, _ := net.ParseCIDR("10.1.1.0/24")
|
||||||
|
_, net6, _ := net.ParseCIDR("fd01::/64")
|
||||||
|
|
||||||
|
regexIP6, _ := regexp.Compile("^ip-" + regexMatchV6 + "\\.domain\\.com\\.$")
|
||||||
|
regexIpv4dynamic, _ := regexp.Compile("^dynamic-" + regexMatchV4 + "-intern\\.dynamic\\.domain\\.com\\.$")
|
||||||
|
regexIpv6dynamic, _ := regexp.Compile("^dynamic-" + regexMatchV6 + "-intern\\.dynamic\\.domain\\.com\\.$")
|
||||||
|
regexIpv4vpndynamic, _ := regexp.Compile("^dynamic-" + regexMatchV4 + "-vpn\\.dynamic\\.domain\\.com\\.$")
|
||||||
|
|
||||||
|
serverBlockKeys := []string{"domain.com.:8053", "dynamic.domain.com.:8053" }
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
inputFileRules string
|
||||||
|
shouldErr bool
|
||||||
|
networks networks
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// with defaults
|
||||||
|
`reverse fd01::/64`,
|
||||||
|
false,
|
||||||
|
networks{network{
|
||||||
|
IPnet: net6,
|
||||||
|
Template: "ip-{ip}.domain.com.",
|
||||||
|
Zone: "domain.com.",
|
||||||
|
TTL: 60,
|
||||||
|
RegexMatchIP: regexIP6,
|
||||||
|
Fallthrough: false,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`reverse`,
|
||||||
|
true,
|
||||||
|
networks{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
//no cidr
|
||||||
|
`reverse 10.1.1.1`,
|
||||||
|
true,
|
||||||
|
networks{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
//no cidr
|
||||||
|
`reverse 10.1.1.0/16 fd00::`,
|
||||||
|
true,
|
||||||
|
networks{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// invalid key
|
||||||
|
`reverse 10.1.1.0/24 {
|
||||||
|
notavailable
|
||||||
|
}`,
|
||||||
|
true,
|
||||||
|
networks{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// no domain suffix
|
||||||
|
`reverse 10.1.1.0/24 {
|
||||||
|
hostname ip-{ip}.
|
||||||
|
}`,
|
||||||
|
true,
|
||||||
|
networks{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// hostname requires an second arg
|
||||||
|
`reverse 10.1.1.0/24 {
|
||||||
|
hostname
|
||||||
|
}`,
|
||||||
|
true,
|
||||||
|
networks{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// template breaks regex compile
|
||||||
|
`reverse 10.1.1.0/24 {
|
||||||
|
hostname ip-{[-x
|
||||||
|
}`,
|
||||||
|
true,
|
||||||
|
networks{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// ttl requires an (u)int
|
||||||
|
`reverse 10.1.1.0/24 {
|
||||||
|
ttl string
|
||||||
|
}`,
|
||||||
|
true,
|
||||||
|
networks{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`reverse fd01::/64 {
|
||||||
|
hostname dynamic-{ip}-intern.{zone[1]}
|
||||||
|
ttl 50
|
||||||
|
}
|
||||||
|
reverse 10.1.1.0/24 {
|
||||||
|
hostname dynamic-{ip}-vpn.{zone[1]}
|
||||||
|
fallthrough
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
networks{network{
|
||||||
|
IPnet: net6,
|
||||||
|
Template: "dynamic-{ip}-intern.dynamic.domain.com.",
|
||||||
|
Zone: "dynamic.domain.com.",
|
||||||
|
TTL: 50,
|
||||||
|
RegexMatchIP:regexIpv6dynamic,
|
||||||
|
Fallthrough: false,
|
||||||
|
}, network{
|
||||||
|
IPnet: net4,
|
||||||
|
Template: "dynamic-{ip}-vpn.dynamic.domain.com.",
|
||||||
|
Zone: "dynamic.domain.com.",
|
||||||
|
TTL: 60,
|
||||||
|
RegexMatchIP: regexIpv4vpndynamic,
|
||||||
|
Fallthrough:true,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// multiple networks in one stanza
|
||||||
|
`reverse fd01::/64 10.1.1.0/24 {
|
||||||
|
hostname dynamic-{ip}-intern.{zone[1]}
|
||||||
|
ttl 50
|
||||||
|
fallthrough
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
networks{network{
|
||||||
|
IPnet: net6,
|
||||||
|
Template: "dynamic-{ip}-intern.dynamic.domain.com.",
|
||||||
|
Zone: "dynamic.domain.com.",
|
||||||
|
TTL: 50,
|
||||||
|
RegexMatchIP:regexIpv6dynamic,
|
||||||
|
Fallthrough: true,
|
||||||
|
}, network{
|
||||||
|
IPnet: net4,
|
||||||
|
Template: "dynamic-{ip}-intern.dynamic.domain.com.",
|
||||||
|
Zone: "dynamic.domain.com.",
|
||||||
|
TTL: 50,
|
||||||
|
RegexMatchIP: regexIpv4dynamic,
|
||||||
|
Fallthrough: true,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// fix domain in template
|
||||||
|
`reverse fd01::/64 {
|
||||||
|
hostname dynamic-{ip}-intern.dynamic.domain.com
|
||||||
|
ttl 300
|
||||||
|
fallthrough
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
networks{network{
|
||||||
|
IPnet: net6,
|
||||||
|
Template: "dynamic-{ip}-intern.dynamic.domain.com.",
|
||||||
|
Zone: "dynamic.domain.com.",
|
||||||
|
TTL: 300,
|
||||||
|
RegexMatchIP:regexIpv6dynamic,
|
||||||
|
Fallthrough: true,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
c := caddy.NewTestController("dns", test.inputFileRules)
|
||||||
|
c.ServerBlockKeys = serverBlockKeys
|
||||||
|
networks, err := reverseParse(c)
|
||||||
|
|
||||||
|
if err == nil && test.shouldErr {
|
||||||
|
t.Fatalf("Test %d expected errors, but got no error", i)
|
||||||
|
} else if err != nil && !test.shouldErr {
|
||||||
|
t.Fatalf("Test %d expected no errors, but got '%v'", i, err)
|
||||||
|
} else {
|
||||||
|
for j, n := range networks {
|
||||||
|
reflect.DeepEqual(test.networks[j], n)
|
||||||
|
if !reflect.DeepEqual(test.networks[j], n) {
|
||||||
|
t.Fatalf("Test %d/%d expected %v, got %v", i, j, test.networks[j], n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue