Remove manual client ID and TLS CLI options
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Successful in 34s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m20s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m33s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m16s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m48s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m7s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m46s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m31s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m58s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m35s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been cancelled

This commit is contained in:
2026-03-19 19:32:57 +08:00
parent 937536e422
commit e4999abf47
14 changed files with 112 additions and 202 deletions

View File

@@ -14,8 +14,7 @@ go build -o client ./cmd/client
./server -c server.yaml # with config file ./server -c server.yaml # with config file
# Run client # Run client
./client -s <server>:7000 -t <token> -id <client-id> ./client -s <server>:7000 -t <token>
./client -s <server>:7000 -t <token> -id <client-id> -no-tls # disable TLS
# Web UI development (in web/ directory) # Web UI development (in web/ directory)
cd web && npm install && npm run dev # development server cd web && npm install && npm run dev # development server
@@ -86,7 +85,7 @@ External User → Server Port → Yamux Stream → Client → Local Service
### Configuration ### Configuration
- Server: YAML config + SQLite database for client rules and JS plugins - Server: YAML config + SQLite database for client rules and JS plugins
- Client: Command-line flags only (server address, token, client ID) - Client: Command-line flags only (server address, token)
- Default ports: 7000 (tunnel), 7500 (web console) - Default ports: 7000 (tunnel), 7500 (web console)
## API Documentation ## API Documentation

View File

@@ -14,8 +14,7 @@ go build -o client ./cmd/client
./server -c server.yaml # with config file ./server -c server.yaml # with config file
# Run client # Run client
./client -s <server>:7000 -t <token> -id <client-id> ./client -s <server>:7000 -t <token>
./client -s <server>:7000 -t <token> -id <client-id> -no-tls # disable TLS
# Web UI development (in web/ directory) # Web UI development (in web/ directory)
cd web && npm install && npm run dev # development server cd web && npm install && npm run dev # development server
@@ -86,7 +85,7 @@ External User → Server Port → Yamux Stream → Client → Local Service
### Configuration ### Configuration
- Server: YAML config + SQLite database for client rules and JS plugins - Server: YAML config + SQLite database for client rules and JS plugins
- Client: Command-line flags only (server address, token, client ID) - Client: Command-line flags only (server address, token)
- Default ports: 7000 (tunnel), 7500 (web console) - Default ports: 7000 (tunnel), 7500 (web console)
## API Documentation ## API Documentation

View File

