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() }