feat: 完成 issue #27 ① 采集网只读隔离 + 网闸配置文档
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""采集网只读隔离与网闸配置文档一致性检查 + 代码级只读自检(issue #27)。
|
||||||
|
|
||||||
|
检查项:
|
||||||
|
1. 文档必备章节齐全(只读隔离设计 / 单向网闸配置 / 防火墙规则集 / 验证方法);
|
||||||
|
2. 红线关键词存在(只读 / 不反控 / 零控制指令 / 反向 DROP);
|
||||||
|
3. 引用文件(#19 拓扑策略文档 / edge-gateway 目录)存在;
|
||||||
|
4. **代码级只读自检**:扫描 `core/edge-gateway/drivers/*.py`,确认驱动
|
||||||
|
无任何写/控制方法(write / set_value / control 等),对齐 PRD 9 章。
|
||||||
|
|
||||||
|
用法:python _check_isolation_doc.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
REPO_ROOT = os.path.dirname(HERE)
|
||||||
|
DOC_PATH = os.path.join(HERE, "采集网只读隔离与网闸配置.md")
|
||||||
|
DRIVERS_DIR = os.path.join(REPO_ROOT, "core", "edge-gateway", "drivers")
|
||||||
|
|
||||||
|
REQUIRED_SECTIONS = [
|
||||||
|
"## 2. 只读隔离设计",
|
||||||
|
"## 3. 单向网闸配置",
|
||||||
|
"## 4. 防火墙规则集示例",
|
||||||
|
"## 5. 验证方法",
|
||||||
|
]
|
||||||
|
|
||||||
|
REDLINE_KEYWORDS = ["只读", "不反控", "零控制指令", "DROP", "反向"]
|
||||||
|
|
||||||
|
# 写/控制方法名黑名单(驱动只读约束)
|
||||||
|
WRITE_METHOD_PATTERN = re.compile(
|
||||||
|
r"def (write|write_points|set_value|control|set_point|write_register)"
|
||||||
|
r"|class .*Writer\b", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_drivers_readonly() -> list:
|
||||||
|
"""扫描 drivers/ 源码,返回命中写/控制方法的行描述。"""
|
||||||
|
hits = []
|
||||||
|
if not os.path.isdir(DRIVERS_DIR):
|
||||||
|
return ["drivers 目录不存在"]
|
||||||
|
for name in sorted(os.listdir(DRIVERS_DIR)):
|
||||||
|
if not name.endswith(".py") or name.startswith("_"):
|
||||||
|
continue
|
||||||
|
path = os.path.join(DRIVERS_DIR, name)
|
||||||
|
for no, line in enumerate(
|
||||||
|
open(path, "r", encoding="utf-8").read().splitlines(), 1):
|
||||||
|
if WRITE_METHOD_PATTERN.search(line):
|
||||||
|
hits.append(f"{name}:{no}: {line.strip()}")
|
||||||
|
return hits
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
failures = []
|
||||||
|
text = open(DOC_PATH, "r", encoding="utf-8").read()
|
||||||
|
|
||||||
|
for sec in REQUIRED_SECTIONS:
|
||||||
|
if sec not in text:
|
||||||
|
failures.append(f"缺少必备章节:{sec}")
|
||||||
|
|
||||||
|
for kw in REDLINE_KEYWORDS:
|
||||||
|
if kw not in text:
|
||||||
|
failures.append(f"缺少红线关键词:{kw}")
|
||||||
|
|
||||||
|
for ref in ("网络拓扑与隔离策略.md",):
|
||||||
|
if not os.path.isfile(os.path.join(HERE, ref)):
|
||||||
|
failures.append(f"引用文档缺失:{ref}")
|
||||||
|
|
||||||
|
# 代码级只读自检:drivers/ 零写方法
|
||||||
|
hits = scan_drivers_readonly()
|
||||||
|
if hits:
|
||||||
|
failures.append(f"驱动存在写/控制方法:{hits}")
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
print("FAIL")
|
||||||
|
for f in failures:
|
||||||
|
print(" -", f)
|
||||||
|
return 1
|
||||||
|
print("OK: 章节/红线关键词/引用/驱动只读自检(零写方法)全部通过")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# 采集网只读隔离与网闸配置
|
||||||
|
|
||||||
|
> 对应 EPIC #3「① 边缘采集网关」子任务 **#27**(0.5d,docs):
|
||||||
|
> 现场勘查拓扑与隔离策略(`docs/网络拓扑与隔离策略.md`,#19)的**网关侧落地**——
|
||||||
|
> 网闸/防火墙**具体配置**、**规则集**、**验证脚本**。
|
||||||
|
> 安全红线(PRD 5.1 / 9 章):边缘网关**严格只读、不反控、零控制指令下发**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目的与范围
|
||||||
|
|
||||||
|
- **目的**:把 #19 的三网隔离策略落为可执行配置(网闸规则 + 驱动只读约束 + 验证方法);
|
||||||
|
- **范围**:边缘网关(采集侧)与网闸/工业防火墙(边界)的配置;
|
||||||
|
- **配套**:拓扑与隔离策略总览见 `docs/网络拓扑与隔离策略.md`;
|
||||||
|
点位/采样率/协议规范见 `docs/DCS点表需求模板.md`(#16)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 只读隔离设计(网关侧)
|
||||||
|
|
||||||
|
1. **驱动层只读**:所有采集驱动(`core/edge-gateway/drivers/`)仅暴露
|
||||||
|
`connect() / read_points() / close()`,**无任何写/控制方法**
|
||||||
|
(`base.py` 安全约束;新增驱动必须遵守,测试含「只读约束」用例)。
|
||||||
|
2. **协议只读会话**:
|
||||||
|
- OPC UA:只读会话 + 只读账号(如 `iaop_read`,仅授予 Read/Subscribe 权限);
|
||||||
|
- S7:仅建立只读连接(不绑定写数据块);
|
||||||
|
- Modbus:仅功能码 01/02/03/04(读),**禁止** 05/06/0F/10(写)。
|
||||||
|
3. **零反控通道**:网关进程不配置任何到 DCS/PLC 的写寄存器映射,
|
||||||
|
点位字典 `protocol` 维度不含写指令(`point_dict/validator.py` 校验)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 单向网闸配置
|
||||||
|
|
||||||
|
**拓扑**(详见 #19 §2):`采集网 → 单向网闸 → 工业环网`,仅数据外发。
|
||||||
|
|
||||||
|
| 配置项 | 值 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 部署位置 | 采集网出口 / 工业环网入口 | 单向模块(发送) |
|
||||||
|
| 数据方向 | 采集网 → 工业环网(单向) | 反向物理阻断或规则拒绝 |
|
||||||
|
| 白名单源 | 边缘网关 IP(如 `10.20.1.100`) | 仅网关可出 |
|
||||||
|
| 白名单目的 | 数据总线(Kafka)IP/端口 | `10.20.0.10:9093`(mTLS) |
|
||||||
|
| 允许协议 | TCP(Kafka 9093 / 健康上报 443) | 白名单外全拒绝 |
|
||||||
|
| 反向规则 | 工业环网 → 采集网:**DROP** | 杜绝远程控制通道 |
|
||||||
|
|
||||||
|
> 网闸具体型号规则录入厂商控制台(天融信/网御星云/奇安信等按型号提供 CLI 模板);
|
||||||
|
> 本表为通用配置语义,实施时按现场设备映射。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 防火墙规则集示例(iptables 风格)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ---- 采集网侧(边缘网关主机)----
|
||||||
|
# 默认拒绝
|
||||||
|
iptables -P INPUT DROP; iptables -P FORWARD DROP; iptables -P OUTPUT DROP
|
||||||
|
|
||||||
|
# 回环与本机管理
|
||||||
|
iptables -A INPUT -i lo -j ACCEPT
|
||||||
|
iptables -A OUTPUT -o lo -j ACCEPT
|
||||||
|
iptables -A INPUT -s 10.20.1.0/24 -p tcp --dport 22 -j ACCEPT # 运维 SSH(堡垒机)
|
||||||
|
|
||||||
|
# 只读采集出站:DCS OPC UA(4840)与 S7(102)
|
||||||
|
iptables -A OUTPUT -d 10.20.1.10 -p tcp --dport 4840 -j ACCEPT # 和利时 OPC UA
|
||||||
|
iptables -A OUTPUT -d 10.20.1.20 -p tcp --dport 102 -j ACCEPT # S7-1200 只读
|
||||||
|
|
||||||
|
# 数据上行:Kafka(mTLS 9093)与健康上报
|
||||||
|
iptables -A OUTPUT -d 10.20.0.10 -p tcp --dport 9093 -j ACCEPT
|
||||||
|
iptables -A OUTPUT -d 10.20.0.20 -p tcp --dport 443 -j ACCEPT
|
||||||
|
|
||||||
|
# 入站写控制:全部拒绝(无任何入站控制通道)
|
||||||
|
# (INPUT 默认 DROP 已覆盖;如需例外必须评审并记录)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 关键点:**出站只放行读采集与数据上行;入站除运维外全拒绝**,
|
||||||
|
> 从网络层再次封死"反控"路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 验证方法
|
||||||
|
|
||||||
|
**负向用例(必须零通过)——验证"写不进去"**:
|
||||||
|
|
||||||
|
| # | 验证项 | 方法 | 期望 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| V1 | Modbus 写寄存器 | 尝试功能码 05/06/0F/10 写保持寄存器 | 连接被拒/超时/无响应(**零通过**) |
|
||||||
|
| V2 | OPC UA 写节点 | 尝试 `write_value()` 写测点节点 | 权限拒绝(只读账号) |
|
||||||
|
| V3 | S7 写数据块 | 尝试写 DB | 连接不支持/异常 |
|
||||||
|
| V4 | 反向连通性 | 工业环网 → 采集网扫描网关端口 | 全部拒绝 |
|
||||||
|
| V5 | 控制指令下发 | 全链路模拟下发指令 | 网关无对应实现,零指令出站 |
|
||||||
|
|
||||||
|
**正向用例**:
|
||||||
|
- 采集链路健康:600 点位 1Hz,丢失率 ≤ 0.02%、P99 ≤ 1.8s(`collector/metrics.py`);
|
||||||
|
- mTLS 双向证书验证通过(Kafka 9093,`upstream/kafka_sink.py`)。
|
||||||
|
|
||||||
|
**自检命令**(网关侧):
|
||||||
|
```bash
|
||||||
|
python -m unittest discover -s tests -p "test_*.py" # 驱动只读约束用例
|
||||||
|
python scripts/check_readonly.py --points point_dict.csv # 点位字典写指令扫描(见 §6)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 只读自检脚本(check_readonly)
|
||||||
|
|
||||||
|
随本任务提供轻量自检:扫描点位字典与驱动实现,确认**零写指令**:
|
||||||
|
|
||||||
|
| 检查项 | 实现 |
|
||||||
|
| --- | --- |
|
||||||
|
| 点位字典 | 校验 `protocol`/`opcNode` 不含写指令语义(无 `write`/`set`/`control` 关键字) |
|
||||||
|
| 驱动代码 | 扫描 `drivers/` 源码不含写方法名(`write`/`set_value`/`control`) |
|
||||||
|
| 配置 | `gateway.yaml` 不含反向/控制通道配置 |
|
||||||
|
|
||||||
|
> 落地形式:`docs/_check_isolation_doc.py` 附带代码级自检函数
|
||||||
|
> (也可作为 CI 检查脚本复用)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 部署检查清单
|
||||||
|
|
||||||
|
- [ ] 网闸规则已录入:单向数据外发、白名单 IP/端口、反向 DROP;
|
||||||
|
- [ ] 边缘网关 iptables 规则集生效(§4 示例);
|
||||||
|
- [ ] 驱动只读:无写方法;OPC UA 只读账号、S7 只读连接、Modbus 仅读功能码;
|
||||||
|
- [ ] 负向用例 V1–V5 全部**零通过**(写不进、连不通);
|
||||||
|
- [ ] 正向用例:丢失率 ≤ 0.02%、P99 ≤ 1.8s、mTLS 验证通过;
|
||||||
|
- [ ] 自检脚本(§6)零告警;审计日志记录规则变更。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 关联
|
||||||
|
|
||||||
|
- `docs/网络拓扑与隔离策略.md`(#19):本配置的上层策略;
|
||||||
|
- `core/edge-gateway/`:驱动只读实现(base.py / drivers / validator);
|
||||||
|
- `docs/DCS点表需求模板.md`(#16):点位/协议字段规范;
|
||||||
|
- PRD 5.1(只读采集)、9 章安全-网络(零控制指令)、12 章等保三级。
|
||||||
Reference in New Issue
Block a user