feat(client, server): add client name handling and machine ID retrieval
Some checks failed
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been cancelled
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
Build Multi-Platform Binaries / build-frontend (push) Has been cancelled
Some checks failed
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been cancelled
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
Build Multi-Platform Binaries / build-frontend (push) Has been cancelled
This commit is contained in:
@@ -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)
|
||||
|
||||
133
internal/client/tunnel/machine_id.go
Normal file
133
internal/client/tunnel/machine_id.go
Normal file
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user