diff --git a/.gitignore b/.gitignore index 1bcd1cfa..b6212b82 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ todo.txt !docs/config.yml node_modules package-lock.json +.vscode/ \ No newline at end of file diff --git a/config/config.go b/config/config.go index 2e203d0e..598d4869 100644 --- a/config/config.go +++ b/config/config.go @@ -428,6 +428,7 @@ type Config struct { HostsFile HostsFileConfig `yaml:"hostsFile"` FqdnOnly bool `yaml:"fqdnOnly" default:"false"` Filtering FilteringConfig `yaml:"filtering"` + Ede EdeConfig `yaml:"ede"` } type BootstrapConfig bootstrapConfig // to avoid infinite recursion. See BootstrapConfig.UnmarshalYAML. @@ -540,6 +541,10 @@ type FilteringConfig struct { QueryTypes QTypeSet `yaml:"queryTypes"` } +type EdeConfig struct { + Enable bool `yaml:"enable" default:"false"` +} + // nolint:gochecknoglobals var ( config = &Config{} diff --git a/docs/config.yml b/docs/config.yml index 7c9878dc..3543d5f3 100644 --- a/docs/config.yml +++ b/docs/config.yml @@ -218,3 +218,8 @@ logFormat: text logTimestamp: true # optional: obfuscate log output (replace all alphanumeric characters with *) for user sensitive data like request domains or responses to increase privacy. Default: false logPrivacy: false + +# optional: add EDE error codes to dns response +ede: + # enabled if true, Default: false + enable: true \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md index 9a8fd329..3368e279 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -633,6 +633,23 @@ Configuration parameters: refreshPeriod: 30m ``` +### Deliver EDE codes as EDNS0 option + +DNS responses can be extended with EDE codes according to [RFC8914](https://datatracker.ietf.org/doc/rfc8914/). + +Configuration parameters: + +| Parameter | Type | Mandatory | Default value | Description | +|--------------------------|--------------------------------|-----------|---------------|----------------------------------------------------| +| ede.enable | bool | no | false | If true, DNS responses are deliverd with EDE codes | + +!!! example + + ```yaml + ede: + enable: true + ``` + ## SSL certificate configuration (DoH / TLS listener) See [Wiki - Configuration of HTTPS](https://github.com/0xERR0R/blocky/wiki/Configuration-of-HTTPS-for-DoH-and-Rest-API) diff --git a/resolver/ede_resolver.go b/resolver/ede_resolver.go new file mode 100644 index 00000000..affe4488 --- /dev/null +++ b/resolver/ede_resolver.go @@ -0,0 +1,80 @@ +package resolver + +import ( + "github.com/0xERR0R/blocky/config" + "github.com/0xERR0R/blocky/model" + "github.com/miekg/dns" +) + +type EdeResolver struct { + NextResolver + config config.EdeConfig +} + +func NewEdeResolver(cfg config.EdeConfig) ChainedResolver { + return &EdeResolver{ + config: cfg, + } +} + +func (r *EdeResolver) Resolve(request *model.Request) (*model.Response, error) { + resp, err := r.next.Resolve(request) + + if r.config.Enable { + addExtraReasoning(resp) + } + + return resp, err +} + +func (r *EdeResolver) Configuration() (result []string) { + if r.config.Enable { + result = []string{"activated"} + } else { + result = []string{"deactivated"} + } + + return result +} + +func addExtraReasoning(res *model.Response) { + // dns.ExtendedErrorCodeOther seams broken in some clients + infocode := convertToExtendedErrorCode(res.RType) + if infocode > 0 { + opt := new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + opt.Option = append(opt.Option, convertExtendedError(res, infocode)) + res.Res.Extra = append(res.Res.Extra, opt) + } +} + +func convertExtendedError(input *model.Response, infocode uint16) *dns.EDNS0_EDE { + return &dns.EDNS0_EDE{ + InfoCode: infocode, + ExtraText: input.Reason, + } +} + +func convertToExtendedErrorCode(input model.ResponseType) uint16 { + switch input { + case model.ResponseTypeRESOLVED: + return dns.ExtendedErrorCodeOther + case model.ResponseTypeCACHED: + return dns.ExtendedErrorCodeCachedError + case model.ResponseTypeCONDITIONAL: + return dns.ExtendedErrorCodeForgedAnswer + case model.ResponseTypeCUSTOMDNS: + return dns.ExtendedErrorCodeForgedAnswer + case model.ResponseTypeHOSTSFILE: + return dns.ExtendedErrorCodeForgedAnswer + case model.ResponseTypeNOTFQDN: + return dns.ExtendedErrorCodeBlocked + case model.ResponseTypeBLOCKED: + return dns.ExtendedErrorCodeBlocked + case model.ResponseTypeFILTERED: + return dns.ExtendedErrorCodeFiltered + default: + return dns.ExtendedErrorCodeOther + } +} diff --git a/resolver/ede_resolver_test.go b/resolver/ede_resolver_test.go new file mode 100644 index 00000000..ab0ba098 --- /dev/null +++ b/resolver/ede_resolver_test.go @@ -0,0 +1,87 @@ +package resolver + +import ( + "github.com/0xERR0R/blocky/config" + "github.com/0xERR0R/blocky/model" + + "github.com/miekg/dns" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" +) + +var _ = Describe("EdeResolver", func() { + var ( + sut *EdeResolver + sutConfig config.EdeConfig + m *MockResolver + mockAnswer *dns.Msg + ) + + BeforeEach(func() { + mockAnswer = new(dns.Msg) + }) + + JustBeforeEach(func() { + m = &MockResolver{} + m.On("Resolve", mock.Anything).Return(&model.Response{ + Res: mockAnswer, + RType: model.ResponseTypeCUSTOMDNS, + Reason: "Test", + }, nil) + + sut = NewEdeResolver(sutConfig).(*EdeResolver) + sut.Next(m) + }) + + When("Ede is disabled", func() { + BeforeEach(func() { + sutConfig = config.EdeConfig{ + Enable: false, + } + }) + It("Shouldn't add EDE information", func() { + resp, err := sut.Resolve(newRequest("example.com", dns.Type(dns.TypeA))) + Expect(err).Should(Succeed()) + Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess)) + Expect(resp.RType).Should(Equal(model.ResponseTypeCUSTOMDNS)) + Expect(resp.Res.Answer).Should(BeEmpty()) + Expect(resp.Res.Extra).Should(BeEmpty()) + + // delegated to next resolver + Expect(m.Calls).Should(HaveLen(1)) + }) + It("Configure should output deactivated", func() { + c := sut.Configuration() + Expect(c).Should(HaveLen(1)) + Expect(c[0]).Should(Equal("deactivated")) + }) + }) + When("Ede is enabled", func() { + BeforeEach(func() { + sutConfig = config.EdeConfig{ + Enable: true, + } + }) + It("Should add EDE information", func() { + resp, err := sut.Resolve(newRequest("example.com", dns.Type(dns.TypeA))) + Expect(err).Should(Succeed()) + Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess)) + Expect(resp.RType).Should(Equal(model.ResponseTypeCUSTOMDNS)) + Expect(resp.Res.Answer).Should(BeEmpty()) + Expect(resp.Res.Extra).Should(HaveLen(1)) + opt, ok := resp.Res.Extra[0].(*dns.OPT) + Expect(ok).Should(BeTrue()) + Expect(opt).ShouldNot(BeNil()) + ede, ok := opt.Option[0].(*dns.EDNS0_EDE) + Expect(ok).Should(BeTrue()) + Expect(ede.InfoCode).Should(Equal(dns.ExtendedErrorCodeForgedAnswer)) + Expect(ede.ExtraText).Should(Equal("Test")) + }) + It("Configure should output activated", func() { + c := sut.Configuration() + Expect(c).Should(HaveLen(1)) + Expect(c[0]).Should(Equal("activated")) + }) + }) +}) diff --git a/server/server.go b/server/server.go index 2a35f304..6bbc7287 100644 --- a/server/server.go +++ b/server/server.go @@ -399,6 +399,7 @@ func createQueryResolver( resolver.NewFilteringResolver(cfg.Filtering), resolver.NewFqdnOnlyResolver(*cfg), clientNamesResolver, + resolver.NewEdeResolver(cfg.Ede), resolver.NewQueryLoggingResolver(cfg.QueryLog), resolver.NewMetricsResolver(cfg.Prometheus), resolver.NewRewriterResolver(cfg.CustomDNS.RewriteConfig, resolver.NewCustomDNSResolver(cfg.CustomDNS)),