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/control/lifecycle.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/control/lifecycle.go')
| -rw-r--r-- | internal/control/lifecycle.go | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/internal/control/lifecycle.go b/internal/control/lifecycle.go new file mode 100644 index 0000000..a45339f --- /dev/null +++ b/internal/control/lifecycle.go @@ -0,0 +1,205 @@ +package control + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "strings" +) + +func SetNodeEnabled(node Node, enabled bool) Node { + node.Enabled = enabled + return node +} + +func RotateNodeSecrets(node Node) (Node, error) { + for idx := range node.Protocols { + protocol := &node.Protocols[idx] + switch protocol.Type { + case "vless", "vmess": + if protocol.Auth == nil { + protocol.Auth = &AuthProfile{} + } + uuid, err := randomUUID() + if err != nil { + return node, err + } + protocol.Auth.UUID = uuid + case "shadowsocks": + if protocol.Auth == nil { + protocol.Auth = &AuthProfile{} + } + password, err := randomHex(16) + if err != nil { + return node, err + } + protocol.Auth.Password = password + case "hysteria2": + if err := ensureHysteria2Profile(protocol); err != nil { + return node, err + } + password, err := randomBase64(16) + if err != nil { + return node, err + } + protocol.Auth.Password = password + protocol.Hysteria2.UserPassword = password + obfsPassword, err := randomHex(32) + if err != nil { + return node, err + } + protocol.Hysteria2.ObfsPassword = obfsPassword + if protocol.Extra == nil { + protocol.Extra = map[string]any{} + } + protocol.Extra["obfs_password"] = obfsPassword + } + } + return node, nil +} + +func AddSocks5Protocol(node Node, port int) (Node, error) { + if port <= 0 { + port = 54101 + } + for _, protocol := range node.Protocols { + if protocol.Type == "socks5" || protocol.Type == "socks" { + return node, fmt.Errorf("node %s already has SOCKS5 enabled", node.ID) + } + } + node.Protocols = append(node.Protocols, ProtocolProfile{ + Type: "socks5", + Enabled: true, + Port: port, + }) + return node, nil +} + +func DestroyNode(ctx context.Context, runner SSHExecutor, dnsClient DNSProvider, zone string, node Node, inventoryDir, stateDir string) []string { + var warnings []string + + if dnsClient != nil && strings.TrimSpace(node.Domain) != "" && strings.HasSuffix(node.Domain, "."+zone) { + name := strings.TrimSuffix(node.Domain, "."+zone) + name = strings.TrimSuffix(name, ".") + if err := dnsClient.DeleteARecord(ctx, zone, name); err != nil { + warnings = append(warnings, "dns cleanup failed: "+err.Error()) + } + } + + if strings.TrimSpace(node.Host) != "" { + if _, err := runner.Run(ctx, node, RenderDestroyScript()); err != nil { + warnings = append(warnings, "remote cleanup failed: "+err.Error()) + } + } + + if err := DeleteNodeState(stateDir, node.ID); err != nil { + warnings = append(warnings, "state cleanup failed: "+err.Error()) + } + if err := DeleteNodeFile(inventoryDir, node.ID); err != nil { + warnings = append(warnings, "inventory cleanup failed: "+err.Error()) + } + + return warnings +} + +func UpgradeNode(ctx context.Context, runner SSHExecutor, node Node, stateDir string) (*NodeState, error) { + if _, err := BootstrapNode(ctx, runner, node, BootstrapOptions{ + StateDir: stateDir, + DryRun: false, + }); err != nil { + return nil, err + } + + state, err := CheckNode(ctx, runner, node, stateDir) + if state != nil { + if state.Metadata == nil { + state.Metadata = map[string]any{} + } + state.Metadata["lifecycle_action"] = "upgrade" + _ = SaveNodeState(stateDir, *state) + } + return state, err +} + +func RepairReinstallNode(ctx context.Context, runner SSHExecutor, node Node, stateDir string) (*NodeState, error) { + return reinstallNode(ctx, runner, node, stateDir, "repair_reinstall") +} + +func CleanReinstallNode(ctx context.Context, runner SSHExecutor, node Node, stateDir string) (*NodeState, error) { + return reinstallNode(ctx, runner, node, stateDir, "clean_reinstall") +} + +func reinstallNode(ctx context.Context, runner SSHExecutor, node Node, stateDir, action string) (*NodeState, error) { + cleanupWarning := "" + if strings.TrimSpace(node.Host) != "" { + if _, err := runner.Run(ctx, node, RenderDestroyScript()); err != nil { + cleanupWarning = err.Error() + } + } + + if _, err := BootstrapNode(ctx, runner, node, BootstrapOptions{ + StateDir: stateDir, + DryRun: false, + }); err != nil { + return nil, err + } + + state, err := CheckNode(ctx, runner, node, stateDir) + if state != nil { + if state.Metadata == nil { + state.Metadata = map[string]any{} + } + state.Metadata["lifecycle_action"] = action + if cleanupWarning != "" { + state.Metadata["cleanup_warning"] = cleanupWarning + } + _ = SaveNodeState(stateDir, *state) + } + return state, err +} + +func RenderDestroyScript() string { + return `set -eu +if [ -f /opt/vpnem-node/current/docker-compose.yml ]; then + if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + docker compose -f /opt/vpnem-node/current/docker-compose.yml down -v || true + elif command -v docker-compose >/dev/null 2>&1; then + docker-compose -f /opt/vpnem-node/current/docker-compose.yml down -v || true + fi +fi +rm -rf /opt/vpnem-node +printf 'vpnem-node removed\n' +` +} + +func randomHex(size int) (string, error) { + buf := make([]byte, size) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return hex.EncodeToString(buf), nil +} + +func randomUUID() (string, error) { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + return "", err + } + buf[6] = (buf[6] & 0x0f) | 0x40 + buf[8] = (buf[8] & 0x3f) | 0x80 + hexID := hex.EncodeToString(buf) + return fmt.Sprintf("%s-%s-%s-%s-%s", + hexID[0:8], + hexID[8:12], + hexID[12:16], + hexID[16:20], + hexID[20:32], + ), nil +} + +func RandomHexForAPI(size int) (string, error) { return randomHex(size) } + +func RandomBase64ForAPI(size int) (string, error) { return randomBase64(size) } + +func RandomUUIDForAPI() (string, error) { return randomUUID() } |
