Skip to content

指定分配网段内任意 IPv6 作为出口 IP

谁不想要 2^64 个 IP 的代理池 ? - zu1k

基于ip6tables构建随机出口 - Type Boom

利用 IPV6 绕过B站的反爬 | yllhwa's blog

创建一个自己的 IPv6 代理池 (ndppd + openresty) - 企鹅大大的博客

IPv6地址分配统计 - 运营商·运营人 - 通信人家园 - Powered by C114

ipv6攻击视角 - r0fus0d 的博客

查看 ipv6 地址

sh
ip -6 addr show scope global

输出形如:

sh
2: eno1: <BROADCAST,MULTICAST,ALLMULTI,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 240?:????:????:????:abcd:1234:5678:90ab/64 scope global temporary dynamic
       valid_lft 258952sec preferred_lft 85972sec
    inet6 240?:????:????:????:1234:5678:abcd:1124/64 scope global temporary deprecated dynamic
       valid_lft 258952sec preferred_lft 0sec
    inet6 240?:????:????:????:7890:3456:abcd:0101/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 258952sec preferred_lft 172552sec

可以看到,240?:????:????:????::/64 是分配到的 ipv6 网段。

可以进一步过滤出当前的公网 ipv6 地址:

sh
ip -6 addr show scope global | grep -E "2409.*" | grep -E "mngtmpaddr" | grep -v "deprecated"
sh
    inet6 240?:????:????:????:7890:3456:abcd:0101/64 scope global dynamic mngtmpaddr noprefixroute

输出形如:

三大运营商的 ipv6 地址开头:

  • 中国联通:2408
  • 中国移动:2409
  • 中国电信:240e

添加路由

将上面的获得的 ipv6 网段添加到路由表中:

sh
sudo ip route add local 240?:????:????:????::/64 dev eno1
  • 240?:????:????:????::/64 替换为实际的 ipv6 网段
  • eno1 替换为实际的网卡名称
  • 这里的 devdevice 的缩写,表示指定网络接口设备

查看路由

sh
ip -6 route show

形如:

sh
::1 dev lo proto kernel metric 256 pref medium
240?:????:????:???::/64 dev eno1 proto ra metric 100 pref medium
240?:????:????:???::/60 via fe80::1 dev eno1 proto ra metric 100 pref high
fe80::/64 dev ????? proto kernel metric 256 pref medium
fe80::/64 dev eno1 proto kernel metric 1024 pref medium
default via fe80::1 dev eno1 proto ra metric 100 pref medium

如果只想显示公网地址,可以过滤掉包含 fe80::lo 的地址:

sh
ip -6 route show | grep -v 'fe80::' | grep -v 'lo'

输出形如:

sh
240?:????:????:???::/64 dev eno1 proto ra metric 100 pref medium

启用 ip_nonlocal_bind

sh
sudo nano /etc/sysctl.conf

在文件末尾添加内容并保存:

sh
net.ipv6.ip_nonlocal_bind = 1

使配置生效:

sh
sudo sysctl -p

安装 ndppd

sh
sudo apt install ndppd

配置 ndppd

sh
sudo nano /etc/ndppd.conf

添加内容:

lua
route-ttl 30000
proxy eno1 {
    router no
    timeout 500
    ttl 30000
    rule 240?:????:????:???::/64 {
        static
    }
}
  • eno1 替换为实际的网卡名称
  • 240?:????:????:????::/64 替换为实际的 ipv6 网段

启动 ndppd

sh
sudo systemctl start ndppd

设置开机自启:

sh
sudo systemctl enable ndppd

测试出口地址

随机选择一个同网段下的 ipv6 地址,测试出口 IP:

sh
curl --int 240?:????:????:????:abcd:9876:5678:0123 http://ifconfig.me/ip
  • --int--interface 的缩写,用于指定出口 IP

如果之前的步骤都正确,输出的 ipv6 地址应该和 --int 指定的相同,形如:

sh
240?:????:????:????:abcd:9876:5678:0123

网段变更

如果光猫重启或者断电,可能会导致 ipv6 网段变更,需要重新添加路由:

sh
sudo ip route add local 240x:xxxx:xxxx:xxxx::/64 dev eno1
  • 这里的 240x:xxxx:xxxx:xxxx::/64 是新的 ipv6 网段

