Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 1 addition & 4 deletions config/blocking.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@ func (c *Blocking) LogConfig(logger *logrus.Entry) {
}

logger.Infof("blockType = %s", c.BlockType)

if c.BlockType != "NXDOMAIN" {
logger.Infof("blockTTL = %s", c.BlockTTL)
}
logger.Infof("blockTTL = %s", c.BlockTTL)

logger.Info("loading:")
log.WithIndent(logger, " ", c.Loading.LogConfig)
Expand Down
38 changes: 22 additions & 16 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ All logging options are optional.

## Init Strategy

A couple of features use an "init/loading strategy" which configures behavior at Blocky startup.
A couple of features use an "init/loading strategy" which configures behavior at Blocky startup.
This applies to all of them. The default strategy is blocking.

| strategy | Description |
Expand Down Expand Up @@ -180,13 +180,13 @@ Blocky supports different upstream strategies (default `parallel_best`) that det

Currently available strategies:

- `parallel_best`: blocky picks 2 random (weighted) resolvers from the upstream group for each query and returns the answer from the fastest one.
If an upstream failed to answer within the last hour, it is less likely to be chosen for the race.
This improves your network speed and increases your privacy - your DNS traffic will be distributed over multiple providers.
- `parallel_best`: blocky picks 2 random (weighted) resolvers from the upstream group for each query and returns the answer from the fastest one.
If an upstream failed to answer within the last hour, it is less likely to be chosen for the race.
This improves your network speed and increases your privacy - your DNS traffic will be distributed over multiple providers.
(When using 10 upstream servers, each upstream will get on average 20% of the DNS requests)
- `random`: blocky picks one random (weighted) resolver from the upstream group for each query and if successful, returns its response.
If the selected resolver fails to respond, a second one is picked to which the query is sent.
The weighting is identical to the `parallel_best` strategy.
- `random`: blocky picks one random (weighted) resolver from the upstream group for each query and if successful, returns its response.
If the selected resolver fails to respond, a second one is picked to which the query is sent.
The weighting is identical to the `parallel_best` strategy.
Although the `random` strategy might be slower than the `parallel_best` strategy, it offers more privacy since each request is sent to a single upstream.
- `strict`: blocky forwards the request in a strict order. If the first upstream does not respond, the second is asked, and so on.

Expand Down Expand Up @@ -578,9 +578,15 @@ queries, NXDOMAIN for other types):
### Block TTL

TTL for answers to blocked domains can be set to customize the time (in **duration format**) clients ask for those
domains again. Default Block TTL is **6hours**. This setting only makes sense when `blockType` is set to `nxDomain` or
`zeroIP`, and will affect how much time it could take for a client to be able to see the real IP address for a domain
after receiving the custom value.
domains again. Default Block TTL is **6 hours**. This setting applies to all blocking modes and will affect how much
time it could take for a client to be able to see the real IP address for a domain after receiving the blocked response.

**For `zeroIP` and custom IP modes:** The TTL is applied to the returned A/AAAA records in the answer section.

