Refactor node startup and config for simplified peer discovery

- Remove configmap.go and bootstrap-specific config logic
- Refactor main.go to use a unified node startup for all roles
- Remove DHT and mDNS from config and node logic; use bootstrap + peer
  exchange
- Update constants to extract bootstrap peer IDs from addresses
- Simplify RQLite advertise logic and remove external IP detection
- Add zeroconf as indirect dependency
- Remove environment variable overrides from config
- Update README with port usage clarification
- Add new CLI and node binaries
This commit is contained in:
anonpenguin 2025-08-12 21:39:53 +03:00
parent dfd1862cfd
commit f8defe1110
12 changed files with 473 additions and 1319 deletions

View File

@ -1,4 +1,4 @@
# Network - Distributed P2P Database System v0.12.5-beta # Network - Distributed P2P Database System v0.19.0-beta
A distributed peer-to-peer network built with Go and LibP2P, providing decentralized database capabilities with RQLite consensus and replication. A distributed peer-to-peer network built with Go and LibP2P, providing decentralized database capabilities with RQLite consensus and replication.
@ -147,7 +147,7 @@ The system uses these ports by default:
- **5001**: RQLite HTTP API - **5001**: RQLite HTTP API
- **7001**: RQLite Raft consensus - **7001**: RQLite Raft consensus
Ensure these ports are available or configure firewall rules accordingly. Ensure these ports are available or configure firewall rules accordingly. The system will also use +1 for each extra node started. For example 4002, 5002, 7002
## Quick Start ## Quick Start

BIN
cli Executable file

Binary file not shown.

View File

