Skip to content

Commit f145bce

Browse files
tls: Add tls_resolvers global option for DNS challenge configuration (#7297)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
1 parent 174fa2d commit f145bce

File tree

10 files changed

+547
-2
lines changed

10 files changed

+547
-2
lines changed

caddyconfig/httpcaddyfile/options.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func init() {
6464
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
6565
RegisterGlobalOption("persist_config", parseOptPersistConfig)
6666
RegisterGlobalOption("dns", parseOptDNS)
67+
RegisterGlobalOption("tls_resolvers", parseOptTLSResolvers)
6768
RegisterGlobalOption("ech", parseOptECH)
6869
RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio)
6970
}
@@ -306,6 +307,15 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
306307
return val, nil
307308
}
308309

310+
func parseOptTLSResolvers(d *caddyfile.Dispenser, _ any) (any, error) {
311+
d.Next() // consume option name
312+
resolvers := d.RemainingArgs()
313+
if len(resolvers) == 0 {
314+
return nil, d.ArgErr()
315+
}
316+
return resolvers, nil
317+
}
318+
309319
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
310320
d.Next() // consume option name
311321

caddyconfig/httpcaddyfile/options_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package httpcaddyfile
22

33
import (
4+
"encoding/json"
45
"testing"
56

67
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
8+
"github.com/caddyserver/caddy/v2/modules/caddytls"
79
_ "github.com/caddyserver/caddy/v2/modules/logging"
810
)
911

@@ -62,3 +64,105 @@ func TestGlobalLogOptionSyntax(t *testing.T) {
6264
}
6365
}
6466
}
67+
68+
func TestGlobalResolversOption(t *testing.T) {
69+
tests := []struct {
70+
name string
71+
input string
72+
expectResolvers []string
73+
expectError bool
74+
}{
75+
{
76+
name: "single resolver",
77+
input: `{
78+
tls_resolvers 1.1.1.1
79+
}
80+
example.com {
81+
}`,
82+
expectResolvers: []string{"1.1.1.1"},
83+
expectError: false,
84+
},
85+
{
86+
name: "two resolvers",
87+
input: `{
88+
tls_resolvers 1.1.1.1 8.8.8.8
89+
}
90+
example.com {
91+
}`,
92+
expectResolvers: []string{"1.1.1.1", "8.8.8.8"},
93+
expectError: false,
94+
},
95+
{
96+
name: "multiple resolvers",
97+
input: `{
98+
tls_resolvers 1.1.1.1 8.8.8.8 9.9.9.9
99+
}
100+
example.com {
101+
}`,
102+
expectResolvers: []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"},
103+
expectError: false,
104+
},
105+
{
106+
name: "no resolvers specified",
107+
input: `{
108+
}
109+
example.com {
110+
}`,
111+
expectResolvers: nil,
112+
expectError: false,
113+
},
114+
}
115+
116+
for _, tc := range tests {
117+
t.Run(tc.name, func(t *testing.T) {
118+
adapter := caddyfile.Adapter{
119+
ServerType: ServerType{},
120+
}
121+
122+
out, _, err := adapter.Adapt([]byte(tc.input), nil)
123+
124+
if (err != nil) != tc.expectError {
125+
t.Errorf("error expectation failed. Expected error: %v, got: %v", tc.expectError, err)
126+
return
127+
}
128+
129+
if tc.expectError {
130+
return
131+
}
132+
133+
// Parse the output JSON to check resolvers
134+
var config struct {
135+
Apps struct {
136+
TLS *caddytls.TLS `json:"tls"`
137+
} `json:"apps"`
138+
}
139+
140+
if err := json.Unmarshal(out, &config); err != nil {
141+
t.Errorf("failed to unmarshal output: %v", err)
142+
return
143+
}
144+
145+
// Check if resolvers match expected
146+
if config.Apps.TLS == nil {
147+
if tc.expectResolvers != nil {
148+
t.Errorf("Expected TLS config with resolvers %v, but TLS config is nil", tc.expectResolvers)
149+
}
150+
return
151+
}
152+
153+
actualResolvers := config.Apps.TLS.Resolvers
154+
if len(tc.expectResolvers) == 0 && len(actualResolvers) == 0 {
155+
return // Both empty, ok
156+
}
157+
if len(actualResolvers) != len(tc.expectResolvers) {
158+
t.Errorf("Expected %d resolvers, got %d. Expected: %v, got: %v", len(tc.expectResolvers), len(actualResolvers), tc.expectResolvers, actualResolvers)
159+
return
160+
}
161+
for j, expected := range tc.expectResolvers {
162+
if actualResolvers[j] != expected {
163+
t.Errorf("Resolver %d mismatch. Expected: %s, got: %s", j, expected, actualResolvers[j])
164+
}
165+
}
166+
})
167+
}
168+
}

