github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/models/record.go (about)

     1  package models
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/miekg/dns"
    10  	"github.com/miekg/dns/dnsutil"
    11  )
    12  
    13  // RecordConfig stores a DNS record.
    14  // Valid types:
    15  //   Official:
    16  //     A
    17  //     AAAA
    18  //     ANAME  // Technically not an official rtype yet.
    19  //     CAA
    20  //     CNAME
    21  //     MX
    22  //     NAPTR
    23  //     NS
    24  //     PTR
    25  //     SRV
    26  //     SSHFP
    27  //     TLSA
    28  //     TXT
    29  //   Pseudo-Types:
    30  //     ALIAS
    31  //     AUTODNSSEC
    32  //     CF_REDIRECT
    33  //     CF_TEMP_REDIRECT
    34  //     FRAME
    35  //     IMPORT_TRANSFORM
    36  //     NAMESERVER
    37  //     NO_PURGE
    38  //     PAGE_RULE
    39  //     PURGE
    40  //     URL
    41  //     URL301
    42  //
    43  // Notes about the fields:
    44  //
    45  // Name:
    46  //    This is the shortname i.e. the NameFQDN without the origin suffix.
    47  //    It should never have a trailing "."
    48  //    It should never be null. The apex (naked domain) is stored as "@".
    49  //    If the origin is "foo.com." and Name is "foo.com", this literally means
    50  //        the intended FQDN is "foo.com.foo.com." (which may look odd)
    51  // NameFQDN:
    52  //    This is the FQDN version of Name.
    53  //    It should never have a trailiing ".".
    54  //    NOTE: Eventually we will unexport Name/NameFQDN. Please start using
    55  //      the setters (SetLabel/SetLabelFromFQDN) and getters (GetLabel/GetLabelFQDN).
    56  //      as they will always work.
    57  // Target:
    58  //   This is the host or IP address of the record, with
    59  //     the other related paramters (weight, priority, etc.) stored in individual
    60  //     fields.
    61  //   NOTE: Eventually we will unexport Target. Please start using the
    62  //     setters (SetTarget*) and getters (GetTarget*) as they will always work.
    63  //
    64  // Idioms:
    65  //  rec.Label() == "@"   // Is this record at the apex?
    66  //
    67  type RecordConfig struct {
    68  	Type             string            `json:"type"`   // All caps rtype name.
    69  	Name             string            `json:"name"`   // The short name. See above.
    70  	NameFQDN         string            `json:"-"`      // Must end with ".$origin". See above.
    71  	Target           string            `json:"target"` // If a name, must end with "."
    72  	TTL              uint32            `json:"ttl,omitempty"`
    73  	Metadata         map[string]string `json:"meta,omitempty"`
    74  	MxPreference     uint16            `json:"mxpreference,omitempty"`
    75  	SrvPriority      uint16            `json:"srvpriority,omitempty"`
    76  	SrvWeight        uint16            `json:"srvweight,omitempty"`
    77  	SrvPort          uint16            `json:"srvport,omitempty"`
    78  	CaaTag           string            `json:"caatag,omitempty"`
    79  	CaaFlag          uint8             `json:"caaflag,omitempty"`
    80  	NaptrOrder       uint16            `json:"naptrorder,omitempty"`
    81  	NaptrPreference  uint16            `json:"naptrpreference,omitempty"`
    82  	NaptrFlags       string            `json:"naptrflags,omitempty"`
    83  	NaptrService     string            `json:"naptrservice,omitempty"`
    84  	NaptrRegexp      string            `json:"naptrregexp,omitempty"`
    85  	SshfpAlgorithm   uint8             `json:"sshfpalgorithm,omitempty"`
    86  	SshfpFingerprint uint8             `json:"sshfpfingerprint,omitempty"`
    87  	SoaMbox          string            `json:"soambox,omitempty"`
    88  	SoaSerial        uint32            `json:"soaserial,omitempty"`
    89  	SoaRefresh       uint32            `json:"soarefresh,omitempty"`
    90  	SoaRetry         uint32            `json:"soaretry,omitempty"`
    91  	SoaExpire        uint32            `json:"soaexpire,omitempty"`
    92  	SoaMinttl        uint32            `json:"soaminttl,omitempty"`
    93  	TlsaUsage        uint8             `json:"tlsausage,omitempty"`
    94  	TlsaSelector     uint8             `json:"tlsaselector,omitempty"`
    95  	TlsaMatchingType uint8             `json:"tlsamatchingtype,omitempty"`
    96  	TxtStrings       []string          `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one.
    97  	R53Alias         map[string]string `json:"r53_alias,omitempty"`
    98  
    99  	Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
   100  }
   101  
   102  // Copy returns a deep copy of a RecordConfig.
   103  func (rc *RecordConfig) Copy() (*RecordConfig, error) {
   104  	newR := &RecordConfig{}
   105  	err := copyObj(rc, newR)
   106  	return newR, err
   107  }
   108  
   109  // SetLabel sets the .Name/.NameFQDN fields given a short name and origin.
   110  // origin must not have a trailing dot: The entire code base
   111  //   maintains dc.Name without the trailig dot. Finding a dot here means
   112  //   something is very wrong.
   113  // short must not have a training dot: That would mean you have
   114  //   a FQDN, and shouldn't be using SetLabel().  Maybe SetLabelFromFQDN()?
   115  func (rc *RecordConfig) SetLabel(short, origin string) {
   116  
   117  	// Assertions that make sure the function is being used correctly:
   118  	if strings.HasSuffix(origin, ".") {
   119  		panic(fmt.Errorf("origin (%s) is not supposed to end with a dot", origin))
   120  	}
   121  	if strings.HasSuffix(short, ".") {
   122  		panic(fmt.Errorf("short (%s) is not supposed to end with a dot", origin))
   123  	}
   124  
   125  	// TODO(tlim): We should add more validation here or in a separate validation
   126  	// module.  We might want to check things like (\w+\.)+
   127  
   128  	short = strings.ToLower(short)
   129  	origin = strings.ToLower(origin)
   130  	if short == "" || short == "@" {
   131  		rc.Name = "@"
   132  		rc.NameFQDN = origin
   133  	} else {
   134  		rc.Name = short
   135  		rc.NameFQDN = dnsutil.AddOrigin(short, origin)
   136  	}
   137  }
   138  
   139  // UnsafeSetLabelNull sets the label to "". Normally the FQDN is denoted by .Name being
   140  // "@" however this can be used to violate that assertion. It should only be used
   141  // on copies of a RecordConfig that is being used for non-standard things like
   142  // Marshalling yaml.
   143  func (rc *RecordConfig) UnsafeSetLabelNull() {
   144  	rc.Name = ""
   145  }
   146  
   147  // SetLabelFromFQDN sets the .Name/.NameFQDN fields given a FQDN and origin.
   148  // fqdn may have a trailing "." but it is not required.
   149  // origin may not have a trailing dot.
   150  func (rc *RecordConfig) SetLabelFromFQDN(fqdn, origin string) {
   151  
   152  	// Assertions that make sure the function is being used correctly:
   153  	if strings.HasSuffix(origin, ".") {
   154  		panic(fmt.Errorf("origin (%s) is not supposed to end with a dot", origin))
   155  	}
   156  	if strings.HasSuffix(fqdn, "..") {
   157  		panic(fmt.Errorf("fqdn (%s) is not supposed to end with double dots", origin))
   158  	}
   159  
   160  	if strings.HasSuffix(fqdn, ".") {
   161  		// Trim off a trailing dot.
   162  		fqdn = fqdn[:len(fqdn)-1]
   163  	}
   164  
   165  	fqdn = strings.ToLower(fqdn)
   166  	origin = strings.ToLower(origin)
   167  	rc.Name = dnsutil.TrimDomainName(fqdn, origin)
   168  	rc.NameFQDN = fqdn
   169  }
   170  
   171  // GetLabel returns the shortname of the label associated with this RecordConfig.
   172  // It will never end with "."
   173  // It does not need further shortening (i.e. if it returns "foo.com" and the
   174  //   domain is "foo.com" then the FQDN is actually "foo.com.foo.com").
   175  // It will never be "" (the apex is returned as "@").
   176  func (rc *RecordConfig) GetLabel() string {
   177  	return rc.Name
   178  }
   179  
   180  // GetLabelFQDN returns the FQDN of the label associated with this RecordConfig.
   181  // It will not end with ".".
   182  func (rc *RecordConfig) GetLabelFQDN() string {
   183  	return rc.NameFQDN
   184  }
   185  
   186  // ToDiffable returns a string that is comparable by a differ.
   187  // extraMaps: a list of maps that should be included in the comparison.
   188  func (rc *RecordConfig) ToDiffable(extraMaps ...map[string]string) string {
   189  	content := fmt.Sprintf("%v ttl=%d", rc.GetTargetCombined(), rc.TTL)
   190  	if rc.Type == "SOA" {
   191  		content = fmt.Sprintf("%s %v %d %d %d %d ttl=%d", rc.Target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl, rc.TTL)
   192  		// SoaSerial is not used in comparison
   193  	}
   194  	for _, valueMap := range extraMaps {
   195  		// sort the extra values map keys to perform a deterministic
   196  		// comparison since Golang maps iteration order is not guaranteed
   197  
   198  		// FIXME(tlim) The keys of each map is sorted per-map, not across
   199  		// all maps. This may be intentional since we'd have no way to
   200  		// deal with duplicates.
   201  
   202  		keys := make([]string, 0)
   203  		for k := range valueMap {
   204  			keys = append(keys, k)
   205  		}
   206  		sort.Strings(keys)
   207  		for _, k := range keys {
   208  			v := valueMap[k]
   209  			content += fmt.Sprintf(" %s=%s", k, v)
   210  		}
   211  	}
   212  	return content
   213  }
   214  
   215  // ToRR converts a RecordConfig to a dns.RR.
   216  func (rc *RecordConfig) ToRR() dns.RR {
   217  
   218  	// Don't call this on fake types.
   219  	rdtype, ok := dns.StringToType[rc.Type]
   220  	if !ok {
   221  		log.Fatalf("No such DNS type as (%#v)\n", rc.Type)
   222  	}
   223  
   224  	// Magicallly create an RR of the correct type.
   225  	rr := dns.TypeToRR[rdtype]()
   226  
   227  	// Fill in the header.
   228  	rr.Header().Name = rc.NameFQDN + "."
   229  	rr.Header().Rrtype = rdtype
   230  	rr.Header().Class = dns.ClassINET
   231  	rr.Header().Ttl = rc.TTL
   232  	if rc.TTL == 0 {
   233  		rr.Header().Ttl = DefaultTTL
   234  	}
   235  
   236  	// Fill in the data.
   237  	switch rdtype { // #rtype_variations
   238  	case dns.TypeA:
   239  		rr.(*dns.A).A = rc.GetTargetIP()
   240  	case dns.TypeAAAA:
   241  		rr.(*dns.AAAA).AAAA = rc.GetTargetIP()
   242  	case dns.TypeCNAME:
   243  		rr.(*dns.CNAME).Target = rc.GetTargetField()
   244  	case dns.TypePTR:
   245  		rr.(*dns.PTR).Ptr = rc.GetTargetField()
   246  	case dns.TypeNAPTR:
   247  		rr.(*dns.NAPTR).Order = rc.NaptrOrder
   248  		rr.(*dns.NAPTR).Preference = rc.NaptrPreference
   249  		rr.(*dns.NAPTR).Flags = rc.NaptrFlags
   250  		rr.(*dns.NAPTR).Service = rc.NaptrService
   251  		rr.(*dns.NAPTR).Regexp = rc.NaptrRegexp
   252  		rr.(*dns.NAPTR).Replacement = rc.GetTargetField()
   253  	case dns.TypeMX:
   254  		rr.(*dns.MX).Preference = rc.MxPreference
   255  		rr.(*dns.MX).Mx = rc.GetTargetField()
   256  	case dns.TypeNS:
   257  		rr.(*dns.NS).Ns = rc.GetTargetField()
   258  	case dns.TypeSOA:
   259  		rr.(*dns.SOA).Ns = rc.GetTargetField()
   260  		rr.(*dns.SOA).Mbox = rc.SoaMbox
   261  		rr.(*dns.SOA).Serial = rc.SoaSerial
   262  		rr.(*dns.SOA).Refresh = rc.SoaRefresh
   263  		rr.(*dns.SOA).Retry = rc.SoaRetry
   264  		rr.(*dns.SOA).Expire = rc.SoaExpire
   265  		rr.(*dns.SOA).Minttl = rc.SoaMinttl
   266  	case dns.TypeSRV:
   267  		rr.(*dns.SRV).Priority = rc.SrvPriority
   268  		rr.(*dns.SRV).Weight = rc.SrvWeight
   269  		rr.(*dns.SRV).Port = rc.SrvPort
   270  		rr.(*dns.SRV).Target = rc.GetTargetField()
   271  	case dns.TypeSSHFP:
   272  		rr.(*dns.SSHFP).Algorithm = rc.SshfpAlgorithm
   273  		rr.(*dns.SSHFP).Type = rc.SshfpFingerprint
   274  		rr.(*dns.SSHFP).FingerPrint = rc.GetTargetField()
   275  	case dns.TypeCAA:
   276  		rr.(*dns.CAA).Flag = rc.CaaFlag
   277  		rr.(*dns.CAA).Tag = rc.CaaTag
   278  		rr.(*dns.CAA).Value = rc.GetTargetField()
   279  	case dns.TypeTLSA:
   280  		rr.(*dns.TLSA).Usage = rc.TlsaUsage
   281  		rr.(*dns.TLSA).MatchingType = rc.TlsaMatchingType
   282  		rr.(*dns.TLSA).Selector = rc.TlsaSelector
   283  		rr.(*dns.TLSA).Certificate = rc.GetTargetField()
   284  	case dns.TypeTXT:
   285  		rr.(*dns.TXT).Txt = rc.TxtStrings
   286  	default:
   287  		panic(fmt.Sprintf("ToRR: Unimplemented rtype %v", rc.Type))
   288  		// We panic so that we quickly find any switch statements
   289  		// that have not been updated for a new RR type.
   290  	}
   291  
   292  	return rr
   293  }
   294  
   295  // RecordKey represents a resource record in a format used by some systems.
   296  type RecordKey struct {
   297  	NameFQDN string
   298  	Type     string
   299  }
   300  
   301  // Key converts a RecordConfig into a RecordKey.
   302  func (rc *RecordConfig) Key() RecordKey {
   303  	t := rc.Type
   304  	if rc.R53Alias != nil {
   305  		if v, ok := rc.R53Alias["type"]; ok {
   306  			// Route53 aliases append their alias type, so that records for the same
   307  			// label with different alias types are considered separate.
   308  			t = fmt.Sprintf("%s_%s", t, v)
   309  		}
   310  	}
   311  	return RecordKey{rc.NameFQDN, t}
   312  }
   313  
   314  // Records is a list of *RecordConfig.
   315  type Records []*RecordConfig
   316  
   317  // HasRecordTypeName returns True if there is a record with this rtype and name.
   318  func (recs Records) HasRecordTypeName(rtype, name string) bool {
   319  	for _, r := range recs {
   320  		if r.Type == rtype && r.Name == name {
   321  			return true
   322  		}
   323  	}
   324  	return false
   325  }
   326  
   327  // FQDNMap returns a map of all LabelFQDNs. Useful for making a
   328  // truthtable of labels that exist in Records.
   329  func (recs Records) FQDNMap() (m map[string]bool) {
   330  	m = map[string]bool{}
   331  	for _, rec := range recs {
   332  		m[rec.GetLabelFQDN()] = true
   333  	}
   334  	return m
   335  }
   336  
   337  // GroupedByKey returns a map of keys to records.
   338  func (recs Records) GroupedByKey() map[RecordKey]Records {
   339  	groups := map[RecordKey]Records{}
   340  	for _, rec := range recs {
   341  		groups[rec.Key()] = append(groups[rec.Key()], rec)
   342  	}
   343  	return groups
   344  }
   345  
   346  // GroupedByLabel returns a map of keys to records, and their original key order.
   347  func (recs Records) GroupedByLabel() ([]string, map[string]Records) {
   348  	order := []string{}
   349  	groups := map[string]Records{}
   350  	for _, rec := range recs {
   351  		if _, found := groups[rec.Name]; !found {
   352  			order = append(order, rec.Name)
   353  		}
   354  		groups[rec.Name] = append(groups[rec.Name], rec)
   355  	}
   356  	return order, groups
   357  }
   358  
   359  // GroupedByFQDN returns a map of keys to records, grouped by FQDN.
   360  func (recs Records) GroupedByFQDN() ([]string, map[string]Records) {
   361  	order := []string{}
   362  	groups := map[string]Records{}
   363  	for _, rec := range recs {
   364  		namefqdn := rec.GetLabelFQDN()
   365  		if _, found := groups[namefqdn]; !found {
   366  			order = append(order, namefqdn)
   367  		}
   368  		groups[namefqdn] = append(groups[namefqdn], rec)
   369  	}
   370  	return order, groups
   371  }
   372  
   373  // PostProcessRecords does any post-processing of the downloaded DNS records.
   374  func PostProcessRecords(recs []*RecordConfig) {
   375  	downcase(recs)
   376  }
   377  
   378  // Downcase converts all labels and targets to lowercase in a list of RecordConfig.
   379  func downcase(recs []*RecordConfig) {
   380  	for _, r := range recs {
   381  		r.Name = strings.ToLower(r.Name)
   382  		r.NameFQDN = strings.ToLower(r.NameFQDN)
   383  		switch r.Type { // #rtype_variations
   384  		case "ANAME", "CNAME", "MX", "NS", "PTR", "NAPTR", "SRV":
   385  			// These record types have a target that is case insensitive, so we downcase it.
   386  			r.Target = strings.ToLower(r.Target)
   387  		case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SSHFP", "CF_REDIRECT", "CF_TEMP_REDIRECT":
   388  			// These record types have a target that is case sensitive, or is an IP address. We leave them alone.
   389  			// Do nothing.
   390  		case "SOA":
   391  			if r.Target != "DEFAULT_NOT_SET." {
   392  				r.Target = strings.ToLower(r.Target) // .Target stores the Ns
   393  			}
   394  			if r.SoaMbox != "DEFAULT_NOT_SET." {
   395  				r.SoaMbox = strings.ToLower(r.SoaMbox)
   396  			}
   397  		default:
   398  			// TODO: we'd like to panic here, but custom record types complicate things.
   399  		}
   400  	}
   401  	return
   402  }