diff --git a/cmd/relay_check.go b/cmd/relay_check.go new file mode 100644 index 0000000..56be639 --- /dev/null +++ b/cmd/relay_check.go @@ -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) +} diff --git a/internal/relay/check.go b/internal/relay/check.go new file mode 100644 index 0000000..ca11eb5 --- /dev/null +++ b/internal/relay/check.go @@ -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 +}