@ -1,163 +0,0 @@
package main
import (
"fmt"
"os"
"strings"
"git.debros.io/DeBros/network/pkg/config"
"git.debros.io/DeBros/network/pkg/client"
"git.debros.io/DeBros/network/pkg/constants"
"git.debros.io/DeBros/network/pkg/logging"
)
// NodeFlagValues holds parsed CLI flag values in a structured form.
type NodeFlagValues struct {
DataDir string
NodeID string
Bootstrap string
Role string
P2PPort int
RqlHTTP int
RqlRaft int
Advertise string
}
// isTruthyEnv returns true if the environment variable is set to a common truthy value
func isTruthyEnv(key string) bool {
v := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
switch v {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
// MapFlagsAndEnvToConfig applies environment overrides and CLI flags to cfg.
// Precedence: flags > env > defaults. Behavior mirrors previous inline logic in main.go.
// Returns the derived RQLite Raft join address for non-bootstrap nodes (empty for bootstrap nodes).
func MapFlagsAndEnvToConfig(cfg *config.Config, fv NodeFlagValues, isBootstrap bool, logger *logging.StandardLogger) string {
// Apply environment variable overrides first so that flags can override them after
config.ApplyEnvOverrides(cfg)
// Detect dev-local mode (set via -dev-local -> NETWORK_DEV_LOCAL=1)
devLocal := isTruthyEnv("NETWORK_DEV_LOCAL")
// Determine data directory if not provided
if fv.DataDir == "" {
if isBootstrap {
fv.DataDir = "./data/bootstrap"
} else {
if fv.NodeID != "" {
fv.DataDir = fmt.Sprintf("./data/node-%s", fv.NodeID)
} else {
fv.DataDir = "./data/node"
}
}
}
// Node basics
cfg.Node.DataDir = fv.DataDir
cfg.Node.ListenAddresses = []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", fv.P2PPort),
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic", fv.P2PPort),
}
// Database port settings
cfg.Database.RQLitePort = fv.RqlHTTP
cfg.Database.RQLiteRaftPort = fv.RqlRaft
cfg.Database.AdvertiseMode = strings.ToLower(fv.Advertise)
logger.Printf("RQLite advertise mode: %s", cfg.Database.AdvertiseMode)
// Bootstrap-specific vs regular-node logic
if isBootstrap {
if devLocal {
// In dev-local, run a primary bootstrap locally
cfg.Database.RQLiteJoinAddress = ""
// Do not set bootstrap peers to avoid including self; clients can still
// derive DB endpoints via DefaultDatabaseEndpoints in dev-local.
logger.Printf("Dev-local: Primary bootstrap node - localhost defaults enabled (no bootstrap peers set to avoid self)")
return ""
}
bootstrapPeers := constants.GetBootstrapPeers()
isSecondaryBootstrap := false
if len(bootstrapPeers) > 1 {
for i := 1; i < len(bootstrapPeers); i++ {
host := parseHostFromMultiaddr(bootstrapPeers[i])
if host != "" && isLocalIP(host) {
isSecondaryBootstrap = true
break
}
}
}
if isSecondaryBootstrap {
primaryBootstrapHost := parseHostFromMultiaddr(bootstrapPeers[0])
cfg.Database.RQLiteJoinAddress = fmt.Sprintf("%s:%d", primaryBootstrapHost, 7001)
logger.Printf("Secondary bootstrap node - joining primary bootstrap (raft) at: %s", cfg.Database.RQLiteJoinAddress)
} else {
cfg.Database.RQLiteJoinAddress = ""
logger.Printf("Primary bootstrap node - starting new RQLite cluster")
}
return ""
}
// Regular node: compute bootstrap peers and join address
var rqliteJoinAddr string
if fv.Bootstrap != "" {
cfg.Discovery.BootstrapPeers = []string{fv.Bootstrap}
bootstrapHost := parseHostFromMultiaddr(fv.Bootstrap)
if bootstrapHost != "" {
if (bootstrapHost == "127.0.0.1" || strings.EqualFold(bootstrapHost, "localhost")) && cfg.Database.AdvertiseMode != "localhost" {
if extIP, err := getPreferredLocalIP(); err == nil && extIP != "" {
logger.Printf("Translating localhost bootstrap to external IP %s for RQLite join", extIP)
bootstrapHost = extIP
} else {
logger.Printf("Warning: Failed to resolve external IP, keeping localhost for RQLite join")
}
}
rqliteJoinAddr = fmt.Sprintf("%s:%d", bootstrapHost, 7001)
logger.Printf("Using extracted bootstrap host %s for RQLite Raft join (port 7001)", bootstrapHost)
} else {
logger.Printf("Warning: Could not extract host from bootstrap peer %s, using localhost fallback", fv.Bootstrap)
rqliteJoinAddr = fmt.Sprintf("localhost:%d", 7001)
}
logger.Printf("Using command line bootstrap peer: %s", fv.Bootstrap)
} else {
bootstrapPeers := cfg.Discovery.BootstrapPeers
if devLocal {
// Force localhost bootstrap for development
bootstrapPeers = client.DefaultBootstrapPeers()
logger.Printf("Dev-local: overriding bootstrap peers to %v", bootstrapPeers)
}
if len(bootstrapPeers) == 0 {
bootstrapPeers = constants.GetBootstrapPeers()
}
if len(bootstrapPeers) > 0 {
cfg.Discovery.BootstrapPeers = bootstrapPeers
bootstrapHost := parseHostFromMultiaddr(bootstrapPeers[0])
if bootstrapHost != "" {
rqliteJoinAddr = fmt.Sprintf("%s:%d", bootstrapHost, 7001)
logger.Printf("Using extracted bootstrap host %s for RQLite Raft join", bootstrapHost)
} else {
logger.Printf("Warning: Could not extract host from bootstrap peer %s", bootstrapPeers[0])
rqliteJoinAddr = "localhost:7001"
}
logger.Printf("Using environment bootstrap peers: %v", bootstrapPeers)
} else {
logger.Printf("Warning: No bootstrap peers configured")
rqliteJoinAddr = "localhost:7001"
logger.Printf("Using localhost fallback for RQLite Raft join")
}
logger.Printf("=== NETWORK DIAGNOSTICS ===")
logger.Printf("Target RQLite Raft join address: %s", rqliteJoinAddr)
runNetworkDiagnostics(rqliteJoinAddr, logger)
}
cfg.Database.RQLiteJoinAddress = rqliteJoinAddr
logger.Printf("Regular node - joining RQLite cluster (raft) at: %s", cfg.Database.RQLiteJoinAddress)
return rqliteJoinAddr
}

View File

@ -5,131 +5,169 @@ import (
"flag" "flag"
"fmt" "fmt"
"log" "log"
"net"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strings"
"syscall" "syscall"
"git.debros.io/DeBros/network/pkg/anyoneproxy" "git.debros.io/DeBros/network/pkg/anyoneproxy"
"git.debros.io/DeBros/network/pkg/client"
"git.debros.io/DeBros/network/pkg/config" "git.debros.io/DeBros/network/pkg/config"
"git.debros.io/DeBros/network/pkg/constants"
"git.debros.io/DeBros/network/pkg/logging" "git.debros.io/DeBros/network/pkg/logging"
"git.debros.io/DeBros/network/pkg/node" "git.debros.io/DeBros/network/pkg/node"
"go.uber.org/zap"
) )
func main() { func setup_logger(component logging.Component) (logger *logging.ColoredLogger) {
var (
dataDir = flag.String("data", "", "Data directory (auto-detected if not provided)")
nodeID = flag.String("id", "", "Node identifier (for running multiple local nodes)")
bootstrap = flag.String("bootstrap", "", "Bootstrap peer address (for manual override)")
role = flag.String("role", "auto", "Node role: auto|bootstrap|node (auto detects based on config)")
p2pPort = flag.Int("p2p-port", 4001, "LibP2P listen port")
rqlHTTP = flag.Int("rqlite-http-port", 5001, "RQLite HTTP API port")
rqlRaft = flag.Int("rqlite-raft-port", 7001, "RQLite Raft port")
advertise = flag.String("advertise", "auto", "Advertise mode: auto|localhost|ip")
devLocal = flag.Bool("dev-local", false, "Enable development localhost defaults for the client library (sets NETWORK_DEV_LOCAL=1)")
disableAnon = flag.Bool("disable-anonrc", false, "Disable Anyone proxy routing (defaults to enabled on 127.0.0.1:9050)")
help = flag.Bool("help", false, "Show help")
)
flag.Parse()
// Apply proxy disable flag early
anyoneproxy.SetDisabled(*disableAnon)
if *help {
flag.Usage()
return
}
// Enable development localhost defaults for the client library if requested
if *devLocal {
os.Setenv("NETWORK_DEV_LOCAL", "1")
log.Printf("Development local mode enabled (NETWORK_DEV_LOCAL=1)")
}
// Determine node role
var isBootstrap bool
switch strings.ToLower(*role) {
case "bootstrap":
isBootstrap = true
case "node":
isBootstrap = false
default:
// Auto-detect if this is a bootstrap node based on configuration
isBootstrap = isBootstrapNode()
}
// Set default data directory if not provided
if *dataDir == "" {
if isBootstrap {
*dataDir = "./data/bootstrap"
} else {
if *nodeID != "" {
*dataDir = fmt.Sprintf("./data/node-%s", *nodeID)
} else {
*dataDir = "./data/node"
}
}
}
// LibP2P uses configurable port (default 4001); RQLite uses 5001 (HTTP) and 7001 (Raft)
port := *p2pPort
// Create logger with appropriate component type
var logger *logging.StandardLogger
var err error var err error
if isBootstrap {
logger, err = logging.NewStandardLogger(logging.ComponentBootstrap) logger, err = logging.NewColoredLogger(component, true)
} else {
logger, err = logging.NewStandardLogger(logging.ComponentNode)
}
if err != nil { if err != nil {
log.Fatalf("Failed to create logger: %v", err) log.Fatalf("Failed to create logger: %v", err)
} }
// Load configuration based on node type return logger
var cfg *config.Config }
if isBootstrap {
cfg = config.BootstrapConfig() func parse_and_return_network_flags() (dataDir, nodeID *string, p2pPort, rqlHTTP, rqlRaft *int, disableAnon *bool, rqlJoinAddr *string, help *bool) {
logger.Printf("Starting bootstrap node...") logger := setup_logger(logging.ComponentNode)
dataDir = flag.String("data", "", "Data directory (auto-detected if not provided)")
nodeID = flag.String("id", "", "Node identifier (for running multiple local nodes)")
p2pPort = flag.Int("p2p-port", 4001, "LibP2P listen port")
rqlHTTP = flag.Int("rqlite-http-port", 5001, "RQLite HTTP API port")
rqlRaft = flag.Int("rqlite-raft-port", 7001, "RQLite Raft port")
disableAnon = flag.Bool("disable-anonrc", false, "Disable Anyone proxy routing (defaults to enabled on 127.0.0.1:9050)")
rqlJoinAddr = flag.String("rqlite-join-address", "", "RQLite address to join (e.g., /ip4/)")
help = flag.Bool("help", false, "Show help")
flag.Parse()
logger.Info("Successfully parsed all flags and arguments.")
return
}
func disable_anon_proxy(disableAnon *bool) bool {
anyoneproxy.SetDisabled(*disableAnon)
logger := setup_logger(logging.ComponentAnyone)
if *disableAnon {
logger.Info("Anyone proxy routing is disabled. This means the node will not use the default Tor proxy for anonymous routing.\n")
}
return true
}
func check_if_should_open_help(help *bool) {
if *help {
flag.Usage()
return
}
}
func select_data_dir(dataDir *string, nodeID *string) {
logger := setup_logger(logging.ComponentNode)
if *nodeID == "" {
*dataDir = "./data/node"
}
logger.Info("Successfully selected Data Directory of: %s", zap.String("dataDir", *dataDir))
}
func startNode(ctx context.Context, cfg *config.Config, port int) error {
logger := setup_logger(logging.ComponentNode)
// Create and start node using the unified node implementation
n, err := node.NewNode(cfg)
if err != nil {
logger.Error("failed to create node: %v", zap.Error(err))
}
if err := n.Start(ctx); err != nil {
logger.Error("failed to start node: %v", zap.Error(err))
}
// Save the peer ID to a file for CLI access (especially useful for bootstrap)
peerID := n.GetPeerID()
peerInfoFile := filepath.Join(cfg.Node.DataDir, "peer.info")
peerMultiaddr := fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/p2p/%s", port, peerID)
if err := os.WriteFile(peerInfoFile, []byte(peerMultiaddr), 0644); err != nil {
logger.Error("Failed to save peer info: %v", zap.Error(err))
} else { } else {
logger.Info("Peer info saved to: %s", zap.String("path", peerInfoFile))
logger.Info("Bootstrap multiaddr: %s", zap.String("path", peerMultiaddr))
}
logger.Info("Node started successfully")
// Wait for context cancellation
<-ctx.Done()
// Stop node
return n.Stop()
}
// load_args_into_config applies command line argument overrides to the config
func load_args_into_config(cfg *config.Config, p2pPort, rqlHTTP, rqlRaft *int, rqlJoinAddr *string) {
logger := setup_logger(logging.ComponentNode)
// Apply RQLite HTTP port override
if *rqlHTTP != 5001 {
cfg.Database.RQLitePort = *rqlHTTP
logger.ComponentInfo(logging.ComponentNode, "Overriding RQLite HTTP port", zap.Int("port", *rqlHTTP))
}
// Apply RQLite Raft port override
if *rqlRaft != 7001 {
cfg.Database.RQLiteRaftPort = *rqlRaft
logger.ComponentInfo(logging.ComponentNode, "Overriding RQLite Raft port", zap.Int("port", *rqlRaft))
}
// Apply P2P port override
if *p2pPort != 4001 {
cfg.Node.ListenAddresses = []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", *p2pPort),
}
logger.ComponentInfo(logging.ComponentNode, "Overriding P2P port", zap.Int("port", *p2pPort))
}
// Apply RQLite join address
if *rqlJoinAddr != "" {
cfg.Database.RQLiteJoinAddress = *rqlJoinAddr
logger.ComponentInfo(logging.ComponentNode, "Setting RQLite join address", zap.String("address", *rqlJoinAddr))
}
}
func main() {
logger := setup_logger(logging.ComponentNode)
dataDir, nodeID, p2pPort, rqlHTTP, rqlRaft, disableAnon, rqlJoinAddr, help := parse_and_return_network_flags()
disable_anon_proxy(disableAnon)
check_if_should_open_help(help)
select_data_dir(dataDir, nodeID)
// Load Node Configuration
var cfg *config.Config
cfg = config.DefaultConfig() cfg = config.DefaultConfig()
logger.Printf("Starting regular node...") logger.ComponentInfo(logging.ComponentNode, "Default configuration loaded successfully")
}
// Centralized mapping from flags/env to config (flags > env > defaults) // Apply command line argument overrides
_ = MapFlagsAndEnvToConfig(cfg, NodeFlagValues{ load_args_into_config(cfg, p2pPort, rqlHTTP, rqlRaft, rqlJoinAddr)
DataDir: *dataDir, logger.ComponentInfo(logging.ComponentNode, "Command line arguments applied to configuration")
NodeID: *nodeID,
Bootstrap: *bootstrap,
Role: *role,
P2PPort: port,
RqlHTTP: *rqlHTTP,
RqlRaft: *rqlRaft,
Advertise: *advertise,
}, isBootstrap, logger)
logger.Printf("Data directory: %s", cfg.Node.DataDir) // LibP2P uses configurable port (default 4001); RQLite uses 5001 (HTTP) and 7001 (Raft)
logger.Printf("Listen addresses: %v", cfg.Node.ListenAddresses) port := *p2pPort
logger.Printf("RQLite HTTP port: %d", cfg.Database.RQLitePort)
logger.Printf("RQLite Raft port: %d", cfg.Database.RQLiteRaftPort)
// For development visibility, print what the CLIENT library will return by default logger.ComponentInfo(logging.ComponentNode, "Node configuration summary",
clientBootstrap := client.DefaultBootstrapPeers() zap.Strings("listen_addresses", cfg.Node.ListenAddresses),
clientDB := client.DefaultDatabaseEndpoints() zap.Int("rqlite_http_port", cfg.Database.RQLitePort),
logger.Printf("[Client Defaults] Bootstrap peers: %v", clientBootstrap) zap.Int("rqlite_raft_port", cfg.Database.RQLiteRaftPort),
logger.Printf("[Client Defaults] Database endpoints: %v", clientDB) zap.Int("p2p_port", port),
// Also show node-configured values zap.Strings("bootstrap_peers", cfg.Discovery.BootstrapPeers),
logger.Printf("[Node Config] Bootstrap peers: %v", cfg.Discovery.BootstrapPeers) zap.String("rqlite_join_address", cfg.Database.RQLiteJoinAddress),
if cfg.Database.RQLiteJoinAddress != "" { zap.String("data_directory", *dataDir))
logger.Printf("[Node Config] RQLite Raft join: %s", cfg.Database.RQLiteJoinAddress)
} else if isBootstrap {
logger.Printf("[Node Config] Bootstrap node: starting new RQLite cluster (no join)")
}
// Create context for graceful shutdown // Create context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -139,7 +177,7 @@ func main() {
errChan := make(chan error, 1) errChan := make(chan error, 1)
doneChan := make(chan struct{}) doneChan := make(chan struct{})
go func() { go func() {
if err := startNode(ctx, cfg, port, isBootstrap, logger); err != nil { if err := startNode(ctx, cfg, port); err != nil {
errChan <- err errChan <- err
} }
close(doneChan) close(doneChan)
@ -151,236 +189,13 @@ func main() {
select { select {
case err := <-errChan: case err := <-errChan:
logger.Printf("Failed to start node: %v", err) logger.ComponentError(logging.ComponentNode, "Failed to start node", zap.Error(err))
os.Exit(1) os.Exit(1)
case <-c: case <-c:
logger.Printf("Shutting down node...") logger.ComponentInfo(logging.ComponentNode, "Shutting down node...")
cancel() cancel()
// Wait for node goroutine to finish cleanly // Wait for node goroutine to finish cleanly
<-doneChan <-doneChan
logger.Printf("Node shutdown complete") logger.ComponentInfo(logging.ComponentNode, "Node shutdown complete")
} }
} }
// isBootstrapNode determines if this should be a bootstrap node
// by checking the local machine's configuration and bootstrap peer list
func isBootstrapNode() bool {
// Get the bootstrap peer addresses to check if this machine should be a bootstrap
bootstrapPeers := constants.GetBootstrapPeers()
// Check if any bootstrap peer is localhost/127.0.0.1 (development)
// or if we're running on a production bootstrap server
hostname, _ := os.Hostname()
for _, peerAddr := range bootstrapPeers {
// Parse the multiaddr to extract the host
host := parseHostFromMultiaddr(peerAddr)
// Check if this is a local bootstrap (development)
if host == "127.0.0.1" || host == "localhost" {
return true // In development, assume we're running the bootstrap
}
// Check if this is a production bootstrap server by IP
if host != "" && isLocalIP(host) {
return true
}
// Check if this is a production bootstrap server by hostname
if hostname != "" && strings.Contains(peerAddr, hostname) {
return true
}
}
// Default: if no specific match, run as regular node
return false
}
// getPreferredLocalIP returns a non-loopback IPv4 address of this machine
func getPreferredLocalIP() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range ifaces {
if (iface.Flags&net.FlagUp) == 0 || (iface.Flags&net.FlagLoopback) != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue
}
return ip.String(), nil
}
}
return "", fmt.Errorf("no non-loopback IPv4 found")
}
// isLocalIP checks if the given IP address belongs to this machine
func isLocalIP(ip string) bool {
if ip == "127.0.0.1" || strings.EqualFold(ip, "localhost") {
return true
}
ifaces, err := net.Interfaces()
if err != nil {
return false
}
for _, iface := range ifaces {
if (iface.Flags & net.FlagUp) == 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var a net.IP
switch v := addr.(type) {
case *net.IPNet:
a = v.IP
case *net.IPAddr:
a = v.IP
}
if a != nil && a.String() == ip {
return true
}
}
}
return false
}
// parseHostFromMultiaddr extracts the host from a multiaddr
func parseHostFromMultiaddr(multiaddr string) string {
// Simple parsing for /ip4/host/tcp/port/p2p/peerid format
parts := strings.Split(multiaddr, "/")
// Look for ip4/ip6/dns host in the multiaddr
for i, part := range parts {
if (part == "ip4" || part == "ip6" || part == "dns" || part == "dns4" || part == "dns6") && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
func startNode(ctx context.Context, cfg *config.Config, port int, isBootstrap bool, logger *logging.StandardLogger) error {
// Create and start node using the unified node implementation
n, err := node.NewNode(cfg)
if err != nil {
return fmt.Errorf("failed to create node: %w", err)
}
if err := n.Start(ctx); err != nil {
return fmt.Errorf("failed to start node: %w", err)
}
// Save the peer ID to a file for CLI access (especially useful for bootstrap)
if isBootstrap {
peerID := n.GetPeerID()
peerInfoFile := filepath.Join(cfg.Node.DataDir, "peer.info")
peerMultiaddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/%d/p2p/%s", port, peerID)
if err := os.WriteFile(peerInfoFile, []byte(peerMultiaddr), 0644); err != nil {
logger.Printf("Warning: Failed to save peer info: %v", err)
} else {
logger.Printf("Peer info saved to: %s", peerInfoFile)
logger.Printf("Bootstrap multiaddr: %s", peerMultiaddr)
}
}
logger.Printf("Node started successfully")
// Wait for context cancellation
<-ctx.Done()
// Stop node
return n.Stop()
}
// runNetworkDiagnostics performs network connectivity tests
func runNetworkDiagnostics(target string, logger *logging.StandardLogger) {
// If target has scheme, treat as HTTP URL. Otherwise treat as host:port raft.
var host, port string
if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
url := strings.TrimPrefix(strings.TrimPrefix(target, "http://"), "https://")
parts := strings.Split(url, ":")
if len(parts) == 2 {
host, port = parts[0], parts[1]
}
} else {
parts := strings.Split(target, ":")
if len(parts) == 2 {
host, port = parts[0], parts[1]
}
}
if host == "" || port == "" {
logger.Printf("Cannot parse host:port from %s", target)
return
}
logger.Printf("Testing TCP connectivity to %s:%s", host, port)
if output, err := exec.Command("timeout", "5", "nc", "-z", "-v", host, port).CombinedOutput(); err == nil {
logger.Printf("✅ Port %s:%s is reachable", host, port)
logger.Printf("netcat output: %s", strings.TrimSpace(string(output)))
} else {
logger.Printf("❌ Port %s:%s is NOT reachable", host, port)
logger.Printf("netcat error: %v", err)
logger.Printf("netcat output: %s", strings.TrimSpace(string(output)))
}
// Also probe HTTP status on port 5001 of the same host, which is the default HTTP API
httpURL := fmt.Sprintf("http://%s:%d/status", host, 5001)
if output, err := exec.Command("timeout", "5", "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", httpURL).Output(); err == nil {
httpCode := strings.TrimSpace(string(output))
if httpCode == "200" {
logger.Printf("✅ HTTP service on %s is responding correctly (status: %s)", httpURL, httpCode)
} else {
logger.Printf("⚠️ HTTP service on %s responded with status: %s", httpURL, httpCode)
}
} else {
logger.Printf("❌ HTTP request to %s failed: %v", httpURL, err)
}
// Ping test
if output, err := exec.Command("ping", "-c", "3", "-W", "2", host).Output(); err == nil {
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, "packet loss") {
logger.Printf("🏓 Ping result: %s", strings.TrimSpace(line))
break
}
}
} else {
logger.Printf("❌ Ping test failed: %v", err)
}
// DNS resolution
if output, err := exec.Command("nslookup", host).Output(); err == nil {
logger.Printf("🔍 DNS resolution successful")
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, "Address:") && !strings.Contains(line, "#53") {
logger.Printf("DNS result: %s", strings.TrimSpace(line))
}
}
} else {
logger.Printf("❌ DNS resolution failed: %v", err)
}
logger.Printf("=== END DIAGNOSTICS ===")
}

