引言
最近给老家的爸妈升级了下网络设备,让他们也能随时玩些 YouTube、Netflix。为了给孩子看动画片,顺便安装了 Jellyfin,使其远程连接到我的 NAS 系统。同时为了约束后期孩子的上网行为,软路由则是必须要安排的了。老家的宽带用的电信,那么顺便也改下桥接,要个公网吧。
桥接很快安排了,但是拨号后拿到的确实内网 ip,这不搞笑嘛,赶紧联系了装维人员,被告知保定已经不让申请公网 ip 了。无奈,只好放弃,虽然牺牲了部分功能,但是起码能用。虽然拿不到公网ip,但是可以内网穿透不是!
软路由正好自带了 frpc 客户端,看了下大概的原理,需要一个具有公网的服务器,这不简单,直接用我代理的服务器就行了。域名?这更不在话下了,毕竟是开发人员,域名是必须的。剩下的就是部署 frps 服务端了。
软路由带的 frpc 版本比较老——v0.40.0 的,也只能下载对应版本的服务端了。a long time later…,一顿操作之后,能够访问了,但是几次下来,直接阻断了。完,被运营商 QoS 了,看了下方案,采用 TLS 或者 KCP 可以解决 QoS 问题,但是发现效果并没有什么卵用,再加上不是什么刚需,尝试了几次之后,放弃了。
转机
事情出现了“转机”(否则也没有本篇文章的出现了),几天前打算把孩子的照片上传到 NAS 里,由于是晚上,传了部分之后,就睡觉了,打算让它后台慢慢传。然后第二天看看传的进度。“转机”出现了,相册打不开了,尝试了几次之后发现还是不行。出现这个问题我想大概只有三种情况:一、家里断网了;二、DDNS 正在更新;三、公网 ip 出问题了。但是我突然有种不好的预感,也就是说可能发生第三种情况,因为前两种情况是很好排除的。
为了验证我的猜想,赶紧回到小区,进入路由后台一看,心凉了,拨号后拿到的 ip 就是一个内网 ip。此时内心的心情,你懂的,想把电信整个族谱问候一遍(没错,我小区用的也是电信的宽带)。与老家不同,如果没有公网 ip,那么意味着所有的网络拓扑 OpenWrt、QB、NAS 都会出现问题。赶紧打了 10000 投诉,中间的过程省略了,总之用网协议都签了,也不行,公网 ip 死活不给。想要公网 ip 只能专线,价格嘛,20M 宽带 2400 一年。
对于公网 ip,除了电信,就只能问问联通了,结果不用想。
方案确定
由于试过 frp,所以这个方案我是直接 pass 的,给我的感觉是不稳定。先前用过 ngrok,所以想看看它的付费方案,毕竟我需要一个稳定的方案。每月最低 8 刀的价格也并没满足我当下的需求。
尝试了不少国内的付费方案发现要不不稳定,要不不满足我当下的需求,无奈只能放弃。此时脑海里符合我预期的就是 frp 了。我在想不稳定是不是版本太老了,试试最新的版本看看如何。
frp 实现内网穿透
此次确定方案的版本为 v0.60.0,经过了几次测试之后,发现远比 v0.40.0 版本强多了,所以方案最终敲定。
服务端部署
服务端比客户端部署要简单的多,配置文件也比较简单:
1 | bindPort = 7000 |
7000 为服务监听端口,负责客户端的连接(这里就不介绍原理),443 为 HTTPS 访问端口,也可以改成其他的(如果仅仅是 HTTP 访问,则需要改成 vhostHTTPPort)。后期访问就是:https://域名+端口。接下来两个参数就是客户端认证方法。最后一个参数为多路复用,该参数具有争议,有人说关闭之后,服务稳定性下降,但是速度提升。开启则相反,我这里之所以开启是因为,关闭状态下,公网访问隔段时间就会断流,开启状态下则没有这个问题。
接下来就是开启服务器的防火墙,放行 bindPort
、vhostHTTPSPort
两个端口。然后执行以下命令启用:
1 | nohup ./frps -c ./frps.toml > info.log 2>&1 & |
客户端部署
客户端最开始打算升级 OpenWrt 中 frpc 的版本,但是又牵扯到视图层了,所以放弃了。那么就需要更换其他的方案。仔细想一下,frpc 其实就是一个注册服务,有没有视图无所谓,那么这不就更适合 Docker 中部署嘛。而环境中的 Docker 全权是交给 NAS 中的,那么 frpc 就顺理成章的由 NAS 托管就好了。
镜像制作
运行时环境采用 alpine,将 frpc 打到镜像中,由于服务的注册是以配置文件的方式实现的。这样我们就可以将配置文件暴露出来以挂载的方式实现动态注册。
1
2
3
4
5FROM alpine:3.20.3
RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
ADD frpc /usr/local/apps/frpc/bin/frpc
RUN chmod +x /usr/local/apps/frpc/bin/frpc
ENTRYPOINT [ "sh", "-c", "./usr/local/apps/frpc/bin/frpc -c /usr/local/apps/frpc/config/frpc.toml"]内容也很简单,配置好时区,然后运行 frpc,我们运行的时候,需要将配置文件挂载出来。
配置文件处理
为了方便配置文件的更新同步,我把配置文件存到 NAS 中,同时将它与本地双向同步,这样,本地修改之后就会同步到远程 NAS,从而进一步被 NAS 中的 frpc 感知到,从而实现更新。
为了方便配置文件的管理,我按照以下路径存放:
1
2
3
4
5
6
7
8
9|---frp
|---certs
|---xxx.pem
|---xxx.key
|---confd
|---a.toml
|---b.toml
|---c.toml
|---frpc.toml我们挂载的时候,直接挂载 frp 目录即可,certs 证书目录、confd 配置目录、frpc.toml 入口文件则自动挂载了。
frpc.toml
1
2
3
4
5
6
7serverAddr = "公网域名"
serverPort = 443
auth.method = "token"
auth.token = "xxxxx"
transport.tcpMux = true
includes = ["/usr/local/apps/frpc/config/confd/*.toml"]由于我们要暴露到公网的服务比较多,全部放到 frpc 中其实不便于管理,所以我们利用
includes
参数将他们分散到外部的confd
路径中,这样一来,只要你想增加暴露的服务,往confd
中新增一个配置文件即可,无需修改存量文件。serverAddr
我推荐写成域名,这样当域名后的服务更新时,也不需要更改配置文件了。xxx.tmol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20[[proxies]]
name = "AdGuardHome"
type = "https"
customDomains = ["ad.二级域名"]
transport.useEncryption = true
transport.useCompression = true
healthCheck.type = "tcp"
healthCheck.timeoutSeconds = 3
healthCheck.maxFailed = 3
healthCheck.intervalSeconds = 10
[proxies.plugin]
type = "https2http"
localAddr = "172.31.0.1:3000"
crtPath = "/usr/local/apps/frpc/config/certs/ad.二级域名.pem"
keyPath = "/usr/local/apps/frpc/config/certs/ad.二级域名.key"
hostHeaderRewrite = "172.31.0.1"
requestHeaders.set.x-from-where = "frp"我们每暴露一个服务,只需要增加类似的配置文件即可,要更改的就是
customDomains
访问域名,localAddr
目标服务运行的ip
及端口、该域名对应证书。ssh.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14[[proxies]]
name = "SSH"
type = "tcp"
localIP = "172.31.0.1"
localPort = 22
remotePort = 15020
transport.useEncryption = true
transport.useCompression = true
healthCheck.type = "tcp"
healthCheck.timeoutSeconds = 3
healthCheck.maxFailed = 3
healthCheck.intervalSeconds = 10对于 ssh 访问,稍有不同,需要增加一个
remotePort
参数。我们访问的时候需要:1
ssh xx@xxx -p 15020
域名解析
所有服务的访问都是通过域名来暴露的,我们只需要从我们的域名服务商那里添加一个泛解析即可,如果你的二级域名是:
test.com
,只需要增加*.test.com
解析到公网服务器ip
即可。
开启 TLS 双向校验
为了避免中间人攻击,我们可以启用客户端、服务端双线验证,以确保链接的安全性。
从 v0.50.0 开始,transport.tls.enable 的默认值为 true,即:默认开启 TLS 协议加密。如果 frps 端没有配置证书,则会使用随机生成的证书来加密流量。默认情况下,frpc 开启 TLS 加密功能,但是不校验 frps 的证书。
💡 Tips
注意:启用此功能后除 xtcp 外,可以不用再设置 transport.useEncryption 重复加密。
准备以下配置文件 openssl.cnf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23[ ca ]
default_ca = CA_default
[ CA_default ]
x509_extensions = usr_cert
[ req ]
default_bits = 2048
default_md = sha256
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca
string_mask = utf8only
[ req_distinguished_name ]
[ req_attributes ]
[ usr_cert ]
basicConstraints = CA:FALSE
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = CA:true生成 CA
1
2openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -subj "/CN=frps.ca.com" -days 5000 -out ca.crt生成服务端证书
1
2
3
4
5
6
7
8
9
10
11
12openssl genrsa -out server.key 2048
openssl req -new -sha256 -key server.key \
-subj "/C=XX/ST=DEFAULT/L=DEFAULT/O=DEFAULT/CN=server.com" \
-reqexts SAN \
-config <(cat openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost,IP: 你的服务端公网 ip,DNS:你的服务端域名")) \
-out server.csr
openssl x509 -req -days 5000 -sha256 \
-in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-extfile <(printf "subjectAltName=DNS:localhost,IP: 你的服务端公网 ip,DNS:你的服务端域名") \
-out server.crt生成客户端证书
1
2
3
4
5
6
7
8
9
10
11
12openssl genrsa -out client.key 2048
openssl req -new -sha256 -key client.key \
-subj "/C=XX/ST=DEFAULT/L=DEFAULT/O=DEFAULT/CN=frp.client.com" \
-reqexts SAN \
-config <(cat openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:frp.client.com,DNS:frp.client.com")) \
-out client.csr
openssl x509 -req -days 5000 -sha256 \
-in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-extfile <(printf "subjectAltName=DNS:frp.client.com,DNS:frp.client.com") \
-out client.crt最终的配置文件
1
2
3
4
5
6
7
8
9
10
11
12.
├── ca.crt
├── ca.key
├── ca.srl
├── client.crt
├── client.csr
├── client.key
├── openssl.cnf
├── server.crt
├── server.csr
└── server.key
将证书上传到服务端并修改配置文件,增加 tls 验证相关配置:
frpc.toml
1
2
3transport.tls.certFile = "/usr/local/apps/frpc/config/certs/client.crt"
transport.tls.keyFile = "/usr/local/apps/frpc/config/certs/client.key"
transport.tls.trustedCaFile = "/usr/local/apps/frpc/config/certs/ca.crt"fprs.toml
1
2
3transport.tls.certFile = "/to/cert/path/server.crt"
transport.tls.keyFile = "/to/key/path/server.key"
transport.tls.trustedCaFile = "/to/ca/path/ca.crt"
随后重启服务端和客户端。至此,客户端和服务端的双向验证配置完毕。
Docker 网络模式
整个系统的网关是软路由,而软路由上也存在其他需要暴露的服务,我们在 Docker 中部署服务的时候,网络模式需要选为 Host
模式。这样的好处就是,容器以宿主机(NAS)IP 作为自己的 IP,端口作为宿主机的端口,配置起来比较方便。而 bridge
模式相当于与 NAS 平级,那么由于软路由采用静态 IP 绑定,分配容器 IP 就会稍许麻烦,因此并不是很推荐。