feat: update node and gateway commands to use Orama naming convention

- Renamed the node executable from `node` to `orama-node` in the Makefile and various scripts to reflect the new naming convention.
- Updated the gateway command to `orama-gateway` for consistency.
- Modified service configurations and systemd templates to ensure proper execution of the renamed binaries.
- Enhanced the interactive installer to prompt for the gateway URL, allowing users to select between local and remote nodes.
- Added functionality to extract domain information for TLS configuration, improving security for remote connections.
This commit is contained in:
anonpenguin23 2025-11-28 22:27:27 +02:00
parent 3505a6a0eb
commit 9193f088a3
17 changed files with 540 additions and 106 deletions

View File

@ -13,6 +13,28 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
### Deprecated
### Fixed
## [0.72.0] - 2025-11-28
### Added
- Interactive prompt for selecting local or remote gateway URL during CLI login.
- Support for discovering and configuring IPFS Cluster peers during installation and runtime via the gateway status endpoint.
- New CLI flags (`--ipfs-cluster-peer`, `--ipfs-cluster-addrs`) added to the `prod install` command for cluster discovery.
### Changed
- Renamed the main network node executable from `node` to `orama-node` and the gateway executable to `orama-gateway`.
- Improved the `auth login` flow to use a TLS-aware HTTP client, supporting Let's Encrypt staging certificates for remote gateways.
- Updated the production installer to set `CAP_NET_BIND_SERVICE` on `orama-node` to allow binding to privileged ports (80/443) without root.
- Updated the production installer to configure IPFS Cluster to listen on port 9098 for consistent multi-node communication.
- Refactored the `prod install` process to generate configurations before initializing services, ensuring configuration files are present.
### Deprecated
### Removed
### Fixed
- Corrected the IPFS Cluster API port used in `node.yaml` template from 9096 to 9098 to match the cluster's LibP2P port.
- Fixed the `anyone-client` systemd service configuration to use the correct binary name and allow writing to the home directory.
## [0.71.0] - 2025-11-27
### Added

View File

@ -19,7 +19,7 @@ test-e2e:
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks kill
VERSION := 0.71.0
VERSION := 0.72.0
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'
@ -29,7 +29,7 @@ build: deps
@echo "Building network executables (version=$(VERSION))..."
@mkdir -p bin
go build -ldflags "$(LDFLAGS)" -o bin/identity ./cmd/identity
go build -ldflags "$(LDFLAGS)" -o bin/node ./cmd/node
go build -ldflags "$(LDFLAGS)" -o bin/orama-node ./cmd/node
go build -ldflags "$(LDFLAGS)" -o bin/orama cmd/cli/main.go
# Inject gateway build metadata via pkg path variables
go build -ldflags "$(LDFLAGS) -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildVersion=$(VERSION)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildCommit=$(COMMIT)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildTime=$(DATE)'" -o bin/gateway ./cmd/gateway
@ -51,25 +51,25 @@ clean:
run-node:
@echo "Starting node..."
@echo "Config: ~/.orama/node.yaml"
go run ./cmd/node --config node.yaml
go run ./cmd/orama-node --config node.yaml
# Run second node - requires join address
run-node2:
@echo "Starting second node..."
@echo "Config: ~/.orama/node2.yaml"
go run ./cmd/node --config node2.yaml
go run ./cmd/orama-node --config node2.yaml
# Run third node - requires join address
run-node3:
@echo "Starting third node..."
@echo "Config: ~/.orama/node3.yaml"
go run ./cmd/node --config node3.yaml
go run ./cmd/orama-node --config node3.yaml
# Run gateway HTTP server
run-gateway:
@echo "Starting gateway HTTP server..."
@echo "Note: Config must be in ~/.orama/data/gateway.yaml"
go run ./cmd/gateway
go run ./cmd/orama-gateway
# Setup local domain names for development
setup-domains:

View File

@ -10,6 +10,8 @@ import (
"os"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/tlsutil"
)
// PerformSimpleAuthentication performs a simple authentication flow where the user
@ -91,7 +93,13 @@ func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, err
}
endpoint := gatewayURL + "/v1/auth/simple-key"
resp, err := http.Post(endpoint, "application/json", bytes.NewReader(payload))
// Extract domain from URL for TLS configuration
// This uses tlsutil which handles Let's Encrypt staging certificates for *.debros.network
domain := extractDomainFromURL(gatewayURL)
client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain)
resp, err := client.Post(endpoint, "application/json", bytes.NewReader(payload))
if err != nil {
return "", fmt.Errorf("failed to call gateway: %w", err)
}
@ -114,3 +122,23 @@ func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, err
return apiKey, nil
}
// extractDomainFromURL extracts the domain from a URL
// Removes protocol (https://, http://), path, and port components
func extractDomainFromURL(url string) string {
// Remove protocol prefixes
url = strings.TrimPrefix(url, "https://")
url = strings.TrimPrefix(url, "http://")
// Remove path component
if idx := strings.Index(url, "/"); idx != -1 {
url = url[:idx]
}
// Remove port component
if idx := strings.Index(url, ":"); idx != -1 {
url = url[:idx]
}
return url
}

View File

