diff --git a/CHANGELOG.md b/CHANGELOG.md index 44fd70f0..0e20e7be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,35 @@ See also the [v0.107.65 GitHub milestone][ms-v0.107.65]. NOTE: Add new changes BELOW THIS COMMENT. --> +### Added + +- A separate checkbox in the Web UI to enable or disable the global DNS response cache without losing the configured cache size. + +- A new `"cache_enabled"` field to the HTTP API (`GET /control/dns_info` and `POST /control/dns_config`). See `openapi/openapi.yaml` for the full description. + +### Changed + +#### Configuration changes + +In this release, the schema version has changed from 29 to 30. + +- Added a new boolean field `dns.cache_enabled` to the configuration. This field explicitly controls whether DNS caching is enabled, replacing the previous implicit logic based on `dns.cache_size`. + + ```yaml + # BEFORE: + 'dns': + # … + 'cache_size': 123456 + + # AFTER: + 'dns': + # … + 'cache_enabled': true + 'cache_size': 123456 + ``` + + To roll back this change, set the schema_version back to `29`. + ### Fixed - Disabled state of Top clients action button in web UI ([#7923]). diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index e8e17250..cbf089f8 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -655,7 +655,10 @@ "safe_search": "Safe Search", "blocklist": "Blocklist", "milliseconds_abbreviation": "ms", + "cache_enabled": "Enable cache", + "cache_enabled_desc": "Store DNS responses locally.", "cache_size": "Cache size", + "cache_size_validation": "The cache size must be greater than zero when enabled.", "cache_size_desc": "DNS cache size (in bytes). To disable caching, set to 0.", "cache_ttl_min_override": "Override minimum TTL", "cache_ttl_max_override": "Override maximum TTL", diff --git a/client/src/components/Settings/Dns/Cache/Form.tsx b/client/src/components/Settings/Dns/Cache/Form.tsx index aea6a299..62323e2d 100644 --- a/client/src/components/Settings/Dns/Cache/Form.tsx +++ b/client/src/components/Settings/Dns/Cache/Form.tsx @@ -32,6 +32,7 @@ const INPUTS_FIELDS = [ ]; type FormData = { + cache_enabled: boolean; cache_size: number; cache_ttl_min: number; cache_ttl_max: number; @@ -54,10 +55,11 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => { handleSubmit, watch, control, - formState: { isSubmitting, isDirty }, + formState: { isSubmitting }, } = useForm({ mode: 'onBlur', defaultValues: { + cache_enabled: initialValues?.cache_enabled || false, cache_size: initialValues?.cache_size || 0, cache_ttl_min: initialValues?.cache_ttl_min || 0, cache_ttl_max: initialValues?.cache_ttl_max || 0, @@ -65,10 +67,13 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => { }, }); + const cache_enabled = watch('cache_enabled'); + const cache_size = watch('cache_size'); const cache_ttl_min = watch('cache_ttl_min'); const cache_ttl_max = watch('cache_ttl_max'); const minExceedsMax = cache_ttl_min > 0 && cache_ttl_max > 0 && cache_ttl_min > cache_ttl_max; + const cacheSizeZeroWhenEnabled = cache_enabled && cache_size === 0; const handleClearCache = () => { if (window.confirm(t('confirm_dns_cache_clear'))) { @@ -79,6 +84,24 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => { return (
+
+
+ ( + + )} + /> +
+
+ {INPUTS_FIELDS.map(({ name, title, description, placeholder }) => (
@@ -102,6 +125,12 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => { setValueAs: (value) => replaceZeroWithEmptyString(value), })} /> + + {name === CACHE_CONFIG_FIELDS.cache_size && cacheSizeZeroWhenEnabled && ( + + {t('cache_size_validation')} + + )}
@@ -133,7 +162,7 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => { type="submit" data-testid="dns_save" className="btn btn-success btn-standard btn-large" - disabled={isSubmitting || !isDirty || processingSetConfig || minExceedsMax}> + disabled={isSubmitting || processingSetConfig || minExceedsMax || cacheSizeZeroWhenEnabled}> {t('save_btn')} diff --git a/client/src/components/Settings/Dns/Cache/index.tsx b/client/src/components/Settings/Dns/Cache/index.tsx index 1eeff122..51c04c41 100644 --- a/client/src/components/Settings/Dns/Cache/index.tsx +++ b/client/src/components/Settings/Dns/Cache/index.tsx @@ -13,7 +13,7 @@ import { RootState } from '../../../../initialState'; const CacheConfig = () => { const { t } = useTranslation(); const dispatch = useDispatch(); - const { cache_size, cache_ttl_max, cache_ttl_min, cache_optimistic } = useSelector( + const { cache_enabled, cache_size, cache_ttl_max, cache_ttl_min, cache_optimistic } = useSelector( (state: RootState) => state.dnsConfig, shallowEqual, ); @@ -32,6 +32,7 @@ const CacheConfig = () => {
0 + + return nil +} diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 123174ba..a2dfeefe 100644 --- a/internal/dnsforward/config.go +++ b/internal/dnsforward/config.go @@ -27,6 +27,7 @@ import ( "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/timeutil" + "github.com/AdguardTeam/golibs/validate" "github.com/ameshkov/dnscrypt/v2" ) @@ -104,6 +105,9 @@ type Config struct { // DNS cache settings + // CacheEnabled defines if the DNS cache should be used. + CacheEnabled bool `yaml:"cache_enabled"` + // CacheSize is the DNS cache size (in bytes). CacheSize uint32 `yaml:"cache_size"` @@ -369,6 +373,7 @@ func (s *Server) newProxyConfig(ctx context.Context) (conf *proxy.Config, err er } conf, err = prepareCacheConfig(conf, + srvConf.CacheEnabled, srvConf.CacheSize, srvConf.CacheMinTTL, srvConf.CacheMaxTTL, @@ -385,13 +390,20 @@ func (s *Server) newProxyConfig(ctx context.Context) (conf *proxy.Config, err er // there is one. func prepareCacheConfig( conf *proxy.Config, + isEnabled bool, size uint32, minTTL uint32, maxTTL uint32, ) (prepared *proxy.Config, err error) { - if size != 0 { + if isEnabled { + cacheSize := int(size) + err = validate.Positive("cache_size", cacheSize) + if err != nil { + return nil, fmt.Errorf("cache_enabled is true: %w", err) + } + conf.CacheEnabled = true - conf.CacheSizeBytes = int(size) + conf.CacheSizeBytes = cacheSize } err = validateCacheTTL(minTTL, maxTTL) diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go index 1612081f..572a6247 100644 --- a/internal/dnsforward/http.go +++ b/internal/dnsforward/http.go @@ -93,6 +93,9 @@ type jsonDNSConfig struct { // CacheMaxTTL is custom maximum TTL for cached DNS responses. CacheMaxTTL *uint32 `json:"cache_ttl_max"` + // CacheEnabled defines if the DNS cache should be used. + CacheEnabled *bool `json:"cache_enabled"` + // CacheOptimistic defines if expired entries should be served. CacheOptimistic *bool `json:"cache_optimistic"` @@ -162,6 +165,7 @@ func (s *Server) getDNSConfig(ctx context.Context) (c *jsonDNSConfig) { enableDNSSEC := s.conf.EnableDNSSEC aaaaDisabled := s.conf.AAAADisabled + cacheEnabled := s.conf.CacheEnabled cacheSize := s.conf.CacheSize cacheMinTTL := s.conf.CacheMinTTL cacheMaxTTL := s.conf.CacheMaxTTL @@ -207,6 +211,7 @@ func (s *Server) getDNSConfig(ctx context.Context) (c *jsonDNSConfig) { DNSSECEnabled: &enableDNSSEC, DisableIPv6: &aaaaDisabled, BlockedResponseTTL: &blockedResponseTTL, + CacheEnabled: &cacheEnabled, CacheSize: &cacheSize, CacheMinTTL: &cacheMinTTL, CacheMaxTTL: &cacheMaxTTL, @@ -278,6 +283,7 @@ func (req *jsonDNSConfig) validate( ownAddrs addrPortSet, sysResolvers SystemResolvers, privateNets netutil.SubnetSet, + curCacheSize uint32, ) (err error) { defer func() { err = errors.Annotate(err, "validating dns config: %w") }() @@ -305,7 +311,7 @@ func (req *jsonDNSConfig) validate( return err } - err = req.checkCacheTTL() + err = req.validateCacheSettings(curCacheSize) if err != nil { // Don't wrap the error since it's informative enough as is. return err @@ -421,9 +427,14 @@ func (req *jsonDNSConfig) validateUpstreamDNSServers( return nil } -// checkCacheTTL returns an error if the configuration of the cache TTL is -// invalid. -func (req *jsonDNSConfig) checkCacheTTL() (err error) { +// validateCacheSettings returns an error if the cache configuration is invalid. +func (req *jsonDNSConfig) validateCacheSettings(curCacheSize uint32) (err error) { + err = req.validateCacheSize(curCacheSize) + if err != nil { + // Don't wrap the error because it's informative enough as is. + return err + } + if req.CacheMinTTL == nil && req.CacheMaxTTL == nil { return nil } @@ -440,6 +451,28 @@ func (req *jsonDNSConfig) checkCacheTTL() (err error) { return validateCacheTTL(minTTL, maxTTL) } +// validateCacheSize returns an error if the cache size configuration is +// invalid. It also explicitly sets CacheEnabled to support legacy behavior. +func (req *jsonDNSConfig) validateCacheSize(curCacheSize uint32) (err error) { + if req.CacheEnabled != nil && *req.CacheEnabled { + size := curCacheSize + if req.CacheSize != nil { + size = *req.CacheSize + } + + if size == 0 { + return errors.Error("cache_size must be greater than zero when cache_enabled is true") + } + } + + if req.CacheEnabled == nil && req.CacheSize != nil { + isEnabled := *req.CacheSize > 0 + req.CacheEnabled = &isEnabled + } + + return nil +} + // checkRatelimitSubnetMaskLen returns an error if the length of the subnet mask // for IPv4 or IPv6 addresses is invalid. func (req *jsonDNSConfig) checkRatelimitSubnetMaskLen() (err error) { @@ -505,7 +538,7 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) { return } - err = req.validate(ourAddrs, s.sysResolvers, s.privateNets) + err = req.validate(ourAddrs, s.sysResolvers, s.privateNets, s.conf.CacheSize) if err != nil { aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) @@ -598,6 +631,7 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) { setIfNotNil(&s.conf.FallbackDNS, dc.Fallbacks), setIfNotNil(&s.conf.EDNSClientSubnet.Enabled, dc.EDNSCSEnabled), setIfNotNil(&s.conf.EDNSClientSubnet.UseCustom, dc.EDNSCSUseCustom), + setIfNotNil(&s.conf.CacheEnabled, dc.CacheEnabled), setIfNotNil(&s.conf.CacheSize, dc.CacheSize), setIfNotNil(&s.conf.CacheMinTTL, dc.CacheMinTTL), setIfNotNil(&s.conf.CacheMaxTTL, dc.CacheMaxTTL), diff --git a/internal/dnsforward/http_internal_test.go b/internal/dnsforward/http_internal_test.go index 47272adb..1353bfdc 100644 --- a/internal/dnsforward/http_internal_test.go +++ b/internal/dnsforward/http_internal_test.go @@ -229,6 +229,9 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) { }, { name: "cache_size", wantSet: "", + }, { + name: "cache_enabled", + wantSet: "", }, { name: "upstream_mode_parallel", wantSet: "", diff --git a/internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json b/internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json index d4e76a20..9a3e4750 100644 --- a/internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json +++ b/internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json @@ -32,6 +32,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -72,6 +73,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -112,6 +114,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, diff --git a/internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json b/internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json index d0967ece..37e25945 100644 --- a/internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json +++ b/internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json @@ -37,6 +37,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -79,6 +80,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -122,6 +124,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -165,6 +168,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -208,6 +212,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -253,6 +258,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -299,6 +305,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -342,6 +349,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -387,6 +395,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -432,6 +441,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -475,6 +485,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -518,6 +529,52 @@ "cache_size": 1024, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": true, + "cache_optimistic": false, + "resolve_clients": false, + "use_private_ptr_resolvers": false, + "local_ptr_upstreams": [], + "edns_cs_use_custom": false, + "edns_cs_custom_ip": "" + } + }, + "cache_enabled": { + "req": { + "cache_enabled": true, + "cache_size": 1024 + }, + "want": { + "upstream_dns": [ + "8.8.8.8:53", + "8.8.4.4:53" + ], + "upstream_dns_file": "", + "bootstrap_dns": [ + "9.9.9.10", + "149.112.112.10", + "2620:fe::10", + "2620:fe::fe:10" + ], + "fallback_dns": [], + "protection_enabled": true, + "protection_disabled_until": null, + "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], + "blocking_mode": "default", + "blocking_ipv4": "", + "blocking_ipv6": "", + "blocked_response_ttl": 10, + "upstream_timeout": 10, + "edns_cs_enabled": false, + "dnssec_enabled": false, + "disable_ipv6": false, + "upstream_mode": "", + "cache_size": 1024, + "cache_ttl_min": 0, + "cache_ttl_max": 0, + "cache_enabled": true, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -561,6 +618,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -604,6 +662,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -649,6 +708,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -694,6 +754,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -738,6 +799,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -781,6 +843,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -826,6 +889,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -874,6 +938,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -917,6 +982,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -964,6 +1030,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -1007,6 +1074,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -1053,6 +1121,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, @@ -1096,6 +1165,7 @@ "cache_size": 0, "cache_ttl_min": 0, "cache_ttl_max": 0, + "cache_enabled": false, "cache_optimistic": false, "resolve_clients": false, "use_private_ptr_resolvers": false, diff --git a/internal/home/config.go b/internal/home/config.go index 607f14ee..65452f8f 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -466,7 +466,8 @@ var config = &configuration{ }, { Prefix: netip.MustParsePrefix("::1/128"), }}, - CacheSize: 4 * 1024 * 1024, + CacheEnabled: true, + CacheSize: 4 * 1024 * 1024, EDNSClientSubnet: &dnsforward.EDNSClientSubnet{ CustomIP: netip.Addr{}, diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index 20132c42..06d28e82 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -4,6 +4,10 @@ ## v0.108.0: API changes +## v0.107.64: API changes + +- The new field `"cache_enabled"` in `GET /control/dns_info` and `POST /control/dns_config`. Setting this flag to true turns the DNS-response cache on and requires a positive `cache_size` value (or a positive `dns.cache_size` in the configuration file). + ## v0.107.58: API changes ### The ability to check rules for query types and/or clients: GET /control/check_host diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 77315d41..342e67bc 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1559,6 +1559,14 @@ 'type': 'integer' 'cache_ttl_max': 'type': 'integer' + 'cache_enabled': + 'type': 'boolean' + 'description': | + Enables or disables the DNS response cache. + + If `cache_enabled` is `true`, the companion field `cache_size` must + be present and greater than 0, or the `dns.cache_size` setting in + the configuration file must already be greater than 0. 'cache_optimistic': 'type': 'boolean' 'upstream_mode':