diff --git a/.gitignore b/.gitignore index 83f7e41..9dfc426 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,6 @@ scripts/remote-nodes.conf orama-cli-linux -rnd/ \ No newline at end of file +rnd/ + +keys_backup/ \ No newline at end of file diff --git a/README.md b/README.md index 9661607..fec2e94 100644 --- a/README.md +++ b/README.md @@ -324,10 +324,56 @@ curl -X DELETE http://localhost:6001/v1/functions/hello-world?namespace=default - 5001 - RQLite HTTP API - 6001 - Unified Gateway - 8080 - IPFS Gateway -- 9050 - Anyone Client SOCKS5 proxy +- 9050 - Anyone SOCKS5 proxy - 9094 - IPFS Cluster API - 3320/3322 - Olric Cache +**Anyone Relay Mode (optional, for earning rewards):** + +- 9001 - Anyone ORPort (relay traffic, must be open externally) + +### Anyone Network Integration + +Orama Network integrates with the [Anyone Protocol](https://anyone.io) for anonymous routing. By default, nodes run as **clients** (consuming the network). Optionally, you can run as a **relay operator** to earn rewards. + +**Client Mode (Default):** +- Routes traffic through Anyone network for anonymity +- SOCKS5 proxy on localhost:9050 +- No rewards, just consumes network + +**Relay Mode (Earn Rewards):** +- Provide bandwidth to the Anyone network +- Earn $ANYONE tokens as a relay operator +- Requires 100 $ANYONE tokens in your wallet +- Requires ORPort (9001) open to the internet + +```bash +# Install as relay operator (earn rewards) +sudo orama install --vps-ip --domain \ + --anyone-relay \ + --anyone-nickname "MyRelay" \ + --anyone-contact "operator@email.com" \ + --anyone-wallet "0x1234...abcd" + +# With exit relay (legal implications apply) +sudo orama install --vps-ip --domain \ + --anyone-relay \ + --anyone-exit \ + --anyone-nickname "MyExitRelay" \ + --anyone-contact "operator@email.com" \ + --anyone-wallet "0x1234...abcd" + +# Migrate existing Anyone installation +sudo orama install --vps-ip --domain \ + --anyone-relay \ + --anyone-migrate \ + --anyone-nickname "MyRelay" \ + --anyone-contact "operator@email.com" \ + --anyone-wallet "0x1234...abcd" +``` + +**Important:** After installation, register your relay at [dashboard.anyone.io](https://dashboard.anyone.io) to start earning rewards. + ### Installation **macOS (Homebrew):** diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a2a7861..80fbc45 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -52,6 +52,13 @@ The system follows a clean, layered architecture with clear separation of concer │ │ │ │ │ Port 9094 │ │ In-Process │ └─────────────────┘ └──────────────┘ + + ┌─────────────────┐ + │ Anyone │ + │ (Anonymity) │ + │ │ + │ Port 9050 │ + └─────────────────┘ ``` ## Core Components @@ -226,7 +233,38 @@ pkg/config/ └── gateway.go ``` -### 6. Shared Utilities +### 6. Anyone Integration (`pkg/anyoneproxy/`) + +Integration with the Anyone Protocol for anonymous routing. + +**Modes:** + +| Mode | Purpose | Port | Rewards | +|------|---------|------|---------| +| Client | Route traffic anonymously | 9050 (SOCKS5) | No | +| Relay | Provide bandwidth to network | 9001 (ORPort) + 9050 | Yes ($ANYONE) | + +**Key Files:** +- `pkg/anyoneproxy/socks.go` - SOCKS5 proxy client interface +- `pkg/gateway/anon_proxy_handler.go` - Anonymous proxy API endpoint +- `pkg/environments/production/installers/anyone_relay.go` - Relay installation + +**Features:** +- Smart routing (bypasses proxy for local/private addresses) +- Automatic detection of existing Anyone installations +- Migration support for existing relay operators +- Exit relay mode with legal warnings + +**API Endpoint:** +- `POST /v1/proxy/anon` - Route HTTP requests through Anyone network + +**Relay Requirements:** +- Linux OS (Debian/Ubuntu) +- 100 $ANYONE tokens in wallet +- ORPort accessible from internet +- Registration at dashboard.anyone.io + +### 7. Shared Utilities **HTTP Utilities (`pkg/httputil/`):** - Request parsing and validation diff --git a/pkg/cli/production/install/command.go b/pkg/cli/production/install/command.go index 5b2d0e3..d0de911 100644 --- a/pkg/cli/production/install/command.go +++ b/pkg/cli/production/install/command.go @@ -39,6 +39,12 @@ func Handle(args []string) { os.Exit(1) } + // Validate Anyone relay configuration if enabled + if err := orchestrator.validator.ValidateAnyoneRelayFlags(); err != nil { + fmt.Fprintf(os.Stderr, "❌ %v\n", err) + os.Exit(1) + } + // Execute installation if err := orchestrator.Execute(); err != nil { fmt.Fprintf(os.Stderr, "❌ %v\n", err) diff --git a/pkg/cli/production/install/flags.go b/pkg/cli/production/install/flags.go index e8e67f5..51e9610 100644 --- a/pkg/cli/production/install/flags.go +++ b/pkg/cli/production/install/flags.go @@ -27,6 +27,16 @@ type Flags struct { IPFSAddrs string IPFSClusterPeerID string IPFSClusterAddrs string + + // Anyone relay operator flags + AnyoneRelay bool // Run as relay operator instead of client + AnyoneExit bool // Run as exit relay (legal implications) + AnyoneMigrate bool // Migrate existing Anyone installation + AnyoneNickname string // Relay nickname (1-19 alphanumeric) + AnyoneContact string // Contact info (email or @telegram) + AnyoneWallet string // Ethereum wallet for rewards + AnyoneORPort int // ORPort for relay (default 9001) + AnyoneFamily string // Comma-separated fingerprints of other relays you operate } // ParseFlags parses install command flags @@ -58,6 +68,16 @@ func ParseFlags(args []string) (*Flags, error) { fs.StringVar(&flags.IPFSClusterPeerID, "ipfs-cluster-peer", "", "Peer ID of existing IPFS Cluster node") fs.StringVar(&flags.IPFSClusterAddrs, "ipfs-cluster-addrs", "", "Comma-separated multiaddrs of existing IPFS Cluster node") + // Anyone relay operator flags + fs.BoolVar(&flags.AnyoneRelay, "anyone-relay", false, "Run as Anyone relay operator (earn rewards)") + fs.BoolVar(&flags.AnyoneExit, "anyone-exit", false, "Run as exit relay (requires --anyone-relay, legal implications)") + fs.BoolVar(&flags.AnyoneMigrate, "anyone-migrate", false, "Migrate existing Anyone installation into Orama Network") + fs.StringVar(&flags.AnyoneNickname, "anyone-nickname", "", "Relay nickname (1-19 alphanumeric chars)") + fs.StringVar(&flags.AnyoneContact, "anyone-contact", "", "Contact info (email or @telegram)") + fs.StringVar(&flags.AnyoneWallet, "anyone-wallet", "", "Ethereum wallet address for rewards") + fs.IntVar(&flags.AnyoneORPort, "anyone-orport", 9001, "ORPort for relay (default 9001)") + fs.StringVar(&flags.AnyoneFamily, "anyone-family", "", "Comma-separated fingerprints of other relays you operate") + if err := fs.Parse(args); err != nil { if err == flag.ErrHelp { return nil, err diff --git a/pkg/cli/production/install/orchestrator.go b/pkg/cli/production/install/orchestrator.go index a32ef8d..4777a1e 100644 --- a/pkg/cli/production/install/orchestrator.go +++ b/pkg/cli/production/install/orchestrator.go @@ -33,6 +33,21 @@ func NewOrchestrator(flags *Flags) (*Orchestrator, error) { setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, flags.Branch, flags.NoPull, flags.SkipChecks) setup.SetNameserver(flags.Nameserver) + + // Configure Anyone relay if enabled + if flags.AnyoneRelay { + setup.SetAnyoneRelayConfig(&production.AnyoneRelayConfig{ + Enabled: true, + Exit: flags.AnyoneExit, + Migrate: flags.AnyoneMigrate, + Nickname: flags.AnyoneNickname, + Contact: flags.AnyoneContact, + Wallet: flags.AnyoneWallet, + ORPort: flags.AnyoneORPort, + MyFamily: flags.AnyoneFamily, + }) + } + validator := NewValidator(flags, oramaDir) return &Orchestrator{ @@ -60,7 +75,18 @@ func (o *Orchestrator) Execute() error { // Dry-run mode: show what would be done and exit if o.flags.DryRun { - utils.ShowDryRunSummary(o.flags.VpsIP, o.flags.Domain, o.flags.Branch, o.peers, o.flags.JoinAddress, o.validator.IsFirstNode(), o.oramaDir) + var relayInfo *utils.AnyoneRelayDryRunInfo + if o.flags.AnyoneRelay { + relayInfo = &utils.AnyoneRelayDryRunInfo{ + Enabled: true, + Exit: o.flags.AnyoneExit, + Nickname: o.flags.AnyoneNickname, + Contact: o.flags.AnyoneContact, + Wallet: o.flags.AnyoneWallet, + ORPort: o.flags.AnyoneORPort, + } + } + utils.ShowDryRunSummaryWithRelay(o.flags.VpsIP, o.flags.Domain, o.flags.Branch, o.peers, o.flags.JoinAddress, o.validator.IsFirstNode(), o.oramaDir, relayInfo) return nil } diff --git a/pkg/cli/production/install/validator.go b/pkg/cli/production/install/validator.go index 6e8bf5c..3a02325 100644 --- a/pkg/cli/production/install/validator.go +++ b/pkg/cli/production/install/validator.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/DeBrosOfficial/network/pkg/cli/utils" + "github.com/DeBrosOfficial/network/pkg/environments/production/installers" ) // Validator validates install command inputs @@ -43,7 +44,17 @@ func (v *Validator) ValidateRootPrivileges() error { // ValidatePorts validates port availability func (v *Validator) ValidatePorts() error { - if err := utils.EnsurePortsAvailable("install", utils.DefaultPorts()); err != nil { + ports := utils.DefaultPorts() + + // Add ORPort check for relay mode (skip if migrating existing installation) + if v.flags.AnyoneRelay && !v.flags.AnyoneMigrate { + ports = append(ports, utils.PortSpec{ + Name: "Anyone ORPort", + Port: v.flags.AnyoneORPort, + }) + } + + if err := utils.EnsurePortsAvailable("install", ports); err != nil { return err } return nil @@ -104,3 +115,107 @@ func (v *Validator) SaveSecrets() error { func (v *Validator) IsFirstNode() bool { return v.isFirstNode } + +// ValidateAnyoneRelayFlags validates Anyone relay configuration and displays warnings +func (v *Validator) ValidateAnyoneRelayFlags() error { + // Skip validation if not running as relay + if !v.flags.AnyoneRelay { + return nil + } + + fmt.Printf("\n🔗 Anyone Relay Configuration\n") + + // Check for existing Anyone installation + existing, err := installers.DetectExistingAnyoneInstallation() + if err != nil { + fmt.Printf(" ⚠️ Warning: failed to detect existing installation: %v\n", err) + } + + if existing != nil { + fmt.Printf(" ⚠️ Existing Anyone relay detected:\n") + if existing.Fingerprint != "" { + fmt.Printf(" Fingerprint: %s\n", existing.Fingerprint) + } + if existing.Nickname != "" { + fmt.Printf(" Nickname: %s\n", existing.Nickname) + } + if existing.Wallet != "" { + fmt.Printf(" Wallet: %s\n", existing.Wallet) + } + if existing.MyFamily != "" { + familyCount := len(strings.Split(existing.MyFamily, ",")) + fmt.Printf(" MyFamily: %d relays\n", familyCount) + } + fmt.Printf(" Keys: %s\n", existing.KeysPath) + fmt.Printf(" Config: %s\n", existing.ConfigPath) + if existing.IsRunning { + fmt.Printf(" Status: Running\n") + } + if !v.flags.AnyoneMigrate { + fmt.Printf("\n 💡 Use --anyone-migrate to preserve existing keys and fingerprint\n") + } else { + fmt.Printf("\n ✓ Will migrate existing installation (keys preserved)\n") + // Auto-populate missing values from existing installation + if v.flags.AnyoneNickname == "" && existing.Nickname != "" { + v.flags.AnyoneNickname = existing.Nickname + fmt.Printf(" ✓ Using existing nickname: %s\n", existing.Nickname) + } + if v.flags.AnyoneWallet == "" && existing.Wallet != "" { + v.flags.AnyoneWallet = existing.Wallet + fmt.Printf(" ✓ Using existing wallet: %s\n", existing.Wallet) + } + } + fmt.Println() + } + + // Validate required fields for relay mode + if v.flags.AnyoneNickname == "" { + return fmt.Errorf("--anyone-nickname is required for relay mode") + } + if err := installers.ValidateNickname(v.flags.AnyoneNickname); err != nil { + return fmt.Errorf("invalid --anyone-nickname: %w", err) + } + + if v.flags.AnyoneWallet == "" { + return fmt.Errorf("--anyone-wallet is required for relay mode (for rewards)") + } + if err := installers.ValidateWallet(v.flags.AnyoneWallet); err != nil { + return fmt.Errorf("invalid --anyone-wallet: %w", err) + } + + if v.flags.AnyoneContact == "" { + return fmt.Errorf("--anyone-contact is required for relay mode") + } + + // Validate ORPort + if v.flags.AnyoneORPort < 1 || v.flags.AnyoneORPort > 65535 { + return fmt.Errorf("--anyone-orport must be between 1 and 65535") + } + + // Display configuration summary + fmt.Printf(" Nickname: %s\n", v.flags.AnyoneNickname) + fmt.Printf(" Contact: %s\n", v.flags.AnyoneContact) + fmt.Printf(" Wallet: %s\n", v.flags.AnyoneWallet) + fmt.Printf(" ORPort: %d\n", v.flags.AnyoneORPort) + if v.flags.AnyoneExit { + fmt.Printf(" Mode: Exit Relay\n") + } else { + fmt.Printf(" Mode: Non-exit Relay\n") + } + + // Warning about token requirement + fmt.Printf("\n ⚠️ IMPORTANT: Relay operators must hold 100 $ANYONE tokens\n") + fmt.Printf(" in wallet %s to receive rewards.\n", v.flags.AnyoneWallet) + fmt.Printf(" Register at: https://dashboard.anyone.io\n") + + // Exit relay warning + if v.flags.AnyoneExit { + fmt.Printf("\n ⚠️ EXIT RELAY WARNING:\n") + fmt.Printf(" Running an exit relay may expose you to legal liability\n") + fmt.Printf(" for traffic that exits through your node.\n") + fmt.Printf(" Ensure you understand the implications before proceeding.\n") + } + + fmt.Println() + return nil +} diff --git a/pkg/cli/production/upgrade/flags.go b/pkg/cli/production/upgrade/flags.go index 892a134..2d6b276 100644 --- a/pkg/cli/production/upgrade/flags.go +++ b/pkg/cli/production/upgrade/flags.go @@ -13,6 +13,16 @@ type Flags struct { NoPull bool Branch string Nameserver *bool // Pointer so we can detect if explicitly set vs default + + // Anyone relay operator flags + AnyoneRelay bool + AnyoneExit bool + AnyoneMigrate bool + AnyoneNickname string + AnyoneContact string + AnyoneWallet string + AnyoneORPort int + AnyoneFamily string } // ParseFlags parses upgrade command flags @@ -30,6 +40,16 @@ func ParseFlags(args []string) (*Flags, error) { // Nameserver flag - use pointer to detect if explicitly set nameserver := fs.Bool("nameserver", false, "Make this node a nameserver (uses saved preference if not specified)") + // Anyone relay operator flags + fs.BoolVar(&flags.AnyoneRelay, "anyone-relay", false, "Run as Anyone relay operator (earn rewards)") + fs.BoolVar(&flags.AnyoneExit, "anyone-exit", false, "Run as exit relay (requires --anyone-relay, legal implications)") + fs.BoolVar(&flags.AnyoneMigrate, "anyone-migrate", false, "Migrate existing Anyone installation into Orama Network") + fs.StringVar(&flags.AnyoneNickname, "anyone-nickname", "", "Relay nickname (1-19 alphanumeric chars)") + fs.StringVar(&flags.AnyoneContact, "anyone-contact", "", "Contact info (email or @telegram)") + fs.StringVar(&flags.AnyoneWallet, "anyone-wallet", "", "Ethereum wallet address for rewards") + fs.IntVar(&flags.AnyoneORPort, "anyone-orport", 9001, "ORPort for relay (default 9001)") + fs.StringVar(&flags.AnyoneFamily, "anyone-family", "", "Comma-separated fingerprints of other relays you operate") + // Support legacy flags for backwards compatibility nightly := fs.Bool("nightly", false, "Use nightly branch (deprecated, use --branch nightly)") main := fs.Bool("main", false, "Use main branch (deprecated, use --branch main)") diff --git a/pkg/cli/production/upgrade/orchestrator.go b/pkg/cli/production/upgrade/orchestrator.go index dd4d186..23bb899 100644 --- a/pkg/cli/production/upgrade/orchestrator.go +++ b/pkg/cli/production/upgrade/orchestrator.go @@ -44,6 +44,20 @@ func NewOrchestrator(flags *Flags) *Orchestrator { setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, branch, flags.NoPull, false) setup.SetNameserver(isNameserver) + // Configure Anyone relay if enabled + if flags.AnyoneRelay { + setup.SetAnyoneRelayConfig(&production.AnyoneRelayConfig{ + Enabled: true, + Exit: flags.AnyoneExit, + Migrate: flags.AnyoneMigrate, + Nickname: flags.AnyoneNickname, + Contact: flags.AnyoneContact, + Wallet: flags.AnyoneWallet, + ORPort: flags.AnyoneORPort, + MyFamily: flags.AnyoneFamily, + }) + } + return &Orchestrator{ oramaHome: oramaHome, oramaDir: oramaDir, @@ -192,7 +206,8 @@ func (o *Orchestrator) stopServices() error { "debros-ipfs-cluster.service", // Depends on IPFS "debros-ipfs.service", // Base IPFS "debros-olric.service", // Independent - "debros-anyone-client.service", // Independent + "debros-anyone-client.service", // Client mode + "debros-anyone-relay.service", // Relay mode } for _, svc := range services { unitPath := filepath.Join("/etc/systemd/system", svc) diff --git a/pkg/cli/utils/install.go b/pkg/cli/utils/install.go index 21ff11c..bc01c07 100644 --- a/pkg/cli/utils/install.go +++ b/pkg/cli/utils/install.go @@ -17,8 +17,23 @@ type IPFSClusterPeerInfo struct { Addrs []string } +// AnyoneRelayDryRunInfo contains Anyone relay info for dry-run summary +type AnyoneRelayDryRunInfo struct { + Enabled bool + Exit bool + Nickname string + Contact string + Wallet string + ORPort int +} + // ShowDryRunSummary displays what would be done during installation without making changes func ShowDryRunSummary(vpsIP, domain, branch string, peers []string, joinAddress string, isFirstNode bool, oramaDir string) { + ShowDryRunSummaryWithRelay(vpsIP, domain, branch, peers, joinAddress, isFirstNode, oramaDir, nil) +} + +// ShowDryRunSummaryWithRelay displays what would be done during installation with optional relay info +func ShowDryRunSummaryWithRelay(vpsIP, domain, branch string, peers []string, joinAddress string, isFirstNode bool, oramaDir string, relayInfo *AnyoneRelayDryRunInfo) { fmt.Print("\n" + strings.Repeat("=", 70) + "\n") fmt.Printf("DRY RUN - No changes will be made\n") fmt.Print(strings.Repeat("=", 70) + "\n\n") @@ -57,7 +72,11 @@ func ShowDryRunSummary(vpsIP, domain, branch string, peers []string, joinAddress fmt.Printf(" - IPFS/Kubo 0.38.2\n") fmt.Printf(" - IPFS Cluster (latest)\n") fmt.Printf(" - Olric 0.7.0\n") - fmt.Printf(" - anyone-client (npm)\n") + if relayInfo != nil && relayInfo.Enabled { + fmt.Printf(" - anon (relay binary via apt)\n") + } else { + fmt.Printf(" - anyone-client (npm)\n") + } fmt.Printf(" - DeBros binaries (built from %s branch)\n", branch) fmt.Printf("\n🔐 Secrets that would be generated:\n") @@ -74,7 +93,11 @@ func ShowDryRunSummary(vpsIP, domain, branch string, peers []string, joinAddress fmt.Printf(" - debros-ipfs-cluster.service\n") fmt.Printf(" - debros-olric.service\n") fmt.Printf(" - debros-node.service (includes embedded gateway + RQLite)\n") - fmt.Printf(" - debros-anyone-client.service\n") + if relayInfo != nil && relayInfo.Enabled { + fmt.Printf(" - debros-anyone-relay.service (relay operator mode)\n") + } else { + fmt.Printf(" - debros-anyone-client.service\n") + } fmt.Printf("\n🌐 Ports that would be used:\n") fmt.Printf(" External (must be open in firewall):\n") @@ -82,6 +105,9 @@ func ShowDryRunSummary(vpsIP, domain, branch string, peers []string, joinAddress fmt.Printf(" - 443 (HTTPS gateway)\n") fmt.Printf(" - 4101 (IPFS swarm)\n") fmt.Printf(" - 7001 (RQLite Raft)\n") + if relayInfo != nil && relayInfo.Enabled { + fmt.Printf(" - %d (Anyone ORPort - relay traffic)\n", relayInfo.ORPort) + } fmt.Printf(" Internal (localhost only):\n") fmt.Printf(" - 4501 (IPFS API)\n") fmt.Printf(" - 5001 (RQLite HTTP)\n") @@ -91,6 +117,23 @@ func ShowDryRunSummary(vpsIP, domain, branch string, peers []string, joinAddress fmt.Printf(" - 9094 (IPFS Cluster API)\n") fmt.Printf(" - 3320/3322 (Olric)\n") + // Show relay-specific configuration + if relayInfo != nil && relayInfo.Enabled { + fmt.Printf("\n🔗 Anyone Relay Configuration:\n") + fmt.Printf(" Mode: Relay Operator\n") + fmt.Printf(" Nickname: %s\n", relayInfo.Nickname) + fmt.Printf(" Contact: %s\n", relayInfo.Contact) + fmt.Printf(" Wallet: %s\n", relayInfo.Wallet) + fmt.Printf(" ORPort: %d\n", relayInfo.ORPort) + if relayInfo.Exit { + fmt.Printf(" Exit: Yes (legal implications apply)\n") + } else { + fmt.Printf(" Exit: No (non-exit relay)\n") + } + fmt.Printf("\n ⚠️ IMPORTANT: You need 100 $ANYONE tokens in wallet to receive rewards\n") + fmt.Printf(" Register at: https://dashboard.anyone.io\n") + } + fmt.Print("\n" + strings.Repeat("=", 70) + "\n") fmt.Printf("To proceed with installation, run without --dry-run\n") fmt.Print(strings.Repeat("=", 70) + "\n\n") diff --git a/pkg/cli/utils/systemd.go b/pkg/cli/utils/systemd.go index e73c40e..268b406 100644 --- a/pkg/cli/utils/systemd.go +++ b/pkg/cli/utils/systemd.go @@ -160,6 +160,7 @@ func GetProductionServices() []string { "debros-ipfs-cluster", "debros-ipfs", "debros-anyone-client", + "debros-anyone-relay", } // Filter to only existing services by checking if unit file exists diff --git a/pkg/environments/production/installers/anyone_relay.go b/pkg/environments/production/installers/anyone_relay.go new file mode 100644 index 0000000..c0b8178 --- /dev/null +++ b/pkg/environments/production/installers/anyone_relay.go @@ -0,0 +1,385 @@ +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 +} diff --git a/pkg/environments/production/orchestrator.go b/pkg/environments/production/orchestrator.go index 576e9ca..5e4a787 100644 --- a/pkg/environments/production/orchestrator.go +++ b/pkg/environments/production/orchestrator.go @@ -8,8 +8,22 @@ import ( "path/filepath" "strings" "time" + + "github.com/DeBrosOfficial/network/pkg/environments/production/installers" ) +// AnyoneRelayConfig holds configuration for Anyone relay mode +type AnyoneRelayConfig struct { + Enabled bool // Whether to run as relay operator + Exit bool // Whether to run as exit relay + Migrate bool // Whether to migrate existing installation + 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) + MyFamily string // Comma-separated fingerprints of other relays (for multi-relay operators) +} + // ProductionSetup orchestrates the entire production deployment type ProductionSetup struct { osInfo *OSInfo @@ -21,6 +35,7 @@ type ProductionSetup struct { skipOptionalDeps bool skipResourceChecks bool isNameserver bool // Whether this node is a nameserver (runs CoreDNS + Caddy) + anyoneRelayConfig *AnyoneRelayConfig // Configuration for Anyone relay mode privChecker *PrivilegeChecker osDetector *OSDetector archDetector *ArchitectureDetector @@ -123,6 +138,16 @@ func (ps *ProductionSetup) IsNameserver() bool { return ps.isNameserver } +// SetAnyoneRelayConfig sets the Anyone relay configuration +func (ps *ProductionSetup) SetAnyoneRelayConfig(config *AnyoneRelayConfig) { + ps.anyoneRelayConfig = config +} + +// IsAnyoneRelay returns whether this node is configured as an Anyone relay operator +func (ps *ProductionSetup) IsAnyoneRelay() bool { + return ps.anyoneRelayConfig != nil && ps.anyoneRelayConfig.Enabled +} + // Phase1CheckPrerequisites performs initial environment validation func (ps *ProductionSetup) Phase1CheckPrerequisites() error { ps.logf("Phase 1: Checking prerequisites...") @@ -275,9 +300,47 @@ func (ps *ProductionSetup) Phase2bInstallBinaries() error { ps.logf(" ⚠️ Olric install warning: %v", err) } - // Install anyone-client for SOCKS5 proxy - if err := ps.binaryInstaller.InstallAnyoneClient(); err != nil { - ps.logf(" ⚠️ anyone-client install warning: %v", err) + // Install Anyone (client or relay based on configuration) + if ps.IsAnyoneRelay() { + ps.logf(" Installing Anyone relay (operator mode)...") + relayConfig := installers.AnyoneRelayConfig{ + Nickname: ps.anyoneRelayConfig.Nickname, + Contact: ps.anyoneRelayConfig.Contact, + Wallet: ps.anyoneRelayConfig.Wallet, + ORPort: ps.anyoneRelayConfig.ORPort, + ExitRelay: ps.anyoneRelayConfig.Exit, + Migrate: ps.anyoneRelayConfig.Migrate, + MyFamily: ps.anyoneRelayConfig.MyFamily, + } + relayInstaller := installers.NewAnyoneRelayInstaller(ps.arch, ps.logWriter, relayConfig) + + // Check for existing installation if migration is requested + if relayConfig.Migrate { + existing, err := installers.DetectExistingAnyoneInstallation() + if err != nil { + ps.logf(" ⚠️ Failed to detect existing installation: %v", err) + } else if existing != nil { + backupDir := filepath.Join(ps.oramaDir, "backups") + if err := relayInstaller.MigrateExistingInstallation(existing, backupDir); err != nil { + ps.logf(" ⚠️ Migration warning: %v", err) + } + } + } + + // Install the relay + if err := relayInstaller.Install(); err != nil { + ps.logf(" ⚠️ Anyone relay install warning: %v", err) + } + + // Configure the relay + if err := relayInstaller.Configure(); err != nil { + ps.logf(" ⚠️ Anyone relay config warning: %v", err) + } + } else { + // Install anyone-client for SOCKS5 proxy (default client mode) + if err := ps.binaryInstaller.InstallAnyoneClient(); err != nil { + ps.logf(" ⚠️ anyone-client install warning: %v", err) + } } // Install DeBros binaries (must be done before CoreDNS since we need the RQLite plugin source) @@ -551,12 +614,20 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(enableHTTPS bool) error { } ps.logf(" ✓ Node service created: debros-node.service (with embedded gateway)") - // Anyone Client service (SOCKS5 proxy) - anyoneUnit := ps.serviceGenerator.GenerateAnyoneClientService() - if err := ps.serviceController.WriteServiceUnit("debros-anyone-client.service", anyoneUnit); err != nil { - return fmt.Errorf("failed to write Anyone Client service: %w", err) + // Anyone service (Client or Relay based on configuration) + if ps.IsAnyoneRelay() { + anyoneUnit := ps.serviceGenerator.GenerateAnyoneRelayService() + if err := ps.serviceController.WriteServiceUnit("debros-anyone-relay.service", anyoneUnit); err != nil { + return fmt.Errorf("failed to write Anyone Relay service: %w", err) + } + ps.logf(" ✓ Anyone Relay service created (operator mode, ORPort: %d)", ps.anyoneRelayConfig.ORPort) + } else { + anyoneUnit := ps.serviceGenerator.GenerateAnyoneClientService() + if err := ps.serviceController.WriteServiceUnit("debros-anyone-client.service", anyoneUnit); err != nil { + return fmt.Errorf("failed to write Anyone Client service: %w", err) + } + ps.logf(" ✓ Anyone Client service created") } - ps.logf(" ✓ Anyone Client service created") // CoreDNS and Caddy services (only for nameserver nodes) if ps.isNameserver { @@ -595,7 +666,14 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(enableHTTPS bool) error { // Enable services (unified names - no bootstrap/node distinction) // Note: debros-gateway.service is no longer needed - each node has an embedded gateway // Note: debros-rqlite.service is NOT created - RQLite is managed by each node internally - services := []string{"debros-ipfs.service", "debros-ipfs-cluster.service", "debros-olric.service", "debros-node.service", "debros-anyone-client.service"} + services := []string{"debros-ipfs.service", "debros-ipfs-cluster.service", "debros-olric.service", "debros-node.service"} + + // Add appropriate Anyone service based on mode + if ps.IsAnyoneRelay() { + services = append(services, "debros-anyone-relay.service") + } else { + services = append(services, "debros-anyone-client.service") + } // Add CoreDNS and Caddy only for nameserver nodes if ps.isNameserver { @@ -617,15 +695,30 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(enableHTTPS bool) error { // Start services in dependency order ps.logf(" Starting services...") - // Start infrastructure first (IPFS, Olric, Anyone Client) - RQLite is managed internally by each node + // Start infrastructure first (IPFS, Olric, Anyone) - RQLite is managed internally by each node infraServices := []string{"debros-ipfs.service", "debros-olric.service"} - - // Check if port 9050 is already in use (e.g., another anyone-client or similar service) - if ps.portChecker.IsPortInUse(9050) { - ps.logf(" ℹ️ Port 9050 is already in use (anyone-client or similar service running)") - ps.logf(" ℹ️ Skipping debros-anyone-client startup - using existing service") + + // Add appropriate Anyone service based on mode + if ps.IsAnyoneRelay() { + // For relay mode, check if ORPort is already in use + orPort := 9001 + if ps.anyoneRelayConfig != nil && ps.anyoneRelayConfig.ORPort > 0 { + orPort = ps.anyoneRelayConfig.ORPort + } + if ps.portChecker.IsPortInUse(orPort) { + ps.logf(" ℹ️ ORPort %d is already in use (existing anon relay running)", orPort) + ps.logf(" ℹ️ Skipping debros-anyone-relay startup - using existing service") + } else { + infraServices = append(infraServices, "debros-anyone-relay.service") + } } else { - infraServices = append(infraServices, "debros-anyone-client.service") + // For client mode, check if SOCKS port 9050 is already in use + if ps.portChecker.IsPortInUse(9050) { + ps.logf(" ℹ️ Port 9050 is already in use (anyone-client or similar service running)") + ps.logf(" ℹ️ Skipping debros-anyone-client startup - using existing service") + } else { + infraServices = append(infraServices, "debros-anyone-client.service") + } } for _, svc := range infraServices { @@ -720,11 +813,27 @@ func (ps *ProductionSetup) LogSetupComplete(peerID string) { ps.logf(" %s/logs/olric.log", ps.oramaDir) ps.logf(" %s/logs/node.log", ps.oramaDir) ps.logf(" %s/logs/gateway.log", ps.oramaDir) - ps.logf(" %s/logs/anyone-client.log", ps.oramaDir) - ps.logf("\nStart All Services:") - ps.logf(" systemctl start debros-ipfs debros-ipfs-cluster debros-olric debros-anyone-client debros-node") + + // Anyone mode-specific logs and commands + if ps.IsAnyoneRelay() { + ps.logf(" /var/log/anon/notices.log (Anyone Relay)") + ps.logf("\nStart All Services:") + ps.logf(" systemctl start debros-ipfs debros-ipfs-cluster debros-olric debros-anyone-relay debros-node") + ps.logf("\nAnyone Relay Operator:") + ps.logf(" ORPort: %d", ps.anyoneRelayConfig.ORPort) + ps.logf(" Wallet: %s", ps.anyoneRelayConfig.Wallet) + ps.logf(" Config: /etc/anon/anonrc") + ps.logf(" Register at: https://dashboard.anyone.io") + ps.logf(" IMPORTANT: You need 100 $ANYONE tokens in your wallet to receive rewards") + } else { + ps.logf(" %s/logs/anyone-client.log", ps.oramaDir) + ps.logf("\nStart All Services:") + ps.logf(" systemctl start debros-ipfs debros-ipfs-cluster debros-olric debros-anyone-client debros-node") + ps.logf("\nAnyone Client:") + ps.logf(" # SOCKS5 proxy on localhost:9050") + } + ps.logf("\nVerify Installation:") ps.logf(" curl http://localhost:6001/health") - ps.logf(" curl http://localhost:5001/status") - ps.logf(" # Anyone Client SOCKS5 proxy on localhost:9050\n") + ps.logf(" curl http://localhost:5001/status\n") } diff --git a/pkg/environments/production/services.go b/pkg/environments/production/services.go index 88ac905..a48404b 100644 --- a/pkg/environments/production/services.go +++ b/pkg/environments/production/services.go @@ -324,6 +324,36 @@ WantedBy=multi-user.target `, ssg.oramaHome, logFile, ssg.oramaDir) } +// GenerateAnyoneRelayService generates the Anyone Relay operator systemd unit +// Uses debian-anon user created by the anon apt package +func (ssg *SystemdServiceGenerator) GenerateAnyoneRelayService() string { + return `[Unit] +Description=Anyone Relay (Orama Network) +Documentation=https://docs.anyone.io +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=debian-anon +Group=debian-anon +ExecStart=/usr/bin/anon --agree-to-terms +Restart=always +RestartSec=10 +SyslogIdentifier=anon-relay + +# Security hardening +NoNewPrivileges=yes +ProtectSystem=full +ProtectHome=read-only +PrivateTmp=yes +ReadWritePaths=/var/lib/anon /var/log/anon /etc/anon + +[Install] +WantedBy=multi-user.target +` +} + // GenerateCoreDNSService generates the CoreDNS systemd unit func (ssg *SystemdServiceGenerator) GenerateCoreDNSService() string { return `[Unit]