Skip to content

Commit 94e65b9

Browse files
committed
feat: status 增强 + diagnose 诊断 + list 增强 + 环境变量支持
- status: 合并 Cloud/Relay 统一视图,支持 --json 输出 - diagnose: Cloud 模式链路诊断(cloudflared/API/本地服务/DNS/HTTPS) - list: 同时显示 Cloud 路由和 Relay 规则,增加鉴权标识 - config: 支持 CFTUNNEL_API_TOKEN 等环境变量覆盖配置
1 parent 47e7221 commit 94e65b9

5 files changed

Lines changed: 523 additions & 20 deletions

File tree

cmd/diagnose.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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/daemon"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var diagnoseJSON bool
15+
16+
func init() {
17+
diagnoseCmd.Flags().BoolVar(&diagnoseJSON, "json", false, "JSON 格式输出")
18+
rootCmd.AddCommand(diagnoseCmd)
19+
}
20+
21+
var diagnoseCmd = &cobra.Command{
22+
Use: "diagnose",
23+
Short: "诊断 Cloud 模式链路连通性",
24+
Long: "检测 cloudflared 状态、Cloudflare API 连通性、本地服务、DNS 解析和域名可达性。",
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
cfg, err := config.Load()
27+
if err != nil {
28+
return err
29+
}
30+
31+
var routes []daemon.RouteInput
32+
for _, r := range cfg.Routes {
33+
routes = append(routes, daemon.RouteInput{
34+
Name: r.Name,
35+
Hostname: r.Hostname,
36+
Service: r.Service,
37+
})
38+
}
39+
40+
result := daemon.Diagnose(routes)
41+
42+
if diagnoseJSON {
43+
enc := json.NewEncoder(os.Stdout)
44+
enc.SetIndent("", " ")
45+
return enc.Encode(result)
46+
}
47+
48+
printDiagnose(result)
49+
return nil
50+
},
51+
}
52+
53+
func printDiagnose(r daemon.DiagnoseResult) {
54+
fmt.Println("Cloud 链路诊断")
55+
fmt.Println("==============")
56+
57+
// cloudflared 状态
58+
c := r.Cloudflared
59+
if c.Installed {
60+
fmt.Printf("cloudflared: ✓ 已安装 (%s)\n", c.Version)
61+
fmt.Printf(" 路径: %s\n", c.Path)
62+
if c.Running {
63+
fmt.Printf(" 进程: 运行中 (PID: %d)\n", c.PID)
64+
} else {
65+
fmt.Println(" 进程: 未运行")
66+
}
67+
} else {
68+
fmt.Println("cloudflared: ✗ 未安装")
69+
}
70+
71+
// API 连通性
72+
a := r.API
73+
if a.Reachable {
74+
fmt.Printf("Cloudflare API: ✓ 可达 (%dms)\n", a.LatencyMS)
75+
} else {
76+
fmt.Printf("Cloudflare API: ✗ %s\n", a.Err)
77+
}
78+
fmt.Println()
79+
80+
if len(r.Routes) == 0 {
81+
fmt.Println("暂无路由需要检测")
82+
return
83+
}
84+
85+
// 路由检测表格
86+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
87+
fmt.Fprintln(w, "路由\t域名\t本地服务\tDNS\tHTTPS")
88+
fmt.Fprintln(w, "----\t----\t--------\t---\t-----")
89+
for _, route := range r.Routes {
90+
local := "✓"
91+
if !route.LocalOK {
92+
local = "✗ " + route.LocalErr
93+
}
94+
dns := "✓"
95+
if !route.DNSOK {
96+
dns = "✗ " + route.DNSErr
97+
}
98+
https := "-"
99+
if route.DNSOK {
100+
if route.HTTPOK {
101+
https = "✓"
102+
} else {
103+
https = "✗ " + route.HTTPErr
104+
}
105+
}
106+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
107+
route.Name, route.Hostname, local, dns, https)
108+
}
109+
w.Flush()
110+
111+
fmt.Printf("\n结果: %d 条路由, %d 通 / %d 断\n", r.Total, r.Passed, r.Failed)
112+
}

cmd/list.go

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
6+
"text/tabwriter"
57

