指定分配网段内任意 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
替换为实际的网卡名称 - 这里的
dev
是device
的缩写,表示指定网络接口设备
查看路由
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]