mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 22:46:57 +00:00
567 lines
19 KiB
Go
567 lines
19 KiB
Go
package installers
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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)
|
|
BandwidthRate int // RelayBandwidthRate in KBytes/s (0 = unlimited)
|
|
BandwidthBurst int // RelayBandwidthBurst in KBytes/s (0 = unlimited)
|
|
AccountingMax int // Monthly data cap in GB (0 = unlimited)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Pre-accept terms via debconf to avoid interactive prompt during apt install.
|
|
// The anon package preinst script checks "anon/terms" via debconf.
|
|
preseed := exec.Command("bash", "-c", `echo "anon anon/terms boolean true" | debconf-set-selections`)
|
|
if output, err := preseed.CombinedOutput(); err != nil {
|
|
fmt.Fprintf(ari.logWriter, " ⚠️ debconf preseed warning: %v (%s)\n", err, string(output))
|
|
}
|
|
|
|
// Install the anon package non-interactively.
|
|
// --force-confold keeps existing config files if present (e.g. during migration).
|
|
cmd := exec.Command("apt-get", "install", "-y", "-o", "Dpkg::Options::=--force-confold", "anon")
|
|
cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
|
|
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)
|
|
|
|
// Stop and disable the default 'anon' systemd service that the apt package
|
|
// auto-enables. We use our own 'orama-anyone-relay' service instead.
|
|
exec.Command("systemctl", "stop", "anon").Run()
|
|
exec.Command("systemctl", "disable", "anon").Run()
|
|
|
|
// Fix logrotate: the apt package installs /etc/logrotate.d/anon with
|
|
// "invoke-rc.d anon reload" in postrotate, but we disabled the anon service.
|
|
// Without this fix, log rotation leaves an empty notices.log and the relay
|
|
// keeps writing to the old (rotated) file descriptor.
|
|
ari.fixLogrotate()
|
|
|
|
fmt.Fprintf(ari.logWriter, " ✓ Anyone relay binary installed\n")
|
|
|
|
// Install nyx for relay monitoring (connects to ControlPort 9051)
|
|
if err := ari.installNyx(); err != nil {
|
|
fmt.Fprintf(ari.logWriter, " ⚠️ nyx install warning: %v\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// fixLogrotate replaces the apt-provided logrotate config which uses
|
|
// "invoke-rc.d anon reload" (broken because we disable the anon service).
|
|
// Without this, log rotation creates an empty notices.log but the relay
|
|
// process keeps writing to the old file descriptor, so bootstrap detection
|
|
// and all log-based monitoring breaks after the first midnight rotation.
|
|
func (ari *AnyoneRelayInstaller) fixLogrotate() {
|
|
config := `/var/log/anon/*log {
|
|
daily
|
|
rotate 5
|
|
compress
|
|
delaycompress
|
|
missingok
|
|
notifempty
|
|
create 0640 debian-anon adm
|
|
sharedscripts
|
|
postrotate
|
|
/usr/bin/killall -HUP anon 2>/dev/null || true
|
|
endscript
|
|
}
|
|
`
|
|
if err := os.WriteFile("/etc/logrotate.d/anon", []byte(config), 0644); err != nil {
|
|
fmt.Fprintf(ari.logWriter, " ⚠️ logrotate fix warning: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// installNyx installs the nyx relay monitor tool
|
|
func (ari *AnyoneRelayInstaller) installNyx() error {
|
|
// Check if already installed
|
|
if _, err := exec.LookPath("nyx"); err == nil {
|
|
fmt.Fprintf(ari.logWriter, " ✓ nyx already installed\n")
|
|
return nil
|
|
}
|
|
|
|
fmt.Fprintf(ari.logWriter, " Installing nyx (relay monitor)...\n")
|
|
cmd := exec.Command("apt-get", "install", "-y", "nyx")
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to install nyx: %w\n%s", err, string(output))
|
|
}
|
|
|
|
fmt.Fprintf(ari.logWriter, " ✓ nyx installed (use 'nyx' to monitor relay on ControlPort 9051)\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
|
|
}
|
|
|
|
// ConfigureClient generates a client-only anonrc (SocksPort 9050, no relay)
|
|
func (ari *AnyoneRelayInstaller) ConfigureClient() error {
|
|
fmt.Fprintf(ari.logWriter, " Configuring Anyone client-only mode...\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)
|
|
}
|
|
}
|
|
|
|
config := `# Anyone Client Configuration (Managed by Orama Network)
|
|
# Client-only mode — no relay traffic, no ORPort
|
|
|
|
SocksPort 9050
|
|
|
|
Log notice file /var/log/anon/notices.log
|
|
DataDirectory /var/lib/anon
|
|
ControlPort 9051
|
|
`
|
|
|
|
if err := os.WriteFile(configPath, []byte(config), 0644); err != nil {
|
|
return fmt.Errorf("failed to write client anonrc: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(ari.logWriter, " ✓ Anyone client configured (SocksPort 9050)\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")
|
|
|
|
// Bandwidth limiting
|
|
if ari.config.BandwidthRate > 0 {
|
|
sb.WriteString("\n")
|
|
sb.WriteString("# Bandwidth limiting (managed by Orama Network)\n")
|
|
sb.WriteString(fmt.Sprintf("RelayBandwidthRate %d KBytes\n", ari.config.BandwidthRate))
|
|
sb.WriteString(fmt.Sprintf("RelayBandwidthBurst %d KBytes\n", ari.config.BandwidthBurst))
|
|
|
|
rateMbps := float64(ari.config.BandwidthRate) * 8 / 1024
|
|
burstMbps := float64(ari.config.BandwidthBurst) * 8 / 1024
|
|
sb.WriteString(fmt.Sprintf("# Rate: %.1f Mbps, Burst: %.1f Mbps\n", rateMbps, burstMbps))
|
|
}
|
|
|
|
// Monthly data cap
|
|
if ari.config.AccountingMax > 0 {
|
|
sb.WriteString("\n")
|
|
sb.WriteString("# Monthly data cap (managed by Orama Network)\n")
|
|
sb.WriteString("AccountingStart month 1 00:00\n")
|
|
sb.WriteString(fmt.Sprintf("AccountingMax %d GBytes\n", ari.config.AccountingMax))
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// MeasureBandwidth downloads a test file and returns the measured download speed in KBytes/s.
|
|
// Uses wget to download a 10MB file from a public CDN and measures throughput.
|
|
// Returns 0 if the test fails (caller should skip bandwidth limiting).
|
|
func MeasureBandwidth(logWriter io.Writer) (int, error) {
|
|
fmt.Fprintf(logWriter, " Running bandwidth test...\n")
|
|
|
|
testFile := "/tmp/speedtest-orama.tmp"
|
|
defer os.Remove(testFile)
|
|
|
|
// Use wget with progress output to download a 10MB test file
|
|
// We time the download ourselves for accuracy
|
|
start := time.Now()
|
|
cmd := exec.Command("wget", "-q", "-O", testFile, "http://speedtest.tele2.net/10MB.zip")
|
|
cmd.Env = append(os.Environ(), "LC_ALL=C")
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(logWriter, " ⚠️ Bandwidth test failed: %v\n", err)
|
|
return 0, fmt.Errorf("bandwidth test download failed: %w", err)
|
|
}
|
|
|
|
elapsed := time.Since(start)
|
|
|
|
// Get file size
|
|
info, err := os.Stat(testFile)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to stat test file: %w", err)
|
|
}
|
|
|
|
// Calculate speed in KBytes/s
|
|
sizeKB := int(info.Size() / 1024)
|
|
seconds := elapsed.Seconds()
|
|
if seconds < 0.1 {
|
|
seconds = 0.1 // avoid division by zero
|
|
}
|
|
speedKBs := int(float64(sizeKB) / seconds)
|
|
|
|
speedMbps := float64(speedKBs) * 8 / 1024 // Convert KBytes/s to Mbps
|
|
fmt.Fprintf(logWriter, " Measured download speed: %d KBytes/s (%.1f Mbps)\n", speedKBs, speedMbps)
|
|
|
|
return speedKBs, nil
|
|
}
|
|
|
|
// CalculateBandwidthLimits computes RelayBandwidthRate and RelayBandwidthBurst
|
|
// from measured speed and a percentage. Returns rate and burst in KBytes/s.
|
|
func CalculateBandwidthLimits(measuredKBs int, percent int) (rate int, burst int) {
|
|
rate = measuredKBs * percent / 100
|
|
burst = rate * 3 / 2 // 1.5x rate for burst headroom
|
|
if rate < 1 {
|
|
rate = 1
|
|
}
|
|
if burst < rate {
|
|
burst = rate
|
|
}
|
|
return rate, burst
|
|
}
|
|
|
|
// 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
|
|
}
|