diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2025-12-01 03:23:58 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-01 03:23:58 +0100 |
| commit | 40c801123e64837c1cf046bbcaa8d8ae4530f2a4 (patch) | |
| tree | 5e5c0c0389d7226aa2eb095fe3dd74e0c74dc289 | |
| parent | ec800b66a16947b265b91fa8bbadfd16b302f4bc (diff) | |
| download | fog-40c801123e64837c1cf046bbcaa8d8ae4530f2a4.tar.gz fog-40c801123e64837c1cf046bbcaa8d8ae4530f2a4.tar.xz fog-40c801123e64837c1cf046bbcaa8d8ae4530f2a4.zip | |
Update fmt.Println message from 'Hello' to 'Goodbye'
| -rw-r--r-- | fog.go | 2411 |
1 files changed, 917 insertions, 1494 deletions
@@ -1,5 +1,5 @@ -// fog v1.3.3-minimal - Anonymous SMTP Relay -// Fixed Sphinx encryption with AES-256-GCM + Full mixnetwork +// fog v2.0.1 - Anonymous SMTP Relay with Sphinx Mixnet +// Fixed padding, no PKI server needed, debug logs // Copyright 2025 - fog Project package main @@ -13,18 +13,16 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha256" - "encoding/binary" "encoding/base64" + "encoding/binary" "encoding/json" "errors" "flag" "fmt" "io" "log" - "math" - mathrand "math/rand" + "math/big" "net" - "net/http" "net/smtp" "os" "os/signal" @@ -40,119 +38,90 @@ import ( "golang.org/x/net/proxy" ) -// ============================================================================ -// CONSTANTS -// ============================================================================ - const ( - Version = "1.3.3-minimal" - AppName = "fog" - - // Network - TorSocksProxyAddr = "127.0.0.1:9050" - DefaultSMTPPort = "2525" - DefaultNodePort = "9999" - - // Timing - MinDelay = 100 * time.Millisecond - MaxDelay = 10 * time.Second - PoissonLambda = 2.0 - PKIRefreshInterval = 5 * time.Minute // Sync keys frequently - PKIRetryInterval = 2 * time.Minute - HealthCheckInterval = 5 * time.Minute - StatsInterval = 60 * time.Second - KeyRotationInterval = 24 * time.Hour // 24 hours - - // Mixnet batching - BatchInterval = 30 * time.Second - BatchSize = 10 - MinBatchDelay = 5 * time.Second - MaxBatchDelay = 60 * time.Second - - // Limits - MaxMessageSize = 10 * 1024 * 1024 // 10MB - MaxRecipients = 100 - MessageQueueSize = 1000 - ReplayCacheSize = 10000 - ReplayCacheTTL = 24 * time.Hour - WorkerCount = 5 - - // Padding - MinPaddingSize = 512 - MaxPaddingSize = 32768 - PaddingBuckets = 9 - - // Rate limiting - RateLimitMessages = 100 - RateLimitWindow = 1 * time.Hour - - // Sphinx - Variable hop routing - MinSphinxHops = 3 - MaxSphinxHops = 6 - SphinxHeaderSize = 256 - SphinxPayloadSize = 10 * 1024 * 1024 - AESKeySize = 32 // AES-256 - AESNonceSize = 12 // GCM nonce - HMACSize = 32 // SHA256 -) + Version = "2.0.1" -// Padding bucket sizes -var PaddingSizes = [PaddingBuckets]int{ - 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, -} + TorSocks = "127.0.0.1:9050" + DefaultPort = "2525" + NodePort = "9999" + + MinDelay = 500 * time.Millisecond + MaxDelay = 5 * time.Second + + BatchWindow = 30 * time.Second + BatchSize = 10 + + HealthInterval = 3 * time.Minute + StatsInterval = 60 * time.Second + + MaxMsgSize = 10 << 20 + MaxRecipient = 50 + QueueSize = 500 + Workers = 3 + + CacheSize = 10000 + CacheTTL = 24 * time.Hour + + SphinxHops = 3 + HeaderSize = 256 + AESKeySize = 32 + NonceSize = 12 + HMACSize = 32 + + // Padding bucket for Sphinx packets (not message content) + PaddedPayloadSize = 64 * 1024 // 64KB fixed size for all Sphinx payloads +) // ============================================================================ // TYPES // ============================================================================ type Message struct { - ID string - From string - To []string - Data []byte - Timestamp time.Time - Size int + ID string + From string + To []string + Data []byte + Time time.Time } -type LocalNode struct { - NodeID string - PrivateKey []byte - PublicKey []byte - Address string - Version string - CreatedAt time.Time - mu sync.RWMutex +type Node struct { + ID string `json:"node_id"` + PubKey string `json:"public_key"` + Address string `json:"address"` + Name string `json:"name"` + Version string `json:"version"` + Healthy bool `json:"-"` + LastOK time.Time `json:"-"` } -type PKINode struct { - NodeID string `json:"node_id"` - PublicKey []byte `json:"public_key"` - Address string `json:"address"` - Version string `json:"version"` - LastSeen time.Time `json:"last_seen"` - Healthy bool `json:"healthy"` - FailureCount int `json:"failure_count"` - SuccessCount int `json:"success_count"` - LastHealthy time.Time `json:"last_healthy"` +func (n *Node) GetPubKey() ([]byte, error) { + return base64.StdEncoding.DecodeString(n.PubKey) } -type PKIDirectory struct { - Nodes map[string]*PKINode `json:"nodes"` - UpdatedAt time.Time `json:"updated_at"` - mu sync.RWMutex +type PKI struct { + Version string `json:"version"` + Updated string `json:"updated"` + Nodes []*Node `json:"nodes"` + mu sync.RWMutex } -type Statistics struct { - StartTime time.Time `json:"start_time"` - MessagesReceived int64 `json:"messages_received"` - MessagesRelayed int64 `json:"messages_relayed"` - MessagesFailed int64 `json:"messages_failed"` - BytesProcessed int64 `json:"bytes_processed"` - SphinxRouted int64 `json:"sphinx_routed"` - DirectRouted int64 `json:"direct_routed"` - SphinxReceived int64 `json:"sphinx_received"` - SphinxForwarded int64 `json:"sphinx_forwarded"` - mu sync.RWMutex +type LocalNode struct { + ID string + Private []byte + Public []byte + Address string + mu sync.RWMutex +} + +type Stats struct { + Start time.Time + Recv int64 + Sent int64 + Failed int64 + Sphinx int64 + Direct int64 + MixRecv int64 + MixFwd int64 } type ReplayCache struct { @@ -160,1538 +129,911 @@ type ReplayCache struct { mu sync.RWMutex } -type RateLimiter struct { - connections map[string][]time.Time - mu sync.RWMutex +type Batch struct { + packets []*SphinxPacket + start time.Time + mu sync.Mutex } -// Sphinx packet structures type SphinxHeader struct { - Version byte - EphemeralKey [32]byte // Curve25519 public key - RoutingInfo []byte // Encrypted routing for all hops - HMAC [32]byte // Authentication + Version byte + EphKey [32]byte + Routing []byte + MAC [32]byte } type SphinxPacket struct { Header *SphinxHeader - Payload []byte // AES-256-GCM encrypted payload + Payload []byte } type RoutingInfo struct { - NextHop string // Next node address or "EXIT" - MessageFrom string // Original sender (only for exit node) - MessageTo string // Final recipient (only for exit node) -} - -type SMTPServer struct { - hostname string - addr string - listener net.Listener -} - -type SphinxNodeServer struct { - addr string - listener net.Listener -} - -// Mixnet batch -type MixnetBatch struct { - packets []*SphinxPacket - startTime time.Time - mu sync.Mutex + Next string `json:"next"` + From string `json:"from,omitempty"` + To string `json:"to,omitempty"` } // ============================================================================ -// GLOBAL STATE +// GLOBALS // ============================================================================ var ( - // Core components - localNode *LocalNode - pkiDirectory *PKIDirectory - stats *Statistics - replayCache *ReplayCache - rateLimiter *RateLimiter - messageQueue chan *Message - serverHostname string - - // Mixnet - mixnetBatch *MixnetBatch - - // Tor connectivity + local *LocalNode + pki *PKI + stats *Stats + replay *ReplayCache + batch *Batch + queue chan *Message torDialer proxy.Dialer - - // Shutdown - shutdownCtx context.Context - shutdownCancel context.CancelFunc - shutdownWg sync.WaitGroup - - // Configuration - enableSphinx atomic.Bool - pkiURL string - dataDir string - statsFile string - nodeAddr string + hostname string + pkiFile string + dataDir string + useSphinx atomic.Bool + debugMode bool + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup ) // ============================================================================ -// MIXNET BATCH +// DEBUG LOGGING // ============================================================================ -func NewMixnetBatch() *MixnetBatch { - return &MixnetBatch{ - packets: make([]*SphinxPacket, 0, BatchSize), - startTime: time.Now(), - } -} - -func (b *MixnetBatch) Add(packet *SphinxPacket) { - b.mu.Lock() - defer b.mu.Unlock() - b.packets = append(b.packets, packet) -} - -func (b *MixnetBatch) Ready() bool { - b.mu.Lock() - defer b.mu.Unlock() - - // Batch ready if: full OR timeout reached - if len(b.packets) >= BatchSize { - return true - } - if time.Since(b.startTime) > BatchInterval { - return len(b.packets) > 0 - } - return false -} - -func (b *MixnetBatch) Flush() []*SphinxPacket { - b.mu.Lock() - defer b.mu.Unlock() - - packets := b.packets - b.packets = make([]*SphinxPacket, 0, BatchSize) - b.startTime = time.Now() - - // Shuffle batch for timing attack resistance - mathrand.Shuffle(len(packets), func(i, j int) { - packets[i], packets[j] = packets[j], packets[i] - }) - - return packets -} - -func MixnetBatchWorker() { - defer shutdownWg.Done() - - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-shutdownCtx.Done(): - return - case <-ticker.C: - if mixnetBatch.Ready() { - packets := mixnetBatch.Flush() - log.Printf("[MIXNET] Flushing batch: %d packets", len(packets)) - - for _, packet := range packets { - go processSphinxPacket(packet) - } - } - } +func dbg(format string, args ...interface{}) { + if debugMode { + log.Printf("[DEBUG] "+format, args...) } } // ============================================================================ -// PKI DIRECTORY +// CRYPTO // ============================================================================ -func NewPKIDirectory() *PKIDirectory { - return &PKIDirectory{ - Nodes: make(map[string]*PKINode), - UpdatedAt: time.Now(), - } -} - -func (p *PKIDirectory) AddNode(node *PKINode) { - p.mu.Lock() - defer p.mu.Unlock() - p.Nodes[node.NodeID] = node - p.UpdatedAt = time.Now() -} - -func (p *PKIDirectory) GetNode(nodeID string) (*PKINode, bool) { - p.mu.RLock() - defer p.mu.RUnlock() - node, ok := p.Nodes[nodeID] - return node, ok -} - -func (p *PKIDirectory) GetHealthyNodes() []*PKINode { - p.mu.RLock() - defer p.mu.RUnlock() - - var healthy []*PKINode - for _, node := range p.Nodes { - // Skip unhealthy nodes - if !node.Healthy { - continue - } - // Skip self only if localNode is initialized - if localNode != nil && node.NodeID == localNode.NodeID { - continue - } - healthy = append(healthy, node) - } - return healthy -} - -func (p *PKIDirectory) HealthyCount() int { - return len(p.GetHealthyNodes()) +func secureDelay() time.Duration { + b := make([]byte, 8) + rand.Read(b) + val := binary.BigEndian.Uint64(b) + // Map to range [MinDelay, MaxDelay] + rangeMs := uint64((MaxDelay - MinDelay).Milliseconds()) + delayMs := MinDelay.Milliseconds() + int64(val%rangeMs) + return time.Duration(delayMs) * time.Millisecond } -func (p *PKIDirectory) LoadFromFile(path string) error { - data, err := os.ReadFile(path) - if err != nil { - return err - } - - p.mu.Lock() - defer p.mu.Unlock() - - if err := json.Unmarshal(data, p); err != nil { - return err +func secureShuffle[T any](s []T) { + for i := len(s) - 1; i > 0; i-- { + jBig, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1))) + j := int(jBig.Int64()) + s[i], s[j] = s[j], s[i] } - - log.Printf("[PKI] Loaded %d nodes from file", len(p.Nodes)) - return nil } -func (p *PKIDirectory) SaveToFile(path string) error { - p.mu.RLock() - data, err := json.MarshalIndent(p, "", " ") - p.mu.RUnlock() - - if err != nil { - return err +func deriveKeys(secret []byte) (enc, mac []byte, err error) { + kdf := hkdf.New(sha256.New, secret, nil, []byte("fog-v2")) + enc = make([]byte, AESKeySize) + mac = make([]byte, AESKeySize) + if _, err = io.ReadFull(kdf, enc); err != nil { + return } - - return os.WriteFile(path, data, 0600) + _, err = io.ReadFull(kdf, mac) + return } -func (p *PKIDirectory) LoadFromURL(url string) error { - client := &http.Client{ - Timeout: 30 * time.Second, - Transport: &http.Transport{ - Dial: torDialer.Dial, - }, - } - - resp, err := client.Get(url) +func encrypt(plain, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("HTTP %d", resp.StatusCode) + return nil, err } - - data, err := io.ReadAll(resp.Body) + gcm, err := cipher.NewGCM(block) if err != nil { - return err + return nil, err } - - p.mu.Lock() - defer p.mu.Unlock() - - if err := json.Unmarshal(data, p); err != nil { - return err + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, err } - - log.Printf("[PKI] Loaded %d nodes from URL", len(p.Nodes)) - return nil + return gcm.Seal(nonce, nonce, plain, nil), nil } -// ============================================================================ -// LOCAL NODE -// ============================================================================ - -func InitializeLocalNode(address string) { - keyFile := filepath.Join(dataDir, "node.key") - - var privateKey []byte - - // Try to load existing key - if data, err := os.ReadFile(keyFile); err == nil && len(data) == 32 { - privateKey = data - log.Printf("[NODE] Loaded existing key from %s", keyFile) - } else { - // Generate new key - privateKey = make([]byte, 32) - if _, err := rand.Read(privateKey); err != nil { - log.Fatalf("[NODE] Failed to generate private key: %v", err) - } - - // Save key - if err := os.WriteFile(keyFile, privateKey, 0600); err != nil { - log.Printf("[NODE] Warning: failed to save key: %v", err) - } else { - log.Printf("[NODE] Saved new key to %s", keyFile) - } +func decrypt(ciphertext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err } - - publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint) + gcm, err := cipher.NewGCM(block) if err != nil { - log.Fatalf("[NODE] Failed to generate public key: %v", err) + return nil, err } - - nodeID := generateNodeID(publicKey) - - localNode = &LocalNode{ - NodeID: nodeID, - PrivateKey: privateKey, - PublicKey: publicKey, - Address: address, - Version: Version, - CreatedAt: time.Now(), + if len(ciphertext) < gcm.NonceSize() { + return nil, errors.New("too short") } - - log.Printf("[NODE] Initialized: %s", nodeID[:16]) - log.Printf("[NODE] Address: %s", address) - log.Printf("[NODE] Public key: %x", publicKey[:16]) + nonce := ciphertext[:gcm.NonceSize()] + return gcm.Open(nil, nonce, ciphertext[gcm.NonceSize():], nil) } -func generateNodeID(publicKey []byte) string { - hash := sha256.Sum256(publicKey) - return fmt.Sprintf("%x", hash[:]) +func computeMAC(data, key []byte) []byte { + h := hmac.New(sha256.New, key) + h.Write(data) + return h.Sum(nil) } -func RotatePKIKeys() { - defer shutdownWg.Done() - - ticker := time.NewTicker(KeyRotationInterval) - defer ticker.Stop() - - for { - select { - case <-shutdownCtx.Done(): - return - case <-ticker.C: - localNode.mu.Lock() - - oldKey := localNode.NodeID[:16] - - privateKey := make([]byte, 32) - if _, err := rand.Read(privateKey); err != nil { - log.Printf("[PKI] Key rotation failed: %v", err) - localNode.mu.Unlock() - continue - } - - publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint) - if err != nil { - log.Printf("[PKI] Key rotation failed: %v", err) - localNode.mu.Unlock() - continue - } - - localNode.PrivateKey = privateKey - localNode.PublicKey = publicKey - localNode.NodeID = generateNodeID(publicKey) - - log.Printf("[PKI] Keys rotated: %s -> %s", oldKey, localNode.NodeID[:16]) - localNode.mu.Unlock() - } - } +func verifyMAC(data, mac, key []byte) bool { + return hmac.Equal(computeMAC(data, key), mac) } // ============================================================================ -// STATISTICS +// PADDING - FIXED: Preserves original message +// Format: [4 bytes original length][original data][random padding] // ============================================================================ -func NewStatistics() *Statistics { - return &Statistics{ - StartTime: time.Now(), - } -} - -func (s *Statistics) IncrementReceived() { - atomic.AddInt64(&s.MessagesReceived, 1) -} - -func (s *Statistics) IncrementRelayed() { - atomic.AddInt64(&s.MessagesRelayed, 1) -} - -func (s *Statistics) IncrementFailed() { - atomic.AddInt64(&s.MessagesFailed, 1) -} - -func (s *Statistics) AddBytes(n int64) { - atomic.AddInt64(&s.BytesProcessed, n) -} - -func (s *Statistics) IncrementSphinx() { - atomic.AddInt64(&s.SphinxRouted, 1) -} - -func (s *Statistics) IncrementDirect() { - atomic.AddInt64(&s.DirectRouted, 1) -} - -func (s *Statistics) IncrementSphinxReceived() { - atomic.AddInt64(&s.SphinxReceived, 1) -} - -func (s *Statistics) IncrementSphinxForwarded() { - atomic.AddInt64(&s.SphinxForwarded, 1) -} - -func (s *Statistics) Print() { - uptime := time.Since(s.StartTime) - fmt.Printf("\n=== fog Statistics ===\n") - fmt.Printf("Uptime: %s\n", uptime.Round(time.Second)) - fmt.Printf("Messages: R:%d D:%d F:%d\n", - atomic.LoadInt64(&s.MessagesReceived), - atomic.LoadInt64(&s.MessagesRelayed), - atomic.LoadInt64(&s.MessagesFailed)) - fmt.Printf("Routing: Sphinx:%d Direct:%d\n", - atomic.LoadInt64(&s.SphinxRouted), - atomic.LoadInt64(&s.DirectRouted)) - fmt.Printf("Mixnet: Recv:%d Fwd:%d\n", - atomic.LoadInt64(&s.SphinxReceived), - atomic.LoadInt64(&s.SphinxForwarded)) - fmt.Printf("Bytes: %d MB\n", atomic.LoadInt64(&s.BytesProcessed)/1024/1024) - fmt.Printf("Success rate: %.1f%%\n", s.SuccessRate()) - fmt.Println() -} - -func (s *Statistics) SuccessRate() float64 { - total := atomic.LoadInt64(&s.MessagesReceived) - if total == 0 { - return 0 - } - relayed := atomic.LoadInt64(&s.MessagesRelayed) - return float64(relayed) / float64(total) * 100.0 +func padPayload(data []byte) []byte { + origLen := len(data) + + // Create padded payload with length prefix + // Format: [4 bytes len][data][random padding to PaddedPayloadSize] + padded := make([]byte, PaddedPayloadSize) + + // First 4 bytes: original length + binary.BigEndian.PutUint32(padded[:4], uint32(origLen)) + + // Copy original data + copy(padded[4:], data) + + // Fill rest with random bytes + rand.Read(padded[4+origLen:]) + + dbg("Padded payload: %d -> %d bytes", origLen, PaddedPayloadSize) + return padded } -func (s *Statistics) SaveToFile(path string) error { - s.mu.RLock() - defer s.mu.RUnlock() - - data, err := json.MarshalIndent(s, "", " ") - if err != nil { - return err +func unpadPayload(padded []byte) ([]byte, error) { + if len(padded) < 4 { + return nil, errors.New("padded data too short") } - - return os.WriteFile(path, data, 0600) -} - -func StatsMonitor() { - defer shutdownWg.Done() - - ticker := time.NewTicker(StatsInterval) - defer ticker.Stop() - - for { - select { - case <-shutdownCtx.Done(): - if statsFile != "" { - stats.SaveToFile(statsFile) - } - return - case <-ticker.C: - uptime := time.Since(stats.StartTime).Round(time.Second) - log.Printf("[STATS] Up:%s | R:%d D:%d F:%d | Sphinx:%d Direct:%d | Mixnet R:%d F:%d", - uptime, - atomic.LoadInt64(&stats.MessagesReceived), - atomic.LoadInt64(&stats.MessagesRelayed), - atomic.LoadInt64(&stats.MessagesFailed), - atomic.LoadInt64(&stats.SphinxRouted), - atomic.LoadInt64(&stats.DirectRouted), - atomic.LoadInt64(&stats.SphinxReceived), - atomic.LoadInt64(&stats.SphinxForwarded)) - - if statsFile != "" { - stats.SaveToFile(statsFile) - } - } + + origLen := binary.BigEndian.Uint32(padded[:4]) + + if int(origLen) > len(padded)-4 { + return nil, fmt.Errorf("invalid length: %d > %d", origLen, len(padded)-4) } + + data := make([]byte, origLen) + copy(data, padded[4:4+origLen]) + + dbg("Unpadded payload: %d -> %d bytes", len(padded), origLen) + return data, nil } // ============================================================================ // REPLAY CACHE // ============================================================================ -func NewReplayCache(size int) *ReplayCache { - return &ReplayCache{ - cache: make(map[string]time.Time, size), - } +func newReplayCache() *ReplayCache { + return &ReplayCache{cache: make(map[string]time.Time)} } -func (r *ReplayCache) Check(msgID string) bool { +func (r *ReplayCache) Check(id string) bool { r.mu.Lock() defer r.mu.Unlock() - if _, exists := r.cache[msgID]; exists { + if _, exists := r.cache[id]; exists { + dbg("Replay detected: %s", id) return false } - r.cache[msgID] = time.Now() + r.cache[id] = time.Now() - if len(r.cache) > ReplayCacheSize { + if len(r.cache) > CacheSize { now := time.Now() - for id, t := range r.cache { - if now.Sub(t) > ReplayCacheTTL { - delete(r.cache, id) + for k, v := range r.cache { + if now.Sub(v) > CacheTTL { + delete(r.cache, k) } } } - return true } // ============================================================================ -// RATE LIMITER +// LOCAL NODE // ============================================================================ -func NewRateLimiter() *RateLimiter { - return &RateLimiter{ - connections: make(map[string][]time.Time), - } -} - -func (r *RateLimiter) Allow(ip string) bool { - r.mu.Lock() - defer r.mu.Unlock() - - now := time.Now() - cutoff := now.Add(-RateLimitWindow) +func initNode(addr string) { + keyFile := filepath.Join(dataDir, "node.key") - times := r.connections[ip] - var recent []time.Time - for _, t := range times { - if t.After(cutoff) { - recent = append(recent, t) - } + var priv []byte + if data, err := os.ReadFile(keyFile); err == nil && len(data) == 32 { + priv = data + log.Printf("[NODE] Loaded existing key") + } else { + priv = make([]byte, 32) + rand.Read(priv) + os.WriteFile(keyFile, priv, 0600) + log.Printf("[NODE] Generated new key") } - if len(recent) >= RateLimitMessages { - return false + pub, _ := curve25519.X25519(priv, curve25519.Basepoint) + id := fmt.Sprintf("%x", sha256.Sum256(pub)) + + local = &LocalNode{ + ID: id, + Private: priv, + Public: pub, + Address: addr, } - recent = append(recent, now) - r.connections[ip] = recent - return true + log.Printf("[NODE] ID: %s", id[:16]) + log.Printf("[NODE] Address: %s", addr) + dbg("Full ID: %s", id) + dbg("PubKey: %x", pub) } // ============================================================================ -// TOR INITIALIZATION +// PKI - File based, no server needed // ============================================================================ -func InitializeTor() { - dialer, err := proxy.SOCKS5("tcp", TorSocksProxyAddr, nil, proxy.Direct) - if err != nil { - log.Fatalf("[TOR] Failed to create SOCKS5 dialer: %v", err) - } - - torDialer = dialer - log.Printf("[TOR] Initialized: %s", TorSocksProxyAddr) +func newPKI() *PKI { + return &PKI{Nodes: make([]*Node, 0)} } -// ============================================================================ -// PADDING -// ============================================================================ - -func calculatePadding(size int) int { - for _, bucket := range PaddingSizes { - if size <= bucket { - return bucket - size - } +func (p *PKI) Load(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err } - return 0 -} -func applyPadding(data []byte) []byte { - currentSize := len(data) - paddingNeeded := calculatePadding(currentSize) + p.mu.Lock() + defer p.mu.Unlock() - if paddingNeeded == 0 { - return data + if err := json.Unmarshal(data, p); err != nil { + return err } - padded := make([]byte, currentSize+paddingNeeded) - copy(padded, data) - - if _, err := rand.Read(padded[currentSize:]); err != nil { - log.Printf("[PADDING] Warning: failed to generate random padding: %v", err) + // Mark all as healthy initially + for _, n := range p.Nodes { + n.Healthy = true + n.LastOK = time.Now() + dbg("Loaded node: %s (%s) at %s", n.Name, n.ID[:16], n.Address) } - return padded + log.Printf("[PKI] Loaded %d nodes from %s", len(p.Nodes), path) + return nil } -// ============================================================================ -// TIMING -// ============================================================================ - -func calculateDelay() time.Duration { - u := mathrand.Float64() - delay := time.Duration(-math.Log(1-u) / PoissonLambda * float64(time.Second)) +func (p *PKI) GetAllOther() []*Node { + p.mu.RLock() + defer p.mu.RUnlock() - if delay < MinDelay { - delay = MinDelay + var nodes []*Node + for _, n := range p.Nodes { + if local != nil && n.ID == local.ID { + continue + } + nodes = append(nodes, n) } - if delay > MaxDelay { - delay = MaxDelay + return nodes +} + +func (p *PKI) GetHealthy() []*Node { + p.mu.RLock() + defer p.mu.RUnlock() + + var nodes []*Node + for _, n := range p.Nodes { + if !n.Healthy { + continue + } + if local != nil && n.ID == local.ID { + continue + } + nodes = append(nodes, n) } + return nodes +} - return delay +func (p *PKI) HealthyCount() int { + return len(p.GetHealthy()) } // ============================================================================ -// SPHINX CRYPTOGRAPHY (AES-256-GCM) +// BATCH // ============================================================================ -func selectHopCount(availableNodes int) int { - // Determine max hops we can actually use - maxPossible := MaxSphinxHops - if availableNodes < maxPossible { - maxPossible = availableNodes - } - - // Need at least MinSphinxHops - if maxPossible < MinSphinxHops { - return availableNodes // Use all available +func newBatch() *Batch { + return &Batch{ + packets: make([]*SphinxPacket, 0, BatchSize), + start: time.Now(), } - - // Random between min and max - hopCount := MinSphinxHops + mathrand.Intn(maxPossible-MinSphinxHops+1) - - log.Printf("[SPHINX] Selected %d hops (available: %d, max: %d)", - hopCount, availableNodes, MaxSphinxHops) - - return hopCount } -func deriveKeys(sharedSecret []byte) (encKey, macKey []byte, err error) { - kdf := hkdf.New(sha256.New, sharedSecret, nil, []byte("fog-sphinx-v1.3.2")) - - encKey = make([]byte, AESKeySize) - if _, err := io.ReadFull(kdf, encKey); err != nil { - return nil, nil, err - } - - macKey = make([]byte, AESKeySize) - if _, err := io.ReadFull(kdf, macKey); err != nil { - return nil, nil, err - } - - return encKey, macKey, nil +func (b *Batch) Add(p *SphinxPacket) { + b.mu.Lock() + defer b.mu.Unlock() + b.packets = append(b.packets, p) + dbg("Batch: added packet, now %d", len(b.packets)) } -func encryptLayer(plaintext, key []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err := rand.Read(nonce); err != nil { - return nil, err - } - - // GCM provides both encryption and authentication - ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) - return ciphertext, nil +func (b *Batch) Ready() bool { + b.mu.Lock() + defer b.mu.Unlock() + return len(b.packets) >= BatchSize || (len(b.packets) > 0 && time.Since(b.start) > BatchWindow) } -func decryptLayer(ciphertext, key []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonceSize := gcm.NonceSize() - if len(ciphertext) < nonceSize { - return nil, errors.New("ciphertext too short") - } - - nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] - - // GCM verifies authentication automatically - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, err - } - - return plaintext, nil -} +func (b *Batch) Flush() []*SphinxPacket { + b.mu.Lock() + defer b.mu.Unlock() -func computeHMAC(data, key []byte) []byte { - h := hmac.New(sha256.New, key) - h.Write(data) - return h.Sum(nil) + pkts := b.packets + b.packets = make([]*SphinxPacket, 0, BatchSize) + b.start = time.Now() + + secureShuffle(pkts) + dbg("Batch: flushed %d packets (shuffled)", len(pkts)) + return pkts } -func verifyHMAC(data, mac, key []byte) bool { - expected := computeHMAC(data, key) - return hmac.Equal(mac, expected) +func batchWorker() { + defer wg.Done() + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if batch.Ready() { + pkts := batch.Flush() + log.Printf("[BATCH] Processing %d packets", len(pkts)) + for _, p := range pkts { + go processPacket(p) + } + } + } + } } // ============================================================================ -// SPHINX PACKET CREATION +// SPHINX PACKET // ============================================================================ -func createSphinxPacket(message *Message, route []*PKINode) (*SphinxPacket, error) { - hopCount := len(route) - if hopCount < MinSphinxHops { - return nil, fmt.Errorf("route must have at least %d hops, got %d", MinSphinxHops, hopCount) - } - if hopCount > MaxSphinxHops { - return nil, fmt.Errorf("route must have at most %d hops, got %d", MaxSphinxHops, hopCount) +func createPacket(msg *Message, route []*Node) (*SphinxPacket, error) { + if len(route) != SphinxHops { + return nil, fmt.Errorf("need %d hops, got %d", SphinxHops, len(route)) } - // Prepare routing info for each hop - routingInfos := make([]RoutingInfo, hopCount) - - // Entry and middle nodes: next hop address - for i := 0; i < hopCount-1; i++ { - routingInfos[i] = RoutingInfo{ - NextHop: route[i+1].Address, - } + dbg("Creating Sphinx packet for %s", msg.ID) + dbg("Route: %s -> %s -> %s", route[0].Name, route[1].Name, route[2].Name) + + // Build routing info + routings := make([]RoutingInfo, SphinxHops) + for i := 0; i < SphinxHops-1; i++ { + routings[i] = RoutingInfo{Next: route[i+1].Address} + dbg("Hop %d: forward to %s", i, route[i+1].Address) } - - // Exit node: final destination - // Filter out empty recipients - validRecipients := []string{} - for _, to := range message.To { + + // Last hop: EXIT with delivery info + var validTo []string + for _, to := range msg.To { to = strings.TrimSpace(to) if to != "" { - validRecipients = append(validRecipients, to) + validTo = append(validTo, to) } } - - if len(validRecipients) == 0 { - return nil, fmt.Errorf("no valid recipients") + if len(validTo) == 0 { + return nil, errors.New("no recipients") } - - routingInfos[hopCount-1] = RoutingInfo{ - NextHop: "EXIT", - MessageFrom: message.From, - MessageTo: strings.Join(validRecipients, ","), + + routings[SphinxHops-1] = RoutingInfo{ + Next: "EXIT", + From: msg.From, + To: strings.Join(validTo, ","), } + dbg("Hop %d: EXIT, deliver to %s", SphinxHops-1, validTo) - // Start with message data - payload := message.Data + // PAD the message payload BEFORE encryption + paddedMsg := padPayload(msg.Data) + dbg("Message padded: %d -> %d bytes", len(msg.Data), len(paddedMsg)) + + // Encrypt layers (reverse: exit -> entry) + payload := paddedMsg + var ephKeys [][32]byte + var secrets [][]byte - // Encrypt in reverse order (exit -> entry) - var ephemeralKeys [][32]byte - var sharedSecrets [][]byte - for i := len(route) - 1; i >= 0; i-- { node := route[i] + dbg("Encrypting layer %d for %s", i, node.Name) - // Generate ephemeral key pair for this hop - ephemeralPriv := make([]byte, 32) - if _, err := rand.Read(ephemeralPriv); err != nil { - return nil, err - } + // Ephemeral key + ephPriv := make([]byte, 32) + rand.Read(ephPriv) + ephPub, _ := curve25519.X25519(ephPriv, curve25519.Basepoint) - ephemeralPub, err := curve25519.X25519(ephemeralPriv, curve25519.Basepoint) + // Shared secret + nodePub, err := node.GetPubKey() if err != nil { - return nil, err + return nil, fmt.Errorf("decode pubkey for %s: %w", node.Name, err) } + shared, _ := curve25519.X25519(ephPriv, nodePub) - // Compute shared secret - sharedSecret, err := curve25519.X25519(ephemeralPriv, node.PublicKey) - if err != nil { - return nil, err - } - - // Derive encryption and MAC keys - encKey, _, err := deriveKeys(sharedSecret) - if err != nil { - return nil, err - } + encKey, _, _ := deriveKeys(shared) - // Serialize routing info - routingData, err := json.Marshal(routingInfos[i]) - if err != nil { - return nil, err - } + // Routing JSON + routeData, _ := json.Marshal(routings[i]) + dbg("Layer %d routing: %s", i, string(routeData)) - // Prepend routing info to payload - combined := append(routingData, payload...) - - // Add length prefix - lengthPrefix := make([]byte, 4) - binary.BigEndian.PutUint32(lengthPrefix, uint32(len(routingData))) - combined = append(lengthPrefix, combined...) + // Combine: [4-byte routing length][routing][payload] + combined := make([]byte, 4+len(routeData)+len(payload)) + binary.BigEndian.PutUint32(combined[:4], uint32(len(routeData))) + copy(combined[4:], routeData) + copy(combined[4+len(routeData):], payload) - // Encrypt with AES-256-GCM - encrypted, err := encryptLayer(combined, encKey) + // Encrypt + encrypted, err := encrypt(combined, encKey) if err != nil { - return nil, err + return nil, fmt.Errorf("encrypt layer %d: %w", i, err) } + dbg("Layer %d: %d -> %d bytes (encrypted)", i, len(combined), len(encrypted)) payload = encrypted - - var ephKey [32]byte - copy(ephKey[:], ephemeralPub) - ephemeralKeys = append([][32]byte{ephKey}, ephemeralKeys...) - sharedSecrets = append([][]byte{sharedSecret}, sharedSecrets...) + + var ek [32]byte + copy(ek[:], ephPub) + ephKeys = append([][32]byte{ek}, ephKeys...) + secrets = append([][]byte{shared}, secrets...) } - // Create header with routing information + // Build header header := &SphinxHeader{ - Version: 1, - EphemeralKey: ephemeralKeys[0], + Version: 2, + EphKey: ephKeys[0], } - // Pack remaining ephemeral keys as raw bytes (not JSON) - // Format: [key1_32bytes][key2_32bytes]... - var routingInfoBytes []byte - for _, key := range ephemeralKeys[1:] { - routingInfoBytes = append(routingInfoBytes, key[:]...) + // Remaining ephemeral keys + for _, k := range ephKeys[1:] { + header.Routing = append(header.Routing, k[:]...) } - // Store remaining ephemeral keys (not encrypted - they're public) - header.RoutingInfo = routingInfoBytes + // HMAC + _, macKey, _ := deriveKeys(secrets[0]) + buf := new(bytes.Buffer) + buf.WriteByte(header.Version) + buf.Write(header.EphKey[:]) + binary.Write(buf, binary.BigEndian, uint32(len(header.Routing))) + buf.Write(header.Routing) + copy(header.MAC[:], computeMAC(buf.Bytes(), macKey)) - // Compute HMAC over header WITHOUT the HMAC field itself - tempBuf := new(bytes.Buffer) - tempBuf.WriteByte(header.Version) - tempBuf.Write(header.EphemeralKey[:]) - lenBytes := make([]byte, 4) - binary.BigEndian.PutUint32(lenBytes, uint32(len(header.RoutingInfo))) - tempBuf.Write(lenBytes) - tempBuf.Write(header.RoutingInfo) - - // Derive MAC key from first shared secret - _, headerKey, err := deriveKeys(sharedSecrets[0]) - if err != nil { - return nil, err - } - - mac := computeHMAC(tempBuf.Bytes(), headerKey) - copy(header.HMAC[:], mac) + dbg("Sphinx packet created: header=%d bytes, payload=%d bytes", + 1+32+4+len(header.Routing)+32, len(payload)) - return &SphinxPacket{ - Header: header, - Payload: payload, - }, nil + return &SphinxPacket{Header: header, Payload: payload}, nil } -func serializeHeader(h *SphinxHeader) ([]byte, error) { +func serializeHeader(h *SphinxHeader) []byte { buf := new(bytes.Buffer) buf.WriteByte(h.Version) - buf.Write(h.EphemeralKey[:]) - - lenBytes := make([]byte, 4) - binary.BigEndian.PutUint32(lenBytes, uint32(len(h.RoutingInfo))) - buf.Write(lenBytes) - buf.Write(h.RoutingInfo) - buf.Write(h.HMAC[:]) - - return buf.Bytes(), nil + buf.Write(h.EphKey[:]) + binary.Write(buf, binary.BigEndian, uint32(len(h.Routing))) + buf.Write(h.Routing) + buf.Write(h.MAC[:]) + return buf.Bytes() } func deserializeHeader(data []byte) (*SphinxHeader, error) { - if len(data) < 1+32+4 { + if len(data) < 37 { return nil, errors.New("header too short") } - - h := &SphinxHeader{ - Version: data[0], - } - - copy(h.EphemeralKey[:], data[1:33]) - - routingLen := binary.BigEndian.Uint32(data[33:37]) - - // Validate routing length is reasonable (max 10KB for routing info) - if routingLen > 10240 { - return nil, errors.New("invalid routing info length: too large") + + h := &SphinxHeader{Version: data[0]} + copy(h.EphKey[:], data[1:33]) + + routeLen := binary.BigEndian.Uint32(data[33:37]) + if routeLen > 10240 { + return nil, errors.New("routing too large") } - - if len(data) < 37+int(routingLen)+32 { - return nil, fmt.Errorf("invalid routing info length: need %d, have %d", 37+int(routingLen)+32, len(data)) + if len(data) < 37+int(routeLen)+32 { + return nil, errors.New("incomplete header") } - - h.RoutingInfo = data[37 : 37+routingLen] - copy(h.HMAC[:], data[37+routingLen:37+routingLen+32]) - + + h.Routing = data[37 : 37+routeLen] + copy(h.MAC[:], data[37+routeLen:37+routeLen+32]) + return h, nil } // ============================================================================ -// SPHINX PACKET PROCESSING (MIXNET NODE) +// SPHINX PROCESSING // ============================================================================ -func processSphinxPacket(packet *SphinxPacket) { - stats.IncrementSphinxReceived() - - // Apply random delay (mixnet timing) - delay := time.Duration(mathrand.Intn(int(MaxBatchDelay-MinBatchDelay))) + MinBatchDelay +func processPacket(pkt *SphinxPacket) { + atomic.AddInt64(&stats.MixRecv, 1) + + dbg("Processing Sphinx packet") + dbg("EphKey: %x", pkt.Header.EphKey[:8]) + + // Random delay + delay := secureDelay() + dbg("Delay: %s", delay) time.Sleep(delay) - - // Compute shared secret with ephemeral key - sharedSecret, err := curve25519.X25519(localNode.PrivateKey, packet.Header.EphemeralKey[:]) - if err != nil { - log.Printf("[SPHINX] Failed to compute shared secret: %v", err) - stats.IncrementFailed() - return - } - - // Derive keys - encKey, macKey, err := deriveKeys(sharedSecret) + + // Derive shared secret + shared, err := curve25519.X25519(local.Private, pkt.Header.EphKey[:]) if err != nil { - log.Printf("[SPHINX] Failed to derive keys: %v", err) - stats.IncrementFailed() + log.Printf("[SPHINX] shared secret failed: %v", err) return } - - log.Printf("[SPHINX] Debug: my_pubkey=%x ephemeral=%x", - localNode.PublicKey[:8], packet.Header.EphemeralKey[:8]) - - // Verify HMAC (only on first hop, subsequent hops have zero HMAC) - isZeroHMAC := true - for _, b := range packet.Header.HMAC { + dbg("Shared secret derived") + + encKey, macKey, _ := deriveKeys(shared) + + // Verify HMAC if present + isZeroMAC := true + for _, b := range pkt.Header.MAC { if b != 0 { - isZeroHMAC = false + isZeroMAC = false break } } - - if !isZeroHMAC { - // Reconstruct header bytes without HMAC for verification - tempBuf := new(bytes.Buffer) - tempBuf.WriteByte(packet.Header.Version) - tempBuf.Write(packet.Header.EphemeralKey[:]) - lenBytes := make([]byte, 4) - binary.BigEndian.PutUint32(lenBytes, uint32(len(packet.Header.RoutingInfo))) - tempBuf.Write(lenBytes) - tempBuf.Write(packet.Header.RoutingInfo) - - if !verifyHMAC(tempBuf.Bytes(), packet.Header.HMAC[:], macKey) { - log.Printf("[SPHINX] HMAC verification failed") - stats.IncrementFailed() + + if !isZeroMAC { + buf := new(bytes.Buffer) + buf.WriteByte(pkt.Header.Version) + buf.Write(pkt.Header.EphKey[:]) + binary.Write(buf, binary.BigEndian, uint32(len(pkt.Header.Routing))) + buf.Write(pkt.Header.Routing) + + if !verifyMAC(buf.Bytes(), pkt.Header.MAC[:], macKey) { + log.Printf("[SPHINX] HMAC verification FAILED") return } + dbg("HMAC verified OK") + } else { + dbg("No HMAC (forwarded packet)") } - - // Decrypt payload layer - plainPayload, err := decryptLayer(packet.Payload, encKey) + + // Decrypt payload + plain, err := decrypt(pkt.Payload, encKey) if err != nil { - log.Printf("[SPHINX] Failed to decrypt payload: %v", err) - stats.IncrementFailed() + log.Printf("[SPHINX] decrypt failed: %v", err) return } - - // Extract routing info length and data - if len(plainPayload) < 4 { - log.Printf("[SPHINX] Payload too short") - stats.IncrementFailed() + dbg("Decrypted payload: %d bytes", len(plain)) + + if len(plain) < 4 { + log.Printf("[SPHINX] payload too short") return } - - routingLen := binary.BigEndian.Uint32(plainPayload[:4]) - if len(plainPayload) < 4+int(routingLen) { - log.Printf("[SPHINX] Invalid routing length") - stats.IncrementFailed() + + routeLen := binary.BigEndian.Uint32(plain[:4]) + if len(plain) < 4+int(routeLen) { + log.Printf("[SPHINX] invalid route length") return } - - routingData := plainPayload[4 : 4+routingLen] - innerPayload := plainPayload[4+routingLen:] - - // Parse routing info + + routeData := plain[4 : 4+routeLen] + innerPayload := plain[4+routeLen:] + var routing RoutingInfo - if err := json.Unmarshal(routingData, &routing); err != nil { - log.Printf("[SPHINX] Failed to parse routing: %v", err) - stats.IncrementFailed() + if err := json.Unmarshal(routeData, &routing); err != nil { + log.Printf("[SPHINX] route parse failed: %v", err) return } - - if routing.NextHop == "EXIT" { - // We are exit node - deliver message + dbg("Routing: next=%s", routing.Next) + + if routing.Next == "EXIT" { log.Printf("[SPHINX] EXIT node - delivering message") - - recipients := strings.Split(routing.MessageTo, ",") - for _, recipient := range recipients { - recipient = strings.TrimSpace(recipient) - if recipient == "" { - continue // Skip empty recipients + dbg("From: %s, To: %s", routing.From, routing.To) + + // UNPAD the payload to get original message + originalMsg, err := unpadPayload(innerPayload) + if err != nil { + log.Printf("[SPHINX] unpad failed: %v", err) + atomic.AddInt64(&stats.Failed, 1) + return + } + dbg("Unpadded message: %d bytes", len(originalMsg)) + + for _, to := range strings.Split(routing.To, ",") { + to = strings.TrimSpace(to) + if to == "" { + continue } - if err := deliverMessage(routing.MessageFrom, recipient, innerPayload); err != nil { - log.Printf("[SPHINX] Delivery failed to %s: %v", recipient, err) - stats.IncrementFailed() + if err := deliver(routing.From, to, originalMsg); err != nil { + log.Printf("[SPHINX] delivery FAILED %s: %v", to, err) + atomic.AddInt64(&stats.Failed, 1) } else { - log.Printf("[SPHINX] SUCCESS: Delivered to %s", recipient) - stats.IncrementRelayed() + log.Printf("[SPHINX] SUCCESS delivered to %s", to) + atomic.AddInt64(&stats.Sent, 1) } } } else { - // Forward to next hop - log.Printf("[SPHINX] Forwarding to next hop: %s", routing.NextHop) - - // Extract remaining ephemeral keys from raw bytes + log.Printf("[SPHINX] Forwarding to %s", routing.Next) + + // Extract remaining ephemeral keys var ephKeys [][32]byte - routingInfo := packet.Header.RoutingInfo - for i := 0; i+32 <= len(routingInfo); i += 32 { - var key [32]byte - copy(key[:], routingInfo[i:i+32]) - ephKeys = append(ephKeys, key) + for i := 0; i+32 <= len(pkt.Header.Routing); i += 32 { + var k [32]byte + copy(k[:], pkt.Header.Routing[i:i+32]) + ephKeys = append(ephKeys, k) } - + dbg("Remaining ephemeral keys: %d", len(ephKeys)) + if len(ephKeys) == 0 { - log.Printf("[SPHINX] No more ephemeral keys - packet routing complete") - stats.IncrementFailed() + log.Printf("[SPHINX] no more ephemeral keys") return } - - // Create new packet for next hop + newHeader := &SphinxHeader{ - Version: 1, - EphemeralKey: ephKeys[0], + Version: 2, + EphKey: ephKeys[0], } - - // Pack remaining keys as raw bytes + if len(ephKeys) > 1 { - var remainingBytes []byte - for _, key := range ephKeys[1:] { - remainingBytes = append(remainingBytes, key[:]...) + for _, k := range ephKeys[1:] { + newHeader.Routing = append(newHeader.Routing, k[:]...) } - newHeader.RoutingInfo = remainingBytes - } else { - newHeader.RoutingInfo = []byte{} } - - // HMAC not used for forwarded packets - copy(newHeader.HMAC[:], make([]byte, 32)) - - newPacket := &SphinxPacket{ - Header: newHeader, - Payload: innerPayload, - } - - if err := sendSphinxPacket(routing.NextHop, newPacket); err != nil { - log.Printf("[SPHINX] Failed to forward: %v", err) - stats.IncrementFailed() + + newPkt := &SphinxPacket{Header: newHeader, Payload: innerPayload} + + if err := sendPacket(routing.Next, newPkt); err != nil { + log.Printf("[SPHINX] forward FAILED: %v", err) } else { - stats.IncrementSphinxForwarded() + log.Printf("[SPHINX] Forwarded to %s", routing.Next) + atomic.AddInt64(&stats.MixFwd, 1) } } } +func sendPacket(addr string, pkt *SphinxPacket) error { + dbg("Connecting to %s via Tor", addr) + + conn, err := torDialer.Dial("tcp", addr) + if err != nil { + return fmt.Errorf("connect: %w", err) + } + defer conn.Close() + + dbg("Connected, sending packet") + + // Send padded header + hdr := serializeHeader(pkt.Header) + padded := make([]byte, HeaderSize) + copy(padded, hdr) + + if _, err := conn.Write(padded); err != nil { + return fmt.Errorf("write header: %w", err) + } + if _, err := conn.Write(pkt.Payload); err != nil { + return fmt.Errorf("write payload: %w", err) + } + + dbg("Packet sent: header=%d, payload=%d", HeaderSize, len(pkt.Payload)) + return nil +} + // ============================================================================ -// SPHINX NODE SERVER +// NODE SERVER (receives Sphinx packets) // ============================================================================ -func StartSphinxNodeServer(addr string) error { - listener, err := net.Listen("tcp", addr) +func startNodeServer(addr string) error { + ln, err := net.Listen("tcp", addr) if err != nil { return err } - log.Printf("[SPHINX] Node server listening on %s", addr) + log.Printf("[NODE] Sphinx server on %s", addr) go func() { - defer shutdownWg.Done() - + defer wg.Done() for { - conn, err := listener.Accept() + conn, err := ln.Accept() if err != nil { select { - case <-shutdownCtx.Done(): + case <-ctx.Done(): return default: - log.Printf("[SPHINX] Accept error: %v", err) continue } } - - go handleSphinxConnection(conn) + go handleNode(conn) } }() return nil } -func handleSphinxConnection(conn net.Conn) { +func handleNode(conn net.Conn) { defer conn.Close() + + remote := conn.RemoteAddr().String() + dbg("Node connection from %s", remote) - // Read packet - headerBytes := make([]byte, SphinxHeaderSize) - if _, err := io.ReadFull(conn, headerBytes); err != nil { - // Ignore EOF - this is typically a health check - if err != io.EOF && err != io.ErrUnexpectedEOF { - log.Printf("[SPHINX] Failed to read header: %v", err) + hdrBuf := make([]byte, HeaderSize) + if _, err := io.ReadFull(conn, hdrBuf); err != nil { + if err != io.EOF { + dbg("Header read failed from %s: %v", remote, err) } return } - header, err := deserializeHeader(headerBytes) + hdr, err := deserializeHeader(hdrBuf) if err != nil { - log.Printf("[SPHINX] Failed to deserialize header: %v", err) + log.Printf("[NODE] header parse failed: %v", err) return } payload, err := io.ReadAll(conn) if err != nil { - log.Printf("[SPHINX] Failed to read payload: %v", err) - return - } - - packet := &SphinxPacket{ - Header: header, - Payload: payload, - } - - // Add to mixnet batch - mixnetBatch.Add(packet) - log.Printf("[SPHINX] Packet received and batched") -} - -// ============================================================================ -// RELAY LOGIC -// ============================================================================ - -func RelayWorker(id int) { - defer shutdownWg.Done() - - log.Printf("[WORKER %d] Started", id) - - for { - select { - case <-shutdownCtx.Done(): - log.Printf("[WORKER %d] Stopped", id) - return - case msg := <-messageQueue: - processMessage(id, msg) - } - } -} - -func processMessage(workerID int, msg *Message) { - delay := calculateDelay() - log.Printf("[WORKER %d] Delay: %s for %s", workerID, delay.Round(time.Millisecond), msg.ID) - - select { - case <-time.After(delay): - case <-shutdownCtx.Done(): + log.Printf("[NODE] payload read failed: %v", err) return } - // Attempt Sphinx routing if enabled and enough nodes - if enableSphinx.Load() && pkiDirectory.HealthyCount() >= MinSphinxHops { - // Apply padding for Sphinx - paddedData := applyPadding(msg.Data) - originalSize := len(msg.Data) - paddedSize := len(paddedData) - log.Printf("[WORKER %d] Padding: %d -> %d bytes", workerID, originalSize, paddedSize) - - if err := sphinxRoute(msg, paddedData); err != nil { - log.Printf("[WORKER %d] Sphinx failed: %v, fallback to direct", workerID, err) - stats.IncrementDirect() - // Use ORIGINAL message data, not Sphinx payload - directRelay(msg, msg.Data) - } else { - stats.IncrementSphinx() - } - } else { - stats.IncrementDirect() - // Direct relay uses original data - directRelay(msg, msg.Data) - } -} - -func sphinxRoute(msg *Message, data []byte) error { - healthy := pkiDirectory.GetHealthyNodes() - if len(healthy) < MinSphinxHops { - return fmt.Errorf("not enough healthy nodes: have %d, need at least %d", - len(healthy), MinSphinxHops) - } - - // Select random hop count - hopCount := selectHopCount(len(healthy)) - - // Shuffle all available nodes for complete randomness - mathrand.Shuffle(len(healthy), func(i, j int) { - healthy[i], healthy[j] = healthy[j], healthy[i] - }) - - // Select first N nodes as route - route := healthy[:hopCount] - - // Log route with variable length - routeLog := "" - for i, node := range route { - if i > 0 { - routeLog += " -> " - } - routeLog += node.NodeID[:8] - } - log.Printf("[SPHINX] Route (%d hops): %s", hopCount, routeLog) - log.Printf("[SPHINX] First hop pubkey: %x", route[0].PublicKey[:8]) - - // Create Sphinx packet - packet, err := createSphinxPacket(msg, route) - if err != nil { - return fmt.Errorf("packet creation failed: %w", err) - } - - // Send to entry node - entryNode := route[0] - if err := sendSphinxPacket(entryNode.Address, packet); err != nil { - return fmt.Errorf("failed to send to entry node: %w", err) - } - - stats.IncrementRelayed() - log.Printf("[SPHINX] SUCCESS: %s via %s (%d hops)", - msg.ID, entryNode.NodeID[:8], hopCount) - return nil -} - -func sendSphinxPacket(address string, packet *SphinxPacket) error { - log.Printf("[SPHINX] Connecting to %s via Tor...", address) - - conn, err := torDialer.Dial("tcp", address) - if err != nil { - log.Printf("[SPHINX] Connection failed to %s: %v", address, err) - return err - } - defer conn.Close() - - log.Printf("[SPHINX] Connected to %s, sending packet...", address) - - // Serialize and send header - headerBytes, err := serializeHeader(packet.Header) - if err != nil { - return err - } - - // Pad header to fixed size - paddedHeader := make([]byte, SphinxHeaderSize) - copy(paddedHeader, headerBytes) - - if _, err := conn.Write(paddedHeader); err != nil { - log.Printf("[SPHINX] Failed to send header to %s: %v", address, err) - return err - } - - // Send payload - if _, err := conn.Write(packet.Payload); err != nil { - log.Printf("[SPHINX] Failed to send payload to %s: %v", address, err) - return err - } - - log.Printf("[SPHINX] Packet sent successfully to %s (%d bytes)", address, len(paddedHeader)+len(packet.Payload)) - return nil -} - -func directRelay(msg *Message, data []byte) { - for _, recipient := range msg.To { - recipient = strings.TrimSpace(recipient) - if recipient == "" { - continue // Skip empty recipients - } - if err := deliverMessage(msg.From, recipient, data); err != nil { - log.Printf("[RELAY] FAILED: %s to %s: %v", msg.ID, recipient, err) - stats.IncrementFailed() - } else { - log.Printf("[RELAY] SUCCESS: %s to %s (%d bytes)", - msg.ID, recipient, len(data)) - stats.IncrementRelayed() - } - } -} - -func deliverMessage(from, to string, data []byte) error { - // Determine server - domain := strings.Split(to, "@") - if len(domain) != 2 { - return fmt.Errorf("invalid email: %s", to) - } - - var server string - if strings.HasSuffix(domain[1], ".onion") { - server = domain[1] + ":25" - } else { - server = domain[1] + ":25" - } - - return sendSMTP(server, from, to, data) -} - -func sendSMTP(server, from, to string, data []byte) error { - conn, err := torDialer.Dial("tcp", server) - if err != nil { - return fmt.Errorf("connection failed: %w", err) - } - defer conn.Close() - - client, err := smtp.NewClient(conn, serverHostname) - if err != nil { - return fmt.Errorf("SMTP client failed: %w", err) - } - defer client.Close() - - if err := client.Hello(serverHostname); err != nil { - return fmt.Errorf("EHLO failed: %w", err) - } - - if err := client.Mail(from); err != nil { - return fmt.Errorf("MAIL FROM failed: %w", err) - } - - if err := client.Rcpt(to); err != nil { - return fmt.Errorf("RCPT TO failed: %w", err) - } - - wc, err := client.Data() - if err != nil { - return fmt.Errorf("DATA failed: %w", err) - } - - if _, err := wc.Write(data); err != nil { - wc.Close() - return fmt.Errorf("write failed: %w", err) - } - - if err := wc.Close(); err != nil { - return fmt.Errorf("close failed: %w", err) - } - - return client.Quit() + dbg("Received packet: header parsed, payload=%d bytes", len(payload)) + batch.Add(&SphinxPacket{Header: hdr, Payload: payload}) + log.Printf("[NODE] Packet received and batched") } // ============================================================================ -// SMTP SERVER +// SMTP SERVER (entry point) // ============================================================================ -func NewSMTPServer(hostname, addr string) *SMTPServer { - return &SMTPServer{ - hostname: hostname, - addr: addr, - } -} - -func (s *SMTPServer) Start() error { - listener, err := net.Listen("tcp", s.addr) +func startSMTP(addr string) error { + ln, err := net.Listen("tcp", addr) if err != nil { return err } - s.listener = listener - log.Printf("[SMTP] Listening on %s", s.addr) + log.Printf("[SMTP] Listening on %s", addr) for { - conn, err := listener.Accept() + conn, err := ln.Accept() if err != nil { select { - case <-shutdownCtx.Done(): + case <-ctx.Done(): return nil default: - log.Printf("[SMTP] Accept error: %v", err) continue } } - - go s.handleConnection(conn) + go handleSMTP(conn) } } -func (s *SMTPServer) handleConnection(conn net.Conn) { +func handleSMTP(conn net.Conn) { defer conn.Close() - - remoteAddr := conn.RemoteAddr().String() - ip := strings.Split(remoteAddr, ":")[0] - - log.Printf("[SMTP] Connection from %s", remoteAddr) - - if !rateLimiter.Allow(ip) { - log.Printf("[SMTP] Rate limit exceeded: %s", ip) - return - } + + remote := conn.RemoteAddr().String() + log.Printf("[SMTP] Connection from %s", remote) reader := bufio.NewReader(conn) writer := bufio.NewWriter(conn) - fmt.Fprintf(writer, "220 %s ESMTP %s %s\r\n", s.hostname, AppName, Version) + fmt.Fprintf(writer, "220 %s fog/%s\r\n", hostname, Version) writer.Flush() - var ( - mailFrom string - rcptTo []string - dataBuffer bytes.Buffer - ) + var from string + var to []string + var data bytes.Buffer for { line, err := reader.ReadString('\n') if err != nil { - if err != io.EOF { - log.Printf("[SMTP] Read error: %v", err) - } return } - line = strings.TrimRight(line, "\r\n") - if line == "" { - continue - } + dbg("SMTP <- %s: %s", remote, line) - cmd := strings.ToUpper(strings.Fields(line)[0]) - log.Printf("[SMTP] <- %s: %s", remoteAddr, line) + parts := strings.SplitN(line, " ", 2) + cmd := strings.ToUpper(parts[0]) switch cmd { case "EHLO", "HELO": - fmt.Fprintf(writer, "250-%s\r\n", s.hostname) - fmt.Fprintf(writer, "250-SIZE %d\r\n", MaxMessageSize) - fmt.Fprintf(writer, "250 8BITMIME\r\n") + fmt.Fprintf(writer, "250-%s\r\n250-SIZE %d\r\n250 8BITMIME\r\n", hostname, MaxMsgSize) + dbg("SMTP -> EHLO response") case "MAIL": if !strings.HasPrefix(strings.ToUpper(line), "MAIL FROM:") { - fmt.Fprintf(writer, "501 Syntax error\r\n") + fmt.Fprintf(writer, "501 Syntax\r\n") writer.Flush() continue } - - parts := strings.SplitN(line, ":", 2) - if len(parts) != 2 { - fmt.Fprintf(writer, "501 Syntax error\r\n") - writer.Flush() - continue - } - - addr := strings.TrimSpace(parts[1]) + addr := strings.TrimSpace(strings.SplitN(line, ":", 2)[1]) if idx := strings.Index(addr, " "); idx != -1 { addr = addr[:idx] } - addr = strings.Trim(addr, "<>") - - mailFrom = addr - rcptTo = nil + from = strings.Trim(addr, "<>") + to = nil fmt.Fprintf(writer, "250 OK\r\n") + dbg("MAIL FROM: %s", from) case "RCPT": - if mailFrom == "" { + if from == "" { fmt.Fprintf(writer, "503 MAIL first\r\n") writer.Flush() continue } - - if len(rcptTo) >= MaxRecipients { - fmt.Fprintf(writer, "452 Too many recipients\r\n") + if len(to) >= MaxRecipient { + fmt.Fprintf(writer, "452 Too many\r\n") writer.Flush() continue } - if !strings.HasPrefix(strings.ToUpper(line), "RCPT TO:") { - fmt.Fprintf(writer, "501 Syntax error\r\n") - writer.Flush() - continue - } - - parts := strings.SplitN(line, ":", 2) - if len(parts) != 2 { - fmt.Fprintf(writer, "501 Syntax error\r\n") + fmt.Fprintf(writer, "501 Syntax\r\n") writer.Flush() continue } - - addr := strings.Trim(strings.TrimSpace(parts[1]), "<>") + addr := strings.Trim(strings.TrimSpace(strings.SplitN(line, ":", 2)[1]), "<>") if addr == "" { - fmt.Fprintf(writer, "501 Invalid recipient address\r\n") + fmt.Fprintf(writer, "501 Invalid\r\n") writer.Flush() continue } - rcptTo = append(rcptTo, addr) + to = append(to, addr) fmt.Fprintf(writer, "250 OK\r\n") + dbg("RCPT TO: %s", addr) case "DATA": - if mailFrom == "" || len(rcptTo) == 0 { + if from == "" || len(to) == 0 { fmt.Fprintf(writer, "503 MAIL/RCPT first\r\n") writer.Flush() continue } - fmt.Fprintf(writer, "354 End data with <CR><LF>.<CR><LF>\r\n") + fmt.Fprintf(writer, "354 End with .\r\n") writer.Flush() - dataBuffer.Reset() - + data.Reset() for { line, err := reader.ReadString('\n') if err != nil { return } - - // Accept both ".\r\n" and ".\n" as end marker + // FIX: Accept both .\r\n and .\n if line == ".\r\n" || line == ".\n" { break } - if strings.HasPrefix(line, "..") { line = line[1:] } - - dataBuffer.WriteString(line) - - if dataBuffer.Len() > MaxMessageSize { - fmt.Fprintf(writer, "552 Message too large\r\n") + data.WriteString(line) + if data.Len() > MaxMsgSize { + fmt.Fprintf(writer, "552 Too large\r\n") writer.Flush() return } } - msgID := generateMessageID() + id := genID() msg := &Message{ - ID: msgID, - From: mailFrom, - To: rcptTo, - Data: dataBuffer.Bytes(), - Timestamp: time.Now(), - Size: dataBuffer.Len(), + ID: id, + From: from, + To: to, + Data: data.Bytes(), + Time: time.Now(), } - if !replayCache.Check(msgID) { - fmt.Fprintf(writer, "550 Duplicate message\r\n") + if !replay.Check(id) { + fmt.Fprintf(writer, "550 Duplicate\r\n") writer.Flush() - return + continue } select { - case messageQueue <- msg: - stats.IncrementReceived() - stats.AddBytes(int64(msg.Size)) - fmt.Fprintf(writer, "250 OK: Message queued as %s\r\n", msgID) - log.Printf("[SMTP] Queued %s from %s to %d recipients (%d bytes)", - msgID, mailFrom, len(rcptTo), msg.Size) + case queue <- msg: + atomic.AddInt64(&stats.Recv, 1) + fmt.Fprintf(writer, "250 OK: %s\r\n", id) + log.Printf("[SMTP] Queued %s from %s to %d rcpt (%d bytes)", + id, from, len(to), data.Len()) default: fmt.Fprintf(writer, "452 Queue full\r\n") } - mailFrom = "" - rcptTo = nil + from = "" + to = nil case "RSET": - mailFrom = "" - rcptTo = nil + from = "" + to = nil fmt.Fprintf(writer, "250 OK\r\n") case "QUIT": @@ -1700,86 +1042,246 @@ func (s *SMTPServer) handleConnection(conn net.Conn) { return default: - fmt.Fprintf(writer, "502 Command not implemented\r\n") + fmt.Fprintf(writer, "502 Unknown\r\n") } writer.Flush() } } -func generateMessageID() string { +func genID() string { b := make([]byte, 16) rand.Read(b) return fmt.Sprintf("%x", b) } // ============================================================================ -// PKI REFRESH WORKER +// DELIVERY // ============================================================================ -func PKIRefreshWorker() { - defer shutdownWg.Done() +func deliver(from, to string, data []byte) error { + parts := strings.Split(to, "@") + if len(parts) != 2 { + return fmt.Errorf("invalid email: %s", to) + } + + server := parts[1] + ":25" + dbg("Delivering to %s via %s", to, server) - ticker := time.NewTicker(PKIRefreshInterval) - defer ticker.Stop() + conn, err := torDialer.Dial("tcp", server) + if err != nil { + return fmt.Errorf("connect: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, hostname) + if err != nil { + return fmt.Errorf("smtp client: %w", err) + } + defer client.Close() + + if err := client.Hello(hostname); err != nil { + return fmt.Errorf("EHLO: %w", err) + } + if err := client.Mail(from); err != nil { + return fmt.Errorf("MAIL: %w", err) + } + if err := client.Rcpt(to); err != nil { + return fmt.Errorf("RCPT: %w", err) + } + + wc, err := client.Data() + if err != nil { + return fmt.Errorf("DATA: %w", err) + } + + if _, err := wc.Write(data); err != nil { + wc.Close() + return fmt.Errorf("write: %w", err) + } + + if err := wc.Close(); err != nil { + return fmt.Errorf("close: %w", err) + } + + dbg("Delivered %d bytes to %s", len(data), to) + return client.Quit() +} + +// ============================================================================ +// WORKERS +// ============================================================================ + +func worker(id int) { + defer wg.Done() + log.Printf("[WORKER %d] Started", id) for { select { - case <-shutdownCtx.Done(): + case <-ctx.Done(): + log.Printf("[WORKER %d] Stopped", id) return - case <-ticker.C: - if pkiURL != "" { - if err := pkiDirectory.LoadFromURL(pkiURL); err != nil { - log.Printf("[PKI] Refresh failed: %v", err) - } else { - log.Printf("[PKI] Refreshed: %d nodes", pkiDirectory.HealthyCount()) - } - } + case msg := <-queue: + processMsg(id, msg) + } + } +} + +func processMsg(wid int, msg *Message) { + delay := secureDelay() + log.Printf("[WORKER %d] Processing %s (delay %s)", wid, msg.ID, delay.Round(time.Millisecond)) + time.Sleep(delay) + + healthy := pki.HealthyCount() + dbg("Healthy nodes: %d (need %d for Sphinx)", healthy, SphinxHops) + + if useSphinx.Load() && healthy >= SphinxHops { + if err := sphinxRoute(msg); err != nil { + log.Printf("[WORKER %d] Sphinx failed: %v, using direct", wid, err) + atomic.AddInt64(&stats.Direct, 1) + directRoute(msg) + } else { + atomic.AddInt64(&stats.Sphinx, 1) + } + } else { + if useSphinx.Load() { + log.Printf("[WORKER %d] Not enough nodes (%d/%d), using direct", wid, healthy, SphinxHops) + } + atomic.AddInt64(&stats.Direct, 1) + directRoute(msg) + } +} + +func sphinxRoute(msg *Message) error { + healthy := pki.GetHealthy() + if len(healthy) < SphinxHops { + return fmt.Errorf("need %d nodes, have %d", SphinxHops, len(healthy)) + } + + // Secure shuffle and select + secureShuffle(healthy) + route := healthy[:SphinxHops] + + log.Printf("[SPHINX] Route: %s -> %s -> %s", route[0].Name, route[1].Name, route[2].Name) + + pkt, err := createPacket(msg, route) + if err != nil { + return fmt.Errorf("create packet: %w", err) + } + + if err := sendPacket(route[0].Address, pkt); err != nil { + return fmt.Errorf("send to entry: %w", err) + } + + atomic.AddInt64(&stats.Sent, 1) + log.Printf("[SPHINX] SUCCESS: %s via %s", msg.ID, route[0].Name) + return nil +} + +func directRoute(msg *Message) { + for _, to := range msg.To { + to = strings.TrimSpace(to) + if to == "" { + continue + } + if err := deliver(msg.From, to, msg.Data); err != nil { + log.Printf("[RELAY] FAILED %s -> %s: %v", msg.ID, to, err) + atomic.AddInt64(&stats.Failed, 1) + } else { + log.Printf("[RELAY] SUCCESS %s -> %s", msg.ID, to) + atomic.AddInt64(&stats.Sent, 1) } } } // ============================================================================ -// HEALTH CHECK WORKER +// HEALTH CHECK - Checks ALL nodes, not just healthy ones // ============================================================================ -func HealthCheckWorker() { - defer shutdownWg.Done() - - ticker := time.NewTicker(HealthCheckInterval) +func healthChecker() { + defer wg.Done() + + // Initial check + time.Sleep(5 * time.Second) + doHealthCheck() + + ticker := time.NewTicker(HealthInterval) defer ticker.Stop() for { select { - case <-shutdownCtx.Done(): + case <-ctx.Done(): return case <-ticker.C: - nodes := pkiDirectory.GetHealthyNodes() - log.Printf("[HEALTH] Checking %d nodes", len(nodes)) - - for _, node := range nodes { - go checkNodeHealth(node) - } + doHealthCheck() } } } -func checkNodeHealth(node *PKINode) { - conn, err := torDialer.Dial("tcp", node.Address) +func doHealthCheck() { + nodes := pki.GetAllOther() + log.Printf("[HEALTH] Checking %d nodes", len(nodes)) + + var wgCheck sync.WaitGroup + for _, n := range nodes { + wgCheck.Add(1) + go func(node *Node) { + defer wgCheck.Done() + checkNode(node) + }(n) + } + wgCheck.Wait() + + log.Printf("[HEALTH] Done. %d nodes healthy", pki.HealthyCount()) +} + +func checkNode(n *Node) { + dbg("Checking %s at %s", n.Name, n.Address) + + conn, err := torDialer.Dial("tcp", n.Address) if err != nil { - pkiDirectory.mu.Lock() - node.Healthy = false - node.FailureCount++ - pkiDirectory.mu.Unlock() + pki.mu.Lock() + n.Healthy = false + pki.mu.Unlock() + log.Printf("[HEALTH] %s DOWN: %v", n.Name, err) return } conn.Close() - pkiDirectory.mu.Lock() - node.Healthy = true - node.LastHealthy = time.Now() - node.SuccessCount++ - pkiDirectory.mu.Unlock() + pki.mu.Lock() + n.Healthy = true + n.LastOK = time.Now() + pki.mu.Unlock() + log.Printf("[HEALTH] %s OK", n.Name) +} + +// ============================================================================ +// STATS +// ============================================================================ + +func statsMonitor() { + defer wg.Done() + ticker := time.NewTicker(StatsInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + up := time.Since(stats.Start).Round(time.Second) + log.Printf("[STATS] Up:%s R:%d S:%d F:%d | Sphinx:%d Direct:%d | Mix R:%d F:%d | Healthy:%d", + up, + atomic.LoadInt64(&stats.Recv), + atomic.LoadInt64(&stats.Sent), + atomic.LoadInt64(&stats.Failed), + atomic.LoadInt64(&stats.Sphinx), + atomic.LoadInt64(&stats.Direct), + atomic.LoadInt64(&stats.MixRecv), + atomic.LoadInt64(&stats.MixFwd), + pki.HealthyCount()) + } + } } // ============================================================================ @@ -1787,231 +1289,152 @@ func checkNodeHealth(node *PKINode) { // ============================================================================ func main() { - // Flags - serverName := flag.String("name", "fog.local", "Server hostname") - smtpAddr := flag.String("addr", "127.0.0.1:2525", "SMTP listen address") - nodeAddrFlag := flag.String("node-addr", "", "Sphinx node address (auto-derived if empty)") - enableSphinxFlag := flag.Bool("sphinx", false, "Enable Sphinx routing") - pkiFileFlag := flag.String("pki-file", "", "PKI file path") - pkiURLFlag := flag.String("pki-url", "", "PKI URL (auto-refresh)") - dataDirFlag := flag.String("data-dir", "./fog-data", "Data directory") - showStats := flag.Bool("stats", false, "Show statistics and exit") - showNodes := flag.Bool("nodes", false, "Show network nodes and exit") - exportNodeInfo := flag.Bool("export-node-info", false, "Export this node's info for nodes.json and exit") - showVersion := flag.Bool("version", false, "Show version and exit") + name := flag.String("name", "fog.local", "Hostname (your .onion address)") + shortName := flag.String("short-name", "", "Short name for logs (e.g. kvara, news, mail)") + smtpAddr := flag.String("smtp", "127.0.0.1:2525", "SMTP listen address") + nodeAddr := flag.String("node", "127.0.0.1:9999", "Node listen address") + sphinx := flag.Bool("sphinx", false, "Enable Sphinx mixnet") + pkiFlag := flag.String("pki-file", "", "Path to nodes.json") + dataFlag := flag.String("data-dir", "./fog-data", "Data directory") + debug := flag.Bool("debug", false, "Enable debug logging") + export := flag.Bool("export-node-info", false, "Export node info for nodes.json") + version := flag.Bool("version", false, "Show version") flag.Parse() - // Version - if *showVersion { - fmt.Printf("%s v%s\n", AppName, Version) - fmt.Println("\nSecurity Features:") - fmt.Println(" ✓ Sphinx with AES-256-GCM encryption") - fmt.Println(" ✓ Variable-hop routing (3-6 hops random)") - fmt.Println(" ✓ Random route selection") - fmt.Println(" ✓ Multi-hop mixnet routing") - fmt.Println(" ✓ Batch processing with shuffling") - fmt.Println(" ✓ Forward secrecy (ECDH)") - fmt.Println(" ✓ HMAC authentication") - fmt.Println(" ✓ Exponential timing delays") - fmt.Println(" ✓ Adaptive padding") - fmt.Println(" ✓ Replay protection") + if *version { + fmt.Printf("fog v%s\n\n", Version) + fmt.Println("Security features:") + fmt.Println(" ✓ Curve25519 ECDH + AES-256-GCM") + fmt.Println(" ✓ 3-hop Sphinx mixnet") + fmt.Println(" ✓ crypto/rand everywhere (no math/rand)") + fmt.Println(" ✓ Traffic analysis resistance (batching)") + fmt.Println(" ✓ Timing attack resistance (random delays)") + fmt.Println(" ✓ Size correlation resistance (fixed payload)") + fmt.Println(" ✓ Replay protection (24h cache)") fmt.Println(" ✓ No metadata retention") - fmt.Println("\nLibraries:") - fmt.Println(" - crypto/aes + crypto/cipher (AES-256-GCM)") - fmt.Println(" - crypto/hmac + crypto/sha256") - fmt.Println(" - golang.org/x/crypto/curve25519 (ECDH)") - fmt.Println(" - golang.org/x/crypto/hkdf (key derivation)") - fmt.Println(" - golang.org/x/net/proxy (Tor SOCKS5)") + fmt.Println("\nNo PKI server needed - uses local nodes.json") os.Exit(0) } - // Setup data directory - dataDir = *dataDirFlag - if err := os.MkdirAll(dataDir, 0700); err != nil { - log.Fatalf("[INIT] Failed to create data directory: %v", err) - } - - statsFile = filepath.Join(dataDir, "stats.json") - - // Show stats - if *showStats { - if data, err := os.ReadFile(statsFile); err == nil { - var s Statistics - if err := json.Unmarshal(data, &s); err == nil { - stats = &s - stats.Print() - os.Exit(0) - } - } - fmt.Println("No statistics available") - os.Exit(1) - } + debugMode = *debug + dataDir = *dataFlag + os.MkdirAll(dataDir, 0700) - // Show nodes - pkiDirectory = NewPKIDirectory() - if *showNodes { - if *pkiFileFlag != "" { - if err := pkiDirectory.LoadFromFile(*pkiFileFlag); err != nil { - log.Fatalf("[PKI] Failed to load: %v", err) - } + if *export { + initNode(*nodeAddr) + + // Use short name if provided, otherwise use hostname + displayName := *shortName + if displayName == "" { + displayName = *name } - - nodes := pkiDirectory.GetHealthyNodes() - fmt.Printf("Healthy nodes: %d\n\n", len(nodes)) - for _, node := range nodes { - fmt.Printf("Node: %s\n", node.NodeID[:32]) - fmt.Printf("Address: %s\n", node.Address) - fmt.Printf("Version: %s\n", node.Version) - fmt.Printf("Last seen: %s\n", node.LastSeen.Format("2006-01-02 15:04:05")) - fmt.Println("---") + + // Create properly formatted nodes.json + nodeInfo := map[string]interface{}{ + "version": Version, + "updated": time.Now().UTC().Format(time.RFC3339), + "nodes": []map[string]string{ + { + "node_id": local.ID, + "public_key": base64.StdEncoding.EncodeToString(local.Public), + "address": *name + ":9999", + "name": displayName, + "version": Version, + }, + }, } - os.Exit(0) - } - - // Export node info for nodes.json - if *exportNodeInfo { - // Derive node address if needed - var nodeAddress string - if *nodeAddrFlag == "" { - host, port, _ := net.SplitHostPort(*smtpAddr) - nodePort := 0 - fmt.Sscanf(port, "%d", &nodePort) - nodeAddress = fmt.Sprintf("%s:%d", host, nodePort+1000) - } else { - nodeAddress = *nodeAddrFlag + + data, _ := json.MarshalIndent(nodeInfo, "", " ") + + // Write to nodes.json + if err := os.WriteFile("nodes.json", data, 0600); err != nil { + log.Fatalf("Failed to write nodes.json: %v", err) } - - // Initialize node to generate keys - InitializeLocalNode(nodeAddress) - - // Format as JSON for nodes.json - pubKeyB64 := base64.StdEncoding.EncodeToString(localNode.PublicKey) - fmt.Println("{") - fmt.Printf(" \"%s\": {\n", localNode.NodeID) - fmt.Printf(" \"node_id\": \"%s\",\n", localNode.NodeID) - fmt.Printf(" \"public_key\": \"%s\",\n", pubKeyB64) - fmt.Printf(" \"address\": \"%s:9999\",\n", *serverName) - fmt.Printf(" \"version\": \"%s\",\n", Version) - fmt.Printf(" \"last_seen\": \"%s\",\n", time.Now().UTC().Format(time.RFC3339)) - fmt.Printf(" \"healthy\": true,\n") - fmt.Printf(" \"failure_count\": 0,\n") - fmt.Printf(" \"success_count\": 0,\n") - fmt.Printf(" \"last_healthy\": \"%s\"\n", time.Now().UTC().Format(time.RFC3339)) - fmt.Println(" }") - fmt.Println("}") - fmt.Println() - fmt.Println("Copy this JSON block into your nodes.json file.") - fmt.Printf("NodeID (short): %s\n", localNode.NodeID[:16]) + fmt.Println(string(data)) + log.Printf("Written to nodes.json") os.Exit(0) } - // Normal startup log.Printf("[FOG] Starting v%s", Version) - - shutdownCtx, shutdownCancel = context.WithCancel(context.Background()) - defer shutdownCancel() - - serverHostname = *serverName - log.Printf("[FOG] Hostname: %s", serverHostname) - - // Initialize components - stats = NewStatistics() - replayCache = NewReplayCache(ReplayCacheSize) - rateLimiter = NewRateLimiter() - messageQueue = make(chan *Message, MessageQueueSize) - mixnetBatch = NewMixnetBatch() - - // Derive node address if needed - if *nodeAddrFlag == "" { - host, port, _ := net.SplitHostPort(*smtpAddr) - nodePort := 0 - fmt.Sscanf(port, "%d", &nodePort) - nodeAddr = fmt.Sprintf("%s:%d", host, nodePort+1000) - } else { - nodeAddr = *nodeAddrFlag + if debugMode { + log.Printf("[FOG] Debug mode ENABLED") } - InitializeLocalNode(nodeAddr) - InitializeTor() + ctx, cancel = context.WithCancel(context.Background()) + defer cancel() - enableSphinx.Store(*enableSphinxFlag) - pkiURL = *pkiURLFlag + hostname = *name + pkiFile = *pkiFlag - if *enableSphinxFlag { - log.Printf("[INIT] Sphinx ENABLED with AES-256-GCM") - log.Printf("[INIT] Variable-hop routing: %d-%d hops (random)", - MinSphinxHops, MaxSphinxHops) + // Init + stats = &Stats{Start: time.Now()} + replay = newReplayCache() + batch = newBatch() + queue = make(chan *Message, QueueSize) + pki = newPKI() - if *pkiFileFlag != "" { - pkiDirectory.LoadFromFile(*pkiFileFlag) - } + initNode(*nodeAddr) + + // Tor + dialer, err := proxy.SOCKS5("tcp", TorSocks, nil, proxy.Direct) + if err != nil { + log.Fatalf("[TOR] Failed: %v", err) + } + torDialer = dialer + log.Printf("[TOR] Connected to %s", TorSocks) - if pkiURL != "" { - pkiDirectory.LoadFromURL(pkiURL) - shutdownWg.Add(1) - go PKIRefreshWorker() + useSphinx.Store(*sphinx) + + if *sphinx { + log.Printf("[FOG] Sphinx mode ENABLED") + + if pkiFile == "" { + log.Printf("[FOG] WARNING: No -pki-file specified, Sphinx will use direct relay") + } else { + if err := pki.Load(pkiFile); err != nil { + log.Printf("[PKI] Load failed: %v", err) + } } - shutdownWg.Add(1) - go HealthCheckWorker() + wg.Add(1) + go healthChecker() - // Start Sphinx node server - shutdownWg.Add(1) - if err := StartSphinxNodeServer(nodeAddr); err != nil { - log.Fatalf("[SPHINX] Failed to start node server: %v", err) + wg.Add(1) + if err := startNodeServer(*nodeAddr); err != nil { + log.Fatalf("[NODE] Failed: %v", err) } - // Start mixnet batch worker - shutdownWg.Add(1) - go MixnetBatchWorker() + wg.Add(1) + go batchWorker() } else { - log.Printf("[INIT] Sphinx DISABLED (direct relay only)") + log.Printf("[FOG] Direct relay mode (no Sphinx)") } - // Start workers - for i := 0; i < WorkerCount; i++ { - shutdownWg.Add(1) - go RelayWorker(i) + // Workers + for i := 0; i < Workers; i++ { + wg.Add(1) + go worker(i) } - shutdownWg.Add(1) - go StatsMonitor() - - // Key rotation every 30 days - shutdownWg.Add(1) - go RotatePKIKeys() - - // Start SMTP server - server := NewSMTPServer(*serverName, *smtpAddr) + wg.Add(1) + go statsMonitor() - // Handle signals - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + // Signals + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) go func() { - <-sigChan + <-sig log.Printf("[FOG] Shutdown signal received") - shutdownCancel() - if server.listener != nil { - server.listener.Close() - } + cancel() }() - if err := server.Start(); err != nil { - log.Fatalf("[FOG] Server error: %v", err) - } - - shutdownWg.Wait() - - // Save final state - if *pkiFileFlag != "" { - pkiDirectory.SaveToFile(*pkiFileFlag) - } - if statsFile != "" { - stats.SaveToFile(statsFile) + if err := startSMTP(*smtpAddr); err != nil { + log.Fatalf("[SMTP] Failed: %v", err) } + wg.Wait() log.Printf("[FOG] Shutdown complete") } |
