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 }