1111
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Successful in 37s
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 (amd64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been cancelled

This commit is contained in:
2026-01-04 22:48:49 +08:00
parent 02f8c521c2
commit 007c8ed440
8 changed files with 124 additions and 17 deletions

View File

@@ -5,8 +5,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db" "github.com/gotunnel/internal/server/db"
// removed router import
"github.com/gotunnel/internal/server/router/dto" "github.com/gotunnel/internal/server/router/dto"
"github.com/gotunnel/pkg/protocol"
) )
// ClientHandler 客户端处理器 // ClientHandler 客户端处理器
@@ -410,10 +410,14 @@ func (h *ClientHandler) deleteClientPlugin(clientID, pluginID string) error {
} }
var newPlugins []db.ClientPlugin var newPlugins []db.ClientPlugin
var pluginName string
var pluginPort int
found := false found := false
for _, p := range client.Plugins { for _, p := range client.Plugins {
if p.ID == pluginID { if p.ID == pluginID {
found = true found = true
pluginName = p.Name
pluginPort = p.RemotePort
continue continue
} }
newPlugins = append(newPlugins, p) newPlugins = append(newPlugins, p)
@@ -423,7 +427,22 @@ func (h *ClientHandler) deleteClientPlugin(clientID, pluginID string) error {
return fmt.Errorf("plugin %s not found", pluginID) 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.Plugins = newPlugins
client.Rules = newRules
return h.app.GetClientStore().UpdateClient(client) return h.app.GetClientStore().UpdateClient(client)
} }

View File

@@ -49,6 +49,9 @@ type ServerInterface interface {
GetClientPluginStatus(clientID string) ([]protocol.PluginStatusEntry, error) GetClientPluginStatus(clientID string) ([]protocol.PluginStatusEntry, error)
// 插件规则管理 // 插件规则管理
StartPluginRule(clientID string, rule protocol.ProxyRule) error StartPluginRule(clientID string, rule protocol.ProxyRule) error
StopPluginRule(clientID string, remotePort int) error
// 端口检查
IsPortAvailable(port int, excludeClientID string) bool
// 插件 API 代理 // 插件 API 代理
ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error) ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error)
} }

View File

@@ -254,13 +254,19 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
// 更新插件配置 // 更新插件配置
found := false found := false
portChanged := false portChanged := false
var oldPort, newPort int
for i, p := range client.Plugins { for i, p := range client.Plugins {
if p.Name == pluginName { if p.Name == pluginName {
oldPort = client.Plugins[i].RemotePort
// 提取 remote_port 并单独处理 // 提取 remote_port 并单独处理
if portStr, ok := req.Config["remote_port"]; ok { if portStr, ok := req.Config["remote_port"]; ok {
var newPort int
fmt.Sscanf(portStr, "%d", &newPort) 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 client.Plugins[i].RemotePort = newPort
portChanged = true portChanged = true
} }
@@ -277,6 +283,20 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
return 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 { if err := h.app.GetClientStore().UpdateClient(client); err != nil {
InternalError(c, err.Error()) InternalError(c, err.Error())
@@ -287,7 +307,6 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
online, _, _ := h.app.GetServer().GetClientStatus(clientID) online, _, _ := h.app.GetServer().GetClientStatus(clientID)
if online { if online {
if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil { 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()) PartialSuccess(c, gin.H{"status": "partial", "port_changed": portChanged}, "config saved but sync failed: "+err.Error())
return return
} }

View File

@@ -1,6 +1,7 @@
package handler package handler
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"time" "time"
@@ -216,6 +217,11 @@ func (h *StoreHandler) Install(c *gin.Context) {
// 自动创建代理规则(如果指定了端口) // 自动创建代理规则(如果指定了端口)
if req.RemotePort > 0 { 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 ruleExists := false
for i, r := range dbClient.Rules { for i, r := range dbClient.Rules {
if r.Name == req.PluginName { 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].AuthEnabled = req.AuthEnabled
dbClient.Rules[i].AuthUsername = req.AuthUsername dbClient.Rules[i].AuthUsername = req.AuthUsername
dbClient.Rules[i].AuthPassword = req.AuthPassword dbClient.Rules[i].AuthPassword = req.AuthPassword
dbClient.Rules[i].PluginManaged = true
ruleExists = true ruleExists = true
break break
} }
@@ -240,6 +247,7 @@ func (h *StoreHandler) Install(c *gin.Context) {
AuthEnabled: req.AuthEnabled, AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername, AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword, AuthPassword: req.AuthPassword,
PluginManaged: true,
}) })
} }
} }

View File

@@ -1464,6 +1464,50 @@ func (s *Server) StartPluginRule(clientID string, rule protocol.ProxyRule) error
return nil 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 请求到客户端 // ProxyPluginAPIRequest 代理插件 API 请求到客户端
func (s *Server) ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error) { func (s *Server) ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error) {
s.mu.RLock() s.mu.RLock()

View File

@@ -108,6 +108,8 @@ type ProxyRule struct {
AuthEnabled bool `json:"auth_enabled,omitempty" yaml:"auth_enabled"` AuthEnabled bool `json:"auth_enabled,omitempty" yaml:"auth_enabled"`
AuthUsername string `json:"auth_username,omitempty" yaml:"auth_username"` AuthUsername string `json:"auth_username,omitempty" yaml:"auth_username"`
AuthPassword string `json:"auth_password,omitempty" yaml:"auth_password"` AuthPassword string `json:"auth_password,omitempty" yaml:"auth_password"`
// 插件管理标记 - 由插件自动创建的规则,不允许手动编辑/删除
PluginManaged bool `json:"plugin_managed,omitempty" yaml:"plugin_managed"`
} }
// IsEnabled 检查规则是否启用,默认为 true // IsEnabled 检查规则是否启用,默认为 true

View File

@@ -7,6 +7,7 @@ export interface ProxyRule {
type?: string type?: string
enabled?: boolean enabled?: boolean
plugin_config?: Record<string, string> plugin_config?: Record<string, string>
plugin_managed?: boolean // 插件管理标记 - 由插件自动创建的规则
} }
// 客户端已安装的插件 // 客户端已安装的插件

View File

@@ -167,7 +167,10 @@ const saveRename = async () => {
} }
const startEdit = () => { const startEdit = () => {
editRules.value = rules.value.map(rule => ({ // 只编辑非插件管理的规则
editRules.value = rules.value
.filter(rule => !rule.plugin_managed)
.map(rule => ({
...rule, ...rule,
type: rule.type || 'tcp', type: rule.type || 'tcp',
enabled: rule.enabled !== false enabled: rule.enabled !== false
@@ -191,7 +194,10 @@ const removeRule = (index: number) => {
const saveEdit = async () => { const saveEdit = async () => {
try { 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 editing.value = false
message.success('保存成功') message.success('保存成功')
await loadClient() await loadClient()
@@ -476,6 +482,7 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
<th>远程端口</th> <th>远程端口</th>
<th>类型</th> <th>类型</th>
<th>状态</th> <th>状态</th>
<th>来源</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -494,6 +501,10 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
{{ rule.enabled !== false ? '启用' : '禁用' }} {{ rule.enabled !== false ? '启用' : '禁用' }}
</n-tag> </n-tag>
</td> </td>
<td>
<n-tag v-if="rule.plugin_managed" size="small" type="info">插件</n-tag>
<n-tag v-else size="small" type="default">手动</n-tag>
</td>
</tr> </tr>
</tbody> </tbody>
</n-table> </n-table>