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