github.com/teknogeek/dnscontrol@v0.2.8/models/record.go (about)

     1  package models
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"strings"
     7  
     8  	"github.com/miekg/dns"
     9  	"github.com/miekg/dns/dnsutil"
    10  	"github.com/pkg/errors"
    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  //     NS
    23  //     PTR
    24  //     SRV
    25  //     TLSA
    26  //     TXT
    27  //   Pseudo-Types:
    28  //     ALIAS
    29  //     CF_REDIRECT
    30  //     CF_TEMP_REDIRECT
    31  //     FRAME
    32  //     IMPORT_TRANSFORM
    33  //     NAMESERVER
    34  //     NO_PURGE
    35  //     PAGE_RULE
    36  //     PURGE
    37  //     URL
    38  //     URL301
    39  //
    40  // Notes about the fields:
    41  //
    42  // Name:
    43  //    This is the shortname i.e. the NameFQDN without the origin suffix.
    44  //    It should never have a trailing "."
    45  //    It should never be null. The apex (naked domain) is stored as "@".
    46  //    If the origin is "foo.com." and Name is "foo.com", this literally means
    47  //        the intended FQDN is "foo.com.foo.com." (which may look odd)
    48  // NameFQDN:
    49  //    This is the FQDN version of Name.
    50  //    It should never have a trailiing ".".
    51  //    NOTE: Eventually we will unexport Name/NameFQDN. Please start using
    52  //      the setters (SetLabel/SetLabelFromFQDN) and getters (GetLabel/GetLabelFQDN).
    53  //      as they will always work.
    54  // Target:
    55  //   This is the host or IP address of the record, with
    56  //     the other related paramters (weight, priority, etc.) stored in individual
    57  //     fields.
    58  //   NOTE: Eventually we will unexport Target. Please start using the
    59  //     setters (SetTarget*) and getters (GetTarget*) as they will always work.
    60  //
    61  // Idioms:
    62  //  rec.Label() == "@"   // Is this record at the apex?
    63  //
    64  type RecordConfig struct {
    65  	Type             string            `json:"type"`   // All caps rtype name.
    66  	Name             string            `json:"name"`   // The short name. See above.
    67  	NameFQDN         string            `json:"-"`      // Must end with ".$origin". See above.
    68  	Target           string            `json:"target"` // If a name, must end with "."
    69  	TTL              uint32            `json:"ttl,omitempty"`
    70  	Metadata         map[string]string `json:"meta,omitempty"`
    71  	MxPreference     uint16            `json:"mxpreference,omitempty"`
    72  	SrvPriority      uint16            `json:"srvpriority,omitempty"`
    73  	SrvWeight        uint16            `json:"srvweight,omitempty"`
    74  	SrvPort          uint16            `json:"srvport,omitempty"`
    75  	CaaTag           string            `json:"caatag,omitempty"`
    76  	CaaFlag          uint8             `json:"caaflag,omitempty"`
    77  	TlsaUsage        uint8             `json:"tlsausage,omitempty"`
    78  	TlsaSelector     uint8             `json:"tlsaselector,omitempty"`
    79  	TlsaMatchingType uint8             `json:"tlsamatchingtype,omitempty"`
    80  	TxtStrings       []string          `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one.
    81  	R53Alias         map[string]string `json:"r53_alias,omitempty"`
    82  
    83  	Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
    84  }
    85  
    86  // Copy returns a deep copy of a RecordConfig.
    87  func (rc *RecordConfig) Copy() (*RecordConfig, error) {
    88  	newR := &RecordConfig{}
    89  	err := copyObj(rc, newR)
    90  	return newR, err
    91  }
    92  
    93  // SetLabel sets the .Name/.NameFQDN fields given a short name and origin.
    94  // origin must not have a trailing dot: The entire code base
    95  //   maintains dc.Name without the trailig dot. Finding a dot here means
    96  //   something is very wrong.
    97  // short must not have a training dot: That would mean you have
    98  //   a FQDN, and shouldn't be using SetLabel().  Maybe SetLabelFromFQDN()?
    99  func (rc *RecordConfig) SetLabel(short, origin string) {
   100  
   101  	// Assertions that make sure the function is being used correctly:
   102  	if strings.HasSuffix(origin, ".") {
   103  		panic(errors.Errorf("origin (%s) is not supposed to end with a dot", origin))
   104  	}
   105  	if strings.HasSuffix(short, ".") {
   106  		panic(errors.Errorf("short (%s) is not supposed to end with a dot", origin))
   107  	}
   108  
   109  	// TODO(tlim): We should add more validation here or in a separate validation
   110  	// module.  We might want to check things like (\w+\.)+
   111  
   112  	short = strings.ToLower(short)
   113  	origin = strings.ToLower(origin)
   114  	if short == "" || short == "@" {
   115  		rc.Name = "@"
   116  		rc.NameFQDN = origin
   117  	} else {
   118  		rc.Name = short
   119  		rc.NameFQDN = dnsutil.AddOrigin(short, origin)
   120  	}
   121  }
   122  
   123  // UnsafeSetLabelNull sets the label to "". Normally the FQDN is denoted by .Name being
   124  // "@" however this can be used to violate that assertion. It should only be used
   125  // on copies of a RecordConfig that is being used for non-standard things like
   126  // Marshalling yaml.
   127  func (rc *RecordConfig) UnsafeSetLabelNull() {
   128  	rc.Name = ""
   129  }
   130  
   131  // SetLabelFromFQDN sets the .Name/.NameFQDN fields given a FQDN and origin.
   132  // fqdn may have a trailing "." but it is not required.
   133  // origin may not have a trailing dot.
   134  func (rc *RecordConfig) SetLabelFromFQDN(fqdn, origin string) {
   135  
   136  	// Assertions that make sure the function is being used correctly:
   137  	if strings.HasSuffix(origin, ".") {
   138  		panic(errors.Errorf("origin (%s) is not supposed to end with a dot", origin))
   139  	}
   140  	if strings.HasSuffix(fqdn, "..") {
   141  		panic(errors.Errorf("fqdn (%s) is not supposed to end with double dots", origin))
   142  	}
   143  
   144  	if strings.HasSuffix(fqdn, ".") {
   145  		// Trim off a trailing dot.
   146  		fqdn = fqdn[:len(fqdn)-1]
   147  	}
   148  
   149  	fqdn = strings.ToLower(fqdn)
   150  	origin = strings.ToLower(origin)
   151  	rc.Name = dnsutil.TrimDomainName(fqdn, origin)
   152  	rc.NameFQDN = fqdn
   153  }
   154  
   155  // GetLabel returns the shortname of the label associated with this RecordConfig.
   156  // It will never end with "."
   157  // It does not need further shortening (i.e. if it returns "foo.com" and the
   158  //   domain is "foo.com" then the FQDN is actually "foo.com.foo.com").
   159  // It will never be "" (the apex is returned as "@").
   160  func (rc *RecordConfig) GetLabel() string {
   161  	return rc.Name
   162  }
   163  
   164  // GetLabelFQDN returns the FQDN of the label associated with this RecordConfig.
   165  // It will not end with ".".
   166  func (rc *RecordConfig) GetLabelFQDN() string {
   167  	return rc.NameFQDN
   168  }
   169  
   170  // ToRR converts a RecordConfig to a dns.RR.
   171  func (rc *RecordConfig) ToRR() dns.RR {
   172  
   173  	// Don't call this on fake types.
   174  	rdtype, ok := dns.StringToType[rc.Type]
   175  	if !ok {
   176  		log.Fatalf("No such DNS type as (%#v)\n", rc.Type)
   177  	}
   178  
   179  	// Magicallly create an RR of the correct type.
   180  	rr := dns.TypeToRR[rdtype]()
   181  
   182  	// Fill in the header.
   183  	rr.Header().Name = rc.NameFQDN + "."
   184  	rr.Header().Rrtype = rdtype
   185  	rr.Header().Class = dns.ClassINET
   186  	rr.Header().Ttl = rc.TTL
   187  	if rc.TTL == 0 {
   188  		rr.Header().Ttl = DefaultTTL
   189  	}
   190  
   191  	// Fill in the data.
   192  	switch rdtype { // #rtype_variations
   193  	case dns.TypeA:
   194  		rr.(*dns.A).A = rc.GetTargetIP()
   195  	case dns.TypeAAAA:
   196  		rr.(*dns.AAAA).AAAA = rc.GetTargetIP()
   197  	case dns.TypeCNAME:
   198  		rr.(*dns.CNAME).Target = rc.GetTargetField()
   199  	case dns.TypePTR:
   200  		rr.(*dns.PTR).Ptr = rc.GetTargetField()
   201  	case dns.TypeMX:
   202  		rr.(*dns.MX).Preference = rc.MxPreference
   203  		rr.(*dns.MX).Mx = rc.GetTargetField()
   204  	case dns.TypeNS:
   205  		rr.(*dns.NS).Ns = rc.GetTargetField()
   206  	case dns.TypeSOA:
   207  		t := strings.Replace(rc.GetTargetField(), `\ `, ` `, -1)
   208  		parts := strings.Fields(t)
   209  		rr.(*dns.SOA).Ns = parts[0]
   210  		rr.(*dns.SOA).Mbox = parts[1]
   211  		rr.(*dns.SOA).Serial = atou32(parts[2])
   212  		rr.(*dns.SOA).Refresh = atou32(parts[3])
   213  		rr.(*dns.SOA).Retry = atou32(parts[4])
   214  		rr.(*dns.SOA).Expire = atou32(parts[5])
   215  		rr.(*dns.SOA).Minttl = atou32(parts[6])
   216  	case dns.TypeSRV:
   217  		rr.(*dns.SRV).Priority = rc.SrvPriority
   218  		rr.(*dns.SRV).Weight = rc.SrvWeight
   219  		rr.(*dns.SRV).Port = rc.SrvPort
   220  		rr.(*dns.SRV).Target = rc.GetTargetField()
   221  	case dns.TypeCAA:
   222  		rr.(*dns.CAA).Flag = rc.CaaFlag
   223  		rr.(*dns.CAA).Tag = rc.CaaTag
   224  		rr.(*dns.CAA).Value = rc.GetTargetField()
   225  	case dns.TypeTLSA:
   226  		rr.(*dns.TLSA).Usage = rc.TlsaUsage
   227  		rr.(*dns.TLSA).MatchingType = rc.TlsaMatchingType
   228  		rr.(*dns.TLSA).Selector = rc.TlsaSelector
   229  		rr.(*dns.TLSA).Certificate = rc.GetTargetField()
   230  	case dns.TypeTXT:
   231  		rr.(*dns.TXT).Txt = rc.TxtStrings
   232  	default:
   233  		panic(fmt.Sprintf("ToRR: Unimplemented rtype %v", rc.Type))
   234  		// We panic so that we quickly find any switch statements
   235  		// that have not been updated for a new RR type.
   236  	}
   237  
   238  	return rr
   239  }
   240  
   241  // RecordKey represents a resource record in a format used by some systems.
   242  type RecordKey struct {
   243  	NameFQDN string
   244  	Type     string
   245  }
   246  
   247  // Key converts a RecordConfig into a RecordKey.
   248  func (rc *RecordConfig) Key() RecordKey {
   249  	t := rc.Type
   250  	if rc.R53Alias != nil {
   251  		if v, ok := rc.R53Alias["type"]; ok {
   252  			// Route53 aliases append their alias type, so that records for the same
   253  			// label with different alias types are considered separate.
   254  			t = fmt.Sprintf("%s_%s", t, v)
   255  		}
   256  	}
   257  	return RecordKey{rc.NameFQDN, t}
   258  }
   259  
   260  // Records is a list of *RecordConfig.
   261  type Records []*RecordConfig
   262  
   263  // Grouped returns a map of keys to records.
   264  func (r Records) Grouped() map[RecordKey]Records {
   265  	groups := map[RecordKey]Records{}
   266  	for _, rec := range r {
   267  		groups[rec.Key()] = append(groups[rec.Key()], rec)
   268  	}
   269  	return groups
   270  }
   271  
   272  // GroupedByLabel returns a map of keys to records, and their original key order.
   273  func (r Records) GroupedByLabel() ([]string, map[string]Records) {
   274  	order := []string{}
   275  	groups := map[string]Records{}
   276  	for _, rec := range r {
   277  		if _, found := groups[rec.Name]; !found {
   278  			order = append(order, rec.Name)
   279  		}
   280  		groups[rec.Name] = append(groups[rec.Name], rec)
   281  	}
   282  	return order, groups
   283  }
   284  
   285  // PostProcessRecords does any post-processing of the downloaded DNS records.
   286  func PostProcessRecords(recs []*RecordConfig) {
   287  	downcase(recs)
   288  }
   289  
   290  // Downcase converts all labels and targets to lowercase in a list of RecordConfig.
   291  func downcase(recs []*RecordConfig) {
   292  	for _, r := range recs {
   293  		r.Name = strings.ToLower(r.Name)
   294  		r.NameFQDN = strings.ToLower(r.NameFQDN)
   295  		switch r.Type { // #rtype_variations
   296  		case "ANAME", "CNAME", "MX", "NS", "PTR", "SRV":
   297  			// These record types have a target that is case insensitive, so we downcase it.
   298  			r.Target = strings.ToLower(r.Target)
   299  		case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SOA", "CF_REDIRECT", "CF_TEMP_REDIRECT":
   300  			// These record types have a target that is case sensitive, or is an IP address. We leave them alone.
   301  			// Do nothing.
   302  		default:
   303  			// TODO: we'd like to panic here, but custom record types complicate things.
   304  		}
   305  	}
   306  	return
   307  }