mirror of
https://github.com/qingchencloud/cftunnel.git
synced 2026-08-28 18:20:34 +08:00
feat(relay): 新增 relay check 链路检测命令
支持检测 frps 服务器连通性、本地服务监听、远程穿透端口状态和延迟, 支持 --json 输出供 GUI 解析,支持指定单条规则或批量检测全部规则。
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/qingchencloud/cftunnel/internal/config"
|
||||
"github.com/qingchencloud/cftunnel/internal/relay"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var checkJSON bool
|
||||
|
||||
func init() {
|
||||
relayCheckCmd.Flags().BoolVar(&checkJSON, "json", false, "JSON 格式输出")
|
||||
relayCmd.AddCommand(relayCheckCmd)
|
||||
}
|
||||
|
||||
var relayCheckCmd = &cobra.Command{
|
||||
Use: "check [规则名]",
|
||||
Short: "检测中继链路连通性",
|
||||
Long: "检测 frps 服务器、本地服务、远程穿透端口的连通性和延迟。不指定规则名则检测全部规则。",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Relay.Server == "" {
|
||||
return fmt.Errorf("未配置中继服务器,请先执行 cftunnel relay init")
|
||||
}
|
||||
|
||||
ruleName := ""
|
||||
if len(args) > 0 {
|
||||
ruleName = args[0]
|
||||
}
|
||||
|
||||
result := relay.Check(&cfg.Relay, ruleName)
|
||||
|
||||
if checkJSON {
|
||||
return printCheckJSON(result)
|
||||
}
|
||||
printCheckTable(result)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func printCheckTable(r relay.CheckResult) {
|
||||
fmt.Println("中继链路检测")
|
||||
fmt.Println("============")
|
||||
|
||||
// 服务器状态
|
||||
if r.ServerOK {
|
||||
fmt.Printf("服务器: %s ✓ 可达 (%dms)\n", r.Server, r.ServerLatency)
|
||||
} else {
|
||||
fmt.Printf("服务器: %s ✗ 不可达\n", r.Server)
|
||||
}
|
||||
|
||||
// frpc 进程状态
|
||||
if r.FrpcRunning {
|
||||
fmt.Printf("frpc: 运行中 (PID: %d)\n", r.FrpcPID)
|
||||
} else {
|
||||
fmt.Println("frpc: 未运行")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if len(r.Rules) == 0 {
|
||||
fmt.Println("暂无规则需要检测")
|
||||
return
|
||||
}
|
||||
|
||||
// 规则检测结果表格
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "规则\t协议\t本地端口\t远程端口\t本地服务\t远程穿透\t延迟")
|
||||
fmt.Fprintln(w, "----\t----\t--------\t--------\t--------\t--------\t----")
|
||||
for _, rule := range r.Rules {
|
||||
local := "✓"
|
||||
if !rule.LocalOK {
|
||||
local = "✗ " + rule.LocalErr
|
||||
}
|
||||
remote := "-"
|
||||
if rule.RemotePort > 0 {
|
||||
if rule.RemoteOK {
|
||||
remote = "✓"
|
||||
} else {
|
||||
remote = "✗ " + rule.RemoteErr
|
||||
}
|
||||
}
|
||||
latency := "-"
|
||||
if rule.LatencyMS > 0 {
|
||||
latency = fmt.Sprintf("%dms", rule.LatencyMS)
|
||||
}
|
||||
remotePort := "-"
|
||||
if rule.RemotePort > 0 {
|
||||
remotePort = fmt.Sprintf("%d", rule.RemotePort)
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\t%s\t%s\n",
|
||||
rule.Name, rule.Proto, rule.LocalPort, remotePort, local, remote, latency)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
fmt.Printf("\n结果: %d 条规则, %d 通 / %d 断\n", r.Total, r.Passed, r.Failed)
|
||||
}
|
||||
|
||||
func printCheckJSON(r relay.CheckResult) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(r)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qingchencloud/cftunnel/internal/config"
|
||||
)
|
||||
|
||||
const checkTimeout = 3 * time.Second
|
||||
|
||||
// CheckResult 链路检测总结果
|
||||
type CheckResult struct {
|
||||
Server string `json:"server"`
|
||||
ServerOK bool `json:"server_ok"`
|
||||
ServerLatency int64 `json:"server_latency_ms"`
|
||||
FrpcRunning bool `json:"frpc_running"`
|
||||
FrpcPID int `json:"frpc_pid"`
|
||||
Rules []RuleCheckResult `json:"rules"`
|
||||
Total int `json:"total"`
|
||||
Passed int `json:"passed"`
|
||||
Failed int `json:"failed"`
|
||||
}
|
||||
|
||||
// RuleCheckResult 单条规则检测结果
|
||||
type RuleCheckResult struct {
|
||||
Name string `json:"name"`
|
||||
Proto string `json:"proto"`
|
||||
LocalPort int `json:"local_port"`
|
||||
RemotePort int `json:"remote_port"`
|
||||
LocalOK bool `json:"local_ok"`
|
||||
RemoteOK bool `json:"remote_ok"`
|
||||
LatencyMS int64 `json:"latency_ms"`
|
||||
LocalErr string `json:"local_err,omitempty"`
|
||||
RemoteErr string `json:"remote_err,omitempty"`
|
||||
}
|
||||
|
||||
// Check 执行链路检测
|
||||
func Check(cfg *config.RelayConfig, ruleName string) CheckResult {
|
||||
result := CheckResult{Server: cfg.Server}
|
||||
|
||||
// 检测 frps 服务器连通性
|
||||
if cfg.Server != "" {
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", cfg.Server, checkTimeout)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
result.ServerOK = true
|
||||
result.ServerLatency = time.Since(start).Milliseconds()
|
||||
}
|
||||
}
|
||||
|
||||
// 检测 frpc 进程
|
||||
result.FrpcRunning = Running()
|
||||
result.FrpcPID = PID()
|
||||
|
||||
// 筛选要检测的规则
|
||||
rules := cfg.Rules
|
||||
if ruleName != "" {
|
||||
rules = nil
|
||||
for _, r := range cfg.Rules {
|
||||
if r.Name == ruleName {
|
||||
rules = append(rules, r)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 并行检测所有规则
|
||||
result.Rules = make([]RuleCheckResult, len(rules))
|
||||
var wg sync.WaitGroup
|
||||
for i, rule := range rules {
|
||||
wg.Add(1)
|
||||
go func(idx int, r config.RelayRule) {
|
||||
defer wg.Done()
|
||||
result.Rules[idx] = checkRule(r, cfg.Server)
|
||||
}(i, rule)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 统计
|
||||
result.Total = len(result.Rules)
|
||||
for _, r := range result.Rules {
|
||||
if r.LocalOK && (r.RemoteOK || r.RemotePort == 0) {
|
||||
result.Passed++
|
||||
} else {
|
||||
result.Failed++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// checkRule 检测单条规则
|
||||
func checkRule(r config.RelayRule, server string) RuleCheckResult {
|
||||
rc := RuleCheckResult{
|
||||
Name: r.Name,
|
||||
Proto: r.Proto,
|
||||
LocalPort: r.LocalPort,
|
||||
RemotePort: r.RemotePort,
|
||||
}
|
||||
|
||||
localIP := r.LocalIP
|
||||
if localIP == "" {
|
||||
localIP = "127.0.0.1"
|
||||
}
|
||||
|
||||
// 检测本地服务
|
||||
localAddr := fmt.Sprintf("%s:%d", localIP, r.LocalPort)
|
||||
conn, err := net.DialTimeout("tcp", localAddr, checkTimeout)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
rc.LocalOK = true
|
||||
} else {
|
||||
rc.LocalErr = "未监听"
|
||||
}
|
||||
|
||||
// 检测远程穿透端口
|
||||
if r.RemotePort > 0 && server != "" {
|
||||
host, _, _ := net.SplitHostPort(server)
|
||||
remoteAddr := fmt.Sprintf("%s:%d", host, r.RemotePort)
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", remoteAddr, checkTimeout)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
rc.RemoteOK = true
|
||||
rc.LatencyMS = time.Since(start).Milliseconds()
|
||||
} else {
|
||||
rc.RemoteErr = "超时"
|
||||
}
|
||||
}
|
||||
|
||||
return rc
|
||||
}
|
||||
Reference in New Issue
Block a user