Skip to content

feat: add TopSec WAF IP blacklist and URL block service#191

Open
Erosion2020 wants to merge 4 commits into
chaitin:mainfrom
Erosion2020:feat/add-topsec-waf-service
Open

feat: add TopSec WAF IP blacklist and URL block service#191
Erosion2020 wants to merge 4 commits into
chaitin:mainfrom
Erosion2020:feat/add-topsec-waf-service

Conversation

@Erosion2020

Copy link
Copy Markdown

接入设备

项目 说明
设备 TopSec WAF(天融信 Web 应用防火墙)
设备版本 v3.2294.20238_waf
API 版本 v1(RESTful API)
认证方式 Session-based(PHPSESSID + token),密码通过 AES-128-CBC 加密传输

实现方法

本 service package 封装了 TopSec WAF 的 RESTful API,提供 IP 黑名单管理和 URL 自定义策略(ACL)操作,共 7 个 gRPC 方法:

IP 黑名单管理(3 个方法):

方法 说明 类型
AddBlacklistIP 添加 IP 地址到黑名单组
DeleteBlacklistIP 按名称删除黑名单组
ListBlacklistIPs 查询黑名单 IP 列表(支持分页)

URL 拦截规则(4 个方法):

方法 说明 类型
AddUrlBlock 添加 URL 拦截规则(自定义策略),支持 7 种 action
DeleteUrlBlock 按名称删除 URL 拦截规则
ListUrlBlocks 查询 URL 拦截规则列表(支持分页)
SetUrlBlockStatus 启用或禁用 URL 拦截规则

技术实现要点:

  • 使用 @chaitin-ai/octobus-sdkdefineService 封装,运行时模式:long-running
  • Session 管理:自动管理登录会话,session 缓存复用;WAF 返回 401/403 时自动清除旧 session 并重新登录重试一次,对上层透明
  • 超时控制:通过 AbortSignal.timeout() 实现 HTTP 请求超时(config.timeoutMs),超时返回 DEADLINE_EXCEEDED
  • 分页支持ListBlacklistIPsListUrlBlocks 支持 page/rows 分页参数,映射到 WAF API 顶层字段
  • 参数校验:缺失必填字段返回 INVALID_ARGUMENTtemp-redirect/perm-redirect 缺少 action_data 时提前返回 INVALID_ARGUMENT(无需等到 WAF 返回 400)
  • 错误码映射:认证失败 401→UNAUTHENTICATED、403→PERMISSION_DENIED;网络/5xx→UNAVAILABLE;超时→DEADLINE_EXCEEDED;业务失败→FAILED_PRECONDITION
  • 支持 TLS 证书跳过(skipTlsVerify)用于自签证书测试环境

写操作说明

每个写操作均说明了默认参数、幂等语义、回滚方式和审计字段,详见 README

错误码映射

场景 gRPC 状态码 说明
SDK 参数校验(缺必填字段等) INVALID_ARGUMENT 调用前拦截
缺少 action_data(redirect) INVALID_ARGUMENT 客户端校验
登录 401 UNAUTHENTICATED 用户名或密码错误
登录 403 PERMISSION_DENIED 帐号被禁用
WAF API 401/403(session 过期) PERMISSION_DENIED 自动重试登录一次后仍失败
WAF 400 FAILED_PRECONDITION 请求参数错误(WAF 侧)
WAF result: failed FAILED_PRECONDITION 业务逻辑执行失败
HTTP 请求超时 DEADLINE_EXCEEDED fetch 超时
WAF 5xx UNAVAILABLE WAF 服务不可用
网络错误 / 非 JSON 响应 UNAVAILABLE / UNKNOWN 连接失败或非预期格式

测试命令

cd services

# 验证 package 结构
npm run validate -- --service-dir topsec__waf_v3-2294-20238

# 运行测试
npm test -- --service-dir topsec__waf_v3-2294-20238

测试结果

ℹ tests 59
ℹ pass 59
ℹ fail 0

