github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/bind/prettyzone.go (about)

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