From fe16d503b5b9b97b363b67e42c1d91b864505da5 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Thu, 30 Oct 2025 06:21:32 +0200 Subject: [PATCH] feat: integrate Anyone Relay (Anon) into the development environment - Added support for installing and configuring the Anyone Relay (Anon) for anonymous networking in the setup process. - Updated the Makefile to include the Anon client in the development stack, allowing it to run alongside other services. - Implemented a new HTTP proxy handler for the Anon service, enabling proxied requests through the Anyone network. - Enhanced the installation script to manage Anon installation, configuration, and firewall settings. - Introduced tests for the Anon proxy handler to ensure proper request validation and error handling. - Updated documentation to reflect the new Anon service and its usage in the development environment. --- Makefile | 37 +++- pkg/cli/setup.go | 224 ++++++++++++++++++++++- pkg/gateway/anon_proxy_handler.go | 237 +++++++++++++++++++++++++ pkg/gateway/anon_proxy_handler_test.go | 189 ++++++++++++++++++++ pkg/gateway/middleware.go | 3 + pkg/gateway/routes.go | 3 + scripts/install-debros-network.sh | 170 ++++++++++++++++++ terms-agreement | 1 + 8 files changed, 856 insertions(+), 8 deletions(-) create mode 100644 pkg/gateway/anon_proxy_handler.go create mode 100644 pkg/gateway/anon_proxy_handler_test.go create mode 100644 terms-agreement diff --git a/Makefile b/Makefile index b45505f..9ee7a4c 100644 --- a/Makefile +++ b/Makefile @@ -77,12 +77,34 @@ run-gateway: @echo "Generate it with: network-cli config init --type gateway" go run ./cmd/gateway -# One-command dev: Start bootstrap, node2, node3, and gateway in background +# One-command dev: Start bootstrap, node2, node3, gateway, and anon in background # Requires: configs already exist in ~/.debros dev: build @echo "🚀 Starting development network stack..." @mkdir -p .dev/pids @mkdir -p $$HOME/.debros/logs + @echo "Starting Anyone client (anon proxy)..." + @if [ "$$(uname)" = "Darwin" ]; then \ + echo " Detected macOS - using npx anyone-client"; \ + if command -v npx >/dev/null 2>&1; then \ + nohup npx anyone-client > $$HOME/.debros/logs/anon.log 2>&1 & echo $$! > .dev/pids/anon.pid; \ + echo " Anyone client started (PID: $$(cat .dev/pids/anon.pid))"; \ + else \ + echo " ⚠️ npx not found - skipping Anyone client"; \ + echo " Install with: npm install -g npm"; \ + fi; \ + elif [ "$$(uname)" = "Linux" ]; then \ + echo " Detected Linux - checking systemctl"; \ + if systemctl is-active --quiet anon 2>/dev/null; then \ + echo " ✓ Anon service already running"; \ + elif command -v systemctl >/dev/null 2>&1; then \ + echo " Starting anon service..."; \ + sudo systemctl start anon 2>/dev/null || echo " ⚠️ Failed to start anon service"; \ + else \ + echo " ⚠️ systemctl not found - skipping Anon"; \ + fi; \ + fi + @sleep 2 @echo "Starting bootstrap node..." @nohup ./bin/node --config bootstrap.yaml > $$HOME/.debros/logs/bootstrap.log 2>&1 & echo $$! > .dev/pids/bootstrap.pid @sleep 2 @@ -100,12 +122,16 @@ dev: build @echo "============================================================" @echo "" @echo "Processes:" + @if [ -f .dev/pids/anon.pid ]; then \ + echo " Anon: PID=$$(cat .dev/pids/anon.pid) (SOCKS: 9050)"; \ + fi @echo " Bootstrap: PID=$$(cat .dev/pids/bootstrap.pid)" @echo " Node2: PID=$$(cat .dev/pids/node2.pid)" @echo " Node3: PID=$$(cat .dev/pids/node3.pid)" @echo " Gateway: PID=$$(cat .dev/pids/gateway.pid)" @echo "" @echo "Ports:" + @echo " Anon SOCKS: 9050 (proxy endpoint: POST /v1/proxy/anon)" @echo " Bootstrap P2P: 4001, HTTP: 5001, Raft: 7001" @echo " Node2 P2P: 4002, HTTP: 5002, Raft: 7002" @echo " Node3 P2P: 4003, HTTP: 5003, Raft: 7003" @@ -114,8 +140,13 @@ dev: build @echo "Press Ctrl+C to stop all processes" @echo "============================================================" @echo "" - @trap 'echo "Stopping all processes..."; kill $$(cat .dev/pids/*.pid) 2>/dev/null; rm -f .dev/pids/*.pid; exit 0' INT; \ - tail -f $$HOME/.debros/logs/bootstrap.log $$HOME/.debros/logs/node2.log $$HOME/.debros/logs/node3.log $$HOME/.debros/logs/gateway.log + @if [ -f .dev/pids/anon.pid ]; then \ + trap 'echo "Stopping all processes..."; kill $$(cat .dev/pids/*.pid) 2>/dev/null; rm -f .dev/pids/*.pid; exit 0' INT; \ + tail -f $$HOME/.debros/logs/anon.log $$HOME/.debros/logs/bootstrap.log $$HOME/.debros/logs/node2.log $$HOME/.debros/logs/node3.log $$HOME/.debros/logs/gateway.log; \ + else \ + trap 'echo "Stopping all processes..."; kill $$(cat .dev/pids/*.pid) 2>/dev/null; rm -f .dev/pids/*.pid; exit 0' INT; \ + tail -f $$HOME/.debros/logs/bootstrap.log $$HOME/.debros/logs/node2.log $$HOME/.debros/logs/node3.log $$HOME/.debros/logs/gateway.log; \ + fi # Help help: diff --git a/pkg/cli/setup.go b/pkg/cli/setup.go index 0c75cfa..95f4530 100644 --- a/pkg/cli/setup.go +++ b/pkg/cli/setup.go @@ -56,11 +56,12 @@ func HandleSetupCommand(args []string) { fmt.Printf(" 2. Install system dependencies (curl, git, make, build tools)\n") fmt.Printf(" 3. Install Go 1.21+ (if needed)\n") fmt.Printf(" 4. Install RQLite database\n") - fmt.Printf(" 5. Create directories (/home/debros/bin, /home/debros/src)\n") - fmt.Printf(" 6. Clone and build DeBros Network\n") - fmt.Printf(" 7. Generate configuration files\n") - fmt.Printf(" 8. Create systemd services (debros-node, debros-gateway)\n") - fmt.Printf(" 9. Start and enable services\n") + fmt.Printf(" 5. Install Anyone Relay (Anon) for anonymous networking\n") + fmt.Printf(" 6. Create directories (/home/debros/bin, /home/debros/src)\n") + fmt.Printf(" 7. Clone and build DeBros Network\n") + fmt.Printf(" 8. Generate configuration files\n") + fmt.Printf(" 9. Create systemd services (debros-node, debros-gateway)\n") + fmt.Printf(" 10. Start and enable services\n") fmt.Printf(strings.Repeat("=", 70) + "\n\n") fmt.Printf("Ready to begin setup? (yes/no): ") @@ -83,6 +84,9 @@ func HandleSetupCommand(args []string) { // Step 4: Install RQLite installRQLite() + // Step 4.5: Install Anon (Anyone relay) + installAnon() + // Step 5: Setup directories setupDirectories() @@ -112,6 +116,10 @@ func HandleSetupCommand(args []string) { fmt.Printf("Verify Installation:\n") fmt.Printf(" curl http://localhost:6001/health\n") fmt.Printf(" curl http://localhost:5001/status\n\n") + fmt.Printf("Anyone Relay (Anon):\n") + fmt.Printf(" sudo systemctl status anon\n") + fmt.Printf(" sudo tail -f /home/debros/.debros/logs/anon/notices.log\n") + fmt.Printf(" Proxy endpoint: POST http://localhost:6001/v1/proxy/anon\n\n") } func detectLinuxDistro() string { @@ -377,6 +385,212 @@ func installRQLite() { fmt.Printf(" ✓ RQLite installed\n") } +func installAnon() { + fmt.Printf("🔐 Installing Anyone Relay (Anon)...\n") + + // Check if already installed + if _, err := exec.LookPath("anon"); err == nil { + fmt.Printf(" ✓ Anon already installed\n") + configureAnonLogs() + configureFirewallForAnon() + return + } + + // Install via APT (official method from docs.anyone.io) + fmt.Printf(" Adding Anyone APT repository...\n") + + // Add GPG key + cmd := exec.Command("sh", "-c", "curl -fsSL https://deb.anyone.io/gpg.key | gpg --dearmor -o /usr/share/keyrings/anyone-archive-keyring.gpg") + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "⚠️ Failed to add Anyone GPG key: %v\n", err) + fmt.Fprintf(os.Stderr, " You can manually install with:\n") + fmt.Fprintf(os.Stderr, " curl -fsSL https://deb.anyone.io/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/anyone-archive-keyring.gpg\n") + fmt.Fprintf(os.Stderr, " echo 'deb [signed-by=/usr/share/keyrings/anyone-archive-keyring.gpg] https://deb.anyone.io/ anyone main' | sudo tee /etc/apt/sources.list.d/anyone.list\n") + fmt.Fprintf(os.Stderr, " sudo apt update && sudo apt install -y anon\n") + return + } + + // Add repository + repoLine := "deb [signed-by=/usr/share/keyrings/anyone-archive-keyring.gpg] https://deb.anyone.io/ anyone main" + if err := os.WriteFile("/etc/apt/sources.list.d/anyone.list", []byte(repoLine+"\n"), 0644); err != nil { + fmt.Fprintf(os.Stderr, "⚠️ Failed to add Anyone repository: %v\n", err) + return + } + + // Update package list + fmt.Printf(" Updating package list...\n") + exec.Command("apt", "update", "-qq").Run() + + // Install anon + fmt.Printf(" Installing Anon package...\n") + cmd = exec.Command("apt", "install", "-y", "anon") + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "⚠️ Anon installation failed: %v\n", err) + return + } + + // Verify installation + if _, err := exec.LookPath("anon"); err != nil { + fmt.Fprintf(os.Stderr, "⚠️ Anon installation may have failed\n") + return + } + + fmt.Printf(" ✓ Anon installed\n") + + // Configure with sensible defaults + configureAnonDefaults() + + // Configure logs + configureAnonLogs() + + // Configure firewall + configureFirewallForAnon() + + // Enable and start service + fmt.Printf(" Enabling Anon service...\n") + exec.Command("systemctl", "enable", "anon").Run() + exec.Command("systemctl", "start", "anon").Run() + + if exec.Command("systemctl", "is-active", "--quiet", "anon").Run() == nil { + fmt.Printf(" ✓ Anon service is running\n") + } else { + fmt.Fprintf(os.Stderr, "⚠️ Anon service may not be running. Check: systemctl status anon\n") + } +} + +func configureAnonDefaults() { + fmt.Printf(" Configuring Anon with default settings...\n") + + hostname := "debros-node" + if h, err := os.Hostname(); err == nil && h != "" { + hostname = strings.Split(h, ".")[0] + } + + anonrcPath := "/etc/anon/anonrc" + if _, err := os.Stat(anonrcPath); err == nil { + // Backup existing config + exec.Command("cp", anonrcPath, anonrcPath+".bak").Run() + + // Read existing config + data, err := os.ReadFile(anonrcPath) + if err != nil { + return + } + config := string(data) + + // Add settings if not present + if !strings.Contains(config, "Nickname") { + config += fmt.Sprintf("\nNickname %s\n", hostname) + } + if !strings.Contains(config, "ControlPort") { + config += "ControlPort 9051\n" + } + if !strings.Contains(config, "SocksPort") { + config += "SocksPort 9050\n" + } + + // Write back + os.WriteFile(anonrcPath, []byte(config), 0644) + + fmt.Printf(" Nickname: %s\n", hostname) + fmt.Printf(" ORPort: 9001 (default)\n") + fmt.Printf(" ControlPort: 9051\n") + fmt.Printf(" SOCKSPort: 9050\n") + } +} + +func configureAnonLogs() { + fmt.Printf(" Configuring Anon logs...\n") + + // Create log directory + logDir := "/home/debros/.debros/logs/anon" + if err := os.MkdirAll(logDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "⚠️ Failed to create log directory: %v\n", err) + return + } + + // Change ownership to debian-anon (the user anon runs as) + exec.Command("chown", "-R", "debian-anon:debian-anon", logDir).Run() + + // Update anonrc if it exists + anonrcPath := "/etc/anon/anonrc" + if _, err := os.Stat(anonrcPath); err == nil { + // Read current config + data, err := os.ReadFile(anonrcPath) + if err == nil { + config := string(data) + + // Replace log file path + newConfig := strings.ReplaceAll(config, + "Log notice file /var/log/anon/notices.log", + "Log notice file /home/debros/.debros/logs/anon/notices.log") + + // Write back + if err := os.WriteFile(anonrcPath, []byte(newConfig), 0644); err != nil { + fmt.Fprintf(os.Stderr, "⚠️ Failed to update anonrc: %v\n", err) + } else { + fmt.Printf(" ✓ Anon logs configured to %s\n", logDir) + + // Restart anon service if running + if exec.Command("systemctl", "is-active", "--quiet", "anon").Run() == nil { + exec.Command("systemctl", "restart", "anon").Run() + } + } + } + } +} + +func configureFirewallForAnon() { + fmt.Printf(" Checking firewall configuration...\n") + + // Check for UFW + if _, err := exec.LookPath("ufw"); err == nil { + output, _ := exec.Command("ufw", "status").CombinedOutput() + if strings.Contains(string(output), "Status: active") { + fmt.Printf(" Adding UFW rules for Anon...\n") + exec.Command("ufw", "allow", "9001/tcp", "comment", "Anon ORPort").Run() + exec.Command("ufw", "allow", "9051/tcp", "comment", "Anon ControlPort").Run() + fmt.Printf(" ✓ UFW rules added\n") + return + } + } + + // Check for firewalld + if _, err := exec.LookPath("firewall-cmd"); err == nil { + output, _ := exec.Command("firewall-cmd", "--state").CombinedOutput() + if strings.Contains(string(output), "running") { + fmt.Printf(" Adding firewalld rules for Anon...\n") + exec.Command("firewall-cmd", "--permanent", "--add-port=9001/tcp").Run() + exec.Command("firewall-cmd", "--permanent", "--add-port=9051/tcp").Run() + exec.Command("firewall-cmd", "--reload").Run() + fmt.Printf(" ✓ firewalld rules added\n") + return + } + } + + // Check for iptables + if _, err := exec.LookPath("iptables"); err == nil { + output, _ := exec.Command("iptables", "-L", "-n").CombinedOutput() + if strings.Contains(string(output), "Chain INPUT") { + fmt.Printf(" Adding iptables rules for Anon...\n") + exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "--dport", "9001", "-j", "ACCEPT", "-m", "comment", "--comment", "Anon ORPort").Run() + exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "--dport", "9051", "-j", "ACCEPT", "-m", "comment", "--comment", "Anon ControlPort").Run() + + // Try to save rules + if _, err := exec.LookPath("netfilter-persistent"); err == nil { + exec.Command("netfilter-persistent", "save").Run() + } else if _, err := exec.LookPath("iptables-save"); err == nil { + cmd := exec.Command("sh", "-c", "iptables-save > /etc/iptables/rules.v4") + cmd.Run() + } + fmt.Printf(" ✓ iptables rules added\n") + return + } + } + + fmt.Printf(" No active firewall detected\n") +} + func setupDirectories() { fmt.Printf("📁 Creating directories...\n") diff --git a/pkg/gateway/anon_proxy_handler.go b/pkg/gateway/anon_proxy_handler.go new file mode 100644 index 0000000..493a32a --- /dev/null +++ b/pkg/gateway/anon_proxy_handler.go @@ -0,0 +1,237 @@ +package gateway + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/DeBrosOfficial/network/pkg/anyoneproxy" + "github.com/DeBrosOfficial/network/pkg/logging" + "go.uber.org/zap" +) + +// anonProxyRequest represents the JSON payload for proxy requests +type anonProxyRequest struct { + URL string `json:"url"` + Method string `json:"method"` + Headers map[string]string `json:"headers,omitempty"` + Body string `json:"body,omitempty"` +} + +// anonProxyResponse represents the JSON response from proxy requests +type anonProxyResponse struct { + StatusCode int `json:"status_code"` + Headers map[string]string `json:"headers"` + Body string `json:"body"` + Error string `json:"error,omitempty"` +} + +const ( + maxProxyRequestSize = 10 * 1024 * 1024 // 10MB + maxProxyTimeout = 60 * time.Second +) + +// anonProxyHandler handles proxied HTTP requests through the Anyone network +func (g *Gateway) anonProxyHandler(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + writeError(w, http.StatusMethodNotAllowed, "only POST method is allowed") + return + } + + // Limit request body size + r.Body = http.MaxBytesReader(w, r.Body, maxProxyRequestSize) + + // Parse request payload + var req anonProxyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid JSON payload: %v", err)) + return + } + + // Validate URL + targetURL, err := url.Parse(req.URL) + if err != nil { + writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid URL: %v", err)) + return + } + + // Only allow HTTPS for external requests + if targetURL.Scheme != "https" && targetURL.Scheme != "http" { + writeError(w, http.StatusBadRequest, "only http/https schemes are allowed") + return + } + + // Block requests to private/local addresses + if isPrivateOrLocalHost(targetURL.Host) { + writeError(w, http.StatusForbidden, "requests to private/local addresses are not allowed") + return + } + + // Validate HTTP method + method := strings.ToUpper(req.Method) + if method == "" { + method = "GET" + } + allowedMethods := map[string]bool{ + "GET": true, + "POST": true, + "PUT": true, + "DELETE": true, + "PATCH": true, + "HEAD": true, + } + if !allowedMethods[method] { + writeError(w, http.StatusBadRequest, fmt.Sprintf("method %s not allowed", method)) + return + } + + // Check if Anyone proxy is running (after all validation) + if !anyoneproxy.Running() { + g.logger.ComponentWarn(logging.ComponentGeneral, "Anyone proxy not available", + zap.String("socks_addr", anyoneproxy.Address())) + writeJSON(w, http.StatusServiceUnavailable, anonProxyResponse{ + Error: fmt.Sprintf("Anyone proxy not available at %s", anyoneproxy.Address()), + }) + return + } + + // Create HTTP client with Anyone proxy + client := anyoneproxy.NewHTTPClient() + client.Timeout = maxProxyTimeout + + // Create the proxied request + var bodyReader io.Reader + if req.Body != "" { + bodyReader = strings.NewReader(req.Body) + } + + proxyReq, err := http.NewRequestWithContext(r.Context(), method, req.URL, bodyReader) + if err != nil { + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create request: %v", err)) + return + } + + // Copy headers, excluding hop-by-hop headers + for key, value := range req.Headers { + if !isHopByHopHeader(key) { + proxyReq.Header.Set(key, value) + } + } + + // Set default User-Agent if not provided + if proxyReq.Header.Get("User-Agent") == "" { + proxyReq.Header.Set("User-Agent", "DeBros-Gateway/1.0") + } + + // Log the proxy request + g.logger.ComponentInfo(logging.ComponentGeneral, "proxying request through Anyone", + zap.String("method", method), + zap.String("url", req.URL), + zap.String("socks_addr", anyoneproxy.Address())) + + // Execute the request + start := time.Now() + resp, err := client.Do(proxyReq) + duration := time.Since(start) + + if err != nil { + g.logger.ComponentError(logging.ComponentGeneral, "proxy request failed", + zap.Error(err), + zap.String("url", req.URL), + zap.Duration("duration", duration)) + writeJSON(w, http.StatusBadGateway, anonProxyResponse{ + Error: fmt.Sprintf("proxy request failed: %v", err), + }) + return + } + defer resp.Body.Close() + + // Read response body + respBody, err := io.ReadAll(io.LimitReader(resp.Body, maxProxyRequestSize)) + if err != nil { + g.logger.ComponentError(logging.ComponentGeneral, "failed to read proxy response", + zap.Error(err)) + writeJSON(w, http.StatusBadGateway, anonProxyResponse{ + Error: fmt.Sprintf("failed to read response: %v", err), + }) + return + } + + // Extract response headers (excluding hop-by-hop) + respHeaders := make(map[string]string) + for key, values := range resp.Header { + if !isHopByHopHeader(key) && len(values) > 0 { + respHeaders[key] = values[0] + } + } + + g.logger.ComponentInfo(logging.ComponentGeneral, "proxy request completed", + zap.String("url", req.URL), + zap.Int("status", resp.StatusCode), + zap.Int("bytes", len(respBody)), + zap.Duration("duration", duration)) + + // Return the proxied response + writeJSON(w, http.StatusOK, anonProxyResponse{ + StatusCode: resp.StatusCode, + Headers: respHeaders, + Body: string(respBody), + }) +} + +// isHopByHopHeader returns true for HTTP hop-by-hop headers that should not be forwarded +func isHopByHopHeader(header string) bool { + hopByHop := map[string]bool{ + "Connection": true, + "Keep-Alive": true, + "Proxy-Authenticate": true, + "Proxy-Authorization": true, + "Te": true, + "Trailers": true, + "Transfer-Encoding": true, + "Upgrade": true, + } + return hopByHop[http.CanonicalHeaderKey(header)] +} + +// isPrivateOrLocalHost checks if a host is private, local, or loopback +func isPrivateOrLocalHost(host string) bool { + // Strip port if present + if idx := strings.LastIndex(host, ":"); idx != -1 { + host = host[:idx] + } + + // Check for localhost variants + if host == "localhost" || host == "127.0.0.1" || host == "::1" { + return true + } + + // Check common private ranges (basic check) + if strings.HasPrefix(host, "10.") || + strings.HasPrefix(host, "192.168.") || + strings.HasPrefix(host, "172.16.") || + strings.HasPrefix(host, "172.17.") || + strings.HasPrefix(host, "172.18.") || + strings.HasPrefix(host, "172.19.") || + strings.HasPrefix(host, "172.20.") || + strings.HasPrefix(host, "172.21.") || + strings.HasPrefix(host, "172.22.") || + strings.HasPrefix(host, "172.23.") || + strings.HasPrefix(host, "172.24.") || + strings.HasPrefix(host, "172.25.") || + strings.HasPrefix(host, "172.26.") || + strings.HasPrefix(host, "172.27.") || + strings.HasPrefix(host, "172.28.") || + strings.HasPrefix(host, "172.29.") || + strings.HasPrefix(host, "172.30.") || + strings.HasPrefix(host, "172.31.") { + return true + } + + return false +} diff --git a/pkg/gateway/anon_proxy_handler_test.go b/pkg/gateway/anon_proxy_handler_test.go new file mode 100644 index 0000000..005e124 --- /dev/null +++ b/pkg/gateway/anon_proxy_handler_test.go @@ -0,0 +1,189 @@ +package gateway + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/DeBrosOfficial/network/pkg/logging" +) + +func newTestGateway(t *testing.T) *Gateway { + logger, err := logging.NewColoredLogger(logging.ComponentGeneral, true) + if err != nil { + t.Fatalf("Failed to create logger: %v", err) + } + return &Gateway{logger: logger} +} + +func TestAnonProxyHandler_MethodValidation(t *testing.T) { + gw := newTestGateway(t) + + // Test GET request (should fail - only POST allowed) + req := httptest.NewRequest(http.MethodGet, "/v1/proxy/anon", nil) + w := httptest.NewRecorder() + + gw.anonProxyHandler(w, req) + + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("Expected status %d, got %d", http.StatusMethodNotAllowed, w.Code) + } +} + +func TestAnonProxyHandler_InvalidJSON(t *testing.T) { + gw := newTestGateway(t) + + // Test invalid JSON + req := httptest.NewRequest(http.MethodPost, "/v1/proxy/anon", bytes.NewBufferString("invalid json")) + w := httptest.NewRecorder() + + gw.anonProxyHandler(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) + } +} + +func TestAnonProxyHandler_InvalidURL(t *testing.T) { + gw := newTestGateway(t) + + tests := []struct { + name string + payload anonProxyRequest + }{ + { + name: "invalid URL scheme", + payload: anonProxyRequest{ + URL: "ftp://example.com", + Method: "GET", + }, + }, + { + name: "malformed URL", + payload: anonProxyRequest{ + URL: "://invalid", + Method: "GET", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body, _ := json.Marshal(tt.payload) + req := httptest.NewRequest(http.MethodPost, "/v1/proxy/anon", bytes.NewBuffer(body)) + w := httptest.NewRecorder() + + gw.anonProxyHandler(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) + } + }) + } +} + +func TestAnonProxyHandler_PrivateAddressBlocking(t *testing.T) { + gw := newTestGateway(t) + + tests := []struct { + name string + url string + }{ + {"localhost", "http://localhost/test"}, + {"127.0.0.1", "http://127.0.0.1/test"}, + {"private 10.x", "http://10.0.0.1/test"}, + {"private 192.168.x", "http://192.168.1.1/test"}, + {"private 172.16.x", "http://172.16.0.1/test"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + payload := anonProxyRequest{ + URL: tt.url, + Method: "GET", + } + body, _ := json.Marshal(payload) + req := httptest.NewRequest(http.MethodPost, "/v1/proxy/anon", bytes.NewBuffer(body)) + w := httptest.NewRecorder() + + gw.anonProxyHandler(w, req) + + if w.Code != http.StatusForbidden { + t.Errorf("Expected status %d for %s, got %d", http.StatusForbidden, tt.url, w.Code) + } + }) + } +} + +func TestAnonProxyHandler_InvalidMethod(t *testing.T) { + gw := newTestGateway(t) + + payload := anonProxyRequest{ + URL: "https://example.com", + Method: "INVALID", + } + body, _ := json.Marshal(payload) + req := httptest.NewRequest(http.MethodPost, "/v1/proxy/anon", bytes.NewBuffer(body)) + w := httptest.NewRecorder() + + gw.anonProxyHandler(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) + } +} + +func TestIsHopByHopHeader(t *testing.T) { + tests := []struct { + header string + expected bool + }{ + {"Connection", true}, + {"Keep-Alive", true}, + {"Proxy-Authorization", true}, + {"Transfer-Encoding", true}, + {"Upgrade", true}, + {"Content-Type", false}, + {"Authorization", false}, + {"User-Agent", false}, + } + + for _, tt := range tests { + t.Run(tt.header, func(t *testing.T) { + result := isHopByHopHeader(tt.header) + if result != tt.expected { + t.Errorf("isHopByHopHeader(%s) = %v, want %v", tt.header, result, tt.expected) + } + }) + } +} + +func TestIsPrivateOrLocalHost(t *testing.T) { + tests := []struct { + host string + expected bool + }{ + {"localhost", true}, + {"127.0.0.1", true}, + {"::1", true}, + {"10.0.0.1", true}, + {"192.168.1.1", true}, + {"172.16.0.1", true}, + {"172.31.255.255", true}, + {"example.com", false}, + {"8.8.8.8", false}, + {"1.1.1.1", false}, + {"172.32.0.1", false}, // Not in private range + } + + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + result := isPrivateOrLocalHost(tt.host) + if result != tt.expected { + t.Errorf("isPrivateOrLocalHost(%s) = %v, want %v", tt.host, result, tt.expected) + } + }) + } +} diff --git a/pkg/gateway/middleware.go b/pkg/gateway/middleware.go index c08dd48..0924488 100644 --- a/pkg/gateway/middleware.go +++ b/pkg/gateway/middleware.go @@ -303,6 +303,9 @@ func requiresNamespaceOwnership(p string) bool { if strings.HasPrefix(p, "/v1/rqlite/") { return true } + if strings.HasPrefix(p, "/v1/proxy/") { + return true + } return false } diff --git a/pkg/gateway/routes.go b/pkg/gateway/routes.go index 4ad7cc9..cce24e8 100644 --- a/pkg/gateway/routes.go +++ b/pkg/gateway/routes.go @@ -44,5 +44,8 @@ func (g *Gateway) Routes() http.Handler { mux.HandleFunc("/v1/pubsub/publish", g.pubsubPublishHandler) mux.HandleFunc("/v1/pubsub/topics", g.pubsubTopicsHandler) + // anon proxy (authenticated users only) + mux.HandleFunc("/v1/proxy/anon", g.anonProxyHandler) + return g.withMiddleware(mux) } diff --git a/scripts/install-debros-network.sh b/scripts/install-debros-network.sh index a33d6f1..31d86ae 100755 --- a/scripts/install-debros-network.sh +++ b/scripts/install-debros-network.sh @@ -190,6 +190,168 @@ verify_installation() { fi } +install_anon() { + echo -e "" + echo -e "${BLUE}========================================${NOCOLOR}" + echo -e "${GREEN}Step 1.5: Install Anyone Relay (Anon)${NOCOLOR}" + echo -e "${BLUE}========================================${NOCOLOR}" + echo -e "" + + log "Installing Anyone relay for anonymous networking..." + + # Check if anon is already installed + if command -v anon &>/dev/null; then + success "Anon already installed" + configure_anon_logs + configure_firewall_for_anon + return 0 + fi + + # Install via APT (official method from docs.anyone.io) + log "Adding Anyone APT repository..." + + # Add GPG key + if ! curl -fsSL https://deb.anyone.io/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/anyone-archive-keyring.gpg 2>/dev/null; then + warning "Failed to add Anyone GPG key" + log "You can manually install later with:" + log " curl -fsSL https://deb.anyone.io/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/anyone-archive-keyring.gpg" + log " echo 'deb [signed-by=/usr/share/keyrings/anyone-archive-keyring.gpg] https://deb.anyone.io/ anyone main' | sudo tee /etc/apt/sources.list.d/anyone.list" + log " sudo apt update && sudo apt install -y anon" + return 1 + fi + + # Add repository + echo "deb [signed-by=/usr/share/keyrings/anyone-archive-keyring.gpg] https://deb.anyone.io/ anyone main" | sudo tee /etc/apt/sources.list.d/anyone.list >/dev/null + + # Update and install + log "Installing Anon package..." + sudo apt update -qq + if ! sudo apt install -y anon; then + warning "Anon installation failed" + return 1 + fi + + # Verify installation + if ! command -v anon &>/dev/null; then + warning "Anon installation may have failed" + return 1 + fi + + success "Anon installed successfully" + + # Configure with sensible defaults + configure_anon_defaults + + # Configure log directory + configure_anon_logs + + # Configure firewall if present + configure_firewall_for_anon + + # Enable and start service + log "Enabling Anon service..." + sudo systemctl enable anon 2>/dev/null || true + sudo systemctl start anon 2>/dev/null || true + + if systemctl is-active --quiet anon; then + success "Anon service is running" + else + warning "Anon service may not be running. Check: sudo systemctl status anon" + fi + + return 0 +} + +configure_anon_defaults() { + log "Configuring Anon with default settings..." + + HOSTNAME=$(hostname -s 2>/dev/null || echo "debros-node") + + # Create or update anonrc with our defaults + if [ -f /etc/anon/anonrc ]; then + # Backup existing config + sudo cp /etc/anon/anonrc /etc/anon/anonrc.bak 2>/dev/null || true + + # Update key settings if not already set + if ! grep -q "^Nickname" /etc/anon/anonrc; then + echo "Nickname ${HOSTNAME}" | sudo tee -a /etc/anon/anonrc >/dev/null + fi + + if ! grep -q "^ControlPort" /etc/anon/anonrc; then + echo "ControlPort 9051" | sudo tee -a /etc/anon/anonrc >/dev/null + fi + + if ! grep -q "^SocksPort" /etc/anon/anonrc; then + echo "SocksPort 9050" | sudo tee -a /etc/anon/anonrc >/dev/null + fi + + log " Nickname: ${HOSTNAME}" + log " ORPort: 9001 (default)" + log " ControlPort: 9051" + log " SOCKSPort: 9050" + fi +} + +configure_anon_logs() { + log "Configuring Anon logs..." + + # Create log directory + sudo mkdir -p /home/debros/.debros/logs/anon + + # Change ownership to debian-anon (the user anon runs as) + sudo chown -R debian-anon:debian-anon /home/debros/.debros/logs/anon 2>/dev/null || true + + # Update anonrc to point logs to our directory + if [ -f /etc/anon/anonrc ]; then + sudo sed -i.bak 's|Log notice file.*|Log notice file /home/debros/.debros/logs/anon/notices.log|g' /etc/anon/anonrc + success "Anon logs configured to /home/debros/.debros/logs/anon" + fi +} + +configure_firewall_for_anon() { + log "Checking firewall configuration..." + + # Check for UFW + if command -v ufw &>/dev/null && sudo ufw status | grep -q "Status: active"; then + log "UFW detected and active, adding Anon ports..." + sudo ufw allow 9001/tcp comment 'Anon ORPort' 2>/dev/null || true + sudo ufw allow 9051/tcp comment 'Anon ControlPort' 2>/dev/null || true + success "UFW rules added for Anon" + return 0 + fi + + # Check for firewalld + if command -v firewall-cmd &>/dev/null && sudo firewall-cmd --state 2>/dev/null | grep -q "running"; then + log "firewalld detected and active, adding Anon ports..." + sudo firewall-cmd --permanent --add-port=9001/tcp 2>/dev/null || true + sudo firewall-cmd --permanent --add-port=9051/tcp 2>/dev/null || true + sudo firewall-cmd --reload 2>/dev/null || true + success "firewalld rules added for Anon" + return 0 + fi + + # Check for iptables + if command -v iptables &>/dev/null; then + # Check if iptables has any rules (indicating it's in use) + if sudo iptables -L -n | grep -q "Chain INPUT"; then + log "iptables detected, adding Anon ports..." + sudo iptables -A INPUT -p tcp --dport 9001 -j ACCEPT -m comment --comment "Anon ORPort" 2>/dev/null || true + sudo iptables -A INPUT -p tcp --dport 9051 -j ACCEPT -m comment --comment "Anon ControlPort" 2>/dev/null || true + + # Try to save rules if iptables-persistent is available + if command -v netfilter-persistent &>/dev/null; then + sudo netfilter-persistent save 2>/dev/null || true + elif command -v iptables-save &>/dev/null; then + sudo iptables-save | sudo tee /etc/iptables/rules.v4 >/dev/null 2>&1 || true + fi + success "iptables rules added for Anon" + return 0 + fi + fi + + log "No active firewall detected, skipping firewall configuration" +} + run_setup() { echo -e "" echo -e "${BLUE}========================================${NOCOLOR}" @@ -241,6 +403,11 @@ show_completion() { echo -e " • Switch to testnet: ${CYAN}network-cli testnet enable${NOCOLOR}" echo -e " • Show environment: ${CYAN}network-cli env current${NOCOLOR}" echo -e "" + echo -e "${CYAN}Anyone Relay (Anon):${NOCOLOR}" + echo -e " • Check Anon status: ${CYAN}sudo systemctl status anon${NOCOLOR}" + echo -e " • View Anon logs: ${CYAN}sudo tail -f /home/debros/.debros/logs/anon/notices.log${NOCOLOR}" + echo -e " • Proxy endpoint: ${CYAN}POST http://localhost:6001/v1/proxy/anon${NOCOLOR}" + echo -e "" echo -e "${CYAN}Documentation: https://docs.debros.io${NOCOLOR}" echo -e "" } @@ -270,6 +437,9 @@ main() { exit 1 fi + # Install Anon (optional but recommended) + install_anon || warning "Anon installation skipped or failed" + # Run setup run_setup diff --git a/terms-agreement b/terms-agreement new file mode 100644 index 0000000..e340065 --- /dev/null +++ b/terms-agreement @@ -0,0 +1 @@ +agreed \ No newline at end of file