123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561 |
- package zones
- import (
- "encoding/json"
- "fmt"
- "log"
- "net"
- "os"
- "runtime/debug"
- "sort"
- "strconv"
- "strings"
- "github.com/abh/geodns/targeting"
- "github.com/abh/geodns/typeutil"
- "github.com/abh/errorutil"
- "github.com/miekg/dns"
- )
- // ZoneList maps domain names to zone data
- type ZoneList map[string]*Zone
- func (zone *Zone) ReadZoneFile(fileName string) (zerr error) {
- defer func() {
- if r := recover(); r != nil {
- log.Printf("reading %s failed: %s", zone.Origin, r)
- debug.PrintStack()
- zerr = fmt.Errorf("reading %s failed: %s", zone.Origin, r)
- }
- }()
- fh, err := os.Open(fileName)
- if err != nil {
- log.Printf("Could not read '%s': %s", fileName, err)
- panic(err)
- }
- fileInfo, err := fh.Stat()
- if err != nil {
- log.Printf("Could not stat '%s': %s", fileName, err)
- } else {
- zone.Options.Serial = int(fileInfo.ModTime().Unix())
- }
- var objmap map[string]interface{}
- decoder := json.NewDecoder(fh)
- if err = decoder.Decode(&objmap); err != nil {
- extra := ""
- if serr, ok := err.(*json.SyntaxError); ok {
- if _, serr := fh.Seek(0, os.SEEK_SET); serr != nil {
- log.Fatalf("seek error: %v", serr)
- }
- line, col, highlight := errorutil.HighlightBytePosition(fh, serr.Offset)
- extra = fmt.Sprintf(":\nError at line %d, column %d (file offset %d):\n%s",
- line, col, serr.Offset, highlight)
- }
- return fmt.Errorf("error parsing JSON object in config file %s%s\n%v",
- fh.Name(), extra, err)
- }
- //log.Println(objmap)
- var data map[string]interface{}
- for k, v := range objmap {
- //log.Printf("k: %s v: %#v, T: %T\n", k, v, v)
- switch k {
- case "ttl":
- zone.Options.Ttl = typeutil.ToInt(v)
- case "serial":
- zone.Options.Serial = typeutil.ToInt(v)
- case "contact":
- zone.Options.Contact = v.(string)
- case "max_hosts":
- zone.Options.MaxHosts = typeutil.ToInt(v)
- case "closest":
- zone.Options.Closest = v.(bool)
- if zone.Options.Closest {
- zone.HasClosest = true
- }
- case "targeting":
- zone.Options.Targeting, err = targeting.ParseTargets(v.(string))
- if err != nil {
- return fmt.Errorf("parsing targeting '%s': %s", v, err)
- }
- case "logging":
- {
- logging := new(ZoneLogging)
- for logger, v := range v.(map[string]interface{}) {
- switch logger {
- case "stathat":
- logging.StatHat = typeutil.ToBool(v)
- case "stathat_api":
- logging.StatHatAPI = typeutil.ToString(v)
- logging.StatHat = true
- default:
- log.Println("Unknown logger option", logger)
- }
- }
- zone.Logging = logging
- // log.Printf("logging options: %#v", logging)
- }
- continue
- case "data":
- data = v.(map[string]interface{})
- }
- }
- setupZoneData(data, zone)
- //log.Printf("ZO T: %T %s\n", Zones["0.us"], Zones["0.us"])
- //log.Println("IP", string(Zone.Regions["0.us"].IPv4[0].ip))
- if zone.Options.Targeting == 0 && !zone.HasClosest {
- // no targeting requested
- return nil
- }
- if targeting.Geo() == nil {
- log.Printf("'%s': No geo provider configured", zone.Origin)
- return nil
- }
- switch {
- case zone.Options.Targeting >= targeting.TargetRegionGroup || zone.HasClosest:
- if ok, err := targeting.Geo().HasLocation(); !ok {
- log.Printf("Zone '%s' requested location/city targeting but geo provider isn't available: %s", zone.Origin, err)
- }
- case zone.Options.Targeting >= targeting.TargetContinent:
- if ok, err := targeting.Geo().HasCountry(); !ok {
- log.Printf("Zone '%s' requested country targeting but geo provider isn't available: %s", zone.Origin, err)
- }
- }
- if zone.Options.Targeting&targeting.TargetASN > 0 {
- if ok, err := targeting.Geo().HasASN(); !ok {
- log.Printf("Zone '%s' requested ASN targeting but geo provider isn't available: %s", zone.Origin, err)
- }
- }
- if zone.HasClosest {
- zone.SetLocations()
- }
- return nil
- }
- func setupZoneData(data map[string]interface{}, zone *Zone) {
- recordTypes := map[string]uint16{
- "a": dns.TypeA,
- "aaaa": dns.TypeAAAA,
- "alias": dns.TypeMF,
- "cname": dns.TypeCNAME,
- "mx": dns.TypeMX,
- "ns": dns.TypeNS,
- "txt": dns.TypeTXT,
- "spf": dns.TypeSPF,
- "srv": dns.TypeSRV,
- "ptr": dns.TypePTR,
- }
- for dk, dv_inter := range data {
- dv := dv_inter.(map[string]interface{})
- //log.Printf("K %s V %s TYPE-V %T\n", dk, dv, dv)
- label := zone.AddLabel(dk)
- for rType, rdata := range dv {
- switch rType {
- case "max_hosts":
- label.MaxHosts = typeutil.ToInt(rdata)
- continue
- case "closest":
- label.Closest = rdata.(bool)
- if label.Closest {
- zone.HasClosest = true
- }
- continue
- case "ttl":
- label.Ttl = typeutil.ToInt(rdata)
- continue
- case "health":
- zone.addHealthReference(label, rdata)
- continue
- }
- dnsType, ok := recordTypes[rType]
- if !ok {
- log.Printf("'%s' unsupported record type '%s'\n", zone.Origin, rType)
- continue
- }
- if rdata == nil {
- //log.Printf("No %s records for label %s\n", rType, dk)
- continue
- }
- //log.Printf("rdata %s TYPE-R %T\n", rdata, rdata)
- records := make(map[string][]interface{})
- switch rdata.(type) {
- case map[string]interface{}:
- // Handle NS map syntax, map[ns2.example.net:<nil> ns1.example.net:<nil>]
- tmp := make([]interface{}, 0)
- for rdataK, rdataV := range rdata.(map[string]interface{}) {
- if rdataV == nil {
- rdataV = ""
- }
- tmp = append(tmp, []string{rdataK, rdataV.(string)})
- }
- records[rType] = tmp
- case string:
- // CNAME and alias
- tmp := make([]interface{}, 1)
- tmp[0] = rdata.(string)
- records[rType] = tmp
- default:
- records[rType] = rdata.([]interface{})
- }
- //log.Printf("RECORDS %s TYPE-REC %T\n", Records, Records)
- label.Records[dnsType] = make(Records, len(records[rType]))
- for i := 0; i < len(records[rType]); i++ {
- //log.Printf("RT %T %#v\n", records[rType][i], records[rType][i])
- record := new(Record)
- var h dns.RR_Header
- h.Class = dns.ClassINET
- h.Rrtype = dnsType
- {
- // allow for individual health test name overrides
- if rec, ok := records[rType][i].(map[string]interface{}); ok {
- if h, ok := rec["health"].(string); ok {
- record.Test = h
- }
- }
- }
- switch len(label.Label) {
- case 0:
- h.Name = zone.Origin + "."
- default:
- h.Name = label.Label + "." + zone.Origin + "."
- }
- switch dnsType {
- case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
- rec := records[rType][i]
- var ip string
- switch rec.(type) {
- case []interface{}:
- str, weight := getStringWeight(records[rType][i].([]interface{}))
- ip = str
- record.Weight = weight
- case map[string]interface{}:
- r := rec.(map[string]interface{})
- if _, ok := r["ip"]; ok {
- ip = r["ip"].(string)
- }
- if len(ip) == 0 || dnsType == dns.TypePTR {
- switch dnsType {
- case dns.TypeA:
- ip = r["a"].(string)
- case dns.TypeAAAA:
- ip = r["aaaa"].(string)
- case dns.TypePTR:
- ip = r["ptr"].(string)
- }
- }
- if w, ok := r["weight"]; ok {
- record.Weight = typeutil.ToInt(w)
- }
- if h, ok := r["health"]; ok {
- record.Test = typeutil.ToString(h)
- }
- }
- switch dnsType {
- case dns.TypePTR:
- record.RR = &dns.PTR{Hdr: h, Ptr: ip}
- break
- case dns.TypeA:
- if x := net.ParseIP(ip); x != nil {
- record.RR = &dns.A{Hdr: h, A: x}
- break
- }
- panic(fmt.Errorf("Bad A record %q for %q", ip, dk))
- case dns.TypeAAAA:
- if x := net.ParseIP(ip); x != nil {
- record.RR = &dns.AAAA{Hdr: h, AAAA: x}
- break
- }
- panic(fmt.Errorf("Bad AAAA record %q for %q", ip, dk))
- }
- case dns.TypeMX:
- rec := records[rType][i].(map[string]interface{})
- pref := uint16(0)
- mx := rec["mx"].(string)
- if !strings.HasSuffix(mx, ".") {
- mx = mx + "."
- }
- if rec["weight"] != nil {
- record.Weight = typeutil.ToInt(rec["weight"])
- }
- if rec["preference"] != nil {
- pref = uint16(typeutil.ToInt(rec["preference"]))
- }
- record.RR = &dns.MX{
- Hdr: h,
- Mx: mx,
- Preference: pref}
- case dns.TypeSRV:
- rec := records[rType][i].(map[string]interface{})
- priority := uint16(0)
- srv_weight := uint16(0)
- port := uint16(0)
- target := rec["target"].(string)
- if !dns.IsFqdn(target) {
- target = target + "." + zone.Origin
- }
- if rec["srv_weight"] != nil {
- srv_weight = uint16(typeutil.ToInt(rec["srv_weight"]))
- }
- if rec["port"] != nil {
- port = uint16(typeutil.ToInt(rec["port"]))
- }
- if rec["priority"] != nil {
- priority = uint16(typeutil.ToInt(rec["priority"]))
- }
- record.RR = &dns.SRV{
- Hdr: h,
- Priority: priority,
- Weight: srv_weight,
- Port: port,
- Target: target}
- case dns.TypeCNAME:
- rec := records[rType][i]
- var target string
- var weight int
- switch rec.(type) {
- case string:
- target = rec.(string)
- case []interface{}:
- target, weight = getStringWeight(rec.([]interface{}))
- case map[string]interface{}:
- r := rec.(map[string]interface{})
- if t, ok := r["cname"]; ok {
- target = typeutil.ToString(t)
- }
- if w, ok := r["weight"]; ok {
- weight = typeutil.ToInt(w)
- }
- if h, ok := r["health"]; ok {
- record.Test = typeutil.ToString(h)
- }
- }
- if !dns.IsFqdn(target) {
- target = target + "." + zone.Origin
- }
- record.Weight = weight
- record.RR = &dns.CNAME{Hdr: h, Target: dns.Fqdn(target)}
- case dns.TypeMF:
- rec := records[rType][i]
- // MF records (how we store aliases) are not FQDNs
- record.RR = &dns.MF{Hdr: h, Mf: rec.(string)}
- case dns.TypeNS:
- rec := records[rType][i]
- var ns string
- switch rec.(type) {
- case string:
- ns = rec.(string)
- case []string:
- recl := rec.([]string)
- ns = recl[0]
- if len(recl[1]) > 0 {
- log.Println("NS records with names syntax not supported")
- }
- default:
- log.Printf("Data: %T %#v\n", rec, rec)
- panic("Unrecognized NS format/syntax")
- }
- rr := &dns.NS{Hdr: h, Ns: dns.Fqdn(ns)}
- record.RR = rr
- case dns.TypeTXT:
- rec := records[rType][i]
- var txt string
- switch rec.(type) {
- case string:
- txt = rec.(string)
- case map[string]interface{}:
- recmap := rec.(map[string]interface{})
- if weight, ok := recmap["weight"]; ok {
- record.Weight = typeutil.ToInt(weight)
- }
- if t, ok := recmap["txt"]; ok {
- txt = t.(string)
- }
- }
- if len(txt) > 0 {
- rr := &dns.TXT{Hdr: h, Txt: []string{txt}}
- record.RR = rr
- } else {
- log.Printf("Zero length txt record for '%s' in '%s'\n", label.Label, zone.Origin)
- continue
- }
- // Initial SPF support added here, cribbed from the TypeTXT case definition - SPF records should be handled identically
- case dns.TypeSPF:
- rec := records[rType][i]
- var spf string
- switch rec.(type) {
- case string:
- spf = rec.(string)
- case map[string]interface{}:
- recmap := rec.(map[string]interface{})
- if weight, ok := recmap["weight"]; ok {
- record.Weight = typeutil.ToInt(weight)
- }
- if t, ok := recmap["spf"]; ok {
- spf = t.(string)
- }
- }
- if len(spf) > 0 {
- rr := &dns.SPF{Hdr: h, Txt: []string{spf}}
- record.RR = rr
- } else {
- log.Printf("Zero length SPF record for '%s' in '%s'\n", label.Label, zone.Origin)
- continue
- }
- default:
- log.Println("type:", rType)
- panic("Don't know how to handle this type")
- }
- if record.RR == nil {
- panic("record.RR is nil")
- }
- label.Weight[dnsType] += record.Weight
- label.Records[dnsType][i] = record
- }
- if label.Weight[dnsType] > 0 {
- sort.Sort(RecordsByWeight{label.Records[dnsType]})
- }
- }
- }
- // Loop over exisiting labels, create zone records for missing sub-domains
- // and set TTLs
- for k, l := range zone.Labels {
- if strings.Contains(k, ".") {
- subLabels := strings.Split(k, ".")
- for i := 1; i < len(subLabels); i++ {
- subSubLabel := strings.Join(subLabels[i:], ".")
- if _, ok := zone.Labels[subSubLabel]; !ok {
- zone.AddLabel(subSubLabel)
- }
- }
- }
- for qtype, records := range l.Records {
- setWeight := false
- if _, ok := alwaysWeighted[qtype]; ok && l.Weight[qtype] == 0 {
- setWeight = true
- }
- for _, r := range records {
- // We add the TTL as a last pass because we might not have
- // processed it yet when we process the record data.
- if setWeight {
- r.Weight = 1
- l.Weight[qtype] += r.Weight
- }
- var defaultTtl uint32 = 86400
- if r.RR.Header().Rrtype != dns.TypeNS {
- // NS records have special treatment. If they are not specified, they default to 86400 rather than
- // defaulting to the zone ttl option. The label TTL option always works though
- defaultTtl = uint32(zone.Options.Ttl)
- }
- if zone.Labels[k].Ttl > 0 {
- defaultTtl = uint32(zone.Labels[k].Ttl)
- }
- if r.RR.Header().Ttl == 0 {
- r.RR.Header().Ttl = defaultTtl
- }
- }
- }
- }
- zone.addSOA()
- }
- func getStringWeight(rec []interface{}) (string, int) {
- str := rec[0].(string)
- var weight int
- if len(rec) > 1 {
- switch rec[1].(type) {
- case string:
- var err error
- weight, err = strconv.Atoi(rec[1].(string))
- if err != nil {
- panic("Error converting weight to integer")
- }
- case float64:
- weight = int(rec[1].(float64))
- }
- }
- return str, weight
- }
|