FRP源码阅读,学习FRP工作原理

admin 2025年6月8日14:07:51评论9 views字数 7461阅读24分52秒阅读模式

好久没阅读源码学习了,闲暇之余,找了下frp早期源码看了看并做下阅读记录。

简介

frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。

发展

FRP项目从github仓库来看,最早起源在2016年,作者发布了v 0.1.0版本,也是最早FRP的雏形,截止到现在,已经发布了104个release,当前版本为v 0.62.1.

FRP源码阅读,学习FRP工作原理

前言

FRP从最早的C/S命令行格式,发展到现在的web应用,其代码的架构已经发生了很大的变化,整体可以从作者的代码文件结构上看出很大的区别,那么本文主要还是通过作者早期写的代码来查看FRP的工作原理,是如何实现内网穿透。

代码分析

文件结构

从代码的大致文件结构可以看出,第一版的FRP代码是比较简约的,比较通俗易懂。

在src文件夹下,各个文件夹的功能分工明确,cmd主要是用于工具的入口,model模块则是客户端和服务端实现相应功能的具体实现,如客户端/服务端配置信息,请求和返回信息的结构等等。

utils则是工具类,如网络连接,日志处理,协议定义等等。

conf文件夹则是客户端或者是服务端需要加载的配置文件。

FRP源码阅读,学习FRP工作原理

阅读方式

整体代码主要是围绕工具的核心功能进行分析,如连接处理、加密处理、服务端和控制的连接处理进行分析,来了解其功能特点。

配置文件

服务器配置文件

# common是必须的section
[common]
bind_addr = 0.0.0.0
bind_port = 7000
log_file = ./frps.log
# debug, info, warn, error
log_level = debug
# file, console
log_way = console 

# test1即为name
[test1]
passwd = 123
bind_addr = 0.0.0.0
listen_port = 6000
  • [common]部分:这是公共配置,bind_addr表示服务器监听的地址,0.0.0.0表示监听所有地址;bind_port是服务器监听的端口,这里是7000;log_file指定日志文件的位置;log_level是日志的级别,debug表示输出详细的调试信息;log_way是日志输出的方式,console表示输出到控制台。
  • [test1]部分:这是一个代理服务的配置,passwd是客户端连接的密码,listen_port是服务器为这个代理服务监听的端口,这里是6000。

客户端配置文件

# common是必须的section
[common]
server_addr = 127.0.0.1
server_port = 7000
log_file = ./frpc.log
# debug, info, warn, error
log_level = debug
# file, console
log_way = console

# test1即为name
[test1]
passwd = 123
local_port = 22
  • [common]部分:server_addr是服务器的地址,这里是本地地址;server_port是服务器监听的端口,要和服务器配置中的bind_port一致;其他日志相关配置和服务器类似。
  • [test1]部分:passwd要和服务器配置中的密码一致,local_port是内网服务的端口,这里是22,通常是SSH服务的端口。

核心功能

连接处理(conn.go)

在conn.go文件中,作者定义了一个Listen的公共函数,负责监听和处理网络连接。

funcListen(bindAddr string, bindPort int64)(l *Listener, err error) {
    tcpAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", bindAddr, bindPort))
    listener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
return l, err
    }

    l = &Listener{
        addr:      listener.Addr(),
        l:         listener,
        conns:     make(chan *Conn),
        closeFlag: false,
    }

gofunc() {
for {
            conn, err := l.l.AcceptTCP()
if err != nil {
if l.closeFlag {
return
                }
continue
            }

            c := &Conn{
                TcpConn:   conn,
                closeFlag: false,
            }
            c.Reader = bufio.NewReader(c.TcpConn)
            l.conns <- c
        }
    }()
return l, err
}
  • 首先,Listen函数会将传入的地址和端口解析成TCP地址,然后开始监听这个地址和端口。
  • 接着,创建一个Listener对象,这里面有一个通道conns,用于存储新的连接。
  • 最后,启动一个goroutine(可以理解为一个轻量级的线程),不断接受新的连接,将其封装成Conn对象,然后发送到conns通道中。

加密处理(pcrypto.go)

自定义加密模块中,一共通过三次加密来保证数据的安全性(有点像现阶段FRP通信协议的雏形,就是少了一些字段验证)

