修复 frps/frpc TOML 配置注入:新增 protocol.TOMLQuote 并在两处配置渲染全部使用

Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
This commit is contained in:
ryan
2026-08-26 10:01:33 +08:00
parent ed1efd3d54
commit 4f8e7e66e3
5 changed files with 69 additions and 9 deletions
+1
View File
@@ -40,3 +40,4 @@
{"run":39,"commit":"be5d067","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":76,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"auth_cache negative 缓存加上限防 DoS + relay/flared 删除重复 authenticateAccessToken 改用 agent 共享缓存版","timestamp":1787708052241,"segment":0,"confidence":null,"asi":{"hypothesis":"negative cache 无上限可被伪造 token 撑爆内存;relay/flared 与 agent 三份重复的 authenticateAccessToken","next_action_hint":"继续扫其他无界缓存/限流缺口","result":"metric 持平 8(8 个均为 deliberate keeper),安全修复不计入 metric","security":"negative cache 加 10k 上限+过期清理;relay/flared 复用 agent.AuthenticateAccessToken(共享 2min 正/10min 负缓存,DB 压力下降)"}}
{"run":40,"commit":"0dd2cf9","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"websocket 三 hub 去重:抽 runWritePump 共享写泵 + 合并 agent 广播函数为 broadcastAgent","timestamp":1787708370650,"segment":0,"confidence":null,"asi":{"hypothesis":"三份 hub 的 writePump 完全重复(仅日志前缀不同),readPump 已有 runReadPump 抽取先例;BroadcastWAFIPGroups/BroadcastActiveConfig 复制粘贴","next_action_hint":"close() 3 份小重复可再合并但收益低;继续找其他模块的重复/无界增长","result":"metric 持平 8,全测试绿","refactor":"新增 websocket/write_pump.go runWritePump(对齐 runReadPump 模式),agent/relay/flared writePump 改委托;agent_hub 抽 broadcastAgent 合并两个广播函数"}}
{"run":41,"commit":"efd8268","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":75,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"websocket 三 client 结构体去重:嵌入共享 wsClientCore(close/enqueue 单份实现)","timestamp":1787708975609,"segment":0,"confidence":null,"asi":{"hypothesis":"agentClient/relayClient/flaredClient 字段与 close/enqueue 完全相同,用组合(嵌入 wsClientCore)消除三份重复","next_action_hint":"代码库经 40 轮已高度收敛;后续可周期性跑 go test -race 全量","result":"metric 持平 8,全测试绿;净减 ~60 行重复代码","refactor":"新增 websocket/client_core.go:wsClientCore(nodeID/conn/send/done/once) + 共享 close/enqueue;三个 client 结构体改为嵌入"}}
{"run":42,"commit":"ed1efd3","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"补 wsClientCore 并发测试 + close() 防 nil conn 守卫","timestamp":1787709222794,"segment":0,"confidence":null,"asi":{"hypothesis":"wsClientCore 并发语义(close 幂等、enqueue 不阻塞/关后拒绝)无测试覆盖","next_action_hint":"websocket 包已有基础并发测试;继续其他模块扫描","result":"metric 持平 8;测试还暴露 close 未防 nil conn 的防御缺口,已补守卫","refactor":"新增 websocket/client_core_test.go 3 个 -race 测试;client_core.go close() 增加 nil conn 守卫"}}
+10 -6
View File
@@ -285,23 +285,27 @@ func buildFrpcToml(relay service.FlaredRelayInfo, proxies []service.FlaredProxyE
host, port := parseAddr(relay.Address)
fmt.Fprintf(&buf, "serverAddr = \"%s\"\nserverPort = %s\n", host, port)
fmt.Fprintf(&buf, "serverAddr = %s\nserverPort = %s\n", service.TOMLQuote(host), service.TOMLQuote(port))
if relay.AuthToken != "" {
fmt.Fprintf(&buf, "auth.method = \"token\"\nauth.token = \"%s\"\n", relay.AuthToken)
fmt.Fprintf(&buf, "auth.method = \"token\"\nauth.token = %s\n", service.TOMLQuote(relay.AuthToken))
}
if relay.ProxyURL != "" {
fmt.Fprintf(&buf, "transport.proxyURL = \"%s\"\n", relay.ProxyURL)
fmt.Fprintf(&buf, "transport.proxyURL = %s\n", service.TOMLQuote(relay.ProxyURL))
}
buf.WriteString("\n")
for _, proxy := range proxies {
fmt.Fprintf(&buf, "[[proxies]]\nname = \"%s\"\ntype = \"%s\"\nlocalIP = \"%s\"\nlocalPort = %d\n",
proxy.Name, proxy.Type, proxy.LocalAddr, proxy.LocalPort)
fmt.Fprintf(&buf, "[[proxies]]\nname = %s\ntype = %s\nlocalIP = %s\nlocalPort = %d\n",
service.TOMLQuote(proxy.Name), service.TOMLQuote(proxy.Type), service.TOMLQuote(proxy.LocalAddr), proxy.LocalPort)
if len(proxy.CustomDomains) > 0 {
fmt.Fprintf(&buf, "customDomains = [\"%s\"]\n", strings.Join(proxy.CustomDomains, "\", \""))
quoted := make([]string, len(proxy.CustomDomains))
for i := range proxy.CustomDomains {
quoted[i] = service.TOMLQuote(proxy.CustomDomains[i])
}
fmt.Fprintf(&buf, "customDomains = [%s]\n", strings.Join(quoted, ", "))
}
buf.WriteString("\n")
}
+2 -3
View File
@@ -182,7 +182,7 @@ func (m *Manager) renderConfig(cfg *service.RelayConfig) error {
if cfg.AuthToken != "" {
buf.WriteString("[auth]\n")
buf.WriteString("method = \"token\"\n")
fmt.Fprintf(&buf, "token = \"%s\"\n", cfg.AuthToken)
buf.WriteString("token = " + service.TOMLQuote(cfg.AuthToken) + "\n")
}
// WebServer configuration
@@ -203,8 +203,7 @@ func (m *Manager) renderConfig(cfg *service.RelayConfig) error {
if password == "" {
password = "admin"
}
fmt.Fprintf(&buf, "password = \"%s\"\n", password)
fmt.Fprintf(&buf, "password = %s\n", service.TOMLQuote(password))
return os.WriteFile(m.configPath, buf.Bytes(), frpsConfigFilePerm)
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package protocol
import "strings"
// TOMLQuote renders s as a quoted TOML basic string, escaping characters that
// would otherwise break the document or allow key injection (quotes,
// backslashes, control/newline characters). Use it for every interpolated
// value written into frps/frpc TOML configs.
func TOMLQuote(s string) string {
var b strings.Builder
b.WriteByte('"')
for _, r := range s {
switch r {
case '\\':
b.WriteString(`\\`)
case '"':
b.WriteString(`\"`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
default:
b.WriteRune(r)
}
}
b.WriteByte('"')
return b.String()
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package protocol
import "testing"
func TestTOMLQuote(t *testing.T) {
cases := map[string]string{
`plain`: `"plain"`,
`a"b`: `"a\"b"`,
`a\b`: `"a\\b"`,
"injection\"\n": `"injection\"\n"`,
"": `""`,
"a\tb": `"a\tb"`,
}
for in, want := range cases {
if got := TOMLQuote(in); got != want {
t.Errorf("TOMLQuote(%q) = %q, want %q", in, got, want)
}
}
}