diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 4afba152..121effc6 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -22,6 +22,12 @@ sidebar: false ## [unreleased] +## [v3.4.3-beta.3] - 2026-07-20 + +### 新增 + +- 安全性限流页「配置」页签新增请求频率限制配置:支持在全局配置单 IP 的请求频率限制(例如 10r/s、100r/m),触发限制时边缘节点返回 429 状态码;同时支持站点级别单独配置或通过 `-1` 显式禁用请求频率限制。 + ### 改进 - 限流页请求压力图的 RPS 纵轴按可见时间窗口最高值的 1.5 倍动态缩放,拖动底部时间范围条时同步更新。 diff --git a/docs/docs.go b/docs/docs.go index 00fea8aa..e970aa8f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5571,7 +5571,7 @@ const docTemplate = `{ }, { "type": "integer", - "description": "趋势桶分钟数(1、5 或 60,默认 60)", + "description": "趋势桶分钟数(1、3、5 或 60,默认 60)", "name": "bucket_minutes", "in": "query" } @@ -18272,6 +18272,9 @@ const docTemplate = `{ "limit_rate": { "type": "string" }, + "limit_req_per_ip": { + "type": "string" + }, "origin_address": { "type": "string" }, @@ -18388,6 +18391,9 @@ const docTemplate = `{ "limit_rate": { "type": "string" }, + "limit_req_per_ip": { + "type": "string" + }, "origin_host": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index 577fd4d6..b92eea7d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5564,7 +5564,7 @@ }, { "type": "integer", - "description": "趋势桶分钟数(1、5 或 60,默认 60)", + "description": "趋势桶分钟数(1、3、5 或 60,默认 60)", "name": "bucket_minutes", "in": "query" } @@ -18265,6 +18265,9 @@ "limit_rate": { "type": "string" }, + "limit_req_per_ip": { + "type": "string" + }, "origin_address": { "type": "string" }, @@ -18381,6 +18384,9 @@ "limit_rate": { "type": "string" }, + "limit_req_per_ip": { + "type": "string" + }, "origin_host": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7d5b231f..aca02b01 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2928,6 +2928,8 @@ definitions: type: integer limit_rate: type: string + limit_req_per_ip: + type: string origin_address: type: string origin_host: @@ -3005,6 +3007,8 @@ definitions: type: integer limit_rate: type: string + limit_req_per_ip: + type: string origin_host: type: string origin_id: @@ -7522,7 +7526,7 @@ paths: in: query name: hours type: integer - - description: 趋势桶分钟数(1、5 或 60,默认 60) + - description: 趋势桶分钟数(1、3、5 或 60,默认 60) in: query name: bucket_minutes type: integer diff --git a/frontend/app/(main)/rate-limits/components/config-tab.tsx b/frontend/app/(main)/rate-limits/components/config-tab.tsx index 231f8ad2..6c76f832 100644 --- a/frontend/app/(main)/rate-limits/components/config-tab.tsx +++ b/frontend/app/(main)/rate-limits/components/config-tab.tsx @@ -25,6 +25,7 @@ const optionsQueryKey = ['openflare', 'options'] as const; const KEY_CONN_PER_SERVER = 'openresty_default_limit_conn_per_server'; const KEY_CONN_PER_IP = 'openresty_default_limit_conn_per_ip'; const KEY_LIMIT_RATE = 'openresty_default_limit_rate'; +const KEY_LIMIT_REQ_PER_IP = 'openresty_default_limit_req_per_ip'; const limitRatePattern = /^\d+(?:[kKmM])?$/; @@ -32,12 +33,14 @@ type RateLimitFields = { openresty_default_limit_conn_per_server: string; openresty_default_limit_conn_per_ip: string; openresty_default_limit_rate: string; + openresty_default_limit_req_per_ip: string; }; const defaultFields: RateLimitFields = { openresty_default_limit_conn_per_server: '0', openresty_default_limit_conn_per_ip: '0', openresty_default_limit_rate: '', + openresty_default_limit_req_per_ip: '', }; function optionsToMap(options: Array<{ key: string; value: string }>) { @@ -55,6 +58,7 @@ function mapOptionsToFields( optionMap[KEY_CONN_PER_SERVER] ?? '0', openresty_default_limit_conn_per_ip: optionMap[KEY_CONN_PER_IP] ?? '0', openresty_default_limit_rate: optionMap[KEY_LIMIT_RATE] ?? '', + openresty_default_limit_req_per_ip: optionMap[KEY_LIMIT_REQ_PER_IP] ?? '', }; } @@ -71,6 +75,13 @@ function validateFields(fields: RateLimitFields) { if (rate && rate !== '0' && !limitRatePattern.test(rate)) { throw new Error('限速格式不合法,请使用 512k、1m、纯数字,或留空关闭'); } + + const reqRate = fields.openresty_default_limit_req_per_ip.trim(); + if (reqRate && reqRate !== '0' && !/^\d+r\/[sm]$/i.test(reqRate)) { + throw new Error( + '请求频率限制格式不合法,请使用类似 10r/s、100r/m,或留空关闭', + ); + } } function normalizeConnValue(value: string) { @@ -119,6 +130,10 @@ export function ConfigTab() { key: KEY_LIMIT_RATE, value: normalizeRateValue(fields.openresty_default_limit_rate), }, + { + key: KEY_LIMIT_REQ_PER_IP, + value: normalizeRateValue(fields.openresty_default_limit_req_per_ip), + }, ]); }, onSuccess: async () => { @@ -256,6 +271,25 @@ export function ConfigTab() { 单请求带宽默认值,例如 512k 或 1m