func(pc *Pcrypto)Encrypto(src []byte)([]byte, error) {
// aes
    src = PKCS7Padding(src, aes.BlockSize)
    blockMode := cipher.NewCBCEncrypter(pc.paes, pc.pkey)
    crypted := make([]bytelen(src))
    blockMode.CryptBlocks(crypted, src)

// gzip
var zbuf bytes.Buffer
    zwr := gzip.NewWriter(&zbuf)
defer zwr.Close()
    zwr.Write(crypted)
    zwr.Flush()

// base64
return []byte(base64.StdEncoding.EncodeToString(zbuf.Bytes())), nil
}
  • 加密过程分为三步:
    • 第一步,对数据进行PKCS7填充,方便后续的加密操作。
    • 第二步,使用AES的CBC模式进行加密,将数据变成密文。
    • 第三步,使用gzip进行压缩,减少数据的体积,最后进行Base64编码,将二进制数据转换为可打印的文本。
funcLoadConf(confFile string)(err error) {
var tmpStr string
var ok bool

    conf, err := ini.LoadFile(confFile)
if err != nil {
return err
    }

// common
    tmpStr, ok = conf.Get("common""bind_addr")
if ok {
        BindAddr = tmpStr
    }

// 其他配置代码,整体也是使用get方法来获取

// servers
for name, section := range conf {
if name != "common" {
            proxyServer := &ProxyServer{}
            proxyServer.Name = name

            proxyServer.Passwd, ok = section["passwd"]
if !ok {
return fmt.Errorf("Parse ini file error: proxy [%s] no passwd found", proxyServer.Name)
            }

// 同理

            proxyServer.Init()
            ProxyServers[proxyServer.Name] = proxyServer
        }
    }

iflen(ProxyServers) == 0 {
return fmt.Errorf("Parse ini file error: no proxy config found")
    }

returnnil
}
  • 首先,使用ini.LoadFile函数加载配置文件。
  • 然后,读取common部分的配置,将配置项的值赋给相应的变量。
  • 接着,遍历所有的代理服务器配置,创建ProxyServer对象,读取配置项的值并初始化对象,最后将对象存储在ProxyServers映射中。

控制连接处理(control.go)

服务器端(frps/control.go)

服务器端的ProcessControlConn函数,负责处理客户端的连接请求。

funcProcessControlConn(l *conn.Listener) {
for {
        c, err := l.GetConn()
if err != nil {
return
        }
        log.Debug("Get one new conn, %v", c.GetRemoteAddr())
go controlWorker(c)
    }
}
  • 不断从监听的连接中获取新的连接,如果出现错误则返回。
  • 每获取到一个新的连接,就启动一个新的controlWorker函数来处理这个连接。

controlWorker函数具体处理连接的细节:

funccontrolWorker(c *conn.Conn) {
// 读取客户端发送的消息
    res, err := c.ReadLine()
if err != nil {
        log.Warn("Read error, %v", err)
return
    }
    log.Debug("get: %s", res)

// 解析消息
    clientCtlReq := &msg.ClientCtlReq{}
if err := json.Unmarshal([]byte(res), &clientCtlReq); err != nil {
        log.Warn("Parse err: %v : %s", err, res)
return
    }

// 检查代理信息
    succ, info, needRes := checkProxy(clientCtlReq, c)
if !succ {
        clientCtlRes := &msg.ClientCtlRes{}
        clientCtlRes.Code = 1
        clientCtlRes.Msg = info
        buf, _ := json.Marshal(clientCtlRes)
        err = c.Write(string(buf) + "n")
if err != nil {
            log.Warn("Write error, %v", err)
            time.Sleep(1 * time.Second)
return
        }
    }

// 处理其他消息
    s, ok := server.ProxyServers[clientCtlReq.ProxyName]
if !ok {
        log.Warn("ProxyName [%s] is not exist", clientCtlReq.ProxyName)
return
    }

// 读取客户端的控制消息
go readControlMsgFromClient(s, c)

// 向客户端发送工作连接请求
    serverCtlReq := &msg.ClientCtlReq{}
    serverCtlReq.Type = consts.WorkConn
for {
        closeFlag := s.WaitUserConn()
if closeFlag {
            log.Debug("ProxyName [%s], goroutine for dealing user conn is closed", s.Name)
break
        }
        buf, _ := json.Marshal(serverCtlReq)
        err = c.Write(string(buf) + "n")
if err != nil {
            log.Warn("ProxyName [%s], write to client error, proxy exit", s.Name)
            s.Close()
return
        }

        log.Debug("ProxyName [%s], write to client to add work conn success", s.Name)
    }

    log.Info("ProxyName [%s], I'm dead!", s.Name)
return
}
  • 首先,读取客户端发送的消息并解析。
  • 然后,调用checkProxy函数检查代理信息,包括代理名称是否存在、密码是否正确等。
  • 如果检查通过,启动一个新的goroutine来读取客户端的控制消息。
  • 最后,不断等待用户连接,向客户端发送工作连接请求。

