背景
我其实经常把 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 only
RemotePort int `json:"remote_port"`
// http and https only
CustomDomains []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"`
// stcp
Sk string `json:"sk"`
// tcpmux
Multiplexer 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 kcp
return 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 多路复用的实现。