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
被指向Service
的cfgFile
,也就是svr.cfgFile
。全局搜索一下被用于哪些地方。
基本都源于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)
}
|
发现除了ParseClientConfig
,startService
也使用了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.go
和client/admin_api.go
部分。当然,入口处也需要更改。首先给startService
增加可选参数。
cmd/frpc/sub/root.go
主要是runClient
和startservice
函数部分。加点简单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.NewService
对cfgFile
的调用,利用之前的方法,依然添加可选参数,处理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
}
|
代码到这里就实现完成了。
最终大概就是。
配置文件
指定参数