Skip to content

Commit 94479eb

Browse files
committed
feat(relay): 新增 relay check 链路检测命令
支持检测 frps 服务器连通性、本地服务监听、远程穿透端口状态和延迟, 支持 --json 输出供 GUI 解析,支持指定单条规则或批量检测全部规则。
1 parent d976ee2 commit 94479eb

2 files changed

Lines changed: 246 additions & 0 deletions

File tree

cmd/relay_check.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"text/tabwriter"
8+
9+
"github.com/qingchencloud/cftunnel/internal/config"
10+
"github.com/qingchencloud/cftunnel/internal/relay"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var checkJSON bool
15+
16+
func init() {
17+
relayCheckCmd.Flags().BoolVar(&checkJSON, "json", false, "JSON 格式输出")
18+
relayCmd.AddCommand(relayCheckCmd)
19+
}
20+
21+
var relayCheckCmd = &cobra.Command{
22+
Use: "check [规则名]",
23+
Short: "检测中继链路连通性",
24+
Long: "检测 frps 服务器、本地服务、远程穿透端口的连通性和延迟。不指定规则名则检测全部规则。",
25+
Args: cobra.MaximumNArgs(1),
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
cfg, err := config.Load()
28+
if err != nil {
29+
return err
30+
}
31+
if cfg.Relay.Server == "" {
32+
return fmt.Errorf("未配置中继服务器,请先执行 cftunnel relay init")
33+
}
34+
35+
ruleName := ""
36+
if len(args) > 0 {
37+
ruleName = args[0]
38+
}
39+
40+
result := relay.Check(&cfg.Relay, ruleName)
41+
42+
if checkJSON {
43+
return printCheckJSON(result)
44+
}
45+
printCheckTable(result)
46+
return nil
47+
},
48+
}
49+
50+
func printCheckTable(r relay.CheckResult) {
51+
fmt.Println("中继链路检测")
52+
fmt.Println("============")
53+
54+
// 服务器状态
55+
if r.ServerOK {
56+
fmt.Printf("服务器: %s ✓ 可达 (%dms)\n", r.Server, r.ServerLatency)
57+
} else {
58+
fmt.Printf("服务器: %s ✗ 不可达\n", r.Server)
59+
}
60+
61+
// frpc 进程状态
62+
if r.FrpcRunning {
63+
fmt.Printf("frpc: 运行中 (PID: %d)\n", r.FrpcPID)
64+
} else {
65+
fmt.Println("frpc: 未运行")
66+
}
67+
fmt.Println()
68+
69+
if len(r.Rules) == 0 {
70+
fmt.Println("暂无规则需要检测")
71+
return
72+
}
73+
74+
// 规则检测结果表格
75+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
76+
fmt.Fprintln(w, "规则\t协议\t本地端口\t远程端口\t本地服务\t远程穿透\t延迟")
77+
fmt.Fprintln(w, "----\t----\t--------\t--------\t--------\t--------\t----")
78+
for _, rule := range r.Rules {
79+
local := "✓"
80+
if !rule.LocalOK {
81+
local = "✗ " + rule.LocalErr
82+
}
83+
remote := "-"
84+
if rule.RemotePort > 0 {
85+
if rule.RemoteOK {
86+
remote = "✓"
87+
} else {
88+
remote = "✗ " + rule.RemoteErr
89+
}
90+
}
91+
latency := "-"
92+
if rule.LatencyMS > 0 {
93+
latency = fmt.Sprintf("%dms", rule.LatencyMS)
94+
}
95+
remotePort := "-"
96+
if rule.RemotePort > 0 {
97+
remotePort = fmt.Sprintf("%d", rule.RemotePort)
98+
}
99+
fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\t%s\t%s\n",
100+
rule.Name, rule.Proto, rule.LocalPort, remotePort, local, remote, latency)
101+
}
102+
w.Flush()
103+
104+
fmt.Printf("\n结果: %d 条规则, %d 通 / %d 断\n", r.Total, r.Passed, r.Failed)
105+
}
106+
107+
func printCheckJSON(r relay.CheckResult) error {
108+
enc := json.NewEncoder(os.Stdout)
109+
enc.SetIndent("", " ")
110+
return enc.Encode(r)
111+
}

