背景

我其实经常把 frp 当成一个网络项目,或者说是以网络为底层,在周边建立生态的一个项目。既然是一个网络项目,那就必然涉及网络的方方面面,比如通信协议。 因为是 C-S 架构,那比如有 C-S 的通信协议;又因为底层依赖网络,那比如涉及到很多 TCP 和 UDP 等协议。

本文希望从协议两个字开始说起,说说 frp 的自定义协议以及底层依赖的网络协议内容,最后会简单讲下多路复用。

自定义协议

协议总览

  1. TypeLogin: Login{},
  2. TypeLoginResp: LoginResp{},
  3. TypeNewProxy: NewProxy{},
  4. TypeNewProxyResp: NewProxyResp{},
  5. TypeCloseProxy: CloseProxy{},
  6. TypeNewWorkConn: NewWorkConn{},
  7. TypeReqWorkConn: ReqWorkConn{},
  8. TypeStartWorkConn: StartWorkConn{},
  9. TypeNewVisitorConn: NewVisitorConn{},
  10. TypeNewVisitorConnResp: NewVisitorConnResp{},
  11. TypePing: Ping{},
  12. TypePong: Pong{},
  13. TypeUDPPacket: UDPPacket{},
  14. TypeNatHoleVisitor: NatHoleVisitor{},
  15. TypeNatHoleClient: NatHoleClient{},
  16. TypeNatHoleResp: NatHoleResp{},
  17. TypeNatHoleClientDetectOK: NatHoleClientDetectOK{},
  18. TypeNatHoleSid: NatHoleSid{},

可以看到,frp 自定义了很多协议,大致可以分类为登录,控制面 Proxy 操作协议,数据面 work 协议,visitor 协议,ping-pong 协议,udp 报文协议,p2p 打洞协议。

接下来,我们一个一个讲。

登录

  1. type Login struct {
  2. Version string `json:"version"`
  3. Hostname string `json:"hostname"`
  4. Os string `json:"os"`
  5. Arch string `json:"arch"`
  6. User string `json:"user"`
  7. PrivilegeKey string `json:"privilege_key"`
  8. Timestamp int64 `json:"timestamp"`
  9. RunID string `json:"run_id"`
  10. Metas map[string]string `json:"metas"`
  11. // Some global configures.
  12. PoolCount int `json:"pool_count"`
  13. }
  14. type LoginResp struct {
  15. Version string `json:"version"`
  16. RunID string `json:"run_id"`
  17. ServerUDPPort int `json:"server_udp_port"`
  18. Error string `json:"error"`
  19. }

登录协议是用在 frpc 连接 frps 的时候,frpc 发送的数据,包含的数据从命名基本上可以确定出来。

proxy 控制流

  1. // When frpc login success, send this message to frps for running a new proxy.
  2. type NewProxy struct {
  3. ProxyName string `json:"proxy_name"`
  4. ProxyType string `json:"proxy_type"`
  5. UseEncryption bool `json:"use_encryption"`
  6. UseCompression bool `json:"use_compression"`
  7. Group string `json:"group"`
  8. GroupKey string `json:"group_key"`
  9. Metas map[string]string `json:"metas"`
  10. // tcp and udp only
  11. RemotePort int `json:"remote_port"`
  12. // http and https only
  13. CustomDomains []string `json:"custom_domains"`
  14. SubDomain string `json:"subdomain"`
  15. Locations []string `json:"locations"`
  16. HTTPUser string `json:"http_user"`
  17. HTTPPwd string `json:"http_pwd"`
  18. HostHeaderRewrite string `json:"host_header_rewrite"`
  19. Headers map[string]string `json:"headers"`
  20. // stcp
  21. Sk string `json:"sk"`
  22. // tcpmux
  23. Multiplexer string `json:"multiplexer"`
  24. }
  25. type NewProxyResp struct {
  26. ProxyName string `json:"proxy_name"`
  27. RemoteAddr string `json:"remote_addr"`
  28. Error string `json:"error"`
  29. }
  30. type CloseProxy struct {
  31. ProxyName string `json:"proxy_name"`
  32. }

当 frpc 里配置了新的 proxy 类型,frpc 就会发送 NewProxy 的消息给 frps,frps 就会创建一个 proxy 作为代理,每个 proxy 都有一个唯一确定的 proxyname。

proxy 数据流

  1. type NewWorkConn struct {
  2. RunID string `json:"run_id"`
  3. PrivilegeKey string `json:"privilege_key"`
  4. Timestamp int64 `json:"timestamp"`
  5. }
  6. type ReqWorkConn struct {
  7. }
  8. type StartWorkConn struct {
  9. ProxyName string `json:"proxy_name"`
  10. SrcAddr string `json:"src_addr"`
  11. DstAddr string `json:"dst_addr"`
  12. SrcPort uint16 `json:"src_port"`
  13. DstPort uint16 `json:"dst_port"`
  14. Error string `json:"error"`
  15. }

假设没有开启多路复用,来了一个数据连接,frps 需要告诉 frpc 发起一个 work 的连接,然后 frps 会用这个新创建的连接来服务用户数据。 开启了多路复用其实类似,只是连接是虚拟的了。

心跳

  1. type Ping struct {
  2. PrivilegeKey string `json:"privilege_key"`
  3. Timestamp int64 `json:"timestamp"`
  4. }
  5. type Pong struct {
  6. Error string `json:"error"`
  7. }

frpc 与 frps 保持心跳协议。这里都是frpc 主动发起心跳 ping,frps 返回 pong。

UDP报文协议

  1. type UDPPacket struct {
  2. Content string `json:"c"`
  3. LocalAddr *net.UDPAddr `json:"l"`
  4. RemoteAddr *net.UDPAddr `json:"r"`
  5. }

因为 frp 可以代理 udp 数据,这里 udp 是被封装为一个个消息然后通过 tcp 传输的。

其他几个协议,本人以为可以忽略,感兴趣的可以仔细看看。

底层协议(物理连接)

image.png

frpc 与 frps 最底层的物理连接,只可能是 TCP 和 UDP,其中 TCP 里有裸的 TCP 和 websocket,UDP 里有 KCP。 从源码里可以看出来。

  1. func ConnectServerByProxy(proxyURL string, protocol string, addr string) (c net.Conn, err error) {
  2. switch protocol {
  3. case "tcp":
  4. return gnet.DialTcpByProxy(proxyURL, addr)
  5. case "kcp":
  6. // http proxy is not supported for kcp
  7. return ConnectServer(protocol, addr)
  8. case "websocket":
  9. return ConnectWebsocketServer(addr)
  10. default:
  11. return nil, fmt.Errorf("unsupport protocol: %s", protocol)
  12. }
  13. }

TLS 协议是在底层协议基础之上的。

  1. var (
  2. FRPTLSHeadByte = 0x17
  3. )
  4. func WrapTLSClientConn(c net.Conn, tlsConfig *tls.Config, disableCustomTLSHeadByte bool) (out net.Conn) {
  5. if !disableCustomTLSHeadByte {
  6. c.Write([]byte{byte(FRPTLSHeadByte)})
  7. }
  8. out = tls.Client(c, tlsConfig)
  9. return
  10. }

多路复用(虚拟连接)

tcp多路复用

image.png
如果开启了多路复用,frpc 和 frps 之间只会有一个连接。所有的控制流和数据流都是用的这个物理连接。具体可以参考 https://github.com/hashicorp/yamux

总结

本文讲了 frp 的自定义协议,里面包含了各种控制流相关的 message。第二部分详细写了 frp 里的各个底层协议之间的关系,第三部分讲了虚拟连接 tcp 多路复用的实现。