@@ -17,7 +17,7 @@ GoTunnel 是一个类似 frp 的内网穿透解决方案,核心特点是**服
| TLS 证书 | 自动生成,零配置 | 需手动配置 | | TLS 证书 | 自动生成,零配置 | 需手动配置 |
| 管理界面 | 内置 Web 控制台 (naive-ui) | 需额外部署 Dashboard | | 管理界面 | 内置 Web 控制台 (naive-ui) | 需额外部署 Dashboard |
| 客户端部署 | 仅需 2 个参数 | 需配置文件 | | 客户端部署 | 仅需 2 个参数 | 需配置文件 |
| 客户端 ID | 可选,服务端自动分配 | 需手动配置 | | 客户端 ID | 自动根据设备标识计算 | 需手动配置 |
### 架构设计 ### 架构设计
@@ -111,14 +111,9 @@ go build -o client ./cmd/client
### 客户端启动 ### 客户端启动
```bash ```bash
# 最简启动ID 由服务端自动分配 # 最简启动ID 由客户端根据设备标识自动计算
./client -s <服务器IP>:7000 -t <Token> ./client -s <服务器IP>:7000 -t <Token>
# 指定客户端 ID
./client -s <服务器IP>:7000 -t <Token> -id <客户端ID>
# 禁用 TLS需服务端也禁用
./client -s <服务器IP>:7000 -t <Token> -no-tls
``` ```
**参数说明:** **参数说明:**
@@ -127,9 +122,6 @@ go build -o client ./cmd/client
|------|------|------| |------|------|------|
| `-s` | 服务器地址 (ip:port) | 是 | | `-s` | 服务器地址 (ip:port) | 是 |
| `-t` | 认证 Token | 是 | | `-t` | 认证 Token | 是 |
| `-id` | 客户端 ID | 否(服务端自动分配) |
| `-no-tls` | 禁用 TLS 加密 | 否 |
| `-skip-verify` | 跳过证书验证(不安全,仅测试用) | 否 |
## 配置系统 ## 配置系统
@@ -388,7 +380,7 @@ curl -X POST http://server:7500/api/clients \
-d '{"id":"home","rules":[{"name":"web","type":"tcp","local_ip":"127.0.0.1","local_port":80,"remote_port":8080}]}' -d '{"id":"home","rules":[{"name":"web","type":"tcp","local_ip":"127.0.0.1","local_port":80,"remote_port":8080}]}'
# 客户端连接 # 客户端连接
./client -s server:7000 -t <token> -id home ./client -s server:7000 -t <token>
# 访问http://server:8080 -> 内网 127.0.0.1:80 # 访问http://server:8080 -> 内网 127.0.0.1:80
``` ```
@@ -411,7 +403,7 @@ A: 在 Web 控制台点击客户端详情,进入编辑模式即可设置昵称
**Q: 如何禁用 TLS** **Q: 如何禁用 TLS**
A: 服务端配置 `tls_disabled: true`,客户端使用 `-no-tls` 参数 A: 客户端命令行默认使用 TLS如需兼容旧的非 TLS 部署,请改用客户端配置文件中的 `no_tls: true`
**Q: 端口被占用怎么办?** **Q: 端口被占用怎么办?**
@@ -419,7 +411,7 @@ A: 服务端会自动检测端口冲突,请检查日志并更换端口。
**Q: 客户端 ID 是如何分配的?** **Q: 客户端 ID 是如何分配的?**
A: 如果客户端未指定 `-id` 参数,服务端会自动生成 16 位随机 ID。 A: 客户端会把系统机器 ID、全部可用 MAC、主机名和网卡名等稳定标识组合后再进行哈希得到固定客户端 ID服务端不再为客户端分配或修正 ID。
**Q: 如何更新服务端/客户端?** **Q: 如何更新服务端/客户端?**

View File