internal/relay/check.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package relay
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"sync"
7+
"time"
8+
9+
"github.com/qingchencloud/cftunnel/internal/config"
10+
)
11+
12+
const checkTimeout = 3 * time.Second
13+
14+
// CheckResult 链路检测总结果
15+
type CheckResult struct {
16+
Server string `json:"server"`
17+
ServerOK bool `json:"server_ok"`
18+
ServerLatency int64 `json:"server_latency_ms"`
19+
FrpcRunning bool `json:"frpc_running"`
20+
FrpcPID int `json:"frpc_pid"`
21+
Rules []RuleCheckResult `json:"rules"`
22+
Total int `json:"total"`
23+
Passed int `json:"passed"`
24+
Failed int `json:"failed"`
25+
}
26+
27+
// RuleCheckResult 单条规则检测结果
28+
type RuleCheckResult struct {
29+
Name string `json:"name"`
30+
Proto string `json:"proto"`
31+
LocalPort int `json:"local_port"`
32+
RemotePort int `json:"remote_port"`
33+
LocalOK bool `json:"local_ok"`
34+
RemoteOK bool `json:"remote_ok"`
35+
LatencyMS int64 `json:"latency_ms"`
36+
LocalErr string `json:"local_err,omitempty"`
37+
RemoteErr string `json:"remote_err,omitempty"`
38+
}
39+
40+
// Check 执行链路检测
41+
func Check(cfg *config.RelayConfig, ruleName string) CheckResult {
42+
result := CheckResult{Server: cfg.Server}
43+
44+
// 检测 frps 服务器连通性
45+
if cfg.Server != "" {
46+
start := time.Now()
47+
conn, err := net.DialTimeout("tcp", cfg.Server, checkTimeout)
48+
if err == nil {
49+
conn.Close()
50+
result.ServerOK = true
51+
result.ServerLatency = time.Since(start).Milliseconds()
52+
}
53+
}
54+
55+
// 检测 frpc 进程
56+
result.FrpcRunning = Running()
57+
result.FrpcPID = PID()
58+
59+
// 筛选要检测的规则
60+
rules := cfg.Rules
61+
if ruleName != "" {
62+
rules = nil
63+
for _, r := range cfg.Rules {
64+
if r.Name == ruleName {
65+
rules = append(rules, r)
66+
break
67+
}
68+
}
69+
}
70+
71+
// 并行检测所有规则
72+
result.Rules = make([]RuleCheckResult, len(rules))
73+
var wg sync.WaitGroup
74+
for i, rule := range rules {
75+
wg.Add(1)
76+
go func(idx int, r config.RelayRule) {
77+
defer wg.Done()
78+
result.Rules[idx] = checkRule(r, cfg.Server)
79+
}(i, rule)
80+
}
81+
wg.Wait()
82+
83+
// 统计
84+
result.Total = len(result.Rules)
85+
for _, r := range result.Rules {
86+
if r.LocalOK && (r.RemoteOK || r.RemotePort == 0) {
87+
result.Passed++
88+
} else {
89+
result.Failed++
90+
}
91+
}
92+
return result
93+
}
94+
95+
// checkRule 检测单条规则
96+
func checkRule(r config.RelayRule, server string) RuleCheckResult {
97+
rc := RuleCheckResult{
98+
Name: r.Name,
99+
Proto: r.Proto,
100+
LocalPort: r.LocalPort,
101+
RemotePort: r.RemotePort,
102+
}
103+
104+
localIP := r.LocalIP
105+
if localIP == "" {
106+
localIP = "127.0.0.1"
107+
}
108+
109+
// 检测本地服务
110+
localAddr := fmt.Sprintf("%s:%d", localIP, r.LocalPort)
111+
conn, err := net.DialTimeout("tcp", localAddr, checkTimeout)
112+
if err == nil {
113+
conn.Close()
114+
rc.LocalOK = true
115+
} else {
116+
rc.LocalErr = "未监听"
117+
}
118+
119+
// 检测远程穿透端口
120+
if r.RemotePort > 0 && server != "" {
121+
host, _, _ := net.SplitHostPort(server)
122+
remoteAddr := fmt.Sprintf("%s:%d", host, r.RemotePort)
123+
start := time.Now()
124+
conn, err := net.DialTimeout("tcp", remoteAddr, checkTimeout)
125+
if err == nil {
126+
conn.Close()
127+
rc.RemoteOK = true
128+
rc.LatencyMS = time.Since(start).Milliseconds()
129+
} else {
130+
rc.RemoteErr = "超时"
131+
}
132+
}
133+
134+
return rc
135+
}

0 commit comments

Comments
 (0)