之前分享了那些常住在我 Homelab 的服务,有人对没有公网 IPv4 的情况下外网访问家庭内服务挺感兴趣,所以整理下自己 Homelab 远程访问的方案。
家用 Homelab 远程访问的场景大多有三个:
- 共享内部服务到外网访问 (比如 RSSHub);
- 外部设备连接家庭内网,访问内网的各种服务/设备;
- PT 连通性和上传速度。
常用的方案有 FRP 代理、IPv6、Zerotier 等,但都有各自的缺点:
- FRP 非直连,依赖代理服务器带宽,不一定跑慢带宽;
- IPv6 要求外部设备的网络也支持 IPv6,好多办公网络是不支持的;
- Zerotier 有时间不能直连,需要中转速度很慢;
- 都无法解决 PT 上传的问题。
我现在折腾的方案用了大概两年,稳定性很好,几乎可以媲美公网 IPv4。
但是也有一些前置条件:
- 宽带 NAT 为 NAT1(Full Cone);
- 路由拨号,路由器系统最好是 Openwrt (其他 Linux 发行版也行,但无法抄作业)。
- 有一个内网设备支持运行 Clash (运行在主路由也行)。
也有一些好处:
- 不需要代理服务器,可以跑满带宽。
- 不依赖 IPv6,有 IPv6 更好一些。
- 支持 PT 上传。
如果你的网络和设备满足上面的条件,就可以按照我的方案来尝试了。
网络结构图
先分享一下我网络的最小化结构和主机 IP,方便后续参考对比。

场景 1:共享内部服务到外网访问
我内网运行了一个 RSSHub,需要给外网的订阅器访问。
这类的场景比较简单,我选用的是 Cloudflare Tunnel。按照官方文档运行在 Docker 里边几乎没遇到问题。如果遇到连接 Cloudflare 数据中心的问题,可以尝试切换网络连接方式:HTTP2+IPv4/HTTP2+IPv6/QUIC+IPv4/QUIC+IPv6,都可以试一下。实在不行可以将这个 Docker 容器的流量重定向到 Clash 代理。
参考配置:
# $HOME/.cloudflared/config.yml
protocol: http2 # Available values are auto, http2, and quic.
# edge-ip-version: "6" # Available values are auto, 4, and 6.场景 2:外部设备连接家庭内网
这个场景下,我选用到的是 Clash Meta 入站 + NATMap,然后将动态 IP 和端口上传到自建的 Vercel 服务,转为 Clash 订阅格式给外网设备订阅。
1. 配置路由器 home 后缀 DNS
1.1 DHCP 给常用设备分配固定 IP
我给路由器分配的 IP 10.10.10.10,OMV 是 10.10.10.100。
1.2 DHCP 将主机名映射到固定 IP

2. 配置服务端 Clash Meta 入站
创建一个或者多个 listener,并且将整个 home 后缀的域名设为直连 (home TLD 非有效 TLD,拦截它的流量人畜无害)。
参考文档:配置 Clash LISTENERS
参考配置:
# $HOME/.clash/config.yml
# hone 后缀使用路由器 DNS,注意修改
dns:
nameserver-policy:
'*.home': 10.10.10.10 # 或者 system 或者 dhcp://en0
# 监听入站端口
listeners:
- name: shadowsocks-in-auto
type: shadowsocks
port: 8901 # 自定义,记住后面配置路由器防火墙需要使用
listen: 0.0.0.0
password: chimiantiaome # 自定义,记住后面配置订阅需要使用
cipher: aes-128-gcm
rules:
- DOMAIN-SUFFIX,home,DIRECT # HOME3. 路由器防火墙增加端口转发
此方法会在路由器上监听一个端口 8901,然后将流量转发到 Clash 设备的 8901 端口,使用 Clash 设备访问内网。

4. 上传到 IP 和端口信息到 Vercel 服务端
4.1 部署 Vercel 服务端存储 IP 和端口信息
Vercel 服务端是我自己写的一个小服务,你可以部署到 Vercel,绑定域名后,拿到 API 地址。
部署完成你会有一个 API 地址,例如 https://magic.miantiao.me。
修改下面脚本里边的端口地址后,上传到路由器 /usr/bin/diy/dip.sh,下一步配置通知使用。
# #!/bin/sh
# 脚本地址:https://github.com/miantiao-me/without-ipv4/blob/master/shell/dip.sh
# DIP
outter_ip=$1
outter_port=$2
inner_ip=$3
inner_port=$4
protocol=$5
logger -t "DIP" "[DIP] start : ${protocol}: ${outter_ip}:${outter_port} to ${inner_ip}:${inner_port}"
if [ "${outter_port}" ]; then
logger -t "DIP" "${outter_ip}:${outter_port}"
curl -Ss -o /dev/null -X POST \
-H 'Content-Type: application/json' \
-d '{"ip": "'"${outter_ip}"'", "port": "'"${outter_port}"'", "key": "'"${inner_port}"'"}' \
"https://magic.miantiao.me/dip"
fi
logger -t "DIP" "[DIP] ${inner_port} end"5. 配置 NATMap
安装 NATMap 的 OpenWRT 插件。增加打洞配置。

6. 手动增加防火墙转发

7. 外部设备上订阅 Clash 节点
以上流程跑通以后,就可以在外部设备上订阅 Clash 节点了。
第 4 步部署的服务,可以提供 CLASH 订阅文件,地址是 https://magic.miantiao.me/dip?key=8901&password=chimiantiaome,API 地址、key、password 注意换成你的。
8. Clash 客户端增加转发规则
rules:
- DOMAIN-SUFFIX,home,HOME-8901 # 节点名称可以查看上一步订阅的节点配置完后重启 Clash,外部设备浏览器打开 http://OMV.home http://AX3600.home 等设备分配的主机名就可以访问内网设备了。

