Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ linters:
- linters:
- dupl
- funlen
- goconst
- gochecknoglobals
- gochecknoinits
- gosec
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ GO_BUILD_OUTPUT:=$(BIN_OUT_DIR)/$(BINARY_NAME)$(BINARY_SUFFIX)
# 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
GOLANG_LINT_VERSION=v2.2.1

GINKGO_PROCS?=-p
GINKGO_PROCS?=

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

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Blocky is a DNS proxy and ad-blocker for the local network written in Go with fo
- **Security and Privacy** - Secure communication

- Supports modern DNS extensions: DNSSEC, eDNS, ...
- DNSSEC validation of upstream resolvers
- Free configurable blocking lists - no hidden filtering etc.
- Provides DoH Endpoint
- Uses random upstream resolvers from the configuration - increases your privacy through the distribution of your DNS
Expand Down
2 changes: 1 addition & 1 deletion cmd/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ var _ = Describe("Serve command", func() {
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+port, 200*time.Millisecond)
g.Expect(err).Should(Succeed())
defer conn.Close()
}).Should(Succeed())
}, "5s").Should(Succeed())
})

By("terminate with signal", func() {
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ type Config struct {
EDE EDE `yaml:"ede"`
ECS ECS `yaml:"ecs"`
SUDN SUDN `yaml:"specialUseDomains"`
DNSSEC DNSSEC `yaml:"dnssec"`

// Deprecated options
Deprecated struct {
Expand Down
44 changes: 44 additions & 0 deletions config/dnssec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package config

import (
"github.com/sirupsen/logrus"
)

// DNSSEC is the configuration for DNSSEC validation
type DNSSEC struct {
Validate bool `default:"false" yaml:"validate"`
TrustAnchors []string `yaml:"trustAnchors"`
MaxChainDepth uint `default:"10" yaml:"maxChainDepth"`
CacheExpirationHours uint `default:"1" yaml:"cacheExpirationHours"`
MaxNSEC3Iterations uint `default:"150" yaml:"maxNSEC3Iterations"` // RFC 5155 §10.3
// DoS protection: max upstream queries per validation
MaxUpstreamQueries uint `default:"30" yaml:"maxUpstreamQueries"`
// Clock skew tolerance in seconds for signature validation (default: 3600 = 1 hour)
// Allows validation to succeed even if system clock is off by this amount.
// Matches Unbound/BIND defaults for real-world deployments (VMs, containers, embedded systems).
// Per RFC 6781 §4.1.2: Validators should account for clock skew in deployment environments.
ClockSkewToleranceSec uint `default:"3600" yaml:"clockSkewToleranceSec"`
}

// IsEnabled returns true if DNSSEC validation is enabled
func (c *DNSSEC) IsEnabled() bool {
return c.Validate
}

// LogConfig logs the DNSSEC configuration
func (c *DNSSEC) LogConfig(logger *logrus.Entry) {
logger.Infof("Validation = %t", c.Validate)

if c.Validate {
if len(c.TrustAnchors) > 0 {
logger.Infof("Custom trust anchors = %d", len(c.TrustAnchors))
} else {
logger.Info("Using default root trust anchors")
}
logger.Infof("Max chain depth = %d", c.MaxChainDepth)
logger.Infof("Cache expiration = %d hour(s)", c.CacheExpirationHours)
logger.Infof("Max NSEC3 iterations = %d", c.MaxNSEC3Iterations)
logger.Infof("Max upstream queries per validation = %d", c.MaxUpstreamQueries)
logger.Infof("Clock skew tolerance = %d second(s)", c.ClockSkewToleranceSec)
}
}
257 changes: 257 additions & 0 deletions config/dnssec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package config

import (
"bytes"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sirupsen/logrus"
)

var _ = Describe("DNSSEC config", func() {
Describe("IsEnabled", func() {
It("should return true when Validate is true", func() {
cfg := &DNSSEC{
Validate: true,
}

Expect(cfg.IsEnabled()).Should(BeTrue())
})

It("should return false when Validate is false", func() {
cfg := &DNSSEC{
Validate: false,
}

Expect(cfg.IsEnabled()).Should(BeFalse())
})

It("should return false for zero value", func() {
cfg := &DNSSEC{}

Expect(cfg.IsEnabled()).Should(BeFalse())
})
})

Describe("LogConfig", func() {
var (
logger *logrus.Entry
buf *bytes.Buffer
)

BeforeEach(func() {
buf = new(bytes.Buffer)
logInstance := logrus.New()
logInstance.Out = buf
logInstance.SetLevel(logrus.InfoLevel)
logger = logrus.NewEntry(logInstance)
})

It("should log validation status when disabled", func() {
cfg := &DNSSEC{
Validate: false,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Validation = false"))
})

It("should log validation status and all settings when enabled", func() {
cfg := &DNSSEC{
Validate: true,
MaxChainDepth: 10,
CacheExpirationHours: 1,
MaxNSEC3Iterations: 150,
MaxUpstreamQueries: 30,
ClockSkewToleranceSec: 3600,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Validation = true"))
Expect(output).Should(ContainSubstring("Max chain depth = 10"))
Expect(output).Should(ContainSubstring("Cache expiration = 1 hour(s)"))
Expect(output).Should(ContainSubstring("Max NSEC3 iterations = 150"))
Expect(output).Should(ContainSubstring("Max upstream queries per validation = 30"))
Expect(output).Should(ContainSubstring("Clock skew tolerance = 3600 second(s)"))
})

It("should log default trust anchors when none provided", func() {
cfg := &DNSSEC{
Validate: true,
TrustAnchors: []string{},
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Using default root trust anchors"))
})

It("should log custom trust anchors count when provided", func() {
cfg := &DNSSEC{
Validate: true,
TrustAnchors: []string{
"anchor1",
"anchor2",
"anchor3",
},
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Custom trust anchors = 3"))
})

It("should not log detailed settings when disabled", func() {
cfg := &DNSSEC{
Validate: false,
MaxChainDepth: 10,
CacheExpirationHours: 1,
MaxNSEC3Iterations: 150,
MaxUpstreamQueries: 30,
ClockSkewToleranceSec: 3600,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Validation = false"))
Expect(output).ShouldNot(ContainSubstring("Max chain depth"))
Expect(output).ShouldNot(ContainSubstring("Cache expiration"))
Expect(output).ShouldNot(ContainSubstring("Max NSEC3 iterations"))
})

It("should log different cache expiration values", func() {
cfg := &DNSSEC{
Validate: true,
CacheExpirationHours: 24,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Cache expiration = 24 hour(s)"))
})

It("should log different max chain depth values", func() {
cfg := &DNSSEC{
Validate: true,
MaxChainDepth: 20,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Max chain depth = 20"))
})

It("should log different NSEC3 iteration limits", func() {
cfg := &DNSSEC{
Validate: true,
MaxNSEC3Iterations: 500,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Max NSEC3 iterations = 500"))
})

It("should log different upstream query limits", func() {
cfg := &DNSSEC{
Validate: true,
MaxUpstreamQueries: 50,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Max upstream queries per validation = 50"))
})

It("should log different clock skew tolerance values", func() {
cfg := &DNSSEC{
Validate: true,
ClockSkewToleranceSec: 7200,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Clock skew tolerance = 7200 second(s)"))
})

It("should handle zero values when enabled", func() {
cfg := &DNSSEC{
Validate: true,
MaxChainDepth: 0,
CacheExpirationHours: 0,
MaxNSEC3Iterations: 0,
MaxUpstreamQueries: 0,
ClockSkewToleranceSec: 0,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Max chain depth = 0"))
Expect(output).Should(ContainSubstring("Cache expiration = 0 hour(s)"))
Expect(output).Should(ContainSubstring("Max NSEC3 iterations = 0"))
Expect(output).Should(ContainSubstring("Max upstream queries per validation = 0"))
Expect(output).Should(ContainSubstring("Clock skew tolerance = 0 second(s)"))
})

It("should handle single custom trust anchor", func() {
cfg := &DNSSEC{
Validate: true,
TrustAnchors: []string{
"single-anchor",
},
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Custom trust anchors = 1"))
})

It("should handle nil trust anchors same as empty", func() {
cfg := &DNSSEC{
Validate: true,
TrustAnchors: nil,
}

cfg.LogConfig(logger)

output := buf.String()
Expect(output).Should(ContainSubstring("Using default root trust anchors"))
})
})

Describe("Configuration defaults", func() {
It("should document default values via struct tags", func() {
// This test documents the expected default values
// The actual defaults are set by the config loader based on struct tags
cfg := &DNSSEC{}

// Document expected defaults (set by config loader)
expectedDefaults := map[string]interface{}{
"validate": false,
"maxChainDepth": uint(10),
"cacheExpirationHours": uint(1),
"maxNSEC3Iterations": uint(150),
"maxUpstreamQueries": uint(30),
"clockSkewToleranceSec": uint(3600),
}

// Verify struct has the expected fields
Expect(cfg).ShouldNot(BeNil())
_ = expectedDefaults // Document defaults
})
})
})
Loading
Loading