先说一下我这里的网络,光猫改桥接,R5S OpenWrt 作为主路由拨号,没有公网 IPv4,有公网 IPv6,能拿到 /60 的 IPv6-PD,这应该是国内三大运营商下目前大部分人的一个网络环境。
需求则是家里的 NAS 搭建了一些内网服务需要从公网访问,大概有这么几个使用场景:电脑需要随时能开机进入远程桌面;Jellyfin 视频服务器,需要远程连接看视频;immich 相册备份,我所有的照片都同步在这里,需要随时能查看相册;Ubuntu 虚拟机里搭建了 MineCraft 服务器,需要朋友们能顺利联机。
IPv6 DDNS
首先能想到最直接的还是通过 IPv6 直连,我在 NAS 上通过 Docker 部署了 sanjusss/aliyun-ddns,每 5 分钟刷新一次 DNS 记录,然后设置 OpenWrt 防火墙将对应服务的端口暴露给公网即可。
需要注意的是因为 NAS 和电脑都能直接拿到公网 IPv6 地址,所以并不需要配置端口转发,而是直接在 OpenWrt 的网络 - 防火墙 - 通信规则
中添加相应规则即可。
这里我使用的是 EUI-64 地址来配置防火墙。解释一下什么是 EUI-64:
EUI-64
是一种根据设备的 MAC 地址 自动生成 IPv6 接口标识符(Interface Identifier) 的方法,常用于 SLAAC(无状态地址自动配置)机制下的 IPv6 地址生成。IPv6 地址长度为 128 位,通常由两个部分组成:
区域 长度 说明 网络前缀 64 位 由路由器下发(如 2408:8270:455:270::/64
)接口标识符 64 位 主机自己生成,可由 EUI-64 算法自动生成
要让设备拿到 EUI-64 后缀,需要在 OpenWrt 的 LAN 接口里配置一下,在高级设置
的 IPv6
后缀中填写 eui64
,这样就会给所有设备下发带有 EUI-64 后缀的 IPv6 地址。
因为国内的 IPv6 为动态前缀,而根据 MAC 生成的 EUI-64 后缀是固定的,所以通过添加掩码就可以将数据包只转发给指定设备,防止内网其它设备同端口暴露在公网。
ZeroTier
IPv6 虽然能直连,但还是有不小的局限性。首先虽然 IPv6 基本已经普及,但几乎所有人家里的网络配置 IPv6 都是默认关闭的,很大的可能是我要给朋友分享一个东西,然后发现他根本无法访问 IPv6 地址;又或者某些比较敏感的东西并不希望暴露在公网环境;以及某些服务可能根本不支持 IPv6 网络(点名批评帕鲁),折腾半天搭建起来结果发现 IPv6 根本连不进来。
于是我又在路由器上搭建了内网穿透工具 ZeroTier,搭建教程我这里就不讲了,网上已经非常多了。
配置好之后只要远程连接,就可以访问家里整个内网网段的所有设备,非常方便。
缺点就是需要客户端额外安装 ZeroTier 软件并授权才能连接,而且即使我在一台国内阿里云服务器上搭建了 Moon 服务器,某些网络环境下依然会打洞失败,然后网络流量不知道去哪里绕了一大圈才回来,导致延迟爆炸。
服务器反代
以前我用 frp 做过一些 web 服务以及 ssh 的中转,用一台便宜的小带宽服务器基本够用,但像目前这种需要 Jellyfin 看视频以及相册加载大量图片的场景就力不从心了。于是我又想起来之前大学时折腾过的 NAT 服务器,多台服务器共享一个公网 IP,以及只有数量不多的高位端口,好处是大带宽并且价格便宜。
一番搜索之后找到一台只要 9.9 元/月的成都电信服务器,100Mbps 带宽 1TB 流量,相当够用了,毕竟我家里的上传也就六十多 Mbps。
因为图省事,这次没有用 frp,而是直接直接在这台服务器上也安装了 ZeroTier,测试了一下延迟没有问题,然后直接反代 ZeroTier 的内网 IP 即可,非常好用。
比如我 Jellyfin 内网的端口是 11454,通过 NAS 的 IPv6 地址可以直接访问。那么在服务器上使用 Nginx 在 443 端口反代 11454,然后在服务器的控制面板将 443 端口映射到 11452 端口上,此时通过服务器的 11452 端口就能顺利访问了。
最后把服务器的 IPv4 地址添加域名解析,这样无论是 IPv4 还是 IPv6 都可以顺利访问到内网服务了,并且不需要额外安装任何软件。
HTTPS
暴露在公网的服务还是加上 HTTPS 比较令人安心。目前是 v4 服务器反代和 v6 直连两种访问方式共存,所以需要分别添加 HTTPS 证书。
服务器反代
因为 80 和 443 端口都不能用,所以申请证书稍微麻烦了一点,常规的 HTTP-01 方式不能用,需要使用 DNS-01 模式。这里我选择用 certbot + 阿里云 DNS 插件。
提前配置好阿里云的 Access Key 到 /path/to/credentials.ini
:
dns_aliyun_access_key = 12345678
dns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef
然后通过这条命令申请证书:
certbot certonly \
--authenticator=dns-aliyun \
--dns-aliyun-credentials='/path/to/credentials.ini' \
-d "*.example.com,example.com"
最后在 crontab 里加一条自动续期:
0 3 1 */2 * certbot renew --quiet
Nginx 配置的时候需要注意一点,我们平时配置 301 HTTP 重定向到 HTTPS,是从 80 端口跳转到 443 端口。但是这里我想只使用一个端口,如果在 11452 上配置了 HTTPS,此时如果通过 HTTP 访问,就会报 400 错误 The plain HTTP request was sent to HTTPS port
,字面意思是 HTTP 请求被发送到了 HTTPS 端口。
那么,有没有办法能在同端口进行跳转呢?还真被我找到了解决方案:
只要在 Nginx 配置里加一行
error_page 497 301 =307 https://$host:$server_port$request_uri;
意思是把 497 和 301 错误重定向为 307 到指定 URL。497 是 Nginx 的非标准错误码,专门用来处理这种情况,表示“客户端尝试用 HTTP 访问了一个只支持 HTTPS 的端口”,所以完美地解决了问题。
Unraid 反代
NAS 这边我用的是 Unraid,原本想法是用 certbot 的 docker,和上面一样的方案申请证书,但是折腾了半天都没解决阿里云 DNS 插件的问题,只能另寻方案,最后决定使用 Nginx Proxy Manager,可以同时解决证书申请和反代。
安装好 NPM 之后,在首页选择 SSL Certificaltes - ADD SSL Certificalte - Let's Encrypt
这里勾上 Use a DNS Challenge
,DNS Provider 选 Aliyun
,下面填上阿里云的 Access Key,其它照常填写,就能顺利申请到证书,并且 NPM 会自动完成证书续期,不需要自己操心。
接下来配置反代,添加一个 Proxy Host。由于 NPM 的 HTTPS 反代只能在 443 端口监听,所以这里需要一丢丢的歪门邪道:
就是把 NPM 容器的网络类型改为 Bridge,然后容器的 443 端口映射到 11452 端口,这样 NPM 自己在 443 端口监听,实际到物理机这边通过 11452 端口访问。
其它的就没有什么问题了,转发本地的 11454 端口,协议选 http,最后在 SSL 一栏选择刚才申请的证书,并勾选 Force SSL
,保存之后就可以通过 HTTPS 完美访问。