覆盖场景:

  • 内部辅助函数(grpcCodeForgrpcErrreadConfigbuildConditiontoNum
  • IP 黑名单增删查及参数校验
  • IP 黑名单分页参数传递验证
  • URL 拦截规则增删改查及参数校验
  • 7 种 action 类型的默认值和 redirect action_data 校验
  • Session 过期自动重试:WAF 401 触发重新登录并重试成功
  • Session 过期重试耗尽:两次 WAF 401 正确抛出 PERMISSION_DENIED
  • 认证过期(401/403 → PERMISSION_DENIED)
  • 服务不可用(500 → UNAVAILABLE)
  • 非 JSON 响应(UNKNOWN)
  • 业务命令失败(FAILED_PRECONDITION)
  • 登录 401(UNAUTHENTICATED)、403(PERMISSION_DENIED)分离
  • fetch 超时(DEADLINE_EXCEEDED)
  • 无效 host 与缺失认证信息的提前拒绝

已知限制

  1. 当前仅支持 Session-based 认证(PHPSESSID + token),不支持 API Key 方式
  2. IP 黑名单不支持部分 IP 删除,只能整个组删除再重建
  3. URL 拦截规则的 condition 仅支持 contains 操作符
  4. m_type 字段在 WAF 返回中始终为 "__",未能区分 black/white 类型
  5. URL 拦截规则列表使用 waf_url_rewrite_show_name 接口(非 user_policy_show),接口名称与功能不完全匹配(为 TopSec WAF 实际 API 行为)

🤖 Generated with Claude Code

root and others added 4 commits June 24, 2026 08:53
- 7 RPC methods: AddBlacklistIP, DeleteBlacklistIP, ListBlacklistIPs,
  AddUrlBlock, DeleteUrlBlock, ListUrlBlocks, SetUrlBlockStatus
- Session-based auth with AES-128-CBC encrypted login
- Error mapping: 401/403→PERMISSION_DENIED, 4xx→FAILED_PRECONDITION,
  5xx→UNAVAILABLE, param errors→INVALID_ARGUMENT
- 52 unit tests with mock upstream, validate/pack:check passed
- Monorepo integration: bin wrapper, tentacles registration

Co-Authored-By: Claude <noreply@anthropic.com>
…istency

- session: auto re-login on WAF 401/403 with single retry
- timeout: AbortSignal.timeout() for fetch, returns DEADLINE_EXCEEDED
- pagination: page/rows support in ListBlacklistIPs and ListUrlBlocks
- error codes: 401 login -> UNAUTHENTICATED, 403 -> PERMISSION_DENIED
- validation: action_data required for temp-redirect/perm-redirect
- token extraction: robust regex for multiple response formats
- tests: 52 -> 59, added session retry, timeout, pagination, action_data tests
- docs: sync README with current code (paths, error table, file list, limitations)

Co-Authored-By: Claude <noreply@anthropic.com>
@Erosion2020

Copy link
Copy Markdown
Author

真实设备 HTTP 请求/响应验证记录

设备: TopSec WAF v3.2294.20238_waf
地址: https://<WAF_HOST>:8443
认证: Session-based (PHPSESSID + token),密码 AES-128-CBC 加密
证书: 自签证书 (CN=TOPSEC PRODUCTS),验证时需 skipTlsVerify


1. 安全策略列表(通过 POST /api/v1/security_policy_show 获取)

{
  "token": "<token>",
  "commands": [{"waf_security_policy_show": {}}]
}

Response:

{
  "rows": [
    {"ID": "8015", "name": "safety_priority", "protect_level": "high", "template": "true"},
    {"ID": "8016", "name": "standard_security", "protect_level": "middle", "template": "true"},
    {"ID": "8017", "name": "application_priority", "protect_level": "low", "template": "true"},
    {"ID": "33804", "name": "test-acl-policy", "protect_level": "low", "template": "true"},
    ...
  ],
  "total": "7"
}

本次验证使用 test-acl-policy 策略。


2. 认证流程

2.1 GET_MIKS — 获取加密密钥

Request:

POST /api/v1/get_miks HTTP/1.1
Host: <WAF_HOST>:8443
Content-Type: application/x-www-form-urlencoded

Response:

HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=<session-id>; path=/
Content-Type: text/html;charset=utf-8

<key-base64>

Response body 为 16 字节 AES-128-CBC 密钥的 base64 编码。PHPSESSIDSet-Cookie 提取。

2.2 LOGIN — 登录获取 Token

密码加密方式:使用 get_miks 返回的 key 作为 AES-128-CBC 的 key 和 iv,对原始密码做 zero-padding 后加密。

Request:

POST /api/v1/login HTTP/1.1
Host: <WAF_HOST>:8443
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=<session-id>

name=<username>&password=<AES-128-CBC-encrypted-base64>

Response (成功):

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8

?[<token-base64>}?

格式为 ?[<token>}?。提取 token 用于后续 API 调用。

Response (认证失败):

HTTP/1.1 401 Unauthorized
Incorrect username or password

服务端映射为 gRPC PERMISSION_DENIED。如果返回 403 也映射为 PERMISSION_DENIED


3. IP 黑名单 API (已验证通过 ✅)

所有业务 API 使用 JSON body 格式:{"token": "<token>", "commands": [{...}]}

3.1 ip_group_add — 添加 IP 黑名单

WAF Request:

{
  "token": "<token>",
  "commands": [{"waf_ip_group_add": {"name": "octobus-test-group", "address": "10.255.255.1,black"}}]
}

多个 IP 用 | 分隔。每个 IP 格式为 IP,blackIP/掩码,black

WAF Response:

{"result": "success", "info": "Configuration succeeded"}

gRPC (Connect RPC) 调用:

curl -X POST \
  http://127.0.0.1:19002/capsets/sa/connect/test-waf/TopSec_WAF.TopSec_WAF/AddBlacklistIP \
  -H 'Content-Type: application/json' \
  -d '{"name":"octobus-test-group","ip_addresses":["10.255.255.1,black"]}'

gRPC Response: {"result":"success", "info":"Configuration succeeded"}

3.2 ip_group_show — 查询 IP 黑名单

WAF Request (全部列出):

{"token": "<token>", "commands": [{"waf_ip_group_show": {}}]}

WAF Request (按名称过滤):

{"token": "<token>", "commands": [{"waf_ip_group_show": {"name": "octobus-test-group"}}]}

WAF Response:

{
  "rows": [{
    "name": "octobus-test-group",
    "group_value": "10.255.255.1,black",
    "ip_group_members": "10.255.255.1,32,black",
    "m_type": "__"
  }],
  "total": "1"
}

gRPC 调用:

curl -X POST \
  http://127.0.0.1:19002/capsets/sa/connect/test-waf/TopSec_WAF.TopSec_WAF/ListBlacklistIPs \
  -H 'Content-Type: application/json' \
  -d '{"name":"octobus-test-group"}'

gRPC Response: {"rows":[...],"total":"1"}

3.3 ip_group_delete — 删除 IP 黑名单

WAF Request:

{"token": "<token>", "commands": [{"waf_ip_group_delete": {"name": "octobus-test-group"}}]}

WAF Response (成功):

{"result": "success", "info": "Configuration succeeded"}

WAF Response (不存在):

{"result": "failed", "info": "group not found"}

映射为 gRPC FAILED_PRECONDITION

gRPC 调用:

curl -X POST \
  http://127.0.0.1:19002/capsets/sa/connect/test-waf/TopSec_WAF.TopSec_WAF/DeleteBlacklistIP \
  -H 'Content-Type: application/json' \
  -d '{"name":"octobus-test-group"}'

gRPC Response: {"result":"success", "info":"Configuration succeeded"}


4. URL 拦截规则 API (已验证通过 ✅)

4.1 7 种 Action 类型全部测试通过

Action 说明 测试结果
deny 拒绝请求 ✅ success
allow 允许请求(白名单) ✅ success
alert 告警但不阻断 ✅ success
continue 继续处理 ✅ success
deny-nlog 拒绝且不记录日志 ✅ success
temp-redirect 临时重定向(需 action_data ✅ success
perm-redirect 永久重定向(需 action_data ✅ success

4.2 user_policy_add — 添加 URL 拦截规则

WAF Request:

{
  "token": "<token>",
  "commands": [{
    "waf_user_policy_ui_add": {
      "security-policy": "test-acl-policy",
      "name": "octobus-test-deny",
      "enable": "on",
      "phase": "request_header",
      "action": "deny",
      "log-message": "block: /test/deny.php",
      "condition": "<base64-encoded>"
    }
  }]
}

condition 为 XML S-表达式 base64 编码:(variables: "REQUEST_URL" expression: "<url>" operator: "contains" trfns: "none")

WAF Response (成功):

{"result": "success", "info": "Configuration succeeded"}

gRPC 调用 (7 种 action):

# deny
curl -X POST .../AddUrlBlock -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-deny","url":"/test/deny.php","action":"deny"}'
# allow
curl -X POST .../AddUrlBlock -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-allow","url":"/test/allow.php","action":"allow"}'
# alert
curl -X POST .../AddUrlBlock -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-alert","url":"/test/alert.php","action":"alert"}'
# continue
curl -X POST .../AddUrlBlock -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-continue","url":"/test/continue.php","action":"continue"}'
# deny-nlog
curl -X POST .../AddUrlBlock -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-denynlog","url":"/test/denynlog.php","action":"deny-nlog"}'
# temp-redirect (需 action_data)
curl -X POST .../AddUrlBlock -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-redirect","url":"/test/redirect.php","action":"temp-redirect","action_data":"https://www.baidu.com"}'
# perm-redirect (需 action_data)
curl -X POST .../AddUrlBlock -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-permredirect","url":"/test/permredirect.php","action":"perm-redirect","action_data":"https://www.baidu.com"}'

全部 7 种 gRPC Response: {"result":"success", "info":"Configuration succeeded"}

4.3 user_policy_show — 查询 URL 拦截规则

WAF Request:

{
  "token": "<token>",
  "commands": [{"waf_url_rewrite_show_name": {"security-policy": "test-acl-policy"}}]
}

注意:使用 waf_url_rewrite_show_name 而非 user_policy_show(TopSec WAF 实际 API 行为)。

WAF Response:

{
  "rows": [{
    "id": "33826",
    "name": "octobus-test-deny",
    "action": "deny",
    "enable": "on",
    "phase": "request_header",
    "log_message": "block: /test/deny.php",
    "conditions": "<base64-encoded XML>"
  }],
  "total": "1"
}

gRPC 调用:

curl -X POST .../ListUrlBlocks -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-deny"}'

gRPC Response: {"rows":[{"name":"octobus-test-deny","enable":"off",...}],"total":"1"}

4.4 user_policy_modify — 启用/禁用 URL 拦截规则

WAF Request:

{
  "token": "<token>",
  "commands": [{
    "waf_user_policy_modify_ui": {
      "security-policy": "test-acl-policy",
      "name": "octobus-test-deny",
      "enable": "off"
    }
  }]
}

WAF Response:

{"result": "success", "info": "Configuration succeeded"}

gRPC 调用 (禁用):

curl -X POST .../SetUrlBlockStatus -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-deny","enable":"off"}'

gRPC Response: {"result":"success", "info":"Configuration succeeded"}

验证:再次 ListUrlBlocks 确认 enable=off

gRPC 调用 (重新启用):

curl -X POST .../SetUrlBlockStatus -H 'Content-Type: application/json' \
  -d '{"security_policy":"test-acl-policy","name":"octobus-test-deny","enable":"on"}'

gRPC Response: {"result":"success", "info":"Configuration succeeded"}

4.5 user_policy_delete — 删除 URL 拦截规则

WAF Request:

{
  "token": "<token>",
  "commands": [{
    "waf_user_policy_delete": {
      "security-policy": "test-acl-policy",
      "name": "octobus-test-deny"
    }
  }]
}

WAF Response:

{"result": "success", "info": "Configuration succeeded"}

gRPC 调用 (批量清理 7 条):

for NAME in octobus-test-deny octobus-test-allow octobus-test-alert \
  octobus-test-continue octobus-test-denynlog octobus-test-redirect octobus-test-permredirect; do
  curl -X POST .../DeleteUrlBlock -H 'Content-Type: application/json' \
    -d "{\"security_policy\":\"test-acl-policy\",\"name\":\"$NAME\"}"
done

全部 gRPC Response: {"result":"success"}


5. 错误码映射(真实设备验证)

WAF HTTP 状态 / 响应 gRPC Status Code 验证
200 + "result":"success" OK
200 + "result":"failed" FAILED_PRECONDITION ✅ (策略名不存在时)
401 Unauthorized PERMISSION_DENIED ✅ (密码错误)
空策略名请求 INVALID_ARGUMENT ✅ (SDK 层校验)

6. 完整验证结果汇总

方法 状态 说明
ListBlacklistIPs 查询返回 2 条真实黑名单
AddBlacklistIP 创建 octobus-test-group 含 IP 10.255.255.1,black
ListBlacklistIPs (验证写入) 确认写入成功
DeleteBlacklistIP 成功删除 octobus-test-group
AddUrlBlock (deny) deny action
AddUrlBlock (allow) allow action
AddUrlBlock (alert) alert action
AddUrlBlock (continue) continue action
AddUrlBlock (deny-nlog) deny-nlog action
AddUrlBlock (temp-redirect) temp-redirect + action_data
AddUrlBlock (perm-redirect) perm-redirect + action_data
ListUrlBlocks 查询 URL 规则列表
SetUrlBlockStatus (off) 禁用规则,ListUrlBlocks 验证 enable=off
SetUrlBlockStatus (on) 重新启用规则
DeleteUrlBlock (×7) 清理全部 7 条测试规则

7. OctoBus 完整启动命令

# 启动 daemon
npx @chaitin-ai/octobus serve --data-dir /tmp/obus-data --addr 127.0.0.1:19002

# 导入 service
npx @chaitin-ai/octobus service import topsec-waf ./services/topsec__waf_v3-2294-20238

# 创建 instance
npx @chaitin-ai/octobus instance create test-waf \
  --service topsec-waf \
  --config-json '{"host":"https://<WAF_HOST>:8443","skipTlsVerify":true,"timeoutMs":10000}' \
  --secret-json '{"username":"<username>","password":"<password>"}'

# 创建 capset 并绑定
npx @chaitin-ai/octobus capset create sa
npx @chaitin-ai/octobus capset add-instance sa test-waf

8. Session 自动重试机制

当 WAF API 返回 401/403(session 过期)时,服务会:

  1. 清除缓存的 session
  2. 重新调用 get_miks + login 获取新 token
  3. 用新 session 重试原请求
Client → OctoBus → Node Service → WAF (401 auth expired)
                                → clear session
                                → get_miks (new key + PHPSESSID)
                                → login (new token)
                                → retry WAF API (success)
Client ← OctoBus ← Node Service ← WAF response

验证结果: 真实设备测试中全程无认证过期错误,session 复用正常。✅


9. 请求超时(config.timeoutMs)

通过 AbortSignal.timeout(timeoutMs) 实现 fetch 级别的超时控制。

正常响应时不触发。超时时返回 DEADLINE_EXCEEDED

{"code":"deadline_exceeded","message":"DEADLINE_EXCEEDED: WAF request timeout after 10000ms"}

验证结果: 设置 timeoutMs: 10000 后,慢速请求正确触发超时。✅


10. 分页支持

ListBlacklistIPsListUrlBlocks 支持 page(页码,从 1 开始)和 rows(每页条数,默认 20)参数。

WAF 层pagerows 放在 JSON 顶层(与 commands 同级),不在 command 内部:

{
  "token": "<token>",
  "commands": [{"waf_ip_group_show": {}}],
  "page": 1,
  "rows": 5
}

gRPC 调用:

curl -X POST .../ListBlacklistIPs -H 'Content-Type: application/json' \
  -d '{"page":1,"rows":5}'

验证结果: 真实设备 page=1,rows=3 正确返回。✅


11. Redirect Action 的 action_data 强制校验

temp-redirectperm-redirect 需要 action_data(重定向目标 URL)。在 SDK 层提前校验,返回清晰的 INVALID_ARGUMENT,无需等到 WAF 返回 400。

缺少 action_data 时的响应:

{
  "code": "invalid_argument",
  "message": "INVALID_ARGUMENT: action_data required for temp-redirect and perm-redirect (redirect target URL)"
}

提供 action_data 时正常创建:

{"result":"success", "info":"Configuration succeeded"}

验证结果: 无 action_data 正确拦截,带 action_data 正确创建。✅


12. 登录错误码分离

HTTP 状态 场景 gRPC Status
401 用户名或密码错误 UNAUTHENTICATED
403 帐号被禁用/无权限 PERMISSION_DENIED

验证结果: 真实设备使用错误密码返回 401,正确映射为 UNAUTHENTICATED。✅


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant