Skip to content

Commit 77578da

Browse files
authored
feat: add DNSSEC validation (#1914)
1 parent 1cdaf72 commit 77578da

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+20909
-14
lines changed

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ linters:
107107
- linters:
108108
- dupl
109109
- funlen
110+
- goconst
110111
- gochecknoglobals
111112
- gochecknoinits
112113
- gosec

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ GO_BUILD_OUTPUT:=$(BIN_OUT_DIR)/$(BINARY_NAME)$(BINARY_SUFFIX)
2525
# define version of golangci-lint here. If defined in tools.go, go mod perfoms automatically downgrade to older version which doesn't work with golang >=1.18
2626
GOLANG_LINT_VERSION=v2.2.1
2727

28-
GINKGO_PROCS?=-p
28+
GINKGO_PROCS?=
2929

3030
export PATH=$(shell go env GOPATH)/bin:$(shell echo $$PATH)
3131

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Blocky is a DNS proxy and ad-blocker for the local network written in Go with fo
5050
- **Security and Privacy** - Secure communication
5151

5252
- Supports modern DNS extensions: DNSSEC, eDNS, ...
53+
- DNSSEC validation of upstream resolvers
5354
- Free configurable blocking lists - no hidden filtering etc.
5455
- Provides DoH Endpoint
5556
- Uses random upstream resolvers from the configuration - increases your privacy through the distribution of your DNS

cmd/serve_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ var _ = Describe("Serve command", func() {
5858
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+port, 200*time.Millisecond)
5959
g.Expect(err).Should(Succeed())
6060
defer conn.Close()
61-
}).Should(Succeed())
61+
}, "5s").Should(Succeed())
6262
})
6363

