package installers import ( "bufio" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "strings" ) // AnyoneRelayConfig holds configuration for the Anyone relay type AnyoneRelayConfig struct { Nickname string // Relay nickname (1-19 alphanumeric) Contact string // Contact info (email or @telegram) Wallet string // Ethereum wallet for rewards ORPort int // ORPort for relay (default 9001) ExitRelay bool // Whether to run as exit relay Migrate bool // Whether to migrate existing installation MyFamily string // Comma-separated list of family fingerprints (for multi-relay operators) } // ExistingAnyoneInfo contains information about an existing Anyone installation type ExistingAnyoneInfo struct { HasKeys bool HasConfig bool IsRunning bool Fingerprint string Wallet string Nickname string MyFamily string // Existing MyFamily setting (important to preserve!) ConfigPath string KeysPath string } // AnyoneRelayInstaller handles Anyone relay installation type AnyoneRelayInstaller struct { *BaseInstaller config AnyoneRelayConfig } // NewAnyoneRelayInstaller creates a new Anyone relay installer func NewAnyoneRelayInstaller(arch string, logWriter io.Writer, config AnyoneRelayConfig) *AnyoneRelayInstaller { return &AnyoneRelayInstaller{ BaseInstaller: NewBaseInstaller(arch, logWriter), config: config, } } // DetectExistingAnyoneInstallation checks for an existing Anyone relay installation func DetectExistingAnyoneInstallation() (*ExistingAnyoneInfo, error) { info := &ExistingAnyoneInfo{ ConfigPath: "/etc/anon/anonrc", KeysPath: "/var/lib/anon/keys", } // Check for existing keys if _, err := os.Stat(info.KeysPath); err == nil { info.HasKeys = true } // Check for existing config if _, err := os.Stat(info.ConfigPath); err == nil { info.HasConfig = true // Parse existing config for fingerprint/wallet/nickname if file, err := os.Open(info.ConfigPath); err == nil { defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "#") { continue } // Parse Nickname if strings.HasPrefix(line, "Nickname ") { info.Nickname = strings.TrimPrefix(line, "Nickname ") } // Parse ContactInfo for wallet (format: ... @anon:0x... or @anon: 0x...) if strings.HasPrefix(line, "ContactInfo ") { contact := strings.TrimPrefix(line, "ContactInfo ") // Extract wallet address from @anon: prefix (handle space after colon) if idx := strings.Index(contact, "@anon:"); idx != -1 { wallet := strings.TrimSpace(contact[idx+6:]) info.Wallet = wallet } } // Parse MyFamily (critical to preserve for multi-relay operators) if strings.HasPrefix(line, "MyFamily ") { info.MyFamily = strings.TrimPrefix(line, "MyFamily ") } } } } // Check if anon service is running cmd := exec.Command("systemctl", "is-active", "--quiet", "anon") if cmd.Run() == nil { info.IsRunning = true } // Try to get fingerprint from data directory (it's in /var/lib/anon/, not keys/) fingerprintFile := "/var/lib/anon/fingerprint" if data, err := os.ReadFile(fingerprintFile); err == nil { info.Fingerprint = strings.TrimSpace(string(data)) } // Return nil if no installation detected if !info.HasKeys && !info.HasConfig && !info.IsRunning { return nil, nil } return info, nil } // IsInstalled checks if the anon relay binary is installed func (ari *AnyoneRelayInstaller) IsInstalled() bool { // Check if anon binary exists if _, err := exec.LookPath("anon"); err == nil { return true } // Check common installation path if _, err := os.Stat("/usr/bin/anon"); err == nil { return true } return false } // Install downloads and installs the Anyone relay using the official install script func (ari *AnyoneRelayInstaller) Install() error { fmt.Fprintf(ari.logWriter, " Installing Anyone relay...\n") // Create required directories dirs := []string{ "/etc/anon", "/var/lib/anon", "/var/log/anon", } for _, dir := range dirs { if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } } // Download the official install script installScript := "/tmp/anon-install.sh" scriptURL := "https://raw.githubusercontent.com/anyone-protocol/anon-install/refs/heads/main/install.sh" fmt.Fprintf(ari.logWriter, " Downloading install script...\n") if err := DownloadFile(scriptURL, installScript); err != nil { return fmt.Errorf("failed to download install script: %w", err) } // Make script executable if err := os.Chmod(installScript, 0755); err != nil { return fmt.Errorf("failed to chmod install script: %w", err) } // The official script is interactive, so we need to provide answers via stdin // or install the package directly fmt.Fprintf(ari.logWriter, " Installing anon package...\n") // Add the Anyone repository and install the package directly // This is more reliable than running the interactive script if err := ari.addAnyoneRepository(); err != nil { return fmt.Errorf("failed to add Anyone repository: %w", err) } // Install the anon package cmd := exec.Command("apt-get", "install", "-y", "anon") if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to install anon package: %w\n%s", err, string(output)) } // Clean up os.Remove(installScript) fmt.Fprintf(ari.logWriter, " ✓ Anyone relay binary installed\n") return nil } // addAnyoneRepository adds the Anyone apt repository func (ari *AnyoneRelayInstaller) addAnyoneRepository() error { // Add GPG key using wget (as per official install script) fmt.Fprintf(ari.logWriter, " Adding Anyone repository key...\n") // Download and add the GPG key using the official method keyPath := "/etc/apt/trusted.gpg.d/anon.asc" cmd := exec.Command("bash", "-c", "wget -qO- https://deb.en.anyone.tech/anon.asc | tee "+keyPath) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to download GPG key: %w\n%s", err, string(output)) } // Add repository fmt.Fprintf(ari.logWriter, " Adding Anyone repository...\n") // Determine distribution codename codename := "stable" if data, err := exec.Command("lsb_release", "-cs").Output(); err == nil { codename = strings.TrimSpace(string(data)) } // Create sources.list entry using the official format: anon-live-$VERSION_CODENAME repoLine := fmt.Sprintf("deb [signed-by=%s] https://deb.en.anyone.tech anon-live-%s main\n", keyPath, codename) if err := os.WriteFile("/etc/apt/sources.list.d/anon.list", []byte(repoLine), 0644); err != nil { return fmt.Errorf("failed to write repository file: %w", err) } // Update apt cmd = exec.Command("apt-get", "update", "--yes") if output, err := cmd.CombinedOutput(); err != nil { fmt.Fprintf(ari.logWriter, " ⚠️ Warning: apt update failed: %s\n", string(output)) } return nil } // Configure generates the anonrc configuration file func (ari *AnyoneRelayInstaller) Configure() error { fmt.Fprintf(ari.logWriter, " Configuring Anyone relay...\n") configPath := "/etc/anon/anonrc" // Backup existing config if it exists if _, err := os.Stat(configPath); err == nil { backupPath := configPath + ".bak" if err := exec.Command("cp", configPath, backupPath).Run(); err != nil { fmt.Fprintf(ari.logWriter, " ⚠️ Warning: failed to backup existing config: %v\n", err) } else { fmt.Fprintf(ari.logWriter, " Backed up existing config to %s\n", backupPath) } } // Generate configuration config := ari.generateAnonrc() // Write configuration if err := os.WriteFile(configPath, []byte(config), 0644); err != nil { return fmt.Errorf("failed to write anonrc: %w", err) } fmt.Fprintf(ari.logWriter, " ✓ Anyone relay configured\n") return nil } // generateAnonrc creates the anonrc configuration content func (ari *AnyoneRelayInstaller) generateAnonrc() string { var sb strings.Builder sb.WriteString("# Anyone Relay Configuration (Managed by Orama Network)\n") sb.WriteString("# Generated automatically - manual edits may be overwritten\n\n") // Nickname sb.WriteString(fmt.Sprintf("Nickname %s\n", ari.config.Nickname)) // Contact info with wallet if ari.config.Wallet != "" { sb.WriteString(fmt.Sprintf("ContactInfo %s @anon:%s\n", ari.config.Contact, ari.config.Wallet)) } else { sb.WriteString(fmt.Sprintf("ContactInfo %s\n", ari.config.Contact)) } sb.WriteString("\n") // ORPort sb.WriteString(fmt.Sprintf("ORPort %d\n", ari.config.ORPort)) // SOCKS port for local use sb.WriteString("SocksPort 9050\n") sb.WriteString("\n") // Exit relay configuration if ari.config.ExitRelay { sb.WriteString("ExitRelay 1\n") sb.WriteString("# Exit policy - allow common ports\n") sb.WriteString("ExitPolicy accept *:80\n") sb.WriteString("ExitPolicy accept *:443\n") sb.WriteString("ExitPolicy reject *:*\n") } else { sb.WriteString("ExitRelay 0\n") sb.WriteString("ExitPolicy reject *:*\n") } sb.WriteString("\n") // Logging sb.WriteString("Log notice file /var/log/anon/notices.log\n") // Data directory sb.WriteString("DataDirectory /var/lib/anon\n") // Control port for monitoring sb.WriteString("ControlPort 9051\n") // MyFamily for multi-relay operators (preserve from existing config) if ari.config.MyFamily != "" { sb.WriteString("\n") sb.WriteString(fmt.Sprintf("MyFamily %s\n", ari.config.MyFamily)) } return sb.String() } // MigrateExistingInstallation migrates an existing Anyone installation into Orama Network func (ari *AnyoneRelayInstaller) MigrateExistingInstallation(existing *ExistingAnyoneInfo, backupDir string) error { fmt.Fprintf(ari.logWriter, " Migrating existing Anyone installation...\n") // Create backup directory backupAnonDir := filepath.Join(backupDir, "anon-backup") if err := os.MkdirAll(backupAnonDir, 0755); err != nil { return fmt.Errorf("failed to create backup directory: %w", err) } // Stop existing anon service if running if existing.IsRunning { fmt.Fprintf(ari.logWriter, " Stopping existing anon service...\n") exec.Command("systemctl", "stop", "anon").Run() } // Backup keys if existing.HasKeys { fmt.Fprintf(ari.logWriter, " Backing up keys...\n") keysBackup := filepath.Join(backupAnonDir, "keys") if err := exec.Command("cp", "-r", existing.KeysPath, keysBackup).Run(); err != nil { return fmt.Errorf("failed to backup keys: %w", err) } } // Backup config if existing.HasConfig { fmt.Fprintf(ari.logWriter, " Backing up config...\n") configBackup := filepath.Join(backupAnonDir, "anonrc") if err := exec.Command("cp", existing.ConfigPath, configBackup).Run(); err != nil { return fmt.Errorf("failed to backup config: %w", err) } } // Preserve nickname from existing installation if not provided if ari.config.Nickname == "" && existing.Nickname != "" { fmt.Fprintf(ari.logWriter, " Using existing nickname: %s\n", existing.Nickname) ari.config.Nickname = existing.Nickname } // Preserve wallet from existing installation if not provided if ari.config.Wallet == "" && existing.Wallet != "" { fmt.Fprintf(ari.logWriter, " Using existing wallet: %s\n", existing.Wallet) ari.config.Wallet = existing.Wallet } // Preserve MyFamily from existing installation (critical for multi-relay operators) if existing.MyFamily != "" { fmt.Fprintf(ari.logWriter, " Preserving MyFamily configuration (%d relays)\n", len(strings.Split(existing.MyFamily, ","))) ari.config.MyFamily = existing.MyFamily } fmt.Fprintf(ari.logWriter, " ✓ Backup created at %s\n", backupAnonDir) fmt.Fprintf(ari.logWriter, " ✓ Migration complete - keys and fingerprint preserved\n") return nil } // ValidateNickname validates the relay nickname (1-19 alphanumeric chars) func ValidateNickname(nickname string) error { if len(nickname) < 1 || len(nickname) > 19 { return fmt.Errorf("nickname must be 1-19 characters") } if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(nickname) { return fmt.Errorf("nickname must be alphanumeric only") } return nil } // ValidateWallet validates an Ethereum wallet address func ValidateWallet(wallet string) error { if !regexp.MustCompile(`^0x[a-fA-F0-9]{40}$`).MatchString(wallet) { return fmt.Errorf("invalid Ethereum wallet address (must be 0x followed by 40 hex characters)") } return nil }