使用 frp 内网穿透

0 背景

python onnx_app_pyav.py 时注意到:

Running on local URL:  http://127.0.0.1:7860
Running on public URL: https://b68015d75d74ce4815.gradio.live

This share link expires in 72 hours. For free permanent hosting and GPU upgrades, run `gradio deploy` from Terminal to deploy to Spaces (https://huggingface.co/spaces)

这个 Gradio 提供的 联临时 public URL 只能活三天。这是很不好的。

Gradio 也提供了直接在服务器上使用 Nginx 部署的办法:https://www.gradio.app/guides/running-gradio-on-your-web-server-with-nginx。但是现实问题是网络服务器没有足够的 GPU 算力,而付得起的 GPU 服务器(指学校内网 GPU 平台)没有(甚至说不能设置)公网 IP。在这种情况下该怎么办呢?只能曲线救国。

实际运行 Gradio 的机器只能是内网机器,而本地链接又无法在外网访问。我们需要一种办法,使得在公网上能访问内网机器的服务。

我了解到使用 frp 可以进行和 Gradio 同样的内网穿透,故来实践。

1 VPS

有公网 IP,解析好的域名的 VPS。

最好是国外的 VPS,因为规定是:域名解析到国内的服务器上就需要备案,比较复杂。

我这里准备了一个新加坡的服务器。

2 frp

2.1 工作原理

frp 主要由两个组件组成:客户端 (frpc) 和 服务端 (frps)。通常情况下,服务端部署在具有公网 IP 地址的机器上,而客户端部署在需要穿透的内网服务所在的机器上。

由于内网服务缺乏公网 IP 地址,因此无法直接被非局域网内的用户访问。用户通过访问服务端的 frps,frp 负责根据请求的端口或其他信息将请求路由到相应的内网机器,从而实现通信。

2.2 下载

GitHub 地址:https://github.com/fatedier/frp,在 Release 里下载。

跟随最新就好,这样可以匹配最新的官方文档。https://gofrp.org/

wget https://github.com/fatedier/frp/releases/download/v0.52.3/frp_0.52.3_linux_amd64.tar.gz
tar -xvf frp_0.52.3_linux_amd64.tar.gz
mv frp_0.52.3_linux_amd64 frp

3 通过 SSH 访问内网机器

先从最简单的开始。

3.1 设置 frp

设置参考文档:https://gofrp.org/zh-cn/docs/examples/ssh/

内网机器对应 frpc,VPS 对应 frps。

frp 不限机器的类型,比如客户端 frpc 可以是 Linux 也可以是 Windows。操作基本没有区别。

frps.toml:

bindPort = 7000 # frp 所在服务器用于接收客户端连接的端口

frpc.toml:

serverAddr = "xx.xx.xxx.xxx" # frps 所在服务器的公网 IP 地址
serverPort = 7000 # 对应 frps.toml 的 bindPort

[[proxies]]
name = "Gradio" # 名称无所谓
type = "tcp"
localIP = "127.0.0.1" # 内网服务的地址
localPort = 7860 # 内网服务的端口
remotePort = 20000 # frp 服务端监听的端口

3.2 设置 VPS 防火墙

两层:一层防火墙规则(云平台设置),一层防火墙本身,都要修改。

两个端口:bindPortserverPort)和 remotePort 都要开放。

防火墙规则:设置规则,开放两个端口。

防火墙:设置防火墙一般需要 sudo。


Ubuntu:打开防火墙,使用开放指定端口的命令,改完之后重启防火墙。

sudo ufw enable # 启动防火墙:之后选项选 y
sudo ufw allow 22 # 马上 allow 22 的 ssh,其他的还有:7000、80、443 端口等
sudo ufw allow 7000
sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 20000 # 为后续的 http 访问开一个端口备用
sudo ufw status # 查看开放端口,确认都开启
sudo ufw reload # 重启防火墙

另 CentOS 7 命令参考如下:

systemctl start firewalld # 启动防火墙
firewall-cmd --zone=public --add-port=1935/tcp --permanent # 开放指定端口(端口号自己改)
firewall-cmd --zone=public --remove-port=7860/tcp --permanent #关闭指定端口(端口号自己改)
firewall-cmd --zone=public --list-ports # 查看 firewall 端口
firewall-cmd --reload # 重启 firewall
systemctl stop firewalld.service # 停止 firewall
systemctl disable firewalld.service # 禁止 firewall 开机启动

检查方法:(修改完防火墙之后建议等一小会儿)

  1. nmap xx.xx.xxx.xxx -p remotePort 参数为公网地址和开放端口。端口状态是 filtered 则未放行。(open 和 close 都没事)
  2. crul xx.xx.xxx.xxx:remotePort 参数为公网地址和开放端口。

参考:https://github.com/fatedier/frp/issues/982

3.3 frp 运行(测试时多用)

(别忘记先启动你的服务)

frps

