dnscontrol/pkg/prettyzone/sorting.go
Phil Pennock 3c41a39252
BIND: Implement AutoDNSSEC (#648)
There's a philosophy issue here around what is the Bind output meant to
do.  Since AFAIK we're not integrating into Bind's catalog zones or the
like, we're just targeting the zonefiles, we're not in a position to do
_anything_ relating to registrar options such as setting up DS glue.

So at one level, enabling AutoDNSSEC for Bind is a lie. But without
this, folks can't target a Bind zone as a secondary provider for their
domain, to get debug dumps of the zone output, because the checks for
"Can" block it.  So I think this commit achieves a happy compromise: we
write a comment into the Bind zonefile, indicating that DNSSEC was
requested.

Actually: we add support for arbitrary zone comments to be written into
a zonefile via a slightly ugly "can be `nil`" parameter.  We then write
in a generation timestamp comment, and if AutoDNSSEC was requested we
then write that in too.
2020-02-22 13:27:24 -05:00

195 lines
4.4 KiB
Go

package prettyzone
// Generate zonefiles.
// This generates a zonefile that prioritizes beauty over efficiency.
import (
"bytes"
"log"
"strconv"
"strings"
"github.com/StackExchange/dnscontrol/v2/models"
)
type zoneGenData struct {
Origin string
DefaultTTL uint32
Records models.Records
Comments []string
}
func (z *zoneGenData) Len() int { return len(z.Records) }
func (z *zoneGenData) Swap(i, j int) { z.Records[i], z.Records[j] = z.Records[j], z.Records[i] }
func (z *zoneGenData) Less(i, j int) bool {
a, b := z.Records[i], z.Records[j]
// Sort by name.
compA, compB := a.NameFQDN, b.NameFQDN
if compA != compB {
if a.Name == "@" {
compA = "@"
}
if b.Name == "@" {
compB = "@"
}
return zoneLabelLess(compA, compB)
}
// sub-sort by type
if a.Type != b.Type {
return zoneRrtypeLess(a.Type, b.Type)
}
// sub-sort within type:
switch a.Type { // #rtype_variations
case "A":
ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
ipa, ipb := ta2.To4(), tb2.To4()
if ipa == nil || ipb == nil {
log.Fatalf("should not happen: IPs are not 4 bytes: %#v %#v", ta2, tb2)
}
return bytes.Compare(ipa, ipb) == -1
case "AAAA":
ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
ipa, ipb := ta2.To16(), tb2.To16()
if ipa == nil || ipb == nil {
log.Fatalf("should not happen: IPs are not 16 bytes: %#v %#v", ta2, tb2)
}
return bytes.Compare(ipa, ipb) == -1
case "MX":
// sort by priority. If they are equal, sort by Mx.
if a.MxPreference == b.MxPreference {
return a.GetTargetField() < b.GetTargetField()
}
return a.MxPreference < b.MxPreference
case "SRV":
//ta2, tb2 := a.(*dns.SRV), b.(*dns.SRV)
pa, pb := a.SrvPort, b.SrvPort
if pa != pb {
return pa < pb
}
pa, pb = a.SrvPriority, b.SrvPriority
if pa != pb {
return pa < pb
}
pa, pb = a.SrvWeight, b.SrvWeight
if pa != pb {
return pa < pb
}
case "PTR":
//ta2, tb2 := a.(*dns.PTR), b.(*dns.PTR)
pa, pb := a.GetTargetField(), b.GetTargetField()
if pa != pb {
return pa < pb
}
case "CAA":
//ta2, tb2 := a.(*dns.CAA), b.(*dns.CAA)
// sort by tag
pa, pb := a.CaaTag, b.CaaTag
if pa != pb {
return pa < pb
}
// then flag
fa, fb := a.CaaFlag, b.CaaFlag
if fa != fb {
// flag set goes before ones without flag set
return fa > fb
}
default:
// pass through. String comparison is sufficient.
}
return a.String() < b.String()
}
func zoneLabelLess(a, b string) bool {
// Compare two zone labels for the purpose of sorting the RRs in a Zone.
// If they are equal, we are done. All other code is simplified
// because we can assume a!=b.
if a == b {
return false
}
// Sort @ at the top, then *, then everything else lexigraphically.
// i.e. @ always is less. * is is less than everything but @.
if a == "@" {
return true
}
if b == "@" {
return false
}
if a == "*" {
return true
}
if b == "*" {
return false
}
// Split into elements and match up last elements to first. Compare the
// first non-equal elements.
as := strings.Split(a, ".")
bs := strings.Split(b, ".")
ia := len(as) - 1
ib := len(bs) - 1
var min int
if ia < ib {
min = len(as) - 1
} else {
min = len(bs) - 1
}
// Skip the matching highest elements, then compare the next item.
for i, j := ia, ib; min >= 0; i, j, min = i-1, j-1, min-1 {
// Compare as[i] < bs[j]
// Sort @ at the top, then *, then everything else.
// i.e. @ always is less. * is is less than everything but @.
// If both are numeric, compare as integers, otherwise as strings.
if as[i] != bs[j] {
// If the first element is *, it is always less.
if i == 0 && as[i] == "*" {
return true
}
if j == 0 && bs[j] == "*" {
return false
}
// If the elements are both numeric, compare as integers:
au, aerr := strconv.ParseUint(as[i], 10, 64)
bu, berr := strconv.ParseUint(bs[j], 10, 64)
if aerr == nil && berr == nil {
return au < bu
}
// otherwise, compare as strings:
return as[i] < bs[j]
}
}
// The min top elements were equal, so the shorter name is less.
return ia < ib
}
func zoneRrtypeLess(a, b string) bool {
// Compare two RR types for the purpose of sorting the RRs in a Zone.
if a == b {
return false
}
// List SOAs, NSs, etc. then all others alphabetically.
for _, t := range []string{"SOA", "NS", "CNAME",
"A", "AAAA", "MX", "SRV", "TXT",
} {
if a == t {
return true
}
if b == t {
return false
}
}
return a < b
}