aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Richard Hillmann <Project0@users.noreply.github.com> 2017-02-09 20:39:48 +0100
committerGravatar Miek Gieben <miek@miek.nl> 2017-02-09 19:39:48 +0000
commit61afc6dbad70253e812cdda06b33da136e74f5a8 (patch)
tree66c709508b3abd812669ff867d99961e8004f03c
parenta7c9fd5d6b5157958a3df8dba0cdc1f24407957b (diff)
downloadcoredns-61afc6dbad70253e812cdda06b33da136e74f5a8.tar.gz
coredns-61afc6dbad70253e812cdda06b33da136e74f5a8.tar.zst
coredns-61afc6dbad70253e812cdda06b33da136e74f5a8.zip
Add middleware reverse (#452)
-rw-r--r--core/coredns.go1
-rw-r--r--core/dnsserver/zdirectives.go1
-rw-r--r--middleware.cfg1
-rw-r--r--middleware/reverse/README.md97
-rw-r--r--middleware/reverse/network.go123
-rw-r--r--middleware/reverse/network_test.go135
-rw-r--r--middleware/reverse/reverse.go114
-rw-r--r--middleware/reverse/setup.go146
-rw-r--r--middleware/reverse/setup_test.go186
9 files changed, 804 insertions, 0 deletions
diff --git a/core/coredns.go b/core/coredns.go
index d7043c804..b621f2c27 100644
--- a/core/coredns.go
+++ b/core/coredns.go
@@ -25,6 +25,7 @@ import (
_ "github.com/miekg/coredns/middleware/metrics"
_ "github.com/miekg/coredns/middleware/pprof"
_ "github.com/miekg/coredns/middleware/proxy"
+ _ "github.com/miekg/coredns/middleware/reverse"
_ "github.com/miekg/coredns/middleware/rewrite"
_ "github.com/miekg/coredns/middleware/root"
_ "github.com/miekg/coredns/middleware/secondary"
diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go
index a55461854..ff6722998 100644
--- a/core/dnsserver/zdirectives.go
+++ b/core/dnsserver/zdirectives.go
@@ -29,6 +29,7 @@ var directives = []string{
"secondary",
"etcd",
"kubernetes",
+ "reverse",
"proxy",
"httpprox",
"whoami",
diff --git a/middleware.cfg b/middleware.cfg
index 71434ffd8..0a9527e5c 100644
--- a/middleware.cfg
+++ b/middleware.cfg
@@ -37,6 +37,7 @@
160:secondary:
170:etcd:
180:kubernetes:
+185:reverse:
190:proxy:
200:httpprox:
210:whoami:
diff --git a/middleware/reverse/README.md b/middleware/reverse/README.md
new file mode 100644
index 000000000..7437a3079
--- /dev/null
+++ b/middleware/reverse/README.md
@@ -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
+}
+~~~
+
+
+
diff --git a/middleware/reverse/network.go b/middleware/reverse/network.go
new file mode 100644
index 000000000..7f408326b
--- /dev/null
+++ b/middleware/reverse/network.go
@@ -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]
+} \ No newline at end of file
diff --git a/middleware/reverse/network_test.go b/middleware/reverse/network_test.go
new file mode 100644
index 000000000..ee4db1206
--- /dev/null
+++ b/middleware/reverse/network_test.go
@@ -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)
+ }
+ }
+}
diff --git a/middleware/reverse/reverse.go b/middleware/reverse/reverse.go
new file mode 100644
index 000000000..9cf7d751a
--- /dev/null
+++ b/middleware/reverse/reverse.go
@@ -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"
+}
diff --git a/middleware/reverse/setup.go b/middleware/reverse/setup.go
new file mode 100644
index 000000000..024f7a39e
--- /dev/null
+++ b/middleware/reverse/setup.go
@@ -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
+}
diff --git a/middleware/reverse/setup_test.go b/middleware/reverse/setup_test.go
new file mode 100644
index 000000000..4ec9e8a55
--- /dev/null
+++ b/middleware/reverse/setup_test.go
@@ -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)
+ }
+ }
+ }
+ }
+} \ No newline at end of file