1
go.mod
View File

@ -59,6 +59,7 @@ require (
github.com/libp2p/go-netroute v0.2.2 // indirect github.com/libp2p/go-netroute v0.2.2 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.66 // indirect github.com/miekg/dns v1.1.66 // indirect

8
go.sum
View File

@ -184,6 +184,8 @@ github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQsc
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po= github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
@ -192,6 +194,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
@ -445,6 +448,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
@ -482,6 +486,9 @@ golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -503,6 +510,7 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=

BIN
node Executable file

Binary file not shown.

View File

@ -1,9 +1,6 @@
package config package config
import ( import (
"os"
"strconv"
"strings"
"time" "time"
"github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multiaddr"
@ -42,16 +39,15 @@ type DatabaseConfig struct {
RQLitePort int `yaml:"rqlite_port"` // RQLite HTTP API port RQLitePort int `yaml:"rqlite_port"` // RQLite HTTP API port
RQLiteRaftPort int `yaml:"rqlite_raft_port"` // RQLite Raft consensus port RQLiteRaftPort int `yaml:"rqlite_raft_port"` // RQLite Raft consensus port
RQLiteJoinAddress string `yaml:"rqlite_join_address"` // Address to join RQLite cluster RQLiteJoinAddress string `yaml:"rqlite_join_address"` // Address to join RQLite cluster
AdvertiseMode string `yaml:"advertise_mode"` // Advertise mode: "auto" (default), "localhost", or "ip"
} }
// DiscoveryConfig contains peer discovery configuration // DiscoveryConfig contains peer discovery configuration
type DiscoveryConfig struct { type DiscoveryConfig struct {
BootstrapPeers []string `yaml:"bootstrap_peers"` // Bootstrap peer addresses BootstrapPeers []string `yaml:"bootstrap_peers"` // Bootstrap peer addresses
EnableMDNS bool `yaml:"enable_mdns"` // Enable mDNS discovery
EnableDHT bool `yaml:"enable_dht"` // Enable DHT discovery EnableDHT bool `yaml:"enable_dht"` // Enable DHT discovery
DHTPrefix string `yaml:"dht_prefix"` // DHT protocol prefix DHTPrefix string `yaml:"dht_prefix"` // DHT protocol prefix
DiscoveryInterval time.Duration `yaml:"discovery_interval"` // Discovery announcement interval DiscoveryInterval time.Duration `yaml:"discovery_interval"` // Discovery announcement interval
BootstrapPort int `yaml:"bootstrap_port"` // Default port for bootstrap nodes
} }
// SecurityConfig contains security-related configuration // SecurityConfig contains security-related configuration
@ -91,27 +87,13 @@ func (c *Config) ParseMultiaddrs() ([]multiaddr.Multiaddr, error) {
return addrs, nil return addrs, nil
} }
// GetBootstrapMultiaddrs converts bootstrap peer strings to multiaddr objects
func (c *Config) GetBootstrapMultiaddrs() ([]multiaddr.Multiaddr, error) {
var addrs []multiaddr.Multiaddr
for _, addr := range c.Discovery.BootstrapPeers {
ma, err := multiaddr.NewMultiaddr(addr)
if err != nil {
return nil, err
}
addrs = append(addrs, ma)
}
return addrs, nil
}
// DefaultConfig returns a default configuration // DefaultConfig returns a default configuration
func DefaultConfig() *Config { func DefaultConfig() *Config {
return &Config{ return &Config{
Node: NodeConfig{ Node: NodeConfig{
Type: "node", Type: "node",
ListenAddresses: []string{ ListenAddresses: []string{
"/ip4/0.0.0.0/tcp/0", "/ip4/0.0.0.0/tcp/0", // TCP only - compatible with Anyone proxy/SOCKS5
"/ip4/0.0.0.0/udp/0/quic",
}, },
DataDir: "./data", DataDir: "./data",
MaxConnections: 50, MaxConnections: 50,
@ -128,14 +110,15 @@ func DefaultConfig() *Config {
RQLitePort: 5001, RQLitePort: 5001,
RQLiteRaftPort: 7001, RQLiteRaftPort: 7001,
RQLiteJoinAddress: "", // Empty for bootstrap node RQLiteJoinAddress: "", // Empty for bootstrap node
AdvertiseMode: "auto",
}, },
Discovery: DiscoveryConfig{ Discovery: DiscoveryConfig{
BootstrapPeers: []string{}, BootstrapPeers: []string{
EnableMDNS: true, "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWDL6LSjwwP5FwboV9JaTZzuxr8EhjbcZGFfnyFMDt1UDx",
EnableDHT: true, },
BootstrapPort: 4001, // Default LibP2P port
EnableDHT: false, // Disabled - conflicts with Anyone protocol anonymity
DHTPrefix: "/network/kad/1.0.0", DHTPrefix: "/network/kad/1.0.0",
DiscoveryInterval: time.Minute * 5, DiscoveryInterval: time.Second * 15, // Back to 15 seconds for testing
}, },
Security: SecurityConfig{ Security: SecurityConfig{
EnableTLS: false, EnableTLS: false,
@ -147,196 +130,3 @@ func DefaultConfig() *Config {
}, },
} }
} }
// BootstrapConfig returns a default configuration for bootstrap nodes
func BootstrapConfig() *Config {
config := DefaultConfig()
config.Node.Type = "bootstrap"
config.Node.IsBootstrap = true
config.Node.ListenAddresses = []string{
"/ip4/0.0.0.0/tcp/4001",
"/ip4/0.0.0.0/udp/4001/quic",
}
return config
}
// NewConfigFromEnv constructs a config (bootstrap or regular) and applies environment overrides.
// If isBootstrap is true, starts from BootstrapConfig; otherwise from DefaultConfig.
func NewConfigFromEnv(isBootstrap bool) *Config {
var cfg *Config
if isBootstrap {
cfg = BootstrapConfig()
} else {
cfg = DefaultConfig()
}
ApplyEnvOverrides(cfg)
return cfg
}
// ApplyEnvOverrides mutates cfg based on environment variables.
// Precedence: CLI flags (outside this function) > ENV variables > defaults in code.
func ApplyEnvOverrides(cfg *Config) {
// Node
if v := os.Getenv("NODE_ID"); v != "" {
cfg.Node.ID = v
}
if v := os.Getenv("NODE_TYPE"); v != "" { // "bootstrap" or "node"
cfg.Node.Type = strings.ToLower(v)
cfg.Node.IsBootstrap = cfg.Node.Type == "bootstrap"
}
if v := os.Getenv("NODE_LISTEN_ADDRESSES"); v != "" {
parts := splitAndTrim(v)
if len(parts) > 0 {
cfg.Node.ListenAddresses = parts
}
}
if v := os.Getenv("DATA_DIR"); v != "" {
cfg.Node.DataDir = v
}
if v := os.Getenv("MAX_CONNECTIONS"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
cfg.Node.MaxConnections = n
}
}
// Database
if v := os.Getenv("DB_DATA_DIR"); v != "" {
cfg.Database.DataDir = v
}
if v := os.Getenv("REPLICATION_FACTOR"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
cfg.Database.ReplicationFactor = n
}
}
if v := os.Getenv("SHARD_COUNT"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
cfg.Database.ShardCount = n
}
}
if v := os.Getenv("MAX_DB_SIZE"); v != "" { // bytes
if n, err := parseInt64(v); err == nil {
cfg.Database.MaxDatabaseSize = n
}
}
if v := os.Getenv("BACKUP_INTERVAL"); v != "" { // duration, e.g. 24h
if d, err := time.ParseDuration(v); err == nil {
cfg.Database.BackupInterval = d
}
}
if v := os.Getenv("RQLITE_HTTP_PORT"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
cfg.Database.RQLitePort = n
}
}
if v := os.Getenv("RQLITE_RAFT_PORT"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
cfg.Database.RQLiteRaftPort = n
}
}
if v := os.Getenv("RQLITE_JOIN_ADDRESS"); v != "" {
cfg.Database.RQLiteJoinAddress = v
}
if v := os.Getenv("ADVERTISE_MODE"); v != "" { // auto | localhost | ip
cfg.Database.AdvertiseMode = strings.ToLower(v)
}
// Discovery
if v := os.Getenv("BOOTSTRAP_PEERS"); v != "" {
parts := splitAndTrim(v)
if len(parts) > 0 {
cfg.Discovery.BootstrapPeers = parts
}
}
if v := os.Getenv("ENABLE_MDNS"); v != "" {
if b, err := parseBool(v); err == nil {
cfg.Discovery.EnableMDNS = b
}
}
if v := os.Getenv("ENABLE_DHT"); v != "" {
if b, err := parseBool(v); err == nil {
cfg.Discovery.EnableDHT = b
}
}
if v := os.Getenv("DHT_PREFIX"); v != "" {
cfg.Discovery.DHTPrefix = v
}
if v := os.Getenv("DISCOVERY_INTERVAL"); v != "" { // e.g. 5m
if d, err := time.ParseDuration(v); err == nil {
cfg.Discovery.DiscoveryInterval = d
}
}
// Security
if v := os.Getenv("ENABLE_TLS"); v != "" {
if b, err := parseBool(v); err == nil {
cfg.Security.EnableTLS = b
}
}
if v := os.Getenv("PRIVATE_KEY_FILE"); v != "" {
cfg.Security.PrivateKeyFile = v
}
if v := os.Getenv("CERT_FILE"); v != "" {
cfg.Security.CertificateFile = v
}
if v := os.Getenv("AUTH_ENABLED"); v != "" {
if b, err := parseBool(v); err == nil {
cfg.Security.AuthEnabled = b
}
}
// Logging
if v := os.Getenv("LOG_LEVEL"); v != "" {
cfg.Logging.Level = strings.ToLower(v)
}
if v := os.Getenv("LOG_FORMAT"); v != "" {
cfg.Logging.Format = strings.ToLower(v)
}
if v := os.Getenv("LOG_OUTPUT_FILE"); v != "" {
cfg.Logging.OutputFile = v
}
}
// Helpers
func splitAndTrim(csv string) []string {
parts := strings.Split(csv, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
s := strings.TrimSpace(p)
if s != "" {
out = append(out, s)
}
}
return out
}
func parseBool(s string) (bool, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "1", "true", "t", "yes", "y", "on":
return true, nil
case "0", "false", "f", "no", "n", "off":
return false, nil
default:
return strconv.ParseBool(s)
}
}
func parseInt64(s string) (int64, error) {
// Allow plain int or with optional suffixes k, m, g (base-1024)
s = strings.TrimSpace(strings.ToLower(s))
mul := int64(1)
if strings.HasSuffix(s, "k") {
mul = 1024
s = strings.TrimSuffix(s, "k")
} else if strings.HasSuffix(s, "m") {
mul = 1024 * 1024
s = strings.TrimSuffix(s, "m")
} else if strings.HasSuffix(s, "g") {
mul = 1024 * 1024 * 1024
s = strings.TrimSuffix(s, "g")
}
n, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64)
if err != nil {
return 0, err
}
return n * mul, nil
}

