From 3d51aa455006903345f554a2dd90034993796114 Mon Sep 17 00:00:00 2001 From: sergei Date: Tue, 14 Apr 2026 06:23:55 +0400 Subject: vpnem: VPN infrastructure with load-balanced multi-protocol nodes - Multi-protocol VPS nodes (VLESS-REALITY + Hysteria2 + SOCKS5) - Smart load balancing via recommendation API - Windows/Linux client (Go + Wails + sing-box) - Server API with RealIP detection and connection tracking - Auto-deployment via vpnui control plane - Silent Windows installer with UAC elevation - Load-based server recommendation (no sticky sessions) - Best Server one-click connection workflow --- internal/state/state.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 internal/state/state.go (limited to 'internal/state') diff --git a/internal/state/state.go b/internal/state/state.go new file mode 100644 index 0000000..97d2d47 --- /dev/null +++ b/internal/state/state.go @@ -0,0 +1,174 @@ +package state + +import ( + "encoding/json" + "os" + "path/filepath" + "sync" + "time" +) + +// AppState holds persistent client state. +type AppState struct { + SelectedServer string `json:"selected_server"` + SelectedMode string `json:"selected_mode"` + LastSync time.Time `json:"last_sync"` + AutoConnect bool `json:"auto_connect"` + LocalProxyPort int `json:"local_proxy_port,omitempty"` + EnabledRuleSets map[string]bool `json:"enabled_rule_sets,omitempty"` + CustomBypass []string `json:"custom_bypass_processes,omitempty"` + RecommendedServer string `json:"recommended_server,omitempty"` + RecommendedNodeID string `json:"recommended_node_id,omitempty"` + LastRecommendation time.Time `json:"last_recommendation,omitempty"` + RecommendationFetched bool `json:"recommendation_fetched,omitempty"` // true if recommendation was ever fetched +} + +// Store manages persistent state on disk. +type Store struct { + mu sync.Mutex + path string + dataDir string + data AppState +} + +// NewStore creates a state store at the given path. +func NewStore(dataDir string) *Store { + return &Store{ + path: filepath.Join(dataDir, "state.json"), + dataDir: dataDir, + data: AppState{ + SelectedMode: "Комбо (приложения + Re-filter)", + AutoConnect: false, + }, + } +} + +// DataDir returns the data directory path. +func (s *Store) DataDir() string { + s.mu.Lock() + defer s.mu.Unlock() + return s.dataDir +} + +// Load reads state from disk. Returns default state if file doesn't exist. +func (s *Store) Load() error { + s.mu.Lock() + defer s.mu.Unlock() + + data, err := os.ReadFile(s.path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + return json.Unmarshal(data, &s.data) +} + +// Save writes state to disk. +func (s *Store) Save() error { + s.mu.Lock() + defer s.mu.Unlock() + + if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { + return err + } + + data, err := json.MarshalIndent(s.data, "", " ") + if err != nil { + return err + } + return os.WriteFile(s.path, data, 0o644) +} + +// Get returns a copy of the current state. +func (s *Store) Get() AppState { + s.mu.Lock() + defer s.mu.Unlock() + return s.data +} + +// SetServer updates the selected server. +func (s *Store) SetServer(tag string) { + s.mu.Lock() + s.data.SelectedServer = tag + s.mu.Unlock() +} + +// SetMode updates the selected routing mode. +func (s *Store) SetMode(mode string) { + s.mu.Lock() + s.data.SelectedMode = mode + s.mu.Unlock() +} + +// SetLastSync records the last sync time. +func (s *Store) SetLastSync(t time.Time) { + s.mu.Lock() + s.data.LastSync = t + s.mu.Unlock() +} + +// SetAutoConnect updates the auto-connect setting. +func (s *Store) SetAutoConnect(v bool) { + s.mu.Lock() + s.data.AutoConnect = v + s.mu.Unlock() +} + +func (s *Store) SetLocalProxyPort(port int) { + s.mu.Lock() + s.data.LocalProxyPort = port + s.mu.Unlock() +} + +// SetRuleSetEnabled enables/disables an optional rule-set. +func (s *Store) SetRuleSetEnabled(tag string, enabled bool) { + s.mu.Lock() + if s.data.EnabledRuleSets == nil { + s.data.EnabledRuleSets = make(map[string]bool) + } + s.data.EnabledRuleSets[tag] = enabled + s.mu.Unlock() +} + +// IsRuleSetEnabled checks if a rule-set is enabled. +func (s *Store) IsRuleSetEnabled(tag string) bool { + s.mu.Lock() + defer s.mu.Unlock() + if s.data.EnabledRuleSets == nil { + return false + } + return s.data.EnabledRuleSets[tag] +} + +// SetCustomBypass sets custom bypass processes. +func (s *Store) SetCustomBypass(processes []string) { + s.mu.Lock() + s.data.CustomBypass = processes + s.mu.Unlock() +} + +// GetCustomBypass returns custom bypass processes. +func (s *Store) GetCustomBypass() []string { + s.mu.Lock() + defer s.mu.Unlock() + return append([]string{}, s.data.CustomBypass...) +} + +// SetRecommendation saves the server recommendation. +func (s *Store) SetRecommendation(serverIP, nodeID string) { + s.mu.Lock() + s.data.RecommendedServer = serverIP + s.data.RecommendedNodeID = nodeID + s.data.LastRecommendation = time.Now() + s.data.RecommendationFetched = true + s.mu.Unlock() +} + +// GetRecommendation returns the current recommendation. +func (s *Store) GetRecommendation() (serverIP, nodeID string, at time.Time) { + s.mu.Lock() + defer s.mu.Unlock() + return s.data.RecommendedServer, s.data.RecommendedNodeID, s.data.LastRecommendation +} -- cgit v1.2.3