68
"github.com/qingchencloud/cftunnel/internal/config"
79
"github.com/spf13/cobra"
@@ -13,20 +15,60 @@ func init() {
1315

1416
var listCmd = &cobra.Command{
1517
Use: "list",
16-
Short: "列出所有路由",
18+
Short: "列出所有路由和规则",
1719
RunE: func(cmd *cobra.Command, args []string) error {
1820
cfg, err := config.Load()
1921
if err != nil {
2022
return err
2123
}
22-
if len(cfg.Routes) == 0 {
23-
fmt.Println("暂无路由")
24+
25+
hasCloud := len(cfg.Routes) > 0
26+
hasRelay := len(cfg.Relay.Rules) > 0
27+
28+
if !hasCloud && !hasRelay {
29+
fmt.Println("暂无路由或规则")
2430
return nil
2531
}
26-
fmt.Printf("%-12s %-30s %s\n", "名称", "域名", "服务")
27-
for _, r := range cfg.Routes {
28-
fmt.Printf("%-12s %-30s %s\n", r.Name, r.Hostname, r.Service)
32+
33+
if hasCloud {
34+
fmt.Println("Cloud 路由:")
35+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
36+
fmt.Fprintln(w, "名称\t域名\t服务\t鉴权")
37+
fmt.Fprintln(w, "----\t----\t----\t----")
38+
for _, r := range cfg.Routes {
39+
auth := "-"
40+
if r.Auth != nil {
41+
auth = "✓"
42+
}
43+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Name, r.Hostname, r.Service, auth)
44+
}
45+
w.Flush()
46+
}
47+
48+
if hasCloud && hasRelay {
49+
fmt.Println()
50+
}
51+
52+
if hasRelay {
53+
fmt.Println("Relay 规则:")
54+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
55+
fmt.Fprintln(w, "名称\t协议\t本地端口\t远程端口\t域名")
56+
fmt.Fprintln(w, "----\t----\t--------\t--------\t----")
57+
for _, r := range cfg.Relay.Rules {
58+
remote := "-"
59+
if r.RemotePort > 0 {
60+
remote = fmt.Sprintf("%d", r.RemotePort)
61+
}
62+
domain := "-"
63+
if r.Domain != "" {
64+
domain = r.Domain
65+
}
66+
fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\n",
67+
r.Name, r.Proto, r.LocalPort, remote, domain)
68+
}
69+
w.Flush()
2970
}
71+
3072
return nil
3173
},
3274
}

cmd/status.go

Lines changed: 153 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,179 @@
11
package cmd
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"os"
57

68
"github.com/qingchencloud/cftunnel/internal/config"
79
"github.com/qingchencloud/cftunnel/internal/daemon"
10+
"github.com/qingchencloud/cftunnel/internal/relay"
811
"github.com/spf13/cobra"
912
)
1013

14+
var statusJSON bool
15+
1116
func init() {
17+
statusCmd.Flags().BoolVar(&statusJSON, "json", false, "JSON 格式输出")
1218
rootCmd.AddCommand(statusCmd)
1319
}
1420

