GravityAgent(二):把 6Graph、6Forest、Entropy/IP、6Sense 封装成统一算法层
IPv6 网络测绘 GravityAgent 算法
把研究算法封装成统一接口
上一篇介绍了 GravityAgent 的整体架构。这一篇我们下探到第三层——算法适配层,看看那些”只能跑脚本”的研究代码,是怎么被改造成系统可以稳定调用的工程模块的。
GravityAgent 接入了四个上游算法:
| 算法 | 论文 | 作用 |
|---|---|---|
| 6Graph | A graph-theoretic approach to address pattern mining for Internet-wide IPv6 scanning, Computer Networks 2022 | 地址模式挖掘 |
| 6Forest | An Ensemble Learning-based Approach to Target Generation for Internet-wide IPv6 Scanning, INFOCOM 2022 | 地址模式挖掘 |
| Entropy/IP | Uncovering Structure in IPv6 Addresses, IMC 2016 | 分段熵分析 + 规则挖掘 |
| 6Sense | Internet-Wide IPv6 Scanning and its Security Applications, USENIX Security 2024 | LSTM 生成 + 在线扫描 + 去别名 |
统一输出格式
适配层的第一件事,是定义统一的输出结构。对 6Graph 和 6Forest,模式挖掘的结果统一为:
{
"pattern_hex32": "2001da8********...", # 32 个十六进制字符,* 表示自由位
"pattern_colon": "2001:da8::...", # 冒号分隔形式
"seed_count": 42, # 该模式覆盖的种子数
"free_nibbles": 5, # 自由半字节数量
"sample_seeds": ["...", "...", "..."], # 示例种子
}
排序规则也很明确:seed_count 降序 → free_nibbles 升序 → 字典序,最后按 max_patterns 截断。
对 Agent 来说,6Graph 不再是一个”只能跑脚本的算法仓库”,而是一个”可直接返回结构化结果的挖掘器”。
6Graph / 6Forest:模块名冲突的坑
这两个算法的适配层结构几乎一样,核心是三个步骤:
1. IPv6 地址转半字节矩阵
def _ipv6_to_nibbles(ip_str: str) -> list[int]:
return [int(ch, 16) for ch in ipaddress.IPv6Address(ip_str).exploded.replace(":", "")]
一个 IPv6 地址展开后是 32 个十六进制字符,每个字符转成 0–15 的整数,构成一个 n×32 的 uint8 矩阵,直接喂给上游算法。
2. 动态加载上游模块
这里有个真实的坑:6Graph 和 6Forest 都使用同名模块。
- 6Graph 用
SpacePartition和PatternMining - 6Forest 用
SpacePartition和OutlierDetection
如果两个算法在同一个 Python 进程里先后被调用,sys.modules 里缓存的 SpacePartition 会是先加载的那个,导致 6Forest 跑到 6Graph 的代码上去。适配层的解法是在加载前主动清理模块缓存:
def _load_modules(self):
home_str = str(self.home)
if home_str not in sys.path:
sys.path.insert(0, home_str)
# 6Graph / 6Forest 都使用同名模块,先清理缓存避免串模块。
sys.modules.pop("SpacePartition", None)
sys.modules.pop("PatternMining", None)
sp = importlib.import_module("SpacePartition")
pm = importlib.import_module("PatternMining")
return sp, pm
这类问题在上游单算法仓库里根本不会出现,因为研究者默认自己的代码独占一个进程。
3. 屏蔽上游噪声输出
6Forest 的上游代码会往标准输出打印大量调试信息,直接污染系统日志。适配层用 redirect_stdout 把它静默掉:
from contextlib import redirect_stdout
from io import StringIO
mute_out = StringIO()
with redirect_stdout(mute_out):
for region in sp.DHC(arr):
p, _ = od.OutlierDetect(region)
patterns.extend(p)
挖掘流程差异
6Graph 比 6Forest 多了一步离群点迭代:
# 6Graph:先划分区域,对每个区域做离群检测
for region in sp.DHC(arr):
p, o = pm.OutlierDetect(region)
patterns.extend(p)
outliers.extend(o)
# 对离群点再迭代 iterations 轮(默认 3)
for _ in range(iterations):
if len(outliers) < 2:
break
stacked = np.vstack(outliers)
outliers = []
for region in sp.DHC(stacked):
p, o = pm.OutlierDetect(region)
patterns.extend(p)
outliers.extend(o)
其中 pm.threshold 会被动态设置为默认 12。
从模式矩阵反推可读模式
上游返回的是地址矩阵,适配层需要把每一列做统计,只有单一取值的列才是”固定半字节”:
tarr = p.T
pattern_chars = []
free_nibbles = 0
for idx in range(32):
splits = np.bincount(tarr[idx], minlength=16)
nonzero = np.where(splits > 0)[0]
if len(nonzero) == 1:
pattern_chars.append(format(int(nonzero[0]), "x"))
else:
pattern_chars.append("*")
free_nibbles += 1
pattern_hex32 = "".join(pattern_chars)
Entropy/IP:脚本型算法的子进程桥接
Entropy/IP 的上游代码本质上是一套串行脚本流程,不适合直接作为库函数复用。它的三个步骤是:
a1-segments.py:对地址做分段,计算每段熵值;a2-mining.py:挖掘每段的取值规则(固定值 / 区间);a3-encode.py:编码聚类。
适配层的处理方式是:
- 在
agent/vendor/entropy-ip-py3内维护 Python3 可运行版本; - 以子进程调用三个步骤;
- 将输出回读并解析成统一结果;
- 为每次运行写出
manifest.json。
run_dir = entropy_ip_run_dir("mine")
ips_path = run_dir / "ips_hex32.txt"
segments_path = run_dir / "segments.txt"
analysis_path = run_dir / "analysis.txt"
encoded_path = run_dir / "encoded.csv"
ips_path.write_text("\n".join(used) + "\n", encoding="utf-8")
self._run_script("a1-segments.py", [str(ips_path)], segments_path)
self._run_script("a2-mining.py", [str(ips_path), str(segments_path)], analysis_path)
self._run_script("a3-encode.py", [str(ips_path), str(analysis_path)], encoded_path)
解析上游的文本输出
上游输出的是人类可读的文本,适配层用正则把它结构化。比如分段行:
# segment type start stop avg
对应的解析正则是:
_SEGMENT_LINE_RE = re.compile(
r"^# segment\t(?P<stype>[^\t]+)\t\s*(?P<start>\d+)\t\s*(?P<stop>\d+)\t(?P<avg>[0-9.]+)"
)
规则行则分两类:固定值(const)和区间(range):
_CONST_RULE_RE = re.compile(r"^\s{2}(?P<val>[0-9a-f]+)\s+(?P<pct>[0-9]+(?:\.[0-9]+)?)%$")
_RANGE_RULE_RE = re.compile(r"^\*\s+(?P<start>[0-9a-f]+)-(?P<stop>[0-9a-f]+)\s+(?P<pct>[0-9]+(?:\.[0-9]+)?)%$")
解析后的规则按覆盖率降序排列,返回 segments、rules、rules_all、encoded_top(编码聚类 Top10)以及各产物路径。
这种”脚本型算法工程化接入”的关键是:每次运行都有独立工作目录,且写 manifest,这样出问题能快速定位到某一次运行的输入输出。
6Sense:最重的一条流水线
6Sense 的接入最复杂,因为它不只是一个静态预测器,而是一整条研究流水线:
- LSTM checkpoint
- RouteViews
pfx2as scanv6在线扫描器aliasv6去别名器- 训练与生成脚本
它更接近”论文实验环境”,而不是”可直接嵌入 Agent 的生产模块”。
按种子集哈希的 checkpoint 机制
这是整个适配层里最重要的一个改造。
问题:如果整个系统共用一个固定 checkpoint,那么不同组织、不同文件、不同种子分布都落到同一个模型上,存在明显的语义污染。
解法:按种子集内容做哈希,为每个种子集生成独立 checkpoint:
# 1. 对种子集做规范化:去重、地址展开、排序
# 2. 对规范化结果做 sha256
# 3. 取前 16 位哈希作为种子集标识
# 4. 生成 checkpoint 名
checkpoint_name = f"lstm_sid_allocation_base_100_seedset_{seed_hash}_"
这样做的好处:
- 同一批种子换个顺序不会重复训练(规范化后哈希一致);
- 不同种子集会得到独立模型;
- 训练与生成可以稳定复用对应 checkpoint。
训练桥接
agent/sixsense_train.py 把上游训练逻辑桥接为一个受控脚本。默认训练参数:
| 参数 | 默认值 |
|---|---|
| batch size | 3200 |
| learning rate | 0.001 |
| LSTM layers | [512, 256] |
| dropout | 0.2 |
| epochs | 50 |
训练脚本会创建本轮工作目录、准备 all_ips 数据集、为模型目录建立桥接、调用上游训练逻辑、输出最终 all_ips.hdf5,并清理同目录下其他 checkpoint 文件,只保留最后一版。
生成桥接
agent/sixsense_bridge.py 负责建立生成工作目录、写配置文件、调用上游 ComparisonModel_online、读取候选/去别名/别名结果,并统一输出为规范文件名:
candidates_6sense_<label>.txt
dealiased_6sense_<label>.txt
aliased_6sense_<label>.txt
scanner.log
manifest.json
自动探测出口网络
6Sense 的在线扫描器需要知道出口网卡、源 IPv6 和网关 MAC。适配层会从 /proc/net/ipv6_route 和 /proc/net/if_inet6 自动探测,并支持从 EUI-64 link-local 地址还原 MAC。
修复上游除零 bug
适配层还修了上游 6Sense 生成逻辑里的一个除零错误。问题表现是:
- 某轮生成结果
hits = 0; all_allocations_thus_far也为空;- 原始代码仍计算分式,导致
division by zero。
修复方式是:若当前分母为 0,则把 diff 直接置为 0.0,让”没有命中”被视为正常结果而不是异常崩溃。
运行期路径规范
适配层所有产物路径统一由 agent/runtime_paths.py 管理,把固定资产和运行期产物彻底分开:
runs/agent/exports/ # 用户可复用的导出文件
runs/agent/jobs/ # 后台任务和 os_identify 状态目录
runs/agent/tmp/ # 提交到 scanner 前的临时目标文件
runs/agent/entropy_ip/mine/<ts>/ # Entropy/IP 中间产物
runs/6sense/train/<label>/<ts>/ # 6Sense 训练工作目录
runs/6sense/generation/<label>/<ts>/ # 6Sense 生成工作目录
固定资产(6Graph/、6Forest/、entropy-ip/、6SENSE/)和运行期产物彻底分离,排查时只需要先判断:
- 看静态资源 → 进
6SENSE/、6Graph/等; - 看一次训练或生成的中间过程 → 进
runs/6sense/...; - 看 Agent 导出的结果 → 进
runs/agent/exports/...。
小结
这一篇的核心结论是:研究代码工程化的关键不是重写算法,而是补上”接口、隔离、可观测性”三件事。
| 问题 | 工程化手段 |
|---|---|
| 输出格式不统一 | 定义统一的 pattern dict / rules 结构 |
| 模块名冲突 | sys.modules.pop + 动态 import |
| 输出污染日志 | redirect_stdout 静默 |
| 脚本型流程 | 子进程桥接 + manifest |
| 模型语义污染 | 种子集哈希 checkpoint |
| 上游 bug | 定位并修复除零 |
| 路径散乱 | 统一 runtime_paths 规范 |
下一篇,我们进入最底层的扫描执行引擎,看看 Rust 写的 gravity-scanner 是如何在 2¹²⁸ 的地址空间里做无状态探测与任务调度的。
订阅本站
通过 RSS 或邮箱,第一时间收到新文章。