github.com/StackExchange/DNSControl@v0.2.8/providers/bind/prettyzone.go (about)

     1  package bind
     2  
     3  // Generate zonefiles.
     4  // This generates a zonefile that prioritizes beauty over efficiency.
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"log"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/miekg/dns"
    16  	"github.com/miekg/dns/dnsutil"
    17  )
    18  
    19  type zoneGenData struct {
    20  	Origin     string
    21  	DefaultTTL uint32
    22  	Records    []dns.RR
    23  }
    24  
    25  func (z *zoneGenData) Len() int      { return len(z.Records) }
    26  func (z *zoneGenData) Swap(i, j int) { z.Records[i], z.Records[j] = z.Records[j], z.Records[i] }
    27  func (z *zoneGenData) Less(i, j int) bool {
    28  	a, b := z.Records[i], z.Records[j]
    29  	compA, compB := dnsutil.AddOrigin(a.Header().Name, z.Origin+"."), dnsutil.AddOrigin(b.Header().Name, z.Origin+".")
    30  	if compA != compB {
    31  		if compA == z.Origin+"." {
    32  			compA = "@"
    33  		}
    34  		if compB == z.Origin+"." {
    35  			compB = "@"
    36  		}
    37  		return zoneLabelLess(compA, compB)
    38  	}
    39  	rrtypeA, rrtypeB := a.Header().Rrtype, b.Header().Rrtype
    40  	if rrtypeA != rrtypeB {
    41  		return zoneRrtypeLess(rrtypeA, rrtypeB)
    42  	}
    43  	switch rrtypeA { // #rtype_variations
    44  	case dns.TypeA:
    45  		ta2, tb2 := a.(*dns.A), b.(*dns.A)
    46  		ipa, ipb := ta2.A.To4(), tb2.A.To4()
    47  		if ipa == nil || ipb == nil {
    48  			log.Fatalf("should not happen: IPs are not 4 bytes: %#v %#v", ta2, tb2)
    49  		}
    50  		return bytes.Compare(ipa, ipb) == -1
    51  	case dns.TypeAAAA:
    52  		ta2, tb2 := a.(*dns.AAAA), b.(*dns.AAAA)
    53  		ipa, ipb := ta2.AAAA.To16(), tb2.AAAA.To16()
    54  		return bytes.Compare(ipa, ipb) == -1
    55  	case dns.TypeMX:
    56  		ta2, tb2 := a.(*dns.MX), b.(*dns.MX)
    57  		pa, pb := ta2.Preference, tb2.Preference
    58  		// sort by priority. If they are equal, sort by Mx.
    59  		if pa != pb {
    60  			return pa < pb
    61  		}
    62  		return ta2.Mx < tb2.Mx
    63  	case dns.TypeSRV:
    64  		ta2, tb2 := a.(*dns.SRV), b.(*dns.SRV)
    65  		pa, pb := ta2.Port, tb2.Port
    66  		if pa != pb {
    67  			return pa < pb
    68  		}
    69  		pa, pb = ta2.Priority, tb2.Priority
    70  		if pa != pb {
    71  			return pa < pb
    72  		}
    73  		pa, pb = ta2.Weight, tb2.Weight
    74  		if pa != pb {
    75  			return pa < pb
    76  		}
    77  	case dns.TypePTR:
    78  		ta2, tb2 := a.(*dns.PTR), b.(*dns.PTR)
    79  		pa, pb := ta2.Ptr, tb2.Ptr
    80  		if pa != pb {
    81  			return pa < pb
    82  		}
    83  	case dns.TypeCAA:
    84  		ta2, tb2 := a.(*dns.CAA), b.(*dns.CAA)
    85  		// sort by tag
    86  		pa, pb := ta2.Tag, tb2.Tag
    87  		if pa != pb {
    88  			return pa < pb
    89  		}
    90  		// then flag
    91  		fa, fb := ta2.Flag, tb2.Flag
    92  		if fa != fb {
    93  			// flag set goes before ones without flag set
    94  			return fa > fb
    95  		}
    96  	default:
    97  		// pass through. String comparison is sufficient.
    98  	}
    99  	return a.String() < b.String()
   100  }
   101  
   102  // mostCommonTTL returns the most common TTL in a set of records. If there is
   103  // a tie, the highest TTL is selected. This makes the results consistent.
   104  // NS records are not included in the analysis because Tom said so.
   105  func mostCommonTTL(records []dns.RR) uint32 {
   106  	// Index the TTLs in use:
   107  	d := make(map[uint32]int)
   108  	for _, r := range records {
   109  		if r.Header().Rrtype != dns.TypeNS {
   110  			d[r.Header().Ttl]++
   111  		}
   112  	}
   113  	// Find the largest count:
   114  	var mc int
   115  	for _, value := range d {
   116  		if value > mc {
   117  			mc = value
   118  		}
   119  	}
   120  	// Find the largest key with that count:
   121  	var mk uint32
   122  	for key, value := range d {
   123  		if value == mc {
   124  			if key > mk {
   125  				mk = key
   126  			}
   127  		}
   128  	}
   129  	return mk
   130  }
   131  
   132  // WriteZoneFile writes a beautifully formatted zone file.
   133  func WriteZoneFile(w io.Writer, records []dns.RR, origin string) error {
   134  	// This function prioritizes beauty over efficiency.
   135  	// * The zone records are sorted by label, grouped by subzones to
   136  	//   be easy to read and pleasant to the eye.
   137  	// * Within a label, SOA and NS records are listed first.
   138  	// * MX records are sorted numericly by preference value.
   139  	// * SRV records are sorted numericly by port, then priority, then weight.
   140  	// * A records are sorted by IP address, not lexicographically.
   141  	// * Repeated labels are removed.
   142  	// * $TTL is used to eliminate clutter. The most common TTL value is used.
   143  	// * "@" is used instead of the apex domain name.
   144  
   145  	defaultTTL := mostCommonTTL(records)
   146  
   147  	z := &zoneGenData{
   148  		Origin:     dnsutil.AddOrigin(origin, "."),
   149  		DefaultTTL: defaultTTL,
   150  	}
   151  	z.Records = nil
   152  	for _, r := range records {
   153  		z.Records = append(z.Records, r)
   154  	}
   155  	return z.generateZoneFileHelper(w)
   156  }
   157  
   158  // generateZoneFileHelper creates a pretty zonefile.
   159  func (z *zoneGenData) generateZoneFileHelper(w io.Writer) error {
   160  
   161  	nameShortPrevious := ""
   162  
   163  	sort.Sort(z)
   164  	fmt.Fprintln(w, "$TTL", z.DefaultTTL)
   165  	for i, rr := range z.Records {
   166  		line := rr.String()
   167  		if line[0] == ';' {
   168  			continue
   169  		}
   170  		hdr := rr.Header()
   171  
   172  		items := strings.SplitN(line, "\t", 5)
   173  		if len(items) < 5 {
   174  			log.Fatalf("Too few items in: %v", line)
   175  		}
   176  
   177  		// items[0]: name
   178  		nameFqdn := hdr.Name
   179  		nameShort := dnsutil.TrimDomainName(nameFqdn, z.Origin)
   180  		name := nameShort
   181  		if i > 0 && nameShort == nameShortPrevious {
   182  			name = ""
   183  		} else {
   184  			name = nameShort
   185  		}
   186  		nameShortPrevious = nameShort
   187  
   188  		// items[1]: ttl
   189  		ttl := ""
   190  		if hdr.Ttl != z.DefaultTTL && hdr.Ttl != 0 {
   191  			ttl = items[1]
   192  		}
   193  
   194  		// items[2]: class
   195  		if hdr.Class != dns.ClassINET {
   196  			log.Fatalf("generateZoneFileHelper: Unimplemented class=%v", items[2])
   197  		}
   198  
   199  		// items[3]: type
   200  		typeStr := dns.TypeToString[hdr.Rrtype]
   201  
   202  		// items[4]: the remaining line
   203  		target := items[4]
   204  
   205  		fmt.Fprintln(w, formatLine([]int{10, 5, 2, 5, 0}, []string{name, ttl, "IN", typeStr, target}))
   206  	}
   207  	return nil
   208  }
   209  
   210  func formatLine(lengths []int, fields []string) string {
   211  	c := 0
   212  	result := ""
   213  	for i, length := range lengths {
   214  		item := fields[i]
   215  		for len(result) < c {
   216  			result += " "
   217  		}
   218  		if item != "" {
   219  			result += item + " "
   220  		}
   221  		c += length + 1
   222  	}
   223  	return strings.TrimRight(result, " ")
   224  }
   225  
   226  func isNumeric(s string) bool {
   227  	_, err := strconv.ParseFloat(s, 64)
   228  	return err == nil
   229  }
   230  
   231  func zoneLabelLess(a, b string) bool {
   232  	// Compare two zone labels for the purpose of sorting the RRs in a Zone.
   233  
   234  	// If they are equal, we are done. All other code is simplified
   235  	// because we can assume a!=b.
   236  	if a == b {
   237  		return false
   238  	}
   239  
   240  	// Sort @ at the top, then *, then everything else lexigraphically.
   241  	// i.e. @ always is less. * is is less than everything but @.
   242  	if a == "@" {
   243  		return true
   244  	}
   245  	if b == "@" {
   246  		return false
   247  	}
   248  	if a == "*" {
   249  		return true
   250  	}
   251  	if b == "*" {
   252  		return false
   253  	}
   254  
   255  	// Split into elements and match up last elements to first. Compare the
   256  	// first non-equal elements.
   257  
   258  	as := strings.Split(a, ".")
   259  	bs := strings.Split(b, ".")
   260  	ia := len(as) - 1
   261  	ib := len(bs) - 1
   262  
   263  	var min int
   264  	if ia < ib {
   265  		min = len(as) - 1
   266  	} else {
   267  		min = len(bs) - 1
   268  	}
   269  
   270  	// Skip the matching highest elements, then compare the next item.
   271  	for i, j := ia, ib; min >= 0; i, j, min = i-1, j-1, min-1 {
   272  		// Compare as[i] < bs[j]
   273  		// Sort @ at the top, then *, then everything else.
   274  		// i.e. @ always is less. * is is less than everything but @.
   275  		// If both are numeric, compare as integers, otherwise as strings.
   276  
   277  		if as[i] != bs[j] {
   278  
   279  			// If the first element is *, it is always less.
   280  			if i == 0 && as[i] == "*" {
   281  				return true
   282  			}
   283  			if j == 0 && bs[j] == "*" {
   284  				return false
   285  			}
   286  
   287  			// If the elements are both numeric, compare as integers:
   288  			au, aerr := strconv.ParseUint(as[i], 10, 64)
   289  			bu, berr := strconv.ParseUint(bs[j], 10, 64)
   290  			if aerr == nil && berr == nil {
   291  				return au < bu
   292  			}
   293  			// otherwise, compare as strings:
   294  			return as[i] < bs[j]
   295  		}
   296  	}
   297  	// The min top elements were equal, so the shorter name is less.
   298  	return ia < ib
   299  }
   300  
   301  func zoneRrtypeLess(a, b uint16) bool {
   302  	// Compare two RR types for the purpose of sorting the RRs in a Zone.
   303  
   304  	// If they are equal, we are done. All other code is simplified
   305  	// because we can assume a!=b.
   306  	if a == b {
   307  		return false
   308  	}
   309  
   310  	// List SOAs, then NSs, then all others.
   311  	// i.e. SOA is always less. NS is less than everything but SOA.
   312  	if a == dns.TypeSOA {
   313  		return true
   314  	}
   315  	if b == dns.TypeSOA {
   316  		return false
   317  	}
   318  	if a == dns.TypeNS {
   319  		return true
   320  	}
   321  	if b == dns.TypeNS {
   322  		return false
   323  	}
   324  	return a < b
   325  }