View File

@ -2,24 +2,21 @@ package constants
import ( import (
"os" "os"
"git.debros.io/DeBros/network/pkg/config"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
) )
// Bootstrap node configuration // Bootstrap node configuration
var ( var (
// BootstrapPeerIDs are the fixed peer IDs for bootstrap nodes
// Each corresponds to a specific Ed25519 private key
BootstrapPeerIDs []string
// BootstrapAddresses are the full multiaddrs for bootstrap nodes // BootstrapAddresses are the full multiaddrs for bootstrap nodes
BootstrapAddresses []string BootstrapAddresses []string
// BootstrapPort is the default port for bootstrap nodes (LibP2P) // BootstrapPort is the default port for bootstrap nodes (LibP2P)
BootstrapPort int = 4001 BootstrapPort int = 4001
// Primary bootstrap peer ID (first in the list) // Primary bootstrap address (first in the list) - for backward compatibility
BootstrapPeerID string
// Primary bootstrap address (first in the list)
BootstrapAddress string BootstrapAddress string
) )
@ -29,28 +26,15 @@ func init() {
updateBackwardCompatibilityConstants() updateBackwardCompatibilityConstants()
} }
// setDefaultBootstrapConfig sets default bootstrap configuration // setDefaultBootstrapConfig sets default bootstrap configuration for local development
func setDefaultBootstrapConfig() { func setDefaultBootstrapConfig() {
// Check if we're in production environment var cfg *config.Config
BootstrapPeerIDs = []string{ BootstrapAddresses = cfg.Discovery.BootstrapPeers
// "12D3KooWNxt9bNvqftdqXg98JcUHreGxedWSZRUbyqXJ6CW7GaD4", BootstrapPort = cfg.Discovery.BootstrapPort
// "12D3KooWGbdnA22bN24X2gyY1o9jozwTBq9wbfvwtJ7G4XQ9JgFm",
"12D3KooWDL6LSjwwP5FwboV9JaTZzuxr8EhjbcZGFfnyFMDt1UDx",
}
BootstrapAddresses = []string{
// "/ip4/57.129.81.31/tcp/4001/p2p/12D3KooWNxt9bNvqftdqXg98JcUHreGxedWSZRUbyqXJ6CW7GaD4",
// "/ip4/38.242.250.186/tcp/4001/p2p/12D3KooWGbdnA22bN24X2gyY1o9jozwTBq9wbfvwtJ7G4XQ9JgFm",
"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWDL6LSjwwP5FwboV9JaTZzuxr8EhjbcZGFfnyFMDt1UDx",
}
BootstrapPort = 4001
} }
// updateBackwardCompatibilityConstants updates the single constants for backward compatibility // updateBackwardCompatibilityConstants updates the single constants for backward compatibility
func updateBackwardCompatibilityConstants() { func updateBackwardCompatibilityConstants() {
if len(BootstrapPeerIDs) > 0 {
BootstrapPeerID = BootstrapPeerIDs[0]
}
if len(BootstrapAddresses) > 0 { if len(BootstrapAddresses) > 0 {
BootstrapAddress = BootstrapAddresses[0] BootstrapAddress = BootstrapAddresses[0]
} }
@ -67,20 +51,26 @@ func GetBootstrapPeers() []string {
return peers return peers
} }
// GetBootstrapPeerIDs returns a copy of all bootstrap peer IDs // GetBootstrapPeerIDs extracts and returns peer IDs from bootstrap addresses
func GetBootstrapPeerIDs() []string { func GetBootstrapPeerIDs() []string {
if len(BootstrapPeerIDs) == 0 { if len(BootstrapAddresses) == 0 {
setDefaultBootstrapConfig() setDefaultBootstrapConfig()
updateBackwardCompatibilityConstants() updateBackwardCompatibilityConstants()
} }
ids := make([]string, len(BootstrapPeerIDs))
copy(ids, BootstrapPeerIDs) var ids []string
for _, addr := range BootstrapAddresses {
if ma, err := multiaddr.NewMultiaddr(addr); err == nil {
if pi, err := peer.AddrInfoFromP2pAddr(ma); err == nil {
ids = append(ids, pi.ID.String())
}
}
}
return ids return ids
} }
// AddBootstrapPeer adds a new bootstrap peer to the lists (runtime only) // AddBootstrapPeer adds a new bootstrap peer address (runtime only)
func AddBootstrapPeer(peerID, address string) { func AddBootstrapPeer(address string) {
BootstrapPeerIDs = append(BootstrapPeerIDs, peerID)
BootstrapAddresses = append(BootstrapAddresses, address) BootstrapAddresses = append(BootstrapAddresses, address)
updateBackwardCompatibilityConstants() updateBackwardCompatibilityConstants()
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@ -31,7 +30,8 @@ type RQLiteManager struct {
// waitForSQLAvailable waits until a simple query succeeds, indicating a leader is known and queries can be served. // waitForSQLAvailable waits until a simple query succeeds, indicating a leader is known and queries can be served.
func (r *RQLiteManager) waitForSQLAvailable(ctx context.Context) error { func (r *RQLiteManager) waitForSQLAvailable(ctx context.Context) error {
if r.connection == nil { if r.connection == nil {
return fmt.Errorf("no rqlite connection") r.logger.Error("No rqlite connection")
return errors.New("no rqlite connection")
} }
ticker := time.NewTicker(1 * time.Second) ticker := time.NewTicker(1 * time.Second)
@ -73,40 +73,12 @@ func (r *RQLiteManager) Start(ctx context.Context) error {
return fmt.Errorf("failed to create RQLite data directory: %w", err) return fmt.Errorf("failed to create RQLite data directory: %w", err)
} }
// Determine advertise host based on configuration
advertiseHost := "127.0.0.1" // default
mode := strings.ToLower(r.config.AdvertiseMode)
switch mode {
case "localhost":
advertiseHost = "127.0.0.1"
r.logger.Info("Using localhost for RQLite advertising (dev mode)")
case "ip":
if ip, err := r.getExternalIP(); err == nil && ip != "" {
advertiseHost = ip
r.logger.Info("Using external IP for RQLite advertising (forced)", zap.String("ip", ip))
} else {
r.logger.Warn("Failed to get external IP, falling back to localhost", zap.Error(err))
}
default: // auto
if ip, err := r.getExternalIP(); err == nil && ip != "" {
advertiseHost = ip
r.logger.Info("Using external IP for RQLite advertising (auto)", zap.String("ip", ip))
} else {
r.logger.Info("No external IP found, using localhost for RQLite advertising (auto)")
}
}
// Build RQLite command // Build RQLite command
args := []string{ args := []string{
"-http-addr", fmt.Sprintf("0.0.0.0:%d", r.config.RQLitePort), "-http-addr", fmt.Sprintf("0.0.0.0:%d", r.config.RQLitePort),
"-raft-addr", fmt.Sprintf("0.0.0.0:%d", r.config.RQLiteRaftPort), "-raft-addr", fmt.Sprintf("0.0.0.0:%d", r.config.RQLiteRaftPort),
// Auth disabled for testing
} }
// Always set advertised addresses explicitly to avoid 0.0.0.0 announcements
args = append(args, "-http-adv-addr", fmt.Sprintf("%s:%d", advertiseHost, r.config.RQLitePort))
args = append(args, "-raft-adv-addr", fmt.Sprintf("%s:%d", advertiseHost, r.config.RQLiteRaftPort))
// Add join address if specified (for non-bootstrap or secondary bootstrap nodes) // Add join address if specified (for non-bootstrap or secondary bootstrap nodes)
if r.config.RQLiteJoinAddress != "" { if r.config.RQLiteJoinAddress != "" {
r.logger.Info("Joining RQLite cluster", zap.String("join_address", r.config.RQLiteJoinAddress)) r.logger.Info("Joining RQLite cluster", zap.String("join_address", r.config.RQLiteJoinAddress))
@ -140,7 +112,6 @@ func (r *RQLiteManager) Start(ctx context.Context) error {
zap.Int("http_port", r.config.RQLitePort), zap.Int("http_port", r.config.RQLitePort),
zap.Int("raft_port", r.config.RQLiteRaftPort), zap.Int("raft_port", r.config.RQLiteRaftPort),
zap.String("join_address", r.config.RQLiteJoinAddress), zap.String("join_address", r.config.RQLiteJoinAddress),
zap.String("advertise_host", advertiseHost),
zap.Strings("full_args", args), zap.Strings("full_args", args),
) )
@ -342,99 +313,6 @@ func (r *RQLiteManager) waitForJoinTarget(ctx context.Context, joinAddress strin
return lastErr return lastErr
} }
// getExternalIP attempts to get the external IP address of this machine
func (r *RQLiteManager) getExternalIP() (string, error) {
// Method 1: Try using `ip route get` to find the IP used to reach the internet
if output, err := exec.Command("ip", "route", "get", "8.8.8.8").Output(); err == nil {
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, "src") {
parts := strings.Fields(line)
for i, part := range parts {
if part == "src" && i+1 < len(parts) {
ip := parts[i+1]
if net.ParseIP(ip) != nil {
r.logger.Debug("Found external IP via ip route", zap.String("ip", ip))
return ip, nil
}
}
}
}
}
}
// Method 2: Get all network interfaces and find non-localhost, non-private IPs
interfaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
// Prefer public IPs over private IPs
if ip.To4() != nil && !ip.IsPrivate() {
r.logger.Debug("Found public IP", zap.String("ip", ip.String()))
return ip.String(), nil
}
}
}
// Method 3: Fall back to private IPs if no public IP found
for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
// Use any IPv4 address
if ip.To4() != nil {
r.logger.Debug("Found private IP", zap.String("ip", ip.String()))
return ip.String(), nil
}
}
}
return "", fmt.Errorf("no suitable IP address found")
}
// testJoinAddress tests if a join address is reachable // testJoinAddress tests if a join address is reachable
func (r *RQLiteManager) testJoinAddress(joinAddress string) error { func (r *RQLiteManager) testJoinAddress(joinAddress string) error {
// Determine the HTTP status URL to probe. // Determine the HTTP status URL to probe.
@ -467,4 +345,3 @@ func (r *RQLiteManager) testJoinAddress(joinAddress string) error {
r.logger.Info("Leader HTTP reachable", zap.String("status_url", statusURL)) r.logger.Info("Leader HTTP reachable", zap.String("status_url", statusURL))
return nil return nil
} }

View File

@ -46,7 +46,6 @@ type ColoredLogger struct {
type Component string type Component string
const ( const (
ComponentBootstrap Component = "BOOTSTRAP"
ComponentNode Component = "NODE" ComponentNode Component = "NODE"
ComponentRQLite Component = "RQLITE" ComponentRQLite Component = "RQLITE"
ComponentLibP2P Component = "LIBP2P" ComponentLibP2P Component = "LIBP2P"
@ -55,13 +54,12 @@ const (
ComponentClient Component = "CLIENT" ComponentClient Component = "CLIENT"
ComponentDHT Component = "DHT" ComponentDHT Component = "DHT"
ComponentGeneral Component = "GENERAL" ComponentGeneral Component = "GENERAL"
ComponentAnyone Component = "ANYONE"
) )
// getComponentColor returns the color for a specific component // getComponentColor returns the color for a specific component
func getComponentColor(component Component) string { func getComponentColor(component Component) string {
switch component { switch component {
case ComponentBootstrap:
return BrightGreen
case ComponentNode: case ComponentNode:
return BrightBlue return BrightBlue
case ComponentRQLite: case ComponentRQLite:
@ -75,6 +73,10 @@ func getComponentColor(component Component) string {
case ComponentClient: case ComponentClient:
return Blue return Blue
case ComponentDHT: case ComponentDHT:
return Magenta
case ComponentGeneral:
return Yellow
case ComponentAnyone:
return Cyan return Cyan
default: default:
return White return White
@ -278,3 +280,9 @@ func (s *StandardLogger) Println(v ...interface{}) {
msg = strings.TrimSuffix(msg, "\n") msg = strings.TrimSuffix(msg, "\n")
s.logger.ComponentInfo(s.component, msg) s.logger.ComponentInfo(s.component, msg)
} }
func (s *StandardLogger) Errorf(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
msg = strings.TrimSuffix(msg, "\n")
s.logger.ComponentError(s.component, msg)
}

View File

@ -10,20 +10,19 @@ import (
"time" "time"
"github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p"
dht "github.com/libp2p/go-libp2p-kad-dht"
"github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
noise "github.com/libp2p/go-libp2p/p2p/security/noise" noise "github.com/libp2p/go-libp2p/p2p/security/noise"
libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
"github.com/libp2p/go-libp2p/p2p/transport/tcp" "github.com/libp2p/go-libp2p/p2p/transport/tcp"
"github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multiaddr"
"go.uber.org/zap" "go.uber.org/zap"
"git.debros.io/DeBros/network/pkg/anyoneproxy"
"git.debros.io/DeBros/network/pkg/config" "git.debros.io/DeBros/network/pkg/config"
"git.debros.io/DeBros/network/pkg/database" "git.debros.io/DeBros/network/pkg/database"
"git.debros.io/DeBros/network/pkg/anyoneproxy"
"git.debros.io/DeBros/network/pkg/logging" "git.debros.io/DeBros/network/pkg/logging"
"git.debros.io/DeBros/network/pkg/storage" "git.debros.io/DeBros/network/pkg/storage"
) )
@ -33,7 +32,7 @@ type Node struct {
config *config.Config config *config.Config
logger *logging.ColoredLogger logger *logging.ColoredLogger
host host.Host host host.Host
dht *dht.IpfsDHT
rqliteManager *database.RQLiteManager rqliteManager *database.RQLiteManager
rqliteAdapter *database.RQLiteAdapter rqliteAdapter *database.RQLiteAdapter
storageService *storage.Service storageService *storage.Service
@ -45,7 +44,7 @@ type Node struct {
// NewNode creates a new network node // NewNode creates a new network node
func NewNode(cfg *config.Config) (*Node, error) { func NewNode(cfg *config.Config) (*Node, error) {
// Create colored logger // Create colored logger
logger, err := logging.NewDefaultLogger(logging.ComponentNode) logger, err := logging.NewColoredLogger(logging.ComponentNode, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create logger: %w", err) return nil, fmt.Errorf("failed to create logger: %w", err)
} }
@ -56,49 +55,9 @@ func NewNode(cfg *config.Config) (*Node, error) {
}, nil }, nil
} }
// Start starts the network node
func (n *Node) Start(ctx context.Context) error {
n.logger.ComponentInfo(logging.ComponentNode, "Starting network node",
zap.String("data_dir", n.config.Node.DataDir),
)
// Create data directory
if err := os.MkdirAll(n.config.Node.DataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
// Start RQLite
if err := n.startRQLite(ctx); err != nil {
return fmt.Errorf("failed to start RQLite: %w", err)
}
// Start LibP2P host
if err := n.startLibP2P(); err != nil {
return fmt.Errorf("failed to start LibP2P: %w", err)
}
// Start storage service
if err := n.startStorageService(); err != nil {
return fmt.Errorf("failed to start storage service: %w", err)
}
// Get listen addresses for logging
var listenAddrs []string
for _, addr := range n.host.Addrs() {
listenAddrs = append(listenAddrs, addr.String())
}
n.logger.ComponentInfo(logging.ComponentNode, "Network node started successfully",
zap.String("peer_id", n.host.ID().String()),
zap.Strings("listen_addrs", listenAddrs),
)
return nil
}
// startRQLite initializes and starts the RQLite database // startRQLite initializes and starts the RQLite database
func (n *Node) startRQLite(ctx context.Context) error { func (n *Node) startRQLite(ctx context.Context) error {
n.logger.ComponentInfo(logging.ComponentDatabase, "Starting RQLite database") n.logger.Info("Starting RQLite database")
// Create RQLite manager // Create RQLite manager
n.rqliteManager = database.NewRQLiteManager(&n.config.Database, n.config.Node.DataDir, n.logger.Logger) n.rqliteManager = database.NewRQLiteManager(&n.config.Database, n.config.Node.DataDir, n.logger.Logger)
@ -118,6 +77,61 @@ func (n *Node) startRQLite(ctx context.Context) error {
return nil return nil
} }
// connectToBootstrapPeer connects to a single bootstrap peer
func (n *Node) connectToBootstrapPeer(ctx context.Context, addr string) error {
ma, err := multiaddr.NewMultiaddr(addr)
if err != nil {
return fmt.Errorf("invalid multiaddr: %w", err)
}
// Extract peer info from multiaddr
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
if err != nil {
return fmt.Errorf("failed to extract peer info: %w", err)
}
// Log resolved peer info prior to connect
n.logger.ComponentDebug(logging.ComponentNode, "Resolved bootstrap peer",
zap.String("peer_id", peerInfo.ID.String()),
zap.String("addr", addr),
zap.Int("addr_count", len(peerInfo.Addrs)),
)
// Connect to the peer
if err := n.host.Connect(ctx, *peerInfo); err != nil {
return fmt.Errorf("failed to connect to peer: %w", err)
}
n.logger.Info("Connected to bootstrap peer",
zap.String("peer", peerInfo.ID.String()),
zap.String("addr", addr))
return nil
}
// connectToBootstrapPeers connects to configured LibP2P bootstrap peers
func (n *Node) connectToBootstrapPeers(ctx context.Context) error {
if len(n.config.Discovery.BootstrapPeers) == 0 {
n.logger.ComponentDebug(logging.ComponentNode, "No bootstrap peers configured")
return nil
}
// Use passed context with a reasonable timeout for bootstrap connections
connectCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
for _, bootstrapAddr := range n.config.Discovery.BootstrapPeers {
if err := n.connectToBootstrapPeer(connectCtx, bootstrapAddr); err != nil {
n.logger.ComponentWarn(logging.ComponentNode, "Failed to connect to bootstrap peer",
zap.String("addr", bootstrapAddr),
zap.Error(err))
continue
}
}
return nil
}
// startLibP2P initializes the LibP2P host // startLibP2P initializes the LibP2P host
func (n *Node) startLibP2P() error { func (n *Node) startLibP2P() error {
n.logger.ComponentInfo(logging.ComponentLibP2P, "Starting LibP2P host") n.logger.ComponentInfo(logging.ComponentLibP2P, "Starting LibP2P host")
@ -147,7 +161,7 @@ func (n *Node) startLibP2P() error {
} }
// Create LibP2P host with persistent identity // Create LibP2P host with persistent identity
// Build options allowing conditional proxying via Anyone SOCKS5 and optional QUIC disable // Build options allowing conditional proxying via Anyone SOCKS5
var opts []libp2p.Option var opts []libp2p.Option
opts = append(opts, opts = append(opts,
libp2p.Identity(identity), libp2p.Identity(identity),
@ -163,14 +177,6 @@ func (n *Node) startLibP2P() error {
opts = append(opts, libp2p.Transport(tcp.NewTCPTransport)) opts = append(opts, libp2p.Transport(tcp.NewTCPTransport))
} }
// QUIC transport: disabled when proxy is enabled (default),
// enabled only when not proxying.
if !anyoneproxy.Enabled() {
opts = append(opts, libp2p.Transport(libp2pquic.NewTransport))
} else {
n.logger.ComponentDebug(logging.ComponentLibP2P, "QUIC disabled due to proxy being enabled")
}
h, err := libp2p.New(opts...) h, err := libp2p.New(opts...)
if err != nil { if err != nil {
return err return err
@ -178,35 +184,19 @@ func (n *Node) startLibP2P() error {
n.host = h n.host = h
// Create DHT for peer discovery - Use server mode for better peer discovery // DHT removed - incompatible with Anyone proxy anonymity architecture
// Use configured protocol prefix to ensure we discover peers on the correct DHT namespace
dhtPrefix := n.config.Discovery.DHTPrefix
if strings.TrimSpace(dhtPrefix) == "" {
dhtPrefix = "/network/kad/1.0.0"
}
n.logger.ComponentInfo(logging.ComponentDHT, "Using DHT protocol prefix", zap.String("prefix", dhtPrefix))
kademliaDHT, err := dht.New(
context.Background(),
h,
dht.Mode(dht.ModeServer),
dht.ProtocolPrefix(protocol.ID(dhtPrefix)),
)
if err != nil {
return fmt.Errorf("failed to create DHT: %w", err)
}
n.dht = kademliaDHT
// Log configured bootstrap peers // Log configured bootstrap peers
if len(n.config.Discovery.BootstrapPeers) > 0 { if len(n.config.Discovery.BootstrapPeers) > 0 {
n.logger.ComponentInfo(logging.ComponentDHT, "Configured bootstrap peers", n.logger.ComponentInfo(logging.ComponentNode, "Configured bootstrap peers",
zap.Strings("peers", n.config.Discovery.BootstrapPeers)) zap.Strings("peers", n.config.Discovery.BootstrapPeers))
} else { } else {
n.logger.ComponentDebug(logging.ComponentDHT, "No bootstrap peers configured") n.logger.ComponentDebug(logging.ComponentNode, "No bootstrap peers configured")
} }
// Connect to LibP2P bootstrap peers if configured // Connect to LibP2P bootstrap peers if configured
if err := n.connectToBootstrapPeers(); err != nil { if err := n.connectToBootstrapPeers(context.Background()); err != nil {
n.logger.Warn("Failed to connect to bootstrap peers", zap.Error(err)) n.logger.ComponentWarn(logging.ComponentNode, "Failed to connect to bootstrap peers", zap.Error(err))
// Don't fail - continue without bootstrap connections // Don't fail - continue without bootstrap connections
} }
@ -215,103 +205,38 @@ func (n *Node) startLibP2P() error {
if len(n.config.Discovery.BootstrapPeers) > 0 { if len(n.config.Discovery.BootstrapPeers) > 0 {
go func() { go func() {
for i := 0; i < 12; i++ { // ~60s total for i := 0; i < 12; i++ { // ~60s total
if n.host == nil || n.dht == nil { if n.host == nil {
return return
} }
// If we already have peers or DHT table entries, stop retrying // If we already have peers, stop retrying
if len(n.host.Network().Peers()) > 0 || len(n.dht.RoutingTable().ListPeers()) > 0 { if len(n.host.Network().Peers()) > 0 {
return return
} }
if err := n.connectToBootstrapPeers(); err == nil { if err := n.connectToBootstrapPeers(context.Background()); err == nil {
n.logger.Debug("Bootstrap reconnect attempt completed") n.logger.ComponentDebug(logging.ComponentNode, "Bootstrap reconnect attempt completed")
} }
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
} }
}() }()
} }
// Add bootstrap peers to DHT routing table BEFORE bootstrapping // Add bootstrap peers to peerstore for peer exchange
if len(n.config.Discovery.BootstrapPeers) > 0 { if len(n.config.Discovery.BootstrapPeers) > 0 {
n.logger.Info("Adding bootstrap peers to DHT routing table") n.logger.ComponentInfo(logging.ComponentNode, "Adding bootstrap peers to peerstore")
for _, bootstrapAddr := range n.config.Discovery.BootstrapPeers { for _, bootstrapAddr := range n.config.Discovery.BootstrapPeers {
if ma, err := multiaddr.NewMultiaddr(bootstrapAddr); err == nil { if ma, err := multiaddr.NewMultiaddr(bootstrapAddr); err == nil {
if peerInfo, err := peer.AddrInfoFromP2pAddr(ma); err == nil { if peerInfo, err := peer.AddrInfoFromP2pAddr(ma); err == nil {
// Add to peerstore with longer TTL // Add to peerstore with longer TTL for peer exchange
n.host.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, time.Hour*24) n.host.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, time.Hour*24)
n.logger.ComponentDebug(logging.ComponentNode, "Added bootstrap peer to peerstore",
// Force add to DHT routing table
added, err := n.dht.RoutingTable().TryAddPeer(peerInfo.ID, true, true)
if err != nil {
n.logger.Debug("Failed to add bootstrap peer to DHT routing table",
zap.String("peer", peerInfo.ID.String()),
zap.Error(err))
} else if added {
n.logger.Info("Successfully added bootstrap peer to DHT routing table",
zap.String("peer", peerInfo.ID.String())) zap.String("peer", peerInfo.ID.String()))
} }
} }
} }
} }
}
// Bootstrap the DHT AFTER connecting to bootstrap peers and adding them to routing table // DHT and routing table logic removed - using simplified peer exchange instead
if err = kademliaDHT.Bootstrap(context.Background()); err != nil { n.logger.ComponentInfo(logging.ComponentNode, "LibP2P host started successfully - using bootstrap + peer exchange discovery")
n.logger.Warn("Failed to bootstrap DHT", zap.Error(err))
// Don't fail - continue without DHT
} else {
n.logger.ComponentInfo(logging.ComponentDHT, "DHT bootstrap initiated successfully")
}
// Give DHT a moment to initialize, then add connected peers to routing table
go func() {
time.Sleep(2 * time.Second)
connectedPeers := n.host.Network().Peers()
for _, peerID := range connectedPeers {
if peerID != n.host.ID() {
addrs := n.host.Peerstore().Addrs(peerID)
if len(addrs) > 0 {
n.host.Peerstore().AddAddrs(peerID, addrs, time.Hour*24)
n.logger.Info("Added connected peer to DHT peerstore",
zap.String("peer", peerID.String()))
// Try to add this peer to DHT routing table explicitly
if n.dht != nil {
added, err := n.dht.RoutingTable().TryAddPeer(peerID, true, true)
if err != nil {
n.logger.Debug("Failed to add peer to DHT routing table",
zap.String("peer", peerID.String()),
zap.Error(err))
} else if added {
n.logger.Info("Successfully added peer to DHT routing table",
zap.String("peer", peerID.String()))
} else {
n.logger.Debug("Peer already in DHT routing table or rejected",
zap.String("peer", peerID.String()))
}
}
}
}
}
// Force multiple DHT refresh attempts to populate routing table
if n.dht != nil {
n.logger.Info("Forcing DHT refresh to discover peers")
for i := 0; i < 3; i++ {
time.Sleep(1 * time.Second)
n.dht.RefreshRoutingTable()
// Check if routing table is populated
routingPeers := n.dht.RoutingTable().ListPeers()
n.logger.Info("DHT routing table status after refresh",
zap.Int("attempt", i+1),
zap.Int("peers_in_table", len(routingPeers)))
if len(routingPeers) > 0 {
break // Success!
}
}
}
}()
// Start peer discovery and monitoring // Start peer discovery and monitoring
n.startPeerDiscovery() n.startPeerDiscovery()
@ -323,56 +248,20 @@ func (n *Node) startLibP2P() error {
return nil return nil
} }
// connectToBootstrapPeers connects to configured LibP2P bootstrap peers // startStorageService initializes the storage service
func (n *Node) connectToBootstrapPeers() error { func (n *Node) startStorageService() error {
if len(n.config.Discovery.BootstrapPeers) == 0 { n.logger.ComponentInfo(logging.ComponentStorage, "Starting storage service")
n.logger.ComponentDebug(logging.ComponentDHT, "No bootstrap peers configured")
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // Create storage service using the RQLite SQL adapter
defer cancel() service, err := storage.NewService(n.rqliteAdapter.GetSQLDB(), n.logger.Logger)
for _, bootstrapAddr := range n.config.Discovery.BootstrapPeers {
if err := n.connectToBootstrapPeer(ctx, bootstrapAddr); err != nil {
n.logger.Warn("Failed to connect to bootstrap peer",
zap.String("addr", bootstrapAddr),
zap.Error(err))
continue
}
}
return nil
}
// connectToBootstrapPeer connects to a single bootstrap peer
func (n *Node) connectToBootstrapPeer(ctx context.Context, addr string) error {
ma, err := multiaddr.NewMultiaddr(addr)
if err != nil { if err != nil {
return fmt.Errorf("invalid multiaddr: %w", err) return err
} }
// Extract peer info from multiaddr n.storageService = service
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
if err != nil {
return fmt.Errorf("failed to extract peer info: %w", err)
}
// Log resolved peer info prior to connect // Set up stream handler for storage protocol
n.logger.ComponentDebug(logging.ComponentDHT, "Resolved bootstrap peer", n.host.SetStreamHandler("/network/storage/1.0.0", n.storageService.HandleStorageStream)
zap.String("peer_id", peerInfo.ID.String()),
zap.String("addr", addr),
zap.Int("addr_count", len(peerInfo.Addrs)),
)
// Connect to the peer
if err := n.host.Connect(ctx, *peerInfo); err != nil {
return fmt.Errorf("failed to connect to peer: %w", err)
}
n.logger.Info("Connected to bootstrap peer",
zap.String("peer", peerInfo.ID.String()),
zap.String("addr", addr))
return nil return nil
} }
@ -418,59 +307,6 @@ func (n *Node) loadOrCreateIdentity() (crypto.PrivKey, error) {
return priv, nil return priv, nil
} }
// startStorageService initializes the storage service
func (n *Node) startStorageService() error {
n.logger.ComponentInfo(logging.ComponentStorage, "Starting storage service")
// Create storage service using the RQLite SQL adapter
service, err := storage.NewService(n.rqliteAdapter.GetSQLDB(), n.logger.Logger)
if err != nil {
return err
}
n.storageService = service
// Set up stream handler for storage protocol
n.host.SetStreamHandler("/network/storage/1.0.0", n.storageService.HandleStorageStream)
return nil
}
// getListenAddresses returns the current listen addresses as strings
// Stop stops the node and all its services
func (n *Node) Stop() error {
n.logger.ComponentInfo(logging.ComponentNode, "Stopping network node")
// Stop peer discovery
n.stopPeerDiscovery()
// Stop storage service
if n.storageService != nil {
n.storageService.Close()
}
// Stop DHT
if n.dht != nil {
n.dht.Close()
}
// Stop LibP2P host
if n.host != nil {
n.host.Close()
}
// Stop RQLite
if n.rqliteAdapter != nil {
n.rqliteAdapter.Close()
}
if n.rqliteManager != nil {
_ = n.rqliteManager.Stop()
}
n.logger.ComponentInfo(logging.ComponentNode, "Network node stopped")
return nil
}
// GetPeerID returns the peer ID of this node // GetPeerID returns the peer ID of this node
func (n *Node) GetPeerID() string { func (n *Node) GetPeerID() string {
if n.host == nil { if n.host == nil {
@ -485,143 +321,75 @@ func (n *Node) startPeerDiscovery() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
n.discoveryCancel = cancel n.discoveryCancel = cancel
// Start discovery in a goroutine // Start bootstrap peer connections immediately
go func() { go func() {
// Do initial discovery immediately (no delay for faster discovery) n.connectToBootstrapPeers(ctx)
n.discoverPeers(ctx)
// Start with frequent discovery for the first minute // Periodic peer discovery using interval from config
rapidTicker := time.NewTicker(10 * time.Second) ticker := time.NewTicker(n.config.Discovery.DiscoveryInterval)
rapidAttempts := 0 defer ticker.Stop()
maxRapidAttempts := 6 // 6 attempts * 10 seconds = 1 minute
for {
select {
case <-ctx.Done():
rapidTicker.Stop()
return
case <-rapidTicker.C:
n.discoverPeers(ctx)
rapidAttempts++
// After rapid attempts, switch to slower periodic discovery
if rapidAttempts >= maxRapidAttempts {
rapidTicker.Stop()
// Continue with slower periodic discovery every 15 seconds
slowTicker := time.NewTicker(15 * time.Second)
defer slowTicker.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-slowTicker.C: case <-ticker.C:
n.discoverPeers(ctx) n.discoverPeers(ctx)
} }
} }
}
}
}
}() }()
} }
// discoverPeers discovers and connects to new peers // discoverPeers discovers and connects to new peers using peer exchange
func (n *Node) discoverPeers(ctx context.Context) { func (n *Node) discoverPeers(ctx context.Context) {
if n.host == nil || n.dht == nil { if n.host == nil {
return return
} }
connectedPeers := n.host.Network().Peers() connectedPeers := n.host.Network().Peers()
initialCount := len(connectedPeers) initialCount := len(connectedPeers)
n.logger.Debug("Node peer discovery", if initialCount == 0 {
// No peers connected, try bootstrap peers again
n.logger.ComponentInfo(logging.ComponentNode, "No peers connected, retrying bootstrap peers")
n.connectToBootstrapPeers(ctx)
return
}
n.logger.ComponentDebug(logging.ComponentNode, "Discovering peers via peer exchange",
zap.Int("current_peers", initialCount)) zap.Int("current_peers", initialCount))
// Strategy 1: Use DHT to find new peers // Strategy: Use peer exchange through libp2p's identify protocol
newConnections := n.discoverViaDHT(ctx) // LibP2P automatically exchanges peer information when peers connect
// We just need to try connecting to peers in our peerstore
// Strategy 2: Search for random peers using DHT FindPeer newConnections := n.discoverViaPeerExchange(ctx)
finalPeerCount := len(n.host.Network().Peers()) finalPeerCount := len(n.host.Network().Peers())
if newConnections > 0 || finalPeerCount != initialCount { if newConnections > 0 {
n.logger.Debug("Node peer discovery completed", n.logger.ComponentInfo(logging.ComponentNode, "Peer discovery completed",
zap.Int("new_connections", newConnections), zap.Int("new_connections", newConnections),
zap.Int("initial_peers", initialCount), zap.Int("initial_peers", initialCount),
zap.Int("final_peers", finalPeerCount)) zap.Int("final_peers", finalPeerCount))
} }
} }
// discoverViaDHT uses the DHT to find and connect to new peers // discoverViaPeerExchange discovers new peers using peer exchange (identify protocol)
func (n *Node) discoverViaDHT(ctx context.Context) int { func (n *Node) discoverViaPeerExchange(ctx context.Context) int {
if n.dht == nil {
return 0
}
connected := 0 connected := 0
maxConnections := 5 maxConnections := 3 // Conservative limit to avoid overwhelming proxy
// Get peers from routing table // Get all peers from peerstore (includes peers discovered through identify protocol)
routingTablePeers := n.dht.RoutingTable().ListPeers()
n.logger.ComponentDebug(logging.ComponentDHT, "Node DHT routing table has peers", zap.Int("count", len(routingTablePeers)))
// Strategy 1: Connect to peers in DHT routing table
for _, peerID := range routingTablePeers {
if peerID == n.host.ID() {
continue
}
// Check if already connected
if n.host.Network().Connectedness(peerID) == 1 {
continue
}
// Get addresses for this peer
addrs := n.host.Peerstore().Addrs(peerID)
if len(addrs) == 0 {
continue
}
// Try to connect
connectCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
peerInfo := peer.AddrInfo{ID: peerID, Addrs: addrs}
if err := n.host.Connect(connectCtx, peerInfo); err != nil {
cancel()
n.logger.Debug("Failed to connect to DHT peer",
zap.String("peer", peerID.String()),
zap.Error(err))
continue
}
cancel()
n.logger.Debug("Node connected to new peer via DHT",
zap.String("peer", peerID.String()))
connected++
if connected >= maxConnections {
break
}
}
// Strategy 2: Use peer exchange - check what peers our connected peers know about
connectedPeers := n.host.Network().Peers()
for _, connectedPeer := range connectedPeers {
if connectedPeer == n.host.ID() {
continue
}
// Get all peers from peerstore (this includes peers that connected peers might know about)
allKnownPeers := n.host.Peerstore().Peers() allKnownPeers := n.host.Peerstore().Peers()
for _, knownPeer := range allKnownPeers { for _, knownPeer := range allKnownPeers {
if knownPeer == n.host.ID() || knownPeer == connectedPeer { if knownPeer == n.host.ID() {
continue continue
} }
// Skip if already connected // Skip if already connected
if n.host.Network().Connectedness(knownPeer) == 1 { if n.host.Network().Connectedness(knownPeer) == network.Connected {
continue continue
} }
@ -631,23 +399,12 @@ func (n *Node) discoverViaDHT(ctx context.Context) int {
continue continue
} }
// Filter addresses to only include listening ports (not ephemeral client ports) // Filter to only standard P2P ports (avoid ephemeral client ports)
var validAddrs []multiaddr.Multiaddr var validAddrs []multiaddr.Multiaddr
for _, addr := range addrs { for _, addr := range addrs {
addrStr := addr.String() addrStr := addr.String()
// Skip ephemeral ports (typically above 49152) and keep standard ports // Keep addresses with standard P2P ports (4000-4999 range)
if !strings.Contains(addrStr, ":53") && // Skip ephemeral ports starting with 53 if strings.Contains(addrStr, ":400") {
!strings.Contains(addrStr, ":54") && // Skip ephemeral ports starting with 54
!strings.Contains(addrStr, ":55") && // Skip ephemeral ports starting with 55
!strings.Contains(addrStr, ":56") && // Skip ephemeral ports starting with 56
!strings.Contains(addrStr, ":57") && // Skip ephemeral ports starting with 57
!strings.Contains(addrStr, ":58") && // Skip ephemeral ports starting with 58
!strings.Contains(addrStr, ":59") && // Skip ephemeral ports starting with 59
!strings.Contains(addrStr, ":6") && // Skip ephemeral ports starting with 6
(strings.Contains(addrStr, ":400") || // Include 4000-4999 range
strings.Contains(addrStr, ":401") ||
strings.Contains(addrStr, ":402") ||
strings.Contains(addrStr, ":403")) {
validAddrs = append(validAddrs, addr) validAddrs = append(validAddrs, addr)
} }
} }
@ -656,26 +413,25 @@ func (n *Node) discoverViaDHT(ctx context.Context) int {
continue continue
} }
// Try to connect using only valid addresses // Try to connect with shorter timeout (proxy connections are slower)
connectCtx, cancel := context.WithTimeout(ctx, 5*time.Second) connectCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
peerInfo := peer.AddrInfo{ID: knownPeer, Addrs: validAddrs} peerInfo := peer.AddrInfo{ID: knownPeer, Addrs: validAddrs}
if err := n.host.Connect(connectCtx, peerInfo); err != nil { if err := n.host.Connect(connectCtx, peerInfo); err != nil {
cancel() cancel()
n.logger.Debug("Failed to connect to peerstore peer", n.logger.ComponentDebug(logging.ComponentNode, "Failed to connect to peer via exchange",
zap.String("peer", knownPeer.String()), zap.String("peer", knownPeer.String()),
zap.Error(err)) zap.Error(err))
continue continue
} }
cancel() cancel()
n.logger.Debug("Node connected to new peer via peerstore", n.logger.ComponentInfo(logging.ComponentNode, "Connected to new peer via peer exchange",
zap.String("peer", knownPeer.String())) zap.String("peer", knownPeer.String()))
connected++ connected++
if connected >= maxConnections { if connected >= maxConnections {
return connected break
}
} }
} }
@ -711,4 +467,76 @@ func (n *Node) stopPeerDiscovery() {
n.discoveryCancel() n.discoveryCancel()
n.discoveryCancel = nil n.discoveryCancel = nil
} }
n.logger.ComponentInfo(logging.ComponentNode, "Peer discovery stopped")
}
// getListenAddresses returns the current listen addresses as strings
// Stop stops the node and all its services
func (n *Node) Stop() error {
n.logger.ComponentInfo(logging.ComponentNode, "Stopping network node")
// Stop peer discovery
n.stopPeerDiscovery()
// Stop storage service
if n.storageService != nil {
n.storageService.Close()
}
// Stop DHT
// DHT removed - using simplified bootstrap + peer exchange discovery
// Stop LibP2P host
if n.host != nil {
n.host.Close()
}
// Stop RQLite
if n.rqliteAdapter != nil {
n.rqliteAdapter.Close()
}
if n.rqliteManager != nil {
_ = n.rqliteManager.Stop()
}
n.logger.ComponentInfo(logging.ComponentNode, "Network node stopped")
return nil
}
// Starts the network node
func (n *Node) Start(ctx context.Context) error {
n.logger.Info("Starting network node", zap.String("data_dir", n.config.Node.DataDir))
// Create data directory
if err := os.MkdirAll(n.config.Node.DataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
// Start RQLite
if err := n.startRQLite(ctx); err != nil {
return fmt.Errorf("failed to start RQLite: %w", err)
}
// Start LibP2P host
if err := n.startLibP2P(); err != nil {
return fmt.Errorf("failed to start LibP2P: %w", err)
}
// Start storage service
if err := n.startStorageService(); err != nil {
return fmt.Errorf("failed to start storage service: %w", err)
}
// Get listen addresses for logging
var listenAddrs []string
for _, addr := range n.host.Addrs() {
listenAddrs = append(listenAddrs, addr.String())
}
n.logger.ComponentInfo(logging.ComponentNode, "Network node started successfully",
zap.String("peer_id", n.host.ID().String()),
zap.Strings("listen_addrs", listenAddrs),
)
return nil
} }