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
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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -233,13 +240,14 @@ func (h *StoreHandler) Install(c *gin.Context) {
|
|||||||
if !ruleExists {
|
if !ruleExists {
|
||||||
// 创建新规则
|
// 创建新规则
|
||||||
dbClient.Rules = append(dbClient.Rules, protocol.ProxyRule{
|
dbClient.Rules = append(dbClient.Rules, protocol.ProxyRule{
|
||||||
Name: req.PluginName,
|
Name: req.PluginName,
|
||||||
Type: req.PluginName,
|
Type: req.PluginName,
|
||||||
RemotePort: req.RemotePort,
|
RemotePort: req.RemotePort,
|
||||||
Enabled: boolPtr(true),
|
Enabled: boolPtr(true),
|
||||||
AuthEnabled: req.AuthEnabled,
|
AuthEnabled: req.AuthEnabled,
|
||||||
AuthUsername: req.AuthUsername,
|
AuthUsername: req.AuthUsername,
|
||||||
AuthPassword: req.AuthPassword,
|
AuthPassword: req.AuthPassword,
|
||||||
|
PluginManaged: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 // 插件管理标记 - 由插件自动创建的规则
|
||||||
}
|
}
|
||||||
|
|
||||||
// 客户端已安装的插件
|
// 客户端已安装的插件
|
||||||
|
|||||||
@@ -167,11 +167,14 @@ const saveRename = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startEdit = () => {
|
const startEdit = () => {
|
||||||
editRules.value = rules.value.map(rule => ({
|
// 只编辑非插件管理的规则
|
||||||
...rule,
|
editRules.value = rules.value
|
||||||
type: rule.type || 'tcp',
|
.filter(rule => !rule.plugin_managed)
|
||||||
enabled: rule.enabled !== false
|
.map(rule => ({
|
||||||
}))
|
...rule,
|
||||||
|
type: rule.type || 'tcp',
|
||||||
|
enabled: rule.enabled !== false
|
||||||
|
}))
|
||||||
editing.value = true
|
editing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user