看看frp源码(一)

2020-04-16 | Tags: 源码 frp

frp介绍

frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp, udp 协议,为 http 和 https 应用协议提供了额外的能力,且尝试性支持了点对点穿透。

阅读目标

frp在github上非常热门,有多达30k+个star,这边我们尝试看一下frp的源码,主要目的看看frp对tcp和http进行反向代理的实现,顺便进一步熟悉tcp,http协议的一些实现细节。

frp启动

frp在服务端和客户端分为frps和frpc两个程序,我们这边以tcp代理ssh端口和代理http协议为例,在frps所在的外网服务器编写配置文件frps.ini

[common]
bind_port = 8886
vhost_http_port = 80

其中 * bind_port表示frps在本地进行监听通信的端口 * vhost_http_port表示frps在本地监听的外部http访问端口。

在frpc所在的内网服务器编写配置文件frpc.ini

[common]
server_addr = frps.ser.ver.ip
server_port = 8886

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 8888

[web]
type = http
local_port = 8080
custom_domains = www.domain.com

其中 * server_addr表示frps所在服务器的ip地址 * server_port表示frps.ini中的通信监听端口,要和frps.ini中的配置一致 * [ssh]中的配置表示将外部的tcp端口8888的请求发送到内网服务器本机127.0.0.1的22端口,实现了外网ssh服务的转发。 * [web]中的配置表示将外部对frps的vhost_http_port=80,域名为custom_domains的http请求发送到本地8080端口,实现外网http服务转发。

对于tcp应用,这里我们使用ssh -p 8888 frps.ser.ver.ip 就可以访问到内网服务器的ssh服务

相应的http应用,我们访问www.domain.com:80就可以将http请求转发到内网服务器的8080端口,实现http服务的内网穿透

入口源码解析

首先先看一下frps服务的具体实现

首先找到frps的入口函数在cmd/frps/main.go中

追查 Execute()函数看到具体的实现在cmd/frps/root.go中

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

这里rootCmd是由cobra构建的,cobra是一个命令行构建工具,可以方便的构建多级多命令的命令行程序,docker,k8s等都使用了cobra,使用起来十分强大。

var rootCmd = &cobra.Command{
    Use:   "frps",
    Short: "frps is the server of frp (https://github.com/fatedier/frp)",
    RunE: func(cmd *cobra.Command, args []string) error {
        // RunE run with error return
        if showVersion {
            fmt.Println(version.Full())
            return nil
        }

        // use config to parse ini args
        // 2 method FromFile, FromCmd
        var cfg config.ServerCommonConf
        var err error
        if cfgFile != "" {
            var content string
            content, err = config.GetRenderedConfFromFile(cfgFile)
            if err != nil {
                return err
            }
            cfg, err = parseServerCommonCfg(CfgFileTypeIni, content)
        } else {
            cfg, err = parseServerCommonCfg(CfgFileTypeCmd, "")
        }
        if err != nil {
            return err
        }

        // run!
        err = runServer(cfg)
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        return nil
    },
}

这里RunE对应正常cobra文档中的Run,运行时会额外返回一个error,此外没有区别。

frps使用cobra构建了多个可以输入的参数,这里可以看到通过两种方式对参数进行解析

  • 当指定参数文件时,打开文件并进行cfg, err = parseServerCommonCfg(CfgFileTypeIni, content)解析
  • 未指定参数文件时,将命令行输入的参数解析入cfg中cfg, err = parseServerCommonCfg(CfgFileTypeCmd, "")

最后使用cfg启动服务err = runServer(cfg)正式启动frps服务