frp浅改记录

frp改造已经有很多了,一开始用的@unknownsec师傅的modify,由于一些版本及其他一些原因,打算自己研究研究。

前言

主要修改的无落地。参考网上的一些文章,发现都是直接硬修改(之前的配置文件功能会失效),包括版本问题导致用不了。这次修改打算在原有基础上添加指定参数,并进行aes加密,思路都是网上的,本次主要是实现软修改,不破坏之前的功能。

加载逻辑分析

文件解析

入口文件在cmd/frpc/main.go

1
2
3
func main() {
	sub.Execute()
}

sub.Execute来自于cmd/frpc/sub/root.go:146

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

rootCmd.Execute来自于cmd/frpc/sub/root.go:100,rootCmd是一个cobra.Command的重写接口,主要关注RunE的内容即可。关注主要内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var rootCmd = &cobra.Command{
  .......
		if cfgDir != "" {
      ......
					defer wg.Done()
					err := runClient(path)
          .....
		}

		// Do not show command usage here.
		err := runClient(cfgFile)
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		return nil
	},
}

runClient用来启动frp客户端,代码中表示支持非当前目录文件。在cmd/frpc/sub/root.go:192跟进这个函数

1
2
3
4
5
6
7
func runClient(cfgFilePath string) error {
	cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
	if err != nil {
		return err
	}
	return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
}

传入的配置文件被ParseClientConfig解析,然后通过startService启动服务。这里我们先跟进ParseClientConfig函数

pkg/config/parse.go:24找到它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func ParseClientConfig(filePath string) (
	cfg ClientCommonConf,
	pxyCfgs map[string]ProxyConf,
	visitorCfgs map[string]VisitorConf,
	err error,
) {
	var content []byte
	content, err = GetRenderedConfFromFile(filePath)
  .....
	cfg, err = UnmarshalClientConfFromIni(content)
	if err != nil {
		return
	}
  .....
	// Parse all proxy and visitor configs.
	pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
	if err != nil {
		return
	}
	return
}

主要流程是GetRenderedConfFromFile读取文件内容,UnmarshalClientConfFromIni负责对内容进行解析。

首先是GetRenderedConfFromFile,主要是一个io的操作和对配置文件进行模板检测(RenderContent),很多frp配置文件改动都基于此(这里的b参数就是frpc.ini文件的源内容)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func GetRenderedConfFromFile(path string) (out []byte, err error) {
	var b []byte
	b, err = os.ReadFile(path)
	if err != nil {
		return
	}

	out, err = RenderContent(b)
	return
}

通过前面大概能得出解析过程

1
sub.Execute() -> rootCmd.Execute() -> runClient(cfgFile) -> config.ParseClientConfig(cfgFilePath) -> GetRenderedConfFromFile(filePath) -> UnmarshalClientConfFromIni(content)

简单来说返回的就是把文件解析成一个map,通过自定义结构体来读取对应端口、ip协议。

客户端运行

前面说到客户端利用startService进行服务启动,跟进函数。这里我们只关注cfgFile参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func startService(....
  cfgFile string)

	if cfgFile != "" {
		log.Trace("start frpc service for config file [%s]", cfgFile)
		defer log.Trace("frpc service for config file [%s] stopped", cfgFile)
	}
	svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
	......
}

首先是对cfgFile进行存在判断,然后client.NewService启动服务。继续跟进

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func NewService(...
	cfgFile string,
) (svr *Service, err error) {
	ctx, cancel := context.WithCancel(context.Background())
	svr = &Service{
    ...
		cfgFile:     cfgFile,
		pxyCfgs:     pxyCfgs,
		visitorCfgs: visitorCfgs,
		exit:        0,
		ctx:         xlog.NewContext(ctx, xlog.New()),
		cancel:      cancel,
	}
	return
}

发现cfgFile被指向ServicecfgFile,也就是svr.cfgFile。全局搜索一下被用于哪些地方。

https://s2.loli.net/2022/12/21/k6JhnEyWpjMZ5Rg.png

基本都源于admin_api.go。一处一处分析。

