1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

PTR should handle "Classless in-addr.arpa delegation" RFC2317 (#149)

* Handle IPv4 "Classless in-addr.arpa delegation" RFC2317 (partial).
* Validate PTR name when in RFC2317 "Classless in-addr.arpa delegation" domains.
* Update docs
* Set CanUsePTR for Route53 and Google CloudDNS.
* BIND: Replace "/" with "_" in filenames.
This commit is contained in:
Tom Limoncelli
2017-07-10 19:24:55 -04:00
committed by GitHub
parent 9e66402e0b
commit e563c53658
9 changed files with 203 additions and 25 deletions

View File

@ -8,13 +8,54 @@ parameters:
PTR adds a PTR record to the domain. 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 `.`. 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.
* 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.
Target should be a string representing the FQDN of a host. Like all FQDNs in DNSControl, it must end with a `.`. 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 %} {% include startExample.html %}
{% highlight js %} {% highlight js %}
D(REV('1.2.3.0/24'), REGISTRAR, DnsProvider(BIND), 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' 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), 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 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: // If the first parameter is a valid IP address, DNSControl will generate the correct name:

View File

@ -4,17 +4,30 @@ parameters:
- address - 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.` `REV` returns the reverse lookup domain for an IP network. For
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 example `REV('1.2.3.0/24')` returns `3.2.1.in-addr.arpa.` and
reverse DNS (`PTR`) zones. `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 This is a convenience function. You could specify `D('3.2.1.in-addr.arpa`,
and permit typos to creep in. ...` 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 If the address does not include a "/" then `REV` assumes /32 for IPv4 addresses
may generate warnings or errors in the future. 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 %} {% include startExample.html %}
{% highlight js %} {% 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), 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 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: // 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::2", 'two.example.com.'), // 2.0.0...
PTR("2001:db8:302::3", 'three.example.com.'), // PTR("2001:db8:302::3", 'three.example.com.'), // 3.0.0...
); );

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"github.com/pkg/errors"
) )
func ReverseDomainName(cidr string) (string, error) { func ReverseDomainName(cidr string) (string, error) {
@ -16,11 +18,26 @@ func ReverseDomainName(cidr string) (string, error) {
return "", err return "", err
} }
base = strings.TrimRight(base, ".") 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() bits, total := c.Mask.Size()
var toTrim int var toTrim int
if bits == 0 { if bits == 0 {
return "", fmt.Errorf("Cannot use /0 in reverse cidr") 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 total == 32 {
if bits%8 != 0 { if bits%8 != 0 {
return "", fmt.Errorf("IPv4 mask must be multiple of 8 bits") return "", fmt.Errorf("IPv4 mask must be multiple of 8 bits")

View File

@ -10,13 +10,15 @@ func TestReverse(t *testing.T) {
out string out string
}{ }{
{"174.136.107.0/24", false, "107.136.174.in-addr.arpa"}, {"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.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.0.0.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.43.0/8", false, "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::/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"}, {"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"}, {"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"}, {"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: //Errror Cases:
{"0.0.0.0/0", true, ""}, {"0.0.0.0/0", true, ""},
{"2001::/0", true, ""}, {"2001::/0", true, ""},
@ -37,7 +84,7 @@ func TestReverse(t *testing.T) {
t.Error("Should not have errored ", err) t.Error("Should not have errored ", err)
} else if tst.isError && err == nil { } else if tst.isError && err == nil {
t.Errorf("Should have errored, but didn't. Got %s", d) 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) t.Errorf("Expected '%s' but got '%s'", tst.out, d)
} }
}) })

View File

@ -1,7 +1,10 @@
package transform package transform
import ( import (
"fmt"
"net" "net"
"regexp"
"strconv"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -44,10 +47,56 @@ func ipv4magic(name, domain string) (string, error) {
if err != nil { if err != nil {
return name, err return name, err
} }
if !strings.HasSuffix(rev, "."+domain) { result := strings.TrimSuffix(rev, "."+domain)
err = errors.Errorf("ERROR: PTR record %v in wrong IPv4 domain (%v)", name, 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) { func ipv6magic(name, domain string) (string, error) {
@ -63,7 +112,7 @@ func ipv6magic(name, domain string) (string, error) {
return name, err return name, err
} }
if !strings.HasSuffix(rev, "."+domain) { 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 return strings.TrimSuffix(rev, "."+domain), err
} }

View File

@ -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", 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}, {"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: // If it doesn't end in .arpa, the magic is disabled:
{"1.2.3.4", "example.com", "1.2.3.4", false}, {"1.2.3.4", "example.com", "1.2.3.4", false},
{"1", "example.com", "1", false}, {"1", "example.com", "1", false},

View File

@ -181,7 +181,7 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
// Read foundRecords: // Read foundRecords:
foundRecords := make([]*models.RecordConfig, 0) foundRecords := make([]*models.RecordConfig, 0)
var oldSerial, newSerial uint32 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) foundFH, err := os.Open(zonefile)
zoneFileFound := err == nil zoneFileFound := err == nil
if err != nil && !os.IsNotExist(os.ErrNotExist) { if err != nil && !os.IsNotExist(os.ErrNotExist) {

View File

@ -15,7 +15,7 @@ import (
) )
func init() { func init() {
providers.RegisterDomainServiceProviderType("GCLOUD", New) providers.RegisterDomainServiceProviderType("GCLOUD", New, providers.CanUsePTR)
} }
type gcloud struct { type gcloud struct {

View File

@ -42,7 +42,7 @@ func newRoute53(m map[string]string, metadata json.RawMessage) (providers.DNSSer
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("ROUTE53", newRoute53) providers.RegisterDomainServiceProviderType("ROUTE53", newRoute53, providers.CanUsePTR)
} }
func sPtr(s string) *string { func sPtr(s string) *string {
return &s return &s