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
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -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: 如何更新服务端/客户端?**
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user