1.client/admin_api.go:56

1
_, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(svr.cfgFile)

这部分是文件解析,上部分分析过。

2.client/admin_api.go:219

1
2
3
4
5
6
if svr.cfgFile == "" {
		res.Code = 400
		res.Msg = "frpc has no config file path"
		log.Warn("%s", res.Msg)
		return
	}

主要是对值存在与否进行判断。

3.client/admin_api.go:226

1
content, err := config.GetRenderedConfFromFile(svr.cfgFile)

GetRenderedConfFromFile前面分析过。

4.client/admin_api.go:277

1
b, err := os.ReadFile(svr.cfgFile)

文件读取。

5.client/admin_api.go:316

1
err = os.WriteFile(svr.cfgFile, []byte(content), 0o644)

svr.cfgFile文件写入。

综合前面几部分来看,三个函数用到了配置文件。

apiReload对配置文件进行加载。

apiGetConfig读取配置文件。

apiPutConfig函数对cfgFile进行判断,为空则写入配置文件模板。


(客户端部分这里只分析了配置文件解析、使用相关过程,没有分析具体运行逻辑。

改动思路

改动要求主要分为三部分。

1.改动后之前的功能均可用。不破坏之前的功能。(比如admin_api.go

2.我们不需要配置文件。通过指定参数来满足基本要求。这里的想法是ip、server端口、remote端口以及tls值可控。

3.参数不能是 -i 192.xxx -p 1080类型,需要对参数进行整体加密,如 -ts [enc](比如aes密文网上公开的tips)。

有了改动思路了我们就可以对代码进行浅改动了(顺便学习go。

代码实现部分

首先是接收命令行部分cmd/frpc/sub/root.go:79,只需要给它加个参数即可。

1
rootCmd.PersistentFlags().StringVarP(&le3, "config", "x", "[AES_Base64]", "xxx")

参数有了,下一步我们需要给他加进某个函数,这样才能解析我们的值。这里瞄准了cmd/frpc/sub/root.go:133部分,这也是一开始加载文件的地方。主要改动runClient的调用逻辑,通过前面能发现它被调用了两次,一次是传递的是path参数,一次传递的cfgFile参数,由于cfgDir默认为空,第一次调用我们可以不用改动。(代码可以参考配置文件分析部分)。

由于它runClient只使用了cfgFile参数,这个参数是配置文件,所以它是必须参数。但我们改造完成后可以不使用它,所以我们修改这部分。

这里思路是通过给runClient参数添加可变参数,在接收到le3参数时,忽略cfgFile参数。首先添加可变参数。

1
2
3
4
5
6
7
8
func runClient(cfgFilePath string, le3Hint ...string) error {

	cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath, le3Hint...)
	if err != nil {
		return err
	}
	return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
}

添加了然后在root.go修改逻辑。这里的cfgFile参数我们随便赋值即可,后面主要针对扩展参数进行处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
		....
		if (cfgFile == "") && (le3 != "") {
			cfgFile = "test"
			err := runClient(cfgFile, le3)
			if err != nil {
				fmt.Println(err)
				os.Exit(0)
			}
		} else {
			// Do not show command usage here.
			err := runClient(cfgFile)
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}
		}
		......

然后是runClient函数,添加可选变量

1
2
3
4
5
6
7
8
func runClient(cfgFilePath string, le3Hint ...string) error {

	cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath, le3Hint...)
	if err != nil {
		return err
	}
	return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
}

发现除了ParseClientConfigstartService也使用了cfgFilePath参数。接下来是对扩展参数的解析。

由于我们传入是AES的密文,需要对明/密文进行加/解密,这里同样使用map来作为传递参数的明文。也就是通过AES来加密我们的map作为传递参数。

map可以算是golang的json,类型是键值对。

先写AES加/解密(其实只用解密即可),参考https://juejin.cn/post/6953493716078166030