+
+ + + updateField( + 'openresty_default_limit_req_per_ip', + e.target.value, + ) + } + className='h-9 text-xs' + /> +

+ 单个 IP 请求频率默认值,例如 10r/s 或 100r/m,留空关闭 +

+
diff --git a/internal/apps/openflare/config_version/logics.go b/internal/apps/openflare/config_version/logics.go index 76bb6c58..b8146af2 100644 --- a/internal/apps/openflare/config_version/logics.go +++ b/internal/apps/openflare/config_version/logics.go @@ -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", } } diff --git a/internal/apps/openflare/config_version/snapshot.go b/internal/apps/openflare/config_version/snapshot.go index 895c41b9..5921466f 100644 --- a/internal/apps/openflare/config_version/snapshot.go +++ b/internal/apps/openflare/config_version/snapshot.go @@ -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 } diff --git a/internal/apps/openflare/option/openresty_validators.go b/internal/apps/openflare/option/openresty_validators.go index 43e54ad4..7185221c 100644 --- a/internal/apps/openflare/option/openresty_validators.go +++ b/internal/apps/openflare/option/openresty_validators.go @@ -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 +} diff --git a/internal/apps/openflare/proxy_route/build_helpers.go b/internal/apps/openflare/proxy_route/build_helpers.go index 9a2b5dd9..10136e65 100644 --- a/internal/apps/openflare/proxy_route/build_helpers.go +++ b/internal/apps/openflare/proxy_route/build_helpers.go @@ -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 diff --git a/internal/apps/openflare/proxy_route/helpers.go b/internal/apps/openflare/proxy_route/helpers.go index 1c67cec6..9e631042 100644 --- a/internal/apps/openflare/proxy_route/helpers.go +++ b/internal/apps/openflare/proxy_route/helpers.go @@ -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) != "" || diff --git a/internal/apps/openflare/proxy_route/logics.go b/internal/apps/openflare/proxy_route/logics.go index 1570be32..6f5f2801 100644 --- a/internal/apps/openflare/proxy_route/logics.go +++ b/internal/apps/openflare/proxy_route/logics.go @@ -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, diff --git a/internal/db/migrator/goose/postgres/202607200001_add_openresty_default_limit_req_per_ip.sql b/internal/db/migrator/goose/postgres/202607200001_add_openresty_default_limit_req_per_ip.sql new file mode 100644 index 00000000..553265ef --- /dev/null +++ b/internal/db/migrator/goose/postgres/202607200001_add_openresty_default_limit_req_per_ip.sql @@ -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'; diff --git a/internal/db/migrator/goose/sqlite/202607200001_add_openresty_default_limit_req_per_ip.sql b/internal/db/migrator/goose/sqlite/202607200001_add_openresty_default_limit_req_per_ip.sql new file mode 100644 index 00000000..553265ef --- /dev/null +++ b/internal/db/migrator/goose/sqlite/202607200001_add_openresty_default_limit_req_per_ip.sql @@ -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'; diff --git a/internal/model/openflare_proxy_route.go b/internal/model/openflare_proxy_route.go index b6b7d886..41da0e93 100644 --- a/internal/model/openflare_proxy_route.go +++ b/internal/model/openflare_proxy_route.go @@ -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, diff --git a/internal/model/system_configs.go b/internal/model/system_configs.go index fd9ba9d3..746f87d6 100644 --- a/internal/model/system_configs.go +++ b/internal/model/system_configs.go @@ -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 ( diff --git a/pkg/render/openresty/render.go b/pkg/render/openresty/render.go index e17f1963..41c1d0e9 100644 --- a/pkg/render/openresty/render.go +++ b/pkg/render/openresty/render.go @@ -201,7 +201,7 @@ func renderTemplateDirective(enabled bool, statement string) string { } func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot) string { - lines := []string{renderOpenRestyLimitZoneBlock()} + lines := []string{renderOpenRestyLimitZoneBlock(cfg)} if !cfg.CacheEnabled { lines = append(lines, renderOpenRestyObservabilityTemplateBlock()) return strings.Join(lines, "") @@ -222,8 +222,14 @@ func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot) string { return strings.Join(lines, "") } -func renderOpenRestyLimitZoneBlock() string { - return " limit_conn_zone $server_name zone=openflare_conn_per_server:10m;\n limit_conn_zone $binary_remote_addr zone=openflare_conn_per_ip:10m;\n" +func renderOpenRestyLimitZoneBlock(cfg ConfigSnapshot) string { + var builder strings.Builder + builder.WriteString(" limit_conn_zone $server_name zone=openflare_conn_per_server:10m;\n") + builder.WriteString(" limit_conn_zone $binary_remote_addr zone=openflare_conn_per_ip:10m;\n") + if strings.TrimSpace(cfg.DefaultLimitReqPerIP) != "" { + fmt.Fprintf(&builder, " limit_req_zone $binary_remote_addr zone=openflare_req_per_ip:10m rate=%s;\n", strings.TrimSpace(cfg.DefaultLimitReqPerIP)) + } + return builder.String() } func renderOpenRestyObservabilityTemplateBlock() string { @@ -475,6 +481,11 @@ func renderRouteLimitBlock(limitConfig routeLimitConfig) string { if strings.TrimSpace(limitConfig.LimitRate) != "" { fmt.Fprintf(&builder, " limit_rate %s;\n", limitConfig.LimitRate) } + if strings.TrimSpace(limitConfig.LimitReqPerIP) != "" { + burst := calculateBurst(limitConfig.LimitReqPerIP) + fmt.Fprintf(&builder, " limit_req zone=openflare_req_per_ip burst=%d nodelay;\n", burst) + fmt.Fprintf(&builder, " limit_req_status 429;\n") + } return builder.String() } @@ -483,6 +494,7 @@ func mergeRouteLimitConfig(route Route, cfg ConfigSnapshot) routeLimitConfig { LimitConnPerServer: mergeLimitConn(route.LimitConnPerServer, cfg.DefaultLimitConnPerServer), LimitConnPerIP: mergeLimitConn(route.LimitConnPerIP, cfg.DefaultLimitConnPerIP), LimitRate: mergeLimitRate(route.LimitRate, cfg.DefaultLimitRate), + LimitReqPerIP: mergeLimitRate(route.LimitReqPerIP, cfg.DefaultLimitReqPerIP), } } @@ -864,3 +876,29 @@ func buildPathExactMatchPattern(rules []string) string { } return fmt.Sprintf("^(?:%s)$", strings.Join(parts, "|")) } + +func calculateBurst(rateStr string) int { + rateStr = strings.ToLower(strings.TrimSpace(rateStr)) + if rateStr == "" { + return 0 + } + var val int + var unit string + _, err := fmt.Sscanf(rateStr, "%dr/%s", &val, &unit) + if err != nil { + return 5 + } + if val <= 0 { + return 5 + } + if unit == "s" { + return val * 2 + } else if unit == "m" { + b := val / 5 + if b < 5 { + b = 5 + } + return b + } + return 5 +} diff --git a/pkg/render/openresty/render_test.go b/pkg/render/openresty/render_test.go index d6dcfa7a..d5bc1f3d 100644 --- a/pkg/render/openresty/render_test.go +++ b/pkg/render/openresty/render_test.go @@ -464,16 +464,18 @@ func TestMergeRouteLimitConfig(t *testing.T) { DefaultLimitConnPerServer: 100, DefaultLimitConnPerIP: 10, DefaultLimitRate: "512k", + DefaultLimitReqPerIP: "10r/s", }, - want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 10, LimitRate: "512k"}, + want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 10, LimitRate: "512k", LimitReqPerIP: "10r/s"}, }, { name: "explicit off ignores default", - route: Route{LimitConnPerServer: -1, LimitConnPerIP: -1, LimitRate: "-1"}, + route: Route{LimitConnPerServer: -1, LimitConnPerIP: -1, LimitRate: "-1", LimitReqPerIP: "-1"}, cfg: ConfigSnapshot{ DefaultLimitConnPerServer: 100, DefaultLimitConnPerIP: 10, DefaultLimitRate: "512k", + DefaultLimitReqPerIP: "10r/s", }, want: routeLimitConfig{}, }, @@ -484,8 +486,9 @@ func TestMergeRouteLimitConfig(t *testing.T) { DefaultLimitConnPerServer: 100, DefaultLimitConnPerIP: 10, DefaultLimitRate: "512k", + DefaultLimitReqPerIP: "10r/s", }, - want: routeLimitConfig{LimitConnPerServer: 50, LimitConnPerIP: 5, LimitRate: "1m"}, + want: routeLimitConfig{LimitConnPerServer: 50, LimitConnPerIP: 5, LimitRate: "1m", LimitReqPerIP: "10r/s"}, }, { name: "partial inherit", @@ -494,8 +497,9 @@ func TestMergeRouteLimitConfig(t *testing.T) { DefaultLimitConnPerServer: 100, DefaultLimitConnPerIP: 10, DefaultLimitRate: "256k", + DefaultLimitReqPerIP: "10r/s", }, - want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 0, LimitRate: "256k"}, + want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 0, LimitRate: "256k", LimitReqPerIP: "10r/s"}, }, } for _, tc := range cases { @@ -522,6 +526,7 @@ func TestRenderRouteConfigAppliesDefaultLimits(t *testing.T) { DefaultLimitConnPerServer: 120, DefaultLimitConnPerIP: 12, DefaultLimitRate: "512k", + DefaultLimitReqPerIP: "10r/s", }, } rendered, err := RenderRouteConfig(doc, nil) @@ -532,6 +537,8 @@ func TestRenderRouteConfigAppliesDefaultLimits(t *testing.T) { "limit_conn openflare_conn_per_server 120;", "limit_conn openflare_conn_per_ip 12;", "limit_rate 512k;", + "limit_req zone=openflare_req_per_ip burst=20 nodelay;", + "limit_req_status 429;", } { if !strings.Contains(rendered, want) { t.Fatalf("expected %q in route config, got:\n%s", want, rendered) @@ -550,18 +557,20 @@ func TestRenderRouteConfigExplicitOffSkipsDefaultLimits(t *testing.T) { LimitConnPerServer: -1, LimitConnPerIP: -1, LimitRate: "-1", + LimitReqPerIP: "-1", }}, OpenRestyConfig: ConfigSnapshot{ DefaultLimitConnPerServer: 120, DefaultLimitConnPerIP: 12, DefaultLimitRate: "512k", + DefaultLimitReqPerIP: "10r/s", }, } rendered, err := RenderRouteConfig(doc, nil) if err != nil { t.Fatalf("RenderRouteConfig() error = %v", err) } - if strings.Contains(rendered, "limit_conn") || strings.Contains(rendered, "limit_rate") { + if strings.Contains(rendered, "limit_conn") || strings.Contains(rendered, "limit_rate") || strings.Contains(rendered, "limit_req") { t.Fatalf("expected no limit directives, got:\n%s", rendered) } } diff --git a/pkg/render/openresty/types.go b/pkg/render/openresty/types.go index 86bb7714..5bb55e9b 100644 --- a/pkg/render/openresty/types.go +++ b/pkg/render/openresty/types.go @@ -165,6 +165,7 @@ type Route 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"` @@ -312,6 +313,7 @@ type ConfigSnapshot 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"` } // Document is the top-level input structure for the OpenResty renderer, @@ -341,6 +343,7 @@ type routeLimitConfig struct { LimitConnPerServer int LimitConnPerIP int LimitRate string + LimitReqPerIP string } type routeUpstreamConfig struct {