caddyconfig/httpcaddyfile/tlsapp.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,11 @@ func (st ServerType) buildTLSApp(
334334
tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
335335
}
336336

337+
// set up "global" (to the TLS app) DNS resolvers config
338+
if globalResolvers, ok := options["tls_resolvers"]; ok && globalResolvers != nil {
339+
tlsApp.Resolvers = globalResolvers.([]string)
340+
}
341+
337342
// set up ECH from Caddyfile options
338343
if ech, ok := options["ech"].(*caddytls.ECH); ok {
339344
tlsApp.EncryptedClientHello = ech
@@ -595,6 +600,15 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
595600
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
596601
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
597602
}
603+
// apply global resolvers if DNS challenge is configured and resolvers are not already set
604+
globalResolvers := options["tls_resolvers"]
605+
if globalResolvers != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
606+
// Check if DNS challenge is actually configured
607+
hasDNSChallenge := globalACMEDNSok || acmeIssuer.Challenges.DNS.ProviderRaw != nil
608+
if hasDNSChallenge && len(acmeIssuer.Challenges.DNS.Resolvers) == 0 {
609+
acmeIssuer.Challenges.DNS.Resolvers = globalResolvers.([]string)
610+
}
611+
}
598612
return nil
599613
}
600614

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
email test@example.com
3+
dns mock
4+
tls_resolvers 1.1.1.1 8.8.8.8
5+
acme_dns
6+
}
7+
8+
example.com {
9+
}
10+
----------
11+
{
12+
"apps": {
13+
"http": {
14+
"servers": {
15+
"srv0": {
16+
"listen": [
17+
":443"
18+
],
19+
"routes": [
20+
{
21+
"match": [
22+
{
23+
"host": [
24+
"example.com"
25+
]
26+
}
27+
],
28+
"terminal": true
29+
}
30+
]
31+
}
32+
}
33+
},
34+
"tls": {
35+
"automation": {
36+
"policies": [
37+
{
38+
"issuers": [
39+
{
40+
"challenges": {
41+
"dns": {
42+
"resolvers": [
43+
"1.1.1.1",
44+
"8.8.8.8"
45+
]
46+
}
47+
},
48+
"email": "test@example.com",
49+
"module": "acme"
50+
},
51+
{
52+
"ca": "https://acme.zerossl.com/v2/DV90",
53+
"challenges": {
54+
"dns": {
55+
"resolvers": [
56+
"1.1.1.1",
57+
"8.8.8.8"
58+
]
59+
}
60+
},
61+
"email": "test@example.com",
62+
"module": "acme"
63+
}
64+
]
65+
}
66+
]
67+
},
68+
"dns": {
69+
"name": "mock"
70+
},
71+
"resolvers": [
72+
"1.1.1.1",
73+
"8.8.8.8"
74+
]
75+
}
76+
}
77+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
tls_resolvers 1.1.1.1 8.8.8.8
3+
}
4+
5+
example.com {
6+
}
7+
----------
8+
{
9+
"apps": {
10+
"http": {
11+
"servers": {
12+
"srv0": {
13+
"listen": [
14+
":443"
15+
],
16+
"routes": [
17+
{
18+
"match": [
19+
{
20+
"host": [
21+
"example.com"
22+
]
23+
}
24+
],
25+
"terminal": true
26+
}
27+
]
28+
}
29+
}
30+
},
31+
"tls": {
32+
"resolvers": [
33+
"1.1.1.1",
34+
"8.8.8.8"
35+
]
36+
}
37+
}
38+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
email test@example.com
3+
dns mock
4+
tls_resolvers 1.1.1.1 8.8.8.8
5+
}
6+
7+
example.com {
8+
tls {
9+
dns mock
10+
}
11+
}
12+
----------
13+
{
14+
"apps": {
15+
"http": {
16+
"servers": {
17+
"srv0": {
18+
"listen": [
19+
":443"
20+
],
21+
"routes": [
22+
{
23+
"match": [
24+
{
25+
"host": [
26+
"example.com"
27+
]
28+
}
29+
],
30+
"terminal": true
31+
}
32+
]
33+
}
34+
}
35+
},
36+
"tls": {
37+
"automation": {
38+
"policies": [
39+
{
40+
"subjects": [
41+
"example.com"
42+
],
43+
"issuers": [
44+
{
45+
"challenges": {
46+
"dns": {
47+
"provider": {
48+
"name": "mock"
49+
},
50+
"resolvers": [
51+
"1.1.1.1",
52+
"8.8.8.8"
53+
]
54+
}
55+
},
56+
"email": "test@example.com",
57+
"module": "acme"
58+
}
59+
]
60+
}
61+
]
62+
},
63+
"dns": {
64+
"name": "mock"
65+
},
66+
"resolvers": [
67+
"1.1.1.1",
68+
"8.8.8.8"
69+
]
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)