diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 5a8beee8..b1303621 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -888,6 +888,7 @@ Response: "blocking_ipv4": "1.2.3.4", "blocking_ipv6": "1:2:3::4", "edns_cs_enabled": true | false, + "dnssec_enabled": true | false "disable_ipv6": true | false, } @@ -905,6 +906,7 @@ Request: "blocking_ipv4": "1.2.3.4", "blocking_ipv6": "1:2:3::4", "edns_cs_enabled": true | false, + "dnssec_enabled": true | false "disable_ipv6": true | false, } @@ -1254,6 +1256,7 @@ Response: } ... ], + "answer_dnssec": true, "client":"127.0.0.1", "elapsedMs":"0.098403", "filterId":1, diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 5791f6b4..a4f2293e 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -479,5 +479,7 @@ "install_static_configure": "We have detected that a dynamic IP address is used — <0>{{ip}}. Do you want to use it as your static address?", "confirm_static_ip": "AdGuard Home will configure {{ip}} to be your static IP address. Do you want to proceed?", "list_updated": "{{count}} list updated", - "list_updated_plural": "{{count}} lists updated" -} \ No newline at end of file + "list_updated_plural": "{{count}} lists updated", + "dnssec_enable": "Enable DNSSEC", + "dnssec_enable_desc": "Set DNSSEC flag in the outcoming DNS queries and check the result (DNSSEC-enabled resolver is required)" +} diff --git a/client/src/components/Settings/Dns/Config/Form.js b/client/src/components/Settings/Dns/Config/Form.js index 580f09d4..1db400e8 100644 --- a/client/src/components/Settings/Dns/Config/Form.js +++ b/client/src/components/Settings/Dns/Config/Form.js @@ -65,6 +65,18 @@ let Form = ({ /> +
+
+ +
+
{ blocking_ipv4, blocking_ipv6, edns_cs_enabled, + dnssec_enabled, disable_ipv6, processingSetConfig, } = dnsConfig; @@ -35,6 +36,7 @@ const Config = ({ t, dnsConfig, setDnsConfig }) => { blocking_ipv6, edns_cs_enabled, disable_ipv6, + dnssec_enabled, }} onSubmit={handleFormSubmit} processing={processingSetConfig} diff --git a/client/src/reducers/dnsConfig.js b/client/src/reducers/dnsConfig.js index 87f59d36..ad45e631 100644 --- a/client/src/reducers/dnsConfig.js +++ b/client/src/reducers/dnsConfig.js @@ -45,6 +45,7 @@ const dnsConfig = handleActions( blocking_ipv6: DEFAULT_BLOCKING_IPV6, edns_cs_enabled: false, disable_ipv6: false, + dnssec_enabled: false, }, ); diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go index 64949ce1..646f61ff 100644 --- a/dnsforward/dnsforward.go +++ b/dnsforward/dnsforward.go @@ -136,6 +136,8 @@ type FilteringConfig struct { EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option + EnableDNSSEC bool `yaml:"enable_dnssec"` // Set DNSSEC flag in outcoming DNS request + // Respond with an empty answer to all AAAA requests AAAADisabled bool `yaml:"aaaa_disabled"` @@ -512,6 +514,7 @@ type dnsContext struct { err error // error returned from the module protectionEnabled bool // filtering is enabled, dnsfilter object is ready responseFromUpstream bool // response is received from upstream servers + origReqDNSSEC bool // DNSSEC flag in the original request from user } const ( @@ -588,6 +591,18 @@ func processUpstream(ctx *dnsContext) int { } } + if s.conf.EnableDNSSEC { + opt := d.Req.IsEdns0() + if opt == nil { + log.Debug("DNS: Adding OPT record with DNSSEC flag") + d.Req.SetEdns0(4096, true) + } else if !opt.Do() { + opt.SetDo(true) + } else { + ctx.origReqDNSSEC = true + } + } + // request was not filtered so let it be processed further err := s.dnsProxy.Resolve(d) if err != nil { @@ -599,6 +614,50 @@ func processUpstream(ctx *dnsContext) int { return resultDone } +// Process DNSSEC after response from upstream server +func processDNSSECAfterResponse(ctx *dnsContext) int { + d := ctx.proxyCtx + + if !ctx.responseFromUpstream || // don't process response if it's not from upstream servers + !ctx.srv.conf.EnableDNSSEC { + return resultDone + } + + optResp := d.Res.IsEdns0() + if !ctx.origReqDNSSEC && optResp != nil && optResp.Do() { + return resultDone + } + + // Remove RRSIG records from response + // because there is no DO flag in the original request from client, + // but we have EnableDNSSEC set, so we have set DO flag ourselves, + // and now we have to clean up the DNS records our client didn't ask for. + + answers := []dns.RR{} + for _, a := range d.Res.Answer { + switch a.(type) { + case *dns.RRSIG: + log.Debug("Removing RRSIG record from response: %v", a) + default: + answers = append(answers, a) + } + } + d.Res.Answer = answers + + answers = []dns.RR{} + for _, a := range d.Res.Ns { + switch a.(type) { + case *dns.RRSIG: + log.Debug("Removing RRSIG record from response: %v", a) + default: + answers = append(answers, a) + } + } + d.Res.Ns = answers + + return resultDone +} + // Apply filtering logic after we have received response from upstream servers func processFilteringAfterResponse(ctx *dnsContext) int { s := ctx.srv @@ -606,10 +665,6 @@ func processFilteringAfterResponse(ctx *dnsContext) int { res := ctx.result var err error - if !ctx.responseFromUpstream { - return resultDone // don't process response if it's not from upstream servers - } - if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 { d.Req.Question[0] = ctx.origQuestion d.Res.Question[0] = ctx.origQuestion @@ -688,14 +743,19 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error { processInitial, processFilteringBeforeRequest, processUpstream, + processDNSSECAfterResponse, processFilteringAfterResponse, processQueryLogsAndStats, } for _, process := range mods { r := process(ctx) switch r { + case resultDone: + // continue: call the next filter + case resultFinish: return nil + case resultError: return ctx.err } diff --git a/dnsforward/dnsforward_http.go b/dnsforward/dnsforward_http.go index 111fefbd..414b728b 100644 --- a/dnsforward/dnsforward_http.go +++ b/dnsforward/dnsforward_http.go @@ -28,6 +28,7 @@ type dnsConfigJSON struct { BlockingIPv4 string `json:"blocking_ipv4"` BlockingIPv6 string `json:"blocking_ipv6"` EDNSCSEnabled bool `json:"edns_cs_enabled"` + DNSSECEnabled bool `json:"dnssec_enabled"` DisableIPv6 bool `json:"disable_ipv6"` } @@ -40,6 +41,7 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { resp.BlockingIPv6 = s.conf.BlockingIPv6 resp.RateLimit = s.conf.Ratelimit resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet + resp.DNSSECEnabled = s.conf.EnableDNSSEC resp.DisableIPv6 = s.conf.AAAADisabled s.RUnlock() @@ -119,6 +121,10 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) { restart = true } + if js.Exists("dnssec_enabled") { + s.conf.EnableDNSSEC = req.DNSSECEnabled + } + if js.Exists("disable_ipv6") { s.conf.AAAADisabled = req.DisableIPv6 } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index d2ee5a8b..e597786f 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1110,6 +1110,8 @@ definitions: type: "string" edns_cs_enabled: type: "boolean" + dnssec_enabled: + type: "boolean" UpstreamsConfig: type: "object" @@ -1517,6 +1519,8 @@ definitions: description: "Answer from upstream server (optional)" items: $ref: "#/definitions/DnsAnswer" + answer_dnssec: + type: "boolean" client: type: "string" example: "192.168.0.1" diff --git a/querylog/qlog.go b/querylog/qlog.go index 55020612..21d5a1a6 100644 --- a/querylog/qlog.go +++ b/querylog/qlog.go @@ -286,7 +286,15 @@ func logEntryToJSONEntry(entry *logEntry) map[string]interface{} { if msg != nil { jsonEntry["status"] = dns.RcodeToString[msg.Rcode] + + opt := msg.IsEdns0() + dnssecOk := false + if opt != nil { + dnssecOk = opt.Do() + } + jsonEntry["answer_dnssec"] = dnssecOk } + if len(entry.Result.Rule) > 0 { jsonEntry["rule"] = entry.Result.Rule jsonEntry["filterId"] = entry.Result.FilterID