起因:容器只听IPv4,公网v6进不来
家里几个Docker容器一直有个老毛病:应用绑在`0.0.0.0`上,公网IPv6根本连不进来。之前想过装Nginx做反向代理,但又觉得为了两三个自用服务跑一套反代有点重。
周末刷到一个帖子,作者用Go写了个叫nasconn+的工具,专门干两件事:给IPv4端口自动开一份IPv6代理,顺便嗅探HTTP流量自动在`端口+1`上挂HTTPS。听着挺对路,但我不想直接往NAS上扔二进制文件,就想着能不能用Python自己写个精简版,只处理我常用的几个端口。
第一版:socket转发+端口硬编码
最开始的想法很简单:监听`::`(IPv6全零地址),收到连接后往`127.0.0.1:目标端口`转发。用Python在线运行环境直接测了一下核心逻辑,大概30行代码就能跑通一个基础的TCP代理。
import socket
import threading
def forward(source, destination):
try:
while True:
data = source.recv(4096)
if not data:
break
destination.sendall(data)
finally:
source.close()
destination.close()
def handle_client(client_sock, target_port):
try:
target = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
target.connect(('127.0.0.1', target_port))
threading.Thread(target=forward, args=(client_sock, target)).start()
threading.Thread(target=forward, args=(target, client_sock)).start()
except Exception as e:
print(f"连接失败: {e}")
client_sock.close()
def start_proxy(listen_port, target_port):
server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('::', listen_port))
server.listen(5)
print(f"代理启动: [::]:{listen_port} -> 127.0.0.1:{target_port}")
while True:
client, addr = server.accept()
threading.Thread(target=handle_client, args=(client, target_port)).start()这版代码在本地测能通,但直接扔到NAS上跑会遇到几个实际问题:进程挂了怎么办?多个端口要启多少个Python脚本?
日志往哪存?
记日志:SQLite够用了
本来想直接往文本文件append日志,后来发现如果要按端口、按时间查连接记录,还是结构化存储方便。NAS上已经有SQLite,不用额外装数据库,就用SQLite编辑器建了个简单的表:
CREATE TABLE proxy_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT,
listen_port INTEGER,
target_port INTEGER,
client_ip TEXT,
status TEXT
);
CREATE TABLE port_rules (
listen_port INTEGER PRIMARY KEY,
target_port INTEGER,
enabled INTEGER DEFAULT 1
);`port_rules`表存端口映射规则,`proxy_logs`记每次连接的时间、来源IP和状态。代码里加几行SQL操作就能把连接事件落库,后面想看哪个端口被访问过多少次,直接查表就行。
HTTP嗅探:想做自动HTTPS,最后放弃了
nasconn+里有个功能我觉得挺酷:检测到HTTP流量后,自动在`原端口+1`上开HTTPS。理论上可以读TCP流量的前几个字节,看是不是`GET / HTTP/1.1`开头,如果是就判定为HTTP。
我试着写了一版,能识别明文请求,但卡在证书这一步:Let's Encrypt的DNS验证要去域名服务商那边改TXT记录,HTTP验证又要保证`.well-known/acme-challenge/`能从外网访问。我这几个容器都是内网自用,没注册域名,搞自签证书浏览器又要手动信任。
折腾半天发现性价比不高,最后这功能直接砍了。
现在的方案就是纯粹的TCP端口转发,不管上层协议。如果真需要HTTPS,我宁愿在应用层自己配证书,或者老老实实上Caddy。
为什么只做了三个端口
最开始我想过做端口扫描:自动检测本机哪些IPv4端口在监听,然后批量给它们开v6代理。但实际跑起来发现两个问题:
- **扫描本身有风险**。`netstat`或者`ss`能列出监听端口,但要区分哪些是真正需要外网访问的服务、哪些是数据库或内部通信端口,逻辑会变得很复杂。万一把PostgreSQL的5432也代理出去了,那就是安全事故。
- **端口变动不频繁**。我常用的就三个:Jellyfin的8096、qBittorrent的8080、还有一个自建的RSS服务跑在3000。这仨端口半年都不会变,写死在配置里反而最省心。
所以现在的`port_rules`表里就三条记录,代理进程启动时读这张表,只给这三个端口开监听。如果以后要加新服务,手动INSERT一行就行。
怎么让它常驻运行
测试阶段我是直接`python proxy.py`前台跑,关掉SSH连接进程就没了。后来把脚本改成了一个持续监听的服务,用systemd管理(NAS跑的是Debian)。
写了个简单的unit文件:
[Unit]
Description=IPv6 Proxy for Docker Containers
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/scripts/v6proxy.py
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=multi-user.target`systemctl enable v6proxy`设置开机启动,`systemctl start v6proxy`拉起服务。这样进程挂了会自动重启,日志可以用`journalctl -u v6proxy`查。
如果NAS环境不方便装systemd,也可以把脚本扔到VicroCode的在线工具里做成API端点托管(待核实是否支持长连接socket服务),或者干脆写个cron任务每分钟检查进程是否存活。
目前的使用情况
跑了一个多星期,三个端口的v6访问都正常。从公网用`/ai-market-guide/market/735/
性能上没什么压力,Python的socket转发对我这点流量来说完全够用。内存占用稳定在20MB左右,CPU基本是0%。
如果以后真要处理高并发或者大流量,可能得考虑换Go或者Rust重写,但现在这个规模Python足够了。
没做的事和后续打算
- **UDP支持**:nasconn+的作者提到UDP还在路上,我这边也没做。主要是我用的服务都是TCP协议,暂时没遇到需求。
- **SNI嗅探**:如果要做真正的自动HTTPS,应该读TLS握手里的SNI字段,根据域名路由到不同后端。但这需要TLS termination,复杂度一下就上去了。
- **Web管理界面**:现在改端口映射要手动改SQLite表,有点原始。如果后续端口多了,可以做个简单的HTML表单,用Python的http.server模块提供一个配置页面。
总的来说,这个脚本就是给自己用的小工具,不追求功能完整,只求解决具体问题。如果你也遇到类似场景——容器不支持v6、又不想上重量级反向代理,可以试试这个思路。
代码不复杂,改起来也快。