NEW FEATURE: Add RFC4183 support to REV() (#2879)

Co-authored-by: Thomas Misilo <tmisilo@ksu.edu>
Co-authored-by: Jeffrey Cafferata <jeffrey@jcid.nl>
This commit is contained in:
Tom Limoncelli 2024-04-03 16:01:55 -04:00 committed by GitHub
parent f9cff3d5e6
commit 1d96981e11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 601 additions and 145 deletions

View file

@ -159,8 +159,9 @@ release:
## Deprecation warnings
> [!WARNING]
> - **REV() will switch from RFC2317 to RFC4183 in v5.0.** This is a breaking change. Warnings are output if your configuration is affected. No date has been announced for v5.0. See https://docs.dnscontrol.org/language-reference/top-level-functions/revcompat
> - **MSDNS maintainer needed!** Without a new volunteer, this DNS provider will lose support after April 2025. See https://github.com/StackExchange/dnscontrol/issues/2878
> - **Call for new volunteer maintainers for NAMEDOTCOM and SOFTLAYER.** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes.
> - **NAMEDOTCOM and SOFTLAYER need maintainers!** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes.
> - **get-certs/ACME support is frozen and will be removed without notice between now and July 2025.** It has been unsupported since December 2022. If you don't use this feature, do not start. If you do use this feature, migrate ASAP. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400)
## Install

View file

@ -160,8 +160,9 @@ See [dnscontrol-action](https://github.com/koenrh/dnscontrol-action) or [gacts/i
## Deprecation warnings (updated 2024-03-25)
- **REV() will switch from RFC2317 to RFC4183 in v5.0.** This is a breaking change. Warnings are output if your configuration is affected. No date has been announced for v5.0. See https://docs.dnscontrol.org/language-reference/top-level-functions/revcompat
- **MSDNS maintainer needed!** Without a new volunteer, this DNS provider will lose support after April 2025. See https://github.com/StackExchange/dnscontrol/issues/2878
- **Call for new volunteer maintainers for NAMEDOTCOM and SOFTLAYER.** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes.
- **NAMEDOTCOM and SOFTLAYER need maintainers!** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes.
- **get-certs/ACME support is frozen and will be removed without notice between now and July 2025.** It has been unsupported since December 2022. If you don't use this feature, do not start. If you do use this feature, migrate ASAP. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400)
## More info at our website

View file

@ -15,6 +15,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
"github.com/StackExchange/dnscontrol/v4/pkg/notifications"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
"github.com/StackExchange/dnscontrol/v4/pkg/zonerecs"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/urfave/cli/v2"
@ -257,6 +258,7 @@ func prun(args PPreviewArgs, push bool, interactive bool, out printer.CLI, repor
if os.Getenv("TEAMCITY_VERSION") != "" {
fmt.Fprintf(os.Stderr, "##teamcity[buildStatus status='SUCCESS' text='%d corrections']", totalCorrections)
}
rfc4183.PrintWarning()
notifier.Done()
out.Printf("Done. %d corrections.\n", totalCorrections)
err = writeReport(report, reportItems)

View file

@ -16,6 +16,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
"github.com/StackExchange/dnscontrol/v4/pkg/notifications"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
"github.com/StackExchange/dnscontrol/v4/pkg/zonerecs"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/urfave/cli/v2"
@ -293,6 +294,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report
if os.Getenv("TEAMCITY_VERSION") != "" {
fmt.Fprintf(os.Stderr, "##teamcity[buildStatus status='SUCCESS' text='%d corrections']", totalCorrections)
}
rfc4183.PrintWarning()
notifier.Done()
out.Printf("Done. %d corrections.\n", totalCorrections)
if anyErrors {

View file

@ -10,6 +10,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/js"
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
"github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
"github.com/urfave/cli/v2"
)
@ -58,6 +59,7 @@ var _ = cmd(catDebug, func() *cli.Command {
log.SetOutput(os.Stdout)
err := exit(PrintIR(pargs))
rfc4183.PrintWarning()
if err == nil {
fmt.Fprintf(os.Stdout, "No errors.\n")
}

View file

@ -2214,7 +2214,7 @@ declare function PANIC(message: string): never;
*
* Target should be a string representing the FQDN of a host. Like all FQDNs in DNSControl, it must end with a `.`.
*
* **Magic Mode:**
* # Magic Mode
*
* PTR records are complex and typos are common. Therefore DNSControl
* enables features to save labor and
@ -2282,9 +2282,33 @@ declare function PANIC(message: string): never;
* );
* ```
*
* In the future we plan on adding a flag to [`A()`](A.md) which will insert
* the correct PTR() record if the appropriate `.arpa` domain has been
* defined.
* # Automatic forward and reverse lookups
*
* DNSControl does not automatically generate forward and reverse lookups. However
* it is possible to write a macro that does this by using the
* [`D_EXTEND()`](../global/D_EXTEND.md)
* function to insert `A` and `PTR` records into previously-defined domains.
*
* ```javascript
* function FORWARD_AND_REVERSE(ipaddr, fqdn) {
* D_EXTEND(dom,
* A(fqdn, ipaddr)
* );
* D_EXTEND(REV(ipaddr),
* PTR(ipaddr, fqdn)
* );
* }
*
* D("example.com", REGISTRAR, DnsProvider(DSP_NONE),
* ...,
* END);
* D(REV("10.20.30.0/24"), REGISTRAR, DnsProvider(DSP_NONE),
* ...,
* END);
*
* FORWARD_AND_REVERSE("10.20.30.77", "foo.example.com.");
* FORWARD_AND_REVERSE("10.20.30.99", "bar.example.com.");
* ```
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/ptr
*/
@ -2395,53 +2419,115 @@ declare function R53_ZONE(zone_id: string): DomainModifier & RecordModifier;
* `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()`](D.md) 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 but why would you risk making
* typos?
* `REV()` is commonly used with the [`D()`](D.md) functions to create reverse DNS lookup zones.
*
* `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`
* These two are equivalent:
*
* If the address does not include a "/" then `REV` assumes /32 for IPv4 addresses
* ```javascript
* D("3.2.1.in-addr.arpa", ...
* ```
*
* ```javascript
* D(REV("1.2.3.0/24", ...
* ```
*
* The latter is easier to type and less error-prone.
*
* 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.
* # RFC compliance
*
* `REV()` implements both RFC 2317 and the newer RFC 4183. The `REVCOMPAT()`
* function selects which mode is used. If `REVCOMPAT()` is not called, a default
* is selected for you. The default will change to RFC 4183 in DNSControl v5.0.
*
* See [`REVCOMPAT()`](REVCOMPAT.md) for details.
*
* # Host bits
*
* v4.x:
* The host 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.
*
* v5.0 and later:
* The host bits (the ones outside the netmask) are ignored. Thus
* `REV("1.2.3.4/24")` and `REV("1.2.3.0/24")` are equivalent.
*
* # Examples
*
* Here's an example reverse lookup domain:
*
* ```javascript
* D(REV("1.2.3.0/24"), REGISTRAR, DnsProvider(BIND),
* PTR("1", "foo.example.com."),
* PTR("2", "bar.example.com."),
* PTR("3", "baz.example.com."),
* // These take advantage of DNSControl's ability to generate the right name:
* // If the first parameter is an IP address, DNSControl automatically calls REV() for you.
* PTR("1.2.3.10", "ten.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
* // These take advantage of DNSControl's ability to generate the right name:
* // If the first parameter is an IP address, DNSControl automatically calls REV() for you.
* PTR("2001:db8:302::2", "two.example.com."), // 2.0.0...
* PTR("2001:db8:302::3", "three.example.com."), // 3.0.0...
* );
* ```
*
* In the future we plan on adding a flag to [`A()`](../domain/A.md)which will insert
* the correct PTR() record in the appropriate `D(REV())` domain (i.e. `.arpa` domain) has been
* defined.
* # Automatic forward and reverse record generation
*
* DNSControl does not automatically generate forward and reverse lookups. However
* it is possible to write a macro that does this. See
* [`PTR()`](../domain/PTR.md) for an example.
*
* @see https://docs.dnscontrol.org/language-reference/top-level-functions/rev
*/
declare function REV(address: string): string;
/**
* `REVCOMPAT()` controls which RFC the [`REV()`](REV.md) function adheres to.
*
* Include one of these two commands near the top `dnsconfig.js` (at the global level):
*
* ```javascript
* REVCOMPAT("rfc2317"); // RFC 2117: Compatible with old files.
* REVCOMPAT("rfc4183"); // RFC 4183: Adopt the newer standard.
* ```
*
* `REVCOMPAT()` is global for all of `dnsconfig.js`. It must appear before any
* use of `REV()`; If not, behavior is undefined.
*
* # RFC 4183 vs RFC 2317
*
* RFC 2317 and RFC 4183 are two different ways to implement reverse lookups for
* CIDR blocks that are not on 8-bit boundaries (/24, /16, /8).
*
* Originally DNSControl implemented the older standard, which only specifies what
* to do for /8, /16, /24 - /32. Using `REV()` for /9-17 and /17-23 CIDRs was an
* error.
*
* v4 defaults to RFC 2317. In v5.0 the default will change to RFC 4183.
* `REVCOMPAT()` is provided for those that wish to retain the old behavior.
*
* For more information, see [Opinion #9](../../opinions.md#opinion-9-rfc-4183-is-better-than-rfc-2317).
*
* # Transition plan
*
* What's the default behavior if `REVCOMPAT()` is not used?
*
* | Version | /9 to /15 and /17 to /23 | /25 to 32 | Warnings |
* |---------|--------------------------|-----------|----------------------------|
* | v4 | RFC 4183 | RFC 2317 | Only if /25 - /32 are used |
* | v5 | RFC 4183 | RFC 4183 | none |
*
* No warnings are generated if the `REVCOMPAT()` function is used.
*
* @see https://docs.dnscontrol.org/language-reference/top-level-functions/revcompat
*/
declare function REVCOMPAT(rfc: string): string;
/**
* `SOA` adds an `SOA` record to a domain. The name should be `@`. ns and mbox are strings. The other fields are unsigned 32-bit ints.
*

View file

@ -24,6 +24,7 @@
* [NewRegistrar](functions/global/NewRegistrar.md)
* [PANIC](functions/global/PANIC.md)
* [REV](functions/global/REV.md)
* [REVCOMPAT](functions/global/REVCOMPAT.md)
* [getConfiguredDomains](functions/global/getConfiguredDomains.md)
* [require](functions/global/require.md)
* [require_glob](functions/global/require_glob.md)

View file

@ -17,7 +17,7 @@ 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:**
# Magic Mode
PTR records are complex and typos are common. Therefore DNSControl
enables features to save labor and
@ -91,6 +91,32 @@ D(REV("2001:db8:302::/48"), REGISTRAR, DnsProvider(BIND),
```
{% endcode %}
In the future we plan on adding a flag to [`A()`](A.md) which will insert
the correct PTR() record if the appropriate `.arpa` domain has been
defined.
# Automatic forward and reverse lookups
DNSControl does not automatically generate forward and reverse lookups. However
it is possible to write a macro that does this by using the
[`D_EXTEND()`](../global/D_EXTEND.md)
function to insert `A` and `PTR` records into previously-defined domains.
{% code title="dnsconfig.js" %}
```javascript
function FORWARD_AND_REVERSE(ipaddr, fqdn) {
D_EXTEND(dom,
A(fqdn, ipaddr)
);
D_EXTEND(REV(ipaddr),
PTR(ipaddr, fqdn)
);
}
D("example.com", REGISTRAR, DnsProvider(DSP_NONE),
...,
END);
D(REV("10.20.30.0/24"), REGISTRAR, DnsProvider(DSP_NONE),
...,
END);
FORWARD_AND_REVERSE("10.20.30.77", "foo.example.com.");
FORWARD_AND_REVERSE("10.20.30.99", "bar.example.com.");
```
{% endcode %}

View file

@ -10,27 +10,50 @@ ts_return: string
`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()`](D.md) 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 but why would you risk making
typos?
`REV()` is commonly used with the [`D()`](D.md) functions to create reverse DNS lookup zones.
`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`
These two are equivalent:
If the address does not include a "/" then `REV` assumes /32 for IPv4 addresses
{% code title="dnsconfig.js" %}
```javascript
D("3.2.1.in-addr.arpa", ...
```
{% endcode %}
{% code title="dnsconfig.js" %}
```javascript
D(REV("1.2.3.0/24", ...
```
{% endcode %}
The latter is easier to type and less error-prone.
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.
# RFC compliance
`REV()` implements both RFC 2317 and the newer RFC 4183. The `REVCOMPAT()`
function selects which mode is used. If `REVCOMPAT()` is not called, a default
is selected for you. The default will change to RFC 4183 in DNSControl v5.0.
See [`REVCOMPAT()`](REVCOMPAT.md) for details.
# Host bits
v4.x:
The host 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.
v5.0 and later:
The host bits (the ones outside the netmask) are ignored. Thus
`REV("1.2.3.4/24")` and `REV("1.2.3.0/24")` are equivalent.
# Examples
Here's an example reverse lookup domain:
{% code title="dnsconfig.js" %}
```javascript
@ -38,19 +61,21 @@ D(REV("1.2.3.0/24"), REGISTRAR, DnsProvider(BIND),
PTR("1", "foo.example.com."),
PTR("2", "bar.example.com."),
PTR("3", "baz.example.com."),
// These take advantage of DNSControl's ability to generate the right name:
// If the first parameter is an IP address, DNSControl automatically calls REV() for you.
PTR("1.2.3.10", "ten.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
// These take advantage of DNSControl's ability to generate the right name:
// If the first parameter is an IP address, DNSControl automatically calls REV() for you.
PTR("2001:db8:302::2", "two.example.com."), // 2.0.0...
PTR("2001:db8:302::3", "three.example.com."), // 3.0.0...
);
```
{% endcode %}
In the future we plan on adding a flag to [`A()`](../domain/A.md)which will insert
the correct PTR() record in the appropriate `D(REV())` domain (i.e. `.arpa` domain) has been
defined.
# Automatic forward and reverse record generation
DNSControl does not automatically generate forward and reverse lookups. However
it is possible to write a macro that does this. See
[`PTR()`](../domain/PTR.md) for an example.

View file

@ -0,0 +1,47 @@
---
name: REVCOMPAT
parameters:
- rfc
parameter_types:
rfc: string
ts_return: string
---
`REVCOMPAT()` controls which RFC the [`REV()`](REV.md) function adheres to.
Include one of these two commands near the top `dnsconfig.js` (at the global level):
{% code title="dnsconfig.js" %}
```javascript
REVCOMPAT("rfc2317"); // RFC 2117: Compatible with old files.
REVCOMPAT("rfc4183"); // RFC 4183: Adopt the newer standard.
```
{% endcode %}
`REVCOMPAT()` is global for all of `dnsconfig.js`. It must appear before any
use of `REV()`; If not, behavior is undefined.
# RFC 4183 vs RFC 2317
RFC 2317 and RFC 4183 are two different ways to implement reverse lookups for
CIDR blocks that are not on 8-bit boundaries (/24, /16, /8).
Originally DNSControl implemented the older standard, which only specifies what
to do for /8, /16, /24 - /32. Using `REV()` for /9-17 and /17-23 CIDRs was an
error.
v4 defaults to RFC 2317. In v5.0 the default will change to RFC 4183.
`REVCOMPAT()` is provided for those that wish to retain the old behavior.
For more information, see [Opinion #9](../../opinions.md#opinion-9-rfc-4183-is-better-than-rfc-2317).
# Transition plan
What's the default behavior if `REVCOMPAT()` is not used?
| Version | /9 to /15 and /17 to /23 | /25 to 32 | Warnings |
|---------|--------------------------|-----------|----------------------------|
| v4 | RFC 4183 | RFC 2317 | Only if /25 - /32 are used |
| v5 | RFC 4183 | RFC 4183 | none |
No warnings are generated if the `REVCOMPAT()` function is used.

View file

@ -90,7 +90,7 @@ Some examples:
* SPF records are stated in the most verbose way; DNSControl optimizes it for you in a safe, opt-in way.
# Opinion #6 If it is ambiguous in DNS, it is forbidden in DNSControl
# Opinion #6: If it is ambiguous in DNS, it is forbidden in DNSControl
When there is ambiguity an expert knows what the system will do.
Your coworkers should not be expected to be experts. (See [Opinion #2](#opinion-2-non-experts-should-be-able-to-safely-make-dns-changes)).
@ -124,7 +124,7 @@ Therefore, we require all CNAME, MX, and NS targets to be FQDNs (they must
end with a "."), or to be a shortname (no dots at all). Everything
else is ambiguous and therefore an error.
# Opinion #7 Hostnames don't have underscores
# Opinion #7: Hostnames don't have underscores
DNSControl prints warnings if a hostname includes an underscore
(`_`) because underscores are not permitted in hostnames.
@ -151,7 +151,7 @@ unless the rtype is SRV, TLSA, TXT, or if the name starts with
certain prefixes such as `_dmarc`. We're always willing to
[add more exceptions](https://github.com/StackExchange/dnscontrol/pull/453/files).
# Opinion #8 TXT Records are one long string
# Opinion #8: TXT Records are one long string
* TXT records are a single string with a length of 0 to 65,280 bytes
(the maximum possible TXT record size).
@ -180,3 +180,25 @@ control panel let you specify the boundaries, (b) I've never seen a
FAQ or reddit post asking how to specify those boundaries. Therefore,
there is no need for this. I also assert that there will be no such
need in the future.
# Opinion #9: RFC 4183 is better than RFC 2317
There is no standard for how to do reverse lookup zones (in-addr.arpa)
for CIDR blocks that are not /8, /16, or /24. There are only
recommendations.
RFC 2317 is a good recommendation, but it only covers /25 to /32.
It also uses `/` in zone names, which many DNS providers do not
support.
RFC 4183 covers /8 through /32 and uses hyphens, which are supported
universally.
Originally DNSControl implemented RFC 2317.
In v5.0 we will adopt RFC 4183 as the default. A new function,
[REVCOMPAT()](functions/global/REVCOMPAT.md), will be provided to enable backwards compatibility.
v4.x users can use the function to adopt the new behavior early.
See [REVCOMPAT()](functions/global/REVCOMPAT.md) for details.

View file

@ -11,6 +11,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
"github.com/StackExchange/dnscontrol/v4/pkg/transform"
"github.com/robertkrimen/otto" // load underscore js into vm by default
_ "github.com/robertkrimen/otto/underscore" // required by otto
@ -70,6 +71,7 @@ func ExecuteJavascriptString(script []byte, devMode bool, variables map[string]s
vm.Set("require", require)
vm.Set("REV", reverse)
vm.Set("REVCOMPAT", reverseCompat)
vm.Set("glob", listFiles) // used for require_glob()
vm.Set("PANIC", jsPanic)
@ -290,3 +292,16 @@ func reverse(call otto.FunctionCall) otto.Value {
v, _ := otto.ToValue(rev)
return v
}
func reverseCompat(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) != 1 {
throw(call.Otto, "REVCOMPAT takes exactly one argument")
}
dom := call.Argument(0).String()
err := rfc4183.SetCompatibilityMode(dom)
if err != nil {
throw(call.Otto, err.Error())
}
v, _ := otto.ToValue(nil)
return v
}

26
pkg/rfc4183/ipv6.go Normal file
View file

@ -0,0 +1,26 @@
package rfc4183
import (
"fmt"
)
// reverseIPv6 returns the ipv6.arpa string suitable for reverse DNS lookups.
func reverseIPv6(ip []byte, maskbits int) (arpa string, err error) {
// Must be IPv6
if len(ip) != 16 {
return "", fmt.Errorf("not IPv6")
}
buf := []byte("x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.ip6.arpa")
// Poke hex digits into the template
pos := 128/4*2 - 2 // Position of the last "x"
for _, v := range ip {
buf[pos] = hexDigit[v>>4]
buf[pos-2] = hexDigit[v&0xF]
pos = pos - 4
}
// Return only the parts without x's
return string(buf[(128-maskbits)/4*2:]), nil
}
const hexDigit = "0123456789abcdef"

47
pkg/rfc4183/mode.go Normal file
View file

@ -0,0 +1,47 @@
package rfc4183
import (
"fmt"
"strings"
)
var newmode bool
var modeset bool
func SetCompatibilityMode(m string) error {
if modeset {
return fmt.Errorf("ERROR: REVCOMPAT() already set")
}
modeset = true
switch strings.ToLower(m) {
case "rfc2317", "2317", "2", "old":
newmode = false
case "rfc4183", "4183", "4":
newmode = true
default:
return fmt.Errorf("invalid value %q, must be rfc2317 or rfc4182", m)
}
return nil
}
func IsRFC4183Mode() bool {
return newmode
}
var warningNeeded bool = false
func NeedsWarning() {
warningNeeded = true
}
func PrintWarning() {
if modeset {
// No warnings if REVCOMPAT() was used.
return
}
if !warningNeeded {
return
}
fmt.Printf("WARNING: REV() breaking change coming in v5.0. See https://docs.dnscontrol.org/functions/REVCOMPAT\n")
}

79
pkg/rfc4183/reverse.go Normal file
View file

@ -0,0 +1,79 @@
package rfc4183
import (
"fmt"
"net/netip"
"strings"
)
// ReverseDomainName implements RFC4183 for turning a CIDR block into
// a in-addr name. IP addresses are assumed to be /32 or /128 CIDR blocks.
// CIDR host bits are changed to 0s.
func ReverseDomainName(cidr string) (string, error) {
// Mask missing? Add it.
if !strings.Contains(cidr, "/") {
a, err := netip.ParseAddr(cidr)
if err != nil {
return "", fmt.Errorf("not an IP address: %w", err)
}
if a.Is4() {
cidr = cidr + "/32"
} else {
cidr = cidr + "/128"
}
}
// Parse the CIDR.
p, err := netip.ParsePrefix(cidr)
if err != nil {
return "", fmt.Errorf("not a CIDR block: %w", err)
}
// RFC4183 4.1 step 4: The notion of fewer than 8 mask bits is not reasonable.
if p.Bits() < 8 {
return "", fmt.Errorf("mask fewer than 8 bits is unreasonable: %s", cidr)
}
// Handle IPv6 separately:
if p.Addr().Is6() {
return reverseIPv6(p.Addr().AsSlice(), p.Bits())
}
// Zero out any host bits.
p = p.Masked()
// IPv4: Implement the RFC4183 process:
// 4.p Step 1
b := p.Addr().AsSlice()
x, y, z, w := b[0], b[1], b[2], b[3]
m := p.Bits()
if m == 8 {
return fmt.Sprintf("%d.in-addr.arpa", x), nil
}
if m == 16 {
return fmt.Sprintf("%d.%d.in-addr.arpa", y, x), nil
}
if m == 24 {
return fmt.Sprintf("%d.%d.%d.in-addr.arpa", z, y, x), nil
}
if m == 32 {
return fmt.Sprintf("%d.%d.%d.%d.in-addr.arpa", w, z, y, x), nil
}
// 4.1 Step 2
n := w // I don't understand why the RFC changes variable names at this point, but it does.
if m >= 24 && m <= 32 {
return fmt.Sprintf("%d-%d.%d.%d.%d.in-addr.arpa", n, m, z, y, x), nil
}
if m >= 16 && m < 24 {
return fmt.Sprintf("%d-%d.%d.%d.in-addr.arpa", z, m, y, x), nil
}
if m >= 8 && m < 16 {
return fmt.Sprintf("%d-%d.%d.in-addr.arpa", y, m, x), nil
}
return "", fmt.Errorf("fewer than 8 mask bits is not reasonable: %v", cidr)
}

125
pkg/rfc4183/reverse_test.go Normal file
View file

@ -0,0 +1,125 @@
package rfc4183
import (
"fmt"
"testing"
)
func TestReverse(t *testing.T) {
var tests = []struct {
in string
out string
}{
// IPv4 "Classless in-addr.arpa delegation" RFC4183.
// Examples in the RFC:
{"10.100.2.0/26", "0-26.2.100.10.in-addr.arpa"},
{"10.192.0.0/13", "192-13.10.in-addr.arpa"},
{"10.20.128.0/23", "128-23.20.10.in-addr.arpa"},
{"10.20.129.0/23", "128-23.20.10.in-addr.arpa"}, // Not in the RFC but should be!
// IPv6
{"2001::/16", "1.0.0.2.ip6.arpa"},
{"2001:0db8:0123:4567:89ab:cdef:1234:5670/64", "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/68", "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", "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", "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"},
// 8-bit boundaries
{"174.0.0.0/8", "174.in-addr.arpa"},
{"174.136.43.0/8", "174.in-addr.arpa"},
{"174.136.0.44/8", "174.in-addr.arpa"},
{"174.136.45.45/8", "174.in-addr.arpa"},
{"174.136.0.0/16", "136.174.in-addr.arpa"},
{"174.136.43.0/16", "136.174.in-addr.arpa"},
{"174.136.44.255/16", "136.174.in-addr.arpa"},
{"174.136.107.0/24", "107.136.174.in-addr.arpa"},
{"174.136.107.1/24", "107.136.174.in-addr.arpa"},
{"174.136.107.14/32", "14.107.136.174.in-addr.arpa"},
// /25 (all cases)
{"174.1.0.0/25", "0-25.0.1.174.in-addr.arpa"},
{"174.1.0.128/25", "128-25.0.1.174.in-addr.arpa"},
{"174.1.0.129/25", "128-25.0.1.174.in-addr.arpa"}, // host bits
// /26 (all cases)
{"174.1.0.0/26", "0-26.0.1.174.in-addr.arpa"},
{"174.1.0.0/26", "0-26.0.1.174.in-addr.arpa"},
{"174.1.0.64/26", "64-26.0.1.174.in-addr.arpa"},
{"174.1.0.128/26", "128-26.0.1.174.in-addr.arpa"},
{"174.1.0.192/26", "192-26.0.1.174.in-addr.arpa"},
{"174.1.0.194/26", "192-26.0.1.174.in-addr.arpa"}, // host bits
// /27 (all cases)
{"174.1.0.0/27", "0-27.0.1.174.in-addr.arpa"},
{"174.1.0.32/27", "32-27.0.1.174.in-addr.arpa"},
{"174.1.0.64/27", "64-27.0.1.174.in-addr.arpa"},
{"174.1.0.96/27", "96-27.0.1.174.in-addr.arpa"},
{"174.1.0.128/27", "128-27.0.1.174.in-addr.arpa"},
{"174.1.0.160/27", "160-27.0.1.174.in-addr.arpa"},
{"174.1.0.192/27", "192-27.0.1.174.in-addr.arpa"},
{"174.1.0.224/27", "224-27.0.1.174.in-addr.arpa"},
{"174.1.0.225/27", "224-27.0.1.174.in-addr.arpa"}, // host bits
// /28 (first 2, last 2)
{"174.1.0.0/28", "0-28.0.1.174.in-addr.arpa"},
{"174.1.0.16/28", "16-28.0.1.174.in-addr.arpa"},
{"174.1.0.224/28", "224-28.0.1.174.in-addr.arpa"},
{"174.1.0.240/28", "240-28.0.1.174.in-addr.arpa"},
{"174.1.0.241/28", "240-28.0.1.174.in-addr.arpa"}, // host bits
// /29 (first 2 cases)
{"174.1.0.0/29", "0-29.0.1.174.in-addr.arpa"},
{"174.1.0.8/29", "8-29.0.1.174.in-addr.arpa"},
{"174.1.0.9/29", "8-29.0.1.174.in-addr.arpa"}, // host bits
// /30 (first 2 cases)
{"174.1.0.0/30", "0-30.0.1.174.in-addr.arpa"},
{"174.1.0.4/30", "4-30.0.1.174.in-addr.arpa"},
{"174.1.0.5/30", "4-30.0.1.174.in-addr.arpa"}, // host bits
// /31 (first 2 cases)
{"174.1.0.0/31", "0-31.0.1.174.in-addr.arpa"},
{"174.1.0.2/31", "2-31.0.1.174.in-addr.arpa"},
{"174.1.0.3/31", "2-31.0.1.174.in-addr.arpa"}, // host bits
// Other tests:
{"10.100.2.255/23", "2-23.100.10.in-addr.arpa"},
{"10.100.2.255/22", "0-22.100.10.in-addr.arpa"},
{"10.100.2.255/21", "0-21.100.10.in-addr.arpa"},
{"10.100.2.255/20", "0-20.100.10.in-addr.arpa"},
{"10.100.2.255/19", "0-19.100.10.in-addr.arpa"},
{"10.100.2.255/18", "0-18.100.10.in-addr.arpa"},
{"10.100.2.255/17", "0-17.100.10.in-addr.arpa"},
//
{"10.100.2.255/15", "100-15.10.in-addr.arpa"},
{"10.100.2.255/14", "100-14.10.in-addr.arpa"},
{"10.100.2.255/13", "96-13.10.in-addr.arpa"},
{"10.100.2.255/12", "96-12.10.in-addr.arpa"},
{"10.100.2.255/11", "96-11.10.in-addr.arpa"},
{"10.100.2.255/10", "64-10.10.in-addr.arpa"},
{"10.100.2.255/9", "0-9.10.in-addr.arpa"},
}
for i, tst := range tests {
t.Run(fmt.Sprintf("%d--%s", i, tst.in), func(t *testing.T) {
d, err := ReverseDomainName(tst.in)
if err != nil {
t.Errorf("Should not have errored: %v", err)
} else if d != tst.out {
t.Errorf("Expected '%s' but got '%s'", tst.out, d)
}
})
}
}
func TestReverseErrors(t *testing.T) {
var tests = []struct {
in string
}{
{"0.0.0.0/0"},
{"2001::/0"},
{"4.5/16"},
{"foo.com"},
}
for i, tst := range tests {
t.Run(fmt.Sprintf("%d--%s", i, tst.in), func(t *testing.T) {
d, err := ReverseDomainName(tst.in)
if err == nil {
t.Errorf("Should have errored, but didn't. Got %s", d)
}
})
}
}

View file

@ -2,115 +2,60 @@ package transform
import (
"fmt"
"net"
"net/netip"
"strings"
"github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
)
// ReverseDomainName turns a CIDR block into a reversed (in-addr) name.
// For cases not covered by RFC2317, implement RFC4183
// The host bits must all be zeros.
func ReverseDomainName(cidr string) (string, error) {
// If it is an IP address, add the /32 or /128
ip := net.ParseIP(cidr)
if ip != nil {
if ip.To4() != nil {
if rfc4183.IsRFC4183Mode() {
return rfc4183.ReverseDomainName(cidr)
}
// Mask missing? Add it.
if !strings.Contains(cidr, "/") {
a, err := netip.ParseAddr(cidr)
if err != nil {
return "", fmt.Errorf("not an IP address: %w", err)
}
if a.Is4() {
cidr = cidr + "/32"
} else {
cidr = cidr + "/128"
}
}
a, c, err := net.ParseCIDR(cidr)
// Parse the CIDR.
p, err := netip.ParsePrefix(cidr)
if err != nil {
return "", err
return "", fmt.Errorf("not a CIDR block: %w", err)
}
base, err := reverseaddr(a.String())
if err != nil {
return "", err
}
base = strings.TrimRight(base, ".")
if !a.Equal(c.IP) {
bits := p.Bits()
if p.Masked() != p {
return "", fmt.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")
// Cases where RFC4183 is the same as RFC2317:
// IPV6, /0 - /24, /32
if strings.Contains(cidr, ":") || bits <= 24 || bits == 32 {
// There is no p.Is6() so we test for ":" as a workaround.
return rfc4183.ReverseDomainName(cidr)
}
// Record that the change to --revmode default will affect this configuration
rfc4183.NeedsWarning()
// Handle IPv4 "Classless in-addr.arpa delegation" RFC2317:
if total == 32 && bits >= 25 && bits < 32 {
// if 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")
ip := p.Addr().AsSlice()
return fmt.Sprintf("%d/%d.%d.%d.%d.in-addr.arpa",
ip[3], bits, ip[2], ip[1], ip[0]), nil
}
toTrim = (total - bits) / 8
} else if total == 128 {
if bits%4 != 0 {
return "", fmt.Errorf("IPv6 mask must be multiple of 4 bits")
}
toTrim = (total - bits) / 4
} else {
return "", fmt.Errorf("invalid address (not IPv4 or IPv6): %v", cidr)
}
parts := strings.SplitN(base, ".", toTrim+1)
return parts[len(parts)-1], nil
}
// copied from go source.
// https://github.com/golang/go/blob/bfc164c64d33edfaf774b5c29b9bf5648a6447fb/src/net/dnsclient.go#L15
// reverseaddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP
// address addr suitable for rDNS (PTR) record lookup or an error if it fails
// to parse the IP address.
func reverseaddr(addr string) (arpa string, err error) {
ip := net.ParseIP(addr)
if ip == nil {
return "", &net.DNSError{Err: "unrecognized address", Name: addr}
}
if ip.To4() != nil {
return uitoa(uint(ip[15])) + "." + uitoa(uint(ip[14])) + "." + uitoa(uint(ip[13])) + "." + uitoa(uint(ip[12])) + ".in-addr.arpa.", nil
}
// Must be IPv6
buf := make([]byte, 0, len(ip)*4+len("ip6.arpa."))
// Add it, in reverse, to the buffer
for i := len(ip) - 1; i >= 0; i-- {
v := ip[i]
buf = append(buf, hexDigit[v&0xF])
buf = append(buf, '.')
buf = append(buf, hexDigit[v>>4])
buf = append(buf, '.')
}
// Append "ip6.arpa." and return (buf already has the final .)
buf = append(buf, "ip6.arpa."...)
return string(buf), nil
}
// Convert unsigned integer to decimal string.
func uitoa(val uint) 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:])
}
const hexDigit = "0123456789abcdef"

View file

@ -73,6 +73,10 @@ func TestReverse(t *testing.T) {
{"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"},
// Use RFC4183 for cases not covered by RFC2317:
{"10.20.128.0/23", false, "128-23.20.10.in-addr.arpa"},
{"10.192.0.0/13", false, "192-13.10.in-addr.arpa"},
// Error Cases:
{"0.0.0.0/0", true, ""},
{"2001::/0", true, ""},