@@ -23,8 +23,6 @@ func init() {
func main() { func main() {
server := flag.String("s", "", "server address (ip:port)") server := flag.String("s", "", "server address (ip:port)")
token := flag.String("t", "", "auth token") token := flag.String("t", "", "auth token")
id := flag.String("id", "", "client id (optional, auto-assigned if empty)")
noTLS := flag.Bool("no-tls", false, "disable TLS")
configPath := flag.String("c", "", "config file path") configPath := flag.String("c", "", "config file path")
flag.Parse() flag.Parse()
@@ -47,18 +45,12 @@ func main() {
if *token != "" { if *token != "" {
cfg.Token = *token cfg.Token = *token
} }
if *id != "" {
cfg.ID = *id
}
if *noTLS {
cfg.NoTLS = *noTLS
}
if cfg.Server == "" || cfg.Token == "" { if cfg.Server == "" || cfg.Token == "" {
log.Fatal("Usage: client [-c config.yaml] | [-s <server:port> -t <token> [-id <client_id>] [-no-tls]]") log.Fatal("Usage: client [-c config.yaml] | [-s <server:port> -t <token>]")
} }
client := tunnel.NewClient(cfg.Server, cfg.Token, cfg.ID) client := tunnel.NewClient(cfg.Server, cfg.Token)
// TLS 默认启用,默认跳过证书验证(类似 frp // TLS 默认启用,默认跳过证书验证(类似 frp
if !cfg.NoTLS { if !cfg.NoTLS {

View File

@@ -10,7 +10,6 @@ import (
type ClientConfig struct { type ClientConfig struct {
Server string `yaml:"server"` // 服务器地址 Server string `yaml:"server"` // 服务器地址
Token string `yaml:"token"` // 认证 Token Token string `yaml:"token"` // 认证 Token
ID string `yaml:"id"` // 客户端 ID
NoTLS bool `yaml:"no_tls"` // 禁用 TLS NoTLS bool `yaml:"no_tls"` // 禁用 TLS
} }

View File

@@ -48,7 +48,7 @@ type Client struct {
} }
// NewClient 创建客户端 // NewClient 创建客户端
func NewClient(serverAddr, token, id string) *Client { func NewClient(serverAddr, token string) *Client {
// 默认数据目录:优先使用用户主目录,失败时回退到当前工作目录 // 默认数据目录:优先使用用户主目录,失败时回退到当前工作目录
var dataDir string var dataDir string
if home, err := os.UserHomeDir(); err == nil && home != "" { if home, err := os.UserHomeDir(); err == nil && home != "" {
@@ -71,9 +71,7 @@ func NewClient(serverAddr, token, id string) *Client {
} }
// ID 优先级:命令行参数 > 机器ID // ID 优先级:命令行参数 > 机器ID
if id == "" { id := getMachineID()
id = getMachineID()
}
// 获取主机名作为客户端名称 // 获取主机名作为客户端名称
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
@@ -99,7 +97,6 @@ func (c *Client) InitVersionStore() error {
return nil return nil
} }
// logf 安全地记录日志(同时输出到标准日志和日志收集器) // logf 安全地记录日志(同时输出到标准日志和日志收集器)
func (c *Client) logf(format string, args ...interface{}) { func (c *Client) logf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
@@ -190,8 +187,8 @@ func (c *Client) connect() error {
// 如果服务端分配了新 ID则更新 // 如果服务端分配了新 ID则更新
if authResp.ClientID != "" && authResp.ClientID != c.ID { if authResp.ClientID != "" && authResp.ClientID != c.ID {
c.ID = authResp.ClientID conn.Close()
c.logf("ID updated to: %s", c.ID) return fmt.Errorf("server returned unexpected client id: %s", authResp.ClientID)
} }
c.logf("Authenticated as %s", c.ID) c.logf("Authenticated as %s", c.ID)
@@ -416,15 +413,6 @@ func (c *Client) findRuleByPort(port int) *protocol.ProxyRule {
return nil return nil
} }
// handleClientRestart 处理客户端重启请求 // handleClientRestart 处理客户端重启请求
func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) { func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) {
defer stream.Close() defer stream.Close()
@@ -449,8 +437,6 @@ func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) {
} }
} }
// handleUpdateDownload 处理更新下载请求 // handleUpdateDownload 处理更新下载请求
func (c *Client) handleUpdateDownload(stream net.Conn, msg *protocol.Message) { func (c *Client) handleUpdateDownload(stream net.Conn, msg *protocol.Message) {
defer stream.Close() defer stream.Close()
@@ -528,7 +514,7 @@ func (c *Client) performSelfUpdate(downloadURL string) error {
// Windows 需要特殊处理 // Windows 需要特殊处理
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return performWindowsClientUpdate(binaryPath, currentPath, c.ServerAddr, c.Token, c.ID) return performWindowsClientUpdate(binaryPath, currentPath, c.ServerAddr, c.Token)
} }
// 确定目标路径 // 确定目标路径
@@ -579,7 +565,7 @@ func (c *Client) performSelfUpdate(downloadURL string) error {
c.logf("Update completed successfully, restarting...") c.logf("Update completed successfully, restarting...")
// 重启进程(从新路径启动) // 重启进程(从新路径启动)
restartClientProcess(targetPath, c.ServerAddr, c.Token, c.ID) restartClientProcess(targetPath, c.ServerAddr, c.Token)
return nil return nil
} }
@@ -608,15 +594,10 @@ func (c *Client) checkUpdatePermissions(execPath string) error {
return nil return nil
} }
// performWindowsClientUpdate Windows 平台更新 // performWindowsClientUpdate Windows 平台更新
func performWindowsClientUpdate(newFile, currentPath, serverAddr, token, id string) error { func performWindowsClientUpdate(newFile, currentPath, serverAddr, token string) error {
// 创建批处理脚本 // 创建批处理脚本
args := fmt.Sprintf(`-s "%s" -t "%s"`, serverAddr, token) args := fmt.Sprintf(`-s "%s" -t "%s"`, serverAddr, token)
if id != "" {
args += fmt.Sprintf(` -id "%s"`, id)
}
batchScript := fmt.Sprintf(`@echo off batchScript := fmt.Sprintf(`@echo off
:: Check for admin rights, request UAC elevation if needed :: Check for admin rights, request UAC elevation if needed
net session >nul 2>&1 net session >nul 2>&1
@@ -647,11 +628,8 @@ del "%%~f0"
} }
// restartClientProcess 重启客户端进程 // restartClientProcess 重启客户端进程
func restartClientProcess(path, serverAddr, token, id string) { func restartClientProcess(path, serverAddr, token string) {
args := []string{"-s", serverAddr, "-t", token} args := []string{"-s", serverAddr, "-t", token}
if id != "" {
args = append(args, "-id", id)
}
cmd := exec.Command(path, args...) cmd := exec.Command(path, args...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -660,7 +638,6 @@ func restartClientProcess(path, serverAddr, token, id string) {
os.Exit(0) os.Exit(0)
} }
// handleLogRequest 处理日志请求 // handleLogRequest 处理日志请求
func (c *Client) handleLogRequest(stream net.Conn, msg *protocol.Message) { func (c *Client) handleLogRequest(stream net.Conn, msg *protocol.Message) {
if c.logger == nil { if c.logger == nil {
@@ -737,8 +714,6 @@ func (c *Client) handleLogStop(stream net.Conn, msg *protocol.Message) {
c.logger.Unsubscribe(req.SessionID) c.logger.Unsubscribe(req.SessionID)
} }
// handleSystemStatsRequest 处理系统状态请求 // handleSystemStatsRequest 处理系统状态请求
func (c *Client) handleSystemStatsRequest(stream net.Conn, msg *protocol.Message) { func (c *Client) handleSystemStatsRequest(stream net.Conn, msg *protocol.Message) {
defer stream.Close() defer stream.Close()

View File

@@ -7,27 +7,38 @@ import (
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"sort"
"strings" "strings"
) )
// getMachineID 获取机器唯一标识 // getMachineID builds a stable fingerprint from multiple host identifiers
// 优先级系统机器ID > MAC地址哈希 // and hashes the combined result into the client ID we expose externally.
func getMachineID() string { func getMachineID() string {
// 尝试获取系统机器 ID return hashID(strings.Join(collectMachineIDParts(), "|"))
if id := getSystemMachineID(); id != "" { }
return hashID(id)
} func collectMachineIDParts() []string {
parts := []string{"os=" + runtime.GOOS, "arch=" + runtime.GOARCH}
// 备选:使用主网卡 MAC 地址
if id := getMACAddress(); id != "" { if id := getSystemMachineID(); id != "" {
return hashID(id) parts = append(parts, "system="+id)
} }
// 都失败则返回空,让服务端生成 if hostname, err := os.Hostname(); err == nil && hostname != "" {
return "" parts = append(parts, "host="+hostname)
}
if macs := getMACAddresses(); len(macs) > 0 {
parts = append(parts, "macs="+strings.Join(macs, ","))
}
if names := getInterfaceNames(); len(names) > 0 {
parts = append(parts, "ifaces="+strings.Join(names, ","))
}
return parts
} }
// getSystemMachineID 获取系统机器 ID
func getSystemMachineID() string { func getSystemMachineID() string {
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
@@ -41,20 +52,16 @@ func getSystemMachineID() string {
} }
} }
// getLinuxMachineID 获取 Linux 机器 ID
func getLinuxMachineID() string { func getLinuxMachineID() string {
// 优先读取 /etc/machine-id
if data, err := os.ReadFile("/etc/machine-id"); err == nil { if data, err := os.ReadFile("/etc/machine-id"); err == nil {
return strings.TrimSpace(string(data)) return strings.TrimSpace(string(data))
} }
// 备选 /var/lib/dbus/machine-id
if data, err := os.ReadFile("/var/lib/dbus/machine-id"); err == nil { if data, err := os.ReadFile("/var/lib/dbus/machine-id"); err == nil {
return strings.TrimSpace(string(data)) return strings.TrimSpace(string(data))
} }
return "" return ""
} }
// getDarwinMachineID 获取 macOS 机器 ID (IOPlatformUUID)
func getDarwinMachineID() string { func getDarwinMachineID() string {
cmd := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice") cmd := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice")
output, err := cmd.Output() output, err := cmd.Output()
@@ -62,72 +69,84 @@ func getDarwinMachineID() string {
return "" return ""
} }
// 解析 IOPlatformUUID for _, line := range strings.Split(string(output), "\n") {
lines := strings.Split(string(output), "\n") if !strings.Contains(line, "IOPlatformUUID") {
for _, line := range lines { continue
if strings.Contains(line, "IOPlatformUUID") {
parts := strings.Split(line, "=")
if len(parts) == 2 {
uuid := strings.TrimSpace(parts[1])
uuid = strings.Trim(uuid, "\"")
return uuid
}
} }
parts := strings.Split(line, "=")
if len(parts) != 2 {
continue
}
uuid := strings.TrimSpace(parts[1])
return strings.Trim(uuid, "\"")
} }
return "" return ""
} }
// getWindowsMachineID 获取 Windows 机器 ID
func getWindowsMachineID() string { func getWindowsMachineID() string {
cmd := exec.Command("reg", "query", cmd := exec.Command("reg", "query", `HKLM\SOFTWARE\Microsoft\Cryptography`, "/v", "MachineGuid")
`HKLM\SOFTWARE\Microsoft\Cryptography`,
"/v", "MachineGuid")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return "" return ""
} }
// 解析注册表输出 for _, line := range strings.Split(string(output), "\n") {
lines := strings.Split(string(output), "\n") if !strings.Contains(line, "MachineGuid") {
for _, line := range lines { continue
if strings.Contains(line, "MachineGuid") { }
fields := strings.Fields(line)
if len(fields) >= 3 { fields := strings.Fields(line)
return fields[len(fields)-1] if len(fields) >= 3 {
} return fields[len(fields)-1]
} }
} }
return "" return ""
} }
// getMACAddress 获取主网卡 MAC 地址 func getMACAddresses() []string {
func getMACAddress() string {
interfaces, err := net.Interfaces() interfaces, err := net.Interfaces()
if err != nil { if err != nil {
return "" return nil
} }
macs := make([]string, 0, len(interfaces))
for _, iface := range interfaces { for _, iface := range interfaces {
// 跳过回环和无效接口
if iface.Flags&net.FlagLoopback != 0 { if iface.Flags&net.FlagLoopback != 0 {
continue continue
} }
if iface.Flags&net.FlagUp == 0 {
continue
}
if len(iface.HardwareAddr) == 0 { if len(iface.HardwareAddr) == 0 {
continue continue
} }
macs = append(macs, iface.HardwareAddr.String())
// 返回第一个有效的 MAC 地址
return iface.HardwareAddr.String()
} }
return ""
sort.Strings(macs)
return macs
}
func getInterfaceNames() []string {
interfaces, err := net.Interfaces()
if err != nil {
return nil
}
names := make([]string, 0, len(interfaces))
for _, iface := range interfaces {
if iface.Flags&net.FlagLoopback != 0 {
continue
}
names = append(names, iface.Name)
}
sort.Strings(names)
return names
} }
// hashID 对 ID 进行哈希处理,生成固定长度的客户端 ID
func hashID(id string) string { func hashID(id string) string {
hash := sha256.Sum256([]byte(id)) hash := sha256.Sum256([]byte(id))
// 取前 16 个字符作为客户端 ID
return hex.EncodeToString(hash[:])[:16] return hex.EncodeToString(hash[:])[:16]
} }

View File

@@ -5,8 +5,8 @@ func (s *SQLiteStore) CreateInstallToken(token *InstallToken) error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
_, err := s.db.Exec(`INSERT INTO install_tokens (token, client_id, created_at, used) VALUES (?, ?, ?, ?)`, _, err := s.db.Exec(`INSERT INTO install_tokens (token, client_id, created_at, used) VALUES (?, '', ?, ?)`,
token.Token, token.ClientID, token.CreatedAt, 0) token.Token, token.CreatedAt, 0)
return err return err
} }
@@ -17,8 +17,8 @@ func (s *SQLiteStore) GetInstallToken(token string) (*InstallToken, error) {
var t InstallToken var t InstallToken
var used int var used int
err := s.db.QueryRow(`SELECT token, client_id, created_at, used FROM install_tokens WHERE token = ?`, token). err := s.db.QueryRow(`SELECT token, created_at, used FROM install_tokens WHERE token = ?`, token).
Scan(&t.Token, &t.ClientID, &t.CreatedAt, &used) Scan(&t.Token, &t.CreatedAt, &used)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -46,7 +46,6 @@ type TrafficStore interface {
// InstallToken 安装token // InstallToken 安装token
type InstallToken struct { type InstallToken struct {
Token string `json:"token"` Token string `json:"token"`
ClientID string `json:"client_id"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
Used bool `json:"used"` Used bool `json:"used"`
} }

View File

@@ -81,7 +81,7 @@ func (s *SQLiteStore) init() error {
_, err = s.db.Exec(` _, err = s.db.Exec(`
CREATE TABLE IF NOT EXISTS install_tokens ( CREATE TABLE IF NOT EXISTS install_tokens (
token TEXT PRIMARY KEY, token TEXT PRIMARY KEY,
client_id TEXT NOT NULL, client_id TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
used INTEGER NOT NULL DEFAULT 0 used INTEGER NOT NULL DEFAULT 0
) )

View File

@@ -21,34 +21,21 @@ func NewInstallHandler(app AppInterface) *InstallHandler {
return &InstallHandler{app: app} return &InstallHandler{app: app}
} }
// GenerateInstallCommandRequest 生成安装命令请求
type GenerateInstallCommandRequest struct {
ClientID string `json:"client_id" binding:"required"`
}
// InstallCommandResponse 安装命令响应 // InstallCommandResponse 安装命令响应
type InstallCommandResponse struct { type InstallCommandResponse struct {
Token string `json:"token"` Token string `json:"token"`
Commands map[string]string `json:"commands"` Commands map[string]string `json:"commands"`
ExpiresAt int64 `json:"expires_at"` ExpiresAt int64 `json:"expires_at"`
ServerAddr string `json:"server_addr"` ServerAddr string `json:"server_addr"`
} }
// GenerateInstallCommand 生成安装命令 // GenerateInstallCommand 生成安装命令
// @Summary 生成客户端安装命令 // @Summary 生成客户端安装命令
// @Tags install // @Tags install
// @Accept json
// @Produce json // @Produce json
// @Param body body GenerateInstallCommandRequest true "客户端ID"
// @Success 200 {object} InstallCommandResponse // @Success 200 {object} InstallCommandResponse
// @Router /api/install/generate [post] // @Router /api/install/generate [post]
func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) { func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) {
var req GenerateInstallCommandRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 生成随机token // 生成随机token
tokenBytes := make([]byte, 32) tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil { if _, err := rand.Read(tokenBytes); err != nil {
@@ -61,7 +48,6 @@ func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) {
now := time.Now().Unix() now := time.Now().Unix()
installToken := &db.InstallToken{ installToken := &db.InstallToken{
Token: token, Token: token,
ClientID: req.ClientID,
CreatedAt: now, CreatedAt: now,
Used: false, Used: false,
} }
@@ -85,18 +71,13 @@ func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) {
// 生成安装命令 // 生成安装命令
expiresAt := now + 3600 // 1小时过期 expiresAt := now + 3600 // 1小时过期
tlsFlag := ""
if h.app.GetConfig().Server.TLSDisabled {
tlsFlag = " -no-tls"
}
commands := map[string]string{ commands := map[string]string{
"linux": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s", "linux": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s",
serverAddr, token, req.ClientID, tlsFlag), serverAddr, token),
"macos": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s", "macos": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s",
serverAddr, token, req.ClientID, tlsFlag), serverAddr, token),
"windows": fmt.Sprintf("powershell -c \"irm https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1 | iex; Install-GoTunnel -Server '%s' -Token '%s' -ClientID '%s'%s\"", "windows": fmt.Sprintf("powershell -c \"irm https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1 | iex; Install-GoTunnel -Server '%s' -Token '%s'\"",
serverAddr, token, req.ClientID, tlsFlag), serverAddr, token),
} }
c.JSON(http.StatusOK, InstallCommandResponse{ c.JSON(http.StatusOK, InstallCommandResponse{

View File

@@ -1,10 +1,8 @@
package tunnel package tunnel
import ( import (
"crypto/rand"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/hex"
"fmt" "fmt"
"log" "log"
"net" "net"
@@ -38,13 +36,6 @@ func isValidClientID(id string) bool {
return clientIDRegex.MatchString(id) return clientIDRegex.MatchString(id)
} }
// generateClientID 生成随机客户端 ID
func generateClientID() string {
bytes := make([]byte, 8)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// Server 隧道服务端 // Server 隧道服务端
type Server struct { type Server struct {
clientStore db.ClientStore clientStore db.ClientStore
@@ -239,13 +230,7 @@ func (s *Server) handleConnection(conn net.Conn) {
validToken = true validToken = true
isInstallToken = true isInstallToken = true
// 验证客户端ID匹配 // 验证客户端ID匹配
if authReq.ClientID != "" && authReq.ClientID != installToken.ClientID {
security.LogInvalidClientID(clientIP, authReq.ClientID)
s.sendAuthResponse(conn, false, "client id mismatch", "")
return
}
// 使用token中的客户端ID // 使用token中的客户端ID
authReq.ClientID = installToken.ClientID
} }
} }
} }
@@ -259,9 +244,7 @@ func (s *Server) handleConnection(conn net.Conn) {
// 处理客户端 ID // 处理客户端 ID
clientID := authReq.ClientID clientID := authReq.ClientID
if clientID == "" { if clientID == "" || !isValidClientID(clientID) {
clientID = generateClientID()
} else if !isValidClientID(clientID) {
security.LogInvalidClientID(clientIP, clientID) security.LogInvalidClientID(clientIP, clientID)
s.sendAuthResponse(conn, false, "invalid client id format", "") s.sendAuthResponse(conn, false, "invalid client id format", "")
return return
@@ -757,11 +740,6 @@ func (s *Server) DisconnectClient(clientID string) error {
return cs.Session.Close() return cs.Session.Close()
} }
// startUDPListener 启动 UDP 监听 // startUDPListener 启动 UDP 监听
func (s *Server) startUDPListener(cs *ClientSession, rule *protocol.ProxyRule) { func (s *Server) startUDPListener(cs *ClientSession, rule *protocol.ProxyRule) {
if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil { if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil {
@@ -856,15 +834,6 @@ func (s *Server) sendUDPPacket(cs *ClientSession, conn *net.UDPConn, clientAddr
} }
} }
// checkHTTPBasicAuth 检查 HTTP Basic Auth // checkHTTPBasicAuth 检查 HTTP Basic Auth
// 返回 (认证成功, 已读取的数据) // 返回 (认证成功, 已读取的数据)
func (s *Server) checkHTTPBasicAuth(conn net.Conn, username, password string) (bool, []byte) { func (s *Server) checkHTTPBasicAuth(conn net.Conn, username, password string) (bool, []byte) {
@@ -933,8 +902,6 @@ func (s *Server) sendHTTPUnauthorized(conn net.Conn) {
conn.Write([]byte(response)) conn.Write([]byte(response))
} }
// shouldPushToClient 检查是否应推送到指定客户端 // shouldPushToClient 检查是否应推送到指定客户端
func (s *Server) shouldPushToClient(autoPush []string, clientID string) bool { func (s *Server) shouldPushToClient(autoPush []string, clientID string) bool {
if len(autoPush) == 0 { if len(autoPush) == 0 {
@@ -980,11 +947,6 @@ func (s *Server) RestartClient(clientID string) error {
return nil return nil
} }
// IsPortAvailable 检查端口是否可用 // IsPortAvailable 检查端口是否可用
func (s *Server) IsPortAvailable(port int, excludeClientID string) bool { func (s *Server) IsPortAvailable(port int, excludeClientID string) bool {
// 检查系统端口 // 检查系统端口
@@ -1008,11 +970,6 @@ func (s *Server) IsPortAvailable(port int, excludeClientID string) bool {
return true return true
} }
// SendUpdateToClient 发送更新命令到客户端 // SendUpdateToClient 发送更新命令到客户端
func (s *Server) SendUpdateToClient(clientID, downloadURL string) error { func (s *Server) SendUpdateToClient(clientID, downloadURL string) error {
s.mu.RLock() s.mu.RLock()

View File

@@ -182,5 +182,5 @@ export const getServerConfig = () => get<ServerConfigResponse>('/config')
export const updateServerConfig = (config: UpdateServerConfigRequest) => put('/config', config) export const updateServerConfig = (config: UpdateServerConfigRequest) => put('/config', config)
// 安装命令生成 // 安装命令生成
export const generateInstallCommand = (clientId: string) => export const generateInstallCommand = () =>
post<InstallCommandResponse>('/install/generate', { client_id: clientId }) post<InstallCommandResponse>('/install/generate')

View File

@@ -9,7 +9,6 @@ const clients = ref<ClientStatus[]>([])
const loading = ref(true) const loading = ref(true)
const showInstallModal = ref(false) const showInstallModal = ref(false)
const installData = ref<InstallCommandResponse | null>(null) const installData = ref<InstallCommandResponse | null>(null)
const installClientId = ref('')
const generatingInstall = ref(false) const generatingInstall = ref(false)
const loadClients = async () => { const loadClients = async () => {
@@ -31,10 +30,9 @@ const viewClient = (id: string) => {
} }
const openInstallModal = async () => { const openInstallModal = async () => {
installClientId.value = `client-${Date.now()}`
generatingInstall.value = true generatingInstall.value = true
try { try {
const { data } = await generateInstallCommand(installClientId.value) const { data } = await generateInstallCommand()
installData.value = data installData.value = data
showInstallModal.value = true showInstallModal.value = true
} catch (e) { } catch (e) {
@@ -164,7 +162,7 @@ onMounted(loadClients)
<button class="copy-btn" @click="copyCommand(installData.commands.windows)">复制</button> <button class="copy-btn" @click="copyCommand(installData.commands.windows)">复制</button>
</div> </div>
</div> </div>
<p class="token-info">客户端ID: <strong>{{ installClientId }}</strong></p> <p class="token-info">客户端 ID 会在目标机器上根据多种设备标识自动计算</p>
<p class="token-warning"> 此命令包含一次性token使用后需重新生成</p> <p class="token-warning"> 此命令包含一次性token使用后需重新生成</p>
</div> </div>
</div> </div>