重新配置 ndppd:

sh
sudo nano /etc/ndppd.conf

rule 240?:????:????:????::/64 替换为新的 ipv6 网段:

lua
route-ttl 30000
proxy eno1 {
    router no
    timeout 500
    ttl 30000
    rule 240x:xxxx:xxxx:xxxx::/64 {
        static
    }
}

重启 ndppd:

sh
sudo systemctl restart ndppd

一键自动配置

该脚本自动完成如下步骤:

  • 查看本机最新 ipv6 前缀
  • 添加路由
  • 修改 ndppd 配置,重启 ndppd

有两个步骤不包括在内(因为只需要操作一次):

  • 启用 ip_nonlocal_bind
  • 安装 ndppd
ip_router.py
py
import netifaces
import random
import requests
import requests.packages.urllib3.util.connection as urllib3_cn
import socket
import time

from pathlib import Path
from requests.adapters import HTTPAdapter
from tclogger import logger, logstr, decolored, shell_cmd
from typing import Union

REQUESTS_HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
}


class IPv6Adapter(HTTPAdapter):
    def __init__(self, source_address, *args, **kwargs):
        self.source_address = source_address
        super().__init__(*args, **kwargs)

    def init_poolmanager(self, *args, **kwargs):
        kwargs["source_address"] = self.source_address
        return super().init_poolmanager(*args, **kwargs)


class RequestsSessionIPv6Adapter:
    @staticmethod
    def force_ipv4():
        urllib3_cn.allowed_gai_family = lambda: socket.AF_INET

    @staticmethod
    def force_ipv6():
        if urllib3_cn.HAS_IPV6:
            urllib3_cn.allowed_gai_family = lambda: socket.AF_INET6

    def adapt(self, session: requests.Session, ip: str):
        try:
            socket.inet_pton(socket.AF_INET6, ip)
        except Exception as e:
            raise ValueError(f"× Invalid IPv6 format: [{ip}]")

        adapter = IPv6Adapter((ip, 0))
        session.mount("http://", adapter)
        session.mount("https://", adapter)

        return session


