package engine import ( "context" "encoding/json" "fmt" "log" "os" "path/filepath" "sync" box "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/option" "vpnem/internal/config" "vpnem/internal/models" ) type Engine struct { mu sync.Mutex instance *box.Box cancel context.CancelFunc running bool configPath string dataDir string } func New(dataDir string) *Engine { return &Engine{ dataDir: dataDir, configPath: filepath.Join(dataDir, "config.json"), } } func (e *Engine) Start(server models.Server, mode config.Mode, ruleSets []models.RuleSet, serverIPs []string) error { return e.StartFull(server, mode, ruleSets, serverIPs, nil) } func (e *Engine) StartFull(server models.Server, mode config.Mode, ruleSets []models.RuleSet, serverIPs []string, customBypass []string) error { e.mu.Lock() defer e.mu.Unlock() if e.running { return fmt.Errorf("already running") } cfg := config.BuildConfigFull(server, mode, ruleSets, serverIPs, customBypass) data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return fmt.Errorf("marshal config: %w", err) } os.MkdirAll(e.dataDir, 0o755) _ = os.WriteFile(e.configPath, data, 0o644) log.Printf("engine: config saved (%d bytes)", len(data)) var opts option.Options ctx := box.Context( context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), ) if err := opts.UnmarshalJSONContext(ctx, data); err != nil { log.Printf("engine: parse FAILED: %v", err) return fmt.Errorf("parse config: %w", err) } boxCtx, cancel := context.WithCancel(ctx) e.cancel = cancel instance, err := box.New(box.Options{ Context: boxCtx, Options: opts, }) if err != nil { cancel() log.Printf("engine: create FAILED: %v", err) return fmt.Errorf("create sing-box: %w", err) } if err := instance.Start(); err != nil { instance.Close() cancel() log.Printf("engine: start FAILED: %v", err) return fmt.Errorf("start sing-box: %w", err) } e.instance = instance e.running = true log.Println("engine: started ok") return nil } func (e *Engine) Stop() error { e.mu.Lock() defer e.mu.Unlock() if !e.running { return nil } if e.instance != nil { e.instance.Close() e.instance = nil } if e.cancel != nil { e.cancel() } e.running = false log.Println("engine: stopped") return nil } func (e *Engine) Restart(server models.Server, mode config.Mode, ruleSets []models.RuleSet, serverIPs []string) error { e.Stop() return e.Start(server, mode, ruleSets, serverIPs) } func (e *Engine) RestartFull(server models.Server, mode config.Mode, ruleSets []models.RuleSet, serverIPs []string, customBypass []string) error { e.Stop() return e.StartFull(server, mode, ruleSets, serverIPs, customBypass) } func (e *Engine) IsRunning() bool { e.mu.Lock() defer e.mu.Unlock() return e.running } func (e *Engine) ConfigPath() string { return e.configPath }