客户端端(frpc/control.go)

客户端的ControlProcess函数负责与服务器建立连接并处理服务器的消息。

funcControlProcess(cli *client.ProxyClient, wait *sync.WaitGroup) {
defer wait.Done()

    c, err := loginToServer(cli)
if err != nil {
        log.Error("ProxyName [%s], connect to server failed!", cli.Name)
return
    }
    connection = c
defer connection.Close()

for {
// 读取服务器消息并处理
        content, err := connection.ReadLine()
if err == io.EOF || nil == connection || connection.IsClosed() {
// 处理连接关闭情况
// 尝试重新连接
        } elseif err != nil {
            log.Warn("ProxyName [%s], read from server error, %v", cli.Name, err)
continue
        }

// 解析消息并处理
        clientCtlRes := &msg.ClientCtlRes{}
if err := json.Unmarshal([]byte(content), clientCtlRes); err != nil {
            log.Warn("Parse err: %v : %s", err, content)
continue
        }
if consts.SCHeartBeatRes == clientCtlRes.GeneralRes.Code {
// 处理心跳响应
        }

        cli.StartTunnel(client.ServerAddr, client.ServerPort)
    }
}
  • 首先,调用loginToServer函数与服务器建立连接。
  • 然后,不断读取服务器发送的消息,解析并处理。
  • 如果收到心跳响应,进行相应的处理。
  • 最后,启动隧道,建立与本地服务的连接。

FRP工作流程

服务器启动

服务器端在main函数中首先加载配置文件,然后初始化日志,方便记录系统的运行情况,接着监听指定的地址和端口,开始处理控制连接。

funcmain() {
    err := server.LoadConf("./frps.ini")
if err != nil {
        os.Exit(-1)
    }

    log.InitLog(server.LogWay, server.LogFile, server.LogLevel)

    l, err := conn.Listen(server.BindAddr, server.BindPort)
if err != nil {
        log.Error("Create listener error, %v", err)
        os.Exit(-1)
    }

    log.Info("Start frps success")
    ProcessControlConn(l)
}

客户端启动

客户端在main函数中同样先加载配置文件,初始化日志,然后为每个代理客户端启动一个控制处理流程。

funcmain() {
    err := client.LoadConf("./frpc.ini")
if err != nil {
        os.Exit(-1)
    }

    log.InitLog(client.LogWay, client.LogFile, client.LogLevel)

// wait until all control goroutine exit
var wait sync.WaitGroup
    wait.Add(len(client.ProxyClients))

for _, client := range client.ProxyClients {
go ControlProcess(client, &wait)
    }

    log.Info("Start frpc success")

    wait.Wait()
    log.Warn("All proxy exit!")
}

连接建立与数据传输

  1. 客户端向服务器发送控制连接请求,包含代理名称和密码。
  2. 服务器验证请求,检查代理名称和密码是否正确,如果不正确则拒绝连接。
  3. 如果验证通过,服务器启动代理服务,监听用户连接。
  4. 当有用户连接到服务器的代理端口时,服务器通知客户端建立工作连接。
  5. 客户端建立与本地服务的连接和与服务器的工作连接,将两个连接进行数据转发,实现内网服务的暴露。

总结

0.1.0版本的核心代码都是使用原生库实现,而配置解析代码使用github.com/vaughan0/go-ini实现,用于获取键对值相关的配置文件。github.com/astaxie/beego/logs则用于代码的日志管理和输出。

编译就没编译了,看了下配置文件go版本是早期的1.4。个人电脑是1.20版本,就不再重新弄环境了。

FRP源码阅读,学习FRP工作原理

原文始发于微信公众号(事件响应回忆录):FRP源码阅读,学习FRP工作原理

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月8日14:07:51
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   FRP源码阅读,学习FRP工作原理https://cn-sec.com/archives/4145962.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息