From 0a41e107935fe1ca4716e26b059928a432960181 Mon Sep 17 00:00:00 2001 From: Flik Date: Tue, 27 Jan 2026 08:56:05 +0800 Subject: [PATCH] feat(client, server): add client name handling and machine ID retrieval --- go.mod | 2 +- go.sum | 8 +- internal/client/tunnel/client.go | 36 ++--- internal/client/tunnel/machine_id.go | 133 +++++++++++++++++++ internal/server/router/handler/client.go | 16 ++- internal/server/router/handler/interfaces.go | 4 +- internal/server/router/handler/js_plugin.go | 3 +- internal/server/router/handler/log.go | 3 +- internal/server/router/handler/plugin.go | 3 +- internal/server/router/handler/plugin_api.go | 3 +- internal/server/router/handler/store.go | 3 +- internal/server/tunnel/server.go | 36 ++++- pkg/protocol/message.go | 1 + 13 files changed, 196 insertions(+), 55 deletions(-) create mode 100644 internal/client/tunnel/machine_id.go diff --git a/go.mod b/go.mod index bceaa1e..21ccadd 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.58.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/shoenig/go-m1cpu v0.1.7 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/go.sum b/go.sum index c657b50..3495c8c 100644 --- a/go.sum +++ b/go.sum @@ -122,10 +122,10 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0= +github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= +github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= +github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/internal/client/tunnel/client.go b/internal/client/tunnel/client.go index f3437a8..839ed7e 100644 --- a/internal/client/tunnel/client.go +++ b/internal/client/tunnel/client.go @@ -32,7 +32,6 @@ const ( reconnectDelay = 5 * time.Second disconnectDelay = 3 * time.Second udpBufferSize = 65535 - idFileName = "id" ) // Client 隧道客户端 @@ -40,6 +39,7 @@ type Client struct { ServerAddr string Token string ID string + Name string // 客户端名称(主机名) TLSEnabled bool TLSConfig *tls.Config DataDir string // 数据目录 @@ -64,10 +64,14 @@ func NewClient(serverAddr, token, id string) *Client { log.Printf("Failed to create data dir: %v", err) } + // ID 优先级:命令行参数 > 机器ID if id == "" { - id = loadClientID(dataDir) + id = getMachineID() } + // 获取主机名作为客户端名称 + hostname, _ := os.Hostname() + // 初始化日志收集器 logger, err := NewLogger(dataDir) if err != nil { @@ -78,6 +82,7 @@ func NewClient(serverAddr, token, id string) *Client { ServerAddr: serverAddr, Token: token, ID: id, + Name: hostname, DataDir: dataDir, runningPlugins: make(map[string]plugin.ClientPlugin), logger: logger, @@ -94,27 +99,6 @@ func (c *Client) InitVersionStore() error { return nil } -// getIDFilePath 获取 ID 文件路径 -func getIDFilePath(dataDir string) string { - return filepath.Join(dataDir, idFileName) -} - -// loadClientID 从本地文件加载客户端 ID -func loadClientID(dataDir string) string { - data, err := os.ReadFile(getIDFilePath(dataDir)) - if err != nil { - return "" - } - return string(data) -} - -// saveClientID 保存客户端 ID 到本地文件 -func saveClientID(dataDir, id string) { - if err := os.WriteFile(getIDFilePath(dataDir), []byte(id), 0600); err != nil { - log.Printf("Failed to save client ID: %v", err) - } -} - // SetPluginRegistry 设置插件注册表 func (c *Client) SetPluginRegistry(registry *plugin.Registry) { c.pluginRegistry = registry @@ -181,6 +165,7 @@ func (c *Client) connect() error { authReq := protocol.AuthRequest{ ClientID: c.ID, Token: c.Token, + Name: c.Name, OS: runtime.GOOS, Arch: runtime.GOARCH, Version: version.Version, @@ -207,11 +192,10 @@ func (c *Client) connect() error { return fmt.Errorf("auth failed: %s", authResp.Message) } - // 如果服务端分配了新 ID,则更新并保存 + // 如果服务端分配了新 ID,则更新 if authResp.ClientID != "" && authResp.ClientID != c.ID { c.ID = authResp.ClientID - saveClientID(c.DataDir, c.ID) - c.logf("New ID assigned and saved: %s", c.ID) + c.logf("ID updated to: %s", c.ID) } c.logf("Authenticated as %s", c.ID) diff --git a/internal/client/tunnel/machine_id.go b/internal/client/tunnel/machine_id.go new file mode 100644 index 0000000..9beb5a8 --- /dev/null +++ b/internal/client/tunnel/machine_id.go @@ -0,0 +1,133 @@ +package tunnel + +import ( + "crypto/sha256" + "encoding/hex" + "net" + "os" + "os/exec" + "runtime" + "strings" +) + +// getMachineID 获取机器唯一标识 +// 优先级:系统机器ID > MAC地址哈希 +func getMachineID() string { + // 尝试获取系统机器 ID + if id := getSystemMachineID(); id != "" { + return hashID(id) + } + + // 备选:使用主网卡 MAC 地址 + if id := getMACAddress(); id != "" { + return hashID(id) + } + + // 都失败则返回空,让服务端生成 + return "" +} + +// getSystemMachineID 获取系统机器 ID +func getSystemMachineID() string { + switch runtime.GOOS { + case "linux": + return getLinuxMachineID() + case "darwin": + return getDarwinMachineID() + case "windows": + return getWindowsMachineID() + default: + return "" + } +} + +// getLinuxMachineID 获取 Linux 机器 ID +func getLinuxMachineID() string { + // 优先读取 /etc/machine-id + if data, err := os.ReadFile("/etc/machine-id"); err == nil { + return strings.TrimSpace(string(data)) + } + // 备选 /var/lib/dbus/machine-id + if data, err := os.ReadFile("/var/lib/dbus/machine-id"); err == nil { + return strings.TrimSpace(string(data)) + } + return "" +} + +// getDarwinMachineID 获取 macOS 机器 ID (IOPlatformUUID) +func getDarwinMachineID() string { + cmd := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice") + output, err := cmd.Output() + if err != nil { + return "" + } + + // 解析 IOPlatformUUID + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "IOPlatformUUID") { + parts := strings.Split(line, "=") + if len(parts) == 2 { + uuid := strings.TrimSpace(parts[1]) + uuid = strings.Trim(uuid, "\"") + return uuid + } + } + } + return "" +} + +// getWindowsMachineID 获取 Windows 机器 ID +func getWindowsMachineID() string { + cmd := exec.Command("reg", "query", + `HKLM\SOFTWARE\Microsoft\Cryptography`, + "/v", "MachineGuid") + output, err := cmd.Output() + if err != nil { + return "" + } + + // 解析注册表输出 + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "MachineGuid") { + fields := strings.Fields(line) + if len(fields) >= 3 { + return fields[len(fields)-1] + } + } + } + return "" +} + +// getMACAddress 获取主网卡 MAC 地址 +func getMACAddress() string { + interfaces, err := net.Interfaces() + if err != nil { + return "" + } + + for _, iface := range interfaces { + // 跳过回环和无效接口 + if iface.Flags&net.FlagLoopback != 0 { + continue + } + if iface.Flags&net.FlagUp == 0 { + continue + } + if len(iface.HardwareAddr) == 0 { + continue + } + + // 返回第一个有效的 MAC 地址 + return iface.HardwareAddr.String() + } + return "" +} + +// hashID 对 ID 进行哈希处理,生成固定长度的客户端 ID +func hashID(id string) string { + hash := sha256.Sum256([]byte(id)) + // 取前 16 个字符作为客户端 ID + return hex.EncodeToString(hash[:])[:16] +} diff --git a/internal/server/router/handler/client.go b/internal/server/router/handler/client.go index 1f9b805..cfef506 100644 --- a/internal/server/router/handler/client.go +++ b/internal/server/router/handler/client.go @@ -115,7 +115,7 @@ func (h *ClientHandler) Get(c *gin.Context) { return } - online, lastPing, remoteAddr, clientOS, clientArch, clientVersion := h.app.GetServer().GetClientStatus(clientID) + online, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion := h.app.GetServer().GetClientStatus(clientID) // 复制插件列表 plugins := make([]db.ClientPlugin, len(client.Plugins)) @@ -145,9 +145,15 @@ func (h *ClientHandler) Get(c *gin.Context) { } } + // 如果客户端在线且有名称,优先使用在线名称 + nickname := client.Nickname + if online && clientName != "" && nickname == "" { + nickname = clientName + } + resp := dto.ClientResponse{ ID: client.ID, - Nickname: client.Nickname, + Nickname: nickname, Rules: client.Rules, Plugins: plugins, Online: online, @@ -242,8 +248,7 @@ func (h *ClientHandler) Delete(c *gin.Context) { func (h *ClientHandler) PushConfig(c *gin.Context) { clientID := c.Param("id") - online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) - if !online { + if !h.app.GetServer().IsClientOnline(clientID) { ClientNotOnline(c) return } @@ -311,8 +316,7 @@ func (h *ClientHandler) Restart(c *gin.Context) { func (h *ClientHandler) InstallPlugins(c *gin.Context) { clientID := c.Param("id") - online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) - if !online { + if !h.app.GetServer().IsClientOnline(clientID) { ClientNotOnline(c) return } diff --git a/internal/server/router/handler/interfaces.go b/internal/server/router/handler/interfaces.go index f3b78c3..38b7614 100644 --- a/internal/server/router/handler/interfaces.go +++ b/internal/server/router/handler/interfaces.go @@ -19,11 +19,13 @@ type AppInterface interface { // ServerInterface 服务端接口 type ServerInterface interface { - GetClientStatus(clientID string) (online bool, lastPing, remoteAddr, clientOS, clientArch, clientVersion string) + IsClientOnline(clientID string) bool + GetClientStatus(clientID string) (online bool, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion string) GetAllClientStatus() map[string]struct { Online bool LastPing string RemoteAddr string + Name string OS string Arch string Version string diff --git a/internal/server/router/handler/js_plugin.go b/internal/server/router/handler/js_plugin.go index 331aa47..7082740 100644 --- a/internal/server/router/handler/js_plugin.go +++ b/internal/server/router/handler/js_plugin.go @@ -177,8 +177,7 @@ func (h *JSPluginHandler) PushToClient(c *gin.Context) { c.ShouldBindJSON(&pushReq) // 忽略错误,允许空请求体 // 检查客户端是否在线 - online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) - if !online { + if !h.app.GetServer().IsClientOnline(clientID) { ClientNotOnline(c) return } diff --git a/internal/server/router/handler/log.go b/internal/server/router/handler/log.go index 258d32f..fe1f394 100644 --- a/internal/server/router/handler/log.go +++ b/internal/server/router/handler/log.go @@ -35,8 +35,7 @@ func (h *LogHandler) StreamLogs(c *gin.Context) { clientID := c.Param("id") // 检查客户端是否在线 - online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) - if !online { + if !h.app.GetServer().IsClientOnline(clientID) { c.JSON(400, gin.H{"code": 400, "message": "client not online"}) return } diff --git a/internal/server/router/handler/plugin.go b/internal/server/router/handler/plugin.go index f10e28e..4cacffd 100644 --- a/internal/server/router/handler/plugin.go +++ b/internal/server/router/handler/plugin.go @@ -371,8 +371,7 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) { } // 如果客户端在线,同步配置 - online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) - if online { + if h.app.GetServer().IsClientOnline(clientID) { if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil { PartialSuccess(c, gin.H{"status": "partial", "port_changed": portChanged}, "config saved but sync failed: "+err.Error()) return diff --git a/internal/server/router/handler/plugin_api.go b/internal/server/router/handler/plugin_api.go index 0d4e73a..88447f4 100644 --- a/internal/server/router/handler/plugin_api.go +++ b/internal/server/router/handler/plugin_api.go @@ -45,8 +45,7 @@ func (h *PluginAPIHandler) ProxyRequest(c *gin.Context) { } // 检查客户端是否在线 - online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) - if !online { + if !h.app.GetServer().IsClientOnline(clientID) { ClientNotOnline(c) return } diff --git a/internal/server/router/handler/store.go b/internal/server/router/handler/store.go index 6a2f324..6247b13 100644 --- a/internal/server/router/handler/store.go +++ b/internal/server/router/handler/store.go @@ -82,8 +82,7 @@ func (h *StoreHandler) Install(c *gin.Context) { } // 检查客户端是否在线 - online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(req.ClientID) - if !online { + if !h.app.GetServer().IsClientOnline(req.ClientID) { ClientNotOnline(c) return } diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go index 827c055..f1c0bd6 100644 --- a/internal/server/tunnel/server.go +++ b/internal/server/tunnel/server.go @@ -83,6 +83,7 @@ type JSPluginEntry struct { // ClientSession 客户端会话 type ClientSession struct { ID string + Name string // 客户端名称(主机名) RemoteAddr string // 客户端 IP 地址 OS string // 客户端操作系统 Arch string // 客户端架构 @@ -269,13 +270,21 @@ func (s *Server) handleConnection(conn net.Conn) { // 检查客户端是否存在,不存在则自动创建 exists, err := s.clientStore.ClientExists(clientID) if err != nil || !exists { - newClient := &db.Client{ID: clientID, Rules: []protocol.ProxyRule{}} + newClient := &db.Client{ID: clientID, Nickname: authReq.Name, Rules: []protocol.ProxyRule{}} if err := s.clientStore.CreateClient(newClient); err != nil { log.Printf("[Server] Create client error: %v", err) s.sendAuthResponse(conn, false, "failed to create client", "") return } - log.Printf("[Server] New client registered: %s", clientID) + log.Printf("[Server] New client registered: %s (%s)", clientID, authReq.Name) + } else if authReq.Name != "" { + // 客户端已存在,更新名称(如果提供了新名称) + if client, err := s.clientStore.GetClient(clientID); err == nil { + if client.Nickname == "" || client.Nickname != authReq.Name { + client.Nickname = authReq.Name + s.clientStore.UpdateClient(client) + } + } } rules, _ := s.clientStore.GetClientRules(clientID) @@ -290,11 +299,11 @@ func (s *Server) handleConnection(conn net.Conn) { } security.LogAuthSuccess(clientIP, clientID) - s.setupClientSession(conn, clientID, authReq.OS, authReq.Arch, authReq.Version, rules) + s.setupClientSession(conn, clientID, authReq.Name, authReq.OS, authReq.Arch, authReq.Version, rules) } // setupClientSession 建立客户端会话 -func (s *Server) setupClientSession(conn net.Conn, clientID, clientOS, clientArch, clientVersion string, rules []protocol.ProxyRule) { +func (s *Server) setupClientSession(conn net.Conn, clientID, clientName, clientOS, clientArch, clientVersion string, rules []protocol.ProxyRule) { session, err := yamux.Server(conn, nil) if err != nil { log.Printf("[Server] Yamux error: %v", err) @@ -309,6 +318,7 @@ func (s *Server) setupClientSession(conn net.Conn, clientID, clientOS, clientArc cs := &ClientSession{ ID: clientID, + Name: clientName, RemoteAddr: remoteAddr, OS: clientOS, Arch: clientArch, @@ -586,16 +596,24 @@ func (s *Server) sendHeartbeat(cs *ClientSession) bool { } // GetClientStatus 获取客户端状态 -func (s *Server) GetClientStatus(clientID string) (online bool, lastPing, remoteAddr, clientOS, clientArch, clientVersion string) { +func (s *Server) GetClientStatus(clientID string) (online bool, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion string) { s.mu.RLock() defer s.mu.RUnlock() if cs, ok := s.clients[clientID]; ok { cs.mu.Lock() defer cs.mu.Unlock() - return true, cs.LastPing.Format(time.RFC3339), cs.RemoteAddr, cs.OS, cs.Arch, cs.Version + return true, cs.LastPing.Format(time.RFC3339), cs.RemoteAddr, cs.Name, cs.OS, cs.Arch, cs.Version } - return false, "", "", "", "", "" + return false, "", "", "", "", "", "" +} + +// IsClientOnline 检查客户端是否在线 +func (s *Server) IsClientOnline(clientID string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + _, ok := s.clients[clientID] + return ok } // GetClientPluginStatus 获取客户端插件运行状态 @@ -646,6 +664,7 @@ func (s *Server) GetAllClientStatus() map[string]struct { Online bool LastPing string RemoteAddr string + Name string OS string Arch string Version string @@ -662,6 +681,7 @@ func (s *Server) GetAllClientStatus() map[string]struct { Online bool LastPing string RemoteAddr string + Name string OS string Arch string Version string @@ -673,6 +693,7 @@ func (s *Server) GetAllClientStatus() map[string]struct { Online bool LastPing string RemoteAddr string + Name string OS string Arch string Version string @@ -680,6 +701,7 @@ func (s *Server) GetAllClientStatus() map[string]struct { Online: true, LastPing: cs.LastPing.Format(time.RFC3339), RemoteAddr: cs.RemoteAddr, + Name: cs.Name, OS: cs.OS, Arch: cs.Arch, Version: cs.Version, diff --git a/pkg/protocol/message.go b/pkg/protocol/message.go index a262ec2..3bd02b5 100644 --- a/pkg/protocol/message.go +++ b/pkg/protocol/message.go @@ -87,6 +87,7 @@ type Message struct { type AuthRequest struct { ClientID string `json:"client_id"` Token string `json:"token"` + Name string `json:"name,omitempty"` // 客户端名称(主机名) OS string `json:"os,omitempty"` // 客户端操作系统 Arch string `json:"arch,omitempty"` // 客户端架构 Version string `json:"version,omitempty"` // 客户端版本