把加/解密写在pkg/config/utils.go:55

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
......
func PKCS7Padding(text []byte, blockSize int) []byte {
	//PKCS7填充
	padding := blockSize - len(text)%blockSize
	var padText []byte
	if padding == 0 {
		padText = bytes.Repeat([]byte{byte(blockSize)}, blockSize)
	} else {
		padText = bytes.Repeat([]byte{byte(padding)}, padding)
	}
	return append(text, padText...)
}

func UnPKCS7Padding(text []byte) []byte {
	//PKCS7去除
	unPadding := int(text[len(text)-1])
	return text[:len(text)-unPadding]
}

func AesCbcEncrypt(text, key, iv []byte) []byte {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil
	}
	padText := PKCS7Padding(text, block.BlockSize())
	blockMode := cipher.NewCBCEncrypter(block, iv)
	result := make([]byte, len(padText))
	blockMode.CryptBlocks(result, padText)
	return result
}

func AesCbcDecrypt(enc, key, iv []byte) []byte {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil
	}
	blockMode := cipher.NewCBCDecrypter(block, iv)
	result := make([]byte, len(enc))
	blockMode.CryptBlocks(result, enc)
	result = UnPKCS7Padding(result)
	return result
}

我们传入的格式是[AES_Base64]密文,我们可以先生成需要传入的密文。待加密的数据如下

1
2
3
4
5
6
{
		"ip":  "192.168.1.1",
		"port": "8088",
  	"rePort": "9999",
		"tls": "true",
}

用map进行传递加密。iv和key可以自己定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
le3 := map[string]string{
		"ip":     "192.168.1.1",
		"port":   "8088",
		"rePort": "9999",
		"tls":    "true",
	}
	iv, key := []byte("16195aa289b57e22"), []byte("8a23be48f0983bfe")
	enc, _ := json.Marshal(le3)
	res := config.AesCbcEncrypt(enc, key, iv)
	fmt.Println(base64.StdEncoding.EncodeToString(res))

//Kiqh2Qs/364peWaYPfkYYBeX1hyH3oi/US3S+SLhBBIhWmXlpyQV1tLFf1XDqsS2bSSlOGq3b0RU1iNViiMagw==

这是命令行传递的参数,我们需要在frp内部进行解密

1
2
3
4
5
6
7
8
9
dec := config.AesCbcDecrypt(res, key, iv)
	tmp := map[string]string{}
	err = json.Unmarshal(dec, &tmp)
	if err != nil {
		return
	}
	fmt.Println(tmp)

//map[ip:192.168.1.1 port:8088 rePort:9999 tls:true]

取值的话用对应键值即可。获取完参数后就可以继续对ParseClientConfig进行修改了。通过简单if/else对content内容进行截取,最终重写函数内容为后面发现修改GetRenderedConfFromFile会解决后面的问题。添加可选参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func GetRenderedConfFromFile(path string, l3content ...string) (out []byte, err error) {
	var b []byte
	if path != "" && l3content[0] == "" {
		b, err = os.ReadFile(path)
		if err != nil {
			return
		}
	} else {
		rand.Seed(time.Now().UTC().UnixNano())
		var randStr string
		for i := 0; i < 6; i++ {
			num := rand.Intn(10)
			randStr += strconv.Itoa(num)
		}
		iv, key := []byte("16195aa289b57e22"), []byte("8a23be48f0983bfe")
		encAes, _ := base64.StdEncoding.DecodeString(l3content[0])
		dec := AesCbcDecrypt(encAes, key, iv)
		newCfgMap := map[string]string{}
		err = json.Unmarshal(dec, &newCfgMap)
		if err != nil {
			fmt.Println(err)
		}
		newIp := newCfgMap["ip"]
		newPort := newCfgMap["port"]
		tlsBool := newCfgMap["tls"]
		RePort := newCfgMap["rePort"]
		b = []byte(`[common]
		server_addr = ` + newIp + `
		server_port = ` + newPort + `
		tls_enable = ` + tlsBool + `
		[` + randStr + `]
		type = tcp
		remote_port = ` + RePort + `
		plugin = socks5`)
	}
	out, err = RenderContent(b)
	return
}