class IPv6Generator:
    def __init__(self, verbose: bool = False):
        self.verbose = verbose
        self.interfaces = []
        # self.get_prefix()

    def get_addr_prefix(self, addr: str, netmask: str):
        prefix_length = netmask.count("f")
        prefix = addr[: prefix_length // 4 * 5 - 1]
        return prefix, prefix_length * 4

    def get_network_interfaces(self):
        interfaces = netifaces.interfaces()
        for interface in interfaces:
            addresses = netifaces.ifaddresses(interface)
            if netifaces.AF_INET6 not in addresses:
                continue
            for addr_info in addresses[netifaces.AF_INET6]:
                if not addr_info["addr"].startswith("2"):
                    break
                addr = addr_info["addr"]
                netmask = addr_info["netmask"]
                prefix, prefix_bits = self.get_addr_prefix(addr, netmask)
                self.interfaces.append(
                    {
                        "interface": interface,
                        "addr": addr,
                        "netmask": netmask,
                        "prefix": prefix,
                        "prefix_bits": prefix_bits,
                    }
                )

    def get_prefix(self, return_netint: bool = False):
        logger.note("> Get ipv6 prefix:")
        self.get_network_interfaces()
        interface = self.interfaces[0]
        prefix = interface["prefix"]
        prefix_bits = interface["prefix_bits"]
        netint = interface["interface"]
        if self.verbose:
            logger.note(f"> IPv6 prefix:", end=" ")
            logger.success(f"[{prefix}]", end=" ")
            logger.mesg(f"(/{prefix_bits})")
        self.netint = netint
        self.prefix = prefix
        self.prefix_bits = prefix_bits
        logger.file(f"  * prefix: {logstr.success(prefix)}")
        logger.file(f"  * netint: {logstr.success(netint)}")
        if return_netint:
            return self.prefix, netint
        else:
            return self.prefix

    def generate(
        self, prefix: str = None, return_segs: bool = False
    ) -> Union[str, tuple[str, list[str], list[str]]]:
        prefix = prefix or self.prefix
        prefix_segs = prefix.split(":")
        suffix_seg_count = 8 - len(prefix_segs)
        suffix_segs = [f"{random.randint(0, 65535):x}" for _ in range(suffix_seg_count)]
        addr = ":".join(prefix_segs + suffix_segs)
        if return_segs:
            return addr, prefix_segs, suffix_segs
        else:
            return addr


class IPv6RouteModifier:
    def __init__(self, prefix: str, netint: str, ndppd_conf: Union[Path, str] = None):
        self.ndppd_conf = ndppd_conf or Path("/etc/ndppd.conf")
        self.prefix = prefix
        self.netint = netint

    def add_route(self):
        logger.note("> Add IP route:")
        cmd = f"sudo ip route add local {self.prefix}::/64 dev {self.netint}"
        # logger.mesg(cmd)
        shell_cmd(cmd)

    def del_route(self):
        logger.note("> Delete IP route:")
        cmd = f"sudo ip route del local {self.prefix}::/64 dev {self.netint}"
        # logger.mesg(cmd)
        shell_cmd(cmd)

    def modify_ndppd_conf(self, overwrite: bool = False):
        if self.ndppd_conf.exists():
            with open(self.ndppd_conf, "r") as rf:
                old_ndppd_conf_str = rf.read()
            logger.note(f"> Read: {logstr.file(self.ndppd_conf)}")
            logger.mesg(f"{old_ndppd_conf_str}")

        if not self.ndppd_conf.exists() or overwrite:
            new_ndppd_conf_str = (
                f"route-ttl 30000\n"
                f"proxy {logstr.success(self.netint)} {{\n"
                f"    router no\n"
                f"    timeout 500\n"
                f"    ttl 30000\n"
                f"    rule {logstr.success(self.prefix)}::/64 {{\n"
                f"        static\n"
                f"    }}\n"
                f"}}\n"
            )
            logger.note(f"> Write: {logstr.file(self.ndppd_conf)}")
            logger.mesg(f"{new_ndppd_conf_str}")
            with open(self.ndppd_conf, "w") as wf:
                wf.write(decolored(new_ndppd_conf_str))
            logger.success(f"✓ Modified: {logstr.file(self.ndppd_conf)}")

    def restart_ndppd(self):
        logger.note("> Restart ndppd:")
        cmd = "sudo systemctl restart ndppd"
        # logger.mesg(cmd)
        shell_cmd(cmd)
        logger.success(f"✓ Restarted: {logstr.file('ndppd')}")


if __name__ == "__main__":
    generator = IPv6Generator()
    prefix, netint = generator.get_prefix(return_netint=True)

    modifier = IPv6RouteModifier(prefix=prefix, netint=netint)
    modifier.add_route()
    modifier.modify_ndppd_conf(overwrite=True)
    modifier.restart_ndppd()

    sleep_seconds = 5
    logger.note(f"> Waiting {sleep_seconds} seconds for ndppd to work ...")
    time.sleep(sleep_seconds)

    logger.note("> Testing ipv6 addrs:")
    session = requests.Session()
    adapter = RequestsSessionIPv6Adapter()
    for i in range(5):
        ipv6, prefix_segs, suffix_segs = generator.generate(return_segs=True)
        prefix = ":".join(prefix_segs)
        suffix = ":".join(suffix_segs)
        logger.note(f"  > [{prefix}:{logstr.file(suffix)}]")
        adapter.adapt(session, ipv6)
        response = session.get("https://test.ipw.cn", headers=REQUESTS_HEADERS)
        logger.mesg(f"  * [{response.text}]")

运行:

sh
sudo env "PATH=$PATH" python ip_router.py

测试脚本

ip_tester.py
py
import netifaces
import random
import requests
import requests.packages.urllib3.util.connection as urllib3_cn
import socket

from tclogger import logger
from requests.adapters import HTTPAdapter


class IPv6Extractor:
    def __init__(self):
        self.interfaces = []

    def extract_prefix(self, addr: str, netmask: str):
        prefix_length = netmask.count("f")
        prefix = addr[: prefix_length // 4 * 5 - 1]
        return prefix, prefix_length * 4

    def get_network_interfaces(self):
        interfaces = netifaces.interfaces()
        for interface in interfaces:
            addresses = netifaces.ifaddresses(interface)
            if netifaces.AF_INET6 not in addresses:
                continue
            for addr_info in addresses[netifaces.AF_INET6]:
                if not addr_info["addr"].startswith("2"):
                    break
                addr = addr_info["addr"]
                netmask = addr_info["netmask"]
                prefix, prefix_bits = self.extract_prefix(addr, netmask)
                self.interfaces.append(
                    {
                        "interface": interface,
                        "addr": addr,
                        "netmask": netmask,
                        "prefix": prefix,
                        "prefix_bits": prefix_bits,
                    }
                )

    def get_prefix(self):
        self.get_network_interfaces()
        interface = self.interfaces[0]
        prefix = interface["prefix"]
        prefix_bits = interface["prefix_bits"]
        logger.note(f"> IPv6 prefix:", end=" ")
        logger.success(f"[{prefix}]", end=" ")
        logger.mesg(f"(/{prefix_bits})")
        return prefix

    def random_ipv6(self, prefix: str = None) -> str:
        if prefix is None:
            prefix = self.get_prefix()
        prefix_segs = prefix.split(":")
        suffix_seg_count = 8 - len(prefix_segs)
        suffix_seg = [f"{random.randint(0, 65535):x}" for _ in range(suffix_seg_count)]
        addr = ":".join(prefix_segs + suffix_seg)
        return addr


class IPv6Adapter(HTTPAdapter):
    def __init__(self, source_address, *args, **kwargs):
        self.source_address = source_address
        super().__init__(*args, **kwargs)

    def init_poolmanager(self, *args, **kwargs):
        kwargs["source_address"] = self.source_address
        return super().init_poolmanager(*args, **kwargs)


class IPTester:
    def __init__(self):
        self.url = "http://ifconfig.me/ip"

    def force_ipv4(self):
        urllib3_cn.allowed_gai_family = lambda: socket.AF_INET

    def force_ipv6(self):
        if urllib3_cn.HAS_IPV6:
            urllib3_cn.allowed_gai_family = lambda: socket.AF_INET6

    def check_ip_addr(self, ip: str = None):
        if not ip:
            return 4

        try:
            socket.inet_pton(socket.AF_INET6, ip)
            return 6
        except Exception as e:
            logger.warn(f"× Invalid ip string: [{ip}]")
            return None

    def set_session_adapter(self, session: requests.Session, ip: str = None):
        ip_version = self.check_ip_addr(ip)
        if ip_version == 4:
            self.force_ipv4()
        elif ip_version == 6:
            self.force_ipv6()
            adapter = IPv6Adapter((ip, 0))
            session.mount("http://", adapter)
            session.mount("https://", adapter)
        else:
            pass

    def get(self, ip: str = None):
        session = requests.Session()
        self.set_session_adapter(session, ip)
        logger.note(f"  > Set:", end=" ")
        if not ip:
            logger.line(f"[ipv4]")
        else:
            logger.line(f"[{ip}]")
        try:
            resp = session.get(self.url, timeout=5)
            if resp and resp.status_code == 200:
                logger.file(f"  * Get:", end=" ")
                logger.success(f"[{resp.text.strip()}]")
        except Exception as e:
            logger.error(f"× Error: {e}")


if __name__ == "__main__":
    extractor = IPv6Extractor()
    prefix = extractor.get_prefix()
    random_ipv6_addrs = [extractor.random_ipv6(prefix) for _ in range(5)]

    ip_tester = IPTester()
    ip_tester.get()
    for ip in random_ipv6_addrs:
        ip_tester.get(ip)

运行:

sh
# pip install netifaces requests tclogger
python ip_tester.py

输出形如:

sh
> IPv6 prefix: [240?:????:????:????] (/64)
  > Set: [ipv4]
  * Get: [???.???.???.???]
  > Set: [240?:????:????:????:f618:ad3a:fd1a:f0d0]
  * Get: [240?:????:????:????:f618:ad3a:fd1a:f0d0]
  > Set: [240?:????:????:????:410b:9770:6504:de53]
  * Get: [240?:????:????:????:410b:9770:6504:de53]
  > Set: [240?:????:????:????:b05b:87b2:26b4:15f3]
  * Get: [240?:????:????:????:b05b:87b2:26b4:15f3]
  > Set: [240?:????:????:????:d5bc:58c9:dd74:b45]
  * Get: [240?:????:????:????:d5bc:58c9:dd74:b45]