diff --git a/docs/_functions/domain/PTR.md b/docs/_functions/domain/PTR.md index 0b4c55f02..eabac1f81 100644 --- a/docs/_functions/domain/PTR.md +++ b/docs/_functions/domain/PTR.md @@ -8,13 +8,54 @@ parameters: PTR adds a PTR record to the domain. -The name should be the relative label for the domain, or may be a FQDN that ends with `.`. - -* If the name is a valid IP address, DNSControl will *magically* replace it with a string that is appropriate for the domain. That is, if the domain ends with `in-addr.arpa` it will generate the IPv4-style reverse name; if the domain ends with `ipv6.arpa` it will generate the IPv6-style reverse name. DNSControl will truncate it as appropriate for the netmask. -* If the name ends with `in-addr.arpa.` or `ipv6.arpa.` (not the `.` at the end), DNSControl will truncate it as appropriate for the domain. If the FQDN does not fit within the domain, this will raise an error. +The name is normally a the relative label for the domain, or a FQDN that ends with `.`. If magic mode is enabled (see below) it can also be an IP address, which will be replaced by the proper string automatically, thus +saving the user from having to reverse the IP address manually. Target should be a string representing the FQDN of a host. Like all FQDNs in DNSControl, it must end with a `.`. +**Magic Mode:** + +PTR records are complex and typos are common. Therefore DNSControl +enables features to save labor and +prevent typos. This magic is only +enabled when the domain ends with `in-addr.arpa.` or `ipv6.arpa.`. + +*Automatic IP-to-reverse:* If the name is a valid IP address, DNSControl will replace it with +a string that is appropriate for the domain. That is, if the domain +ends with `in-addr.arpa` (no `.`) and name is a valid IPv4 address, the name +will be replaced with the correct string to make a reverse lookup for that address. +IPv6 is properly handled too. + +*Extra Validation:* DNSControl considers it an error to include a name that +is inappropriate for the domain. For example +`PTR('1.2.3.4', 'f.co.')` is valid for the domain `D("3.2.1.in-addr.arpa',` + but DNSControl will generate an error if the domain is `D("9.9.9.in-addr.arpa',`. +This is because `1.2.3.4` is contained in `1.2.3.0/24` but not `9.9.9.0/24`. +This validation works for IPv6, IPv4, and +RFC2317 "Classless in-addr.arpa delegation" domains. + +*Automatic truncation:* DNSControl will automatically truncate FQDNs +as needed. +If the name is a FQDN ending with `.`, DNSControl will verify that the +name is contained within the CIDR block implied by domain. For example +if name is `4.3.2.1.in-addr.arpa.` (note the trailing `.`) +and the domain is `2.1.in-addr.arpa` (no trailing `.`) +then the name will be replaced with `4.3`. Note that the output +of `REV('1.2.3.4')` is `4.3.2.1.in-addr.arpa.`, which means the following +are all equivalent: + +* `PTR(REV('1.2.3.4'), ` +* `PTR('4.3.2.1.in-addr.arpa.'), ` +* `PTR('4.3',` // Assuming the domain is `2.1.in-addr.arpa` + +All magic is RFC2317-aware. We use the first format listed in the +RFC for both `REV()` and `PTR()`. The format is +`FIRST/MASK.C.B.A.in-addr.arpa` where `FIRST` is the first IP address +of the zone, `MASK` is the netmask of the zone (25-31 inclusive), +and A, B, C are the first 3 octets of the IP address. For example +`172.20.18.130/27` is located in a zone named +`128/27.18.20.172.in-addr.arpa` + {% include startExample.html %} {% highlight js %} D(REV('1.2.3.0/24'), REGISTRAR, DnsProvider(BIND), @@ -25,6 +66,10 @@ D(REV('1.2.3.0/24'), REGISTRAR, DnsProvider(BIND), PTR('1.2.3.10', 'ten.example.com.'), // '10' ); +D(REV('9.9.9.128/25'), REGISTRAR, DnsProvider(BIND), + PTR('9.9.9.129', 'first.example.com.'), +); + D(REV('2001:db8:302::/48'), REGISTRAR, DnsProvider(BIND), PTR('1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0', 'foo.example.com.'), // 2001:db8:302::1 // If the first parameter is a valid IP address, DNSControl will generate the correct name: diff --git a/docs/_functions/global/REV.md b/docs/_functions/global/REV.md index f4ad28c95..49ee0321d 100644 --- a/docs/_functions/global/REV.md +++ b/docs/_functions/global/REV.md @@ -4,17 +4,30 @@ parameters: - address --- -`REV` returns the reverse lookup domain for an IP network. For example `REV('1.2.3.0/24')` returns `3.2.1.in-addr.arpa.` -and `REV('2001:db8:302::/48)` returns `2.0.3.0.8.b.d.0.1.0.0.2.ip6.arpa.`. This is used in `D()` functions to create -reverse DNS (`PTR`) zones. +`REV` returns the reverse lookup domain for an IP network. For +example `REV('1.2.3.0/24')` returns `3.2.1.in-addr.arpa.` and +`REV('2001:db8:302::/48)` returns `2.0.3.0.8.b.d.0.1.0.0.2.ip6.arpa.`. +This is used in `D()` functions to create reverse DNS lookup zones. -This is a convenience function. You could specify `D('3.2.1.in-addr.arpa`, ...` if you like to do things manually -and permit typos to creep in. +This is a convenience function. You could specify `D('3.2.1.in-addr.arpa`, +...` if you like to do things manually but why would you risk making +typos? -The network portion of the IP address (`/24`) must always be specified. +`REV` complies with RFC2317, "Classless in-addr.arpa delegation" +for netmasks of size /25 through /31. +While the RFC permits any format, we abide by the recommended format: +`FIRST/MASK.C.B.A.in-addr.arpa` where `FIRST` is the first IP address +of the zone, `MASK` is the netmask of the zone (25-31 inclusive), +and A, B, C are the first 3 octets of the IP address. For example +`172.20.18.130/27` is located in a zone named +`128/27.18.20.172.in-addr.arpa` -Note that the lower bits are zeroed out automatically. Thus, `REV('1.2.3.4/24') is the same as `REV('1.2.3.0/24')`. This -may generate warnings or errors in the future. +If the address does not include a "/" then `REV` assumes /32 for IPv4 addresses +and /128 for IPv6 addresses. + +Note that the lower bits (the ones outside the netmask) must be zeros. They are not +zeroed out automatically. Thus, `REV('1.2.3.4/24') is an error. This is done +to catch typos. {% include startExample.html %} {% highlight js %} @@ -29,8 +42,8 @@ D(REV('1.2.3.0/24'), REGISTRAR, DnsProvider(BIND), D(REV('2001:db8:302::/48'), REGISTRAR, DnsProvider(BIND), PTR("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", 'foo.example.com.'), // 2001:db8:302::1 // These take advantage of DNSControl's ability to generate the right name: - PTR("2001:db8:302::2", 'two.example.com.'), // 2.0.0. etc. etc. - PTR("2001:db8:302::3", 'three.example.com.'), // + PTR("2001:db8:302::2", 'two.example.com.'), // 2.0.0... + PTR("2001:db8:302::3", 'three.example.com.'), // 3.0.0... ); diff --git a/pkg/transform/arpa.go b/pkg/transform/arpa.go index ca35377eb..727333290 100644 --- a/pkg/transform/arpa.go +++ b/pkg/transform/arpa.go @@ -4,6 +4,8 @@ import ( "fmt" "net" "strings" + + "github.com/pkg/errors" ) func ReverseDomainName(cidr string) (string, error) { @@ -16,11 +18,26 @@ func ReverseDomainName(cidr string) (string, error) { return "", err } base = strings.TrimRight(base, ".") + if !a.Equal(c.IP) { + return "", errors.Errorf("CIDR %v has 1 bits beyond the mask", cidr) + } + bits, total := c.Mask.Size() var toTrim int if bits == 0 { return "", fmt.Errorf("Cannot use /0 in reverse cidr") } + + // Handle IPv4 "Classless in-addr.arpa delegation" RFC2317: + if total == 32 && bits >= 25 && bits < 32 { + // first address / netmask . Class-b-arpa. + fparts := strings.Split(c.IP.String(), ".") + first := fparts[3] + bparts := strings.SplitN(base, ".", 2) + return fmt.Sprintf("%s/%d.%s", first, bits, bparts[1]), nil + } + + // Handle IPv4 Class-full and IPv6: if total == 32 { if bits%8 != 0 { return "", fmt.Errorf("IPv4 mask must be multiple of 8 bits") diff --git a/pkg/transform/arpa_test.go b/pkg/transform/arpa_test.go index 42078688c..533c0ba81 100644 --- a/pkg/transform/arpa_test.go +++ b/pkg/transform/arpa_test.go @@ -10,13 +10,15 @@ func TestReverse(t *testing.T) { out string }{ {"174.136.107.0/24", false, "107.136.174.in-addr.arpa"}, + {"174.136.107.1/24", true, "107.136.174.in-addr.arpa"}, {"174.136.0.0/16", false, "136.174.in-addr.arpa"}, - {"174.136.43.0/16", false, "136.174.in-addr.arpa"}, //do bits set inside the masked range matter? Should this be invalid? Is there a shorter way to specify this? + {"174.136.43.0/16", true, "136.174.in-addr.arpa"}, {"174.0.0.0/8", false, "174.in-addr.arpa"}, - {"174.136.43.0/8", false, "174.in-addr.arpa"}, - {"174.136.43.0/8", false, "174.in-addr.arpa"}, + {"174.136.43.0/8", true, "174.in-addr.arpa"}, + {"174.136.0.44/8", true, "174.in-addr.arpa"}, + {"174.136.45.45/8", true, "174.in-addr.arpa"}, {"2001::/16", false, "1.0.0.2.ip6.arpa"}, {"2001:0db8:0123:4567:89ab:cdef:1234:5670/124", false, "7.6.5.4.3.2.1.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1.0.8.b.d.0.1.0.0.2.ip6.arpa"}, @@ -24,6 +26,51 @@ func TestReverse(t *testing.T) { {"174.136.107.14/32", false, "14.107.136.174.in-addr.arpa"}, {"2001:0db8:0123:4567:89ab:cdef:1234:5678/128", false, "8.7.6.5.4.3.2.1.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1.0.8.b.d.0.1.0.0.2.ip6.arpa"}, + // IPv4 "Classless in-addr.arpa delegation" RFC2317. + // From examples in the RFC: + {"192.0.2.0/25", false, "0/25.2.0.192.in-addr.arpa"}, + {"192.0.2.128/26", false, "128/26.2.0.192.in-addr.arpa"}, + {"192.0.2.192/26", false, "192/26.2.0.192.in-addr.arpa"}, + // All the base cases: + {"174.1.0.0/25", false, "0/25.0.1.174.in-addr.arpa"}, + {"174.1.0.0/26", false, "0/26.0.1.174.in-addr.arpa"}, + {"174.1.0.0/27", false, "0/27.0.1.174.in-addr.arpa"}, + {"174.1.0.0/28", false, "0/28.0.1.174.in-addr.arpa"}, + {"174.1.0.0/29", false, "0/29.0.1.174.in-addr.arpa"}, + {"174.1.0.0/30", false, "0/30.0.1.174.in-addr.arpa"}, + {"174.1.0.0/31", false, "0/31.0.1.174.in-addr.arpa"}, + // /25 (all cases) + {"174.1.0.0/25", false, "0/25.0.1.174.in-addr.arpa"}, + {"174.1.0.128/25", false, "128/25.0.1.174.in-addr.arpa"}, + // /26 (all cases) + {"174.1.0.0/26", false, "0/26.0.1.174.in-addr.arpa"}, + {"174.1.0.64/26", false, "64/26.0.1.174.in-addr.arpa"}, + {"174.1.0.128/26", false, "128/26.0.1.174.in-addr.arpa"}, + {"174.1.0.192/26", false, "192/26.0.1.174.in-addr.arpa"}, + // /27 (all cases) + {"174.1.0.0/27", false, "0/27.0.1.174.in-addr.arpa"}, + {"174.1.0.32/27", false, "32/27.0.1.174.in-addr.arpa"}, + {"174.1.0.64/27", false, "64/27.0.1.174.in-addr.arpa"}, + {"174.1.0.96/27", false, "96/27.0.1.174.in-addr.arpa"}, + {"174.1.0.128/27", false, "128/27.0.1.174.in-addr.arpa"}, + {"174.1.0.160/27", false, "160/27.0.1.174.in-addr.arpa"}, + {"174.1.0.192/27", false, "192/27.0.1.174.in-addr.arpa"}, + {"174.1.0.224/27", false, "224/27.0.1.174.in-addr.arpa"}, + // /28 (first 2, last 2) + {"174.1.0.0/28", false, "0/28.0.1.174.in-addr.arpa"}, + {"174.1.0.16/28", false, "16/28.0.1.174.in-addr.arpa"}, + {"174.1.0.224/28", false, "224/28.0.1.174.in-addr.arpa"}, + {"174.1.0.240/28", false, "240/28.0.1.174.in-addr.arpa"}, + // /29 (first 2 cases) + {"174.1.0.0/29", false, "0/29.0.1.174.in-addr.arpa"}, + {"174.1.0.8/29", false, "8/29.0.1.174.in-addr.arpa"}, + // /30 (first 2 cases) + {"174.1.0.0/30", false, "0/30.0.1.174.in-addr.arpa"}, + {"174.1.0.4/30", false, "4/30.0.1.174.in-addr.arpa"}, + // /31 (first 2 cases) + {"174.1.0.0/31", false, "0/31.0.1.174.in-addr.arpa"}, + {"174.1.0.2/31", false, "2/31.0.1.174.in-addr.arpa"}, + //Errror Cases: {"0.0.0.0/0", true, ""}, {"2001::/0", true, ""}, @@ -37,7 +84,7 @@ func TestReverse(t *testing.T) { t.Error("Should not have errored ", err) } else if tst.isError && err == nil { t.Errorf("Should have errored, but didn't. Got %s", d) - } else if d != tst.out { + } else if (!tst.isError) && d != tst.out { t.Errorf("Expected '%s' but got '%s'", tst.out, d) } }) diff --git a/pkg/transform/ptr.go b/pkg/transform/ptr.go index 620d17855..9c677b713 100644 --- a/pkg/transform/ptr.go +++ b/pkg/transform/ptr.go @@ -1,7 +1,10 @@ package transform import ( + "fmt" "net" + "regexp" + "strconv" "strings" "github.com/pkg/errors" @@ -44,10 +47,56 @@ func ipv4magic(name, domain string) (string, error) { if err != nil { return name, err } - if !strings.HasSuffix(rev, "."+domain) { - err = errors.Errorf("ERROR: PTR record %v in wrong IPv4 domain (%v)", name, domain) + result := strings.TrimSuffix(rev, "."+domain) + + // Are we in the right domain? + if strings.HasSuffix(rev, "."+domain) { + return result, nil } - return strings.TrimSuffix(rev, "."+domain), err + if ipMatchesClasslessDomain(ip, domain) { + return strings.SplitN(rev, ".", 2)[0], nil + } + + return "", errors.Errorf("PTR record %v in wrong IPv4 domain (%v)", name, domain) +} + +var isRfc2317Format1 = regexp.MustCompile(`(\d{1,3})/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.in-addr\.arpa$`) + +// ipMatchesClasslessDomain returns true if ip is appropriate for domain. +// domain is a reverse DNS lookup zone (in-addr.arpa) as described in RFC2317. +func ipMatchesClasslessDomain(ip net.IP, domain string) bool { + + // The unofficial but preferred format in RFC2317: + m := isRfc2317Format1.FindStringSubmatch(domain) + if m != nil { + // IP: Domain: + // 172.20.18.27 128/27.18.20.172.in-addr.arpa + // A B C D F M X Y Z + // The following should be true: + // A==Z, B==Y, C==X. + // If you mask ip by M, the last octet should be F. + ii := ip.To4() + a, b, c, _ := ii[0], ii[1], ii[2], ii[3] + f, m, x, y, z := atob(m[1]), atob(m[2]), atob(m[3]), atob(m[4]), atob(m[5]) + masked := ip.Mask(net.CIDRMask(int(m), 32)) + if a == z && b == y && c == x && masked.Equal(net.IPv4(a, b, c, f)) { + return true + } + } + + // To extend this to include other formats, add them here. + + return false +} + +// atob converts a to a byte value or panics. +func atob(s string) byte { + if i, err := strconv.Atoi(s); err == nil { + if i < 256 { + return byte(i) + } + } + panic(fmt.Sprintf("(%v) matched \\d{1,3} but is not a byte", s)) } func ipv6magic(name, domain string) (string, error) { @@ -63,7 +112,7 @@ func ipv6magic(name, domain string) (string, error) { return name, err } if !strings.HasSuffix(rev, "."+domain) { - err = errors.Errorf("ERROR: PTR record %v in wrong IPv6 domain (%v)", name, domain) + err = errors.Errorf("PTR record %v in wrong IPv6 domain (%v)", name, domain) } return strings.TrimSuffix(rev, "."+domain), err } diff --git a/pkg/transform/ptr_test.go b/pkg/transform/ptr_test.go index e255e1e2f..3a3c48162 100644 --- a/pkg/transform/ptr_test.go +++ b/pkg/transform/ptr_test.go @@ -41,6 +41,13 @@ func TestPtrMagic(t *testing.T) { {"1.0.0.0.0.0.0.0.0.0.0.0.0", "0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "1.0.0.0.0.0.0.0.0.0.0.0.0", false}, {"1.0.0.0.0.0.0.0.0.0.0.0.0.0", "0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "1.0.0.0.0.0.0.0.0.0.0.0.0.0", false}, + // RFC2317 (Classless) + // 172.20.18.160/27 is .160 - .191: + {"172.20.18.159", "160/27.18.20.172.in-addr.arpa", "", true}, + {"172.20.18.160", "160/27.18.20.172.in-addr.arpa", "160", false}, + {"172.20.18.191", "160/27.18.20.172.in-addr.arpa", "191", false}, + {"172.20.18.192", "160/27.18.20.172.in-addr.arpa", "", true}, + // If it doesn't end in .arpa, the magic is disabled: {"1.2.3.4", "example.com", "1.2.3.4", false}, {"1", "example.com", "1", false}, diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index 0086778bb..19feb3092 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -181,7 +181,7 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti // Read foundRecords: foundRecords := make([]*models.RecordConfig, 0) var oldSerial, newSerial uint32 - zonefile := filepath.Join(*bindBaseDir, strings.ToLower(dc.Name)+".zone") + zonefile := filepath.Join(*bindBaseDir, strings.Replace(strings.ToLower(dc.Name), "/", "_", -1)+".zone") foundFH, err := os.Open(zonefile) zoneFileFound := err == nil if err != nil && !os.IsNotExist(os.ErrNotExist) { diff --git a/providers/google/google.go b/providers/google/google.go index 001bcfd6c..26439ec0b 100644 --- a/providers/google/google.go +++ b/providers/google/google.go @@ -15,7 +15,7 @@ import ( ) func init() { - providers.RegisterDomainServiceProviderType("GCLOUD", New) + providers.RegisterDomainServiceProviderType("GCLOUD", New, providers.CanUsePTR) } type gcloud struct { diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go index 0998d9a83..4992ce89b 100644 --- a/providers/route53/route53Provider.go +++ b/providers/route53/route53Provider.go @@ -42,7 +42,7 @@ func newRoute53(m map[string]string, metadata json.RawMessage) (providers.DNSSer } func init() { - providers.RegisterDomainServiceProviderType("ROUTE53", newRoute53) + providers.RegisterDomainServiceProviderType("ROUTE53", newRoute53, providers.CanUsePTR) } func sPtr(s string) *string { return &s