0%

Frps 实现内网穿透

引言

最近给老家的爸妈升级了下网络设备,让他们也能随时玩些 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
2
3
4
5
bindPort = 7000
vhostHTTPSPort = 443
auth.method = "token"
auth.token = "xxxxx"
transport.tcpMux = true

7000 为服务监听端口,负责客户端的连接(这里就不介绍原理),443 为 HTTPS 访问端口,也可以改成其他的(如果仅仅是 HTTP 访问,则需要改成 vhostHTTPPort)。后期访问就是:https://域名+端口。接下来两个参数就是客户端认证方法。最后一个参数为多路复用,该参数具有争议,有人说关闭之后,服务稳定性下降,但是速度提升。开启则相反,我这里之所以开启是因为,关闭状态下,公网访问隔段时间就会断流,开启状态下则没有这个问题。

接下来就是开启服务器的防火墙,放行 bindPortvhostHTTPSPort 两个端口。然后执行以下命令启用:

1
nohup ./frps -c ./frps.toml > info.log 2>&1 &

客户端部署

客户端最开始打算升级 OpenWrt 中 frpc 的版本,但是又牵扯到视图层了,所以放弃了。那么就需要更换其他的方案。仔细想一下,frpc 其实就是一个注册服务,有没有视图无所谓,那么这不就更适合 Docker 中部署嘛。而环境中的 Docker 全权是交给 NAS 中的,那么 frpc 就顺理成章的由 NAS 托管就好了。

  • 镜像制作

    运行时环境采用 alpine,将 frpc 打到镜像中,由于服务的注册是以配置文件的方式实现的。这样我们就可以将配置文件暴露出来以挂载的方式实现动态注册。

    1
    2
    3
    4
    5
    FROM 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
      7
      serverAddr = "公网域名"
      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
    2
    openssl 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
    12
    openssl 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
    12
    openssl 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
      3
      transport.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
      3
      transport.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 就会稍许麻烦,因此并不是很推荐。