// Package custom 实现自定义协议插件:用户在平台注册命令钩子, // 平台按统一 Protocol 契约调用(Create/Start/Stop/Status/Config)。 // 钩子通过 stdin 接收专线 Config JSON,约定 stdout 返回结果。 package custom import ( "bytes" "encoding/json" "fmt" "os/exec" "strings" "time" "vpn-platform/backend/internal/core" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/store" ) // Service 以数据库中的 Plugin 定义为驱动的协议实现 type Service struct { plugin *model.Plugin } func New(p *model.Plugin) *Service { return &Service{plugin: p} } func (s *Service) Name() string { return "custom:" + s.plugin.Name } func (s *Service) DisplayName() string { return s.plugin.DisplayName } func (s *Service) Available() (bool, string) { if s.plugin.StartHook == "" { return false, "插件未配置 start 钩子" } return true, "插件钩子: " + s.plugin.StartHook } // runHook 执行钩子命令,stdin 传入专线上下文 JSON func (s *Service) runHook(hook string, t *model.Tunnel) (string, error) { if hook == "" { return "", fmt.Errorf("插件 [%s] 未配置该动作的钩子命令", s.plugin.Name) } payload, _ := json.Marshal(map[string]interface{}{ "id": t.ID, "name": t.Name, "protocol": t.Protocol, "config": json.RawMessage(t.Config), }) ctx := exec.Command("sh", "-c", hook) done := make(chan error, 1) ctx.Stdin = bytes.NewReader(payload) var out, errOut bytes.Buffer ctx.Stdout = &out ctx.Stderr = &errOut if err := ctx.Start(); err != nil { return "", err } go func() { done <- ctx.Wait() }() select { case err := <-done: if err != nil { return out.String(), fmt.Errorf("钩子执行失败: %v %s", err, errOut.String()) } return strings.TrimSpace(out.String()), nil case <-time.After(30 * time.Second): _ = ctx.Process.Kill() return "", fmt.Errorf("钩子执行超时(30s)") } } func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{}, error) { out := map[string]interface{}{} for k, v := range cfg { out[k] = v } return out, nil } func (s *Service) Create(t *model.Tunnel) error { if s.plugin.CreateHook == "" { return nil // 可选 } _, err := s.runHook(s.plugin.CreateHook, t) return err } func (s *Service) Start(t *model.Tunnel) error { _, err := s.runHook(s.plugin.StartHook, t) return err } func (s *Service) Stop(t *model.Tunnel) error { if s.plugin.StopHook == "" { return nil } _, err := s.runHook(s.plugin.StopHook, t) return err } func (s *Service) Status(t *model.Tunnel) (core.Status, error) { if s.plugin.StatusHook == "" { return core.Status{State: "unknown", Detail: "插件未配置 status 钩子", UpdatedAt: timeNow()}, nil } out, err := s.runHook(s.plugin.StatusHook, t) if err != nil { return core.Status{Running: false, State: "error", Detail: err.Error(), UpdatedAt: timeNow()}, nil } // 约定:status 钩子 stdout 输出 running / stopped / 任意描述 running := strings.EqualFold(out, "running") || strings.EqualFold(out, "up") || out == "0" state := "stopped" if running { state = "running" } return core.Status{Running: running, State: state, Detail: out, UpdatedAt: timeNow()}, nil } func timeNow() int64 { return time.Now().Unix() } func (s *Service) GenerateClientConfig(t *model.Tunnel) (string, error) { if s.plugin.ConfigHook == "" { return "", fmt.Errorf("插件未配置 config 钩子,无法生成客户端配置") } return s.runHook(s.plugin.ConfigHook, t) } func (s *Service) Delete(t *model.Tunnel) error { return s.Stop(t) } // LoadAll 从数据库加载全部启用的插件并注册到管理器 func LoadAll(m *core.Manager) { if store.DB == nil { return } var plugins []model.Plugin if err := store.DB.Where("enabled = ?", true).Find(&plugins).Error; err != nil { return } for i := range plugins { m.Register(New(&plugins[i])) } }