1111
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 47s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 47s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m3s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 51s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m6s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 51s

This commit is contained in:
Flik
2025-12-29 19:23:09 +08:00
parent 4116d8934c
commit ab81e08100
16 changed files with 846 additions and 36 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/gotunnel/pkg/plugin"
"github.com/gotunnel/pkg/plugin/script"
"github.com/gotunnel/pkg/plugin/sign"
"github.com/gotunnel/pkg/protocol"
"github.com/gotunnel/pkg/relay"
"github.com/hashicorp/yamux"
@@ -35,11 +36,13 @@ type Client struct {
ID string
TLSEnabled bool
TLSConfig *tls.Config
DataDir string // 数据目录
session *yamux.Session
rules []protocol.ProxyRule
mu sync.RWMutex
pluginRegistry *plugin.Registry
runningPlugins map[string]plugin.ClientPlugin // 运行中的客户端插件
runningPlugins map[string]plugin.ClientPlugin
versionStore *PluginVersionStore
pluginMu sync.RWMutex
}
@@ -48,14 +51,30 @@ func NewClient(serverAddr, token, id string) *Client {
if id == "" {
id = loadClientID()
}
// 默认数据目录
home, _ := os.UserHomeDir()
dataDir := filepath.Join(home, ".gotunnel")
return &Client{
ServerAddr: serverAddr,
Token: token,
ID: id,
DataDir: dataDir,
runningPlugins: make(map[string]plugin.ClientPlugin),
}
}
// InitVersionStore 初始化版本存储
func (c *Client) InitVersionStore() error {
store, err := NewPluginVersionStore(c.DataDir)
if err != nil {
return err
}
c.versionStore = store
return nil
}
// getIDFilePath 获取 ID 文件路径
func getIDFilePath() string {
home, err := os.UserHomeDir()
@@ -478,6 +497,14 @@ func (c *Client) handleJSPluginInstall(stream net.Conn, msg *protocol.Message) {
log.Printf("[Client] Installing JS plugin: %s", req.PluginName)
// 验证官方签名
if err := c.verifyJSPluginSignature(req.PluginName, req.Source, req.Signature); err != nil {
log.Printf("[Client] JS plugin %s signature verification failed: %v", req.PluginName, err)
c.sendJSPluginResult(stream, req.PluginName, false, "signature verification failed: "+err.Error())
return
}
log.Printf("[Client] JS plugin %s signature verified", req.PluginName)
// 创建 JS 插件
jsPlugin, err := script.NewJSPlugin(req.PluginName, req.Source)
if err != nil {
@@ -493,6 +520,14 @@ func (c *Client) handleJSPluginInstall(stream net.Conn, msg *protocol.Message) {
log.Printf("[Client] JS plugin %s installed", req.PluginName)
c.sendJSPluginResult(stream, req.PluginName, true, "")
// 保存版本信息(防止降级攻击)
if c.versionStore != nil {
signed, _ := sign.DecodeSignedPlugin(req.Signature)
if signed != nil {
c.versionStore.SetVersion(req.PluginName, signed.Payload.Version)
}
}
// 自动启动
if req.AutoStart {
c.startJSPlugin(jsPlugin, req)
@@ -530,3 +565,58 @@ func (c *Client) startJSPlugin(handler plugin.ClientPlugin, req protocol.JSPlugi
log.Printf("[Client] JS plugin %s started at %s", req.PluginName, localAddr)
}
// verifyJSPluginSignature 验证 JS 插件签名
func (c *Client) verifyJSPluginSignature(pluginName, source, signature string) error {
if signature == "" {
return fmt.Errorf("missing signature")
}
// 解码签名
signed, err := sign.DecodeSignedPlugin(signature)
if err != nil {
return fmt.Errorf("decode signature: %w", err)
}
// 检查插件是否被撤销
if revoked, reason := sign.IsPluginRevoked(pluginName, signed.Payload.Version); revoked {
return fmt.Errorf("plugin %s v%s has been revoked: %s",
pluginName, signed.Payload.Version, reason)
}
// 检查密钥是否已吊销
if sign.IsKeyRevoked(signed.Payload.KeyID) {
return fmt.Errorf("signing key %s has been revoked", signed.Payload.KeyID)
}
// 根据 KeyID 获取对应公钥
pubKey, err := sign.GetPublicKeyByID(signed.Payload.KeyID)
if err != nil {
return fmt.Errorf("get public key: %w", err)
}
// 验证插件名称匹配
if signed.Payload.Name != pluginName {
return fmt.Errorf("plugin name mismatch: expected %s, got %s",
pluginName, signed.Payload.Name)
}
// 验证签名和源码哈希
if err := sign.VerifyPlugin(pubKey, signed, source); err != nil {
return err
}
// 检查版本降级攻击
if c.versionStore != nil {
currentVer := c.versionStore.GetVersion(pluginName)
if currentVer != "" {
cmp := sign.CompareVersions(signed.Payload.Version, currentVer)
if cmp < 0 {
return fmt.Errorf("version downgrade rejected: %s < %s",
signed.Payload.Version, currentVer)
}
}
}
return nil
}

View File

@@ -0,0 +1,67 @@
package tunnel
import (
"encoding/json"
"os"
"path/filepath"
"sync"
)
// PluginVersionStore 插件版本存储
type PluginVersionStore struct {
path string
versions map[string]string // pluginName -> version
mu sync.RWMutex
}
// NewPluginVersionStore 创建版本存储
func NewPluginVersionStore(dataDir string) (*PluginVersionStore, error) {
store := &PluginVersionStore{
path: filepath.Join(dataDir, "plugin_versions.json"),
versions: make(map[string]string),
}
if err := store.load(); err != nil {
return nil, err
}
return store, nil
}
// load 从文件加载版本信息
func (s *PluginVersionStore) load() error {
data, err := os.ReadFile(s.path)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
return json.Unmarshal(data, &s.versions)
}
// save 保存版本信息到文件
func (s *PluginVersionStore) save() error {
dir := filepath.Dir(s.path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(s.versions, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.path, data, 0600)
}
// GetVersion 获取插件版本
func (s *PluginVersionStore) GetVersion(name string) string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.versions[name]
}
// SetVersion 设置插件版本
func (s *PluginVersionStore) SetVersion(name, version string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.versions[name] = version
return s.save()
}

View File

@@ -20,6 +20,7 @@ type ServerConfig struct {
type JSPluginConfig struct {
Name string `yaml:"name"`
Path string `yaml:"path"` // JS 文件路径
SigPath string `yaml:"sig_path,omitempty"` // 签名文件路径 (默认为 path + ".sig")
AutoPush []string `yaml:"auto_push,omitempty"` // 自动推送到的客户端 ID 列表
Config map[string]string `yaml:"config,omitempty"` // 插件配置
AutoStart bool `yaml:"auto_start,omitempty"` // 是否自动启动
@@ -27,9 +28,12 @@ type JSPluginConfig struct {
// PluginStoreSettings 扩展商店设置
type PluginStoreSettings struct {
URL string `yaml:"url"` // 扩展商店URL例如 GitHub 仓库的 raw URL
// 保留结构体以便未来扩展,但不暴露 URL 配置
}
// 官方插件商店(不可配置)
const OfficialPluginStoreURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/store.json"
// ServerSettings 服务端设置
type ServerSettings struct {
BindAddr string `yaml:"bind_addr"`

View File

@@ -37,6 +37,7 @@ type PluginData struct {
type JSPlugin struct {
Name string `json:"name"`
Source string `json:"source"`
Signature string `json:"signature"` // 官方签名 (Base64)
Description string `json:"description"`
Author string `json:"author"`
AutoPush []string `json:"auto_push"`

View File

@@ -93,6 +93,9 @@ func (s *SQLiteStore) init() error {
return err
}
// 迁移:添加 signature 列
s.db.Exec(`ALTER TABLE js_plugins ADD COLUMN signature TEXT NOT NULL DEFAULT ''`)
return nil
}
@@ -323,7 +326,7 @@ func (s *SQLiteStore) GetAllJSPlugins() ([]JSPlugin, error) {
defer s.mu.RUnlock()
rows, err := s.db.Query(`
SELECT name, source, description, author, auto_push, config, auto_start, enabled
SELECT name, source, signature, description, author, auto_push, config, auto_start, enabled
FROM js_plugins
`)
if err != nil {
@@ -336,7 +339,7 @@ func (s *SQLiteStore) GetAllJSPlugins() ([]JSPlugin, error) {
var p JSPlugin
var autoPushJSON, configJSON string
var autoStart, enabled int
err := rows.Scan(&p.Name, &p.Source, &p.Description, &p.Author,
err := rows.Scan(&p.Name, &p.Source, &p.Signature, &p.Description, &p.Author,
&autoPushJSON, &configJSON, &autoStart, &enabled)
if err != nil {
return nil, err
@@ -359,9 +362,9 @@ func (s *SQLiteStore) GetJSPlugin(name string) (*JSPlugin, error) {
var autoPushJSON, configJSON string
var autoStart, enabled int
err := s.db.QueryRow(`
SELECT name, source, description, author, auto_push, config, auto_start, enabled
SELECT name, source, signature, description, author, auto_push, config, auto_start, enabled
FROM js_plugins WHERE name = ?
`, name).Scan(&p.Name, &p.Source, &p.Description, &p.Author,
`, name).Scan(&p.Name, &p.Source, &p.Signature, &p.Description, &p.Author,
&autoPushJSON, &configJSON, &autoStart, &enabled)
if err != nil {
return nil, err
@@ -390,9 +393,9 @@ func (s *SQLiteStore) SaveJSPlugin(p *JSPlugin) error {
_, err := s.db.Exec(`
INSERT OR REPLACE INTO js_plugins
(name, source, description, author, auto_push, config, auto_start, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, p.Name, p.Source, p.Description, p.Author,
(name, source, signature, description, author, auto_push, config, auto_start, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, p.Name, p.Source, p.Signature, p.Description, p.Author,
string(autoPushJSON), string(configJSON), autoStart, enabled)
return err
}

View File

@@ -59,6 +59,7 @@ type ServerInterface interface {
type JSPluginInstallRequest struct {
PluginName string `json:"plugin_name"`
Source string `json:"source"`
Signature string `json:"signature"`
RuleName string `json:"rule_name"`
RemotePort int `json:"remote_port"`
Config map[string]string `json:"config"`
@@ -589,14 +590,8 @@ func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request)
}
cfg := h.app.GetConfig()
storeURL := cfg.PluginStore.URL
if storeURL == "" {
h.jsonResponse(rw, map[string]interface{}{
"plugins": []StorePluginInfo{},
"store_url": "",
})
return
}
storeURL := config.OfficialPluginStoreURL
_ = cfg // 保留以便未来扩展
// 从远程URL获取插件列表
client := &http.Client{Timeout: 10 * time.Second}
@@ -939,6 +934,7 @@ func (h *APIHandler) pushJSPluginToClient(rw http.ResponseWriter, pluginName, cl
req := JSPluginInstallRequest{
PluginName: p.Name,
Source: p.Source,
Signature: p.Signature,
RuleName: p.Name,
Config: p.Config,
AutoStart: p.AutoStart,

View File

@@ -54,6 +54,7 @@ type Server struct {
type JSPluginEntry struct {
Name string
Source string
Signature string
AutoPush []string
Config map[string]string
AutoStart bool
@@ -873,6 +874,7 @@ func (s *Server) InstallJSPluginToClient(clientID string, req router.JSPluginIns
installReq := protocol.JSPluginInstallRequest{
PluginName: req.PluginName,
Source: req.Source,
Signature: req.Signature,
RuleName: req.RuleName,
RemotePort: req.RemotePort,
Config: req.Config,
@@ -1035,6 +1037,7 @@ func (s *Server) autoPushJSPlugins(cs *ClientSession) {
req := router.JSPluginInstallRequest{
PluginName: jp.Name,
Source: jp.Source,
Signature: jp.Signature,
RuleName: jp.Name,
Config: jp.Config,
AutoStart: jp.AutoStart,