侧边栏壁纸
博主头像
いちごみるく

行动起来,活在当下

  • 累计撰写 70 篇文章
  • 累计创建 48 个标签
  • 累计收到 55 条评论

目 录CONTENT

文章目录

Linux IPv6 地址池与 SNAT 实验:历史实现和网络边界

历史网络实验分析。 本文区分 macvlan、IPv6 地址分配、SNAT 与 HTTP/SOCKS 代理,并提供只读地址检查示例。接口数量不等于可用公网 IP 数量;部署需要先确认地址授权、路由与回滚方式。

一、macvlan 与 SNAT 实验的链路

Ctrip-Crawler 历史实现试图依次创建 eth0 的 macvlan 子接口、枚举每个子接口的 IPv6 地址、建立 ip6tables SNAT 链,再在程序执行中切换出口规则。下面的截图记录了当时的接口与地址:

2023 年 IPv6 接口实验

这条链路有四个独立前提:二层链路允许多个 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 规则推断。

0

评论区