21+
// StatusOutput status 命令的结构化输出
22+
type StatusOutput struct {
23+
Cloud *CloudStatus `json:"cloud,omitempty"`
24+
Relay *RelayStatus `json:"relay,omitempty"`
25+
}
26+
27+
// CloudStatus Cloud 模式状态
28+
type CloudStatus struct {
29+
TunnelName string `json:"tunnel_name"`
30+
TunnelID string `json:"tunnel_id"`
31+
Running bool `json:"running"`
32+
PID int `json:"pid,omitempty"`
33+
Routes []RouteStatus `json:"routes"`
34+
}
35+
36+
// RouteStatus 路由状态
37+
type RouteStatus struct {
38+
Name string `json:"name"`
39+
Hostname string `json:"hostname"`
40+
Service string `json:"service"`
41+
Auth bool `json:"auth"`
42+
}
43+
44+
// RelayStatus Relay 模式状态
45+
type RelayStatus struct {
46+
Server string `json:"server"`
47+
Running bool `json:"running"`
48+
PID int `json:"pid,omitempty"`
49+
Rules []RuleStatus `json:"rules"`
50+
}
51+
52+
// RuleStatus 规则状态
53+
type RuleStatus struct {
54+
Name string `json:"name"`
55+
Proto string `json:"proto"`
56+
LocalPort int `json:"local_port"`
57+
RemotePort int `json:"remote_port,omitempty"`
58+
Domain string `json:"domain,omitempty"`
59+
}
60+
1561
var statusCmd = &cobra.Command{
1662
Use: "status",
17-
Short: "查看隧道状态",
63+
Short: "查看隧道状态(Cloud + Relay)",
1864
RunE: func(cmd *cobra.Command, args []string) error {
1965
cfg, err := config.Load()
2066
if err != nil {
2167
return err
2268
}
23-
if cfg.Tunnel.ID == "" {
24-
fmt.Println("未初始化,请运行 cftunnel init && cftunnel create <名称>")
25-
return nil
69+
70+
out := buildStatus(cfg)
71+
72+
if statusJSON {
73+
enc := json.NewEncoder(os.Stdout)
74+
enc.SetIndent("", " ")
75+
return enc.Encode(out)
2676
}
27-
fmt.Printf("隧道: %s (%s)\n", cfg.Tunnel.Name, cfg.Tunnel.ID)
28-
if daemon.Running() {
29-
fmt.Printf("状态: 运行中 (PID: %d)\n", daemon.PID())
30-
} else {
31-
fmt.Println("状态: 已停止")
77+
78+
printStatus(out)
79+
return nil
80+
},
81+
}
82+
83+
func buildStatus(cfg *config.Config) StatusOutput {
84+
var out StatusOutput
85+
86+
if cfg.Tunnel.ID != "" {
87+
cs := &CloudStatus{
88+
TunnelName: cfg.Tunnel.Name,
89+
TunnelID: cfg.Tunnel.ID,
90+
Running: daemon.Running(),
91+
}
92+
if cs.Running {
93+
cs.PID = daemon.PID()
3294
}
33-
fmt.Printf("路由: %d 条\n", len(cfg.Routes))
3495
for _, r := range cfg.Routes {
35-
fmt.Printf(" %s → %s\n", r.Hostname, r.Service)
96+
cs.Routes = append(cs.Routes, RouteStatus{
97+
Name: r.Name,
98+
Hostname: r.Hostname,
99+
Service: r.Service,
100+
Auth: r.Auth != nil,
101+
})
36102
}
37-
return nil
38-
},
103+
out.Cloud = cs
104+
}
105+
106+
if cfg.Relay.Server != "" {
107+
rs := &RelayStatus{
108+
Server: cfg.Relay.Server,
109+
Running: relay.Running(),
110+
}
111+
if rs.Running {
112+
rs.PID = relay.PID()
113+
}
114+
for _, r := range cfg.Relay.Rules {
115+
rs.Rules = append(rs.Rules, RuleStatus{
116+
Name: r.Name,
117+
Proto: r.Proto,
118+
LocalPort: r.LocalPort,
119+
RemotePort: r.RemotePort,
120+
Domain: r.Domain,
121+
})
122+
}
123+
out.Relay = rs
124+
}
125+
126+
return out
127+
}
128+
129+
func printStatus(out StatusOutput) {
130+
if out.Cloud == nil && out.Relay == nil {
131+
fmt.Println("未配置任何模式,请运行 cftunnel init 或 cftunnel relay init")
132+
return
133+
}
134+
135+
if out.Cloud != nil {
136+
cs := out.Cloud
137+
fmt.Println("Cloud 模式")
138+
fmt.Printf(" 隧道: %s (%s)\n", cs.TunnelName, cs.TunnelID)
139+
if cs.Running {
140+
fmt.Printf(" 状态: ✓ 运行中 (PID: %d)\n", cs.PID)
141+
} else {
142+
fmt.Println(" 状态: ✗ 已停止")
143+
}
144+
fmt.Printf(" 路由: %d 条\n", len(cs.Routes))
145+
for _, r := range cs.Routes {
146+
auth := ""
147+
if r.Auth {
148+
auth = " [鉴权]"
149+
}
150+
fmt.Printf(" %s → %s%s\n", r.Hostname, r.Service, auth)
151+
}
152+
}
153+
154+
if out.Cloud != nil && out.Relay != nil {
155+
fmt.Println()
156+
}
157+
158+
if out.Relay != nil {
159+
rs := out.Relay
160+
fmt.Println("Relay 模式")
161+
fmt.Printf(" 服务器: %s\n", rs.Server)
162+
if rs.Running {
163+
fmt.Printf(" 状态: ✓ 运行中 (PID: %d)\n", rs.PID)
164+
} else {
165+
fmt.Println(" 状态: ✗ 已停止")
166+
}
167+
fmt.Printf(" 规则: %d 条\n", len(rs.Rules))
168+
for _, r := range rs.Rules {
169+
remote := "-"
170+
if r.RemotePort > 0 {
171+
remote = fmt.Sprintf(":%d", r.RemotePort)
172+
}
173+
if r.Domain != "" {
174+
remote = r.Domain
175+
}
176+
fmt.Printf(" %-8s %s :%d → %s\n", r.Name, r.Proto, r.LocalPort, remote)
177+
}
178+
}
39179
}

0 commit comments

Comments
 (0)