-
Notifications
You must be signed in to change notification settings - Fork 1
/
clouddns.go
227 lines (202 loc) · 6.09 KB
/
clouddns.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
// Package clouddns implements a plugin that returns resource records
// from GCP Cloud DNS.
package clouddns
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/khulnasoft-lab/dnserver/plugin"
"github.com/khulnasoft-lab/dnserver/plugin/file"
"github.com/khulnasoft-lab/dnserver/plugin/pkg/fall"
"github.com/khulnasoft-lab/dnserver/plugin/pkg/upstream"
"github.com/khulnasoft-lab/dnserver/request"
"github.com/miekg/dns"
gcp "google.golang.org/api/dns/v1"
)
// CloudDNS is a plugin that returns RR from GCP Cloud DNS.
type CloudDNS struct {
Next plugin.Handler
Fall fall.F
zoneNames []string
client gcpDNS
upstream *upstream.Upstream
zMu sync.RWMutex
zones zones
}
type zone struct {
projectName string
zoneName string
z *file.Zone
dns string
}
type zones map[string][]*zone
// New reads from the keys map which uses domain names as its key and a colon separated
// string of project name and hosted zone name lists as its values, validates
// that each domain name/zone id pair does exist, and returns a new *CloudDNS.
// In addition to this, upstream is passed for doing recursive queries against CNAMEs.
// Returns error if it cannot verify any given domain name/zone id pair.
func New(ctx context.Context, c gcpDNS, keys map[string][]string, up *upstream.Upstream) (*CloudDNS, error) {
zones := make(map[string][]*zone, len(keys))
zoneNames := make([]string, 0, len(keys))
for dnsName, hostedZoneDetails := range keys {
for _, hostedZone := range hostedZoneDetails {
ss := strings.SplitN(hostedZone, ":", 2)
if len(ss) != 2 {
return nil, errors.New("either project or zone name missing")
}
err := c.zoneExists(ss[0], ss[1])
if err != nil {
return nil, err
}
fqdnDNSName := dns.Fqdn(dnsName)
if _, ok := zones[fqdnDNSName]; !ok {
zoneNames = append(zoneNames, fqdnDNSName)
}
zones[fqdnDNSName] = append(zones[fqdnDNSName], &zone{projectName: ss[0], zoneName: ss[1], dns: fqdnDNSName, z: file.NewZone(fqdnDNSName, "")})
}
}
return &CloudDNS{
client: c,
zoneNames: zoneNames,
zones: zones,
upstream: up,
}, nil
}
// Run executes first update, spins up an update forever-loop.
// Returns error if first update fails.
func (h *CloudDNS) Run(ctx context.Context) error {
if err := h.updateZones(ctx); err != nil {
return err
}
go func() {
delay := 1 * time.Minute
timer := time.NewTimer(delay)
defer timer.Stop()
for {
timer.Reset(delay)
select {
case <-ctx.Done():
log.Debugf("Breaking out of CloudDNS update loop for %v: %v", h.zoneNames, ctx.Err())
return
case <-timer.C:
if err := h.updateZones(ctx); err != nil && ctx.Err() == nil /* Don't log error if ctx expired. */ {
log.Errorf("Failed to update zones %v: %v", h.zoneNames, err)
}
}
}
}()
return nil
}
// ServeDNS implements the plugin.Handler interface.
func (h *CloudDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
qname := state.Name()
zName := plugin.Zones(h.zoneNames).Matches(qname)
if zName == "" {
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
}
z, ok := h.zones[zName] // ok true if we are authoritative for the zone
if !ok || z == nil {
return dns.RcodeServerFailure, nil
}
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
var result file.Result
for _, hostedZone := range z {
h.zMu.RLock()
m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(ctx, state, qname)
h.zMu.RUnlock()
// Take the answer if it's non-empty OR if there is another
// record type exists for this name (NODATA).
if len(m.Answer) != 0 || result == file.NoData {
break
}
}
if len(m.Answer) == 0 && result != file.NoData && h.Fall.Through(qname) {
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
}
switch result {
case file.Success:
case file.NoData:
case file.NameError:
m.Rcode = dns.RcodeNameError
case file.Delegation:
m.Authoritative = false
case file.ServerFailure:
return dns.RcodeServerFailure, nil
}
w.WriteMsg(m)
return dns.RcodeSuccess, nil
}
func updateZoneFromRRS(rrs *gcp.ResourceRecordSetsListResponse, z *file.Zone) error {
for _, rr := range rrs.Rrsets {
var rfc1035 string
var r dns.RR
var err error
for _, value := range rr.Rrdatas {
if rr.Type == "CNAME" || rr.Type == "PTR" {
value = dns.Fqdn(value)
}
// Assemble RFC 1035 conforming record to pass into dns scanner.
rfc1035 = fmt.Sprintf("%s %d IN %s %s", dns.Fqdn(rr.Name), rr.Ttl, rr.Type, value)
r, err = dns.NewRR(rfc1035)
if err != nil {
return fmt.Errorf("failed to parse resource record: %v", err)
}
err = z.Insert(r)
if err != nil {
return fmt.Errorf("failed to insert record: %v", err)
}
}
}
return nil
}
// updateZones re-queries resource record sets for each zone and updates the
// zone object.
// Returns error if any zones error'ed out, but waits for other zones to
// complete first.
func (h *CloudDNS) updateZones(ctx context.Context) error {
errc := make(chan error)
defer close(errc)
for zName, z := range h.zones {
go func(zName string, z []*zone) {
var err error
var rrListResponse *gcp.ResourceRecordSetsListResponse
defer func() {
errc <- err
}()
for i, hostedZone := range z {
newZ := file.NewZone(zName, "")
newZ.Upstream = h.upstream
rrListResponse, err = h.client.listRRSets(ctx, hostedZone.projectName, hostedZone.zoneName)
if err != nil {
err = fmt.Errorf("failed to list resource records for %v:%v:%v from gcp: %v", zName, hostedZone.projectName, hostedZone.zoneName, err)
return
}
updateZoneFromRRS(rrListResponse, newZ)
h.zMu.Lock()
(*z[i]).z = newZ
h.zMu.Unlock()
}
}(zName, z)
}
// Collect errors (if any). This will also sync on all zones updates
// completion.
var errs []string
for i := 0; i < len(h.zones); i++ {
err := <-errc
if err != nil {
errs = append(errs, err.Error())
}
}
if len(errs) != 0 {
return fmt.Errorf("errors updating zones: %v", errs)
}
return nil
}
// Name implements the Handler interface.
func (h *CloudDNS) Name() string { return "clouddns" }