From 4f8e7e66e36a652b2ee65b73bdd5622f1c7e9795 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 26 Aug 2026 10:01:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20frps/frpc=20TOML=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=B3=A8=E5=85=A5=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20protocol.TOMLQuote=20=E5=B9=B6=E5=9C=A8=E4=B8=A4=E5=A4=84?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=B8=B2=E6=9F=93=E5=85=A8=E9=83=A8=E4=BD=BF?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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} --- .auto/log.jsonl | 1 + internal/apps/flared/frpc/manager.go | 16 ++++++++----- internal/apps/relay/frps/manager.go | 5 ++-- pkg/protocol/toml.go | 34 ++++++++++++++++++++++++++++ pkg/protocol/toml_test.go | 22 ++++++++++++++++++ 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 pkg/protocol/toml.go create mode 100644 pkg/protocol/toml_test.go diff --git a/.auto/log.jsonl b/.auto/log.jsonl index 5a5bb564..07c42654 100644 --- a/.auto/log.jsonl +++ b/.auto/log.jsonl @@ -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 守卫"}} diff --git a/internal/apps/flared/frpc/manager.go b/internal/apps/flared/frpc/manager.go index 2b3b9f43..43af7544 100644 --- a/internal/apps/flared/frpc/manager.go +++ b/internal/apps/flared/frpc/manager.go @@ -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") } diff --git a/internal/apps/relay/frps/manager.go b/internal/apps/relay/frps/manager.go index ce244569..06faab0a 100644 --- a/internal/apps/relay/frps/manager.go +++ b/internal/apps/relay/frps/manager.go @@ -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) } diff --git a/pkg/protocol/toml.go b/pkg/protocol/toml.go new file mode 100644 index 00000000..d16f5cf8 --- /dev/null +++ b/pkg/protocol/toml.go @@ -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() +} + diff --git a/pkg/protocol/toml_test.go b/pkg/protocol/toml_test.go new file mode 100644 index 00000000..c574a24f --- /dev/null +++ b/pkg/protocol/toml_test.go @@ -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) + } + } +}