Frp源码分析
环境搭建
frp是一个好用的内网穿透工具 github.com/fatedier/fr… ,支持多种协议的转发,具体参考文档 gofrp.org/docs/ 。
frp总共分为两个角色:
- frps: 服务端,部署在一个公网服务器上,负责将用户的请求转发给用户的内网服务上。
- frpc:客户端,和用户的内网服务放在一个网络中,负责将用户请求从frps转发给用户的内网服务中,同时将内网服务的响应转发给frps,委托它转发给用户。
搞一个公网服务器,安装build-essential和go语言,克隆项目源码,make编译,创建frpc.ini的配置文件,并使用如下配置。 bind_port表示的端口是暴露给frpc进行使用,vhost_http_port端口暴露给用户,用户的请求发往该端口,frps转发给对应的内网服务。
[common]
bind_port = 17000
vhost_http_port=9090
使用 fprs -c frpc.ini
命令运行服务端。
我们实现一个简单的webserver,放在自己电脑上,作为我们的内网服务。
package main
import (
"fmt"
"net/http"
"strings"
"log"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // 解析参数,默认是不会解析的
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息
fmt.Println("path", r.URL.Path)
fmt.Println("scheme", r.URL.Scheme)
fmt.Println(r.Form["url_long"])
for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}
fmt.Fprintf(w, "Hello astaxie!") // 这个写入到 w 的是输出到客户端的
}
func main() {
http.HandleFunc("/", sayhelloName) // 设置访问的路由
err := http.ListenAndServe(":9090", nil) // 设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
写一份frpc配置文件,local_ip,local_port为内网服务的地址和端口, 这里必须指定一个域名,如果买了域名解析服务就写域名,也可以写你的公网ip,之后启动frpc和咱们的web服务,就能看到效果了
[common]
server_addr = 公网ip
server_port = 17000
log_level=trace
[web]
type = http
local_ip = 127.0.0.1
local_port = 9090
custom_domains = 公网ip / 域名
工作流程
Frp总体流程如图所示。
当用户启动一个frpc时,frpc会向frps注册,主要是让frps保存一些frpc的一些配置信息,检查配置的有效性,开一个server和frpc通信。
以http内网穿透为例子,frps根据指定的vhost地址开启一个反向代理服务,反向代理服务接收用户请求,向frpc发送开始写命令,并将请求发送给frpc,frpc再将实际的内容转发给内网服务。
项目目录解析
一个软件的目录划分同样属于这个软件的架构部分,frp目录划分分为如下
├── client
│ ├── admin.go
│ ├── admin_api.go
│ ├── control.go
│ ├── event
│ ├── health
│ ├── proxy
│ ├── service.go
│ └── visitor
├── cmd
│ ├── frpc
│ └── frps
├── pkg
│ ├── auth
│ ├── config
│ ├── consts
│ ├── errors
│ ├── metrics
│ ├── msg
│ ├── nathole
│ ├── plugin
│ ├── proto
│ ├── transport
│ └── util
├── server
│ ├── control.go
│ ├── controller
│ ├── dashboard.go
│ ├── dashboard_api.go
│ ├── group
│ ├── metrics
│ ├── ports
│ ├── proxy
│ ├── service.go
│ └── visitor
cmd目录是项目的入口,分别对应frpc和frps的入口,client、server目录包含了frpc,frps的实现逻辑,pkg中定义了frp的公共模块,包括常量、工具类、错误码。plugin中定义了client、server内网穿透插件。
frps源码分析
frps的逻辑主要由service类实现,
github.com/fatedier/fr… 的runServer是frps的主要入口,基本就是new了一个service,然后run
func runServer(cfg config.ServerCommonConf) (err error) {
..... 解析配置文件,初始化日志.....
svr, err := server.NewService(cfg)
if err != nil {
return err
}
log.Info("frps started successfully")
svr.Run()
return
}
newService的逻辑基本上又臭又长,我们只关注创建接入点和内网穿透http,这个接入点是用来给frpc接入进来的,所以会创建一个tcpserver,保存接入点为service的listener字段。
func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
svr = &Service{....}
address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort))
ln, err := net.Listen("tcp", address)
if err != nil {
err = fmt.Errorf("create server listener error, %v", err)
return
}
svr.muxer = mux.NewMux(ln)
go func() {
_ = svr.muxer.Serve()
}()
ln = svr.muxer.DefaultListener()
svr.listener = ln
var (
httpMuxOn bool
httpsMuxOn bool
)
if cfg.BindAddr == cfg.ProxyBindAddr {
if cfg.BindPort == cfg.VhostHTTPPort {
httpMuxOn = true
}
if cfg.BindPort == cfg.VhostHTTPSPort {
httpsMuxOn = true
}
}
// Listen for accepting connections from client.
address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort))
ln, err := net.Listen("tcp", address)
if err != nil {
err = fmt.Errorf("create server listener error, %v", err)
return
}
svr.muxer = mux.NewMux(ln)
svr.muxer.SetKeepAlive(time.Duration(cfg.TCPKeepAlive) * time.Second)
go func() {
_ = svr.muxer.Serve()
}()
ln = svr.muxer.DefaultListener()
svr.listener = ln
log.Info("frps tcp listen on %s", address)
//因为咱们配置了vhosthttpport所以这里会创建一个反向代理服务器
// Create http vhost muxer.
if cfg.VhostHTTPPort > 0 {
rp := vhost.NewHTTPReverseProxy(vhost.HTTPReverseProxyOptions{
ResponseHeaderTimeoutS: cfg.VhostHTTPTimeout,
}, svr.httpVhostRouter)
svr.rc.HTTPReverseProxy = rp
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort))
server := &http.Server{
Addr: address,
Handler: rp,
}
var l net.Listener
if httpMuxOn {
l = svr.muxer.ListenHttp(1)
} else {
l, err = net.Listen("tcp", address)
if err != nil {
err = fmt.Errorf("create vhost http listener error, %v", err)
return
}
}
go func() {
_ = server.Serve(l)
}()
log.Info("http service listen on %s", address)
}
..kcp、webseocket、nat打洞等其他的功能,咱们这里不关注
return
}
接着就是service的run方法,我们这里就关注最后一个,这里实现了frpc的接入处理逻辑。
func (svr *Service) Run() {
....
svr.HandleListener(svr.listener)
}
这个方法的框架就是accept一个连接,开一个协程去处理这个连接, 对于每一个接入的frpc,都会创建一个session和frpc进行通信。
func (svr *Service) HandleListener(l net.Listener) {
// Listen for incoming connections from client.
for {
c, err := l.Accept()
if err != nil {
log.Warn("Listener for incoming connections from client closed")
return
}
....
// Start a new goroutine to handle connection.
go func(ctx context.Context, frpConn net.Conn) {
if svr.cfg.TCPMux {
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
session, err := fmux.Server(frpConn, fmuxCfg)
if err != nil {
log.Warn("Failed to create mux connection: %v", err)
frpConn.Close()
return
}
for {
stream, err := session.AcceptStream()
if err != nil {
log.Debug("Accept new mux stream error: %v", err)
session.Close()
return
}
go svr.handleConnection(ctx, stream)
}
} else {
svr.handleConnection(ctx, frpConn)
}
}(ctx, c)
}
}
它这里的注释说这个session是一个只允许单向通信的一个连接,我们只需要看他怎么处理这个连接的就行,这里也是一个老套路,开协程去处理。
// AcceptStream is used to block until the next available stream
// is ready to be accepted.
func (s *Session) AcceptStream() (*Stream, error) {
select {
case stream := <-s.acceptCh:
if err := stream.sendWindowUpdate(); err != nil {
return nil, err
}
return stream, nil
case <-s.shutdownCh:
return nil, s.shutdownErr
}
}
这里的处理逻辑非常清晰,就是从连接里读一个消息,根据消息的类型去处理,这里主要关注login消息和NewWorkConn消息。
func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
var (
rawMsg msg.Message
err error
)
if rawMsg, err = msg.ReadMsg(conn); err != nil {
log.Trace("Failed to read message: %v", err)
conn.Close()
return
}
switch m := rawMsg.(type) {
case *msg.Login:
// server plugin hook
content := &plugin.LoginContent{
Login: *m,
ClientAddress: conn.RemoteAddr().String(),
}
retContent, err := svr.pluginManager.Login(content)
if err == nil {
m = &retContent.Login
err = svr.RegisterControl(conn, m)
}
// If login failed, send error message there.
// Otherwise send success message in control's work goroutine.
if err != nil {
xl.Warn("register control error: %v", err)
_ = msg.WriteMsg(conn, &msg.LoginResp{
Version: version.Full(),
Error: util.GenerateResponseErrorString("register control error", err, svr.cfg.DetailedErrorsToClient),
})
conn.Close()
}
case *msg.NewWorkConn:
if err := svr.RegisterWorkConn(conn, m); err != nil {
conn.Close()
}
case *msg.NewVisitorConn:
.....
default:
log.Warn("Error message type for the new connection [%s]", conn.RemoteAddr().String())
conn.Close()
}
}
登录消息主要是分配一个runId来标识客户端的身份,并为客户端创建一个controller进行业务处理。因为这里我们没有配插件,所以插件的登录逻辑是用不到的。
注册controller也十分清晰,也是老套路,new然后start,再配监听超时和对端关闭context,异步地处理超时和对端关闭。
func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err error) {
// If client's RunID is empty, it's a new client, we just create a new controller.
// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.
if loginMsg.RunID == "" {
...
}
ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.authVerifier, ctlConn, loginMsg, svr.cfg)
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
oldCtl.allShutdown.WaitDone()
}
ctl.Start()
// for statistics
metrics.Server.NewClient()
go func() {
// block until control closed
ctl.WaitClosed()
svr.ctlManager.Del(loginMsg.RunID, ctl)
}()
return
}
controller的启动就是恢复登录请求,然后异步地读、写对应frpc发过来的消息,对消息做分发,这里主要关注消息分发逻辑manager()
// Start send a login success message to client and start working.
func (ctl *Control) Start() {
loginRespMsg := &msg.LoginResp{
Version: version.Full(),
RunID: ctl.runID,
Error: "",
}
_ = msg.WriteMsg(ctl.conn, loginRespMsg)
go ctl.writer()
for i := 0; i < ctl.poolCount; i++ {
ctl.sendCh <- &msg.ReqWorkConn{}
}
go ctl.manager()
go ctl.reader()
go ctl.stoper()
}
frps和frpc通过心跳机制确认对方存活,这里首先处理的就是心跳包消息,处理消息是从每一个channnel里面读取到,并进行处理。这些消息是被ctl.reader()这个协程给放到channel里的。
ctl.reader()根据收到消息的类型将其放到该类型消息对应的channel里面通知ctl.manager()协程的事件循环进行处理。
当一个新的frpc接入frps时,会发送登录消息,frps会为frpc注册一个代理,如ctl.RegisterProxy
方法。
server/proxy包定义了处理各种协议的代理,这些代理实现了 github.com/fatedier/fr… 中定义的Proxy接口,由proxyManager统一进行管理。
func (ctl *Control) manager() {
for {
select {
case <-heartbeatCh:
if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout")
return
}
case rawMsg, ok := <-ctl.readCh:
if !ok {
return
}
switch m := rawMsg.(type) {
case *msg.NewProxy:
content := &plugin.NewProxyContent{
User: plugin.UserInfo{
User: ctl.loginMsg.User,
Metas: ctl.loginMsg.Metas,
RunID: ctl.loginMsg.RunID,
},
NewProxy: *m,
}
var remoteAddr string
retContent, err := ctl.pluginManager.NewProxy(content)
if err == nil {
m = &retContent.NewProxy
remoteAddr, err = ctl.RegisterProxy(m)
}
// register proxy in this control
resp := &msg.NewProxyResp{
ProxyName: m.ProxyName,
}
if err != nil {
xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err)
resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), err, ctl.serverCfg.DetailedErrorsToClient)
} else {
resp.RemoteAddr = remoteAddr
xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType)
metrics.Server.NewProxy(m.ProxyName, m.ProxyType)
}
ctl.sendCh <- resp
..... 其他类型忽略 .......
}
}
}
}
在ctl的RegisterProxy方法中,主要从frpc消息的payload中获取到代理的配置,并根据配置创建对应的代理,这里咱们frpc.ini配置的是http代理,所以创建的是HTTPProxy这个类,启动流程同样是new一个proxy出来然后run,实际逻辑实现在pxy.Run()中
func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
var pxyConf config.ProxyConf
// Load configures from NewProxy message and validate.
pxyConf, err = config.NewProxyConfFromMsg(pxyMsg, ctl.serverCfg)
if err != nil {
return
}
// User info
userInfo := plugin.UserInfo{
User: ctl.loginMsg.User,
Metas: ctl.loginMsg.Metas,
RunID: ctl.runID,
}
// NewProxy will return an interface Proxy.
// In fact, it creates different proxies based on the proxy type. We just call run() here.
pxy, err := proxy.NewProxy(ctl.ctx, userInfo, ctl.rc, ctl.poolCount, ctl.GetWorkConn, pxyConf, ctl.serverCfg, ctl.loginMsg)
if err != nil {
return remoteAddr, err
}
// Check ports used number in each client
if ctl.serverCfg.MaxPortsPerClient > 0 {
ctl.mu.Lock()
if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.serverCfg.MaxPortsPerClient) {
ctl.mu.Unlock()
err = fmt.Errorf("exceed the max_ports_per_client")
return
}
ctl.portsUsedNum += pxy.GetUsedPortsNum()
ctl.mu.Unlock()
defer func() {
if err != nil {
ctl.mu.Lock()
ctl.portsUsedNum -= pxy.GetUsedPortsNum()
ctl.mu.Unlock()
}
}()
}
if ctl.pxyManager.Exist(pxyMsg.ProxyName) {
err = fmt.Errorf("proxy [%s] already exists", pxyMsg.ProxyName)
return
}
remoteAddr, err = pxy.Run()
if err != nil {
return
}
defer func() {
if err != nil {
pxy.Close()
}
}()
err = ctl.pxyManager.Add(pxyMsg.ProxyName, pxy)
if err != nil {
return
}
ctl.mu.Lock()
ctl.proxies[pxy.GetName()] = pxy
ctl.mu.Unlock()
return
}
这个HTTPProxy的Run方法也是又臭又长,不过大部分代码都是解析配置信息,还有一些错误处理。因为咱们在frpc.ini里配置了反向代理域名,这里实际上就是遍历你配置的域名,将这些域名拼接成路由注册咱们之前提到的反向代理服务器中,这里注册的路由表实体实际上是一个RouteConfig
func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
routeConfig := vhost.RouteConfig{
...
}
...
for _, domain := range pxy.cfg.CustomDomains {
if domain == "" {
continue
}
routeConfig.Domain = domain
for _, location := range locations {
routeConfig.Location = location
tmpRouteConfig := routeConfig
// handle group
if pxy.cfg.Group != "" {
....
} else {
// no group
err = pxy.rc.HTTPReverseProxy.Register(routeConfig)
if err != nil {
return
}
pxy.closeFuncs = append(pxy.closeFuncs, func() {
pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig)
})
}
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPPort))
xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
}
}
remoteAddr = strings.Join(addrs, ",")
return
}
这里又回到前面提到的frps的NewService创建实例的这个初始化方法中,里面通过vhost.NewHTTPReverseProxy来配置咱们的反向代理服务器。
代理服务器是用的go自带的ReverseProxy,Director是装饰器,用来改写咱们的一个请求。因为反向代理要把请求转发给一个目标webserver,在转发时需要和目标webserver建立连接才能进行转发,这里需要配置一个DialContext来说明如何和目标webserver建立tcp连接。
func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *HTTPReverseProxy {
...
proxy := &httputil.ReverseProxy{
// Modify incoming requests by route policies.
Director: func(req *http.Request) {
....
},
// Create a connection to one proxy routed by route policy.
Transport: &http.Transport{
ResponseHeaderTimeout: rp.responseHeaderTimeout,
IdleConnTimeout: 60 * time.Second,
MaxIdleConnsPerHost: 5,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return rp.CreateConnection(ctx.Value(RouteInfoKey).(*RequestRouteInfo), true)
},
Proxy: func(req *http.Request) (*url.URL, error) {
...
},
},
},
}
rp.proxy = proxy
return rp
}
由于之前我们注册了路由表,这里取到我们之前注册的RouteConfig, 从中取出CreateConnFn算子进行实际的执行。这里的CreateConnFn实际上执行的是HTTPProxy的GetRealConn方法。这里确实有点绕,因为这里作者定义了一大堆的抽象层,只有实际调试才能看出来。
func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) {
host, _ := util.CanonicalHost(reqRouteInfo.Host)
vr, ok := rp.getVhost(host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
if ok {
if byEndpoint {
fn := vr.payload.(*RouteConfig).CreateConnByEndpointFn
if fn != nil {
return fn(reqRouteInfo.Endpoint, reqRouteInfo.RemoteAddr)
}
}
fn := vr.payload.(*RouteConfig).CreateConnFn
if fn != nil {
return fn(reqRouteInfo.RemoteAddr)
}
}
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
}
咱们剥去层层外壳,最后发现获取连接的实际执行函数是GetWorkConnFromPool,因为HTTPProxy和BaseProxy有一个继承的关系,所以可以调用父类的方法。这里最主要的流程就是向frpc发送一个StartWorkConn消息,然后将frpc的连接返回给咱们的反向代理服务器,反向代理服务器再把请求转发给frpc,这条流程终于走完了。
type HTTPProxy struct {
*BaseProxy
cfg *config.HTTPProxyConf
closeFuncs []func()
}
func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err error) {
...
tmpConn, errRet := pxy.GetWorkConnFromPool(rAddr, nil)
....
return
}
func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, err error) {
xl := xlog.FromContextSafe(pxy.ctx)
// try all connections from the pool
for i := 0; i < pxy.poolCount+1; i++ {
if workConn, err = pxy.getWorkConnFn(); err != nil {
xl.Warn("failed to get work connection: %v", err)
return
}
xl.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String())
xl.Spawn().AppendPrefix(pxy.GetName())
workConn = utilnet.NewContextConn(pxy.ctx, workConn)
var (
srcAddr string
dstAddr string
srcPortStr string
dstPortStr string
srcPort int
dstPort int
)
if src != nil {
srcAddr, srcPortStr, _ = net.SplitHostPort(src.String())
srcPort, _ = strconv.Atoi(srcPortStr)
}
if dst != nil {
dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String())
dstPort, _ = strconv.Atoi(dstPortStr)
}
err := msg.WriteMsg(workConn, &msg.StartWorkConn{
ProxyName: pxy.GetName(),
SrcAddr: srcAddr,
SrcPort: uint16(srcPort),
DstAddr: dstAddr,
DstPort: uint16(dstPort),
Error: "",
})
if err != nil {
xl.Warn("failed to send message to work connection from pool: %v, times: %d", err, i)
workConn.Close()
} else {
break
}
}
if err != nil {
xl.Error("try to get work connection failed in the end")
return
}
return
}
frp为了支持多种协议,多种功能,封装了大量的抽象层,如果只是实现一个http的内网穿透,可以做得更简单,如图所示。frpc登录时会分配一个id,并将id和fd得映射保存于map中,反向代理服务接收请求时,会尝试从map中获取fd,然后将数据发送给fd。
==== 未完待续 =====