Skip to content

没有公网 IPv4 也能玩转 Homelab 远程访问

Published: at 22:30

之前分享了那些常住在我 Homelab 的服务,有人对没有公网 IPv4 的情况下外网访问家庭内服务挺感兴趣,所以整理下自己 Homelab 远程访问的方案。

家用 Homelab 远程访问的场景大多有三个:

  1. 共享内部服务到外网访问 (比如 RSSHub);
  2. 外部设备连接家庭内网,访问内网的各种服务/设备;
  3. PT 连通性和上传速度。

常用的方案有 FRP 代理、IPv6、Zerotier 等,但都有各自的缺点:

  1. FRP 非直连,依赖代理服务器带宽,不一定跑慢带宽;
  2. IPv6 要求外部设备的网络也支持 IPv6,好多办公网络是不支持的;
  3. Zerotier 有时间不能直连,需要中转速度很慢;
  4. 都无法解决 PT 上传的问题。

我现在折腾的方案用了大概两年,稳定性很好,几乎可以媲美公网 IPv4。

但是也有一些前置条件:

  1. 宽带 NAT 为 NAT1(Full Cone);
  2. 路由拨号,路由器系统最好是 Openwrt (其他 Linux 发行版也行,但无法抄作业)。
  3. 有一个内网设备支持运行 Clash (运行在主路由也行)。

也有一些好处:

  1. 不需要代理服务器,可以跑满带宽。
  2. 不依赖 IPv6,有 IPv6 更好一些。
  3. 支持 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 # HOME

3. 路由器防火墙增加端口转发

此方法会在路由器上监听一个端口 8901,然后将流量转发到 Clash 设备的 8901 端口,使用 Clash 设备访问内网。

端口转发

4. 上传到 IP 和端口信息到 Vercel 服务端

4.1 部署 Vercel 服务端存储 IP 和端口信息

Vercel 服务端是我自己写的一个小服务,你可以部署到 Vercel,绑定域名后,拿到 API 地址。

GitHub

部署完成你会有一个 API 地址,例如 https://magic.miantiao.me

修改下面脚本里边的端口地址后,上传到路由器 /usr/bin/diy/dip.sh,下一步配置通知使用。

# #!/bin/sh

# 脚本地址:https://github.com/ccbikai/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 插件。增加打洞配置。

NATMap

6. 手动增加防火墙转发

防火墙


7. 外部设备上订阅 Clash 节点

以上流程跑通以后,就可以在外部设备上订阅 Clash 节点了。 第 4 步部署的服务,可以提供 CLASH 订阅文件,地址是 https://magic.miantiao.me/dip?key=8901&password=chimiantiaomeAPI 地址、key、password 注意换成你的

8. Clash 客户端增加转发规则

rules:
  - DOMAIN-SUFFIX,home,HOME-8901 #节点名称可以查看上一步订阅的节点

配置完后重启 Clash,外部设备浏览器打开 http://OMV.home http://AX3600.home 等设备分配的主机名就可以访问内网设备了。

AX3600

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

AX3600

场景 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/ccbikai/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 插件。增加打洞配置 (注意更新脚本地址)。

NATMap

4. 增加防火墙 NAT 规则,禁用地址重写

如果不修改,qBittorrent 会发现入站 IP 都是路由器 IP,而不是公网 IP,影响统计。

NAT

效果展示

可以看见,分享率还是很客观的。

分享率


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