6464
By("terminate with signal", func() {

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ type Config struct {
267267
EDE EDE `yaml:"ede"`
268268
ECS ECS `yaml:"ecs"`
269269
SUDN SUDN `yaml:"specialUseDomains"`
270+
DNSSEC DNSSEC `yaml:"dnssec"`
270271

271272
// Deprecated options
272273
Deprecated struct {

config/dnssec.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package config
2+
3+
import (
4+
"github.com/sirupsen/logrus"
5+
)
6+
7+
// DNSSEC is the configuration for DNSSEC validation
8+
type DNSSEC struct {
9+
Validate bool `default:"false" yaml:"validate"`
10+
TrustAnchors []string `yaml:"trustAnchors"`
11+
MaxChainDepth uint `default:"10" yaml:"maxChainDepth"`
12+
CacheExpirationHours uint `default:"1" yaml:"cacheExpirationHours"`
13+
MaxNSEC3Iterations uint `default:"150" yaml:"maxNSEC3Iterations"` // RFC 5155 §10.3
14+
// DoS protection: max upstream queries per validation
15+
MaxUpstreamQueries uint `default:"30" yaml:"maxUpstreamQueries"`
16+
// Clock skew tolerance in seconds for signature validation (default: 3600 = 1 hour)
17+
// Allows validation to succeed even if system clock is off by this amount.
18+
// Matches Unbound/BIND defaults for real-world deployments (VMs, containers, embedded systems).
19+
// Per RFC 6781 §4.1.2: Validators should account for clock skew in deployment environments.
20+
ClockSkewToleranceSec uint `default:"3600" yaml:"clockSkewToleranceSec"`
21+
}
22+
23+
// IsEnabled returns true if DNSSEC validation is enabled
24+
func (c *DNSSEC) IsEnabled() bool {
25+
return c.Validate
26+
}
27+
28+
// LogConfig logs the DNSSEC configuration
29+
func (c *DNSSEC) LogConfig(logger *logrus.Entry) {
30+
logger.Infof("Validation = %t", c.Validate)
31+
32+
if c.Validate {
33+
if len(c.TrustAnchors) > 0 {
34+
logger.Infof("Custom trust anchors = %d", len(c.TrustAnchors))
35+
} else {
36+
logger.Info("Using default root trust anchors")
37+
}
38+
logger.Infof("Max chain depth = %d", c.MaxChainDepth)
39+
logger.Infof("Cache expiration = %d hour(s)", c.CacheExpirationHours)
40+
logger.Infof("Max NSEC3 iterations = %d", c.MaxNSEC3Iterations)
41+
logger.Infof("Max upstream queries per validation = %d", c.MaxUpstreamQueries)
42+
logger.Infof("Clock skew tolerance = %d second(s)", c.ClockSkewToleranceSec)
43+
}
44+
}

config/dnssec_test.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package config
2+
3+
import (
4+
"bytes"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
"github.com/sirupsen/logrus"
9+
)
10+
11+
var _ = Describe("DNSSEC config", func() {
12+
Describe("IsEnabled", func() {
13+
It("should return true when Validate is true", func() {
14+
cfg := &DNSSEC{
15+
Validate: true,
16+
}
17+
18+
Expect(cfg.IsEnabled()).Should(BeTrue())
19+
})
20+
21+
It("should return false when Validate is false", func() {
22+
cfg := &DNSSEC{
23+
Validate: false,
24+
}
25+
26+
Expect(cfg.IsEnabled()).Should(BeFalse())
27+
})
28+
29+
It("should return false for zero value", func() {
30+
cfg := &DNSSEC{}
31+
32+
Expect(cfg.IsEnabled()).Should(BeFalse())
33+
})
34+
})
35+
36+
Describe("LogConfig", func() {
37+
var (
38+
logger *logrus.Entry
39+
buf *bytes.Buffer
40+
)
41+
42+
BeforeEach(func() {
43+
buf = new(bytes.Buffer)
44+
logInstance := logrus.New()
45+
logInstance.Out = buf
46+
logInstance.SetLevel(logrus.InfoLevel)
47+
logger = logrus.NewEntry(logInstance)
48+
})
49+
50+
It("should log validation status when disabled", func() {
51+
cfg := &DNSSEC{
52+
Validate: false,
53+
}
54+
55+
cfg.LogConfig(logger)
56+
57+
output := buf.String()
58+
Expect(output).Should(ContainSubstring("Validation = false"))
59+
})
60+
61+
It("should log validation status and all settings when enabled", func() {
62+
cfg := &DNSSEC{
63+
Validate: true,
64+
MaxChainDepth: 10,
65+
CacheExpirationHours: 1,
66+
MaxNSEC3Iterations: 150,
67+
MaxUpstreamQueries: 30,
68+
ClockSkewToleranceSec: 3600,
69+
}
70+
71+
cfg.LogConfig(logger)
72+
73+
output := buf.String()
74+
Expect(output).Should(ContainSubstring("Validation = true"))
75+
Expect(output).Should(ContainSubstring("Max chain depth = 10"))
76+
Expect(output).Should(ContainSubstring("Cache expiration = 1 hour(s)"))
77+
Expect(output).Should(ContainSubstring("Max NSEC3 iterations = 150"))
78+
Expect(output).Should(ContainSubstring("Max upstream queries per validation = 30"))
79+
Expect(output).Should(ContainSubstring("Clock skew tolerance = 3600 second(s)"))
80+
})
81+
82+
It("should log default trust anchors when none provided", func() {
83+
cfg := &DNSSEC{
84+
Validate: true,
85+
TrustAnchors: []string{},
86+
}
87+
88+
cfg.LogConfig(logger)
89+
90+
output := buf.String()
91+
Expect(output).Should(ContainSubstring("Using default root trust anchors"))
92+
})
93+
94+
It("should log custom trust anchors count when provided", func() {
95+
cfg := &DNSSEC{
96+
Validate: true,
97+
TrustAnchors: []string{
98+
"anchor1",
99+
"anchor2",
100+
"anchor3",
101+
},
102+
}
103+
104+
cfg.LogConfig(logger)
105+
106+
output := buf.String()
107+
Expect(output).Should(ContainSubstring("Custom trust anchors = 3"))
108+
})
109+
110+
It("should not log detailed settings when disabled", func() {
111+
cfg := &DNSSEC{
112+
Validate: false,
113+
MaxChainDepth: 10,
114+
CacheExpirationHours: 1,
115+
MaxNSEC3Iterations: 150,
116+
MaxUpstreamQueries: 30,
117+
ClockSkewToleranceSec: 3600,
118+
}
119+
120+
cfg.LogConfig(logger)
121+
122+
output := buf.String()
123+
Expect(output).Should(ContainSubstring("Validation = false"))
124+
Expect(output).ShouldNot(ContainSubstring("Max chain depth"))
125+
Expect(output).ShouldNot(ContainSubstring("Cache expiration"))
126+
Expect(output).ShouldNot(ContainSubstring("Max NSEC3 iterations"))
127+
})
128+
129+
It("should log different cache expiration values", func() {
130+
cfg := &DNSSEC{
131+
Validate: true,
132+
CacheExpirationHours: 24,
133+
}
134+
135+
cfg.LogConfig(logger)
136+
137+
output := buf.String()
138+
Expect(output).Should(ContainSubstring("Cache expiration = 24 hour(s)"))
139+
})
140+
141+
It("should log different max chain depth values", func() {
142+
cfg := &DNSSEC{
143+
Validate: true,
144+
MaxChainDepth: 20,
145+
}
146+
147+
cfg.LogConfig(logger)
148+
149+
output := buf.String()
150+
Expect(output).Should(ContainSubstring("Max chain depth = 20"))
151+
})
152+
153+
It("should log different NSEC3 iteration limits", func() {
154+
cfg := &DNSSEC{
155+
Validate: true,
156+
MaxNSEC3Iterations: 500,
157+
}
158+
159+
cfg.LogConfig(logger)
160+
161+
output := buf.String()
162+
Expect(output).Should(ContainSubstring("Max NSEC3 iterations = 500"))
163+
})
164+
165+
It("should log different upstream query limits", func() {
166+
cfg := &DNSSEC{
167+
Validate: true,
168+
MaxUpstreamQueries: 50,
169+
}
170+
171+
cfg.LogConfig(logger)
172+
173+
output := buf.String()
174+
Expect(output).Should(ContainSubstring("Max upstream queries per validation = 50"))
175+
})
176+
177+
It("should log different clock skew tolerance values", func() {
178+
cfg := &DNSSEC{
179+
Validate: true,
180+
ClockSkewToleranceSec: 7200,
181+
}
182+
183+
cfg.LogConfig(logger)
184+
185+
output := buf.String()
186+
Expect(output).Should(ContainSubstring("Clock skew tolerance = 7200 second(s)"))
187+
})
188+
189+
It("should handle zero values when enabled", func() {
190+
cfg := &DNSSEC{
191+
Validate: true,
192+
MaxChainDepth: 0,
193+
CacheExpirationHours: 0,
194+
MaxNSEC3Iterations: 0,
195+
MaxUpstreamQueries: 0,
196+
ClockSkewToleranceSec: 0,
197+
}
198+
199+
cfg.LogConfig(logger)
200+
201+
output := buf.String()
202+
Expect(output).Should(ContainSubstring("Max chain depth = 0"))
203+
Expect(output).Should(ContainSubstring("Cache expiration = 0 hour(s)"))
204+
Expect(output).Should(ContainSubstring("Max NSEC3 iterations = 0"))
205+
Expect(output).Should(ContainSubstring("Max upstream queries per validation = 0"))
206+
Expect(output).Should(ContainSubstring("Clock skew tolerance = 0 second(s)"))
207+
})
208+
209+
It("should handle single custom trust anchor", func() {
210+
cfg := &DNSSEC{
211+
Validate: true,
212+
TrustAnchors: []string{
213+
"single-anchor",
214+
},
215+
}
216+
217+
cfg.LogConfig(logger)
218+
219+
output := buf.String()
220+
Expect(output).Should(ContainSubstring("Custom trust anchors = 1"))
221+
})
222+
223+
It("should handle nil trust anchors same as empty", func() {
224+
cfg := &DNSSEC{
225+
Validate: true,
226+
TrustAnchors: nil,
227+
}
228+
229+
cfg.LogConfig(logger)
230+
231+
output := buf.String()
232+
Expect(output).Should(ContainSubstring("Using default root trust anchors"))
233+
})
234+
})
235+
236+
Describe("Configuration defaults", func() {
237+
It("should document default values via struct tags", func() {
238+
// This test documents the expected default values
239+
// The actual defaults are set by the config loader based on struct tags
240+
cfg := &DNSSEC{}
241+
242+
// Document expected defaults (set by config loader)
243+
expectedDefaults := map[string]interface{}{
244+
"validate": false,
245+
"maxChainDepth": uint(10),
246+
"cacheExpirationHours": uint(1),
247+
"maxNSEC3Iterations": uint(150),
248+
"maxUpstreamQueries": uint(30),
249+
"clockSkewToleranceSec": uint(3600),
250+
}
251+
252+
// Verify struct has the expected fields
253+
Expect(cfg).ShouldNot(BeNil())
254+
_ = expectedDefaults // Document defaults
255+
})
256+
})
257+
})

0 commit comments

Comments
 (0)