Frp源码分析

1,361 阅读7分钟

Frp源码分析

环境搭建

frp是一个好用的内网穿透工具 github.com/fatedier/fr… ,支持多种协议的转发,具体参考文档 gofrp.org/docs/

frp总共分为两个角色:

  1. frps: 服务端,部署在一个公网服务器上,负责将用户的请求转发给用户的内网服务上。
  2. 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总体流程如图所示。

tu.png

当用户启动一个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。

p1.png

==== 未完待续 =====