diff --git a/internal/server/router/handler/client.go b/internal/server/router/handler/client.go index 0ed1916..e2419df 100644 --- a/internal/server/router/handler/client.go +++ b/internal/server/router/handler/client.go @@ -5,8 +5,8 @@ import ( "github.com/gin-gonic/gin" "github.com/gotunnel/internal/server/db" - // removed router import "github.com/gotunnel/internal/server/router/dto" + "github.com/gotunnel/pkg/protocol" ) // ClientHandler 客户端处理器 @@ -410,10 +410,14 @@ func (h *ClientHandler) deleteClientPlugin(clientID, pluginID string) error { } var newPlugins []db.ClientPlugin + var pluginName string + var pluginPort int found := false for _, p := range client.Plugins { if p.ID == pluginID { found = true + pluginName = p.Name + pluginPort = p.RemotePort continue } newPlugins = append(newPlugins, p) @@ -423,7 +427,22 @@ func (h *ClientHandler) deleteClientPlugin(clientID, pluginID string) error { return fmt.Errorf("plugin %s not found", pluginID) } + // 删除插件管理的代理规则 + var newRules []protocol.ProxyRule + for _, r := range client.Rules { + if r.PluginManaged && r.Name == pluginName { + continue // 跳过此插件的规则 + } + newRules = append(newRules, r) + } + + // 停止端口监听器 + if pluginPort > 0 { + h.app.GetServer().StopPluginRule(clientID, pluginPort) + } + client.Plugins = newPlugins + client.Rules = newRules return h.app.GetClientStore().UpdateClient(client) } diff --git a/internal/server/router/handler/interfaces.go b/internal/server/router/handler/interfaces.go index 7250fb1..ed62c86 100644 --- a/internal/server/router/handler/interfaces.go +++ b/internal/server/router/handler/interfaces.go @@ -49,6 +49,9 @@ type ServerInterface interface { GetClientPluginStatus(clientID string) ([]protocol.PluginStatusEntry, error) // 插件规则管理 StartPluginRule(clientID string, rule protocol.ProxyRule) error + StopPluginRule(clientID string, remotePort int) error + // 端口检查 + IsPortAvailable(port int, excludeClientID string) bool // 插件 API 代理 ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error) } diff --git a/internal/server/router/handler/plugin.go b/internal/server/router/handler/plugin.go index cf9f7a7..d9cb78f 100644 --- a/internal/server/router/handler/plugin.go +++ b/internal/server/router/handler/plugin.go @@ -254,13 +254,19 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) { // 更新插件配置 found := false portChanged := false + var oldPort, newPort int for i, p := range client.Plugins { if p.Name == pluginName { + oldPort = client.Plugins[i].RemotePort // 提取 remote_port 并单独处理 if portStr, ok := req.Config["remote_port"]; ok { - var newPort int fmt.Sscanf(portStr, "%d", &newPort) - if newPort > 0 && newPort != client.Plugins[i].RemotePort { + if newPort > 0 && newPort != oldPort { + // 检查新端口是否可用 + if !h.app.GetServer().IsPortAvailable(newPort, clientID) { + BadRequest(c, fmt.Sprintf("port %d is already in use", newPort)) + return + } client.Plugins[i].RemotePort = newPort portChanged = true } @@ -277,6 +283,20 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) { return } + // 如果端口变更,同步更新代理规则 + if portChanged { + for i, r := range client.Rules { + if r.Name == pluginName && r.PluginManaged { + client.Rules[i].RemotePort = newPort + break + } + } + // 停止旧端口监听器 + if oldPort > 0 { + h.app.GetServer().StopPluginRule(clientID, oldPort) + } + } + // 保存到数据库 if err := h.app.GetClientStore().UpdateClient(client); err != nil { InternalError(c, err.Error()) @@ -287,7 +307,6 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) { online, _, _ := h.app.GetServer().GetClientStatus(clientID) if online { 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/store.go b/internal/server/router/handler/store.go index 966c881..34cf9f6 100644 --- a/internal/server/router/handler/store.go +++ b/internal/server/router/handler/store.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "io" "net/http" "time" @@ -216,6 +217,11 @@ func (h *StoreHandler) Install(c *gin.Context) { // 自动创建代理规则(如果指定了端口) if req.RemotePort > 0 { + // 检查端口是否可用 + if !h.app.GetServer().IsPortAvailable(req.RemotePort, req.ClientID) { + InternalError(c, fmt.Sprintf("port %d is already in use", req.RemotePort)) + return + } ruleExists := false for i, r := range dbClient.Rules { if r.Name == req.PluginName { @@ -226,6 +232,7 @@ func (h *StoreHandler) Install(c *gin.Context) { dbClient.Rules[i].AuthEnabled = req.AuthEnabled dbClient.Rules[i].AuthUsername = req.AuthUsername dbClient.Rules[i].AuthPassword = req.AuthPassword + dbClient.Rules[i].PluginManaged = true ruleExists = true break } @@ -233,13 +240,14 @@ func (h *StoreHandler) Install(c *gin.Context) { if !ruleExists { // 创建新规则 dbClient.Rules = append(dbClient.Rules, protocol.ProxyRule{ - Name: req.PluginName, - Type: req.PluginName, - RemotePort: req.RemotePort, - Enabled: boolPtr(true), - AuthEnabled: req.AuthEnabled, - AuthUsername: req.AuthUsername, - AuthPassword: req.AuthPassword, + Name: req.PluginName, + Type: req.PluginName, + RemotePort: req.RemotePort, + Enabled: boolPtr(true), + AuthEnabled: req.AuthEnabled, + AuthUsername: req.AuthUsername, + AuthPassword: req.AuthPassword, + PluginManaged: true, }) } } diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go index 42a104a..56a189f 100644 --- a/internal/server/tunnel/server.go +++ b/internal/server/tunnel/server.go @@ -1464,6 +1464,50 @@ func (s *Server) StartPluginRule(clientID string, rule protocol.ProxyRule) error return nil } +// StopPluginRule 停止客户端插件的服务端监听器 +func (s *Server) StopPluginRule(clientID string, remotePort int) error { + s.mu.RLock() + cs, ok := s.clients[clientID] + s.mu.RUnlock() + + if !ok { + return nil // 客户端不在线,无需停止 + } + + cs.mu.Lock() + if ln, exists := cs.Listeners[remotePort]; exists { + ln.Close() + delete(cs.Listeners, remotePort) + } + cs.mu.Unlock() + + s.portManager.Release(remotePort) + return nil +} + +// IsPortAvailable 检查端口是否可用 +func (s *Server) IsPortAvailable(port int, excludeClientID string) bool { + // 检查系统端口 + if !utils.IsPortAvailable(port) { + return false + } + // 检查是否被其他客户端占用 + s.mu.RLock() + defer s.mu.RUnlock() + for clientID, cs := range s.clients { + if clientID == excludeClientID { + continue + } + cs.mu.Lock() + _, occupied := cs.Listeners[port] + cs.mu.Unlock() + if occupied { + return false + } + } + return true +} + // ProxyPluginAPIRequest 代理插件 API 请求到客户端 func (s *Server) ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error) { s.mu.RLock() diff --git a/pkg/protocol/message.go b/pkg/protocol/message.go index f1a6599..02bd91c 100644 --- a/pkg/protocol/message.go +++ b/pkg/protocol/message.go @@ -108,6 +108,8 @@ type ProxyRule struct { AuthEnabled bool `json:"auth_enabled,omitempty" yaml:"auth_enabled"` AuthUsername string `json:"auth_username,omitempty" yaml:"auth_username"` AuthPassword string `json:"auth_password,omitempty" yaml:"auth_password"` + // 插件管理标记 - 由插件自动创建的规则,不允许手动编辑/删除 + PluginManaged bool `json:"plugin_managed,omitempty" yaml:"plugin_managed"` } // IsEnabled 检查规则是否启用,默认为 true diff --git a/web/src/types/index.ts b/web/src/types/index.ts index bbcde33..57ccb0e 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -7,6 +7,7 @@ export interface ProxyRule { type?: string enabled?: boolean plugin_config?: Record + plugin_managed?: boolean // 插件管理标记 - 由插件自动创建的规则 } // 客户端已安装的插件 diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue index 329cf1e..a494af8 100644 --- a/web/src/views/ClientView.vue +++ b/web/src/views/ClientView.vue @@ -167,11 +167,14 @@ const saveRename = async () => { } const startEdit = () => { - editRules.value = rules.value.map(rule => ({ - ...rule, - type: rule.type || 'tcp', - enabled: rule.enabled !== false - })) + // 只编辑非插件管理的规则 + editRules.value = rules.value + .filter(rule => !rule.plugin_managed) + .map(rule => ({ + ...rule, + type: rule.type || 'tcp', + enabled: rule.enabled !== false + })) editing.value = true } @@ -191,7 +194,10 @@ const removeRule = (index: number) => { const saveEdit = async () => { try { - await updateClient(clientId, { id: clientId, nickname: nickname.value, rules: editRules.value }) + // 合并插件管理的规则和编辑后的规则 + const pluginManagedRules = rules.value.filter(r => r.plugin_managed) + const allRules = [...pluginManagedRules, ...editRules.value] + await updateClient(clientId, { id: clientId, nickname: nickname.value, rules: allRules }) editing.value = false message.success('保存成功') await loadClient() @@ -476,6 +482,7 @@ const handleDeletePlugin = (plugin: ClientPlugin) => { 远程端口 类型 状态 + 来源 @@ -494,6 +501,10 @@ const handleDeletePlugin = (plugin: ClientPlugin) => { {{ rule.enabled !== false ? '启用' : '禁用' }} + + 插件 + 手动 +