【踩坑贴】内网穿透踩坑记录

前言

最近在做一个物联网相关的需求,大致要求是用户使用指纹识别开门后,除了将设备属性上报到华为云IoT平台外,还需要在华为云配置数据转发,将开门日志转发到后端服务并存到数据库,用于向用户端展示开门日志。

这其实是一个挺朴实无华的需求的,只需要在华为云上把数据转发配置为自己服务器的地址就行了。问题就在于:我们只有一个已经无法容纳其它较大量级服务的腾讯云服务器,也就是说,我们无法将服务部署上云。

问题

好了,现在的问题就在于:项目在本地进行开发,如何让华为云将数据转发到本地的Web服务?

这里我想到了三个解决方法:

  1. 硬头皮直接将项目部署到云服务器。(下下下策)
  2. 通过内网穿透技术,让公网IP映射到本地服务。
  3. 在服务器部署一个轻量的Python脚本(大的跑不动,小脚本还是能跑的),用于转发我们的Http请求,到时候我只需要在本机和服务器之间开一个端口映射即可。

第一种方案并不可行,项目采用SpringBoot进行开发,引入了一些中间件,部署很繁琐,就算使用docker-compose,每次都要重新打jar包和重新构建镜像也挺烦人的,本地改完代码即可运行它不香吗?

第二个策略——内网穿透,看着很香,但我没试过(于是踩了很多坑)

第三个策略:下下策,实现方案太不优雅了,相当于又引入一个服务。

内网穿透踩坑

研究来研究去,只剩下内网穿透这条路可以走了,那还多说什么,来试试吧,试试就逝世,在这个过程中我踩了挺多坑的,下面我将把这些坑总结一下,方便有和我一样需求的小伙伴少走些弯路。

PS:本文旨在让一些和我一样没用过内网穿透的小伙伴少走些弯路,大佬们看个乐子就好了。当然如果有更好的解决方案也可以教教小弟。

踩坑1:采用ngrok进行内网穿透,并采用其配置的免费域名或者随机生成的域名。

  • 为何采用此方案:免费,不用自己买服务器和域名。
  • 问题
    • 第一次访问需要携带特殊的请求头,否则是一个ngrok自己的页面。
    • 后面不知道为何只有本机能访问。
    • 国外服务器,速度极慢,华为云上连不通。
  • 后续:后面想通过在服务器和本地都跑一个ngrok,但看到ngrok的仓库上一次commit还是在八年前,emmm,想想还是算了。

踩坑2:采用CloudFlare的tunnel做内网穿透

  • 教程:参考各类博客,大致只需要将自己的域名的DNS改成Cloudflare那边的,在平台开通tunnel后在本地跑一个cloudflare的守护进程即可。
  • 问题
    • 华为云上一直报出URL格式错误,原因暂时未知,卡了很久。
    • 带宽太小,加上服务器在国外,华为云那边的连通性测试都无法通过,10次连通性测试大概只有1次能跑通

踩坑3:让用户直接请求我服务器的公网IP,服务器用Nginx反向代理到CloudFlare那边的域名

  • 为啥会这么做:正如我上面所说,因为刚开始我发现只用内网穿透的域名,在华为云那边配置的时候一直会出现callback url error,就是URL的格式错误,我很不能理解,以为加一层代理应该就没问题了。
  • 问题
    • 事实证明这样做也无法解决URL格式错误的问题。
    • 其本质上也没有解决带宽太小的问题。
    • 反向代理后,Host如果没经过特殊修改,还会报出Direct IP access not allowed的错误。脑瓜子更疼了。

最终方案:在云服务器和本地都跑一个frp,通过frp的HTTP代理就可以实现自定义域名访问内网的Web服务。
不过还是踩了个小坑:在配置frp的服务端的时候,要将http监听的端口放到bindPort之上,否则会报new proxy [iot-server] type [http] error: type [http] not supported when vhost http port is not set,原因暂时未知。

下文我将分享,使用frp实现的自定义域名访问内网服务的过程。

frp实践

什么是frp

frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。

