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 }