1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package network
16
+
17
+import (
18
+ "context"
19
+ "errors"
20
+ "fmt"
21
+ "net"
22
+ "sync"
23
+ "testing"
24
+ "time"
25
+
26
+ "go.uber.org/zap"
27
+ "go.uber.org/zap/zaptest"
28
+ "go.uber.org/zap/zaptest/observer"
29
+)
30
+
31
+func TestSRVResolvesAndCaches(t *testing.T) {
32
+ calls := 0
33
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
34
+ calls++
35
+ return "", []*net.SRV{
36
+ {Target: "a.example.", Port: 5432, Priority: 1, Weight: 10},
37
+ {Target: "b.example.", Port: 5433, Priority: 1, Weight: 20},
38
+ }, nil
39
+ }
40
+
41
+ targets, err := SRV(context.Background(), lookup, "svc-cache", "tcp", "x", time.Minute, 0, zap.NewNop())
42
+ if err != nil {
43
+ t.Fatalf("SRV: %v", err)
44
+ }
45
+ if len(targets) != 2 {
46
+ t.Fatalf("targets = %d, want 2", len(targets))
47
+ }
48
+ if targets[0].Host != "a.example." || targets[0].Port != "5432" || targets[0].Weight != 10 {
49
+ t.Errorf("unexpected first target: %+v", targets[0])
50
+ }
51
+
52
+ // second call within refresh must be served from cache (no extra lookup)
53
+ if _, err := SRV(context.Background(), lookup, "svc-cache", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
54
+ t.Fatalf("SRV (cached): %v", err)
55
+ }
56
+ if calls != 1 {
57
+ t.Errorf("lookup calls = %d, want 1 (cached)", calls)
58
+ }
59
+}
60
+
61
+func TestSRVErrorWithoutGrace(t *testing.T) {
62
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
63
+ return "", nil, errors.New("dns boom")
64
+ }
65
+ if _, err := SRV(context.Background(), lookup, "svc-err", "tcp", "x", time.Minute, 0, zap.NewNop()); err == nil {
66
+ t.Fatal("expected an error when lookup fails and nothing is cached")
67
+ }
68
+}
69
+
70
+func TestSRVGracePeriodServesStale(t *testing.T) {
71
+ ok := func(context.Context, string, string, string) (string, []*net.SRV, error) {
72
+ return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
73
+ }
74
+ // tiny refresh so the entry is immediately stale on the next call
75
+ if _, err := SRV(context.Background(), ok, "svc-grace", "tcp", "x", time.Nanosecond, time.Hour, zap.NewNop()); err != nil {
76
+ t.Fatalf("seeding cache: %v", err)
77
+ }
78
+
79
+ fail := func(context.Context, string, string, string) (string, []*net.SRV, error) {
80
+ return "", nil, errors.New("dns boom")
81
+ }
82
+ // a level-enabled logger so the "using previously cached" error log fires
83
+ targets, err := SRV(context.Background(), fail, "svc-grace", "tcp", "x", time.Nanosecond, time.Hour, zaptest.NewLogger(t))
84
+ if err != nil {
85
+ t.Fatalf("grace period should suppress the error: %v", err)
86
+ }
87
+ if len(targets) != 1 {
88
+ t.Errorf("expected the stale cached target to be served, got %d", len(targets))
89
+ }
90
+}
91
+
92
+func TestResetSRV(t *testing.T) {
93
+ calls := 0
94
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
95
+ calls++
96
+ return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
97
+ }
98
+ if _, err := SRV(context.Background(), lookup, "svc-reset", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
99
+ t.Fatal(err)
100
+ }
101
+ ResetSRV("svc-reset", "tcp", "x")
102
+ if _, err := SRV(context.Background(), lookup, "svc-reset", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
103
+ t.Fatal(err)
104
+ }
105
+ if calls != 2 {
106
+ t.Errorf("lookup calls = %d, want 2 (cache was reset between calls)", calls)
107
+ }
108
+}
109
+
110
+func TestAResolvesAndCaches(t *testing.T) {
111
+ calls := 0
112
+ lookup := func(context.Context, string, string) ([]net.IP, error) {
113
+ calls++
114
+ return []net.IP{net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.2")}, nil
115
+ }
116
+
117
+ targets, err := A(context.Background(), lookup, "ip", "db.a-test", "5432", time.Minute, zap.NewNop())
118
+ if err != nil {
119
+ t.Fatalf("A: %v", err)
120
+ }
121
+ if len(targets) != 2 {
122
+ t.Fatalf("targets = %d, want 2", len(targets))
123
+ }
124
+ if targets[0].Host != "10.0.0.1" || targets[0].Port != "5432" {
125
+ t.Errorf("unexpected first target: %+v", targets[0])
126
+ }
127
+
128
+ if _, err := A(context.Background(), lookup, "ip", "db.a-test", "5432", time.Minute, zap.NewNop()); err != nil {
129
+ t.Fatalf("A (cached): %v", err)
130
+ }
131
+ if calls != 1 {
132
+ t.Errorf("lookup calls = %d, want 1 (cached)", calls)
133
+ }
134
+}
135
+
136
+func TestAError(t *testing.T) {
137
+ lookup := func(context.Context, string, string) ([]net.IP, error) {
138
+ return nil, errors.New("dns boom")
139
+ }
140
+ if _, err := A(context.Background(), lookup, "ip", "db.a-err", "5432", time.Minute, zap.NewNop()); err == nil {
141
+ t.Fatal("expected an error when the A lookup fails")
142
+ }
143
+}
144
+
145
+// TestSRVFilteredRecords covers the LookupSRV semantics where invalid names are
146
+// filtered out and an error is returned alongside the usable remainder: the
147
+// usable records must still be returned (the error is downgraded to a warning).
148
+func TestSRVFilteredRecords(t *testing.T) {
149
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
150
+ return "", []*net.SRV{{Target: "ok.example.", Port: 5432, Priority: 1, Weight: 10}},
151
+ errors.New("some SRV names were filtered out")
152
+ }
153
+ // a level-enabled logger so the "SRV records filtered" warning fires
154
+ targets, err := SRV(context.Background(), lookup, "svc-filtered", "tcp", "x", time.Minute, 0, zaptest.NewLogger(t))
155
+ if err != nil {
156
+ t.Fatalf("usable records must be returned despite the partial error: %v", err)
157
+ }
158
+ if len(targets) != 1 || targets[0].Host != "ok.example." {
159
+ t.Fatalf("unexpected targets: %+v", targets)
160
+ }
161
+}
162
+
163
+// TestResetAllSRV verifies that the whole SRV cache is dropped.
164
+func TestResetAllSRV(t *testing.T) {
165
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
166
+ return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
167
+ }
168
+ if _, err := SRV(context.Background(), lookup, "svc-reset-all", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
169
+ t.Fatalf("SRV: %v", err)
170
+ }
171
+ srvMu.RLock()
172
+ populated := len(srvCache) > 0
173
+ srvMu.RUnlock()
174
+ if !populated {
175
+ t.Fatal("expected the SRV cache to be populated before reset")
176
+ }
177
+
178
+ ResetAllSRV()
179
+
180
+ srvMu.RLock()
181
+ n := len(srvCache)
182
+ srvMu.RUnlock()
183
+ if n != 0 {
184
+ t.Fatalf("srvCache len = %d after ResetAllSRV, want 0", n)
185
+ }
186
+}
187
+
188
+// TestSRVCacheBound verifies that inserting a brand-new entry once the cache is
189
+// full evicts an existing one so the cache stays bounded.
190
+func TestSRVCacheBound(t *testing.T) {
191
+ srvMu.Lock()
192
+ srvCache = make(map[string]cacheEntry)
193
+ srvMu.Unlock()
194
+
195
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
196
+ return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
197
+ }
198
+ for i := 0; i < maxCacheEntries+5; i++ {
199
+ name := fmt.Sprintf("svc-bound-%d", i)
200
+ if _, err := SRV(context.Background(), lookup, name, "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
201
+ t.Fatalf("SRV[%d]: %v", i, err)
202
+ }
203
+ }
204
+ srvMu.RLock()
205
+ n := len(srvCache)
206
+ srvMu.RUnlock()
207
+ if n > maxCacheEntries {
208
+ t.Fatalf("srvCache len = %d, want <= %d (cache must stay bounded)", n, maxCacheEntries)
209
+ }
210
+}
211
+
212
+// TestACacheBound verifies the same bounding behavior for the A cache.
213
+func TestACacheBound(t *testing.T) {
214
+ aMu.Lock()
215
+ aCache = make(map[string]cacheEntry)
216
+ aMu.Unlock()
217
+
218
+ lookup := func(context.Context, string, string) ([]net.IP, error) {
219
+ return []net.IP{net.ParseIP("10.0.0.1")}, nil
220
+ }
221
+ for i := 0; i < maxCacheEntries+5; i++ {
222
+ name := fmt.Sprintf("db.bound-%d", i)
223
+ if _, err := A(context.Background(), lookup, "ip", name, "5432", time.Minute, zap.NewNop()); err != nil {
224
+ t.Fatalf("A[%d]: %v", i, err)
225
+ }
226
+ }
227
+ aMu.RLock()
228
+ n := len(aCache)
229
+ aMu.RUnlock()
230
+ if n > maxCacheEntries {
231
+ t.Fatalf("aCache len = %d, want <= %d (cache must stay bounded)", n, maxCacheEntries)
232
+ }
233
+}
234
+
235
+// TestSRVConcurrentRefreshDeduplicates verifies that two concurrent callers
236
+// missing the cache for the same key result in a single lookup: the first
237
+// performs it while holding the write lock, and the second is served the
238
+// cached result once the first releases. Depending on scheduling the second
239
+// caller may be served by either the read-lock fast path or the write-lock
240
+// re-check — this test does not force one branch over the other; it asserts
241
+// deduplication (exactly one lookup). Run under -race it also guards against
242
+// data races on the shared cache.
243
+func TestSRVConcurrentRefreshDeduplicates(t *testing.T) {
244
+ srvMu.Lock()
245
+ srvCache = make(map[string]cacheEntry)
246
+ srvMu.Unlock()
247
+
248
+ var calls int
249
+ var mu sync.Mutex
250
+ inLookup := make(chan struct{})
251
+ release := make(chan struct{})
252
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
253
+ mu.Lock()
254
+ calls++
255
+ first := calls == 1
256
+ mu.Unlock()
257
+ if first {
258
+ close(inLookup)
259
+ <-release // hold the write lock until the second goroutine is queued
260
+ }
261
+ return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
262
+ }
263
+
264
+ done := make(chan struct{}, 2)
265
+ // G1: takes the write lock and blocks inside lookup.
266
+ go func() {
267
+ _, _ = SRV(context.Background(), lookup, "svc-conc", "tcp", "x", time.Minute, 0, zap.NewNop())
268
+ done <- struct{}{}
269
+ }()
270
+ <-inLookup // G1 now holds the write lock; cache is still empty
271
+
272
+ // G2: a second caller for the same key. While G1 holds the write lock, G2
273
+ // cannot acquire srvMu at all (a reader blocks behind the writer); once G1
274
+ // populates the cache and releases, G2 observes the cached entry and
275
+ // performs no lookup. Ordering here is enforced by the channels below, not
276
+ // by timing.
277
+ go func() {
278
+ _, _ = SRV(context.Background(), lookup, "svc-conc", "tcp", "x", time.Minute, 0, zap.NewNop())
279
+ done <- struct{}{}
280
+ }()
281
+
282
+ close(release) // G1 populates the cache and releases the lock
283
+ <-done
284
+ <-done
285
+
286
+ mu.Lock()
287
+ got := calls
288
+ mu.Unlock()
289
+ if got != 1 {
290
+ t.Fatalf("lookup calls = %d, want 1 (concurrent callers for the same key must share one lookup)", got)
291
+ }
292
+}
293
+
294
+// TestAConcurrentRefreshDeduplicates is the A-cache equivalent of the above:
295
+// it asserts deduplication (one lookup for two concurrent callers) and, under
296
+// -race, race-safety; it does not force a particular locking branch.
297
+func TestAConcurrentRefreshDeduplicates(t *testing.T) {
298
+ aMu.Lock()
299
+ aCache = make(map[string]cacheEntry)
300
+ aMu.Unlock()
301
+
302
+ var calls int
303
+ var mu sync.Mutex
304
+ inLookup := make(chan struct{})
305
+ release := make(chan struct{})
306
+ lookup := func(context.Context, string, string) ([]net.IP, error) {
307
+ mu.Lock()
308
+ calls++
309
+ first := calls == 1
310
+ mu.Unlock()
311
+ if first {
312
+ close(inLookup)
313
+ <-release
314
+ }
315
+ return []net.IP{net.ParseIP("10.0.0.1")}, nil
316
+ }
317
+
318
+ done := make(chan struct{}, 2)
319
+ go func() {
320
+ _, _ = A(context.Background(), lookup, "ip", "db.conc", "5432", time.Minute, zap.NewNop())
321
+ done <- struct{}{}
322
+ }()
323
+ <-inLookup
324
+
325
+ // Second caller for the same key; ordering is enforced by the channels,
326
+ // not by timing (see the SRV test above).
327
+ go func() {
328
+ _, _ = A(context.Background(), lookup, "ip", "db.conc", "5432", time.Minute, zap.NewNop())
329
+ done <- struct{}{}
330
+ }()
331
+
332
+ close(release)
333
+ <-done
334
+ <-done
335
+
336
+ mu.Lock()
337
+ got := calls
338
+ mu.Unlock()
339
+ if got != 1 {
340
+ t.Fatalf("lookup calls = %d, want 1 (concurrent callers for the same key must share one lookup)", got)
341
+ }
342
+}
343
+
344
+func TestSRVGracePeriodWithoutCachedResult(t *testing.T) {
345
+ ResetSRV("svc-cold-grace", "tcp", "x")
346
+ t.Cleanup(func() { ResetSRV("svc-cold-grace", "tcp", "x") })
347
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
348
+ return "", nil, errors.New("dns boom")
349
+ }
350
+ targets, err := SRV(context.Background(), lookup, "svc-cold-grace", "tcp", "x", time.Minute, time.Minute, zap.NewNop())
351
+ if err != nil || len(targets) != 0 {
352
+ t.Fatalf("cold-cache failure with grace: targets = %v, err = %v; want no targets and no error", targets, err)
353
+ }
354
+}
355
+
356
+func TestDiscoveryLoggingOnlyOnRefresh(t *testing.T) {
357
+ t.Run("SRV", func(t *testing.T) {
358
+ ResetSRV("svc-log", "tcp", "x")
359
+ t.Cleanup(func() { ResetSRV("svc-log", "tcp", "x") })
360
+ core, logs := observer.New(zap.DebugLevel)
361
+ logger := zap.New(core)
362
+ lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
363
+ return "", []*net.SRV{{Target: "a.example.", Port: 5432, Priority: 1, Weight: 10}}, nil
364
+ }
365
+ for range 2 {
366
+ if _, err := SRV(context.Background(), lookup, "svc-log", "tcp", "x", time.Minute, 0, logger); err != nil {
367
+ t.Fatal(err)
368
+ }
369
+ }
370
+ if got := logs.FilterMessage("refreshing SRV upstreams").Len(); got != 1 {
371
+ t.Fatalf("refresh logs = %d, want 1", got)
372
+ }
373
+ records := logs.FilterMessage("discovered SRV record").All()
374
+ if len(records) != 1 {
375
+ t.Fatalf("discovery logs = %d, want 1", len(records))
376
+ }
377
+ fields := records[0].ContextMap()
378
+ if fields["target"] != "a.example." || fields["port"] != uint16(5432) || fields["priority"] != uint16(1) || fields["weight"] != uint16(10) {
379
+ t.Fatalf("unexpected discovery log fields: %v", fields)
380
+ }
381
+ })
382
+ t.Run("A", func(t *testing.T) {
383
+ aMu.Lock()
384
+ aCache = make(map[string]cacheEntry)
385
+ aMu.Unlock()
386
+ core, logs := observer.New(zap.DebugLevel)
387
+ logger := zap.New(core)
388
+ lookup := func(context.Context, string, string) ([]net.IP, error) {
389
+ return []net.IP{net.ParseIP("10.0.0.1")}, nil
390
+ }
391
+ for range 2 {
392
+ if _, err := A(context.Background(), lookup, "ip", "db.log", "5432", time.Minute, logger); err != nil {
393
+ t.Fatal(err)
394
+ }
395
+ }
396
+ if got := logs.FilterMessage("refreshing A upstreams").Len(); got != 1 {
397
+ t.Fatalf("refresh logs = %d, want 1", got)
398
+ }
399
+ records := logs.FilterMessage("discovered A record").All()
400
+ if len(records) != 1 {
401
+ t.Fatalf("discovery logs = %d, want 1", len(records))
402
+ }
403
+ if got := records[0].ContextMap()["ip"]; got != "10.0.0.1" {
404
+ t.Fatalf("logged IP = %v, want 10.0.0.1", got)
405
+ }
406
+ })
407
+}