mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-15 18:08:30 -07:00
Pull request 2065: 6369-ratelimit-settings-ui
Closes #6369.
Co-authored-by: IldarKamalov <ik@adguard.com>
Squashed commit of the following:
commit efc824667a88765d5a16984fd17ecda2559f2b1e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Wed Nov 15 19:10:47 2023 +0300
all: imp docs
commit 9ec59b59000f005006ea231071329a586d9889ac
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Wed Nov 15 17:21:03 2023 +0300
dnsforward: imp err msg
commit d9710dfc1dcf74d5ee8386b053d7180316f21bce
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Wed Nov 15 15:33:59 2023 +0300
all: upd chlog
commit 29e868b93b15cfce5faed4d0c07b16decbce52f9
Merge: 1c3aec9f1 ebb06a583
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Wed Nov 15 15:26:32 2023 +0300
Merge branch 'master' into 6369-ratelimit-settings-ui
commit 1c3aec9f1478f71afa4d0aa9ba1c454e9d98b8db
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Tue Nov 14 21:21:22 2023 +0300
dnsforward: imp docs
commit 486bf86e5a2b51b6014a231386337a2d1e945c23
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Nov 13 09:57:21 2023 +0300
fix linter
commit aec088f233737fdfa0e7086148ceb79df0d2e39a
Author: Ildar Kamalov <ik@adguard.com>
Date: Sun Nov 12 16:13:46 2023 +0300
client: validate rate limit subnets
commit d4ca4d3a604295cdfaae54e6e461981233eabf3e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Fri Nov 10 20:08:44 2023 +0300
dnsforward: imp code
commit 5c11a1ef5c6fcc786d8496b14b9b16d1de1708cd
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Fri Nov 10 15:07:56 2023 +0300
all: ratelimit settings
This commit is contained in:
parent
ebb06a5831
commit
94bceaa84d
@ -23,12 +23,17 @@ See also the [v0.107.42 GitHub milestone][ms-v0.107.42].
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
### Added
|
||||
|
||||
- Ability to specify rate limiting settings in the Web UI ([#6369]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pre-filling the New static lease window with data ([#6402]).
|
||||
- Protection pause timer synchronization ([#5759]).
|
||||
|
||||
[#5759]: https://github.com/AdguardTeam/AdGuardHome/issues/5759
|
||||
[#6369]: https://github.com/AdguardTeam/AdGuardHome/issues/6369
|
||||
[#6402]: https://github.com/AdguardTeam/AdGuardHome/issues/6402
|
||||
|
||||
<!--
|
||||
|
@ -310,6 +310,16 @@
|
||||
"edns_use_custom_ip": "Use custom IP for EDNS",
|
||||
"edns_use_custom_ip_desc": "Allow to use custom IP for EDNS",
|
||||
"rate_limit_desc": "The number of requests per second allowed per client. Setting it to 0 means no limit.",
|
||||
"rate_limit_subnet_len_ipv4": "Subnet prefix length for IPv4 addresses",
|
||||
"rate_limit_subnet_len_ipv4_desc": "Subnet prefix length for IPv4 addresses used for rate limiting. The default is 24",
|
||||
"rate_limit_subnet_len_ipv4_error": "The IPv4 subnet prefix length should be between 0 and 32",
|
||||
"rate_limit_subnet_len_ipv6": "Subnet prefix length for IPv6 addresses",
|
||||
"rate_limit_subnet_len_ipv6_desc": "Subnet prefix length for IPv6 addresses used for rate limiting. The default is 56",
|
||||
"rate_limit_subnet_len_ipv6_error": "The IPv6 subnet prefix length should be between 0 and 128",
|
||||
"form_enter_rate_limit_subnet_len": "Enter subnet prefix length for rate limiting",
|
||||
"rate_limit_whitelist": "Rate limiting allowlist",
|
||||
"rate_limit_whitelist_desc": "IP addresses excluded from rate limiting",
|
||||
"rate_limit_whitelist_placeholder": "Enter one IP address per line",
|
||||
"blocking_ipv4_desc": "IP address to be returned for a blocked A request",
|
||||
"blocking_ipv6_desc": "IP address to be returned for a blocked AAAA request",
|
||||
"blocking_mode_default": "Default: Respond with zero IP address (0.0.0.0 for A; :: for AAAA) when blocked by Adblock-style rule; respond with the IP address specified in the rule when blocked by /etc/hosts-style rule",
|
||||
|
@ -62,6 +62,10 @@ export const setDnsConfig = (config) => async (dispatch) => {
|
||||
data.upstream_dns = splitByNewLine(config.upstream_dns);
|
||||
hasDnsSettings = true;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'ratelimit_whitelist')) {
|
||||
data.ratelimit_whitelist = splitByNewLine(config.ratelimit_whitelist);
|
||||
hasDnsSettings = true;
|
||||
}
|
||||
|
||||
await apiClient.setDnsConfig(data);
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
import {
|
||||
renderInputField,
|
||||
renderRadioField,
|
||||
renderTextareaField,
|
||||
CheckboxField,
|
||||
toNumber,
|
||||
} from '../../../../helpers/form';
|
||||
@ -14,7 +15,10 @@ import {
|
||||
validateIpv6,
|
||||
validateRequiredValue,
|
||||
validateIp,
|
||||
validateIPv4Subnet,
|
||||
validateIPv6Subnet,
|
||||
} from '../../../../helpers/validators';
|
||||
import { removeEmptyLines } from '../../../../helpers/helpers';
|
||||
import { BLOCKING_MODES, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
|
||||
|
||||
const checkboxes = [
|
||||
@ -90,6 +94,69 @@ const Form = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit_subnet_len_ipv4"
|
||||
className="form__label form__label--with-desc">
|
||||
<Trans>rate_limit_subnet_len_ipv4</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>rate_limit_subnet_len_ipv4_desc</Trans>
|
||||
</div>
|
||||
<Field
|
||||
name="ratelimit_subnet_len_ipv4"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_rate_limit_subnet_len')}
|
||||
normalize={toNumber}
|
||||
validate={[validateRequiredValue, validateIPv4Subnet]}
|
||||
min={0}
|
||||
max={32}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit_subnet_len_ipv6"
|
||||
className="form__label form__label--with-desc">
|
||||
<Trans>rate_limit_subnet_len_ipv6</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>rate_limit_subnet_len_ipv6_desc</Trans>
|
||||
</div>
|
||||
<Field
|
||||
name="ratelimit_subnet_len_ipv6"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_rate_limit_subnet_len')}
|
||||
normalize={toNumber}
|
||||
validate={[validateRequiredValue, validateIPv6Subnet]}
|
||||
min={0}
|
||||
max={128}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit_whitelist"
|
||||
className="form__label form__label--with-desc">
|
||||
<Trans>rate_limit_whitelist</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>rate_limit_whitelist_desc</Trans>
|
||||
</div>
|
||||
<Field
|
||||
name="ratelimit_whitelist"
|
||||
component={renderTextareaField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('rate_limit_whitelist_placeholder')}
|
||||
normalizeOnBlur={removeEmptyLines}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
|
@ -11,6 +11,9 @@ const Config = () => {
|
||||
const {
|
||||
blocking_mode,
|
||||
ratelimit,
|
||||
ratelimit_subnet_len_ipv4,
|
||||
ratelimit_subnet_len_ipv6,
|
||||
ratelimit_whitelist,
|
||||
blocking_ipv4,
|
||||
blocking_ipv6,
|
||||
blocked_response_ttl,
|
||||
@ -36,6 +39,9 @@ const Config = () => {
|
||||
<Form
|
||||
initialValues={{
|
||||
ratelimit,
|
||||
ratelimit_subnet_len_ipv4,
|
||||
ratelimit_subnet_len_ipv6,
|
||||
ratelimit_whitelist,
|
||||
blocking_mode,
|
||||
blocking_ipv4,
|
||||
blocking_ipv6,
|
||||
|
@ -26,6 +26,10 @@ export const R_WIN_ABSOLUTE_PATH = /^([a-zA-Z]:)?(\\|\/)(?:[^\\/:*?"<>|\x00]+\\)
|
||||
|
||||
export const R_CLIENT_ID = /^[a-z0-9-]{1,63}$/;
|
||||
|
||||
export const R_IPV4_SUBNET = /^([0-9]|[1-2][0-9]|3[0-2])?$/;
|
||||
|
||||
export const R_IPV6_SUBNET = /^([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])?$/;
|
||||
|
||||
export const MIN_PASSWORD_LENGTH = 8;
|
||||
export const MAX_PASSWORD_LENGTH = 72;
|
||||
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
R_DOMAIN,
|
||||
MAX_PASSWORD_LENGTH,
|
||||
MIN_PASSWORD_LENGTH,
|
||||
R_IPV4_SUBNET,
|
||||
R_IPV6_SUBNET,
|
||||
} from './constants';
|
||||
import { ip4ToInt, isValidAbsolutePath } from './form';
|
||||
import { isIpInCidr, parseSubnetMask } from './helpers';
|
||||
@ -365,3 +367,25 @@ export const validateIpGateway = (value, allValues) => {
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const validateIPv4Subnet = (value) => {
|
||||
if (!R_IPV4_SUBNET.test(value)) {
|
||||
return i18next.t('rate_limit_subnet_len_ipv4_error');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const validateIPv6Subnet = (value) => {
|
||||
if (!R_IPV6_SUBNET.test(value)) {
|
||||
return i18next.t('rate_limit_subnet_len_ipv6_error');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ const dnsConfig = handleActions(
|
||||
fallback_dns,
|
||||
bootstrap_dns,
|
||||
local_ptr_upstreams,
|
||||
ratelimit_whitelist,
|
||||
...values
|
||||
} = payload;
|
||||
|
||||
@ -30,6 +31,7 @@ const dnsConfig = handleActions(
|
||||
fallback_dns: (fallback_dns && fallback_dns.join('\n')) || '',
|
||||
bootstrap_dns: (bootstrap_dns && bootstrap_dns.join('\n')) || '',
|
||||
local_ptr_upstreams: (local_ptr_upstreams && local_ptr_upstreams.join('\n')) || '',
|
||||
ratelimit_whitelist: (ratelimit_whitelist && ratelimit_whitelist.join('\n')) || '',
|
||||
processingGetConfig: false,
|
||||
};
|
||||
},
|
||||
|
@ -45,8 +45,19 @@ type jsonDNSConfig struct {
|
||||
// ProtectionEnabled defines if protection is enabled.
|
||||
ProtectionEnabled *bool `json:"protection_enabled"`
|
||||
|
||||
// RateLimit is the number of requests per second allowed per client.
|
||||
RateLimit *uint32 `json:"ratelimit"`
|
||||
// Ratelimit is the number of requests per second allowed per client.
|
||||
Ratelimit *uint32 `json:"ratelimit"`
|
||||
|
||||
// RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for
|
||||
// rate limiting requests.
|
||||
RatelimitSubnetLenIPv4 *int `json:"ratelimit_subnet_len_ipv4"`
|
||||
|
||||
// RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for
|
||||
// rate limiting requests.
|
||||
RatelimitSubnetLenIPv6 *int `json:"ratelimit_subnet_len_ipv6"`
|
||||
|
||||
// RatelimitWhitelist is a list of IP addresses excluded from rate limiting.
|
||||
RatelimitWhitelist *[]string `json:"ratelimit_whitelist"`
|
||||
|
||||
// BlockingMode defines the way blocked responses are constructed.
|
||||
BlockingMode *filtering.BlockingMode `json:"blocking_mode"`
|
||||
@ -121,6 +132,9 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||
blockingMode, blockingIPv4, blockingIPv6 := s.dnsFilter.BlockingMode()
|
||||
blockedResponseTTL := s.dnsFilter.BlockedResponseTTL()
|
||||
ratelimit := s.conf.Ratelimit
|
||||
ratelimitSubnetLenIPv4 := s.conf.RatelimitSubnetLenIPv4
|
||||
ratelimitSubnetLenIPv6 := s.conf.RatelimitSubnetLenIPv6
|
||||
ratelimitWhitelist := stringutil.CloneSliceOrEmpty(s.conf.RatelimitWhitelist)
|
||||
|
||||
customIP := s.conf.EDNSClientSubnet.CustomIP
|
||||
enableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled
|
||||
@ -157,7 +171,10 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||
BlockingMode: &blockingMode,
|
||||
BlockingIPv4: blockingIPv4,
|
||||
BlockingIPv6: blockingIPv6,
|
||||
RateLimit: &ratelimit,
|
||||
Ratelimit: &ratelimit,
|
||||
RatelimitSubnetLenIPv4: &ratelimitSubnetLenIPv4,
|
||||
RatelimitSubnetLenIPv6: &ratelimitSubnetLenIPv6,
|
||||
RatelimitWhitelist: &ratelimitWhitelist,
|
||||
EDNSCSCustomIP: customIP,
|
||||
EDNSCSEnabled: &enableEDNSClientSubnet,
|
||||
EDNSCSUseCustom: &useCustom,
|
||||
@ -201,6 +218,7 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||
}
|
||||
|
||||
// checkBlockingMode returns an error if blocking mode is invalid.
|
||||
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
|
||||
if req.BlockingMode == nil {
|
||||
return nil
|
||||
@ -209,12 +227,21 @@ func (req *jsonDNSConfig) checkBlockingMode() (err error) {
|
||||
return validateBlockingMode(*req.BlockingMode, req.BlockingIPv4, req.BlockingIPv6)
|
||||
}
|
||||
|
||||
func (req *jsonDNSConfig) checkUpstreamsMode() bool {
|
||||
valid := []string{"", "fastest_addr", "parallel"}
|
||||
// checkUpstreamsMode returns an error if the upstream mode is invalid.
|
||||
func (req *jsonDNSConfig) checkUpstreamsMode() (err error) {
|
||||
if req.UpstreamMode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return req.UpstreamMode == nil || stringutil.InSlice(valid, *req.UpstreamMode)
|
||||
mode := *req.UpstreamMode
|
||||
if ok := slices.Contains([]string{"", "fastest_addr", "parallel"}, mode); !ok {
|
||||
return fmt.Errorf("upstream_mode: incorrect value %q", mode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkBootstrap returns an error if any bootstrap address is invalid.
|
||||
func (req *jsonDNSConfig) checkBootstrap() (err error) {
|
||||
if req.Bootstraps == nil {
|
||||
return nil
|
||||
@ -229,6 +256,7 @@ func (req *jsonDNSConfig) checkBootstrap() (err error) {
|
||||
}
|
||||
|
||||
if _, err = upstream.NewUpstreamResolver(b, nil); err != nil {
|
||||
// Don't wrap the error because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -244,67 +272,157 @@ func (req *jsonDNSConfig) checkFallbacks() (err error) {
|
||||
|
||||
err = ValidateUpstreams(*req.Fallbacks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating fallback servers: %w", err)
|
||||
return fmt.Errorf("fallback servers: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate returns an error if any field of req is invalid.
|
||||
//
|
||||
// TODO(s.chzhen): Parse, don't validate.
|
||||
func (req *jsonDNSConfig) validate(privateNets netutil.SubnetSet) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "validating dns config: %w") }()
|
||||
|
||||
err = req.validateUpstreamDNSServers(privateNets)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
err = req.checkRatelimitSubnetMaskLen()
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
err = req.checkRatelimitWhitelist()
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
err = req.checkBlockingMode()
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
err = req.checkUpstreamsMode()
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
err = req.checkCacheTTL()
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateUpstreamDNSServers returns an error if any field of req is invalid.
|
||||
func (req *jsonDNSConfig) validateUpstreamDNSServers(privateNets netutil.SubnetSet) (err error) {
|
||||
if req.Upstreams != nil {
|
||||
err = ValidateUpstreams(*req.Upstreams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating upstream servers: %w", err)
|
||||
return fmt.Errorf("upstream servers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if req.LocalPTRUpstreams != nil {
|
||||
err = ValidateUpstreamsPrivate(*req.LocalPTRUpstreams, privateNets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating private upstream servers: %w", err)
|
||||
return fmt.Errorf("private upstream servers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = req.checkBootstrap()
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
err = req.checkFallbacks()
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
err = req.checkBlockingMode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case !req.checkUpstreamsMode():
|
||||
return errors.Error("upstream_mode: incorrect value")
|
||||
case !req.checkCacheTTL():
|
||||
return errors.Error("cache_ttl_min must be less or equal than cache_ttl_max")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *jsonDNSConfig) checkCacheTTL() bool {
|
||||
// checkCacheTTL returns an error if the configuration of the cache TTL is
|
||||
// invalid.
|
||||
func (req *jsonDNSConfig) checkCacheTTL() (err error) {
|
||||
if req.CacheMinTTL == nil && req.CacheMaxTTL == nil {
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
var min, max uint32
|
||||
var minTTL, maxTTL uint32
|
||||
if req.CacheMinTTL != nil {
|
||||
min = *req.CacheMinTTL
|
||||
minTTL = *req.CacheMinTTL
|
||||
}
|
||||
if req.CacheMaxTTL != nil {
|
||||
max = *req.CacheMaxTTL
|
||||
maxTTL = *req.CacheMaxTTL
|
||||
}
|
||||
|
||||
return min <= max
|
||||
if minTTL <= maxTTL {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Error("cache_ttl_min must be less or equal than cache_ttl_max")
|
||||
}
|
||||
|
||||
// checkRatelimitSubnetMaskLen returns an error if the length of the subnet mask
|
||||
// for IPv4 or IPv6 addresses is invalid.
|
||||
func (req *jsonDNSConfig) checkRatelimitSubnetMaskLen() (err error) {
|
||||
err = checkInclusion(req.RatelimitSubnetLenIPv4, 0, netutil.IPv4BitLen)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ratelimit_subnet_len_ipv4 is invalid: %w", err)
|
||||
}
|
||||
|
||||
err = checkInclusion(req.RatelimitSubnetLenIPv6, 0, netutil.IPv6BitLen)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ratelimit_subnet_len_ipv6 is invalid: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkInclusion returns an error if a ptr is not nil and points to value,
|
||||
// that not in the inclusive range between minN and maxN.
|
||||
func checkInclusion(ptr *int, minN, maxN int) (err error) {
|
||||
if ptr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
n := *ptr
|
||||
switch {
|
||||
case n < minN:
|
||||
return fmt.Errorf("value %d less than min %d", n, minN)
|
||||
case n > maxN:
|
||||
return fmt.Errorf("value %d greater than max %d", n, maxN)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRatelimitWhitelist returns an error if any of IP addresses is invalid.
|
||||
func (req *jsonDNSConfig) checkRatelimitWhitelist() (err error) {
|
||||
if req.RatelimitWhitelist == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, ipStr := range *req.RatelimitWhitelist {
|
||||
if _, err = netip.ParseAddr(ipStr); err != nil {
|
||||
return fmt.Errorf("ratelimit whitelist: at index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSetConfig handles requests to the POST /control/dns_config endpoint.
|
||||
@ -401,6 +519,9 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||
setIfNotNil(&s.conf.CacheOptimistic, dc.CacheOptimistic),
|
||||
setIfNotNil(&s.conf.AddrProcConf.UseRDNS, dc.ResolveClients),
|
||||
setIfNotNil(&s.conf.UsePrivateRDNS, dc.UsePrivateRDNS),
|
||||
setIfNotNil(&s.conf.RatelimitSubnetLenIPv4, dc.RatelimitSubnetLenIPv4),
|
||||
setIfNotNil(&s.conf.RatelimitSubnetLenIPv6, dc.RatelimitSubnetLenIPv6),
|
||||
setIfNotNil(&s.conf.RatelimitWhitelist, dc.RatelimitWhitelist),
|
||||
} {
|
||||
shouldRestart = shouldRestart || hasSet
|
||||
if shouldRestart {
|
||||
@ -408,8 +529,8 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||
}
|
||||
}
|
||||
|
||||
if dc.RateLimit != nil && s.conf.Ratelimit != *dc.RateLimit {
|
||||
s.conf.Ratelimit = *dc.RateLimit
|
||||
if dc.Ratelimit != nil && s.conf.Ratelimit != *dc.Ratelimit {
|
||||
s.conf.Ratelimit = *dc.Ratelimit
|
||||
shouldRestart = true
|
||||
}
|
||||
|
||||
|
@ -72,9 +72,11 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
||||
UDPListenAddrs: []*net.UDPAddr{},
|
||||
TCPListenAddrs: []*net.TCPAddr{},
|
||||
Config: Config{
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
FallbackDNS: []string{"9.9.9.10"},
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
FallbackDNS: []string{"9.9.9.10"},
|
||||
RatelimitSubnetLenIPv4: 24,
|
||||
RatelimitSubnetLenIPv6: 56,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
ConfigModified: func() {},
|
||||
}
|
||||
@ -150,8 +152,10 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
UDPListenAddrs: []*net.UDPAddr{},
|
||||
TCPListenAddrs: []*net.TCPAddr{},
|
||||
Config: Config{
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
RatelimitSubnetLenIPv4: 24,
|
||||
RatelimitSubnetLenIPv6: 56,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
ConfigModified: func() {},
|
||||
}
|
||||
@ -179,11 +183,19 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
name: "blocking_mode_good",
|
||||
wantSet: "",
|
||||
}, {
|
||||
name: "blocking_mode_bad",
|
||||
wantSet: "blocking_ipv4 must be valid ipv4 on custom_ip blocking_mode",
|
||||
name: "blocking_mode_bad",
|
||||
wantSet: "validating dns config: " +
|
||||
"blocking_ipv4 must be valid ipv4 on custom_ip blocking_mode",
|
||||
}, {
|
||||
name: "ratelimit",
|
||||
wantSet: "",
|
||||
}, {
|
||||
name: "ratelimit_subnet_len",
|
||||
wantSet: "",
|
||||
}, {
|
||||
name: "ratelimit_whitelist_not_ip",
|
||||
wantSet: `validating dns config: ratelimit whitelist: at index 1: ParseAddr("not.ip"): ` +
|
||||
`unexpected character (at "not.ip")`,
|
||||
}, {
|
||||
name: "edns_cs_enabled",
|
||||
wantSet: "",
|
||||
@ -206,24 +218,26 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
name: "upstream_mode_fastest_addr",
|
||||
wantSet: "",
|
||||
}, {
|
||||
name: "upstream_dns_bad",
|
||||
wantSet: `validating upstream servers: validating upstream "!!!": not an ip:port`,
|
||||
name: "upstream_dns_bad",
|
||||
wantSet: `validating dns config: ` +
|
||||
`upstream servers: validating upstream "!!!": not an ip:port`,
|
||||
}, {
|
||||
name: "bootstraps_bad",
|
||||
wantSet: `checking bootstrap a: invalid address: bootstrap a:53: ` +
|
||||
wantSet: `validating dns config: checking bootstrap a: invalid address: bootstrap a:53: ` +
|
||||
`ParseAddr("a"): unable to parse IP`,
|
||||
}, {
|
||||
name: "cache_bad_ttl",
|
||||
wantSet: `cache_ttl_min must be less or equal than cache_ttl_max`,
|
||||
wantSet: `validating dns config: cache_ttl_min must be less or equal than cache_ttl_max`,
|
||||
}, {
|
||||
name: "upstream_mode_bad",
|
||||
wantSet: `upstream_mode: incorrect value`,
|
||||
wantSet: `validating dns config: upstream_mode: incorrect value "somethingelse"`,
|
||||
}, {
|
||||
name: "local_ptr_upstreams_good",
|
||||
wantSet: "",
|
||||
}, {
|
||||
name: "local_ptr_upstreams_bad",
|
||||
wantSet: `validating private upstream servers: checking domain-specific upstreams: ` +
|
||||
wantSet: `validating dns config: ` +
|
||||
`private upstream servers: checking domain-specific upstreams: ` +
|
||||
`bad arpa domain name "non.arpa.": not a reversed ip network`,
|
||||
}, {
|
||||
name: "local_ptr_upstreams_null",
|
||||
|
@ -17,6 +17,9 @@
|
||||
"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": "",
|
||||
@ -53,6 +56,9 @@
|
||||
"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": "",
|
||||
@ -89,6 +95,9 @@
|
||||
"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": "",
|
||||
|
@ -22,6 +22,9 @@
|
||||
"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": "",
|
||||
@ -60,6 +63,9 @@
|
||||
"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": "",
|
||||
@ -99,6 +105,9 @@
|
||||
"protection_enabled": true,
|
||||
"protection_disabled_until": null,
|
||||
"ratelimit": 0,
|
||||
"ratelimit_subnet_len_ipv4": 24,
|
||||
"ratelimit_subnet_len_ipv6": 56,
|
||||
"ratelimit_whitelist": [],
|
||||
"blocking_mode": "refused",
|
||||
"blocking_ipv4": "",
|
||||
"blocking_ipv6": "",
|
||||
@ -138,6 +147,9 @@
|
||||
"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": "",
|
||||
@ -177,6 +189,98 @@
|
||||
"protection_enabled": true,
|
||||
"protection_disabled_until": null,
|
||||
"ratelimit": 6,
|
||||
"ratelimit_subnet_len_ipv4": 24,
|
||||
"ratelimit_subnet_len_ipv6": 56,
|
||||
"ratelimit_whitelist": [],
|
||||
"blocking_mode": "default",
|
||||
"blocking_ipv4": "",
|
||||
"blocking_ipv6": "",
|
||||
"blocked_response_ttl": 10,
|
||||
"edns_cs_enabled": false,
|
||||
"dnssec_enabled": false,
|
||||
"disable_ipv6": false,
|
||||
"upstream_mode": "",
|
||||
"cache_size": 0,
|
||||
"cache_ttl_min": 0,
|
||||
"cache_ttl_max": 0,
|
||||
"cache_optimistic": false,
|
||||
"resolve_clients": false,
|
||||
"use_private_ptr_resolvers": false,
|
||||
"local_ptr_upstreams": [],
|
||||
"edns_cs_use_custom": false,
|
||||
"edns_cs_custom_ip": ""
|
||||
}
|
||||
},
|
||||
"ratelimit_subnet_len": {
|
||||
"req": {
|
||||
"ratelimit": 12,
|
||||
"ratelimit_subnet_len_ipv4": 32,
|
||||
"ratelimit_subnet_len_ipv6": 128
|
||||
},
|
||||
"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": 12,
|
||||
"ratelimit_subnet_len_ipv4": 32,
|
||||
"ratelimit_subnet_len_ipv6": 128,
|
||||
"ratelimit_whitelist": [],
|
||||
"blocking_mode": "default",
|
||||
"blocking_ipv4": "",
|
||||
"blocking_ipv6": "",
|
||||
"blocked_response_ttl": 10,
|
||||
"edns_cs_enabled": false,
|
||||
"dnssec_enabled": false,
|
||||
"disable_ipv6": false,
|
||||
"upstream_mode": "",
|
||||
"cache_size": 0,
|
||||
"cache_ttl_min": 0,
|
||||
"cache_ttl_max": 0,
|
||||
"cache_optimistic": false,
|
||||
"resolve_clients": false,
|
||||
"use_private_ptr_resolvers": false,
|
||||
"local_ptr_upstreams": [],
|
||||
"edns_cs_use_custom": false,
|
||||
"edns_cs_custom_ip": ""
|
||||
}
|
||||
},
|
||||
"ratelimit_whitelist_not_ip": {
|
||||
"req": {
|
||||
"ratelimit_whitelist": [
|
||||
"1.2.3.4",
|
||||
"not.ip"
|
||||
]
|
||||
},
|
||||
"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": "",
|
||||
@ -216,6 +320,9 @@
|
||||
"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": "",
|
||||
@ -257,6 +364,9 @@
|
||||
"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": "",
|
||||
@ -298,6 +408,9 @@
|
||||
"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": "",
|
||||
@ -337,6 +450,9 @@
|
||||
"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": "",
|
||||
@ -376,6 +492,9 @@
|
||||
"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": "",
|
||||
@ -415,6 +534,9 @@
|
||||
"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": "",
|
||||
@ -454,6 +576,9 @@
|
||||
"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": "",
|
||||
@ -495,6 +620,9 @@
|
||||
"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": "",
|
||||
@ -536,6 +664,9 @@
|
||||
"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": "",
|
||||
@ -576,6 +707,9 @@
|
||||
"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": "",
|
||||
@ -615,6 +749,9 @@
|
||||
"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": "",
|
||||
@ -656,6 +793,9 @@
|
||||
"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": "",
|
||||
@ -700,6 +840,9 @@
|
||||
"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": "",
|
||||
@ -739,6 +882,9 @@
|
||||
"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": "",
|
||||
@ -782,6 +928,9 @@
|
||||
"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": "",
|
||||
@ -821,6 +970,9 @@
|
||||
"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": "",
|
||||
@ -863,6 +1015,9 @@
|
||||
"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": "",
|
||||
|
@ -4,6 +4,25 @@
|
||||
|
||||
## v0.108.0: API changes
|
||||
|
||||
## v0.107.42: API changes
|
||||
|
||||
### The new field `"ratelimit_subnet_len_ipv4"` in `DNSConfig` object
|
||||
|
||||
* The new field `"ratelimit_subnet_len_ipv4"` in `GET /control/dns_info` and
|
||||
`POST /control/dns_config` is the length of the subnet mask for IPv4
|
||||
addresses.
|
||||
|
||||
### The new field `"ratelimit_subnet_len_ipv6"` in `DNSConfig` object
|
||||
|
||||
* The new field `"ratelimit_subnet_len_ipv6"` in `GET /control/dns_info` and
|
||||
`POST /control/dns_config` is the length of the subnet mask for IPv6
|
||||
addresses.
|
||||
|
||||
### The new field `"ratelimit_whitelist"` in `DNSConfig` object
|
||||
|
||||
* The new field `"blocked_response_ttl"` in `GET /control/dns_info` and `POST
|
||||
/control/dns_config` is the list of IP addresses excluded from rate limiting.
|
||||
|
||||
## v0.107.39: API changes
|
||||
|
||||
### New HTTP API 'POST /control/dhcp/update_static_lease'
|
||||
|
@ -1468,6 +1468,23 @@
|
||||
'type': 'boolean'
|
||||
'ratelimit':
|
||||
'type': 'integer'
|
||||
'ratelimit_subnet_subnet_len_ipv4':
|
||||
'description': 'Length of the subnet mask for IPv4 addresses.'
|
||||
'type': 'integer'
|
||||
'default': 24
|
||||
'minimum': 0
|
||||
'maximum': 32
|
||||
'ratelimit_subnet_subnet_len_ipv6':
|
||||
'description': 'Length of the subnet mask for IPv6 addresses.'
|
||||
'type': 'integer'
|
||||
'default': 56
|
||||
'minimum': 0
|
||||
'maximum': 128
|
||||
'ratelimit_whitelist':
|
||||
'type': 'array'
|
||||
'description': 'List of IP addresses excluded from rate limiting.'
|
||||
'items':
|
||||
'type': 'string'
|
||||
'blocking_mode':
|
||||
'type': 'string'
|
||||
'enum':
|
||||
|
Loading…
Reference in New Issue
Block a user