Skip to content

Commit d976ee2

Browse files
committed
feat: relay 模式 + SSH 远程部署 + 官网移动端适配 + 国内镜像源
- 新增 relay 模式: frp 中继引擎,TCP/UDP 全协议穿透 - 新增 relay server setup: SSH 远程安装 frps 服务端 - 新增 sshutil 包: 纯 Go SSH 连接/远程执行 - 官网: 移动端响应式适配 (480/768/900px 三断点) - 官网: 修复 Star 按钮样式,特性卡片 11→12 - install.sh: 国内镜像源加速 (ghfast/gh-proxy/ghproxy) - Docker Compose + install-relay.sh 服务端部署方案
1 parent 0321983 commit d976ee2

30 files changed

Lines changed: 2977 additions & 757 deletions

README.md

Lines changed: 180 additions & 303 deletions
Large diffs are not rendered by default.

cmd/quick.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@ import (
55
"strings"
66

77
"github.com/qingchencloud/cftunnel/internal/daemon"
8+
"github.com/qingchencloud/cftunnel/internal/relay"
89
"github.com/spf13/cobra"
910
)
1011

11-
var quickAuth string
12+
var (
13+
quickAuth string
14+
quickRelay bool
15+
quickProto string
16+
)
1217

1318
func init() {
1419
quickCmd.Flags().StringVar(&quickAuth, "auth", "", "启用密码保护 (格式: 用户名:密码)")
20+
quickCmd.Flags().BoolVar(&quickRelay, "relay", false, "使用中继模式穿透(需先 relay init)")
21+
quickCmd.Flags().StringVar(&quickProto, "proto", "tcp", "中继协议 (tcp/udp),仅 --relay 时有效")
1522
rootCmd.AddCommand(quickCmd)
1623
}
1724

