mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
feat(rate-limit): 站点级请求频率限制支持继承与自定义
在站点详情流量限制中配置 limit_req_per_ip;渲染按 effective rate 生成多 limit_req_zone,并以站点+IP 隔离计数。
This commit is contained in:
@@ -22,6 +22,10 @@ sidebar: false
|
||||
|
||||
## [unreleased]
|
||||
|
||||
### 新增
|
||||
|
||||
- 反代站点「流量限制」支持配置单 IP 请求频率:空或 0 继承全局默认,-1 关闭,填写如 10r/s、100r/m 为站点自定义;不同站点可使用不同频率并按站点隔离计数。
|
||||
|
||||
## [v3.4.3-beta.3] - 2026-07-20
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# 站点级访问频率限制设计
|
||||
|
||||
日期:2026-07-20
|
||||
状态:已评审待实现
|
||||
方案:站点详情 Limits 暴露 `limit_req_per_ip`;渲染时按 effective rate 生成多 `limit_req_zone`,并用站点键隔离 IP 计数
|
||||
|
||||
## 背景
|
||||
|
||||
全局默认已有:
|
||||
|
||||
- `openresty_default_limit_conn_per_server`
|
||||
- `openresty_default_limit_conn_per_ip`
|
||||
- `openresty_default_limit_rate`
|
||||
- `openresty_default_limit_req_per_ip`
|
||||
|
||||
站点级并发/带宽已在「反代站点详情 → 流量限制」中配置,语义为:空/`0` 继承、`-1` 关闭、自定义覆盖。
|
||||
|
||||
请求频率(`limit_req`)后端字段与 merge 已存在,但:
|
||||
|
||||
1. 前端站点详情未暴露 `limit_req_per_ip`
|
||||
2. 渲染侧仅在全局默认非空时输出**单一** `limit_req_zone ... rate=全局值`,站点自定义 rate 无法真正独立生效(nginx 的 rate 写在 zone 上,不能仅靠 location 覆盖)
|
||||
|
||||
## 目标
|
||||
|
||||
1. 在**仅站点详情「流量限制」区块**配置单 IP 请求频率。
|
||||
2. 语义与现有三项一致:空/`0` 继承全局;`-1` 关闭;合法 `Nr/s` / `Nr/m` 为站点自定义。
|
||||
3. 站点自定义 rate **真正按该 rate 生效**(A 站 5r/s、B 站 10r/s 互不影响)。
|
||||
4. 同 IP 在不同站点的频率配额**按站点隔离**。
|
||||
5. 修改后仍需发布配置版本;Agent 使用与 Server 同源的 render 路径。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 在「安全性 → 限流」页增加按站点列表编辑
|
||||
- 新建站点表单中的频率字段
|
||||
- 按路径 / URI 差异化频率限制
|
||||
- 改变 `limit_conn_*` / `limit_rate` 的现有 zone 与合并模型
|
||||
- 业务 Zone(顶级域 + 二级域名资源)模型变更
|
||||
|
||||
## 语义
|
||||
|
||||
### 站点字段 `limit_req_per_ip`(字符串)
|
||||
|
||||
| 值 | 含义 |
|
||||
|----|------|
|
||||
| 空 / `"0"` | 继承全局 `openresty_default_limit_req_per_ip` |
|
||||
| `"-1"` | 本站显式关闭频率限制 |
|
||||
| `^\d+r/[sm]$`(大小写不敏感,存小写) | 本站自定义 rate |
|
||||
|
||||
### 全局默认
|
||||
|
||||
| 值 | 含义 |
|
||||
|----|------|
|
||||
| 空 / `"0"` | 默认关闭;继承方亦不输出 `limit_req` |
|
||||
| 合法 rate | 未配置站点的 effective rate |
|
||||
|
||||
### 合并(与现有 `mergeLimitRate` 一致)
|
||||
|
||||
```
|
||||
if route == -1: effective = off
|
||||
else if route is set: effective = route // 合法 rate
|
||||
else: effective = global // route 空/0
|
||||
// global 空/0 → off
|
||||
```
|
||||
|
||||
## 渲染
|
||||
|
||||
### 问题
|
||||
|
||||
nginx `limit_req_zone` 的 `rate=` 在 zone 声明时固定;多个站点若 effective rate 不同,必须使用不同 zone。
|
||||
|
||||
### 步骤
|
||||
|
||||
1. 在 `RenderRouteConfig` / main 配置生成前,对全部 route 计算 effective `LimitReqPerIP`。
|
||||
2. 收集非空 effective rate 的**去重集合**,在 `http {}`(`renderOpenRestyLimitZoneBlock` 扩展,需能访问 routes 或 precomputed rates)输出:
|
||||
|
||||
```nginx
|
||||
# 变量键:站点名 + IP,保证跨站点计数隔离
|
||||
# 实现可用 map 或在 server 内 set 后引用;zone key 采用组合键
|
||||
limit_req_zone $openflare_req_key zone=openflare_req_<rate_token>:10m rate=<rate>;
|
||||
```
|
||||
|
||||
`rate_token` 由 rate 规范化生成(如 `10r/s` → `10rs`,`100r/m` → `100rm`),仅作 zone 名片段,合法 nginx zone 名。
|
||||
|
||||
3. 每个业务 server 在 access 相关位置之前设置:
|
||||
|
||||
```nginx
|
||||
set $openflare_req_key "$openflare_waf_site$binary_remote_addr";
|
||||
```
|
||||
|
||||
(与现有 `set $openflare_waf_site "..."` 同源 site_name;若某 server 无 waf site 变量则用同一 displayName/site_name。)
|
||||
|
||||
4. `renderRouteLimitBlock` 在 effective rate 非空时输出:
|
||||
|
||||
```nginx
|
||||
limit_req zone=openflare_req_<rate_token> burst=<calculateBurst> nodelay;
|
||||
limit_req_status 429;
|
||||
```
|
||||
|
||||
5. **无任何** effective rate 时:不输出任何 `limit_req_zone` / `limit_req`(避免引用不存在的 zone)。
|
||||
|
||||
6. 应用范围与现有 limit 块一致:HTTP/HTTPS 反代 `location /`、Pages 相关 location;不含 HTTP→HTTPS 重定向-only server。
|
||||
|
||||
### 与旧行为差异
|
||||
|
||||
| 项 | 旧 | 新 |
|
||||
|----|----|----|
|
||||
| zone 数量 | 全局最多 1 个 | 按不同 effective rate 多个 |
|
||||
| zone key | `$binary_remote_addr` | `$openflare_req_key`(站点+IP) |
|
||||
| 站点自定义 rate | 无法真正独立 | 引用对应 rate 的 zone |
|
||||
|
||||
快照 JSON **仍保留站点原始值**(含空/`-1`),不把 merge 结果写回 route。
|
||||
|
||||
## 数据与 API
|
||||
|
||||
- 列 `of_proxy_routes.limit_req_per_ip` 已存在;无新迁移(若环境已跑过既有迁移)。
|
||||
- API `Input` / `View` 已有字段;normalize / 校验已存在。
|
||||
- 前端类型与详情表单补齐即可。
|
||||
|
||||
## 前端
|
||||
|
||||
仅改站点详情 `limits-section.tsx`:
|
||||
|
||||
- 增加「单 IP 请求频率」输入
|
||||
- 校验:空、`0`、`-1`、或 `^\d+r/[sm]$i`
|
||||
- 规范化:trim + lower;`0` → `""`
|
||||
- `ProxyRouteItem` / `ProxyRouteMutationPayload` 增加 `limit_req_per_ip`
|
||||
- `buildPayloadFromRoute` 带上该字段,避免其它区块保存时丢失
|
||||
|
||||
文案:与并发/带宽一致(空或 0 继承;-1 关闭;例如 10r/s、100r/m 自定义)。
|
||||
|
||||
## Agent / 发布
|
||||
|
||||
- 配置保存后须**发布配置版本**
|
||||
- Agent **本地** `RenderJSON`;必须部署含本设计 render 的 Agent,否则 source 有字段但 conf 无指令
|
||||
- 若 Agent 已记录同 version/checksum,升级二进制后需触发重新 apply(重启或强制重同步)
|
||||
|
||||
## 测试
|
||||
|
||||
- `mergeRouteLimitConfig`:继承 / 覆盖 / `-1`(已有则补 rate 断言)
|
||||
- 多站点不同 effective rate:main conf 含多个 `limit_req_zone`,各 location 引用正确 zone 名
|
||||
- 全关闭:无 `limit_req` 相关指令
|
||||
- 仅全局有值:一个 zone + 未自定义站点引用该 zone
|
||||
- 前端类型与表单校验(手工或既有模式)
|
||||
|
||||
## 验收
|
||||
|
||||
1. 全局 `10r/s`,站点空 → 该站 location 有 limit_req,zone rate=10r/s
|
||||
2. 站点改 `5r/s` 并发布 → 该站引用 5r/s zone
|
||||
3. 站点 `-1` → 该站无 limit_req
|
||||
4. 两站不同 rate,同 IP 压测互不抢同一配额
|
||||
|
||||
## 实现边界
|
||||
|
||||
| 层 | 工作量 |
|
||||
|----|--------|
|
||||
| 渲染 `pkg/render/openresty` | 多 zone + 站点键 + location 引用 |
|
||||
| 前端详情 Limits + types + payload | 补字段 |
|
||||
| 后端 API/DB | 已具备,仅回归 |
|
||||
| 文档/changelog | 用户可见变更记中文 changelog |
|
||||
@@ -278,6 +278,25 @@ export function normalizeLimitRate(value: string) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const limitReqPattern = /^\d+r\/[sm]$/i;
|
||||
|
||||
export function validateLimitReqPerIP(value: string) {
|
||||
const normalized = value.trim();
|
||||
if (!normalized || normalized === '0' || normalized === '-1') {
|
||||
return null;
|
||||
}
|
||||
if (!limitReqPattern.test(normalized)) {
|
||||
return '请求频率格式不合法,请使用 10r/s、100r/m,或 -1 关闭';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeLimitReqPerIP(value: string) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized || normalized === '0') return '';
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function validateCacheRules(
|
||||
policy: 'static' | 'all' | 'url' | 'suffix' | 'path_prefix' | 'path_exact',
|
||||
rules: string[],
|
||||
@@ -335,6 +354,7 @@ export function buildPayloadFromRoute(
|
||||
limit_conn_per_server: route.limit_conn_per_server,
|
||||
limit_conn_per_ip: route.limit_conn_per_ip,
|
||||
limit_rate: route.limit_rate,
|
||||
limit_req_per_ip: route.limit_req_per_ip,
|
||||
cache_enabled: route.cache_enabled,
|
||||
cache_policy: (() => {
|
||||
if (!route.cache_enabled) {
|
||||
|
||||
@@ -228,6 +228,7 @@ export function ProxyRouteCreateSheet({
|
||||
limit_conn_per_server: 0,
|
||||
limit_conn_per_ip: 0,
|
||||
limit_rate: '',
|
||||
limit_req_per_ip: '',
|
||||
cache_enabled: true,
|
||||
cache_policy: 'static',
|
||||
cache_rules: [],
|
||||
|
||||
@@ -19,7 +19,9 @@ import type { ProxyRouteItem } from '@/lib/services/openflare';
|
||||
|
||||
import {
|
||||
normalizeLimitRate,
|
||||
normalizeLimitReqPerIP,
|
||||
validateLimitRate,
|
||||
validateLimitReqPerIP,
|
||||
} from '../../components/helpers';
|
||||
import { proxyRouteFormIds } from '../helpers';
|
||||
import { useRouteSectionSave } from '../hooks/use-route-section-save';
|
||||
@@ -30,6 +32,7 @@ const rateLimitSchema = z
|
||||
limit_conn_per_server: z.string(),
|
||||
limit_conn_per_ip: z.string(),
|
||||
limit_rate: z.string(),
|
||||
limit_req_per_ip: z.string(),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
for (const field of [
|
||||
@@ -57,6 +60,15 @@ const rateLimitSchema = z
|
||||
message: limitRateError,
|
||||
});
|
||||
}
|
||||
|
||||
const limitReqError = validateLimitReqPerIP(value.limit_req_per_ip);
|
||||
if (limitReqError) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['limit_req_per_ip'],
|
||||
message: limitReqError,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type RateLimitValues = z.infer<typeof rateLimitSchema>;
|
||||
@@ -99,6 +111,7 @@ export function LimitsSection({
|
||||
limit_conn_per_server: formatConnValue(route.limit_conn_per_server),
|
||||
limit_conn_per_ip: formatConnValue(route.limit_conn_per_ip),
|
||||
limit_rate: route.limit_rate || '',
|
||||
limit_req_per_ip: route.limit_req_per_ip || '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -107,6 +120,7 @@ export function LimitsSection({
|
||||
limit_conn_per_server: formatConnValue(route.limit_conn_per_server),
|
||||
limit_conn_per_ip: formatConnValue(route.limit_conn_per_ip),
|
||||
limit_rate: route.limit_rate || '',
|
||||
limit_req_per_ip: route.limit_req_per_ip || '',
|
||||
});
|
||||
}, [form, route]);
|
||||
|
||||
@@ -129,6 +143,9 @@ export function LimitsSection({
|
||||
),
|
||||
limit_conn_per_ip: parseConnValue(values.limit_conn_per_ip),
|
||||
limit_rate: normalizeLimitRate(values.limit_rate),
|
||||
limit_req_per_ip: normalizeLimitReqPerIP(
|
||||
values.limit_req_per_ip,
|
||||
),
|
||||
},
|
||||
'流量限制已保存',
|
||||
);
|
||||
@@ -172,7 +189,7 @@ export function LimitsSection({
|
||||
control={form.control}
|
||||
name='limit_rate'
|
||||
render={({ field }) => (
|
||||
<FormItem className='md:col-span-2'>
|
||||
<FormItem>
|
||||
<FormLabel>限速</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='512k/1m 或 -1' {...field} />
|
||||
@@ -184,6 +201,24 @@ export function LimitsSection({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='limit_req_per_ip'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>单 IP 请求频率</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='10r/s / 100r/m 或 -1' {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
空或 0 继承全局默认;-1 关闭;例如 10r/s、100r/m
|
||||
为自定义频率。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SectionShell>
|
||||
|
||||
@@ -233,6 +233,7 @@ export interface ProxyRouteItem {
|
||||
limit_conn_per_server: number;
|
||||
limit_conn_per_ip: number;
|
||||
limit_rate: string;
|
||||
limit_req_per_ip: string;
|
||||
cache_enabled: boolean;
|
||||
cache_policy: string;
|
||||
cache_rules: string;
|
||||
@@ -269,6 +270,7 @@ export interface ProxyRouteMutationPayload {
|
||||
limit_conn_per_server?: number;
|
||||
limit_conn_per_ip?: number;
|
||||
limit_rate?: string;
|
||||
limit_req_per_ip?: string;
|
||||
cache_enabled: boolean;
|
||||
cache_policy: string;
|
||||
cache_rules: string[];
|
||||
|
||||
@@ -35,7 +35,7 @@ func RenderJSON(sourceJSON string, certificateFiles []SupportFile) (*Result, err
|
||||
// Render produces a complete OpenResty configuration Result from a Document and
|
||||
// a set of certificate support files.
|
||||
func Render(doc Document, certificateFiles []SupportFile) (*Result, error) {
|
||||
mainConfig := RenderMainConfig(doc.OpenRestyConfig)
|
||||
mainConfig := RenderMainConfig(doc)
|
||||
routeConfig, err := RenderRouteConfig(doc, certificateFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -56,13 +56,15 @@ func Render(doc Document, certificateFiles []SupportFile) (*Result, error) {
|
||||
}
|
||||
|
||||
// RenderMainConfig renders the nginx main configuration string from the given
|
||||
// ConfigSnapshot, falling back to the built-in default template when none is set.
|
||||
func RenderMainConfig(cfg ConfigSnapshot) string {
|
||||
// Document, falling back to the built-in default template when none is set.
|
||||
// Limit-req zones are derived from each route's effective rate after merge.
|
||||
func RenderMainConfig(doc Document) string {
|
||||
cfg := doc.OpenRestyConfig
|
||||
templateText := cfg.MainConfigTemplate
|
||||
if strings.TrimSpace(templateText) == "" {
|
||||
templateText = defaultMainConfigTemplate
|
||||
}
|
||||
return renderMainConfigTemplate(templateText, cfg)
|
||||
return renderMainConfigTemplate(templateText, cfg, collectEffectiveLimitReqRates(doc.Routes, cfg))
|
||||
}
|
||||
|
||||
// ValidateMainConfigTemplate checks that the provided template text is non-empty
|
||||
@@ -157,7 +159,7 @@ func DedupeSupportFiles(files []SupportFile) []SupportFile {
|
||||
return result
|
||||
}
|
||||
|
||||
func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot) string {
|
||||
func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot, limitReqRates []string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
"{{OpenRestyWorkerProcesses}}", cfg.WorkerProcesses,
|
||||
"{{OpenRestyWorkerConnections}}", fmt.Sprintf("%d", cfg.WorkerConnections),
|
||||
@@ -187,7 +189,7 @@ func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot) string {
|
||||
"{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength),
|
||||
"{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel),
|
||||
"{{OpenRestyResolverDirective}}", renderTemplateDirective(cfg.Resolvers != "", fmt.Sprintf("resolver %s;", cfg.Resolvers)),
|
||||
"{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg),
|
||||
"{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg, limitReqRates),
|
||||
"{{OpenRestyRouteConfigInclude}}", RouteConfigPlaceholder,
|
||||
)
|
||||
return replacer.Replace(templateText)
|
||||
@@ -200,8 +202,8 @@ func renderTemplateDirective(enabled bool, statement string) string {
|
||||
return fmt.Sprintf(" %s\n", statement)
|
||||
}
|
||||
|
||||
func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot) string {
|
||||
lines := []string{renderOpenRestyLimitZoneBlock(cfg)}
|
||||
func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot, limitReqRates []string) string {
|
||||
lines := []string{renderOpenRestyLimitZoneBlock(limitReqRates)}
|
||||
if !cfg.CacheEnabled {
|
||||
lines = append(lines, renderOpenRestyObservabilityTemplateBlock())
|
||||
return strings.Join(lines, "")
|
||||
@@ -222,16 +224,47 @@ func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot) string {
|
||||
return strings.Join(lines, "")
|
||||
}
|
||||
|
||||
func renderOpenRestyLimitZoneBlock(cfg ConfigSnapshot) string {
|
||||
func renderOpenRestyLimitZoneBlock(limitReqRates []string) 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))
|
||||
for _, rate := range limitReqRates {
|
||||
fmt.Fprintf(
|
||||
&builder,
|
||||
" limit_req_zone $openflare_waf_site$binary_remote_addr zone=%s:10m rate=%s;\n",
|
||||
limitReqZoneName(rate),
|
||||
rate,
|
||||
)
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func collectEffectiveLimitReqRates(routes []Route, cfg ConfigSnapshot) []string {
|
||||
seen := make(map[string]struct{}, len(routes))
|
||||
for _, route := range routes {
|
||||
rate := strings.TrimSpace(mergeRouteLimitConfig(route, cfg).LimitReqPerIP)
|
||||
if rate == "" {
|
||||
continue
|
||||
}
|
||||
seen[rate] = struct{}{}
|
||||
}
|
||||
if len(seen) == 0 {
|
||||
return nil
|
||||
}
|
||||
rates := make([]string, 0, len(seen))
|
||||
for rate := range seen {
|
||||
rates = append(rates, rate)
|
||||
}
|
||||
sort.Strings(rates)
|
||||
return rates
|
||||
}
|
||||
|
||||
func limitReqZoneName(rate string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(rate))
|
||||
normalized = strings.ReplaceAll(normalized, "/", "")
|
||||
return "openflare_req_" + normalized
|
||||
}
|
||||
|
||||
func renderOpenRestyObservabilityTemplateBlock() string {
|
||||
return fmt.Sprintf(" lua_shared_dict openflare_observability 10m;\n lua_shared_dict openflare_pow_challenges 10m;\n lua_shared_dict openflare_pow_sessions 10m;\n lua_shared_dict openflare_pow_config 1m;\n lua_shared_dict openflare_waf_config 1m;\n lua_shared_dict openflare_waf_ip_groups 64m;\n init_worker_by_lua_file %s/observability/init.lua;\n log_by_lua_file %s/observability/log.lua;\n\n server {\n listen %s;\n server_name openflare-observability;\n access_log off;\n\n location = /openflare/stub_status {\n stub_status;\n }\n\n location = /openflare/observability {\n default_type application/json;\n content_by_lua_file %s/observability/read.lua;\n }\n }\n\n", LuaDirPlaceholder, LuaDirPlaceholder, ObservabilityListenPlaceholder, LuaDirPlaceholder)
|
||||
}
|
||||
@@ -482,8 +515,9 @@ func renderRouteLimitBlock(limitConfig routeLimitConfig) string {
|
||||
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)
|
||||
rate := strings.TrimSpace(limitConfig.LimitReqPerIP)
|
||||
burst := calculateBurst(rate)
|
||||
fmt.Fprintf(&builder, " limit_req zone=%s burst=%d nodelay;\n", limitReqZoneName(rate), burst)
|
||||
fmt.Fprintf(&builder, " limit_req_status 429;\n")
|
||||
}
|
||||
return builder.String()
|
||||
|
||||
@@ -537,7 +537,7 @@ 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 zone=openflare_req_10rs burst=20 nodelay;",
|
||||
"limit_req_status 429;",
|
||||
} {
|
||||
if !strings.Contains(rendered, want) {
|
||||
@@ -546,6 +546,66 @@ func TestRenderRouteConfigAppliesDefaultLimits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMainConfigEmitsLimitReqZonesByEffectiveRate(t *testing.T) {
|
||||
doc := Document{
|
||||
Routes: []Route{
|
||||
{
|
||||
SiteName: "a.example.com",
|
||||
Domains: []string{"a.example.com"},
|
||||
Enabled: true,
|
||||
OriginURL: "http://127.0.0.1:8080",
|
||||
Upstreams: []string{"http://127.0.0.1:8080"},
|
||||
},
|
||||
{
|
||||
SiteName: "b.example.com",
|
||||
Domains: []string{"b.example.com"},
|
||||
Enabled: true,
|
||||
OriginURL: "http://127.0.0.1:8081",
|
||||
Upstreams: []string{"http://127.0.0.1:8081"},
|
||||
LimitReqPerIP: "5r/s",
|
||||
},
|
||||
{
|
||||
SiteName: "c.example.com",
|
||||
Domains: []string{"c.example.com"},
|
||||
Enabled: true,
|
||||
OriginURL: "http://127.0.0.1:8082",
|
||||
Upstreams: []string{"http://127.0.0.1:8082"},
|
||||
LimitReqPerIP: "-1",
|
||||
},
|
||||
},
|
||||
OpenRestyConfig: ConfigSnapshot{
|
||||
DefaultLimitReqPerIP: "10r/s",
|
||||
},
|
||||
}
|
||||
mainConfig := RenderMainConfig(doc)
|
||||
for _, want := range []string{
|
||||
"limit_req_zone $openflare_waf_site$binary_remote_addr zone=openflare_req_10rs:10m rate=10r/s;",
|
||||
"limit_req_zone $openflare_waf_site$binary_remote_addr zone=openflare_req_5rs:10m rate=5r/s;",
|
||||
} {
|
||||
if !strings.Contains(mainConfig, want) {
|
||||
t.Fatalf("expected %q in main config, got:\n%s", want, mainConfig)
|
||||
}
|
||||
}
|
||||
if strings.Contains(mainConfig, "openflare_req_per_ip") {
|
||||
t.Fatalf("unexpected legacy zone name in main config:\n%s", mainConfig)
|
||||
}
|
||||
|
||||
routeConfig, err := RenderRouteConfig(doc, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderRouteConfig() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(routeConfig, "limit_req zone=openflare_req_10rs burst=20 nodelay;") {
|
||||
t.Fatalf("expected inherited zone on route a, got:\n%s", routeConfig)
|
||||
}
|
||||
if !strings.Contains(routeConfig, "limit_req zone=openflare_req_5rs burst=10 nodelay;") {
|
||||
t.Fatalf("expected custom zone on route b, got:\n%s", routeConfig)
|
||||
}
|
||||
// route c is off: count limit_req lines should equal 2 routes * (http+https? depends) — assert c server has no limit_req by site name block is hard; ensure -1 route does not force extra zones
|
||||
if strings.Count(mainConfig, "limit_req_zone") != 2 {
|
||||
t.Fatalf("expected exactly 2 limit_req_zone lines, got main:\n%s", mainConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderRouteConfigExplicitOffSkipsDefaultLimits(t *testing.T) {
|
||||
doc := Document{
|
||||
Routes: []Route{{
|
||||
|
||||
Reference in New Issue
Block a user