然后返回来修改ParseClientConfig

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func ParseClientConfig(filePath string, l3content ...string) (
.....
) {
	var content []byte
	if filePath == "" && l3content[0] != "" {
		content, err = GetRenderedConfFromFile(filePath, l3content[0])
	} else {
		content, err = GetRenderedConfFromFile(filePath)
		if err != nil {
			return
		}
	}
  ................
}

到这解析基本完成。

但前面startService也调用了基础配置文件内容,前面分析过相关过程。主要是针对调用svg.cfgFile的几部分做修改。也就是client/service.goclient/admin_api.go部分。当然,入口处也需要更改。首先给startService增加可选参数。

cmd/frpc/sub/root.go

主要是runClientstartservice函数部分。加点简单if/else就行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func runClient(cfgFilePath string, le3Hint ...string) error {

	cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath, le3Hint...)
	if err != nil {
		return err
	}
  if cfgFile == "" && len(le3Hint) != 0 {
		return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath, le3Hint[0])
	}
	return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
}

func startService(
	cfg config.ClientCommonConf,
	pxyCfgs map[string]config.ProxyConf,
	visitorCfgs map[string]config.VisitorConf,
	cfgFile string,
	le3 ...string,
) (err error) {
	log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,
		cfg.LogMaxDays, cfg.DisableLogColor)

	if cfgFile != "" {
		log.Trace("start frpc service for config file [%s]", cfgFile)
		defer log.Trace("frpc service for config file [%s] stopped", cfgFile)
	}
  if len(le3) != 0 {
		log.Trace("start frpc service for le3")
		defer log.Trace("frpc service for le3 stopped")
	}
......
 }

然后是client.NewServicecfgFile的调用,利用之前的方法,依然添加可选参数,处理runClient传入的可选参数。由于NewService中处理相关变量用到了结构体,所以我们需要在Service结构体添加。

1
2
3
4
5
6
type Service struct {
	...........
	//other var
	le3 []string
	..........
}

然后添加可选参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func NewService(
  ......
	cfgFile string,
	le3 ...string,
) (svr *Service, err error) {
	ctx, cancel := context.WithCancel(context.Background())
	svr = &Service{
		authSetter:  auth.NewAuthSetter(cfg.ClientConfig),
		cfg:         cfg,
		cfgFile:     cfgFile,
		le3:         le3,
    ........
	}
	return
}

跟着前面的分析思路。Service结构体指针指向了svr,所以我们还需要修改svr.cfgFile相关部分。也就是admin_api.go部分。

对第一处,apiReload函数相关部分进行改动。

client/admin_api.go:55,记得添加if/else判断

1
2
3
4
	_, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(svr.cfgFile)
	if len(svr.le3) != 0 {
		_, pxyCfgs, visitorCfgs, err = config.ParseClientConfig(svr.cfgFile, svr.le3[0])
	}

然后是apiGetConfig函数部分。还是简单if/else

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
  .....

  if svr.cfgFile == "" && len(svr.le3) != 0 {
		res.Code = 400
		res.Msg = "frpc has no config file path && not le3"
		log.Warn("%s", res.Msg)
		return
	}
	var content []byte
	var err error
  if svr.cfgFile == "" && len(svr.le3) != 0 {
		content, err = config.GetRenderedConfFromFile(svr.cfgFile, svr.le3)
	} else {
		content, err = config.GetRenderedConfFromFile(svr.cfgFile)
	}
	if err != nil {
		res.Code = 400
		res.Msg = err.Error()
		log.Warn("load frpc and le3 config file error: %s ", res.Msg)
		return
	}
  ..........
}

最后是apiPutConfig,由于他是直接读、写配置文件内容,且属于控制面版功能。对于无文件来说,暂不考虑这个问题。直接对无文件进行判断。

1
2
3
	if svr.cfgFile == "" && svr.le3 != "" {
		return
	}

代码到这里就实现完成了。

最终大概就是。

配置文件

https://s2.loli.net/2022/12/21/KqHjIJORGzgnYw4.png

指定参数

https://s2.loli.net/2022/12/21/cGzwmijPrsb5VEM.png

0%