@@ -21,6 +28,9 @@ var quickCmd = &cobra.Command{
2128
Long: "无需 Cloudflare 账户、API Token 或域名,一条命令生成临时公网地址。\n适合临时分享、快速调试,Ctrl+C 退出后域名自动失效。",
2229
Args: cobra.ExactArgs(1),
2330
RunE: func(cmd *cobra.Command, args []string) error {
31+
if quickRelay {
32+
return relay.StartQuick(args[0], quickProto)
33+
}
2434
if quickAuth != "" {
2535
user, pass, err := parseAuth(quickAuth)
2636
if err != nil {

cmd/relay.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package cmd
2+
3+
import "github.com/spf13/cobra"
4+
5+
var relayCmd = &cobra.Command{
6+
Use: "relay",
7+
Short: "中继模式 — 自建服务器全协议穿透(TCP/UDP/HTTP/...)",
8+
Long: "通过自建中继服务器实现全协议穿透,支持 TCP、UDP、HTTP、HTTPS、STCP 等。\n需要一台公网服务器运行 frps 服务端。",
9+
}
10+
11+
func init() {
12+
rootCmd.AddCommand(relayCmd)
13+
}

cmd/relay_add.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/qingchencloud/cftunnel/internal/config"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var relayAddProto string
12+
var relayAddLocal int
13+
var relayAddRemote int
14+
var relayAddDomain string
15+
16+
func init() {
17+
relayAddCmd.Flags().StringVar(&relayAddProto, "proto", "tcp", "协议类型 (tcp/udp/http/https/stcp)")
18+
relayAddCmd.Flags().IntVar(&relayAddLocal, "local", 0, "本地端口")
19+
relayAddCmd.Flags().IntVar(&relayAddRemote, "remote", 0, "远程端口")
20+
relayAddCmd.Flags().StringVar(&relayAddDomain, "domain", "", "自定义域名(HTTP 模式用)")
21+
relayAddCmd.MarkFlagRequired("local")
22+
relayCmd.AddCommand(relayAddCmd)
23+
}
24+
25+
var relayAddCmd = &cobra.Command{
26+
Use: "add <名称>",
27+
Short: "添加中继穿透规则",
28+
Args: cobra.ExactArgs(1),
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
name := args[0]
31+
32+
cfg, err := config.Load()
33+
if err != nil {
34+
return err
35+
}
36+
if cfg.Relay.Server == "" {
37+
return fmt.Errorf("未配置中继服务器,请先执行 cftunnel relay init")
38+
}
39+
if cfg.FindRelayRule(name) != nil {
40+
return fmt.Errorf("规则 %q 已存在", name)
41+
}
42+
43+
rule := config.RelayRule{
44+
Name: name,
45+
Proto: relayAddProto,
46+
LocalPort: relayAddLocal,
47+
RemotePort: relayAddRemote,
48+
Domain: relayAddDomain,
49+
}
50+
cfg.Relay.Rules = append(cfg.Relay.Rules, rule)
51+
if err := cfg.Save(); err != nil {
52+
return err
53+
}
54+
55+
desc := fmt.Sprintf("localhost:%d", relayAddLocal)
56+
if relayAddRemote > 0 {
57+
desc += " → :" + strconv.Itoa(relayAddRemote)
58+
}
59+
fmt.Printf("✔ 规则已添加: %s (%s %s)\n", name, relayAddProto, desc)
60+
return nil
61+
},
62+
}

cmd/relay_down.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package cmd
2+
3+
import (
4+
"github.com/qingchencloud/cftunnel/internal/relay"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
func init() {
9+
relayCmd.AddCommand(relayDownCmd)
10+
}
11+
12+
var relayDownCmd = &cobra.Command{
13+
Use: "down",
14+
Short: "停止中继客户端",
15+
RunE: func(cmd *cobra.Command, args []string) error {
16+
return relay.Stop()
17+
},
18+
}

cmd/relay_init.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/qingchencloud/cftunnel/internal/config"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var relayInitServer string
11+
var relayInitToken string
12+
13+
func init() {
14+
relayInitCmd.Flags().StringVar(&relayInitServer, "server", "", "中继服务器地址 (格式: IP:端口)")
15+
relayInitCmd.Flags().StringVar(&relayInitToken, "token", "", "鉴权密钥")
16+
relayInitCmd.MarkFlagRequired("server")
17+
relayInitCmd.MarkFlagRequired("token")
18+
relayCmd.AddCommand(relayInitCmd)
19+
}
20+
21+
var relayInitCmd = &cobra.Command{
22+
Use: "init",
23+
Short: "配置中继服务器连接",
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
cfg, err := config.Load()
26+
if err != nil {
27+
return err
28+
}
29+
cfg.Relay.Server = relayInitServer
30+
cfg.Relay.Token = relayInitToken
31+
if err := cfg.Save(); err != nil {
32+
return err
33+
}
34+
fmt.Printf("✔ 中继服务器已配置: %s\n", relayInitServer)
35+
return nil
36+
},
37+
}

cmd/relay_install.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"runtime"
9+
"text/template"
10+
11+
"github.com/qingchencloud/cftunnel/internal/config"
12+
"github.com/qingchencloud/cftunnel/internal/relay"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
func init() {
17+
relayCmd.AddCommand(relayInstallCmd)
18+
relayCmd.AddCommand(relayUninstallCmd)
19+
}
20+
21+
var relayInstallCmd = &cobra.Command{
22+
Use: "install",
23+
Short: "注册中继客户端为系统服务(开机自启)",
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
cfg, err := config.Load()
26+
if err != nil {
27+
return err
28+
}
29+
if cfg.Relay.Server == "" {
30+
return fmt.Errorf("未配置中继服务器,请先执行 cftunnel relay init")
31+
}
32+
if len(cfg.Relay.Rules) == 0 {
33+
return fmt.Errorf("暂无中继规则,请先执行 cftunnel relay add")
34+
}
35+
binPath, err := relay.EnsureFrpc()
36+
if err != nil {
37+
return err
38+
}
39+
if err := relay.GenerateFrpcConfig(&cfg.Relay); err != nil {
40+
return err
41+
}
42+
switch runtime.GOOS {
43+
case "darwin":
44+
return installRelayLaunchd(binPath)
45+
case "linux":
46+
return installRelaySystemd(binPath)
47+
case "windows":
48+
return installRelayWindows(binPath)
49+
default:
50+
return fmt.Errorf("不支持的平台: %s", runtime.GOOS)
51+
}
52+
},
53+
}
54+
55+
// ==================== macOS launchd ====================
56+
57+
const relayPlistName = "com.cftunnel.frpc"
58+
59+
const relayPlistTmpl = `<?xml version="1.0" encoding="UTF-8"?>
60+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
61+
<plist version="1.0">
62+
<dict>
63+
<key>Label</key>
64+
<string>{{.Label}}</string>
65+
<key>ProgramArguments</key>
66+
<array>
67+
<string>{{.BinPath}}</string>
68+
<string>-c</string>
69+
<string>{{.ConfigPath}}</string>
70+
</array>
71+
<key>KeepAlive</key>
72+
<true/>
73+
<key>RunAtLoad</key>
74+
<true/>
75+
<key>StandardOutPath</key>
76+
<string>{{.LogPath}}</string>
77+
<key>StandardErrorPath</key>
78+
<string>{{.LogPath}}</string>
79+
</dict>
80+
</plist>
81+
`
82+
83+
func relayPlistPath() string {
84+
home, _ := os.UserHomeDir()
85+
return filepath.Join(home, "Library/LaunchAgents", relayPlistName+".plist")
86+
}
87+
88+
func installRelayLaunchd(binPath string) error {
89+
home, _ := os.UserHomeDir()
90+
data := map[string]string{
91+
"Label": relayPlistName,
92+
"BinPath": binPath,
93+
"ConfigPath": relay.FrpcConfigPath(),
94+
"LogPath": filepath.Join(home, "Library/Logs/cftunnel-relay.log"),
95+
}
96+
f, err := os.Create(relayPlistPath())
97+
if err != nil {
98+
return err
99+
}
100+
defer f.Close()
101+
if err := template.Must(template.New("").Parse(relayPlistTmpl)).Execute(f, data); err != nil {
102+
return err
103+
}
104+
if err := exec.Command("launchctl", "load", relayPlistPath()).Run(); err != nil {
105+
return fmt.Errorf("launchctl load 失败: %w", err)
106+
}
107+
fmt.Printf("✓ 已注册 launchd 服务: %s\n", relayPlistName)
108+
return nil
109+
}
110+
111+
func uninstallRelayLaunchd() error {
112+
exec.Command("launchctl", "unload", relayPlistPath()).Run()
113+
os.Remove(relayPlistPath())
114+
fmt.Printf("✓ 已卸载 launchd 服务: %s\n", relayPlistName)
115+
return nil
116+
}
117+
118+
// ==================== Linux systemd ====================
119+
120+
const relayUnitName = "cftunnel-relay"
121+
122+
func relayUnitPath() string {
123+
return "/etc/systemd/system/" + relayUnitName + ".service"
124+
}
125+
126+
func installRelaySystemd(binPath string) error {
127+
unit := fmt.Sprintf(`[Unit]
128+
Description=cftunnel relay (frpc)
129+
After=network.target
130+
131+
[Service]
132+
ExecStart=%s -c %s
133+
Restart=always
134+
RestartSec=5
135+
136+
[Install]
137+
WantedBy=multi-user.target
138+
`, binPath, relay.FrpcConfigPath())
139+
140+
if err := os.WriteFile(relayUnitPath(), []byte(unit), 0644); err != nil {
141+
return fmt.Errorf("写入 systemd unit 失败: %w", err)
142+
}
143+
if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil {
144+
return fmt.Errorf("systemctl daemon-reload 失败: %w", err)
145+
}
146+
if err := exec.Command("systemctl", "enable", "--now", relayUnitName).Run(); err != nil {
147+
return fmt.Errorf("systemctl enable 失败: %w", err)
148+
}
149+
fmt.Printf("✓ 已注册 systemd 服务: %s\n", relayUnitName)
150+
return nil
151+
}
152+
153+
func uninstallRelaySystemd() error {
154+
exec.Command("systemctl", "disable", "--now", relayUnitName).Run()
155+
os.Remove(relayUnitPath())
156+
fmt.Printf("✓ 已卸载 systemd 服务: %s\n", relayUnitName)
157+
return nil
158+
}
159+
160+
// ==================== Windows sc ====================
161+
162+
const relaySvcName = "cftunnel-relay"
163+
164+
func installRelayWindows(binPath string) error {
165+
binArg := fmt.Sprintf(`%s -c %s`, binPath, relay.FrpcConfigPath())
166+
if err := exec.Command("sc", "create", relaySvcName, "binPath=", binArg, "start=", "auto").Run(); err != nil {
167+
return fmt.Errorf("创建 Windows 服务失败: %w", err)
168+
}
169+
if err := exec.Command("sc", "start", relaySvcName).Run(); err != nil {
170+
return fmt.Errorf("启动服务失败: %w", err)
171+
}
172+
fmt.Printf("✓ 已注册 Windows 服务: %s\n", relaySvcName)
173+
return nil
174+
}
175+
176+
func uninstallRelayWindows() error {
177+
exec.Command("sc", "stop", relaySvcName).Run()
178+
exec.Command("sc", "delete", relaySvcName).Run()
179+
fmt.Printf("✓ 已卸载 Windows 服务: %s\n", relaySvcName)
180+
return nil
181+
}
182+
183+
var relayUninstallCmd = &cobra.Command{
184+
Use: "uninstall",
185+
Short: "卸载中继客户端系统服务",
186+
RunE: func(cmd *cobra.Command, args []string) error {
187+
switch runtime.GOOS {
188+
case "darwin":
189+
return uninstallRelayLaunchd()
190+
case "linux":
191+
return uninstallRelaySystemd()
192+
case "windows":
193+
return uninstallRelayWindows()
194+
default:
195+
return fmt.Errorf("不支持的平台: %s", runtime.GOOS)
196+
}
197+
},
198+
}

0 commit comments

Comments
 (0)