feat(rate-limit): add default request rate limit configuration

- Support openresty_default_limit_req_per_ip in system_configs.
- Add limit_req and limit_req_status 429 directive generation in openresty renderer.
- Implement route-level limit_req_per_ip override and explicit disable.
- Add frontend UI inputs and validation in rate limits tab config.
- Update swagger API docs and changelog for v3.4.3-beta.3.
This commit is contained in:
ryan
2026-07-20 10:58:13 +08:00
parent fda8d7fcb1
commit ae5345c03e
18 changed files with 191 additions and 12 deletions
@@ -531,6 +531,7 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon
appendIfChanged("OpenRestyDefaultLimitConnPerServer", fmt.Sprintf("%d", left.DefaultLimitConnPerServer), fmt.Sprintf("%d", right.DefaultLimitConnPerServer))
appendIfChanged("OpenRestyDefaultLimitConnPerIP", fmt.Sprintf("%d", left.DefaultLimitConnPerIP), fmt.Sprintf("%d", right.DefaultLimitConnPerIP))
appendIfChanged("OpenRestyDefaultLimitRate", left.DefaultLimitRate, right.DefaultLimitRate)
appendIfChanged("OpenRestyDefaultLimitReqPerIP", left.DefaultLimitReqPerIP, right.DefaultLimitReqPerIP)
return changes
}
@@ -582,5 +583,6 @@ func openRestyOptionKeys() []string {
"OpenRestyDefaultLimitConnPerServer",
"OpenRestyDefaultLimitConnPerIP",
"OpenRestyDefaultLimitRate",
"OpenRestyDefaultLimitReqPerIP",
}
}
@@ -54,6 +54,7 @@ type snapshotRoute struct {
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
LimitRate string `json:"limit_rate,omitempty"`
LimitReqPerIP string `json:"limit_req_per_ip,omitempty"`
CacheEnabled bool `json:"cache_enabled"`
CachePolicy string `json:"cache_policy,omitempty"`
CacheRules []string `json:"cache_rules,omitempty"`
@@ -138,6 +139,7 @@ type openRestyConfigSnapshot struct {
DefaultLimitConnPerServer int `json:"default_limit_conn_per_server,omitempty"`
DefaultLimitConnPerIP int `json:"default_limit_conn_per_ip,omitempty"`
DefaultLimitRate string `json:"default_limit_rate,omitempty"`
DefaultLimitReqPerIP string `json:"default_limit_req_per_ip,omitempty"`
}
type snapshotDocument struct {
@@ -285,6 +287,7 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna
LimitConnPerServer: route.LimitConnPerServer,
LimitConnPerIP: route.LimitConnPerIP,
LimitRate: route.LimitRate,
LimitReqPerIP: route.LimitReqPerIP,
CacheEnabled: route.CacheEnabled,
CachePolicy: route.CachePolicy,
CacheRules: cacheRules,
@@ -548,10 +551,14 @@ func buildOpenRestyConfigSnapshot(ctx context.Context) openRestyConfigSnapshot {
DefaultLimitConnPerServer: getNonNegIntConfig(model.ConfigKeyOpenRestyDefaultLimitConnPerServer, 0),
DefaultLimitConnPerIP: getNonNegIntConfig(model.ConfigKeyOpenRestyDefaultLimitConnPerIP, 0),
DefaultLimitRate: strings.ToLower(strings.TrimSpace(getStringConfig(model.ConfigKeyOpenRestyDefaultLimitRate, ""))),
DefaultLimitReqPerIP: strings.ToLower(strings.TrimSpace(getStringConfig(model.ConfigKeyOpenRestyDefaultLimitReqPerIP, ""))),
}
if snapshot.DefaultLimitRate == "0" {
snapshot.DefaultLimitRate = ""
}
if snapshot.DefaultLimitReqPerIP == "0" {
snapshot.DefaultLimitReqPerIP = ""
}
snapshot.CachePath = normalizeProxyCachePathForSnapshot(snapshot.CacheEnabled, snapshot.CachePath)
return snapshot
}
@@ -53,6 +53,7 @@ var openRestyOptionValidators = map[string]func(key, value string) error{
model.ConfigKeyOpenRestyDefaultLimitConnPerServer: validateNonNegativeIntegerOption,
model.ConfigKeyOpenRestyDefaultLimitConnPerIP: validateNonNegativeIntegerOption,
model.ConfigKeyOpenRestyDefaultLimitRate: validateOpenRestyDefaultLimitRate,
model.ConfigKeyOpenRestyDefaultLimitReqPerIP: validateOpenRestyDefaultLimitReqPerIP,
}
var openRestyDefaultLimitRatePattern = regexp.MustCompile(`^\d+[kKmM]?$`)
@@ -194,3 +195,15 @@ func validateOpenRestyDefaultLimitRate(key, trimmed string) error {
}
return nil
}
var openRestyDefaultLimitReqPerIPPattern = regexp.MustCompile(`^\d+r/[sm]$`)
func validateOpenRestyDefaultLimitReqPerIP(key, trimmed string) error {
if trimmed == "" || trimmed == "0" {
return nil
}
if !openRestyDefaultLimitReqPerIPPattern.MatchString(strings.ToLower(trimmed)) {
return fmt.Errorf("%s 格式不合法,请输入类似 10r/s、100r/m,或留空关闭", key)
}
return nil
}
@@ -90,7 +90,7 @@ func populateProxyRouteFields(
upstreams []string,
originHost, cachePolicy string,
limitConnPerServer, limitConnPerIP int,
limitRate, upstreamType string,
limitRate, limitReqPerIP, upstreamType string,
) {
route.SiteName = siteName
route.OriginID = originID
@@ -103,6 +103,7 @@ func populateProxyRouteFields(
route.LimitConnPerServer = limitConnPerServer
route.LimitConnPerIP = limitConnPerIP
route.LimitRate = limitRate
route.LimitReqPerIP = limitReqPerIP
route.CacheEnabled = input.CacheEnabled
route.CachePolicy = normalizeCachePolicy(input.CacheEnabled, cachePolicy)
route.CacheRules = jsonFields.cacheRulesJSON
@@ -23,6 +23,7 @@ import (
var proxyHeaderKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
var proxyRouteLimitRatePattern = regexp.MustCompile(`^\d+[kKmM]?$`)
var proxyRouteLimitReqPattern = regexp.MustCompile(`^\d+r/[sm]$`)
const (
proxyRouteCachePolicyStatic = "static"
@@ -349,6 +350,20 @@ func normalizeProxyRouteLimitRate(raw string) (string, error) {
return normalized, nil
}
func normalizeProxyRouteLimitReqPerIP(raw string) (string, error) {
normalized := strings.ToLower(strings.TrimSpace(raw))
if normalized == "" || normalized == "0" {
return "", nil
}
if normalized == "-1" {
return "-1", nil
}
if !proxyRouteLimitReqPattern.MatchString(normalized) {
return "", errors.New("请求频率格式不合法,请使用类似 10r/s、100r/m,或 -1 禁用")
}
return normalized, nil
}
func hasStructuredOriginInput(input Input) bool {
return (input.OriginID != nil && *input.OriginID != 0) ||
strings.TrimSpace(input.OriginScheme) != "" ||
@@ -40,6 +40,7 @@ type Input struct {
LimitConnPerServer int `json:"limit_conn_per_server"`
LimitConnPerIP int `json:"limit_conn_per_ip"`
LimitRate string `json:"limit_rate"`
LimitReqPerIP string `json:"limit_req_per_ip"`
CacheEnabled bool `json:"cache_enabled"`
CachePolicy string `json:"cache_policy"`
CacheRules []string `json:"cache_rules"`
@@ -72,6 +73,7 @@ type View struct {
LimitConnPerServer int `json:"limit_conn_per_server"`
LimitConnPerIP int `json:"limit_conn_per_ip"`
LimitRate string `json:"limit_rate"`
LimitReqPerIP string `json:"limit_req_per_ip"`
CacheEnabled bool `json:"cache_enabled"`
CachePolicy string `json:"cache_policy"`
CacheRules string `json:"cache_rules"`
@@ -267,6 +269,10 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input)
if err != nil {
return nil, nil, err
}
limitReqPerIP, err := normalizeProxyRouteLimitReqPerIP(input.LimitReqPerIP)
if err != nil {
return nil, nil, err
}
if err := validateProxyRouteZoneDomainCertificates(ctx, domains, input.EnableHTTPS); err != nil {
return nil, nil, err
}
@@ -307,6 +313,7 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input)
limitConnPerServer,
limitConnPerIP,
limitRate,
limitReqPerIP,
upstreamType,
)
if err := applyProxyRouteUpstreamType(ctx, route, upstreamType, input); err != nil {
@@ -369,6 +376,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e
LimitConnPerServer: route.LimitConnPerServer,
LimitConnPerIP: route.LimitConnPerIP,
LimitRate: route.LimitRate,
LimitReqPerIP: route.LimitReqPerIP,
CacheEnabled: route.CacheEnabled,
CachePolicy: displayCachePolicy(route.CacheEnabled, route.CachePolicy),
CacheRules: route.CacheRules,
@@ -0,0 +1,12 @@
-- +goose Up
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
VALUES
('openresty_default_limit_req_per_ip', '', 'business', 0, '默认单 IP 请求频率限制(空关闭,例如 10r/s、100r/m)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (key) DO NOTHING;
ALTER TABLE w_of_proxy_routes ADD COLUMN limit_req_per_ip VARCHAR(32) NOT NULL DEFAULT '';
-- +goose Down
ALTER TABLE w_of_proxy_routes DROP COLUMN limit_req_per_ip;
DELETE FROM w_system_configs WHERE key = 'openresty_default_limit_req_per_ip';
@@ -0,0 +1,12 @@
-- +goose Up
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
VALUES
('openresty_default_limit_req_per_ip', '', 'business', 0, '默认单 IP 请求频率限制(空关闭,例如 10r/s、100r/m)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (key) DO NOTHING;
ALTER TABLE w_of_proxy_routes ADD COLUMN limit_req_per_ip VARCHAR(32) NOT NULL DEFAULT '';
-- +goose Down
ALTER TABLE w_of_proxy_routes DROP COLUMN limit_req_per_ip;
DELETE FROM w_system_configs WHERE key = 'openresty_default_limit_req_per_ip';
+2
View File
@@ -25,6 +25,7 @@ type ProxyRoute struct {
LimitConnPerServer int `json:"limit_conn_per_server" gorm:"not null;default:0"`
LimitConnPerIP int `json:"limit_conn_per_ip" gorm:"not null;default:0"`
LimitRate string `json:"limit_rate" gorm:"size:32;not null;default:''"`
LimitReqPerIP string `json:"limit_req_per_ip" gorm:"size:32;not null;default:''"`
CacheEnabled bool `json:"cache_enabled" gorm:"not null;default:false"`
CachePolicy string `json:"cache_policy" gorm:"size:32;not null;default:''"`
CacheRules string `json:"cache_rules" gorm:"type:text;not null;default:'[]'"`
@@ -84,6 +85,7 @@ func UpdateProxyRouteRecord(ctx context.Context, route *ProxyRoute) error {
"limit_conn_per_server": route.LimitConnPerServer,
"limit_conn_per_ip": route.LimitConnPerIP,
"limit_rate": route.LimitRate,
"limit_req_per_ip": route.LimitReqPerIP,
"cache_enabled": route.CacheEnabled,
"cache_policy": route.CachePolicy,
"cache_rules": route.CacheRules,
+1
View File
@@ -108,6 +108,7 @@ const (
ConfigKeyOpenRestyDefaultLimitConnPerServer = "openresty_default_limit_conn_per_server" // 默认站点并发连接
ConfigKeyOpenRestyDefaultLimitConnPerIP = "openresty_default_limit_conn_per_ip" // 默认单 IP 并发连接
ConfigKeyOpenRestyDefaultLimitRate = "openresty_default_limit_rate" // 默认单请求带宽
ConfigKeyOpenRestyDefaultLimitReqPerIP = "openresty_default_limit_req_per_ip" // 默认单 IP 请求频率限制
)
const (