**For `nxDomain` mode:** The TTL is applied to the SOA record in the authority section. Per [RFC 2308](https://www.rfc-editor.org/rfc/rfc2308),
Blocky includes an SOA record in NXDOMAIN responses to enable proper negative caching by stub resolvers.
The blockTTL value is used for both the SOA's TTL and its MINIMUM field, ensuring clients cache the
NXDOMAIN response for the configured duration.

!!! example

Expand Down Expand Up @@ -843,7 +849,7 @@ EDNS Client Subnet (ECS) configuration parameters:

## Special Use Domain Names

SUDN (Special Use Domain Names) are always enabled by default as they are required by various RFCs.
SUDN (Special Use Domain Names) are always enabled by default as they are required by various RFCs.
Some RFCs have optional recommendations, which are configurable as described below.
However, you can completely deactivate the blocking of SUDN by setting enable to false.
Warning! You should only disable this if your upstream DNS server is local, as it shouldn't be disabled for remote upstreams.
Expand Down Expand Up @@ -921,7 +927,7 @@ These settings apply only to the resolver under which they are nested.
#### Refresh / Reload

To keep source contents up-to-date, blocky can periodically refresh and reparse them. Default period is
**4 hours**. You can configure this by setting the `refreshPeriod` parameter to a value in **duration format**.
**4 hours**. You can configure this by setting the `refreshPeriod` parameter to a value in **duration format**.
A value of zero or less will disable this feature.

!!! example
Expand Down Expand Up @@ -958,7 +964,7 @@ Configures how HTTP(S) sources are downloaded:

### Strategy

See [Init Strategy](#init-strategy).
See [Init Strategy](#init-strategy).
In this context, "init" is loading and parsing each source, and an error is a single source failing to load/parse.

!!! example
Expand All @@ -970,7 +976,7 @@ In this context, "init" is loading and parsing each source, and an error is a si

### Max Errors per Source

Number of errors allowed when parsing a source before it is considered invalid and parsing stops.
Number of errors allowed when parsing a source before it is considered invalid and parsing stops.
A value of -1 disables the limit.

!!! example
Expand All @@ -982,8 +988,8 @@ A value of -1 disables the limit.

### Concurrency

Blocky downloads and processes sources concurrently. This allows limiting how many can be processed in the same time.
Larger values can reduce the overall list refresh time at the cost of using more RAM. Please consider reducing this value on systems with limited memory.
Blocky downloads and processes sources concurrently. This allows limiting how many can be processed in the same time.
Larger values can reduce the overall list refresh time at the cost of using more RAM. Please consider reducing this value on systems with limited memory.
Default value is 4.

!!! example
Expand Down
70 changes: 66 additions & 4 deletions e2e/blocking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,12 +409,33 @@ var _ = Describe("Domain blocking functionality", func() {
Expect(err).Should(Succeed())
})

It("returns NXDOMAIN for blocked domains", func(ctx context.Context) {
It("returns NXDOMAIN with SOA record for blocked domains", func(ctx context.Context) {
msg := util.NewMsgWithQuestion("blocked.com.", A)
resp, err := doDNSRequest(ctx, blocky, msg)
Expect(err).Should(Succeed())
Expect(resp.Rcode).Should(Equal(dns.RcodeNameError))
Expect(resp.Answer).Should(BeEmpty())

By("returning NXDOMAIN response code", func() {
Expect(resp.Rcode).Should(Equal(dns.RcodeNameError))
})

By("having no answer section", func() {
Expect(resp.Answer).Should(BeEmpty())
})

By("including SOA record in authority section per RFC 2308", func() {
Expect(resp.Ns).Should(HaveLen(1))

soa, ok := resp.Ns[0].(*dns.SOA)
Expect(ok).Should(BeTrue(), "Authority record should be SOA type")

// Verify SOA record fields
Expect(soa.Header().Name).Should(Equal("blocked.com."))
Expect(soa.Header().Rrtype).Should(Equal(dns.TypeSOA))
Expect(soa.Header().Ttl).Should(Equal(uint32(6 * 60 * 60))) // Default 6 hours
Expect(soa.Ns).Should(Equal("blocky.local."))
Expect(soa.Mbox).Should(Equal("hostmaster.blocky.local."))
Expect(soa.Minttl).Should(Equal(uint32(6 * 60 * 60))) // RFC 2308 negative caching TTL
})
})
})

Expand Down Expand Up @@ -459,7 +480,7 @@ var _ = Describe("Domain blocking functionality", func() {
})

Describe("Block TTL", func() {
Context("with custom blockTTL", func() {
Context("with custom blockTTL and zeroIP", func() {
BeforeEach(func(ctx context.Context) {
_, err = createHTTPServerContainer(ctx, "httpserver", e2eNet, "list.txt", "blocked.com")
Expect(err).Should(Succeed())
Expand Down Expand Up @@ -493,6 +514,47 @@ var _ = Describe("Domain blocking functionality", func() {
))
})
})

Context("with custom blockTTL and nxDomain", func() {
BeforeEach(func(ctx context.Context) {
_, err = createHTTPServerContainer(ctx, "httpserver", e2eNet, "list.txt", "blocked.com")
Expect(err).Should(Succeed())

blocky, err = createBlockyContainer(ctx, e2eNet,
"log:",
" level: warn",
"upstreams:",
" groups:",
" default:",
" - moka",
"blocking:",
" blockType: nxDomain",
" blockTTL: 2m",
" denylists:",
" ads:",
" - http://httpserver:8080/list.txt",
" clientGroupsBlock:",
" default:",
" - ads",
)
Expect(err).Should(Succeed())
})

It("uses the configured TTL in SOA record for NXDOMAIN responses", func(ctx context.Context) {
msg := util.NewMsgWithQuestion("blocked.com.", A)
resp, err := doDNSRequest(ctx, blocky, msg)
Expect(err).Should(Succeed())

Expect(resp.Rcode).Should(Equal(dns.RcodeNameError))
Expect(resp.Answer).Should(BeEmpty())
Expect(resp.Ns).Should(HaveLen(1))

soa, ok := resp.Ns[0].(*dns.SOA)
Expect(ok).Should(BeTrue(), "Authority record should be SOA type")
Expect(soa.Header().Ttl).Should(Equal(uint32(120))) // 2m = 120s
Expect(soa.Minttl).Should(Equal(uint32(120))) // RFC 2308 negative caching TTL
})
})
})

Describe("IP blocking", func() {
Expand Down
40 changes: 40 additions & 0 deletions helpertest/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,50 @@ func ToExtra(m *model.Response) []dns.RR {
return m.Res.Extra
}

func ToAuthority(m *model.Response) []dns.RR {
return m.Res.Ns
}

func HaveNoAnswer() types.GomegaMatcher {
return gomega.WithTransform(ToAnswer, gomega.BeEmpty())
}

func HaveAuthority() types.GomegaMatcher {
return gomega.WithTransform(ToAuthority, gomega.Not(gomega.BeEmpty()))
}

func HaveSOARecord(ttl, minTTL uint32) types.GomegaMatcher {
return gcustom.MakeMatcher(func(m *model.Response) (bool, error) {
if len(m.Res.Ns) == 0 {
return false, errors.New("no authority section records")
}

for _, rr := range m.Res.Ns {
if soa, ok := rr.(*dns.SOA); ok {
if soa.Header().Ttl != ttl {
return false, fmt.Errorf("SOA TTL is %d, expected %d", soa.Header().Ttl, ttl)
}

if soa.Minttl != minTTL {
return false, fmt.Errorf("SOA MINTTL is %d, expected %d", soa.Minttl, minTTL)
}

// Verify basic structure
if soa.Ns == "" || soa.Mbox == "" {
return false, errors.New("SOA record has empty nameserver or mailbox")
}

return true, nil
}
}

return false, errors.New("no SOA record found in authority section")
}).WithTemplate(
"Expected:\n{{.Actual}}\n{{.To}} have SOA record with TTL={{index .Data 0}} and MINTTL={{index .Data 1}}",
ttl, minTTL,
)
}

func HaveReason(reason string) types.GomegaMatcher {
return gcustom.MakeMatcher(func(m *model.Response) (bool, error) {
return m.Reason == reason, nil
Expand Down
17 changes: 12 additions & 5 deletions resolver/blocking_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ const defaultBlockingCleanUpInterval = 5 * time.Second

func createBlockHandler(cfg config.Blocking) (blockHandler, error) {
cfgBlockType := cfg.BlockType
blockTime := cfg.BlockTTL.SecondsU32()

if strings.EqualFold(cfgBlockType, "NXDOMAIN") {
return nxDomainBlockHandler{}, nil
return nxDomainBlockHandler{
BlockTimeSec: blockTime,
}, nil
}

blockTime := cfg.BlockTTL.SecondsU32()

if strings.EqualFold(cfgBlockType, "ZEROIP") {
return zeroIPBlockHandler{
BlockTimeSec: blockTime,
Expand Down Expand Up @@ -547,7 +548,9 @@ type zeroIPBlockHandler struct {
BlockTimeSec uint32
}

type nxDomainBlockHandler struct{}
type nxDomainBlockHandler struct {
BlockTimeSec uint32
}

type ipBlockHandler struct {
destinations []net.IP
Expand All @@ -574,8 +577,12 @@ func (b zeroIPBlockHandler) handleBlock(question dns.Question, response *dns.Msg
response.Answer = append(response.Answer, rr)
}

func (b nxDomainBlockHandler) handleBlock(_ dns.Question, response *dns.Msg) {
func (b nxDomainBlockHandler) handleBlock(question dns.Question, response *dns.Msg) {
response.Rcode = dns.RcodeNameError

// Add SOA to authority section per RFC 2308
soa := util.CreateSOAForNegativeResponse(question, b.BlockTimeSec)
response.Ns = []dns.RR{soa}
}

func (b ipBlockHandler) handleBlock(question dns.Question, response *dns.Msg) {
Expand Down
32 changes: 31 additions & 1 deletion resolver/blocking_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,14 +412,44 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
}
})

It("should return NXDOMAIN if query is blocked", func() {
It("should return NXDOMAIN with SOA if query is blocked", func() {
Expect(sut.Resolve(ctx, newRequestWithClient("blocked3.com.", A, "1.2.1.2", "unknown"))).
Should(
SatisfyAll(
HaveNoAnswer(),
HaveResponseType(ResponseTypeBLOCKED),
HaveReturnCode(dns.RcodeNameError),
HaveReason("BLOCKED (defaultGroup)"),
HaveAuthority(),
HaveSOARecord(60, 60), // 1 minute = 60 seconds
))
})
})

When("BlockType is NXDOMAIN with custom BlockTTL", func() {
BeforeEach(func() {
sutConfig = config.Blocking{
BlockType: "NxDomain",
BlockTTL: config.Duration(time.Hour * 2), // 2 hours = 7200 seconds
Denylists: map[string][]config.BytesSource{
"defaultGroup": config.NewBytesSources(defaultGroupFile.Path),
},
ClientGroupsBlock: map[string][]string{
"default": {"defaultGroup"},
},
}
})

It("should return NXDOMAIN with SOA containing custom TTL", func() {
Expect(sut.Resolve(ctx, newRequestWithClient("blocked3.com.", A, "1.2.1.2", "unknown"))).
Should(
SatisfyAll(
HaveNoAnswer(),
HaveResponseType(ResponseTypeBLOCKED),
HaveReturnCode(dns.RcodeNameError),
HaveReason("BLOCKED (defaultGroup)"),
HaveAuthority(),
HaveSOARecord(7200, 7200), // 2 hours in seconds
))
})
})
Expand Down
31 changes: 31 additions & 0 deletions util/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ var (
alphanumeric = regexp.MustCompile("[a-zA-Z0-9]")
)

// SOA record timing defaults for negative responses (RFC 2308)
const (
soaRefresh = 86400 // 24 hours
soaRetry = 7200 // 2 hours
soaExpire = 604800 // 7 days
)

// Obfuscate replaces all alphanumeric characters with * to obfuscate user sensitive data if LogPrivacy is enabled
func Obfuscate(in string) string {
if LogPrivacy.Load() {
Expand Down Expand Up @@ -106,6 +113,30 @@ func CreateHeader(question dns.Question, remainingTTL uint32) dns.RR_Header {
return dns.RR_Header{Name: question.Name, Rrtype: question.Qtype, Class: dns.ClassINET, Ttl: remainingTTL}
}

// CreateSOAForNegativeResponse creates an SOA record for NXDOMAIN responses
// per RFC 2308. The TTL and MINTTL are both set to blockTTL to ensure
// proper negative caching behavior.
func CreateSOAForNegativeResponse(question dns.Question, blockTTL uint32) *dns.SOA {
// Use the queried domain as the zone name
zoneName := dns.Fqdn(question.Name)

return &dns.SOA{
Hdr: dns.RR_Header{
Name: zoneName,
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: blockTTL,
},
Ns: "blocky.local.", // Name server
Mbox: "hostmaster.blocky.local.", // Mailbox (admin contact)
Serial: 1, // Serial number
Refresh: soaRefresh, // 24 hours
Retry: soaRetry, // 2 hours
Expire: soaExpire, // 7 days
Minttl: blockTTL, // Negative caching TTL (RFC 2308)
}
}

// ExtractDomain returns domain string from the question
func ExtractDomain(question dns.Question) string {
return ExtractDomainOnly(question.Name)
Expand Down
Loading