frp 介绍

frp 是一套通用的,基于 C/S 架构的内网穿透软件。软件分为两个部分:client (frpc) 和 server (frps)。

该软件的基本使用流程:

  • 部署:首先需要将 frps 部署到具有公网 IP 的主机上(远端主机);
  • 暴露端口:然后在需要暴露端口内网的设备上(本地主机),运行 frpc,并通过配置文件指定指定:
    • 要暴露的本地主机的本地端口;
    • 要暴露端口映射到的远端主机的远端端口;
  • 访问端口:在任意一台设备,通过远端主机公网 IP 和 暴露到远端端口即可访问到本地主机上的本地端口。

更详细的使用教程参见:官方文档

本文介绍 frp 版本为:v0.44.0

frp 架构图

image

(图片来源 github

该图主要描述的是访问端口的流量情况,用户流量经过部署在具有公网 ip 的主机上的 frps 中转,发送到位于任意内网主机上的 frpc,frpc 再将流量转发到内网主机上的端口。

下文,从 frps 的源码,本部分仅介绍暴露和访问 tcp 端口的流程,忽略鉴权和插件等细节。

frps 启动流程

  • cmd/frps/main.go:27: frps main 函数,最终会调用 rootCmd.RunE
  • rootCmd.RunE:
    • 首先,读取从命令行和配置文件读取配置。
    • 最终,调用 svr.Run
  • svr.Run: 调用 svr.HandleListener,接收来自客户端的请求,支持多种协议:kcp、websocket、TCP(tls)、TCP,下文主要介绍的是基于 TCP 协议。
  • svr.HandleListener: Accept 等待客户端的连接。具体连接处理流程参见下文。

frps 连接处理

该小结主要介绍 frpc 和 frps 之间进行端口暴露的流程。

tcp_mux

当 frpc 启动后,在需要暴露端口时,会建立和 frps 建立一个连接。此时 svr.HandleListener Accept 会返回 net.Conn,并会启动一个协程来处理请求,处理流程分为两种情况:

  • common.tcp_mux 配置如果为 true (默认),该 net.Conn 将会通过 yamux 进行建立一个 Server 侧的 yamux.Session,并等待客户端的在该 Session 建立逻辑连接,yamux.Session 获取到 net.Conn 后,会启动一个协程调用 svr.handleConnection 进行处理。
  • common.tcp_mux 配置如果为 false,该 net.Conn 会直接调用 svr.handleConnection 进行处理。

说明:这里很关键,如果 tcp_mux 位 true, frpc 和 frps 间每个暴露的端口,只建立一个 tcp 连接,访问该端口的所有的逻辑连接的流量都在该物理连接上利用 yamux 多路复用起在该 tcp 连接上进行传输。也就是说,在操作系统层面,只能看到一个连接 TCP 连接。否则,每个请求 frpc 和 frps 之间会都会建立一个 tcp 连接。

连接类型

svr.handleConnection 可以看出,每个链接建立后,客户端会发送一个握手消息,这个消息标识了,frpc 和 frps 间的三种连接类型:

  • msg.Login 控制连接
  • msg.NewWorkConn 工作连接
  • msg.NewVisitorConn 访问连接

其中,访问连接应该是为了实现端到端加密场景使用的(stcp、sudp),在此不多深究了。

控制连接

针对 msg.Login 控制连接,会进入 svr.RegisterControl 进行处理,主要流程为:

  • 进行权限校验
  • 构造并启动一个控制器 ctl.Start
    • 回复 frpc 登录成功。
    • 通过该控制连接给 frpc 发送命令,让 frpc 建立多个工作连接(即工作连接池)。
    • 启动 ctl.manager 协程,该协程会读取 frpc 通过该控制连接发送给 frps 的一些消息,并处理,细节参见下文。
    • 启动 ctl.reader 协程,读取控制连接发送的原始数据流,解析成控制命令,并通过 chan 交由 ctl.manager 处理。
    • 启动 ctl.stoper 协程,等待关闭信号,用于关闭并清理资源。

在此,重点介绍 ctl.manager ,即控制器的核心逻辑,主要处理三种类型消息:

  • msg.NewProxy 新建一个 Proxy,frpc 在接收到登录成功,会根据暴露端口的配置,告知 frps 创建一个指定类型的 proxy,逻辑位于 ctl.RegisterProxy,参见下文。
  • msg.CloseProxy 关闭一个 Proxy。
  • msg.Ping 心跳消息。

建立 proxy 的 ctl.RegisterProxy 主要流程如下:

  • proxy.NewProxy 初始化一个 Proxy 对象,以 TCP 为例,将构造一个 proxy.TCPProxy 对象(其中核心参数为 ctl.GetWorkConn,后文有介绍)。
  • 然后调用 pxy.Run() ,以 proxy.TCPProxy 为例:会在 frps 所在主机上监听一个 TCP 端口,并启动一个协程,该协程会 Accept 连接到该端口的 TCP 连接。这个端口就是提供给用户访问的端口,处理逻辑参见:访问 frps 暴露的端口.

工作连接

针对 msg.NewWorkConn 控制连接,会进入 svr.RegisterWorkConn 进行处理,主要流程为:

访问 frps 暴露的端口

上文 连接处理 介绍了 frpc 和 frps 之间进行暴露端口的流程。本部分将介绍,端口暴露到 frps 的主机后,用户访问该端口的流程(以 TCP 为例)。

如上文提到,这个端口的处理函数函数位于: pxy.startListenHandler 当用户建立连接后,会调用:HandleUserTCPConnection 进行处理:

frpc 创建好工作连接后,会调用 HandleTCPWorkConnection 函数,最终使用 net.Dial 打开一个访问 127.0.0.1 对应本地端口的本地连接。并调用 frpIo.Join 进行工作连接和该本地连接的进行相互拷贝。

至此流量就进入了本地的服务中了。

总结

image

上图介绍了一个 tcp 端口暴露到公网以及访问的主要流程,需要注意的是:

  • 忽略鉴权和插件相关细节。
  • 假设没有启用连接池的场景。
  • 2.3 多条工作连接强调的是存在并发访问时,会创建多条工作连接,但:
    • 每个 2.2 请求建立工作连接 只会建立一个连接。
    • 一个请求只会用到一个工作连接。
  • 如果开启了 tcp_mux(默认),上图控制连接和工作连接都是跑在 yamux 封装的一条 TCP 连接。

其他说明