历史网络实验分析。 本文区分 macvlan、IPv6 地址分配、SNAT 与 HTTP/SOCKS 代理,并提供只读地址检查示例。接口数量不等于可用公网 IP 数量;部署需要先确认地址授权、路由与回滚方式。
一、macvlan 与 SNAT 实验的链路
Ctrip-Crawler 历史实现试图依次创建 eth0 的 macvlan 子接口、枚举每个子接口的 IPv6 地址、建立 ip6tables SNAT 链,再在程序执行中切换出口规则。下面的截图记录了当时的接口与地址:

这条链路有四个独立前提:二层链路允许多个 MAC;网络实际授予可用 IPv6 地址;这些地址有正确的路由与邻居发现;目标连接真正选择 IPv6。创建虚拟接口只解决第一步的一部分,不会凭空产生运营商认可的地址。
IPv6 自动配置需要路由通告、有效前缀及系统配置等条件,地址还要经过重复地址检测;仅执行 ip link add ... type macvlan 不等于取得一个全球可达地址。拥有一个 /64 地址段也不意味着可以不经网络配置就随意使用其中任意地址。IPv6 无状态地址自动配置 RFC 4862
二、接口创建与规则管理的边界
| 操作 | 实验中的做法 | 需要满足的条件 |
|---|---|---|
| 检查 root | os.geteuid() == 0 |
权限足够不等于当前网络支持 macvlan 或有地址授权 |
| 创建接口 | 循环创建多个同前缀 macvlan | 需检查每条命令退出码;已有同名接口不能自动认领 |
| 等待 IPv6 | 等待后从 ip 文本提取地址 |
不能只用固定 sleep;需检查 tentative、dadfailed、路由和地址归属 |
| 枚举地址 | 查找 scope global |
ULA 也可能显示 global scope,不代表可公网访问 |
| 建立 SNAT | 给每个源地址写规则 | NAT 选择的是连接映射,需验证规则顺序、适用流量和回包路径 |
| 切换规则 | 调整链或轮换目标地址 | 不保证现有 keep-alive 连接按下一次 HTTP 请求换 IP |
| 清理 | 按接口前缀批量删除,删除链 | 可能误删非本次创建的接口;引用链要先解绑并清空 |
subprocess.run 若不检查退出码,命令失败后仍可能继续执行并显示创建成功;这会让后续 NAT 规则建立在并不存在的接口上。文本正则还可能把同一接口的多个地址混淆,不能以列表长度作为成功数量。
三、SNAT 不是 HTTP 或 SOCKS 代理
SNAT 修改匹配数据包的源地址。与它不同,浏览器的 --proxy-server=http://127.0.0.1:PORT 要求该端口实际有 HTTP 代理进程在监听。创建 SNAT 链不会同时启动代理服务,因此不能直接将接口地址填入 Chrome 的代理设置。
有状态 NAT 通常在连接首包建立映射,之后的数据包使用 conntrack 中的映射。HTTP 连接池、HTTP/2 等可能让多个请求共享连接;更改规则主要影响后续新连接,不能承诺“每个请求切一次”。IPv4 流量也不会因为 IPv6 SNAT 规则而改变。nftables 对有状态 NAT 的说明
仓库中的 SOCKS 服务实现与这里分析的 SNAT 结构属于不同架构,应分别检查。还需核对监听地址、认证、访问控制、源地址绑定和连接失败行为;监听 0.0.0.0 且无认证不能作为公开代理部署默认值。
四、可直接复用的只读地址检查
下面只读取已有接口,不创建接口、不改路由和防火墙。保存为 inspect_ipv6.py,用实际接口名调用,例如 python inspect_ipv6.py eth0:
import argparse
import ipaddress
import json
import subprocess
def inspect(interface):
result = subprocess.run(
['ip', '-j', '-6', 'address', 'show', 'dev', interface],
check=True, capture_output=True, text=True)
interfaces = json.loads(result.stdout)
rows = []
for device in interfaces:
for addr in device.get('addr_info', []):
if addr.get('family') != 'inet6':
continue
ip = ipaddress.IPv6Address(addr['local'])
flags = addr.get('flags', [])
rows.append({
'interface': device['ifname'],
'address': str(ip),
'prefixlen': addr['prefixlen'],
'scope': addr.get('scope'),
'global_address_class': ip.is_global,
'tentative': bool(addr.get('tentative') or 'tentative' in flags),
'dadfailed': bool(addr.get('dadfailed') or 'dadfailed' in flags),
'preferred_life_time': addr.get('preferred_life_time'),
'valid_life_time': addr.get('valid_life_time'),
})
return rows
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('interface')
args = parser.parse_args()
print(json.dumps(inspect(args.interface), ensure_ascii=False, indent=2))
is_global 是地址分类,仍然不是连通性实测。还要在有权管理的网络中检查出站路由、回程、DNS 地址选择和实际出口;不要只看源地址字符串就宣称已获得一个独立代理。
五、如果继续研究网络改写
先在隔离的网络命名空间或测试主机中设计实验,记录修改前配置。每次创建的接口和链使用明确清单;遇到半途失败,只回滚本次成功创建的对象。删除链时先移除对应引用规则,再清空和删除链,匹配条件应与创建时一致,不能用不带 -o 的另一条规则去删除原规则。
研究 Ctrip-Crawler 历史目录中的实现时,应区分各版本的网络结构。地址可用性、代理服务可用性与目标服务的访问规则是不同问题,不能仅通过创建接口或 NAT 规则推断。
评论区