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
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:
@@ -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
|
||||
}
|
||||
|
||||
67
internal/client/tunnel/version_store.go
Normal file
67
internal/client/tunnel/version_store.go
Normal 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()
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user