使用Python实现:本地IPV4映射公网IPV6端口
背景:博主有一台白群晖,家庭宽带有公网IPV6地址。奇思妙想了一下想要把群晖本地的服务映射到群晖获取的公网IPV6地址。然后通过白群晖自带的DDNS就可以通过域名访问自建服务啦!
下面直接贴代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
import threading
import argparse
import sys
import logging
import netifaces
import time
# 配置日志
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def get_ipv6_address():
"""获取本机的公网IPv6地址"""
try:
logger.info("正在检测IPv6地址...")
for interface in netifaces.interfaces():
addrs = netifaces.ifaddresses(interface)
if netifaces.AF_INET6 in addrs:
for addr in addrs[netifaces.AF_INET6]:
# 排除本地链路地址和私有地址
if 'addr' in addr:
addr_value = addr['addr'].split('%')[0] # 移除接口标识符
if not addr_value.startswith(('fe80:', 'fc00:', 'fd', '::1')):
logger.info(f"在接口 {interface} 上找到公网IPv6地址: {addr_value}")
return addr_value
else:
logger.debug(f"跳过非公网IPv6地址: {addr_value} 在接口 {interface}")
logger.warning("未找到公网IPv6地址")
# 如果没有找到公网地址,返回任何可用的IPv6地址(::)
logger.info("将使用通配符地址 '::'")
return "::"
except Exception as e:
logger.error(f"获取IPv6地址失败: {e}")
logger.info("将使用通配符地址 '::'")
return "::"
class PortForwarder:
def __init__(self, local_ipv4, local_port, ipv6_addr, ipv6_port):
self.local_ipv4 = local_ipv4
self.local_port = local_port
self.ipv6_addr = ipv6_addr
self.ipv6_port = ipv6_port
self.running = False
self.threads = []
def handle_client(self, client_socket, client_addr):
"""处理客户端连接并转发数据"""
logger.info(f"接收来自 {client_addr} 的连接")
try:
# 创建到本地IPv4服务的连接
local_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
local_socket.settimeout(10) # 设置连接超时
logger.debug(f"尝试连接本地服务 {self.local_ipv4}:{self.local_port}")
local_socket.connect((self.local_ipv4, self.local_port))
logger.debug(f"成功连接到本地服务 {self.local_ipv4}:{self.local_port}")
# 创建两个线程用于双向转发数据
t1 = threading.Thread(target=self.forward_data,
args=(client_socket, local_socket, f"IPv6客户端->本地服务"))
t2 = threading.Thread(target=self.forward_data,
args=(local_socket, client_socket, f"本地服务->IPv6客户端"))
t1.daemon = True
t2.daemon = True
t1.start()
t2.start()
t1.join()
t2.join()
except ConnectionRefusedError:
logger.error(f"连接被拒绝: {self.local_ipv4}:{self.local_port},请确认本地服务是否正在运行")
except socket.timeout:
logger.error(f"连接本地服务超时: {self.local_ipv4}:{self.local_port}")
except Exception as e:
logger.error(f"连接本地服务失败: {e}")
finally:
try:
client_socket.close()
except:
pass
def forward_data(self, src_socket, dst_socket, direction):
"""转发数据"""
buffer_size = 8192 # 增大缓冲区
try:
src_socket.settimeout(300) # 5分钟无数据传输超时
bytes_count = 0
while self.running:
try:
data = src_socket.recv(buffer_size)
if not data:
logger.debug(f"{direction}: 连接关闭")
break
bytes_count += len(data)
if bytes_count % (buffer_size * 10) == 0: # 每传输约80KB记录一次
logger.debug(f"{direction}: 已传输 {bytes_count} 字节")
dst_socket.sendall(data)
except socket.timeout:
logger.debug(f"{direction}: 数据接收超时")
break
except Exception as e:
if self.running:
logger.error(f"{direction}: 数据转发错误: {e}")
finally:
try:
logger.debug(f"{direction}: 关闭连接,共传输 {bytes_count} 字节")
src_socket.close()
dst_socket.close()
except:
pass
def start(self):
"""启动端口转发服务"""
self.running = True
# 创建IPv6监听套接字
try:
server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 尝试绑定地址和端口
try:
logger.debug(f"尝试绑定到 [{self.ipv6_addr}]:{self.ipv6_port}")
server.bind((self.ipv6_addr, self.ipv6_port, 0, 0))
logger.info(f"成功绑定到 [{self.ipv6_addr}]:{self.ipv6_port}")
except socket.error as e:
if self.ipv6_addr != "::":
logger.warning(f"绑定到特定IPv6地址失败: {e}")
logger.info("尝试使用通配符地址 '::'")
self.ipv6_addr = "::"
server.bind((self.ipv6_addr, self.ipv6_port, 0, 0))
logger.info(f"成功绑定到 [::](所有IPv6接口):{self.ipv6_port}")
else:
raise
server.listen(5)
logger.info(f"IPv6端口转发服务启动: [{self.ipv6_addr}]:{self.ipv6_port} -> {self.local_ipv4}:{self.local_port}")
logger.info("等待IPv6客户端连接...")
# 创建测试连接的线程
if self.local_ipv4 == "127.0.0.1" or self.local_ipv4 == "localhost":
test_thread = threading.Thread(target=self.test_local_service)
test_thread.daemon = True
test_thread.start()
while self.running:
try:
client_socket, addr = server.accept()
client_thread = threading.Thread(target=self.handle_client, args=(client_socket, addr))
client_thread.daemon = True
client_thread.start()
self.threads.append(client_thread)
except socket.timeout:
continue
except Exception as e:
logger.error(f"接受连接时发生错误: {e}")
if not self.running:
break
except KeyboardInterrupt:
logger.info("接收到中断信号,正在关闭服务...")
except Exception as e:
logger.error(f"服务器错误: {e}")
finally:
self.stop()
server.close()
def test_local_service(self):
"""测试本地服务是否可用"""
time.sleep(1) # 等待服务器完全启动
logger.info(f"正在测试本地服务 {self.local_ipv4}:{self.local_port} 是否可访问...")
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
result = s.connect_ex((self.local_ipv4, self.local_port))
if result == 0:
logger.info(f"本地服务 {self.local_ipv4}:{self.local_port} 可正常访问")
else:
logger.warning(f"本地服务 {self.local_ipv4}:{self.local_port} 不可访问 (错误码: {result})")
logger.warning("请确保本地服务已启动,否则转发将无法工作")
except Exception as e:
logger.warning(f"测试本地服务时发生错误: {e}")
finally:
s.close()
def stop(self):
"""停止端口转发服务"""
self.running = False
for thread in self.threads:
if thread.is_alive():
thread.join(1)
logger.info("端口转发服务已停止")
def main():
parser = argparse.ArgumentParser(description='IPv4到IPv6端口转发工具')
parser.add_argument('--local-ip', type=str, default='127.0.0.1', help='本地IPv4地址,默认为127.0.0.1')
parser.add_argument('--local-port', type=int, required=True, help='本地IPv4端口')
parser.add_argument('--ipv6', type=str, help='监听的IPv6地址,如不指定则自动获取')
parser.add_argument('--ipv6-port', type=int, required=True, help='IPv6监听端口')
parser.add_argument('--debug', action='store_true', help='启用详细调试日志')
args = parser.parse_args()
# 设置日志级别
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
# 如果未指定IPv6地址,则自动获取
ipv6_addr = args.ipv6
if not ipv6_addr:
ipv6_addr = get_ipv6_address()
forwarder = PortForwarder(args.local_ip, args.local_port, ipv6_addr, args.ipv6_port)
try:
forwarder.start()
except KeyboardInterrupt:
forwarder.stop()
sys.exit(0)
if __name__ == "__main__":
main()
就使用了一个库
# vim requirements.txt
netifaces>=0.11.0
运行代码
(venv) root@Synology:/volume1/obitoma/v4tov6# python ipv4_to_ipv6_port_forwarder.py --local-port 22 --ipv6-port 10022
2025-04-23 11:11:17,370 - INFO - 正在检测IPv6地址...
2025-04-23 11:11:17,371 - DEBUG - 跳过非公网IPv6地址: ::1 在接口 lo
2025-04-23 11:11:17,371 - INFO - 在接口 ovs_eth0 上找到公网IPv6地址: 2409:8a20:xxx:xxx:xxxx:d0ff:fe1f:xxxx
2025-04-23 11:11:17,371 - DEBUG - 尝试绑定到 [2409:8a20:xxx:xxx:xxxx:d0ff:fe1f:xxxx]:10022
2025-04-23 11:11:17,372 - INFO - 成功绑定到 [2409:8a20:xxx:xxx:xxxx:d0ff:fe1f:xxxx]:10022
2025-04-23 11:11:17,372 - INFO - IPv6端口转发服务启动: [2409:8a20:xxx:xxx:xxxx:d0ff:fe1f:xxxx]:10022 -> 127.0.0.1:22
2025-04-23 11:11:17,372 - INFO - 等待IPv6客户端连接...
2025-04-23 11:11:18,373 - INFO - 正在测试本地服务 127.0.0.1:22 是否可访问...
2025-04-23 11:11:18,374 - INFO - 本地服务 127.0.0.1:22 可正常访问
2025-04-23 11:11:31,862 - INFO - 接收来自 ('2409:8a20:xxx:xxx:xxxx:d0ff:fe1f:xxxx', 55734, 0, 0) 的连接
2025-04-23 11:11:31,863 - DEBUG - 尝试连接本地服务 127.0.0.1:22
2025-04-23 11:11:31,863 - DEBUG - 成功连接到本地服务 127.0.0.1:22
2025-04-23 11:13:31,892 - DEBUG - 本地服务->IPv6客户端: 连接关闭
2025-04-23 11:13:31,892 - DEBUG - 本地服务->IPv6客户端: 关闭连接,共传输 1513 字节
2025-04-23 11:15:12,285 - ERROR - IPv6客户端->本地服务: 数据转发错误: [Errno 9] Bad file descriptor
2025-04-23 11:15:12,285 - DEBUG - IPv6客户端->本地服务: 关闭连接,共传输 1605 字节
2025-04-23 11:18:29,388 - INFO - 接收来自 ('2409:8a20:xxx:xxx:xxxx:d0ff:fe1f:xxxx', 57183, 0, 0) 的连接
2025-04-23 11:18:29,389 - DEBUG - 尝试连接本地服务 127.0.0.1:22
2025-04-23 11:18:29,389 - DEBUG - 成功连接到本地服务 127.0.0.1:22
2025-04-23 11:19:24,365 - DEBUG - IPv6客户端->本地服务: 连接关闭
2025-04-23 11:19:24,365 - DEBUG - IPv6客户端->本地服务: 关闭连接,共传输 4181 字节
2025-04-23 11:19:24,367 - ERROR - 本地服务->IPv6客户端: 数据转发错误: [Errno 9] Bad file descriptor
2025-04-23 11:19:24,367 - DEBUG - 本地服务->IPv6客户端: 关闭连接,共传输 7885 字节