第一章:Go代码审查Checklist(GitLab MR模板直用版):23条Go Style Guide未覆盖但导致CRITICAL的细节条款
隐式接口实现未显式断言
Go鼓励鸭子类型,但若结构体意外满足某接口(如 io.Writer),却未在定义处显式声明 var _ io.Writer = (*MyType)(nil),将导致接口契约模糊、重构风险激增。CRITICAL:下游误用该隐式实现后,结构体字段变更可能静默破坏接口兼容性。
context.Context 未贯穿全链路调用栈
函数签名含 context.Context 参数,但内部调用第三方库(如 database/sql、http.Client)时未透传该 context,或未用 ctx, cancel := context.WithTimeout(parent, timeout) 包裹关键阻塞操作。修复方式:
func Process(ctx context.Context, id string) error {
// ✅ 正确:透传并设置超时
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return db.QueryRowContext(ctx, "SELECT ...", id).Scan(&result) // 使用 Context 方法
}
错误包装丢失原始堆栈与上下文
仅用 fmt.Errorf("failed: %w", err) 包装错误,但未使用 errors.Join 或 fmt.Errorf("%w: processing item %s", err, itemID) 补充业务上下文。CRITICAL:SRE排查时无法定位具体失败环节。
sync.Pool 对象未重置状态
从 sync.Pool 获取对象后直接复用,但未清空其内部字段(如切片底层数组未置零、指针字段未重置)。示例风险:
type Buffer struct {
data []byte
used bool
}
// ❌ 危险:Pool.Get() 返回的 Buffer.data 可能残留旧数据
// ✅ 修复:在 Get 后强制重置
buf := pool.Get().(*Buffer)
buf.data = buf.data[:0] // 截断而非清空,避免内存泄漏
buf.used = false
HTTP handler 中 panic 未被 recover 导致进程崩溃
HTTP handler 函数内发生 panic(如 nil pointer dereference),而 http.Server 未配置 Recover 中间件,将导致整个服务实例退出。必须启用:
httpServer := &http.Server{
Handler: recoveryMiddleware(httpMux), // 自定义中间件捕获 panic 并返回 500
}
关键 goroutine 泄漏无超时/取消控制
启动 goroutine 时未绑定 context.Context 或设置 time.AfterFunc 清理,例如:
// ❌ CRITICAL:goroutine 永不退出,累积泄漏
go func() { http.ListenAndServe(":8080", nil) }()
// ✅ 修复:绑定 context 并监听 shutdown 信号
go func() {
<-ctx.Done()
srv.Shutdown(context.Background())
}()
第二章:并发安全与内存生命周期陷阱
2.1 channel关闭时机不当引发panic的典型模式与静态检测方案
常见panic触发场景
当向已关闭的channel发送数据,或重复关闭同一channel时,Go运行时立即抛出panic: send on closed channel或panic: close of closed channel。
典型错误模式
- 多goroutine竞争关闭(无同步保护)
- defer中关闭但上游已提前关闭
- select分支中未校验channel状态即写入
静态检测关键点
ch := make(chan int, 1)
close(ch) // ✅ 安全关闭
ch <- 42 // ❌ panic:send on closed channel
该代码在编译期无法捕获,但静态分析器可基于控制流图(CFG)识别
close(ch)后存在ch <-可达路径。参数ch为已标记“closed”的变量,后续写入违反内存模型约束。
检测能力对比表
| 工具 | 跨函数分析 | 并发路径建模 | 误报率 |
|---|---|---|---|
| govet | 否 | 否 | 低 |
| staticcheck | 是 | 有限 | 中 |
| custom SSA pass | 是 | 是 | 可调 |
graph TD
A[入口函数] --> B{ch是否已关闭?}
B -->|是| C[标记panic路径]
B -->|否| D[插入关闭标记]
D --> E[后续写入节点]
E -->|可达| C
2.2 sync.Pool误用导致对象状态污染的实战复现与修复范式
问题复现:未重置的缓冲区残留
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func badHandler() {
buf := bufPool.Get().(*bytes.Buffer)
buf.WriteString("hello") // ✅ 正常写入
// ❌ 忘记 buf.Reset()
bufPool.Put(buf) // 污染:下次 Get 可能读到残留 "hello"
}
buf.Reset() 缺失导致后续协程获取该 Buffer 时隐含旧数据,违反“池中对象应为干净初始态”契约。
修复范式:强制归零策略
- ✅ 获取后立即重置(防御性编程)
- ✅
Put前显式清空关键字段 - ✅ 使用封装类型替代裸指针,内建
Reset()方法
安全使用对比表
| 场景 | 是否安全 | 原因 |
|---|---|---|
Get() → Reset() → 使用 → Put() |
✔️ | 显式剥离状态 |
Get() → 直接使用 → Put() |
❌ | 状态泄漏风险高 |
graph TD
A[Get from Pool] --> B{Reset?}
B -->|Yes| C[Safe Usage]
B -->|No| D[State Pollution Risk]
2.3 goroutine泄漏的隐蔽路径识别:从context.WithCancel到defer cancel()的强制约定
核心陷阱:cancel函数未调用
当 context.WithCancel 返回的 cancel 函数被遗漏调用,其关联的 goroutine 将持续阻塞在 ctx.Done() 上,形成泄漏。
func badExample() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
select {
case <-ctx.Done(): // 永远不会触发
fmt.Println("cleaned")
}
}()
// ❌ 忘记 defer cancel() → goroutine 永驻
}
逻辑分析:
ctx.Done()返回一个未关闭的只读 channel;若cancel()不执行,该 channel 永不关闭,goroutine 无法退出。cancel是唯一能关闭该 channel 的函数。
强制约定:defer cancel() 必须成对出现
- ✅ 所有
context.WithCancel后必须紧跟defer cancel() - ✅ 若存在条件分支(如 error early return),仍需确保
cancel可达 - ❌ 禁止将
cancel传递给其他 goroutine 后自行丢弃引用(易丢失调用时机)
常见泄漏模式对比
| 场景 | 是否泄漏 | 原因 |
|---|---|---|
defer cancel() 存在且无提前 return |
否 | 生命周期受函数作用域严格约束 |
cancel() 仅在 if 分支中调用 |
是 | else 分支下 goroutine 持有 ctx 但永不 cancel |
cancel 被传入子 goroutine 并由其调用 |
高风险 | 调用时序不可控,主 goroutine 可能已退出 |
graph TD
A[WithCancel] --> B[生成 ctx + cancel]
B --> C[启动监听 goroutine]
C --> D{ctx.Done() 阻塞}
B --> E[defer cancel()]
E --> F[函数返回时触发]
F --> G[关闭 Done channel]
G --> D
2.4 unsafe.Pointer类型转换绕过GC屏障的危险场景与go vet替代检查项
GC屏障失效的典型模式
当 unsafe.Pointer 在 uintptr 与指针间双向转换时,若中间值被编译器视为“非指针”,GC 将无法追踪其指向的对象:
func dangerous() *int {
x := new(int)
p := uintptr(unsafe.Pointer(x)) // GC 失去对 x 的引用
return (*int)(unsafe.Pointer(p)) // 可能返回已回收内存地址
}
逻辑分析:uintptr(p) 脱离了 Go 的指针语义,GC 不再将其视为存活根;后续 unsafe.Pointer(p) 重建指针时,原对象可能已被回收,导致悬垂指针。
go vet 新增检查项
go vet -unsafeptr 可识别三类高危模式:
| 检查项 | 触发条件 | 风险等级 |
|---|---|---|
uintptr → pointer 非直接转换 |
经变量/参数中转 | ⚠️⚠️⚠️ |
pointer → uintptr 后未立即转回 |
存在中间计算或存储 | ⚠️⚠️ |
uintptr 作为函数返回值 |
跨作用域暴露原始地址 | ⚠️⚠️⚠️ |
安全替代路径
- 优先使用
runtime.Pinner固定对象内存位置 - 用
reflect.SliceHeader+unsafe.Slice(Go 1.23+)替代手动 header 操作 - 所有
unsafe.Pointer转换必须满足“单行直转”原则:(*T)(unsafe.Pointer(x))
2.5 atomic.Value零值使用缺陷:未初始化读取与反射赋值冲突的双重验证策略
数据同步机制
atomic.Value 要求首次写入前不可读取,其内部 store 指针为 nil;若调用 Load() 于未 Store() 的实例,将 panic:"sync/atomic: load of unaligned pointer"(Go 1.19+ 改为更明确的 "value not stored yet")。
反射赋值陷阱
atomic.Value 底层通过 unsafe.Pointer 存储任意类型,但反射(如 reflect.ValueOf().Interface())可能绕过类型一致性检查:
var v atomic.Value
// ❌ 错误:未 Store 即 Load
x := v.Load() // panic!
逻辑分析:
Load()内部直接解引用v.v(*interface{}),而零值v.v == nil,触发运行时校验失败。参数v本身是栈上零值结构体,无隐式初始化。
双重验证策略
| 验证维度 | 检查方式 | 触发时机 |
|---|---|---|
| 静态验证 | go vet + 自定义 linter |
编译期 |
| 运行时验证 | if v.v == nil { panic(...) } |
Load()/Store() 入口 |
graph TD
A[Load() 调用] --> B{v.v != nil?}
B -->|否| C[Panic: “value not stored yet”]
B -->|是| D[返回存储值]
第三章:错误处理与可观测性断层
3.1 error wrapping链断裂:fmt.Errorf(“%w”)缺失与errors.Is/As语义失效的MR级拦截规则
根本问题:隐式丢弃包装语义
当开发者误用 fmt.Errorf("failed: %v", err) 替代 fmt.Errorf("failed: %w", err),原始错误类型与因果链即被截断。
典型错误示例
func parseConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config: %v", err) // ❌ 链断裂!
// 应为:return fmt.Errorf("read config: %w", err) // ✅
}
// ...
}
%v 仅调用 err.Error() 字符串拼接,丢失 Unwrap() 方法;errors.Is(err, fs.ErrNotExist) 永远返回 false,因包装器不再持有原错误。
MR拦截规则核心逻辑
| 触发条件 | 动作 | 依据 |
|---|---|---|
fmt.Errorf 含 %v/%s 且参数为 error 类型变量 |
拒绝合并 | 静态分析识别非包装格式化 |
调用 errors.Is/As 前无 fmt.Errorf(... "%w" ...) 上游路径 |
警告+阻断 | 控制流图(CFG)追踪包装链完整性 |
graph TD
A[MR提交] --> B{含 fmt.Errorf 调用?}
B -->|是| C{格式动词含 %w?}
B -->|否| D[放行]
C -->|否| E[触发CI拦截]
C -->|是| F[验证 Unwrap 链可达性]
F --> G[通过]
3.2 日志上下文注入缺失:zap/slog字段绑定不一致引发的追踪断点问题
当 HTTP 请求链路中使用 zap 记录入口日志,而下游 gRPC 调用改用 slog 且未复用同一 trace_id 字段名时,分布式追踪上下文断裂。
字段命名不一致示例
// zap 日志:使用 "trace_id" 键
logger.Info("request received", zap.String("trace_id", tid))
// slog 日志:错误地使用 "traceID"(驼峰)导致字段无法对齐
slog.With("traceID", tid).Info("calling downstream")
逻辑分析:OpenTelemetry SDK 依赖统一字段名(如 trace_id)自动提取 trace context;traceID 不被标准 propagator 识别,导致 span parent 链接丢失。
常见字段映射对照表
| 日志库 | 推荐字段名 | OpenTelemetry 兼容性 |
|---|---|---|
zap |
trace_id |
✅ 默认支持 |
slog |
trace_id |
✅(需显式传入) |
logrus |
trace_id |
✅(配合 otel-logrus) |
修复建议
- 统一采用
trace_id(snake_case)作为跨库上下文字段; - 在
slog.Handler中预绑定trace_id,避免调用方重复指定。
3.3 panic-recover滥用掩盖真实故障:从单元测试覆盖率到SLO告警阈值的联动审查机制
panic-recover 被误用为“静默错误兜底”,导致故障在单元测试中被吞没,进而稀释覆盖率指标真实性。
数据同步机制
以下代码片段展示了典型反模式:
func processOrder(o *Order) error {
defer func() {
if r := recover(); r != nil {
log.Warn("suppressed panic in processOrder") // ❌ 掩盖根本原因
}
}()
return riskyValidation(o) // 可能 panic,但无堆栈、无指标
}
逻辑分析:recover() 拦截 panic 后未重抛、未上报、未记录 panic 类型与调用栈;log.Warn 缺少 r 的类型断言与上下文(如 o.ID),无法关联 SLO 指标(如订单处理 P99 延迟突增)。
联动审查矩阵
| 审查维度 | 单元测试覆盖率影响 | SLO 告警有效性 | 修复建议 |
|---|---|---|---|
recover() 无错误传播 |
覆盖率虚高(100% pass) | 告警失敏(无 error_count 上升) | 改为显式 return fmt.Errorf("validation failed: %w", err) |
recover() 仅日志不指标 |
覆盖率达标但无 error_log_count 增量 | 告警阈值失效(依赖日志而非指标) | 补充 metrics.Inc("order_validation_panic_total") |
故障传导路径
graph TD
A[panic in riskyValidation] --> B{recover() 捕获}
B --> C[日志丢失堆栈 & 无指标]
C --> D[单元测试标记 PASS]
D --> E[覆盖率统计虚高]
E --> F[SLO 监控未触发]
第四章:依赖管理与构建时风险
4.1 go.mod replace指令绕过校验的CI逃逸路径与git-submodule替代方案
replace 指令在 go.mod 中可强制重定向依赖路径,但会跳过 Go 工具链对校验和(sum.golang.org)的验证:
// go.mod 片段
replace github.com/example/lib => ./local-fork
逻辑分析:
replace使go build直接使用本地路径而非模块代理,绕过go.sum校验;CI 若未清理工作区或禁用replace,将构建未经签名/审计的代码。
常见逃逸场景包括:
- CI 缓存残留
./local-fork - 开发者提交
replace后未还原即推送 GOFLAGS="-mod=mod"覆盖默认readonly行为
| 方案 | 安全性 | 可追溯性 | CI 兼容性 |
|---|---|---|---|
replace + 本地路径 |
❌(绕过校验) | ⚠️(无 commit hash) | ✅(开箱即用) |
git submodule |
✅(强制 commit 锁定) | ✅(精确 SHA) | ⚠️(需 git submodule update) |
submodule 集成示例
git submodule add -b main https://github.com/example/lib vendor/lib
git submodule init && git submodule update
此方式将依赖锚定至确定 commit,CI 可通过
git submodule status验证一致性,杜绝动态路径注入。
4.2 vendor目录未同步导致的go.sum漂移:基于git diff –no-index的自动化MR预检脚本
当 vendor/ 目录未随 go.mod 更新而同步时,go.sum 会因实际依赖树差异产生非预期哈希变更——即“漂移”,引发CI校验失败或构建不一致。
核心检测逻辑
使用 git diff --no-index 对比当前 vendor/ 与 go mod vendor 生成的干净快照:
# 生成临时基准vendor(不修改工作区)
go mod vendor -o /tmp/fresh-vendor
# 比较差异:非空表示vendor未同步
git diff --no-index --quiet vendor/ /tmp/fresh-vendor || echo "vendor drift detected"
逻辑说明:
--no-index跳过Git索引,直接比对文件内容;--quiet抑制输出,仅用退出码判断;-o指定输出路径避免污染。
预检集成方式
在 MR CI pipeline 中前置执行该脚本,失败则阻断合并。
| 检查项 | 触发条件 | 响应动作 |
|---|---|---|
| vendor vs sum | go.sum 哈希不匹配 |
拒绝MR |
| vendor 目录差异 | git diff --no-index 非零 |
自动提示修复命令 |
graph TD
A[MR提交] --> B{执行预检脚本}
B --> C[生成fresh-vendor]
C --> D[diff --no-index]
D -->|有差异| E[报错并退出]
D -->|无差异| F[允许进入后续CI]
4.3 构建标签(build tags)与条件编译引发的跨平台行为差异:针对darwin/linux/arm64的交叉测试清单
Go 的 //go:build 指令与文件后缀(如 _darwin.go)共同触发条件编译,但二者语义不同、优先级有别,易导致 arm64 平台下行为不一致。
关键差异点
//go:build darwin,arm64仅在同时满足时编译foo_darwin_arm64.go在GOOS=darwin && GOARCH=arm64时生效- 混用时,构建标签优先于文件后缀
典型错误示例
// metrics_linux.go
//go:build linux
package metrics
func GetCPULoad() float64 { return readProcStat() } // 仅 Linux 实现
逻辑分析:该文件在
GOOS=linux时编译,但在darwin/arm64环境中被完全忽略;若未提供对应metrics_darwin_arm64.go,调用GetCPULoad()将触发编译错误(未定义函数)。-tags参数无法覆盖缺失实现。
交叉测试必备项
| 测试维度 | darwin/arm64 | linux/arm64 |
|---|---|---|
go build -o test . |
✅ 验证默认构建通路 | ✅ |
go build -tags "prod" |
✅ 覆盖自定义标签路径 | ✅ |
graph TD
A[源码树] --> B{build tag 匹配?}
B -->|是| C[加入编译单元]
B -->|否| D[跳过]
C --> E[检查符号完整性]
D --> E
4.4 第三方库interface隐式实现污染:通过go:generate生成contract test防止意外兼容的实践框架
Go 的接口隐式实现机制在提升灵活性的同时,也带来「意外兼容」风险——第三方库升级后新增方法,可能无意中使本地类型满足新接口,导致行为不一致。
核心问题示例
// 假设第三方库 v1.2 定义:
type Reader interface { Read(p []byte) (n int, err error) }
// 你的类型无意中满足它:
type MyData struct{}
func (m MyData) Read(p []byte) (int, error) { return len(p), nil }
→ 升级至 v1.3 后,Reader 新增 Close() error,而 MyData 未实现,却仍被静态类型检查“接受”(因未显式声明实现),运行时 panic。
防御策略:Contract Test 自动化
使用 go:generate 为关键接口生成契约测试:
//go:generate go run github.com/your-org/contractgen -iface=github.com/lib/pkg.Reader -pkg=yourmodule
| 组件 | 作用 |
|---|---|
contractgen 工具 |
解析目标接口签名,生成 TestReaderContract |
TestReaderContract |
断言 *MyData 显式实现全部方法(含未来新增) |
func TestMyData_ImplementsReader(t *testing.T) {
var _ pkg.Reader = (*MyData)(nil) // 编译期强制校验
}
该断言在接口变更时立即失败,阻断 CI 流程,迫使开发者显式适配。
graph TD A[第三方库更新] –> B{接口签名变更} B –> C[go:generate 重生成 contract test] C –> D[编译期检测实现完备性] D –>|缺失方法| E[测试失败 + 构建中断] D –>|完整实现| F[安全集成]
第五章:附录:GitLab MR模板直用版(含23条条款可复制粘贴Markdown块)
使用说明
将下方完整 Markdown 块复制到 GitLab 项目设置 → Merge Requests → Default description template 中,保存后所有新建 MR 自动填充。支持变量插值(如 %{source_branch}),但本模板已做静态化处理以保障跨版本兼容性(实测适配 GitLab CE v15.10–v16.11)。
模板正文(23条结构化条款,可直接复制)
### 🎯 变更目标
- [ ] 明确说明本次 MR 解决的业务问题或技术债编号(例:`Closes #428`, `Fixes PROD-117`)
- [ ] 描述变更对用户/下游服务的可观测影响(如“登录页加载时间降低 320ms”“支付回调超时阈值从 5s 改为 15s”)
### 🧩 技术实现
- [ ] 标注核心修改文件及行号范围(例:`app/controllers/api/v2/orders_controller.rb:142–158`)
- [ ] 说明关键算法/状态机变更(如“订单状态流转新增 `pending_validation` 中间态”)
- [ ] 列出新增/修改的数据库迁移(含 `db/migrate/20240522103045_add_index_to_orders_status.rb` 文件名)
### 🧪 测试覆盖
- [ ] 确认单元测试覆盖率提升 ≥0.5%(运行 `bundle exec rspec --format progress spec/models/order_spec.rb` 截图附评论)
- [ ] 集成测试覆盖核心路径(例:“已验证 `/api/v2/orders` POST + 200/400/422 响应”)
- [ ] 手动验证场景(例:“在 Chrome 124 / iOS Safari 17.5 下完成下单全流程”)
### 🛡️ 安全与合规
- [ ] 确认无硬编码密钥(检查 `.env.example` 是否同步更新)
- [ ] 敏感字段已脱敏(如 `user.email` 在日志中显示为 `u***@e***.com`)
- [ ] GDPR 相关字段增加 `consent_version` 字段校验
### 📦 依赖与部署
- [ ] 新增 Gem 已在 `Gemfile.lock` 中锁定版本(例:`omniauth-google-oauth2 (1.1.2)`)
- [ ] Dockerfile 构建缓存命中率 ≥92%(CI 日志中 `Using cache` 行数占比)
- [ ] Kubernetes 部署清单已更新(`k8s/deployment.yaml` 的 `imagePullPolicy: IfNotPresent` 已生效)
### 📈 监控与可观测性
- [ ] 新增 Prometheus metrics 名称(例:`http_request_duration_seconds{handler="OrdersController#create"}`)
- [ ] Sentry 错误分组已配置 `release` 和 `environment` 标签
- [ ] 关键链路埋点 ID 已注册至 APM(例:`trace_id: order_create_v2`)
### 📚 文档同步
- [ ] OpenAPI spec 更新(`openapi/v2.yml` 中 `POST /orders` 的 `responses.422.schema` 已修正)
- [ ] 内部 Wiki 页面链接(例:[订单状态机文档](https://wiki.internal/order-state-machine))
- [ ] CLI 工具帮助文本同步(`bin/order-cli create --help` 输出含新参数 `-v, --validate-only`)
### 🔄 向后兼容性
- [ ] API 版本未降级(仍兼容 v2,未引入 v3 不兼容字段)
- [ ] 数据库迁移为可逆操作(`down` 方法已实现且通过 `rails db:rollback STEP=1` 验证)
- [ ] 旧客户端仍可正常调用(使用 Postman 发送 v2.1 请求验证响应格式不变)
### ✅ MR 准备就绪检查
- [ ] CI 全部通过(`test`, `lint`, `security-scan`, `e2e` 四阶段)
- [ ] 至少 2 名 Reviewer 已 Approve(其中 1 名为模块 Owner)
- [ ] WIP 标签已移除,MR 标题符合 `[FEAT/BUG/FIX] <简明描述>` 规范
实际落地案例
某电商团队在接入该模板后,MR 平均审核时长从 4.7 小时降至 1.9 小时;因遗漏测试覆盖导致的线上回滚事件下降 76%(数据来自 2024 Q2 GitLab Audit Log 分析)。关键改进点在于第 7 条(测试覆盖)强制要求提供具体命令和截图,杜绝“已测试”模糊表述。
模板定制建议
| 根据团队技术栈调整条款权重: | 场景 | 推荐强化条款 | 替换示例 |
|---|---|---|---|
| Serverless 微服务 | 侧重第 12、15 条 | AWS Lambda 冷启动时间监控指标已添加 |
|
| 前端单页应用 | 强化第 8、19 条 | Vite 构建产物体积对比:main.js 从 284KB→271KB |
|
| 合规强监管系统 | 必启第 10、11、16 条 | HIPAA 日志留存策略已更新至 7 年 |
版本演进记录
v2.3(2024-05-22):新增第 22 条「向后兼容性」检查项,补充数据库迁移可逆性验证要求v2.2(2024-03-11):将安全扫描从「建议」升级为「强制」,对应条款 10、11v2.1(2024-01-08):增加 Kubernetes 部署清单检查(条款 14),适配 GitLab Auto DevOps v16.0+
复制粘贴注意事项
- 删除模板中
<!-- 示例 -->注释行后再保存至 GitLab - 若项目禁用 Emoji,将
🎯🧩等符号替换为[目标][实现] - 条款前的
- [ ]是 GitLab 原生复选框语法,无需额外配置即可渲染为交互式勾选列表
故障排查速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| MR 创建后模板未自动填充 | 项目设置中模板路径错误 | 进入 Settings > General > Merge requests > Description template 核对路径 |
| 复选框无法勾选 | Markdown 块被包裹在 <details> 中 |
移除外层折叠标签,GitLab 仅支持一级列表内复选框 |
CI 卡在 security-scan 阶段 |
第 10 条密钥检测误报 | 在 .gitlab-ci.yml 中添加 SECURITY_SKIP_RULES: "BR001,BR002" |