./frps -c ./frps.toml

frpc

./frpc -c ./frpc.toml

最后,访问:公网 IP + : + remotePort 即可。即 xx.xx.xxx.xxx:remotePort

如果有绑定域名的话,还可以将 IP 替换为域名访问。


可能出现的问题:时间不匹配。

参考:https://github.com/fatedier/frp/issues/510

客户端和服务器的时间不对应会导致错误。需要矫正时间。

参考:http://www.mobiletrain.org/about/BBS/150075.html

两边都执行:

sudo timedatectl set-timezone Asia/Shanghai # 中国上海时区,与北京时间保持一致
sudo apt-get install ntp -y # 网络时间协议(NTP)来自动同步系统时间
sudo ntpdate cn.pool.ntp.org # 从中国 NTP 服务器上获取最新的时间,并将系统时间进行更新
sudo hwclock --systohc # 把系统时间同步到硬件时钟中

3.4 systemd 设置持久运行

一般在测试确认无误之后使用。

文档推荐:https://gofrp.org/zh-cn/docs/setup/systemd/


安装 systemd 不必多说。

创建写入 frps.service 文件:

sudo vim /etc/systemd/system/frps.service

写入

其中 frps 的安装路径需要写绝对路径。可以使用 readlink -f 获取绝对路径。

此外,不仅是 server,client 也能画蛇添足写出 systemd 的设置。

/etc/systemd/system/frpc.service

[Unit]
# 服务名称,可自定义
Description = frp client
After = network.target syslog.target
Wants = network.target

[Service]
Type = simple
# 启动 frpc 的命令,需修改为您的 frpc 的安装路径
ExecStart = /root/frp/frpc -c /root/frp/frpc.toml

[Install]
WantedBy = multi-user.target

使用 systemd 命令管理 frps 服务是很方便的:

sudo systemctl start frps # 启动 frp
sudo systemctl stop frps # 停止 frp
sudo systemctl restart frps # 重启 frp
sudo systemctl status frps # 查看 frp 状态

在启动 frp 后,用 sudo systemctl status frpc 查看状态。

如果报错、无法运行,可能是由于安全设置缺少运行二进制文件的权限,可使用 chmod +x

成功启动输出样例:

frpc.service - frp client
   Loaded: loaded (/etc/systemd/system/frpc.service; disabled; vendor preset: enabled)
   Active: active (running) since Fri 2023-10-27 02:36:04 UTC; 4s ago
 Main PID: 29298 (frpc)
    Tasks: 5 (limit: 4915)
   CGroup: /system.slice/frpc.service
           └─29298 /root/frp/frpc -c /root/frp/frpc.toml

最后设置 frps 开机启动:

sudo systemctl enable frps

4 为本地 HTTP 服务启用 HTTPS

前面的方法,既带了端口,又没有 https,这很不好,让我们来改进一下。

文档:https://gofrp.org/zh-cn/docs/examples/https2http/

首先需要指出的是,文档中提到的办法是只使用 frp 自身来进行的操作。实际上还有 frp + Nginx 的组合操作,在网上看解决办法的时候需要注意。

从结论来说,文档中的办法并不是最好的办法。在 frp 单独使用的时候,frp 可以使用代理服务器的 80 和 443 端口;Nginx + frp 的方法中,Nginx 则占用了这两个端口,故 frp 只能转而使用其他端口。不过,不得不说 Nginx 才是专业的,把 443 端口给他用很正确。

使我意识到这一点的是:https://www.cnblogs.com/shook/p/12790532.html,写的十分好的一篇文章。

4.1 共通:证书申请

不管是上面提到的哪种方法,都需要申请 SSL 证书。这里我们使用免费的 Let’s Encrypt。

实际证书的申请和平台无关,故而我们可以在 Linux、Windows 上生成。

Linux 方法

先安装。

sudo apt update
sudo apt upgrade -y
sudo apt install -y certbot # 证书申请工具

证书申请命令:

sudo certbot certonly \
    --manual \
    --preferred-challenges dns \
    --server https://acme-v02.api.letsencrypt.org/directory \
    -m sorrow-time@outlook.com \
    -d "i4.work, *.i4.work"

申请过程中需要进行 TXT 的 DNS 解析,下面是示例:

Please deploy a DNS TXT record under the name
_acme-challenge.i4.work with the following value:

nrmd1SOtdH0WFuzbBWy7Cj7NMEw7XIc_I772-yTz1rE

Before continuing, verify the record is deployed.

我是到阿里云去操作的,求稳可以等到 DNS 解析刷新结束。

txt-dns 解析

结束后证书位置:下载其中 fullchain.pemprivkey.pem 保存留用。后缀名是什么并不重要,不管是 .key 还是 .crt 的都无所谓。

/etc/letsencrypt/live/i4.work/

证书更新:

先输入:

crontab -e # 定时任务

然后写入(可选 vim 操作):

