背景
我其实经常把 frp 当成一个网络项目,或者说是以网络为底层,在周边建立生态的一个项目。既然是一个网络项目,那就必然涉及网络的方方面面,比如通信协议。 因为是 C-S 架构,那比如有 C-S 的通信协议;又因为底层依赖网络,那比如涉及到很多 TCP 和 UDP 等协议。
本文希望从协议两个字开始说起,说说 frp 的自定义协议以及底层依赖的网络协议内容,最后会简单讲下多路复用。
自定义协议
协议总览
TypeLogin: Login{},TypeLoginResp: LoginResp{},TypeNewProxy: NewProxy{},TypeNewProxyResp: NewProxyResp{},TypeCloseProxy: CloseProxy{},TypeNewWorkConn: NewWorkConn{},TypeReqWorkConn: ReqWorkConn{},TypeStartWorkConn: StartWorkConn{},TypeNewVisitorConn: NewVisitorConn{},TypeNewVisitorConnResp: NewVisitorConnResp{},TypePing: Ping{},TypePong: Pong{},TypeUDPPacket: UDPPacket{},TypeNatHoleVisitor: NatHoleVisitor{},TypeNatHoleClient: NatHoleClient{},TypeNatHoleResp: NatHoleResp{},TypeNatHoleClientDetectOK: NatHoleClientDetectOK{},TypeNatHoleSid: NatHoleSid{},
可以看到,frp 自定义了很多协议,大致可以分类为登录,控制面 Proxy 操作协议,数据面 work 协议,visitor 协议,ping-pong 协议,udp 报文协议,p2p 打洞协议。
接下来,我们一个一个讲。
登录
type Login struct {Version string `json:"version"`Hostname string `json:"hostname"`Os string `json:"os"`Arch string `json:"arch"`User string `json:"user"`PrivilegeKey string `json:"privilege_key"`Timestamp int64 `json:"timestamp"`RunID string `json:"run_id"`Metas map[string]string `json:"metas"`// Some global configures.PoolCount int `json:"pool_count"`}type LoginResp struct {Version string `json:"version"`RunID string `json:"run_id"`ServerUDPPort int `json:"server_udp_port"`Error string `json:"error"`}
登录协议是用在 frpc 连接 frps 的时候,frpc 发送的数据,包含的数据从命名基本上可以确定出来。
proxy 控制流
// When frpc login success, send this message to frps for running a new proxy.type NewProxy struct {ProxyName string `json:"proxy_name"`ProxyType string `json:"proxy_type"`UseEncryption bool `json:"use_encryption"`UseCompression bool `json:"use_compression"`Group string `json:"group"`GroupKey string `json:"group_key"`Metas map[string]string `json:"metas"`// tcp and udp onlyRemotePort int `json:"remote_port"`// http and https onlyCustomDomains []string `json:"custom_domains"`SubDomain string `json:"subdomain"`Locations []string `json:"locations"`HTTPUser string `json:"http_user"`HTTPPwd string `json:"http_pwd"`HostHeaderRewrite string `json:"host_header_rewrite"`Headers map[string]string `json:"headers"`// stcpSk string `json:"sk"`// tcpmuxMultiplexer string `json:"multiplexer"`}type NewProxyResp struct {ProxyName string `json:"proxy_name"`RemoteAddr string `json:"remote_addr"`Error string `json:"error"`}type CloseProxy struct {ProxyName string `json:"proxy_name"`}
当 frpc 里配置了新的 proxy 类型,frpc 就会发送 NewProxy 的消息给 frps,frps 就会创建一个 proxy 作为代理,每个 proxy 都有一个唯一确定的 proxyname。
proxy 数据流
type NewWorkConn struct {RunID string `json:"run_id"`PrivilegeKey string `json:"privilege_key"`Timestamp int64 `json:"timestamp"`}type ReqWorkConn struct {}type StartWorkConn struct {ProxyName string `json:"proxy_name"`SrcAddr string `json:"src_addr"`DstAddr string `json:"dst_addr"`SrcPort uint16 `json:"src_port"`DstPort uint16 `json:"dst_port"`Error string `json:"error"`}
假设没有开启多路复用,来了一个数据连接,frps 需要告诉 frpc 发起一个 work 的连接,然后 frps 会用这个新创建的连接来服务用户数据。 开启了多路复用其实类似,只是连接是虚拟的了。
心跳
type Ping struct {PrivilegeKey string `json:"privilege_key"`Timestamp int64 `json:"timestamp"`}type Pong struct {Error string `json:"error"`}
frpc 与 frps 保持心跳协议。这里都是frpc 主动发起心跳 ping,frps 返回 pong。
UDP报文协议
type UDPPacket struct {Content string `json:"c"`LocalAddr *net.UDPAddr `json:"l"`RemoteAddr *net.UDPAddr `json:"r"`}
因为 frp 可以代理 udp 数据,这里 udp 是被封装为一个个消息然后通过 tcp 传输的。
其他几个协议,本人以为可以忽略,感兴趣的可以仔细看看。
底层协议(物理连接)

frpc 与 frps 最底层的物理连接,只可能是 TCP 和 UDP,其中 TCP 里有裸的 TCP 和 websocket,UDP 里有 KCP。 从源码里可以看出来。
func ConnectServerByProxy(proxyURL string, protocol string, addr string) (c net.Conn, err error) {switch protocol {case "tcp":return gnet.DialTcpByProxy(proxyURL, addr)case "kcp":// http proxy is not supported for kcpreturn ConnectServer(protocol, addr)case "websocket":return ConnectWebsocketServer(addr)default:return nil, fmt.Errorf("unsupport protocol: %s", protocol)}}
TLS 协议是在底层协议基础之上的。
var (FRPTLSHeadByte = 0x17)func WrapTLSClientConn(c net.Conn, tlsConfig *tls.Config, disableCustomTLSHeadByte bool) (out net.Conn) {if !disableCustomTLSHeadByte {c.Write([]byte{byte(FRPTLSHeadByte)})}out = tls.Client(c, tlsConfig)return}
多路复用(虚拟连接)
tcp多路复用

如果开启了多路复用,frpc 和 frps 之间只会有一个连接。所有的控制流和数据流都是用的这个物理连接。具体可以参考 https://github.com/hashicorp/yamux 。
总结
本文讲了 frp 的自定义协议,里面包含了各种控制流相关的 message。第二部分详细写了 frp 里的各个底层协议之间的关系,第三部分讲了虚拟连接 tcp 多路复用的实现。