我移动端设备使用的 QuantumX,订阅配置文件后 (需要资源解析器),也是可以正常连接家庭网络的。

场景 3:PT 连通性和上传速度
PT 上传场景与场景2差不多,但是不需要将 IP 和端口上报到服务端。
1. 按照场景 2 的第 1 步,配置完静态 IP 和主机名
2. 上传更新 qBittorrent 脚本
上传脚本到路由器 /usr/bin/diy/natmap-update.sh。
脚本支持在 NATMap 打洞成功后,更改防火墙转发,修改 qBittorrent 端口地址 (支持多实例,可按需修改),并且发送通知到 Bark (可选)。
注意修改脚本中 qBittorrent 的地址、端口、账号和密码。
#!/bin/sh
# 脚本地址:https://github.com/miantiao-me/without-ipv4/blob/master/shell/natmap-update.sh
# NATMap
outter_ip=$1
outter_port=$2
inner_ip=$3
inner_port=$4
protocol=$5
logger -t "NATMap" "[NATMap] start NAT : ${protocol}: ${outter_ip}:${outter_port} to ${inner_port}"
case ${inner_port} in
# qBittorrent
9001)
sleep 1
qbv4="10.10.10.100"
qbwebport="9091"
qbusername="mt"
qbpassword="chimiantiaome"
# ipv4 redirect
uci set firewall.redirectqbv41=redirect
uci set firewall.redirectqbv41.name='qBittorrent9091'
uci set firewall.redirectqbv41.proto='tcp'
uci set firewall.redirectqbv41.src='wan'
uci set firewall.redirectqbv41.dest='lan'
uci set firewall.redirectqbv41.target='DNAT'
uci set firewall.redirectqbv41.src_dport="${inner_port}"
uci set firewall.redirectqbv41.dest_ip="${qbv4}"
uci set firewall.redirectqbv41.dest_port="${outter_port}"
# reload
uci commit firewall
/etc/init.d/firewall reload > /dev/null
sleep 3
# update port
qbcookie=$(\
curl -Ssi -X POST \
-d "username=${qbusername}&password=${qbpassword}" \
"http://${qbv4}:${qbwebport}/api/v2/auth/login" | \
sed -n 's/.*\(SID=.\{32\}\);.*/\1/p' )
curl -X POST \
-s \
-b "${qbcookie}" \
-d 'json={"listen_port":"'${outter_port}'"}' \
"http://${qbv4}:${qbwebport}/api/v2/app/setPreferences"
text="[NATMap] qBittorrent TCP Port: ${outter_ip}:${outter_port} to ${inner_port} to $(uci get firewall.redirectqbv41.dest_ip):$(uci get firewall.redirectqbv41.dest_port)"
;;
# qBittorrent
# 9002)
# sleep 10
# qbv4="10.10.10.100"
# qbwebport="9092"
# qbusername="mt"
# qbpassword="chimiantiaome"
# # ipv4 redirect
# uci set firewall.redirectqbv42=redirect
# uci set firewall.redirectqbv42.name='qBittorrent9092'
# uci set firewall.redirectqbv42.proto='tcp'
# uci set firewall.redirectqbv42.src='wan'
# uci set firewall.redirectqbv42.dest='lan'
# uci set firewall.redirectqbv42.target='DNAT'
# uci set firewall.redirectqbv42.src_dport="${inner_port}"
# uci set firewall.redirectqbv42.dest_ip="${qbv4}"
# uci set firewall.redirectqbv42.dest_port="${outter_port}"
# # reload
# uci commit firewall
# /etc/init.d/firewall reload > /dev/null
# sleep 3
# # update port
# qbcookie=$(\
# curl -Ssi -X POST \
# -d "username=${qbusername}&password=${qbpassword}" \
# "http://${qbv4}:${qbwebport}/api/v2/auth/login" | \
# sed -n 's/.*\(SID=.\{32\}\);.*/\1/p' )
# curl -X POST \
# -s \
# -b "${qbcookie}" \
# -d 'json={"listen_port":"'${outter_port}'"}' \
# "http://${qbv4}:${qbwebport}/api/v2/app/setPreferences"
# text="[NATMap] qBittorrent TCP Port: ${outter_ip}:${outter_port} to ${inner_port} to $(uci get firewall.redirectqbv42.dest_ip):$(uci get firewall.redirectqbv42.dest_port)"
# ;;
*)
text="[NATMap] not NAT: ${protocol}: ${outter_ip}:${outter_port} to ${inner_port}"
;;
esac
if [ "${text}" ]; then
logger -t "NATMap" "${text}"
# 通知端口信息到 Bark, 不需要可以注释掉下面的 curl
curl -Ss -o /dev/null -X POST \
-H 'Content-Type: application/json' \
-d '{"title": "NATMap", "body": "'"${text}"'"}' \
"https://api.day.app/BARK_KEY" # Bark API
fi
logger -t "NATMap" "[NATMap] ${inner_port} NAT end"3. 配置 NATMap
安装 NATMap 的 OpenWRT 插件。增加打洞配置 (注意更新脚本地址)。

4. 增加防火墙 NAT 规则,禁用地址重写
如果不修改,qBittorrent 会发现入站 IP 都是路由器 IP,而不是公网 IP,影响统计。

效果展示
可以看见,分享率还是很客观的。

整体的方案,还是有一定的折腾难度,如果你遇到问题,可以联系我一起讨论。
