在购入 NAS 并搭建常用服务后,外网访问通常是许多人追求的下一个目标。在众多常用的外网访问方案中,frp 内网穿透工具可能是最热门的选择。
frp 特点
根据笔者的体验,frp 相对于其他方案有这些优势:
- 相较于 VPN 异地组网方案,frp 比 Wireguard 配置更简单、更容易理解,比 Tailscale 更自由(Tailscale 想要好的体验需要自托管第三方开发的中继,例如 headscale),并且不要求访问者使用客户端。
- 相较于同样是内网穿透方案、在简中比较出名的 NPS,笔者自认为 frp 更轻量、稳定性更高、性能更好;况且 frp 仍在积极维护并且有齐全文档,而原仓库 ehang-io/nps 早已停止维护并且文档有些模糊,目前只有 yisier/nps 仍在维护。
不过 nps 也有个优点:NPS 的客户端的内网穿透可在服务端可视化配置,方便分发和管理;而 frp 的配置文件只能手写 TOML、不能可视化配置、必须由客户端提供。
方案设计
设想这样一个情景:
家里网段是 192.168.1.0/24,家中有台群晖,IP 地址是 192.168.1.44,上面跑了基于 HTTP 的 Jellyfin 和基于 TCP 的 SSH。
问题来了:要想在家门外访问 Jellyfin 或 SSH,需要知道对应的 IP 地址和端口,即 ip:port。群晖所处网络是内网,而我们在家门外的设备连接的是另一个内网,二者不互通。所以必须要让这个 ip 是一个全球可路由的 IP 地址,即俗称的“公网 IP”。
这个“公网 IP”如何获取?对于非移动用户,可以找客服申请一个动态的公网 IPv4(虽然最近几年申请也很麻烦了);但对于移动用户就没辙,客服死活不分配。于是就有了“内网穿透”这个方案:
- 租一台拥有公网 IP(假设为
vsp-ip)的 VPS,上面运行内网穿透服务; - 内网设备运行内网穿透客户端,主动和内网穿透服务建立持久的连接;
- 外网客户端尝试访问
vsp-ip:remote-port,内网穿透服务将请求转发至local-ip:local-port,对应内网设备上的服务。同时服务的回包也通过 VPS 转发回去。
由此就解决了在外网无法访问内网服务的问题。
理论上说,你也可以选用运营商提供的全球单播 IPv6 地址(相当于 IPv4 的公网地址)来访问内网设备,但一来需要家中网关开启配置,二来尽管截至目前绝大多数设备已经从硬件上支持 IPv6,许多网络环境并不默认开启获取 IPv6 前缀的功能,导致网络内设备根本获取不到全球单播的 IPv6,自然也就无法通信了。
配置步骤
frp 相关的配置步骤具有时效性,部署前请参考 官方文档。
对 frp 的配置基于以下场景:
- 家庭网络环境,子网为
192.168.1.0/24; - frp 客户端(frpc)运行在群晖 NAS 上,IP 地址为
192.168.1.44; - 待穿透服务为 Jellyfin 和 SSH,运行在群晖上:前者走 HTTP 协议、监听端口为 8096;后者走 TCP 协议、监听端口为 2222;
- frp 服务端(frps)运行在 VPS 上,
example.com解析到 VPS 的 IP 地址123.123.123.123。VPS 使用 Linux 主流发行版。
实际上笔者并不推荐穿透 SSH 服务到公网,或至少配置 10000 以上的映射端口,否则容易被自动脚本爆破骚扰;更推荐的做法是通过 Wireguard 等异地组网方案访问 SSH 服务。此处仅仅是为了提供一个简单的 TCP 穿透样例。
但方案具有一定的通用性,略作修改即可在其他场景下应用。
服务端(frps)配置
frp 可直接运行在宿主机上,或者运行在 Docker 容器中;对于 frps,建议选择前者,以避免 Docker 更新导致服务暂时中断。
下载服务端并配置 systemd
从 Github Release 页面下载最新版本的二进制文件压缩包。以 v0.69.0 为例:需要下载名为 frp_0.69.0_linux_amd64.tar.gz 的版本,其中 linux 代表 OS,amd64 代表 x86-64 平台。
1 | wget https://github.com/fatedier/frp/releases/download/v0.69.0/frp_0.69.0_linux_amd64.tar.gz |
然后将压缩包解压到合适的位置,比如当前工作目录:
1 | tar -xzf frp_0.69.0_linux_amd64.tar.gz |
重命名一下:
1 | mv frp_0.69.0_linux_amd64 frps |
进去看看有什么内容:
1 | $ cd frps && ls -al |
可以看到打包了客户端和服务端及各自配置文件,以及 license。我们只需要服务端 frps,为防止混淆可以删掉客户端:
1 | rm frpc frpc.toml |
按理说如果写好了配置文件 frps.toml,现在就可以通过 ./frps -c ./frps.toml 运行了。但这样显然不妥:进程依附于当前 Shell,若连接断开就会关闭;VPS 启动时也不会自动启动 frps。因此我们要配置 systemd。
创建 frps 的 systemd 配置 /etc/systemd/system/frps.service,将以下内容写入文件:
1 | [Unit] |
然后就可以通过 systemd 管理 frps 了。常用命令有:
1 | # 启动 frps |
编写 frps.toml
在启动 frps 前,我们还需要编写其配置文件。frp 的客户端配置存储在 frpc,因此 frps 的配置比较简单。
frps 目前支持的文件格式包括 TOML/YAML/JSON,旧的 INI 格式仍然支持,但已经不再推荐。网络上大量 frps 教程均基于 INI 配置格式,存在一定的误导性。
因为官方文档的配置示例均使用 TOML,所以我们也采用 TOML。下面给出一个适用于大多数场景的配置示例:
1 | # 全局配置,主要和客户端连接方式相关 |
注意:上述配置文件涉及端口需要被 VPS 及其提供商的防火墙放行入站流量。
重点讲解以下字段:
bindPort:frps 监听端口,默认为 7000。frpc 会尝试连接此端口,建立连接后 frpc 和 frps 间的所有流量都走该端口。默认使用 TCP,如需使用其他协议,可参考 官方文档 将此键替换为kcpBindPort/quicBindPort等其他协议对应的配置。vhostHTTPPort:frps HTTP 类型代理监听的端口。外网设备尝试访问穿透的 HTTP 服务时需要访问该端口,frps 会将流量转发给 frpc。类似的还有vhostHTTPSPort等。
TCP 服务不需要类似配置,因为每一个 TCP 服务都需要 frps 监听一个独一无二的端口(由客户端指定),而 frps 可通过域名(由客户端指定)区分不同的 HTTP 连接。
更多字段配置详见 官方文档。
值得注意的是,官方文档将字段归类为不同的表格,表格名称对应 Go 源码中的结构体名称,字段名、类型等也是如此,注意分辨和 TOML 的映射关系。据笔者所知,
ServerConfig/ClientConfig分别对应frps.toml/frpc.toml的顶层表,其余表格名对应一些表格中字段类型名,属于嵌套表,而如果一个字段的类型和表格名匹配,则字段名就是对应表的名称。表数组规则类似。
编写完配置文件后记得运行服务:
1 | systemctl start frps |
此时浏览器访问 http://frps.example.com,输入账号密码后登录,应能看到 frps 的后台面板。不过 frps 提供的面板功能很少,只能起到一个简略的监控作用,无法查看具体日志,也无法修改配置。
客户端(frpc)配置
由于客户端可能是群晖之类的设备,其 Linux 环境不一定标准,因此建议使用 Docker 部署 frpc。
frpc 和需要穿透的服务不一定要在同一台机器上:frpc 本质上是一个反向代理服务器,它可以把请求转发给其他设备,因此也完全可以在支持的网关(如软路由)上部署 frpc。本文为举例方便,仍选择二者合一。
Docker 部署 frpc
推荐使用 Docker Compose 部署 frpc,原因是通用性好、易维护。当然也可以选择使用 Docker CLI 或者是群晖等 NAS 提供的可视化界面,但笔者更习惯 compose.yml + 命令行。
新建存放 compose.yml 和 frpc.toml 的目录 frpc 并 cd 进去:
1 | mkdir frpc && cd frpc |
将以下内容写入 compose.yml:
1 | services: |
待 frpc.toml 编写完成后,通过 docker compose up -d 启动服务。
编写 frpc.toml
frpc 配置稍微复杂,因为不仅需要指定要连接的服务端,还需要指定待穿透的服务。下面是一个配置示例:
1 | serverAddr = "example.com" # 或者直接用 IP 地址 |
注意到 TCP 代理需要配置 remotePort,而 HTTP(S) 代理则不需要。这是因为 HTTP(S) 代理统一通过 http://custom.example.com:vhostHTTPPort 或 https://custom.example.com:vhostHTTPSPort 访问,frps 根据 URL 将请求路由到不同的 frpc。
例如,对于我们假设的场景,假设再增加一个群晖 DSM Web 管理界面的 HTTP 代理,配置 customDomains = ["dsm.example.com"],则:
- 当外网设备访问
http://jellyfin.example.com:8080时,实际访问 Jellyfin 的 Web UI; - 当外网设备访问
http://dsm.example.com:8080时,实际访问 DSM 的 Web UI; - 当外网设备执行
ssh user@123.123.123.123 -p 2222时,实际访问群晖的 SSH 服务。
注意:部分服务可能需要配置代理后的 URL 方可返回正确的响应,例如对于上述情景,Jellyfin 需要在“控制台 -> 网络 -> 基础 URL”中填写
http://jellyfin.example.com:8080。具体配置请参阅被代理服务的官方文档。
【可选】配置反向代理服务器
配置反向代理服务器的理由有这几个:
- 若
vhostHTTPPort/vhostHTTPSPort不配置为默认的80和443,则访问 HTTP 服务时必须带上对应端口号,比较麻烦;而 VPS 可能不止 frps 这一个服务,例如可能还托管了静态博客,80 / 443 端口可能被占用; - 为安全起见,推荐为 HTTP 服务配置 SSL 证书,但若让 frps 管理证书则难以自动化,并且其他服务也要申请证书,如此一来证书散落在各处,不方便续期;
- frps 监听的端口直接暴露在公网,容易被攻击。
而反向代理服务器选择 Caddy 而非普遍使用的 nginx 的理由则是:Caddy 配置反向代理和 SSL 证书极其简单,并且个人内网穿透场景完全不会出现高并发,Caddy 的性能不会成为内网穿透的瓶颈。
在 VPS 上安装 Caddy 后,向其配置文件 /etc/caddy/Caddyfile 写入以下内容:
1 | jellyfin.example.com { |
上述配置自动为 jellyfin.example.com 申请 SSL 证书,将来自 https://jellyfin.example.com:443 的请求转发至 http://localhost:8080,即 frps 监听的 HTTP 协议端口。Caddy 会自动处理 HTTPS,因此 frps 只需要监听 HTTP,不用考虑 HTTPS,因而 vhostHTTPSPort 可以不用配置;另外防火墙也可删除对 8080 和 4430 端口的放行规则,转而配置对 443 端口的放行规则。
随后设置 Caddy 开机自启并立即启动:
1 | systemctl enable --now caddy |
关于更详细的 Caddy 配置,请参阅其 官方文档。
总结
通过上述步骤,我们打通了内外网设备的链路,使得内网服务像服务器一样暴露在公网可供人访问。不过当服务暴露在人人触手可及的地方时,更须注意网络安全,否则整个内网都会受到威胁。