diff options
| author | sergei <sergei@em-sysadmin.xyz> | 2026-04-14 06:23:55 +0400 |
|---|---|---|
| committer | sergei <sergei@em-sysadmin.xyz> | 2026-04-14 06:23:55 +0400 |
| commit | 3d51aa455006903345f554a2dd90034993796114 (patch) | |
| tree | 62a7be2faf047f5eb7886feebc3b815556f03d7f /internal/rules/loader.go | |
| download | vpnem-3d51aa455006903345f554a2dd90034993796114.tar.gz vpnem-3d51aa455006903345f554a2dd90034993796114.tar.bz2 vpnem-3d51aa455006903345f554a2dd90034993796114.zip | |
- 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
Diffstat (limited to 'internal/rules/loader.go')
| -rw-r--r-- | internal/rules/loader.go | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/internal/rules/loader.go b/internal/rules/loader.go new file mode 100644 index 0000000..7bbbe6e --- /dev/null +++ b/internal/rules/loader.go @@ -0,0 +1,210 @@ +package rules + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + + "vpnem/internal/models" +) + +type Store struct { + dataDir string + connections *ConnectionStore +} + +func NewStore(dataDir string) *Store { + s := &Store{ + dataDir: dataDir, + connections: NewConnectionStore(dataDir), + } + _ = s.connections.Load() + return s +} + +// Connections returns the connection store for recommendation logic. +func (s *Store) Connections() *ConnectionStore { + return s.connections +} + +func (s *Store) LoadServers() (*models.ServersResponse, error) { + data, err := os.ReadFile(filepath.Join(s.dataDir, "servers.json")) + if err != nil { + return nil, err + } + var resp models.ServersResponse + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +func (s *Store) LoadRuleSets() (*models.RuleSetManifest, error) { + data, err := os.ReadFile(filepath.Join(s.dataDir, "rulesets.json")) + if err != nil { + return nil, err + } + var manifest models.RuleSetManifest + if err := json.Unmarshal(data, &manifest); err != nil { + return nil, err + } + return &manifest, nil +} + +func (s *Store) LoadVersion() (*models.VersionResponse, error) { + data, err := os.ReadFile(filepath.Join(s.dataDir, "version.json")) + if err != nil { + return nil, err + } + var ver models.VersionResponse + if err := json.Unmarshal(data, &ver); err != nil { + return nil, err + } + return &ver, nil +} + +func (s *Store) LoadCatalogV2() (*models.CatalogV2, error) { + data, err := os.ReadFile(filepath.Join(s.dataDir, "catalog-v2.json")) + if err != nil { + return nil, err + } + var catalog models.CatalogV2 + if err := json.Unmarshal(data, &catalog); err != nil { + return nil, err + } + return &catalog, nil +} + +func (s *Store) LoadCatalogV2OrLegacy() (*models.CatalogV2, error) { + catalog, err := s.LoadCatalogV2() + if err == nil { + return catalog, nil + } + if !os.IsNotExist(err) { + return nil, err + } + + servers, err := s.LoadServers() + if err != nil { + return nil, err + } + return legacyServersToCatalog(servers.Servers), nil +} + +func (s *Store) LoadRoutingPolicy() (*models.RoutingPolicy, error) { + data, err := os.ReadFile(filepath.Join(s.dataDir, "routing-policy.json")) + if err != nil { + return nil, err + } + var policy models.RoutingPolicy + if err := json.Unmarshal(data, &policy); err != nil { + return nil, err + } + return &policy, nil +} + +func (s *Store) RulesDir() string { + return filepath.Join(s.dataDir, "rules") +} + +func (s *Store) ReleasesDir() string { + return filepath.Join(s.dataDir, "releases") +} + +func (s *Store) DataDir() string { + return s.dataDir +} + +func legacyServersToCatalog(servers []models.Server) *models.CatalogV2 { + nodesByID := make(map[string]*models.CatalogNode, len(servers)) + order := make([]string, 0, len(servers)) + for _, server := range servers { + nodeID := server.Tag + if existingID, protocolType, ok := splitLegacyTag(server.Tag); ok && existingID != "" { + nodeID = existingID + server.Type = protocolType + } + + node := nodesByID[nodeID] + if node == nil { + node = &models.CatalogNode{ + ID: nodeID, + Name: nodeID, + Region: server.Region, + Host: server.Server, + PublicHost: server.Server, + Status: "published", + } + nodesByID[nodeID] = node + order = append(order, nodeID) + } + node.Protocols = append(node.Protocols, legacyServerToCatalogProtocol(server)) + } + + nodes := make([]models.CatalogNode, 0, len(order)) + for _, id := range order { + nodes = append(nodes, *nodesByID[id]) + } + return &models.CatalogV2{ + Version: "legacy-adapter", + Nodes: nodes, + } +} + +func legacyServerToCatalogProtocol(server models.Server) models.CatalogProtocol { + protocolType := server.Type + if protocolType == "socks" { + protocolType = "socks5" + } + protocol := models.CatalogProtocol{ + Type: protocolType, + Enabled: true, + Port: server.ServerPort, + TLS: server.TLS, + Extra: make(map[string]any), + } + switch server.Type { + case "vless", "vless-reality", "vmess": + protocol.Auth = &models.CatalogAuth{UUID: server.UUID} + protocol.Extra["legacy_tag"] = server.Tag + if server.Transport != nil { + protocol.Extra["transport_type"] = server.Transport.Type + if server.Transport.Path != "" { + protocol.Extra["path"] = server.Transport.Path + } + } + case "shadowsocks": + protocol.Auth = &models.CatalogAuth{Method: server.Method, Password: server.Password} + protocol.Extra["legacy_tag"] = server.Tag + case "hysteria2": + protocol.Auth = &models.CatalogAuth{Password: server.Password} + protocol.Extra["legacy_tag"] = server.Tag + if server.ObfsPassword != "" { + protocol.Extra["obfs_password"] = server.ObfsPassword + } + if server.UpMbps > 0 { + protocol.Extra["up_mbps"] = server.UpMbps + } + if server.DownMbps > 0 { + protocol.Extra["down_mbps"] = server.DownMbps + } + case "socks": + protocol.Extra["legacy_tag"] = server.Tag + protocol.Extra["udp_over_tcp"] = server.UDPOverTCP + } + if len(protocol.Extra) == 0 { + protocol.Extra = nil + } + return protocol +} + +func splitLegacyTag(tag string) (nodeID, protocolType string, ok bool) { + for _, candidate := range []string{"vless-reality", "vless", "vmess", "shadowsocks", "hysteria2", "socks", "socks5"} { + suffix := "-" + candidate + if strings.HasSuffix(tag, suffix) && len(tag) > len(suffix) { + return strings.TrimSuffix(tag, suffix), candidate, true + } + } + return "", "", false +} |