30 1 10 * * /usr/bin/certbot renew

保存退出。


更详细可参考:https://www.cnblogs.com/wzlinux/p/11188454.html

Windows 方法

参考:https://zhuanlan.zhihu.com/p/627526278

Windows 不让 \ 换行,一行版:

certbot certonly -d "i4.work, *.i4.work" -m sorrow-time@outlook.com --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory

别的参考:https://zhuanlan.zhihu.com/p/438161610

4.2 法 1:frp 自身进行反代

先大致描述情景:Windows 一台作为内网机器(客户端),公网 Linux VPS 一台作为代理(服务端)。

证书仅位于内网机器(客户端)

VPS 仅需要开放防火墙、配置 frps.toml 即可。

bindPort = 7000
vhostHTTPSPort = 443

内网机器上需要:准备好 SSL 证书和私钥、配置 frpc.toml

serverAddr = "xx.xx.xxx.xxx"
serverPort = 7000

[[proxies]]
name = "test_htts2http"
type = "https"
customDomains = ["i4.work"]

[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:7860"

# HTTPS 证书相关的配置
crtPath = "./fullchain.pem"
keyPath = "./privkey.pem"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"

然后先后运行。

4.3 衔接:Nginx 设置

完成法 1 之后可以使用 Nginx 来解决两个小问题:

  1. http 跳转到 https
  2. www 跳转到不带 www(或者相反)

  1. 安装 Nginx
sudo apt install -y nginx

关于 Nginx,不同的安装方式,Nginx 的位置也不一样。本次教程中位于 /etc/nginx/

  1. Nginx 使用 systemd 管理
sudo systemctl status nginx # 查看状态
sudo systemctl stop nginx # 停止
sudo systemctl start nginx # 启动
sudo systemctl restart nginx # 强制重启

nginx -t # 检查格式
  1. Nginx 配置基础知识——自行了解。

建议先学习基本的 Nginx 的配置的知识,对配置有基本的认识。

例如:

总配置 etc/nginx/nginx.conf 中,最后 include 的部分:

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;

我们不直接在该文件中写入配置,而是修改、添加这些 include 中的文件。

  1. 实际解决

方法:

sudo vim /etc/nginx/conf.d/i4.work.conf

写入:

server {
    # Force redirect to https
    if ($scheme != "https") {
        return 301 https://$server_name$request_uri;
    }

    if ($host = 'www.i4.work' ) {
        return 301 $scheme://i4.work$request_uri;
    }
}

之后检查格式,并重启 Nginx:

nginx -t
sudo nginx -s reload
  1. 最后验证

我们希望的结果:四个 url 都能跳转到 https://i4.work/ 即成功。

https://i4.work/
http://i4.work/
https://www.i4.work/
http://www.i4.work/

4.4 法 2:Nginx + frp 配置

证书仅位于公网 VPS(服务端)

可以在完成 4.1 ~ 4.3 的基础上修改部分内容。


内网机器上需要:仅配置 frpc.toml

serverAddr = "xx.xx.xxx.xxx"
serverPort = 7000

[[proxies]]
name = "web"
type = "http"
localPort = 7860
customDomains = ["i4.work"]

VPS 需要:

  1. 再为 http 开放一个端口(如果在 3.2 没操作就要进行了)
  2. 配置 frps.toml
bindPort = 7000
vhostHTTPPort = 20000
  1. 将证书和密钥放到 /etc/nginx/cert 目录下。该目录还不存在,故而使用 mkdir
mkdir /etc/nginx/cert
cp fullchain.pem /etc/nginx/cert/fullchain.pem
cp privkey.pem /etc/nginx/cert/privkey.pem
  1. 补充配置: /etc/nginx/conf.d/i4.work.conf,最后结果:
server {
    listen 80;
    listen 443 ssl;

    # Force redirect to https
    if ($scheme != "https") {
        return 301 https://$server_name$request_uri;
    }

    if ($host = 'www.i4.work' ) {
        return 301 $scheme://i4.work$request_uri;
    }

    server_name i4.work;

    ssl_certificate cert/fullchain.pem;
    ssl_certificate_key cert/privkey.pem;

    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass  http://127.0.0.1:20000; # 反向代理
        # 映射的 frp 服务端 frps.toml 的 vhostHTTPPort 端口
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_max_temp_file_size 0;
        proxy_redirect off;
        proxy_read_timeout 240s;
    }
}
  1. 重载 Nginx 配置。
nginx -t
sudo nginx -s reload

然后先后运行测试一下。验证方法同上。

一点发现

似乎等待大文件处理完毕(同步)的 error 消失了!这真是惊奇的发现。但是我并不知道是为什么。

更新:猜测可能是 Gradio 的共享链接做出了限制,而使用本地链接访问没有这种限制,顺带着内网穿透也没有限制了。

最后修改:2023 年 10 月 29 日
如果觉得我的文章对你有用,请随意赞赏