@ -1,8 +1,10 @@
package cli
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/DeBrosOfficial/network/pkg/auth"
)
@ -56,7 +58,8 @@ func showAuthHelp() {
}
func handleAuthLogin() {
gatewayURL := getGatewayURL()
// Prompt for node selection
gatewayURL := promptForGatewayURL()
fmt.Printf("🔐 Authenticating with gateway at: %s\n", gatewayURL)
// Use the simple authentication flow
@ -161,7 +164,55 @@ func handleAuthStatus() {
}
}
// promptForGatewayURL interactively prompts for the gateway URL
// Allows user to choose between local node or remote node by domain
func promptForGatewayURL() string {
// Check environment variable first (allows override without prompting)
if url := os.Getenv("DEBROS_GATEWAY_URL"); url != "" {
return url
}
reader := bufio.NewReader(os.Stdin)
fmt.Println("\n🌐 Node Connection")
fmt.Println("==================")
fmt.Println("1. Local node (localhost:6001)")
fmt.Println("2. Remote node (enter domain)")
fmt.Print("\nSelect option [1/2]: ")
choice, _ := reader.ReadString('\n')
choice = strings.TrimSpace(choice)
if choice == "1" || choice == "" {
return "http://localhost:6001"
}
if choice != "2" {
fmt.Println("⚠️ Invalid option, using localhost")
return "http://localhost:6001"
}
fmt.Print("Enter node domain (e.g., node-hk19de.debros.network): ")
domain, _ := reader.ReadString('\n')
domain = strings.TrimSpace(domain)
if domain == "" {
fmt.Println("⚠️ No domain entered, using localhost")
return "http://localhost:6001"
}
// Remove any protocol prefix if user included it
domain = strings.TrimPrefix(domain, "https://")
domain = strings.TrimPrefix(domain, "http://")
// Remove trailing slash
domain = strings.TrimSuffix(domain, "/")
// Use HTTPS for remote domains
return fmt.Sprintf("https://%s", domain)
}
// getGatewayURL returns the gateway URL based on environment or env var
// Used by other commands that don't need interactive node selection
func getGatewayURL() string {
// Check environment variable first (for backwards compatibility)
if url := os.Getenv("DEBROS_GATEWAY_URL"); url != "" {

View File

@ -26,6 +26,12 @@ type IPFSPeerInfo struct {
Addrs []string
}
// IPFSClusterPeerInfo contains IPFS Cluster peer information for cluster discovery
type IPFSClusterPeerInfo struct {
PeerID string
Addrs []string
}
// validateSwarmKey validates that a swarm key is 64 hex characters
func validateSwarmKey(key string) error {
key = strings.TrimSpace(key)
@ -76,6 +82,13 @@ func runInteractiveInstaller() {
if len(config.IPFSSwarmAddrs) > 0 {
args = append(args, "--ipfs-addrs", strings.Join(config.IPFSSwarmAddrs, ","))
}
// Pass IPFS Cluster peer info for cluster peer_addresses configuration
if config.IPFSClusterPeerID != "" {
args = append(args, "--ipfs-cluster-peer", config.IPFSClusterPeerID)
}
if len(config.IPFSClusterAddrs) > 0 {
args = append(args, "--ipfs-cluster-addrs", strings.Join(config.IPFSClusterAddrs, ","))
}
}
// Re-run with collected args
@ -315,6 +328,8 @@ func showProdHelp() {
fmt.Printf(" --swarm-key HEX - 64-hex IPFS swarm key (required when joining)\n")
fmt.Printf(" --ipfs-peer ID - IPFS peer ID to connect to (auto-discovered)\n")
fmt.Printf(" --ipfs-addrs ADDRS - IPFS swarm addresses (auto-discovered)\n")
fmt.Printf(" --ipfs-cluster-peer ID - IPFS Cluster peer ID (auto-discovered)\n")
fmt.Printf(" --ipfs-cluster-addrs ADDRS - IPFS Cluster addresses (auto-discovered)\n")
fmt.Printf(" --branch BRANCH - Git branch to use (main or nightly, default: main)\n")
fmt.Printf(" --no-pull - Skip git clone/pull, use existing /home/debros/src\n")
fmt.Printf(" --ignore-resource-checks - Skip disk/RAM/CPU prerequisite validation\n")
@ -369,6 +384,8 @@ func handleProdInstall(args []string) {
swarmKey := fs.String("swarm-key", "", "64-hex IPFS swarm key (for joining existing private network)")
ipfsPeerID := fs.String("ipfs-peer", "", "IPFS peer ID to connect to (auto-discovered from peer domain)")
ipfsAddrs := fs.String("ipfs-addrs", "", "Comma-separated IPFS swarm addresses (auto-discovered from peer domain)")
ipfsClusterPeerID := fs.String("ipfs-cluster-peer", "", "IPFS Cluster peer ID to connect to (auto-discovered from peer domain)")
ipfsClusterAddrs := fs.String("ipfs-cluster-addrs", "", "Comma-separated IPFS Cluster addresses (auto-discovered from peer domain)")
interactive := fs.Bool("interactive", false, "Run interactive TUI installer")
dryRun := fs.Bool("dry-run", false, "Show what would be done without making changes")
noPull := fs.Bool("no-pull", false, "Skip git clone/pull, use existing /home/debros/src")
@ -488,6 +505,19 @@ func handleProdInstall(args []string) {
}
}
// Store IPFS Cluster peer info for cluster peer discovery
var ipfsClusterPeerInfo *IPFSClusterPeerInfo
if *ipfsClusterPeerID != "" {
var addrs []string
if *ipfsClusterAddrs != "" {
addrs = strings.Split(*ipfsClusterAddrs, ",")
}
ipfsClusterPeerInfo = &IPFSClusterPeerInfo{
PeerID: *ipfsClusterPeerID,
Addrs: addrs,
}
}
setup := production.NewProductionSetup(oramaHome, os.Stdout, *force, *branch, *noPull, *skipResourceChecks)
// Inform user if skipping git pull
@ -548,21 +578,8 @@ func handleProdInstall(args []string) {
os.Exit(1)
}
// Phase 2c: Initialize services (after secrets are in place)
fmt.Printf("\nPhase 2c: Initializing services...\n")
var prodIPFSPeer *production.IPFSPeerInfo
if ipfsPeerInfo != nil {
prodIPFSPeer = &production.IPFSPeerInfo{
PeerID: ipfsPeerInfo.PeerID,
Addrs: ipfsPeerInfo.Addrs,
}
}
if err := setup.Phase2cInitializeServices(peers, *vpsIP, prodIPFSPeer); err != nil {
fmt.Fprintf(os.Stderr, "❌ Service initialization failed: %v\n", err)
os.Exit(1)
}
// Phase 4: Generate configs
// Phase 4: Generate configs (BEFORE service initialization)
// This ensures node.yaml exists before services try to access it
fmt.Printf("\n⚙ Phase 4: Generating configurations...\n")
enableHTTPS := *domain != ""
if err := setup.Phase4GenerateConfigs(peers, *vpsIP, enableHTTPS, *domain, *joinAddress); err != nil {
@ -578,6 +595,27 @@ func handleProdInstall(args []string) {
}
fmt.Printf(" ✓ Configuration validated\n")
// Phase 2c: Initialize services (after config is in place)
fmt.Printf("\nPhase 2c: Initializing services...\n")
var prodIPFSPeer *production.IPFSPeerInfo
if ipfsPeerInfo != nil {
prodIPFSPeer = &production.IPFSPeerInfo{
PeerID: ipfsPeerInfo.PeerID,
Addrs: ipfsPeerInfo.Addrs,
}
}
var prodIPFSClusterPeer *production.IPFSClusterPeerInfo
if ipfsClusterPeerInfo != nil {
prodIPFSClusterPeer = &production.IPFSClusterPeerInfo{
PeerID: ipfsClusterPeerInfo.PeerID,
Addrs: ipfsClusterPeerInfo.Addrs,
}
}
if err := setup.Phase2cInitializeServices(peers, *vpsIP, prodIPFSPeer, prodIPFSClusterPeer); err != nil {
fmt.Fprintf(os.Stderr, "❌ Service initialization failed: %v\n", err)
os.Exit(1)
}
// Phase 5: Create systemd services
fmt.Printf("\n🔧 Phase 5: Creating systemd services...\n")
if err := setup.Phase5CreateSystemdServices(enableHTTPS); err != nil {
@ -876,20 +914,23 @@ func handleProdUpgrade(args []string) {
fmt.Printf(" - Join address: %s\n", joinAddress)
}
// Phase 2c: Ensure services are properly initialized (fixes existing repos)
// Now that we have peers and VPS IP, we can properly configure IPFS Cluster
// Note: IPFS peer info is nil for upgrades - peering is only configured during initial install
fmt.Printf("\nPhase 2c: Ensuring services are properly initialized...\n")
if err := setup.Phase2cInitializeServices(peers, vpsIP, nil); err != nil {
fmt.Fprintf(os.Stderr, "❌ Service initialization failed: %v\n", err)
os.Exit(1)
}
// Phase 4: Generate configs (BEFORE service initialization)
// This ensures node.yaml exists before services try to access it
if err := setup.Phase4GenerateConfigs(peers, vpsIP, enableHTTPS, domain, joinAddress); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Config generation warning: %v\n", err)
fmt.Fprintf(os.Stderr, " Existing configs preserved\n")
}
// Phase 2c: Ensure services are properly initialized (fixes existing repos)
// Now that we have peers and VPS IP, we can properly configure IPFS Cluster
// Note: IPFS peer info is nil for upgrades - peering is only configured during initial install
// Note: IPFS Cluster peer info is also nil for upgrades - peer_addresses is only configured during initial install
fmt.Printf("\nPhase 2c: Ensuring services are properly initialized...\n")
if err := setup.Phase2cInitializeServices(peers, vpsIP, nil, nil); err != nil {
fmt.Fprintf(os.Stderr, "❌ Service initialization failed: %v\n", err)
os.Exit(1)
}
// Phase 5: Update systemd services
fmt.Printf("\n🔧 Phase 5: Updating systemd services...\n")
if err := setup.Phase5CreateSystemdServices(enableHTTPS); err != nil {

View File

@ -509,6 +509,9 @@ func (n *NetworkInfoImpl) GetStatus(ctx context.Context) (*NetworkStatus, error)
// Try to get IPFS peer info (optional - don't fail if unavailable)
ipfsInfo := queryIPFSPeerInfo()
// Try to get IPFS Cluster peer info (optional - don't fail if unavailable)
ipfsClusterInfo := queryIPFSClusterPeerInfo()
return &NetworkStatus{
NodeID: host.ID().String(),
PeerID: host.ID().String(),
@ -517,6 +520,7 @@ func (n *NetworkInfoImpl) GetStatus(ctx context.Context) (*NetworkStatus, error)
DatabaseSize: dbSize,
Uptime: time.Since(n.client.startTime),
IPFS: ipfsInfo,
IPFSCluster: ipfsClusterInfo,
}, nil
}
@ -558,6 +562,44 @@ func queryIPFSPeerInfo() *IPFSPeerInfo {
}
}
// queryIPFSClusterPeerInfo queries the local IPFS Cluster API for peer information
// Returns nil if IPFS Cluster is not running or unavailable
func queryIPFSClusterPeerInfo() *IPFSClusterPeerInfo {
// IPFS Cluster API typically runs on port 9094 in our setup
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get("http://localhost:9094/id")
if err != nil {
return nil // IPFS Cluster not available
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil
}
var result struct {
ID string `json:"id"`
Addresses []string `json:"addresses"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil
}
// Filter addresses to only include public/routable ones for cluster discovery
var clusterAddrs []string
for _, addr := range result.Addresses {
// Skip loopback addresses - only keep routable addresses
if !strings.Contains(addr, "127.0.0.1") && !strings.Contains(addr, "/ip6/::1") {
clusterAddrs = append(clusterAddrs, addr)
}
}
return &IPFSClusterPeerInfo{
PeerID: result.ID,
Addresses: clusterAddrs,
}
}
// ConnectToPeer connects to a specific peer
func (n *NetworkInfoImpl) ConnectToPeer(ctx context.Context, peerAddr string) error {
if !n.client.isConnected() {

View File

@ -114,13 +114,14 @@ type PeerInfo struct {
// NetworkStatus contains overall network status
type NetworkStatus struct {
NodeID string `json:"node_id"`
PeerID string `json:"peer_id"`
Connected bool `json:"connected"`
PeerCount int `json:"peer_count"`
DatabaseSize int64 `json:"database_size"`
Uptime time.Duration `json:"uptime"`
IPFS *IPFSPeerInfo `json:"ipfs,omitempty"`
NodeID string `json:"node_id"`
PeerID string `json:"peer_id"`
Connected bool `json:"connected"`
PeerCount int `json:"peer_count"`
DatabaseSize int64 `json:"database_size"`
Uptime time.Duration `json:"uptime"`
IPFS *IPFSPeerInfo `json:"ipfs,omitempty"`
IPFSCluster *IPFSClusterPeerInfo `json:"ipfs_cluster,omitempty"`
}
// IPFSPeerInfo contains IPFS peer information for discovery
@ -129,6 +130,12 @@ type IPFSPeerInfo struct {
SwarmAddresses []string `json:"swarm_addresses"`
}
// IPFSClusterPeerInfo contains IPFS Cluster peer information for cluster discovery
type IPFSClusterPeerInfo struct {
PeerID string `json:"peer_id"` // Cluster peer ID (different from IPFS peer ID)
Addresses []string `json:"addresses"` // Cluster multiaddresses (e.g., /ip4/x.x.x.x/tcp/9098)
}
// HealthStatus contains health check information
type HealthStatus struct {
Status string `json:"status"` // "healthy", "degraded", "unhealthy"

View File

@ -271,7 +271,7 @@ func (d *Manager) discoverViaPeerstore(ctx context.Context, maxConnections int)
}
// Filter peers to only include those with addresses on our port (4001)
// This prevents attempting to connect to IPFS (port 4101) or IPFS Cluster (port 9096)
// This prevents attempting to connect to IPFS (port 4101) or IPFS Cluster (port 9096/9098)
peerInfo := d.host.Peerstore().PeerInfo(pid)
hasValidPort := false
for _, addr := range peerInfo.Addrs {

View File

@ -1024,7 +1024,7 @@ func (pm *ProcessManager) startAnon(ctx context.Context) error {
func (pm *ProcessManager) startNode(name, configFile, logPath string) error {
pidPath := filepath.Join(pm.pidsDir, fmt.Sprintf("%s.pid", name))
cmd := exec.Command("./bin/node", "--config", configFile)
cmd := exec.Command("./bin/orama-node", "--config", configFile)
logFile, _ := os.Create(logPath)
cmd.Stdout = logFile
cmd.Stderr = logFile

View File

@ -388,6 +388,17 @@ func (bi *BinaryInstaller) InstallDeBrosBinaries(branch string, oramaHome string
fmt.Fprintf(bi.logWriter, " ⚠️ Warning: failed to chown bin directory: %v\n", err)
}
// Grant CAP_NET_BIND_SERVICE to orama-node to allow binding to ports 80/443 without root
nodeBinary := filepath.Join(binDir, "orama-node")
if _, err := os.Stat(nodeBinary); err == nil {
if err := exec.Command("setcap", "cap_net_bind_service=+ep", nodeBinary).Run(); err != nil {
fmt.Fprintf(bi.logWriter, " ⚠️ Warning: failed to setcap on orama-node: %v\n", err)
fmt.Fprintf(bi.logWriter, " ⚠️ Gateway may not be able to bind to port 80/443\n")
} else {
fmt.Fprintf(bi.logWriter, " ✓ Set CAP_NET_BIND_SERVICE on orama-node\n")
}
}
fmt.Fprintf(bi.logWriter, " ✓ DeBros binaries installed\n")
return nil
}
@ -418,6 +429,12 @@ type IPFSPeerInfo struct {
Addrs []string
}
// IPFSClusterPeerInfo contains IPFS Cluster peer information for cluster peer discovery
type IPFSClusterPeerInfo struct {
PeerID string // Cluster peer ID (different from IPFS peer ID)
Addrs []string // Cluster multiaddresses (e.g., /ip4/x.x.x.x/tcp/9098)
}
// InitializeIPFSRepo initializes an IPFS repository for a node (unified - no bootstrap/node distinction)
// If ipfsPeer is provided, configures Peering.Peers for peer discovery in private networks
func (bi *BinaryInstaller) InitializeIPFSRepo(ipfsRepoPath string, swarmKeyPath string, apiPort, gatewayPort, swarmPort int, ipfsPeer *IPFSPeerInfo) error {
@ -702,9 +719,12 @@ func (bi *BinaryInstaller) updateClusterConfig(clusterPath, secret string, ipfsA
return fmt.Errorf("failed to parse service.json: %w", err)
}
// Update cluster secret and peer addresses
// Update cluster secret, listen_multiaddress, and peer addresses
if cluster, ok := config["cluster"].(map[string]interface{}); ok {
cluster["secret"] = secret
// Set consistent listen_multiaddress - port 9098 for cluster LibP2P communication
// This MUST match the port used in GetClusterPeerMultiaddr() and peer_addresses
cluster["listen_multiaddress"] = []interface{}{"/ip4/0.0.0.0/tcp/9098"}
// Configure peer addresses for cluster discovery
// This allows nodes to find and connect to each other
if len(bootstrapClusterPeers) > 0 {
@ -712,7 +732,8 @@ func (bi *BinaryInstaller) updateClusterConfig(clusterPath, secret string, ipfsA
}
} else {
clusterConfig := map[string]interface{}{
"secret": secret,
"secret": secret,
"listen_multiaddress": []interface{}{"/ip4/0.0.0.0/tcp/9098"},
}
if len(bootstrapClusterPeers) > 0 {
clusterConfig["peer_addresses"] = bootstrapClusterPeers
@ -821,7 +842,8 @@ func (bi *BinaryInstaller) InitializeRQLiteDataDir(dataDir string) error {
// InstallAnyoneClient installs the anyone-client npm package globally
func (bi *BinaryInstaller) InstallAnyoneClient() error {
// Check if anyone-client is already available via npx (more reliable for scoped packages)
if cmd := exec.Command("npx", "--yes", "@anyone-protocol/anyone-client", "--version"); cmd.Run() == nil {
// Note: the CLI binary is "anyone-client", not the full scoped package name
if cmd := exec.Command("npx", "anyone-client", "--help"); cmd.Run() == nil {
fmt.Fprintf(bi.logWriter, " ✓ anyone-client already installed\n")
return nil
}
@ -873,8 +895,18 @@ func (bi *BinaryInstaller) InstallAnyoneClient() error {
return fmt.Errorf("failed to install anyone-client: %w\n%s", err, string(output))
}
// Verify installation - try npx first (most reliable for scoped packages)
verifyCmd := exec.Command("npx", "--yes", "@anyone-protocol/anyone-client", "--version")
// Create terms-agreement file to bypass interactive prompt when running as a service
termsFile := filepath.Join(debrosHome, "terms-agreement")
if err := os.WriteFile(termsFile, []byte("agreed"), 0644); err != nil {
fmt.Fprintf(bi.logWriter, " ⚠️ Warning: failed to create terms-agreement: %v\n", err)
} else {
if err := exec.Command("chown", "debros:debros", termsFile).Run(); err != nil {
fmt.Fprintf(bi.logWriter, " ⚠️ Warning: failed to chown terms-agreement: %v\n", err)
}
}
// Verify installation - try npx with the correct CLI name (anyone-client, not full scoped package name)
verifyCmd := exec.Command("npx", "anyone-client", "--help")
if err := verifyCmd.Run(); err != nil {
// Fallback: check if binary exists in common locations
possiblePaths := []string{

View File

@ -273,7 +273,8 @@ func (ps *ProductionSetup) Phase2bInstallBinaries() error {
// Phase2cInitializeServices initializes service repositories and configurations
// ipfsPeer can be nil for the first node, or contain peer info for joining nodes
func (ps *ProductionSetup) Phase2cInitializeServices(peerAddresses []string, vpsIP string, ipfsPeer *IPFSPeerInfo) error {
// ipfsClusterPeer can be nil for the first node, or contain IPFS Cluster peer info for joining nodes
func (ps *ProductionSetup) Phase2cInitializeServices(peerAddresses []string, vpsIP string, ipfsPeer *IPFSPeerInfo, ipfsClusterPeer *IPFSClusterPeerInfo) error {
ps.logf("Phase 2c: Initializing services...")
// Ensure directories exist (unified structure)
@ -298,13 +299,28 @@ func (ps *ProductionSetup) Phase2cInitializeServices(peerAddresses []string, vps
return fmt.Errorf("failed to get cluster secret: %w", err)
}
// Get cluster peer addresses from peers if available
// Get cluster peer addresses from IPFS Cluster peer info if available
var clusterPeers []string
if len(peerAddresses) > 0 {
// Infer IP from peers
if ipfsClusterPeer != nil && ipfsClusterPeer.PeerID != "" {
// Construct cluster peer multiaddress using the discovered peer ID
// Format: /ip4/<ip>/tcp/9098/p2p/<cluster-peer-id>
peerIP := inferPeerIP(peerAddresses, vpsIP)
if peerIP != "" {
ps.logf(" Will attempt to connect to cluster peers at %s", peerIP)
// Construct the bootstrap multiaddress for IPFS Cluster
// Note: IPFS Cluster listens on port 9098 for cluster communication
clusterBootstrapAddr := fmt.Sprintf("/ip4/%s/tcp/9098/p2p/%s", peerIP, ipfsClusterPeer.PeerID)
clusterPeers = []string{clusterBootstrapAddr}
ps.logf(" IPFS Cluster will connect to peer: %s", clusterBootstrapAddr)
} else if len(ipfsClusterPeer.Addrs) > 0 {
// Fallback: use the addresses from discovery (if they include peer ID)
for _, addr := range ipfsClusterPeer.Addrs {
if strings.Contains(addr, ipfsClusterPeer.PeerID) {
clusterPeers = append(clusterPeers, addr)
}
}
if len(clusterPeers) > 0 {
ps.logf(" IPFS Cluster will connect to discovered peers: %v", clusterPeers)
}
}
}

View File

@ -42,8 +42,8 @@ ExecStartPre=/bin/bash -c 'if [ -f %[3]s/secrets/swarm.key ] && [ ! -f %[2]s/swa
ExecStart=%[5]s daemon --enable-pubsub-experiment --repo-dir=%[2]s
Restart=always
RestartSec=5
StandardOutput=file:%[4]s
StandardError=file:%[4]s
StandardOutput=append:%[4]s
StandardError=append:%[4]s
SyslogIdentifier=debros-ipfs
NoNewPrivileges=yes
@ -92,8 +92,8 @@ ExecStartPre=/bin/bash -c 'mkdir -p %[2]s && chmod 700 %[2]s'
ExecStart=%[4]s daemon
Restart=always
RestartSec=5
StandardOutput=file:%[3]s
StandardError=file:%[3]s
StandardOutput=append:%[3]s
StandardError=append:%[3]s
SyslogIdentifier=debros-ipfs-cluster
NoNewPrivileges=yes
@ -147,8 +147,8 @@ Environment=HOME=%[1]s
ExecStart=%[5]s %[2]s
Restart=always
RestartSec=5
StandardOutput=file:%[3]s
StandardError=file:%[3]s
StandardOutput=append:%[3]s
StandardError=append:%[3]s
SyslogIdentifier=debros-rqlite
NoNewPrivileges=yes
@ -186,8 +186,8 @@ Environment=OLRIC_SERVER_CONFIG=%[2]s
ExecStart=%[5]s
Restart=always
RestartSec=5
StandardOutput=file:%[3]s
StandardError=file:%[3]s
StandardOutput=append:%[3]s
StandardError=append:%[3]s
SyslogIdentifier=olric
NoNewPrivileges=yes
@ -210,6 +210,8 @@ WantedBy=multi-user.target
func (ssg *SystemdServiceGenerator) GenerateNodeService() string {
configFile := "node.yaml"
logFile := filepath.Join(ssg.oramaDir, "logs", "node.log")
// Note: systemd StandardOutput/StandardError paths should not contain substitution variables
// Use absolute paths directly as they will be resolved by systemd at runtime
return fmt.Sprintf(`[Unit]
Description=DeBros Network Node
@ -222,11 +224,11 @@ User=debros
Group=debros
WorkingDirectory=%[1]s
Environment=HOME=%[1]s
ExecStart=%[1]s/bin/node --config %[2]s/configs/%[3]s
ExecStart=%[1]s/bin/orama-node --config %[2]s/configs/%[3]s
Restart=always
RestartSec=5
StandardOutput=file:%[4]s
StandardError=file:%[4]s
StandardOutput=append:%[4]s
StandardError=append:%[4]s
SyslogIdentifier=debros-node
AmbientCapabilities=CAP_NET_BIND_SERVICE
@ -264,8 +266,8 @@ Environment=HOME=%[1]s
ExecStart=%[1]s/bin/gateway --config %[2]s/data/gateway.yaml
Restart=always
RestartSec=5
StandardOutput=file:%[3]s
StandardError=file:%[3]s
StandardOutput=append:%[3]s
StandardError=append:%[3]s
SyslogIdentifier=debros-gateway
AmbientCapabilities=CAP_NET_BIND_SERVICE
@ -303,17 +305,18 @@ User=debros
Group=debros
Environment=HOME=%[1]s
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/lib/node_modules/.bin
ExecStart=/usr/bin/npx --yes @anyone-protocol/anyone-client
WorkingDirectory=%[1]s
ExecStart=/usr/bin/npx anyone-client
Restart=always
RestartSec=5
StandardOutput=file:%[2]s
StandardError=file:%[2]s
StandardOutput=append:%[2]s
StandardError=append:%[2]s
SyslogIdentifier=anyone-client
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
ProtectHome=no
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes

View File

@ -70,7 +70,7 @@ http_gateway:
routes:
# Note: Raft traffic bypasses SNI gateway - RQLite uses native TLS on port 7002
ipfs.{{.Domain}}: "localhost:4101"
ipfs-cluster.{{.Domain}}: "localhost:9096"
ipfs-cluster.{{.Domain}}: "localhost:9098"
olric.{{.Domain}}: "localhost:3322"
{{end}}

View File

@ -10,7 +10,7 @@ User=debros
Group=debros
WorkingDirectory={{.HomeDir}}
Environment=HOME={{.HomeDir}}
ExecStart={{.HomeDir}}/bin/node --config {{.OramaDir}}/configs/{{.ConfigFile}}
ExecStart={{.HomeDir}}/bin/orama-node --config {{.OramaDir}}/configs/{{.ConfigFile}}
Restart=always
RestartSec=5
StandardOutput=journal

View File

@ -32,9 +32,12 @@ type InstallerConfig struct {
SwarmKeyHex string // 64-hex IPFS swarm key (for joining private network)
IPFSPeerID string // IPFS peer ID (auto-discovered from peer domain)
IPFSSwarmAddrs []string // IPFS swarm addresses (auto-discovered from peer domain)
Branch string
IsFirstNode bool
NoPull bool
// IPFS Cluster peer info for cluster discovery
IPFSClusterPeerID string // IPFS Cluster peer ID (auto-discovered from peer domain)
IPFSClusterAddrs []string // IPFS Cluster addresses (auto-discovered from peer domain)
Branch string
IsFirstNode bool
NoPull bool
}
// Step represents a step in the installation wizard
@ -69,6 +72,7 @@ type Model struct {
discovering bool // Whether domain discovery is in progress
discoveryInfo string // Info message during discovery
discoveredPeer string // Discovered peer ID from domain
sniWarning string // Warning about missing SNI DNS records (non-blocking)
}
// Styles
@ -214,14 +218,13 @@ func (m *Model) handleEnter() (tea.Model, tea.Cmd) {
return m, nil
}
// Check SNI DNS records for this domain
// Check SNI DNS records for this domain (non-blocking warning)
m.discovering = true
m.discoveryInfo = "Validating SNI DNS records for " + domain + "..."
m.discoveryInfo = "Checking SNI DNS records for " + domain + "..."
if err := validateSNIDNSRecords(domain); err != nil {
m.discovering = false
m.err = fmt.Errorf("SNI DNS validation failed: %w", err)
return m, nil
if warning := validateSNIDNSRecords(domain); warning != "" {
// Log warning but continue - SNI DNS is optional for single-node setups
m.sniWarning = warning
}
m.discovering = false
@ -255,14 +258,13 @@ func (m *Model) handleEnter() (tea.Model, tea.Cmd) {
return m, nil
}
// Validate SNI DNS records for peer domain
// Check SNI DNS records for peer domain (non-blocking warning)
m.discovering = true
m.discoveryInfo = "Validating SNI DNS records for " + peerDomain + "..."
m.discoveryInfo = "Checking SNI DNS records for " + peerDomain + "..."
if err := validateSNIDNSRecords(peerDomain); err != nil {
m.discovering = false
m.err = fmt.Errorf("SNI DNS validation failed: %w", err)
return m, nil
if warning := validateSNIDNSRecords(peerDomain); warning != "" {
// Log warning but continue - peer might have different DNS setup
m.sniWarning = warning
}
// Discover peer info from domain (try HTTPS first, then HTTP)
@ -313,6 +315,12 @@ func (m *Model) handleEnter() (tea.Model, tea.Cmd) {
m.config.IPFSSwarmAddrs = discovery.IPFSSwarmAddrs
}
// Store IPFS Cluster peer info for cluster peer_addresses configuration
if discovery.IPFSClusterPeerID != "" {
m.config.IPFSClusterPeerID = discovery.IPFSClusterPeerID
m.config.IPFSClusterAddrs = discovery.IPFSClusterAddrs
}
m.err = nil
m.step = StepClusterSecret
m.setupStepInput()
@ -651,6 +659,13 @@ func (m Model) viewConfirm() string {
}
s.WriteString(boxStyle.Render(config))
// Show SNI DNS warning if present
if m.sniWarning != "" {
s.WriteString("\n\n")
s.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")).Render(m.sniWarning))
}
s.WriteString("\n\n")
s.WriteString(helpStyle.Render("Press Enter to install • Esc to go back"))
return s.String()
@ -713,6 +728,9 @@ type DiscoveryResult struct {
PeerID string // LibP2P peer ID
IPFSPeerID string // IPFS peer ID
IPFSSwarmAddrs []string // IPFS swarm addresses
// IPFS Cluster info for cluster peer discovery
IPFSClusterPeerID string // IPFS Cluster peer ID
IPFSClusterAddrs []string // IPFS Cluster multiaddresses
}
// discoverPeerFromDomain queries an existing node to get its peer ID and IPFS info
@ -741,7 +759,7 @@ func discoverPeerFromDomain(domain string) (*DiscoveryResult, error) {
return nil, fmt.Errorf("unexpected status from %s: %s", domain, resp.Status)
}
// Parse response including IPFS info
// Parse response including IPFS and IPFS Cluster info
var status struct {
PeerID string `json:"peer_id"`
NodeID string `json:"node_id"` // fallback for backward compatibility
@ -749,6 +767,10 @@ func discoverPeerFromDomain(domain string) (*DiscoveryResult, error) {
PeerID string `json:"peer_id"`
SwarmAddresses []string `json:"swarm_addresses"`
} `json:"ipfs,omitempty"`
IPFSCluster *struct {
PeerID string `json:"peer_id"`
Addresses []string `json:"addresses"`
} `json:"ipfs_cluster,omitempty"`
}
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
return nil, fmt.Errorf("failed to parse response from %s: %w", domain, err)
@ -774,6 +796,12 @@ func discoverPeerFromDomain(domain string) (*DiscoveryResult, error) {
result.IPFSSwarmAddrs = status.IPFS.SwarmAddresses
}
// Include IPFS Cluster info if available
if status.IPFSCluster != nil {
result.IPFSClusterPeerID = status.IPFSCluster.PeerID
result.IPFSClusterAddrs = status.IPFSCluster.Addresses
}
return result, nil
}
@ -860,7 +888,8 @@ func detectPublicIP() string {
// It tries to resolve the key SNI hostnames for IPFS, IPFS Cluster, and Olric
// Note: Raft no longer uses SNI - it uses direct RQLite TLS on port 7002
// All should resolve to the same IP (the node's public IP or domain)
func validateSNIDNSRecords(domain string) error {
// Returns a warning string if records are missing (empty string if all OK)
func validateSNIDNSRecords(domain string) string {
// List of SNI services that need DNS records
// Note: raft.domain is NOT included - RQLite uses direct TLS on port 7002
sniServices := []string{
@ -872,11 +901,12 @@ func validateSNIDNSRecords(domain string) error {
// Try to resolve the main domain first to get baseline
mainIPs, err := net.LookupHost(domain)
if err != nil {
return fmt.Errorf("could not resolve main domain %s: %w", domain, err)
// Main domain doesn't resolve - this is just a warning now
return fmt.Sprintf("Warning: could not resolve main domain %s: %v", domain, err)
}
if len(mainIPs) == 0 {
return fmt.Errorf("main domain %s resolved to no IP addresses", domain)
return fmt.Sprintf("Warning: main domain %s resolved to no IP addresses", domain)
}
// Check each SNI service
@ -890,18 +920,15 @@ func validateSNIDNSRecords(domain string) error {
if len(unresolvedServices) > 0 {
serviceList := strings.Join(unresolvedServices, ", ")
return fmt.Errorf(
"SNI DNS records not found for: %s\n\n"+
"You need to add DNS records (A records or wildcard CNAME) for these services:\n"+
" - They should all resolve to the same IP as %s\n"+
" - Option 1: Add individual A records pointing to %s\n"+
" - Option 2: Add wildcard CNAME: *.%s -> %s\n\n"+
"Without these records, multi-node clustering will fail.",
serviceList, domain, domain, domain, domain,
return fmt.Sprintf(
"⚠️ SNI DNS records not found for: %s\n"+
" For multi-node clustering, add wildcard CNAME: *.%s -> %s\n"+
" (Continuing anyway - single-node setup will work)",
serviceList, domain, domain,
)
}
return nil
return ""
}
// Run starts the TUI installer and returns the configuration

View File

@ -490,21 +490,30 @@ func (cm *ClusterConfigManager) UpdateAllClusterPeers() (bool, error) {
}
// RepairPeerConfiguration automatically discovers and repairs peer configuration
// Tries multiple methods: config-based discovery, peer multiaddr, or discovery service
// Tries multiple methods: gateway /v1/network/status, config-based discovery, peer multiaddr
func (cm *ClusterConfigManager) RepairPeerConfiguration() (bool, error) {
if cm.cfg.Database.IPFS.ClusterAPIURL == "" {
return false, nil // IPFS not configured
}
// Skip if this is the first node (creates the cluster, no join address)
// Method 1: Try to discover cluster peers via /v1/network/status endpoint
// This is the most reliable method as it uses the HTTPS gateway
if len(cm.cfg.Discovery.BootstrapPeers) > 0 {
success, err := cm.DiscoverClusterPeersFromGateway()
if err != nil {
cm.logger.Debug("Gateway discovery failed, trying direct API", zap.Error(err))
} else if success {
cm.logger.Info("Successfully discovered cluster peers from gateway")
return true, nil
}
}
// Skip direct API method if this is the first node (creates the cluster, no join address)
if cm.cfg.Database.RQLiteJoinAddress == "" {
return false, nil
}
// Method 1: Try to use peer API URL from config if available
// Check if we have a peer's cluster API URL in discovery metadata
// For now, we'll infer from peers multiaddr
// Method 2: Try direct cluster API (fallback)
var peerAPIURL string
// Try to extract from peers multiaddr
@ -530,7 +539,7 @@ func (cm *ClusterConfigManager) RepairPeerConfiguration() (bool, error) {
}
if success {
cm.logger.Info("Successfully repaired peer configuration")
cm.logger.Info("Successfully repaired peer configuration via direct API")
return true, nil
}
@ -539,6 +548,162 @@ func (cm *ClusterConfigManager) RepairPeerConfiguration() (bool, error) {
return false, nil
}
// DiscoverClusterPeersFromGateway queries bootstrap peers' /v1/network/status endpoint
// to discover IPFS Cluster peer information and updates the local service.json
func (cm *ClusterConfigManager) DiscoverClusterPeersFromGateway() (bool, error) {
if len(cm.cfg.Discovery.BootstrapPeers) == 0 {
cm.logger.Debug("No bootstrap peers configured, skipping gateway discovery")
return false, nil
}
var discoveredPeers []string
seenPeers := make(map[string]bool)
for _, peerAddr := range cm.cfg.Discovery.BootstrapPeers {
// Extract domain or IP from multiaddr
domain := extractDomainFromMultiaddr(peerAddr)
if domain == "" {
continue
}
// Query /v1/network/status endpoint
statusURL := fmt.Sprintf("https://%s/v1/network/status", domain)
cm.logger.Debug("Querying peer network status", zap.String("url", statusURL))
// Use TLS-aware HTTP client (handles staging certs for *.debros.network)
client := tlsutil.NewHTTPClientForDomain(10*time.Second, domain)
resp, err := client.Get(statusURL)
if err != nil {
// Try HTTP fallback
statusURL = fmt.Sprintf("http://%s/v1/network/status", domain)
resp, err = client.Get(statusURL)
if err != nil {
cm.logger.Debug("Failed to query peer status", zap.String("domain", domain), zap.Error(err))
continue
}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
cm.logger.Debug("Peer returned non-OK status", zap.String("domain", domain), zap.Int("status", resp.StatusCode))
continue
}
// Parse response
var status struct {
IPFSCluster *struct {
PeerID string `json:"peer_id"`
Addresses []string `json:"addresses"`
} `json:"ipfs_cluster"`
}
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
cm.logger.Debug("Failed to decode peer status", zap.String("domain", domain), zap.Error(err))
continue
}
if status.IPFSCluster == nil || status.IPFSCluster.PeerID == "" {
cm.logger.Debug("Peer has no IPFS Cluster info", zap.String("domain", domain))
continue
}
// Extract IP from domain or addresses
peerIP := extractIPFromMultiaddrForCluster(peerAddr)
if peerIP == "" {
// Try to resolve domain
ips, err := net.LookupIP(domain)
if err == nil && len(ips) > 0 {
for _, ip := range ips {
if ip.To4() != nil {
peerIP = ip.String()
break
}
}
}
}
if peerIP == "" {
cm.logger.Debug("Could not determine peer IP", zap.String("domain", domain))
continue
}
// Construct cluster multiaddr
// IPFS Cluster listens on port 9098 (REST API port 9094 + 4)
clusterAddr := fmt.Sprintf("/ip4/%s/tcp/9098/p2p/%s", peerIP, status.IPFSCluster.PeerID)
if !seenPeers[clusterAddr] {
discoveredPeers = append(discoveredPeers, clusterAddr)
seenPeers[clusterAddr] = true
cm.logger.Info("Discovered cluster peer from gateway",
zap.String("domain", domain),
zap.String("peer_id", status.IPFSCluster.PeerID),
zap.String("cluster_addr", clusterAddr))
}
}
if len(discoveredPeers) == 0 {
cm.logger.Debug("No cluster peers discovered from gateway")
return false, nil
}
// Load current config
serviceJSONPath := filepath.Join(cm.clusterPath, "service.json")
cfg, err := cm.loadOrCreateConfig(serviceJSONPath)
if err != nil {
return false, fmt.Errorf("failed to load config: %w", err)
}
// Update peerstore file
peerstorePath := filepath.Join(cm.clusterPath, "peerstore")
peerstoreContent := strings.Join(discoveredPeers, "\n") + "\n"
if err := os.WriteFile(peerstorePath, []byte(peerstoreContent), 0644); err != nil {
cm.logger.Warn("Failed to update peerstore file", zap.Error(err))
}
// Update peer_addresses in config
cfg.Cluster.PeerAddresses = discoveredPeers
// Save config
if err := cm.saveConfig(serviceJSONPath, cfg); err != nil {
return false, fmt.Errorf("failed to save config: %w", err)
}
cm.logger.Info("Updated cluster peer addresses from gateway discovery",
zap.Int("peer_count", len(discoveredPeers)),
zap.Strings("peer_addresses", discoveredPeers))
return true, nil
}
// extractDomainFromMultiaddr extracts domain or IP from a multiaddr string
// Handles formats like /dns4/domain/tcp/port/p2p/id or /ip4/ip/tcp/port/p2p/id
func extractDomainFromMultiaddr(multiaddrStr string) string {
ma, err := multiaddr.NewMultiaddr(multiaddrStr)
if err != nil {
return ""
}
// Try DNS4 first (domain name)
if domain, err := ma.ValueForProtocol(multiaddr.P_DNS4); err == nil && domain != "" {
return domain
}
// Try DNS6
if domain, err := ma.ValueForProtocol(multiaddr.P_DNS6); err == nil && domain != "" {
return domain
}
// Try IP4
if ip, err := ma.ValueForProtocol(multiaddr.P_IP4); err == nil && ip != "" {
return ip
}
// Try IP6
if ip, err := ma.ValueForProtocol(multiaddr.P_IP6); err == nil && ip != "" {
return ip
}
return ""
}
// DiscoverClusterPeersFromLibP2P loads IPFS cluster peer addresses from the peerstore file.
// If peerstore is empty, it means there are no peers to connect to.
// Returns true if peers were loaded and configured, false otherwise (non-fatal)

View File

@ -52,7 +52,7 @@ SPECIFIC_PATTERNS=(
"ipfs daemon"
"ipfs-cluster-service daemon"
"olric-server"
"bin/node"
"bin/orama-node"
"bin/gateway"
"anyone-client"
)