frp 仓库的更新很活跃,截至写作本文,最近一次commit是在16hour前,这也是为什么我采用frp而不是ngrok。仓库地址:frp/README_zh.md at dev · fatedier/frp (github.com)

实战操作

下面给出用frp实现内网穿透的步骤。实话说官方文档写得很详细了,大家也可以看看官方文档通过自定义域名访问内网的 Web 服务 | frp (gofrp.org)(不过似乎在HTTP代理配置那里有个小坑)

下载

首先,打开frp官网,找到上面GitHub的Release地址,根据系统下载不同的版本。例如我本地下的是Windows amd64,服务器下的是Linux amd64,下载后将Linux版的传到服务器并解压。

frp 主要由两个组件组成:客户端(frpc) 和 服务端(frps)。通常情况下,服务端部署在具有公网 IP 地址的机器上,而客户端部署在需要穿透的内网服务所在的机器上。release下载的压缩包上同时含有frpc和frps,我们只需要使用其中一个即可(对于本地我们只需要frpc,对于服务器我们使用frps)

配置

根据具体需求,具体请看官网通过 SSH 访问内网机器 | frp (gofrp.org),下面我给出暴露本地服务的配置。

  • 客户端的配置文件是:frpc.toml,toml的格式要求很严格,按照官网的要求写。下面是我的配置:
serverAddr = "服务器公网IP"
serverPort = 7000

[[proxies]]
name = "iot-server"
type = "http"
localPort = 9090
customDomains = ["自定义域名,也可以直接用公网IP"]
  • 服务端的配置文件是:frps.toml,我的配置如下
vhostHTTPPort = 9090
bindPort = 7000

注意,这里有个坑,如果不把vhostHTTPPort写到bindPort前面,会报出下面的错误:new proxy [iot-server] type [http] error: type [http] not supported when vhost http port is not set

通过上述的配置,我就能通过访问公网ip:9090来访问我本地在9090端口跑的SpringBoot应用啦。

启动

  • 客户端:./frpc -c ./frpc.toml
  • 服务端:./frps -c ./frps.toml

最佳实践:使用systemd管理

在 Linux 系统下,使用 systemd 可以方便地控制 frps 服务端的启动、停止、配置后台运行以及开机自启动。

  • 安装systemd
yum install systemd
  • 创建frps.service文件
sudo vim /etc/systemd/system/frps.service
  • 写入内容
[Unit]
# 服务名称,可自定义
Description = frp server
After = network.target syslog.target
Wants = network.target

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

[Install]
WantedBy = multi-user.target
  • 使用systemd命令管理frps服务
# 启动frp
sudo systemctl start frps
# 停止frp
sudo systemctl stop frps
# 重启frp
sudo systemctl restart frps
# 查看frp状态
sudo systemctl status frps
  • 设置开机自启动
sudo systemctl enable frps

这样,我们就成功实现了通过自定义域名/公网IP来访问本地服务的需求。由于内网服务缺乏公网 IP 地址,因此无法直接被非局域网内的用户访问。用户通过访问服务端的 frps,frp 负责根据请求的端口或其他信息将请求路由到相应的内网机器,从而实现通信。

在这之后,我将URL添加到华为云的数据转发后,也不会报格式错误了,并且由于是国内服务器,连通性嘎嘎好。

总结

第一,对于今后有从华为云IoT平台将数据转发到第三方HTTP应用服务器需求的UU,考虑到开发和测试的方便,目前的最佳实践是:(前提是您的项目不需要部署上线,只作为内部测试/比赛演示)

  • 在各大云产商那里购买一个低配的服务器,从而拿到一个公网IP
  • 在本地和服务器上都跑一个frp
  • 利用frp配置 HTTP 类型的代理,让你可以通过自定义域名访问内网的 Web 服务

第二,多逛逛协会的BBS。
如果当初多认真研究卓哥发的《饥荒服务器frp》、《偷偷用frp使用内网GPU》,就肯定直接上手frp了,也不会有这么多踩坑过程了TAT

至于为何使用Cloudflare tunnel的域名,在华为云IoT会报出URL格式问题,目前还在研究…