diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2025-11-04 12:42:19 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-04 12:42:19 +0100 |
| commit | ec800b66a16947b265b91fa8bbadfd16b302f4bc (patch) | |
| tree | 014baa56bf53b946220d6a3662394b74ed8cff2f | |
| parent | 824b4b6d5485168ff8571075736fa1167debbabd (diff) | |
| download | fog-ec800b66a16947b265b91fa8bbadfd16b302f4bc.tar.gz fog-ec800b66a16947b265b91fa8bbadfd16b302f4bc.tar.xz fog-ec800b66a16947b265b91fa8bbadfd16b302f4bc.zip | |
Update print statement from 'Hello' to 'Goodbye'
| -rw-r--r-- | fog.go | 2661 |
1 files changed, 1495 insertions, 1166 deletions
@@ -1,11 +1,19 @@ +// fog v1.3.3-minimal - Anonymous SMTP Relay +// Fixed Sphinx encryption with AES-256-GCM + Full mixnetwork +// Copyright 2025 - fog Project + package main import ( + "bufio" "bytes" + "context" "crypto/aes" "crypto/cipher" + "crypto/hmac" "crypto/rand" "crypto/sha256" + "encoding/binary" "encoding/base64" "encoding/json" "errors" @@ -14,14 +22,13 @@ import ( "io" "log" "math" - "math/big" mathrand "math/rand" "net" "net/http" - "net/textproto" + "net/smtp" "os" "os/signal" - "regexp" + "path/filepath" "strings" "sync" "sync/atomic" @@ -38,149 +45,270 @@ import ( // ============================================================================ const ( - Version = "1.2.0" - AppName = "fog" - TorSocksProxyAddr = "127.0.0.1:9050" - RelayWorkerCount = 5 - DeliveryTimeout = 60 * time.Second - MessageIDCacheDuration = 24 * time.Hour - RateLimitPerIP = 10 - RateLimitWindow = 1 * time.Minute - MaxRetries = 3 - MaxMessageSize = 10 * 1024 * 1024 // 10MB - MaxRecipients = 100 - - // Sphinx and PKI constants - SphinxHopCount = 3 - SphinxHeaderSize = 1024 - SphinxPayloadSize = 32 * 1024 - PKIRotationInterval = 3 * time.Hour - KeyRotationJitter = 30 * time.Minute - PKIRefreshInterval = 15 * time.Minute - NodeHealthCheckTimeout = 5 * time.Second - - // Adaptive padding constants - PaddingAdaptiveWindow = 1 * time.Hour - PaddingMinBucket = 32 * 1024 - PaddingMaxBucket = 10 * 1024 * 1024 - PaddingBucketCount = 9 - - // Exponential delay constants - ExponentialDelayLambda = 1.0 - ExponentialDelayMin = 100 * time.Millisecond - ExponentialDelayMax = 10 * time.Second - - // Sphinx node server - SphinxNodePort = 9999 + 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 ) -var MessageSizeBuckets = []int64{ - 32 * 1024, - 64 * 1024, - 128 * 1024, - 256 * 1024, - 512 * 1024, - 1024 * 1024, - 2 * 1024 * 1024, - 5 * 1024 * 1024, - 10 * 1024 * 1024, +// Padding bucket sizes +var PaddingSizes = [PaddingBuckets]int{ + 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, } // ============================================================================ -// GLOBAL VARIABLES +// TYPES // ============================================================================ -var ( - emailRegExp *regexp.Regexp - localPartRegex *regexp.Regexp - domainRegex *regexp.Regexp +type Message struct { + ID string + From string + To []string + Data []byte + Timestamp time.Time + Size int +} - mailQueue chan *Envelope - mailQueueMutex sync.Mutex +type LocalNode struct { + NodeID string + PrivateKey []byte + PublicKey []byte + Address string + Version string + CreatedAt time.Time + mu sync.RWMutex +} - stats *Statistics - shutdownSignal chan struct{} - shutdownWg sync.WaitGroup +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"` +} - // Tor dialer - torDialer proxy.Dialer +type PKIDirectory struct { + Nodes map[string]*PKINode `json:"nodes"` + UpdatedAt time.Time `json:"updated_at"` + mu sync.RWMutex +} - // Sphinx and PKI - localNode *PKINode +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 ReplayCache struct { + cache map[string]time.Time + mu sync.RWMutex +} + +type RateLimiter struct { + connections map[string][]time.Time + mu sync.RWMutex +} + +// 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 +} + +type SphinxPacket struct { + Header *SphinxHeader + Payload []byte // AES-256-GCM encrypted payload +} + +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 +} + +// ============================================================================ +// GLOBAL STATE +// ============================================================================ + +var ( + // Core components + localNode *LocalNode pkiDirectory *PKIDirectory - sphinxEnabled atomic.Bool - pkiInitialized atomic.Bool + stats *Statistics + replayCache *ReplayCache + rateLimiter *RateLimiter + messageQueue chan *Message + serverHostname string - // Adaptive padding - paddingStats *PaddingStatistics - paddingStatsWindow []int64 - paddingStatsMux sync.RWMutex + // Mixnet + mixnetBatch *MixnetBatch - // Exponential delay generator - delayGenerator *ExponentialDelayGenerator + // Tor connectivity + torDialer proxy.Dialer - // Message ID cache (replay protection) - messageIDCache = make(map[string]time.Time) - messageIDCacheMux sync.RWMutex - messageIDCacheTTL = MessageIDCacheDuration + // Shutdown + shutdownCtx context.Context + shutdownCancel context.CancelFunc + shutdownWg sync.WaitGroup - // Rate limiting - rateLimitMap = make(map[string]*RateLimiter) - rateLimitMapMux sync.RWMutex + // Configuration + enableSphinx atomic.Bool + pkiURL string + dataDir string + statsFile string + nodeAddr string ) // ============================================================================ -// DATA STRUCTURES +// MIXNET BATCH // ============================================================================ -type Envelope struct { - From string - To []string - MessageData *bytes.Buffer - MessageID string - ReceivedAt time.Time - Size int64 - RetryCount int +func NewMixnetBatch() *MixnetBatch { + return &MixnetBatch{ + packets: make([]*SphinxPacket, 0, BatchSize), + startTime: time.Now(), + } } -type Statistics struct { - ConnectionsTotal atomic.Uint64 - ConnectionsActive atomic.Int64 - MessagesReceived atomic.Uint64 - MessagesDelivered atomic.Uint64 - MessagesFailed atomic.Uint64 - BytesTransferred atomic.Uint64 - StartTime time.Time - SphinxRoutingUsed atomic.Uint64 - DirectRelayUsed atomic.Uint64 +func (b *MixnetBatch) Add(packet *SphinxPacket) { + b.mu.Lock() + defer b.mu.Unlock() + b.packets = append(b.packets, packet) } -type RateLimiter struct { - count int - lastReset time.Time - mu sync.Mutex +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 } -// ============================================================================ -// PKI STRUCTURES -// ============================================================================ - -type PKINode struct { - NodeID string `json:"node_id"` - PublicKey []byte `json:"public_key"` - Address string `json:"address"` // fog-node.onion:9999 - LastRotated time.Time `json:"last_rotated"` - IsLocal bool `json:"is_local"` - LastSeen time.Time `json:"last_seen"` - Healthy bool `json:"healthy"` +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 } -type PKIDirectory struct { - Nodes map[string]*PKINode `json:"nodes"` - UpdatedAt time.Time `json:"updated_at"` - mu sync.RWMutex +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) + } + } + } + } } +// ============================================================================ +// PKI DIRECTORY +// ============================================================================ + func NewPKIDirectory() *PKIDirectory { return &PKIDirectory{ Nodes: make(map[string]*PKINode), @@ -188,79 +316,75 @@ func NewPKIDirectory() *PKIDirectory { } } -func (d *PKIDirectory) AddNode(node *PKINode) { - d.mu.Lock() - defer d.mu.Unlock() - d.Nodes[node.NodeID] = node - d.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 (d *PKIDirectory) RemoveNode(nodeID string) { - d.mu.Lock() - defer d.mu.Unlock() - delete(d.Nodes, nodeID) - d.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 (d *PKIDirectory) GetNode(nodeID string) (*PKINode, bool) { - d.mu.RLock() - defer d.mu.RUnlock() - node, exists := d.Nodes[nodeID] - return node, exists -} +func (p *PKIDirectory) GetHealthyNodes() []*PKINode { + p.mu.RLock() + defer p.mu.RUnlock() -func (d *PKIDirectory) GetHealthyNodes(excludeLocal bool) []*PKINode { - d.mu.RLock() - defer d.mu.RUnlock() - - var nodes []*PKINode - for _, node := range d.Nodes { - if excludeLocal && node.IsLocal { + var healthy []*PKINode + for _, node := range p.Nodes { + // Skip unhealthy nodes + if !node.Healthy { continue } - if node.Healthy { - nodes = append(nodes, node) + // Skip self only if localNode is initialized + if localNode != nil && node.NodeID == localNode.NodeID { + continue } + healthy = append(healthy, node) } - return nodes + return healthy } -func (d *PKIDirectory) LoadFromFile(filename string) error { - data, err := os.ReadFile(filename) +func (p *PKIDirectory) HealthyCount() int { + return len(p.GetHealthyNodes()) +} + +func (p *PKIDirectory) LoadFromFile(path string) error { + data, err := os.ReadFile(path) if err != nil { - return fmt.Errorf("failed to read PKI file: %w", err) + return err } - d.mu.Lock() - defer d.mu.Unlock() + p.mu.Lock() + defer p.mu.Unlock() - if err := json.Unmarshal(data, d); err != nil { - return fmt.Errorf("failed to parse PKI JSON: %w", err) + if err := json.Unmarshal(data, p); err != nil { + return err } - log.Printf("[PKI] Loaded %d nodes from %s", len(d.Nodes), filename) + log.Printf("[PKI] Loaded %d nodes from file", len(p.Nodes)) return nil } -func (d *PKIDirectory) SaveToFile(filename string) error { - d.mu.RLock() - data, err := json.MarshalIndent(d, "", " ") - d.mu.RUnlock() +func (p *PKIDirectory) SaveToFile(path string) error { + p.mu.RLock() + data, err := json.MarshalIndent(p, "", " ") + p.mu.RUnlock() if err != nil { - return fmt.Errorf("failed to marshal PKI: %w", err) - } - - if err := os.WriteFile(filename, data, 0600); err != nil { - return fmt.Errorf("failed to write PKI file: %w", err) + return err } - return nil + return os.WriteFile(path, data, 0600) } -func (d *PKIDirectory) LoadFromURL(url string) error { +func (p *PKIDirectory) LoadFromURL(url string) error { client := &http.Client{ - Timeout: 10 * time.Second, + Timeout: 30 * time.Second, Transport: &http.Transport{ Dial: torDialer.Dial, }, @@ -268,1404 +392,1608 @@ func (d *PKIDirectory) LoadFromURL(url string) error { resp, err := client.Get(url) if err != nil { - return fmt.Errorf("failed to fetch PKI from URL: %w", err) + return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("PKI URL returned status %d", resp.StatusCode) + return fmt.Errorf("HTTP %d", resp.StatusCode) } data, err := io.ReadAll(resp.Body) if err != nil { - return fmt.Errorf("failed to read PKI response: %w", err) + return err } - d.mu.Lock() - defer d.mu.Unlock() + p.mu.Lock() + defer p.mu.Unlock() - if err := json.Unmarshal(data, d); err != nil { - return fmt.Errorf("failed to parse PKI JSON: %w", err) + if err := json.Unmarshal(data, p); err != nil { + return err } - log.Printf("[PKI] Loaded %d nodes from URL", len(d.Nodes)) + log.Printf("[PKI] Loaded %d nodes from URL", len(p.Nodes)) return nil } -func (d *PKIDirectory) Count() int { - d.mu.RLock() - defer d.mu.RUnlock() - return len(d.Nodes) -} +// ============================================================================ +// LOCAL NODE +// ============================================================================ -func (d *PKIDirectory) HealthyCount() int { - d.mu.RLock() - defer d.mu.RUnlock() +func InitializeLocalNode(address string) { + keyFile := filepath.Join(dataDir, "node.key") + + var privateKey []byte - count := 0 - for _, node := range d.Nodes { - if node.Healthy && !node.IsLocal { - count++ + // 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) } } - return count -} -// ============================================================================ -// SPHINX STRUCTURES -// ============================================================================ + publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint) + if err != nil { + log.Fatalf("[NODE] Failed to generate public key: %v", err) + } -type SphinxPacket struct { - Header []byte - Payload []byte + nodeID := generateNodeID(publicKey) + + localNode = &LocalNode{ + NodeID: nodeID, + PrivateKey: privateKey, + PublicKey: publicKey, + Address: address, + Version: Version, + CreatedAt: time.Now(), + } + + log.Printf("[NODE] Initialized: %s", nodeID[:16]) + log.Printf("[NODE] Address: %s", address) + log.Printf("[NODE] Public key: %x", publicKey[:16]) } -type SphinxHeader struct { - EphemeralKey []byte // 32 bytes Curve25519 public key - RoutingInfo []byte // Encrypted routing information - MAC []byte // HMAC for integrity +func generateNodeID(publicKey []byte) string { + hash := sha256.Sum256(publicKey) + return fmt.Sprintf("%x", hash[:]) } -type SphinxHopInfo struct { - NextHop string - NextAddress string +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() + } + } } // ============================================================================ -// PADDING STATISTICS +// STATISTICS // ============================================================================ -type PaddingStatistics struct { - buckets [PaddingBucketCount]int64 - totalMessages int64 - mu sync.RWMutex - lastUpdate time.Time -} - -func NewPaddingStatistics() *PaddingStatistics { - return &PaddingStatistics{ - lastUpdate: time.Now(), +func NewStatistics() *Statistics { + return &Statistics{ + StartTime: time.Now(), } } -func (ps *PaddingStatistics) RecordMessage(size int64) { - ps.mu.Lock() - defer ps.mu.Unlock() +func (s *Statistics) IncrementReceived() { + atomic.AddInt64(&s.MessagesReceived, 1) +} - bucketIndex := ps.findBucket(size) - ps.buckets[bucketIndex]++ - ps.totalMessages++ +func (s *Statistics) IncrementRelayed() { + atomic.AddInt64(&s.MessagesRelayed, 1) } -func (ps *PaddingStatistics) findBucket(size int64) int { - for i, threshold := range MessageSizeBuckets { - if size <= threshold { - return i - } - } - return PaddingBucketCount - 1 +func (s *Statistics) IncrementFailed() { + atomic.AddInt64(&s.MessagesFailed, 1) } -func (ps *PaddingStatistics) GetAdaptiveBuckets() []int64 { - ps.mu.RLock() - defer ps.mu.RUnlock() +func (s *Statistics) AddBytes(n int64) { + atomic.AddInt64(&s.BytesProcessed, n) +} - if ps.totalMessages < 100 { - return MessageSizeBuckets - } +func (s *Statistics) IncrementSphinx() { + atomic.AddInt64(&s.SphinxRouted, 1) +} - // Calculate percentiles based on actual traffic - adaptiveBuckets := make([]int64, PaddingBucketCount) - copy(adaptiveBuckets, MessageSizeBuckets) +func (s *Statistics) IncrementDirect() { + atomic.AddInt64(&s.DirectRouted, 1) +} - return adaptiveBuckets +func (s *Statistics) IncrementSphinxReceived() { + atomic.AddInt64(&s.SphinxReceived, 1) } -// ============================================================================ -// EXPONENTIAL DELAY GENERATOR -// ============================================================================ +func (s *Statistics) IncrementSphinxForwarded() { + atomic.AddInt64(&s.SphinxForwarded, 1) +} -type ExponentialDelayGenerator struct { - lambda float64 - min time.Duration - max time.Duration - mu sync.Mutex +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 NewExponentialDelayGenerator(lambda float64, min, max time.Duration) *ExponentialDelayGenerator { - return &ExponentialDelayGenerator{ - lambda: lambda, - min: min, - max: max, +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 (g *ExponentialDelayGenerator) Generate() time.Duration { - g.mu.Lock() - defer g.mu.Unlock() +func (s *Statistics) SaveToFile(path string) error { + s.mu.RLock() + defer s.mu.RUnlock() - u, err := rand.Int(rand.Reader, big.NewInt(1<<32)) + data, err := json.MarshalIndent(s, "", " ") if err != nil { - return g.min + return err } - uFloat := float64(u.Int64()) / float64(1<<32) - delay := -math.Log(1-uFloat) / g.lambda + return os.WriteFile(path, data, 0600) +} - delayDuration := time.Duration(delay * float64(time.Second)) +func StatsMonitor() { + defer shutdownWg.Done() - if delayDuration < g.min { - return g.min - } - if delayDuration > g.max { - return g.max - } + ticker := time.NewTicker(StatsInterval) + defer ticker.Stop() - return delayDuration + 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) + } + } + } } // ============================================================================ -// PKI INITIALIZATION +// REPLAY CACHE // ============================================================================ -func InitializePKI() error { - privateKey := make([]byte, 32) - if _, err := rand.Read(privateKey); err != nil { - return fmt.Errorf("failed to generate private key: %w", err) - } - - publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint) - if err != nil { - return fmt.Errorf("failed to derive public key: %w", err) +func NewReplayCache(size int) *ReplayCache { + return &ReplayCache{ + cache: make(map[string]time.Time, size), } +} - nodeID := generateNodeID(publicKey) +func (r *ReplayCache) Check(msgID string) bool { + r.mu.Lock() + defer r.mu.Unlock() - localNode = &PKINode{ - NodeID: nodeID, - PublicKey: publicKey, - Address: "", - LastRotated: time.Now(), - IsLocal: true, - Healthy: true, - LastSeen: time.Now(), + if _, exists := r.cache[msgID]; exists { + return false } - pkiDirectory = NewPKIDirectory() - pkiDirectory.AddNode(localNode) - - pkiInitialized.Store(true) - - log.Printf("[PKI] Initialized local node: %s", nodeID[:16]) - log.Printf("[PKI] Public key: %s", base64.StdEncoding.EncodeToString(publicKey)[:32]) + r.cache[msgID] = time.Now() - return nil -} + if len(r.cache) > ReplayCacheSize { + now := time.Now() + for id, t := range r.cache { + if now.Sub(t) > ReplayCacheTTL { + delete(r.cache, id) + } + } + } -func generateNodeID(publicKey []byte) string { - hash := sha256.Sum256(publicKey) - return base64.URLEncoding.EncodeToString(hash[:]) + return true } // ============================================================================ -// SPHINX PACKET CREATION +// RATE LIMITER // ============================================================================ -func CreateSphinxPacket(envelope *Envelope, route []*PKINode) (*SphinxPacket, error) { - if len(route) != SphinxHopCount { - return nil, fmt.Errorf("route must have exactly %d hops", SphinxHopCount) +func NewRateLimiter() *RateLimiter { + return &RateLimiter{ + connections: make(map[string][]time.Time), } +} - // Serialize envelope - payload, err := serializeEnvelope(envelope) - if err != nil { - return nil, fmt.Errorf("failed to serialize envelope: %w", err) - } +func (r *RateLimiter) Allow(ip string) bool { + r.mu.Lock() + defer r.mu.Unlock() - // Pad payload - if len(payload) > SphinxPayloadSize { - return nil, fmt.Errorf("payload too large: %d > %d", len(payload), SphinxPayloadSize) - } + now := time.Now() + cutoff := now.Add(-RateLimitWindow) - paddedPayload := make([]byte, SphinxPayloadSize) - copy(paddedPayload, payload) - if _, err := rand.Read(paddedPayload[len(payload):]); err != nil { - return nil, fmt.Errorf("failed to pad payload: %w", err) + times := r.connections[ip] + var recent []time.Time + for _, t := range times { + if t.After(cutoff) { + recent = append(recent, t) + } } - // Create layers (from exit to entry) - encryptedPayload := paddedPayload - var header []byte + if len(recent) >= RateLimitMessages { + return false + } - for i := len(route) - 1; i >= 0; i-- { - node := route[i] + recent = append(recent, now) + r.connections[ip] = recent + return true +} - // Generate ephemeral key pair - ephemeralPrivate := make([]byte, 32) - if _, err := rand.Read(ephemeralPrivate); err != nil { - return nil, fmt.Errorf("failed to generate ephemeral key: %w", err) - } +// ============================================================================ +// TOR INITIALIZATION +// ============================================================================ - ephemeralPublic, err := curve25519.X25519(ephemeralPrivate, curve25519.Basepoint) - if err != nil { - return nil, fmt.Errorf("failed to derive ephemeral public: %w", err) - } +func InitializeTor() { + dialer, err := proxy.SOCKS5("tcp", TorSocksProxyAddr, nil, proxy.Direct) + if err != nil { + log.Fatalf("[TOR] Failed to create SOCKS5 dialer: %v", err) + } - // ECDH with node's public key - sharedSecret, err := curve25519.X25519(ephemeralPrivate, node.PublicKey) - if err != nil { - return nil, fmt.Errorf("ECDH failed: %w", err) - } + torDialer = dialer + log.Printf("[TOR] Initialized: %s", TorSocksProxyAddr) +} - // Derive encryption keys using HKDF - keys := deriveSphinxKeys(sharedSecret, ephemeralPublic) +// ============================================================================ +// PADDING +// ============================================================================ - // Create hop info - var hopInfo SphinxHopInfo - if i < len(route)-1 { - hopInfo.NextHop = route[i+1].NodeID - hopInfo.NextAddress = route[i+1].Address - } else { - // Exit node - include final destination - hopInfo.NextHop = "EXIT" - hopInfo.NextAddress = "" +func calculatePadding(size int) int { + for _, bucket := range PaddingSizes { + if size <= bucket { + return bucket - size } + } + return 0 +} - hopInfoBytes, _ := json.Marshal(hopInfo) +func applyPadding(data []byte) []byte { + currentSize := len(data) + paddingNeeded := calculatePadding(currentSize) - // Encrypt payload - encryptedPayload, err = encryptAESGCM(keys.payloadKey, encryptedPayload) - if err != nil { - return nil, fmt.Errorf("payload encryption failed: %w", err) - } + if paddingNeeded == 0 { + return data + } - // Encrypt header - headerData := append(ephemeralPublic, hopInfoBytes...) - encryptedHeader, err := encryptAESGCM(keys.headerKey, headerData) - if err != nil { - return nil, fmt.Errorf("header encryption failed: %w", err) - } + padded := make([]byte, currentSize+paddingNeeded) + copy(padded, data) - header = encryptedHeader + if _, err := rand.Read(padded[currentSize:]); err != nil { + log.Printf("[PADDING] Warning: failed to generate random padding: %v", err) } - // Pad header to fixed size - if len(header) > SphinxHeaderSize { - return nil, fmt.Errorf("header too large: %d > %d", len(header), SphinxHeaderSize) - } + return padded +} - paddedHeader := make([]byte, SphinxHeaderSize) - copy(paddedHeader, header) +// ============================================================================ +// TIMING +// ============================================================================ - return &SphinxPacket{ - Header: paddedHeader, - Payload: encryptedPayload, - }, nil -} +func calculateDelay() time.Duration { + u := mathrand.Float64() + delay := time.Duration(-math.Log(1-u) / PoissonLambda * float64(time.Second)) -type sphinxKeys struct { - headerKey []byte - payloadKey []byte -} + if delay < MinDelay { + delay = MinDelay + } + if delay > MaxDelay { + delay = MaxDelay + } -func deriveSphinxKeys(sharedSecret, ephemeralPublic []byte) *sphinxKeys { - salt := ephemeralPublic - info := []byte("fog-sphinx-v1") + return delay +} - kdf := hkdf.New(sha256.New, sharedSecret, salt, info) +// ============================================================================ +// SPHINX CRYPTOGRAPHY (AES-256-GCM) +// ============================================================================ - keys := &sphinxKeys{ - headerKey: make([]byte, 32), - payloadKey: make([]byte, 32), +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 + } + + // 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 +} - kdf.Read(keys.headerKey) - kdf.Read(keys.payloadKey) - - return keys +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 encryptAESGCM(key, plaintext []byte) ([]byte, error) { +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 decryptAESGCM(key, ciphertext []byte) ([]byte, error) { +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[:nonceSize] - ciphertext = ciphertext[nonceSize:] - - return gcm.Open(nil, nonce, ciphertext, nil) -} - -func serializeEnvelope(envelope *Envelope) ([]byte, error) { - data := map[string]interface{}{ - "from": envelope.From, - "to": envelope.To, - "message_id": envelope.MessageID, - "size": envelope.Size, - "data": base64.StdEncoding.EncodeToString(envelope.MessageData.Bytes()), - } - - return json.Marshal(data) -} - -func deserializeEnvelope(data []byte) (*Envelope, error) { - var parsed map[string]interface{} - if err := json.Unmarshal(data, &parsed); err != nil { - return nil, err - } - - messageData, err := base64.StdEncoding.DecodeString(parsed["data"].(string)) + + 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 +} - toList := []string{} - for _, t := range parsed["to"].([]interface{}) { - toList = append(toList, t.(string)) - } +func computeHMAC(data, key []byte) []byte { + h := hmac.New(sha256.New, key) + h.Write(data) + return h.Sum(nil) +} - return &Envelope{ - From: parsed["from"].(string), - To: toList, - MessageID: parsed["message_id"].(string), - Size: int64(parsed["size"].(float64)), - MessageData: bytes.NewBuffer(messageData), - ReceivedAt: time.Now(), - }, nil +func verifyHMAC(data, mac, key []byte) bool { + expected := computeHMAC(data, key) + return hmac.Equal(mac, expected) } // ============================================================================ -// SPHINX NODE SERVER +// SPHINX PACKET CREATION // ============================================================================ -func StartSphinxNodeServer(listenAddr string) error { - listener, err := net.Listen("tcp", listenAddr) - if err != nil { - return fmt.Errorf("failed to start Sphinx node server: %w", err) +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) } - log.Printf("[SPHINX] Node server listening on %s", listenAddr) - - go func() { - defer shutdownWg.Done() - defer listener.Close() - - for { - select { - case <-shutdownSignal: - return - default: - } - - listener.(*net.TCPListener).SetDeadline(time.Now().Add(1 * time.Second)) - - conn, err := listener.Accept() - if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - continue - } - log.Printf("[SPHINX] Accept error: %v", err) - continue - } - - go handleSphinxConnection(conn) + // 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, } - }() - - return nil -} - -func handleSphinxConnection(conn net.Conn) { - defer conn.Close() - - log.Printf("[SPHINX] Connection from %s", conn.RemoteAddr()) - - // Read packet - var packet SphinxPacket - decoder := json.NewDecoder(conn) - if err := decoder.Decode(&packet); err != nil { - log.Printf("[SPHINX] Failed to decode packet: %v", err) - return } - - // Process packet - nextHop, nextPacket, envelope, err := ProcessSphinxPacket(&packet, localNode) - if err != nil { - log.Printf("[SPHINX] Packet processing failed: %v", err) - return + + // Exit node: final destination + // Filter out empty recipients + validRecipients := []string{} + for _, to := range message.To { + to = strings.TrimSpace(to) + if to != "" { + validRecipients = append(validRecipients, to) + } + } + + if len(validRecipients) == 0 { + return nil, fmt.Errorf("no valid recipients") + } + + routingInfos[hopCount-1] = RoutingInfo{ + NextHop: "EXIT", + MessageFrom: message.From, + MessageTo: strings.Join(validRecipients, ","), } - // Apply exponential delay - delay := delayGenerator.Generate() - log.Printf("[SPHINX] Applying delay: %v", delay) - time.Sleep(delay) + // Start with message data + payload := message.Data - if nextHop == "EXIT" { - // This is the exit node - relay message - log.Printf("[SPHINX] Exit node - relaying message") - if err := directRelay(envelope); err != nil { - log.Printf("[SPHINX] Direct relay failed: %v", err) + // Encrypt in reverse order (exit -> entry) + var ephemeralKeys [][32]byte + var sharedSecrets [][]byte + + for i := len(route) - 1; i >= 0; i-- { + node := route[i] + + // Generate ephemeral key pair for this hop + ephemeralPriv := make([]byte, 32) + if _, err := rand.Read(ephemeralPriv); err != nil { + return nil, err } - } else { - // Forward to next hop - log.Printf("[SPHINX] Forwarding to next hop: %s", nextHop[:16]) - if err := forwardSphinxPacket(nextPacket, nextHop); err != nil { - log.Printf("[SPHINX] Forward failed: %v", err) + + ephemeralPub, err := curve25519.X25519(ephemeralPriv, curve25519.Basepoint) + if err != nil { + return nil, err } - } -} -func ProcessSphinxPacket(packet *SphinxPacket, node *PKINode) (string, *SphinxPacket, *Envelope, error) { - // This is a simplified version - in production you'd need the private key - // For now, we'll use a mock processing + // Compute shared secret + sharedSecret, err := curve25519.X25519(ephemeralPriv, node.PublicKey) + if err != nil { + return nil, err + } - // Decrypt header layer - headerData, err := decryptAESGCM(node.PublicKey, packet.Header) - if err != nil { - return "", nil, nil, fmt.Errorf("header decryption failed: %w", err) - } + // Derive encryption and MAC keys + encKey, _, err := deriveKeys(sharedSecret) + if err != nil { + return nil, err + } - // Parse hop info - var hopInfo SphinxHopInfo - if err := json.Unmarshal(headerData[32:], &hopInfo); err != nil { - return "", nil, nil, fmt.Errorf("hop info parsing failed: %w", err) - } + // Serialize routing info + routingData, err := json.Marshal(routingInfos[i]) + if err != nil { + return nil, err + } - // Decrypt payload layer - decryptedPayload, err := decryptAESGCM(node.PublicKey, packet.Payload) - if err != nil { - return "", nil, nil, fmt.Errorf("payload decryption failed: %w", err) - } + // 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...) - // If this is exit node, deserialize envelope - var envelope *Envelope - if hopInfo.NextHop == "EXIT" { - envelope, err = deserializeEnvelope(decryptedPayload) + // Encrypt with AES-256-GCM + encrypted, err := encryptLayer(combined, encKey) if err != nil { - return "", nil, nil, fmt.Errorf("envelope deserialization failed: %w", err) + return nil, err } - } - // Create next packet - nextPacket := &SphinxPacket{ - Header: headerData, - Payload: decryptedPayload, + payload = encrypted + + var ephKey [32]byte + copy(ephKey[:], ephemeralPub) + ephemeralKeys = append([][32]byte{ephKey}, ephemeralKeys...) + sharedSecrets = append([][]byte{sharedSecret}, sharedSecrets...) } - return hopInfo.NextHop, nextPacket, envelope, nil -} - -func forwardSphinxPacket(packet *SphinxPacket, nextHopID string) error { - node, exists := pkiDirectory.GetNode(nextHopID) - if !exists { - return fmt.Errorf("next hop not found in PKI: %s", nextHopID) + // Create header with routing information + header := &SphinxHeader{ + Version: 1, + EphemeralKey: ephemeralKeys[0], } - conn, err := torDialer.Dial("tcp", node.Address) - if err != nil { - return fmt.Errorf("failed to connect to next hop: %w", err) + // 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[:]...) } - defer conn.Close() - encoder := json.NewEncoder(conn) - if err := encoder.Encode(packet); err != nil { - return fmt.Errorf("failed to send packet: %w", err) + // Store remaining ephemeral keys (not encrypted - they're public) + header.RoutingInfo = routingInfoBytes + + // 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) - log.Printf("[SPHINX] Forwarded packet to %s", node.Address) - return nil + return &SphinxPacket{ + Header: header, + Payload: payload, + }, nil } -// ============================================================================ -// ROUTE SELECTION -// ============================================================================ - -func SelectSphinxRoute() ([]*PKINode, error) { - healthyNodes := pkiDirectory.GetHealthyNodes(true) +func serializeHeader(h *SphinxHeader) ([]byte, error) { + 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 +} - if len(healthyNodes) < SphinxHopCount { - return nil, fmt.Errorf("not enough healthy nodes: have %d, need %d", len(healthyNodes), SphinxHopCount) +func deserializeHeader(data []byte) (*SphinxHeader, error) { + if len(data) < 1+32+4 { + return nil, errors.New("header too short") } - - // Shuffle nodes - for i := len(healthyNodes) - 1; i > 0; i-- { - j, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1))) - healthyNodes[i], healthyNodes[j.Int64()] = healthyNodes[j.Int64()], healthyNodes[i] + + h := &SphinxHeader{ + Version: data[0], } - - return healthyNodes[:SphinxHopCount], nil + + 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") + } + + 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)) + } + + h.RoutingInfo = data[37 : 37+routingLen] + copy(h.HMAC[:], data[37+routingLen:37+routingLen+32]) + + return h, nil } // ============================================================================ -// SMTP SERVER +// SPHINX PACKET PROCESSING (MIXNET NODE) // ============================================================================ -type SMTPServer struct { - name string - addr string - listener net.Listener -} - -func NewSMTPServer(name, addr string) *SMTPServer { - return &SMTPServer{ - name: name, - addr: addr, +func processSphinxPacket(packet *SphinxPacket) { + stats.IncrementSphinxReceived() + + // Apply random delay (mixnet timing) + delay := time.Duration(mathrand.Intn(int(MaxBatchDelay-MinBatchDelay))) + MinBatchDelay + 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 } -} - -func (s *SMTPServer) Start() error { - listener, err := net.Listen("tcp", s.addr) + + // Derive keys + encKey, macKey, err := deriveKeys(sharedSecret) if err != nil { - return fmt.Errorf("failed to listen on %s: %w", s.addr, err) + log.Printf("[SPHINX] Failed to derive keys: %v", err) + stats.IncrementFailed() + return } - - s.listener = listener - log.Printf("[SMTP] Server listening on %s", s.addr) - - for { - select { - case <-shutdownSignal: - return nil - default: + + 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 { + if b != 0 { + isZeroHMAC = false + break } - - listener.(*net.TCPListener).SetDeadline(time.Now().Add(1 * time.Second)) - - conn, err := listener.Accept() - if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - continue - } - return fmt.Errorf("accept error: %w", err) + } + + 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() + return } - - stats.ConnectionsTotal.Add(1) - stats.ConnectionsActive.Add(1) - - go func() { - defer stats.ConnectionsActive.Add(-1) - s.handleConnection(conn) - }() } -} - -func (s *SMTPServer) handleConnection(conn net.Conn) { - defer conn.Close() - - remoteAddr := conn.RemoteAddr().String() - log.Printf("[SMTP] Connection from %s", remoteAddr) - - if !checkRateLimit(remoteAddr) { - log.Printf("[SMTP] Rate limit exceeded for %s", remoteAddr) + + // Decrypt payload layer + plainPayload, err := decryptLayer(packet.Payload, encKey) + if err != nil { + log.Printf("[SPHINX] Failed to decrypt payload: %v", err) + stats.IncrementFailed() return } - - tc := textproto.NewConn(conn) - defer tc.Close() - - tc.PrintfLine("220 %s fog v%s ESMTP", s.name, Version) - - var mailFrom string - var rcptTo []string - var messageData bytes.Buffer - - for { - line, err := tc.ReadLine() - if err != nil { - if err != io.EOF { - log.Printf("[SMTP] Read error: %v", err) + + // Extract routing info length and data + if len(plainPayload) < 4 { + log.Printf("[SPHINX] Payload too short") + stats.IncrementFailed() + return + } + + routingLen := binary.BigEndian.Uint32(plainPayload[:4]) + if len(plainPayload) < 4+int(routingLen) { + log.Printf("[SPHINX] Invalid routing length") + stats.IncrementFailed() + return + } + + routingData := plainPayload[4 : 4+routingLen] + innerPayload := plainPayload[4+routingLen:] + + // Parse routing info + var routing RoutingInfo + if err := json.Unmarshal(routingData, &routing); err != nil { + log.Printf("[SPHINX] Failed to parse routing: %v", err) + stats.IncrementFailed() + return + } + + if routing.NextHop == "EXIT" { + // We are exit node - deliver message + 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 } + if err := deliverMessage(routing.MessageFrom, recipient, innerPayload); err != nil { + log.Printf("[SPHINX] Delivery failed to %s: %v", recipient, err) + stats.IncrementFailed() + } else { + log.Printf("[SPHINX] SUCCESS: Delivered to %s", recipient) + stats.IncrementRelayed() + } + } + } else { + // Forward to next hop + log.Printf("[SPHINX] Forwarding to next hop: %s", routing.NextHop) + + // Extract remaining ephemeral keys from raw bytes + 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) + } + + if len(ephKeys) == 0 { + log.Printf("[SPHINX] No more ephemeral keys - packet routing complete") + stats.IncrementFailed() return } - - log.Printf("[SMTP] <- %s: %s", remoteAddr, line) - - cmd := strings.ToUpper(strings.Fields(line)[0]) - - switch cmd { - case "EHLO", "HELO": - tc.PrintfLine("250-%s", s.name) - tc.PrintfLine("250-8BITMIME") - tc.PrintfLine("250-SIZE %d", MaxMessageSize) - tc.PrintfLine("250 HELP") - - case "MAIL": - if err := s.handleMAIL(line, &mailFrom, tc); err != nil { - log.Printf("[SMTP] MAIL error: %v", err) - return - } - - case "RCPT": - if err := s.handleRCPT(line, &rcptTo, tc); err != nil { - log.Printf("[SMTP] RCPT error: %v", err) - return - } - - case "DATA": - if err := s.handleDATA(tc, &messageData, mailFrom, rcptTo); err != nil { - log.Printf("[SMTP] DATA error: %v", err) - return + + // Create new packet for next hop + newHeader := &SphinxHeader{ + Version: 1, + EphemeralKey: ephKeys[0], + } + + // Pack remaining keys as raw bytes + if len(ephKeys) > 1 { + var remainingBytes []byte + for _, key := range ephKeys[1:] { + remainingBytes = append(remainingBytes, key[:]...) } - mailFrom = "" - rcptTo = nil - messageData.Reset() - - case "RSET": - mailFrom = "" - rcptTo = nil - messageData.Reset() - tc.PrintfLine("250 OK") - - case "NOOP": - tc.PrintfLine("250 OK") - - case "QUIT": - tc.PrintfLine("221 Bye") - return - - default: - tc.PrintfLine("502 Command not implemented") + 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() + } else { + stats.IncrementSphinxForwarded() } } } -func (s *SMTPServer) handleMAIL(line string, mailFrom *string, tc *textproto.Conn) error { - parts := strings.Fields(line) - if len(parts) < 2 { - tc.PrintfLine("501 Syntax error") - return errors.New("invalid MAIL command") - } - - // Extract FROM: address (ignore BODY= and other ESMTP parameters) - fromPart := parts[1] - if !strings.HasPrefix(strings.ToUpper(fromPart), "FROM:") { - tc.PrintfLine("501 Syntax error in MAIL command") - return errors.New("missing FROM:") - } - - from := strings.TrimPrefix(fromPart, "FROM:") - from = strings.TrimPrefix(from, "from:") - from = strings.Trim(from, "<>") - - if !ValidateEmailAddress(from) { - tc.PrintfLine("553 Invalid sender address") - return errors.New("invalid sender") - } - - *mailFrom = from - tc.PrintfLine("250 OK") - return nil -} +// ============================================================================ +// SPHINX NODE SERVER +// ============================================================================ -func (s *SMTPServer) handleRCPT(line string, rcptTo *[]string, tc *textproto.Conn) error { - parts := strings.Fields(line) - if len(parts) != 2 { - tc.PrintfLine("501 Syntax error") - return errors.New("invalid RCPT command") +func StartSphinxNodeServer(addr string) error { + listener, err := net.Listen("tcp", addr) + if err != nil { + return err } - to := strings.TrimPrefix(parts[1], "TO:") - to = strings.TrimPrefix(to, "to:") - to = strings.Trim(to, "<>") + log.Printf("[SPHINX] Node server listening on %s", addr) - if !ValidateEmailAddress(to) { - tc.PrintfLine("553 Invalid recipient address") - return errors.New("invalid recipient") - } + go func() { + defer shutdownWg.Done() + + for { + conn, err := listener.Accept() + if err != nil { + select { + case <-shutdownCtx.Done(): + return + default: + log.Printf("[SPHINX] Accept error: %v", err) + continue + } + } - if len(*rcptTo) >= MaxRecipients { - tc.PrintfLine("452 Too many recipients") - return errors.New("too many recipients") - } + go handleSphinxConnection(conn) + } + }() - *rcptTo = append(*rcptTo, to) - tc.PrintfLine("250 OK") return nil } -func (s *SMTPServer) handleDATA(tc *textproto.Conn, messageData *bytes.Buffer, mailFrom string, rcptTo []string) error { - if mailFrom == "" || len(rcptTo) == 0 { - tc.PrintfLine("503 Bad sequence of commands") - return errors.New("MAIL/RCPT required before DATA") - } - - tc.PrintfLine("354 End data with <CR><LF>.<CR><LF>") - - messageData.Reset() - dotReader := tc.DotReader() - written, err := io.Copy(messageData, dotReader) - if err != nil { - tc.PrintfLine("554 Transaction failed") - return fmt.Errorf("failed to read message: %w", err) - } +func handleSphinxConnection(conn net.Conn) { + defer conn.Close() - if written > MaxMessageSize { - tc.PrintfLine("552 Message too large") - return errors.New("message too large") + // 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) + } + return } - messageID := generateMessageID() - - if isDuplicateMessage(messageID) { - tc.PrintfLine("250 OK (duplicate)") - log.Printf("[SMTP] Duplicate message %s - ignoring", messageID[:16]) - return nil + header, err := deserializeHeader(headerBytes) + if err != nil { + log.Printf("[SPHINX] Failed to deserialize header: %v", err) + return } - envelope := &Envelope{ - From: mailFrom, - To: rcptTo, - MessageData: bytes.NewBuffer(messageData.Bytes()), - MessageID: messageID, - ReceivedAt: time.Now(), - Size: written, - RetryCount: 0, + payload, err := io.ReadAll(conn) + if err != nil { + log.Printf("[SPHINX] Failed to read payload: %v", err) + return } - select { - case mailQueue <- envelope: - stats.MessagesReceived.Add(1) - tc.PrintfLine("250 OK Message queued %s", messageID[:16]) - log.Printf("[SMTP] Queued message %s from %s to %v (%d bytes)", - messageID[:16], mailFrom, rcptTo, written) - default: - tc.PrintfLine("452 Queue full") - return errors.New("queue full") + packet := &SphinxPacket{ + Header: header, + Payload: payload, } - return nil + // Add to mixnet batch + mixnetBatch.Add(packet) + log.Printf("[SPHINX] Packet received and batched") } // ============================================================================ // RELAY LOGIC // ============================================================================ -func relayWorker(id int) { +func RelayWorker(id int) { defer shutdownWg.Done() - log.Printf("[RELAY] Worker %d started", id) + + log.Printf("[WORKER %d] Started", id) for { select { - case <-shutdownSignal: + case <-shutdownCtx.Done(): + log.Printf("[WORKER %d] Stopped", id) return - case envelope := <-mailQueue: - if err := processEnvelope(envelope); err != nil { - log.Printf("[RELAY] Worker %d failed: %v", id, err) - stats.MessagesFailed.Add(1) - } else { - stats.MessagesDelivered.Add(1) - } + case msg := <-messageQueue: + processMessage(id, msg) } } } -func processEnvelope(envelope *Envelope) error { - // Record message size for adaptive padding - paddingStats.RecordMessage(envelope.Size) +func processMessage(workerID int, msg *Message) { + delay := calculateDelay() + log.Printf("[WORKER %d] Delay: %s for %s", workerID, delay.Round(time.Millisecond), msg.ID) - // Check if Sphinx routing is available - if sphinxEnabled.Load() && pkiDirectory.HealthyCount() >= SphinxHopCount { - log.Printf("[RELAY] Attempting Sphinx routing for %s", envelope.MessageID[:16]) - if err := sphinxRelay(envelope); err != nil { - log.Printf("[RELAY] Sphinx routing failed, falling back to direct: %v", err) - return directRelay(envelope) - } - stats.SphinxRoutingUsed.Add(1) - return nil + select { + case <-time.After(delay): + case <-shutdownCtx.Done(): + return } - // Fallback to direct relay - log.Printf("[RELAY] Direct relay for %s (Sphinx unavailable)", envelope.MessageID[:16]) - stats.DirectRelayUsed.Add(1) - return directRelay(envelope) + // 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 sphinxRelay(envelope *Envelope) error { - // Select route - route, err := SelectSphinxRoute() - if err != nil { - return fmt.Errorf("route selection failed: %w", err) +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) } - log.Printf("[SPHINX] Selected route: %s -> %s -> %s", - route[0].NodeID[:8], route[1].NodeID[:8], route[2].NodeID[:8]) + // 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(envelope, route) + packet, err := createSphinxPacket(msg, route) if err != nil { return fmt.Errorf("packet creation failed: %w", err) } - // Apply exponential delay - delay := delayGenerator.Generate() - log.Printf("[SPHINX] Applying delay: %v", delay) - time.Sleep(delay) - // Send to entry node entryNode := route[0] - conn, err := torDialer.Dial("tcp", entryNode.Address) + 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 { - return fmt.Errorf("failed to connect to entry node: %w", err) + log.Printf("[SPHINX] Connection failed to %s: %v", address, err) + return err } defer conn.Close() + + log.Printf("[SPHINX] Connected to %s, sending packet...", address) - encoder := json.NewEncoder(conn) - if err := encoder.Encode(packet); err != nil { - return fmt.Errorf("failed to send packet: %w", err) + // Serialize and send header + headerBytes, err := serializeHeader(packet.Header) + if err != nil { + return err } - - log.Printf("[SPHINX] Packet sent to entry node %s", entryNode.NodeID[:16]) + + // 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(envelope *Envelope) error { - for _, recipient := range envelope.To { - if err := deliverMessage(envelope, recipient); err != nil { - log.Printf("[RELAY] Delivery to %s failed: %v", recipient, err) - if envelope.RetryCount < MaxRetries { - envelope.RetryCount++ - time.AfterFunc(time.Duration(envelope.RetryCount)*30*time.Second, func() { - mailQueue <- envelope - }) - } - return err +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() } } - return nil } -func deliverMessage(envelope *Envelope, recipient string) error { - // Apply exponential delay for timing obfuscation - delay := delayGenerator.Generate() - log.Printf("[RELAY] Applying delay %v before delivery to %s", delay, recipient) - time.Sleep(delay) - - domain := strings.Split(recipient, "@")[1] - - var smtpHost string - var smtpPort string +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) + } - if strings.HasSuffix(domain, ".onion") { - smtpHost = domain - smtpPort = "25" + var server string + if strings.HasSuffix(domain[1], ".onion") { + server = domain[1] + ":25" } else { - mxRecords, err := net.LookupMX(domain) - if err != nil || len(mxRecords) == 0 { - smtpHost = domain - } else { - smtpHost = strings.TrimSuffix(mxRecords[0].Host, ".") - } - smtpPort = "25" + server = domain[1] + ":25" } - target := net.JoinHostPort(smtpHost, smtpPort) - log.Printf("[RELAY] Connecting to %s", target) + return sendSMTP(server, from, to, data) +} - conn, err := torDialer.Dial("tcp", target) +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() - // Manual SMTP handshake to control EHLO hostname - tc := textproto.NewConn(conn) - defer tc.Close() - - // Read greeting - _, _, err = tc.ReadResponse(220) + client, err := smtp.NewClient(conn, serverHostname) if err != nil { - return fmt.Errorf("failed to read greeting: %w", err) + return fmt.Errorf("SMTP client failed: %w", err) } + defer client.Close() - // Send EHLO with our .onion address - localNodeName := localNode.Address - if localNodeName == "" || localNodeName == "127.0.0.1:9999" { - // Fallback if node address not properly configured - localNodeName = "fog.onion" - } - // Extract just the hostname part (remove :port) - if strings.Contains(localNodeName, ":") { - localNodeName = strings.Split(localNodeName, ":")[0] + if err := client.Hello(serverHostname); err != nil { + return fmt.Errorf("EHLO failed: %w", err) } - tc.PrintfLine("EHLO %s", localNodeName) - _, _, err = tc.ReadResponse(250) - if err != nil { - // Try HELO if EHLO fails - tc.PrintfLine("HELO %s", localNodeName) - _, _, err = tc.ReadResponse(250) - if err != nil { - return fmt.Errorf("EHLO/HELO failed: %w", err) - } - } - - log.Printf("[RELAY] Identified as %s to %s", localNodeName, smtpHost) - - // MAIL FROM - tc.PrintfLine("MAIL FROM:<%s>", envelope.From) - _, _, err = tc.ReadResponse(250) - if err != nil { + if err := client.Mail(from); err != nil { return fmt.Errorf("MAIL FROM failed: %w", err) } - // RCPT TO - tc.PrintfLine("RCPT TO:<%s>", recipient) - _, _, err = tc.ReadResponse(250) - if err != nil { + if err := client.Rcpt(to); err != nil { return fmt.Errorf("RCPT TO failed: %w", err) } - // DATA - tc.PrintfLine("DATA") - _, _, err = tc.ReadResponse(354) + wc, err := client.Data() if err != nil { - return fmt.Errorf("DATA command failed: %w", err) + return fmt.Errorf("DATA failed: %w", err) } - // Write message (NO PADDING to preserve message integrity) - // Padding would corrupt UTF-8 content for Usenet gateways - dw := tc.DotWriter() - if _, err := dw.Write(envelope.MessageData.Bytes()); err != nil { - return fmt.Errorf("message transfer failed: %w", err) - } - if err := dw.Close(); err != nil { - return fmt.Errorf("message completion failed: %w", err) + if _, err := wc.Write(data); err != nil { + wc.Close() + return fmt.Errorf("write failed: %w", err) } - // Read final response - _, _, err = tc.ReadResponse(250) - if err != nil { - return fmt.Errorf("message not accepted: %w", err) + if err := wc.Close(); err != nil { + return fmt.Errorf("close failed: %w", err) } - // QUIT - tc.PrintfLine("QUIT") - tc.ReadResponse(221) + return client.Quit() +} - stats.BytesTransferred.Add(uint64(envelope.MessageData.Len())) - log.Printf("[RELAY] SUCCESS: Delivered %s to %s (%d bytes, no padding)", - envelope.MessageID[:16], recipient, envelope.Size) +// ============================================================================ +// SMTP SERVER +// ============================================================================ - return nil +func NewSMTPServer(hostname, addr string) *SMTPServer { + return &SMTPServer{ + hostname: hostname, + addr: addr, + } } -func applyAdaptivePadding(data []byte, originalSize int64) []byte { - buckets := paddingStats.GetAdaptiveBuckets() - - targetSize := buckets[len(buckets)-1] - for _, bucket := range buckets { - if originalSize <= bucket { - targetSize = bucket - break - } +func (s *SMTPServer) Start() error { + listener, err := net.Listen("tcp", s.addr) + if err != nil { + return err } - if int64(len(data)) >= targetSize { - return data - } + s.listener = listener + log.Printf("[SMTP] Listening on %s", s.addr) - padded := make([]byte, targetSize) - copy(padded, data) - - rand.Read(padded[len(data):]) + for { + conn, err := listener.Accept() + if err != nil { + select { + case <-shutdownCtx.Done(): + return nil + default: + log.Printf("[SMTP] Accept error: %v", err) + continue + } + } - return padded + go s.handleConnection(conn) + } } -// ============================================================================ -// UTILITY FUNCTIONS -// ============================================================================ - -func ValidateEmailAddress(email string) bool { - if len(email) > 254 || !strings.Contains(email, "@") { - return false - } +func (s *SMTPServer) handleConnection(conn net.Conn) { + defer conn.Close() - parts := strings.Split(email, "@") - if len(parts) != 2 { - return false - } + remoteAddr := conn.RemoteAddr().String() + ip := strings.Split(remoteAddr, ":")[0] - localPart, domain := parts[0], parts[1] + log.Printf("[SMTP] Connection from %s", remoteAddr) - if len(localPart) == 0 || len(localPart) > 64 || len(domain) == 0 || len(domain) > 253 { - return false + if !rateLimiter.Allow(ip) { + log.Printf("[SMTP] Rate limit exceeded: %s", ip) + return } - if !localPartRegex.MatchString(localPart) || !domainRegex.MatchString(domain) { - return false - } + reader := bufio.NewReader(conn) + writer := bufio.NewWriter(conn) - return true -} + fmt.Fprintf(writer, "220 %s ESMTP %s %s\r\n", s.hostname, AppName, Version) + writer.Flush() -func generateMessageID() string { - b := make([]byte, 16) - rand.Read(b) - return base64.URLEncoding.EncodeToString(b) -} + var ( + mailFrom string + rcptTo []string + dataBuffer bytes.Buffer + ) -func isDuplicateMessage(messageID string) bool { - messageIDCacheMux.Lock() - defer messageIDCacheMux.Unlock() + for { + line, err := reader.ReadString('\n') + if err != nil { + if err != io.EOF { + log.Printf("[SMTP] Read error: %v", err) + } + return + } - // Clean old entries - now := time.Now() - for id, timestamp := range messageIDCache { - if now.Sub(timestamp) > messageIDCacheTTL { - delete(messageIDCache, id) + line = strings.TrimRight(line, "\r\n") + if line == "" { + continue } - } - if _, exists := messageIDCache[messageID]; exists { - return true - } + cmd := strings.ToUpper(strings.Fields(line)[0]) + log.Printf("[SMTP] <- %s: %s", remoteAddr, line) - messageIDCache[messageID] = now - return false -} + 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") -func checkRateLimit(ip string) bool { - rateLimitMapMux.Lock() - defer rateLimitMapMux.Unlock() + case "MAIL": + if !strings.HasPrefix(strings.ToUpper(line), "MAIL FROM:") { + fmt.Fprintf(writer, "501 Syntax error\r\n") + writer.Flush() + continue + } - limiter, exists := rateLimitMap[ip] - if !exists { - limiter = &RateLimiter{ - count: 1, - lastReset: time.Now(), - } - rateLimitMap[ip] = limiter - return true - } + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + fmt.Fprintf(writer, "501 Syntax error\r\n") + writer.Flush() + continue + } - limiter.mu.Lock() - defer limiter.mu.Unlock() + addr := strings.TrimSpace(parts[1]) + if idx := strings.Index(addr, " "); idx != -1 { + addr = addr[:idx] + } + addr = strings.Trim(addr, "<>") - if time.Since(limiter.lastReset) > RateLimitWindow { - limiter.count = 1 - limiter.lastReset = time.Now() - return true - } + mailFrom = addr + rcptTo = nil + fmt.Fprintf(writer, "250 OK\r\n") - if limiter.count >= RateLimitPerIP { - return false - } + case "RCPT": + if mailFrom == "" { + fmt.Fprintf(writer, "503 MAIL first\r\n") + writer.Flush() + continue + } - limiter.count++ - return true -} + if len(rcptTo) >= MaxRecipients { + fmt.Fprintf(writer, "452 Too many recipients\r\n") + writer.Flush() + continue + } -// ============================================================================ -// BACKGROUND WORKERS -// ============================================================================ + if !strings.HasPrefix(strings.ToUpper(line), "RCPT TO:") { + fmt.Fprintf(writer, "501 Syntax error\r\n") + writer.Flush() + continue + } -func RotatePKIKeys() { - defer shutdownWg.Done() + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + fmt.Fprintf(writer, "501 Syntax error\r\n") + writer.Flush() + continue + } - jitter := time.Duration(mathrand.Int63n(int64(KeyRotationJitter))) - ticker := time.NewTicker(PKIRotationInterval + jitter) - defer ticker.Stop() + addr := strings.Trim(strings.TrimSpace(parts[1]), "<>") + if addr == "" { + fmt.Fprintf(writer, "501 Invalid recipient address\r\n") + writer.Flush() + continue + } + rcptTo = append(rcptTo, addr) + fmt.Fprintf(writer, "250 OK\r\n") - for { - select { - case <-shutdownSignal: - return - case <-ticker.C: - log.Printf("[PKI] Rotating keys") - if err := InitializePKI(); err != nil { - log.Printf("[PKI] Key rotation failed: %v", err) + case "DATA": + if mailFrom == "" || len(rcptTo) == 0 { + fmt.Fprintf(writer, "503 MAIL/RCPT first\r\n") + writer.Flush() + continue } - } - } -} -func PKIRefreshWorker(pkiURL string) { - defer shutdownWg.Done() + fmt.Fprintf(writer, "354 End data with <CR><LF>.<CR><LF>\r\n") + writer.Flush() - ticker := time.NewTicker(PKIRefreshInterval) - defer ticker.Stop() + dataBuffer.Reset() - for { - select { - case <-shutdownSignal: - return - case <-ticker.C: - log.Printf("[PKI] Refreshing directory from URL") - if err := pkiDirectory.LoadFromURL(pkiURL); err != nil { - log.Printf("[PKI] Refresh failed: %v", err) - } else { - log.Printf("[PKI] Directory refreshed: %d nodes (%d healthy)", - pkiDirectory.Count(), pkiDirectory.HealthyCount()) + for { + line, err := reader.ReadString('\n') + if err != nil { + return + } + + // Accept both ".\r\n" and ".\n" as end marker + 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") + writer.Flush() + return + } } - } - } -} -func PaddingUpdateWorker() { - defer shutdownWg.Done() + msgID := generateMessageID() + msg := &Message{ + ID: msgID, + From: mailFrom, + To: rcptTo, + Data: dataBuffer.Bytes(), + Timestamp: time.Now(), + Size: dataBuffer.Len(), + } - ticker := time.NewTicker(PaddingAdaptiveWindow) - defer ticker.Stop() + if !replayCache.Check(msgID) { + fmt.Fprintf(writer, "550 Duplicate message\r\n") + writer.Flush() + return + } - for { - select { - case <-shutdownSignal: + 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) + default: + fmt.Fprintf(writer, "452 Queue full\r\n") + } + + mailFrom = "" + rcptTo = nil + + case "RSET": + mailFrom = "" + rcptTo = nil + fmt.Fprintf(writer, "250 OK\r\n") + + case "QUIT": + fmt.Fprintf(writer, "221 Bye\r\n") + writer.Flush() return - case <-ticker.C: - buckets := paddingStats.GetAdaptiveBuckets() - log.Printf("[PADDING] Updated adaptive buckets: %v", buckets) + + default: + fmt.Fprintf(writer, "502 Command not implemented\r\n") } + + writer.Flush() } } -func statsMonitor() { +func generateMessageID() string { + b := make([]byte, 16) + rand.Read(b) + return fmt.Sprintf("%x", b) +} + +// ============================================================================ +// PKI REFRESH WORKER +// ============================================================================ + +func PKIRefreshWorker() { defer shutdownWg.Done() - ticker := time.NewTicker(60 * time.Second) + ticker := time.NewTicker(PKIRefreshInterval) defer ticker.Stop() for { select { - case <-shutdownSignal: + case <-shutdownCtx.Done(): return case <-ticker.C: - uptime := time.Since(stats.StartTime) - log.Printf("[STATS] Uptime: %v | Connections: %d/%d | Messages: R:%d D:%d F:%d | Sphinx:%d Direct:%d | Bytes: %d MB", - uptime.Round(time.Second), - stats.ConnectionsActive.Load(), - stats.ConnectionsTotal.Load(), - stats.MessagesReceived.Load(), - stats.MessagesDelivered.Load(), - stats.MessagesFailed.Load(), - stats.SphinxRoutingUsed.Load(), - stats.DirectRelayUsed.Load(), - stats.BytesTransferred.Load()/(1024*1024)) + 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()) + } + } } } } -func NodeHealthChecker() { +// ============================================================================ +// HEALTH CHECK WORKER +// ============================================================================ + +func HealthCheckWorker() { defer shutdownWg.Done() - ticker := time.NewTicker(5 * time.Minute) + ticker := time.NewTicker(HealthCheckInterval) defer ticker.Stop() for { select { - case <-shutdownSignal: + case <-shutdownCtx.Done(): return case <-ticker.C: - checkNodeHealth() + nodes := pkiDirectory.GetHealthyNodes() + log.Printf("[HEALTH] Checking %d nodes", len(nodes)) + + for _, node := range nodes { + go checkNodeHealth(node) + } } } } -func checkNodeHealth() { - nodes := pkiDirectory.GetHealthyNodes(true) - - for _, node := range nodes { - go func(n *PKINode) { - conn, err := torDialer.Dial("tcp", n.Address) - if err != nil { - n.Healthy = false - log.Printf("[HEALTH] Node %s is DOWN", n.NodeID[:16]) - return - } - conn.Close() - - n.Healthy = true - n.LastSeen = time.Now() - }(node) +func checkNodeHealth(node *PKINode) { + conn, err := torDialer.Dial("tcp", node.Address) + if err != nil { + pkiDirectory.mu.Lock() + node.Healthy = false + node.FailureCount++ + pkiDirectory.mu.Unlock() + return } + conn.Close() + + pkiDirectory.mu.Lock() + node.Healthy = true + node.LastHealthy = time.Now() + node.SuccessCount++ + pkiDirectory.mu.Unlock() } // ============================================================================ // MAIN // ============================================================================ -func init() { - emailRegExp = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+$`) - localPartRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+$`) - domainRegex = regexp.MustCompile(`^[a-zA-Z0-9.\-]+$`) - - delayGenerator = NewExponentialDelayGenerator(ExponentialDelayLambda, ExponentialDelayMin, ExponentialDelayMax) - paddingStats = NewPaddingStatistics() - - stats = &Statistics{ - StartTime: time.Now(), - } - - shutdownSignal = make(chan struct{}) - - // Seed math/rand for jitter calculations - mathrand.Seed(time.Now().UnixNano()) -} - func main() { - addr := flag.String("addr", "127.0.0.1:2525", "SMTP listen address") - name := flag.String("name", "fog.onion", "Server name") - nodeAddr := flag.String("node-addr", "127.0.0.1:9999", "Sphinx node listen address") - enableSphinx := flag.Bool("sphinx", false, "Enable Sphinx multi-hop routing") - pkiFile := flag.String("pki-file", "", "PKI directory JSON file") - pkiURL := flag.String("pki-url", "", "PKI directory URL (http:// or http://*.onion/)") - showVersion := flag.Bool("version", false, "Show version and features") + // 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") + flag.Parse() + // Version if *showVersion { - fmt.Printf("%s v%s\n\n", AppName, Version) - fmt.Println("LIBRARIES:") - fmt.Println("- golang.org/x/crypto/curve25519 (Curve25519 ECDH)") - fmt.Println("- golang.org/x/crypto/hkdf (HKDF-SHA256 key derivation)") - fmt.Println("- golang.org/x/net/proxy (Tor SOCKS5)") - fmt.Println("- crypto/aes + crypto/cipher (AES-GCM)") - fmt.Println("- crypto/hmac + crypto/sha256 (HMAC)") - fmt.Println("\nSECURITY FEATURES:") - fmt.Println("✓ Sphinx packet format (forward secrecy)") - fmt.Println("✓ Multi-hop routing (3-hop unlinkability)") - fmt.Println("✓ Dynamic PKI with 3h key rotation") - fmt.Println("✓ Exponential timing delays (anti-correlation)") - fmt.Println("✓ Adaptive padding geometry (size obfuscation)") - fmt.Println("✓ Replay protection (24h message-ID cache)") - fmt.Println("✓ No metadata retention (memory-only)") - fmt.Println("✓ Global adversary resistance") - fmt.Println("\nMODES:") - fmt.Println(" Direct relay (default): fog relays via Tor only") - fmt.Println(" Sphinx mode (-sphinx): Multi-hop through fog network") - fmt.Println("\nUSAGE:") - fmt.Println(" Direct mode:") - fmt.Println(" fog -addr 127.0.0.1:2525 -name fog.onion") - fmt.Println("\n Sphinx mode:") - fmt.Println(" fog -addr 127.0.0.1:2525 -name fog.onion -sphinx -pki-file nodes.json") + 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") + 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)") os.Exit(0) } - log.Printf("[FOG] Starting %s v%s", AppName, Version) - log.Printf("[FOG] Server name: %s", *name) - - // Initialize Tor dialer - dialer, err := proxy.SOCKS5("tcp", TorSocksProxyAddr, nil, proxy.Direct) - if err != nil { - log.Fatalf("[FOG] Failed to create Tor dialer: %v", err) + // Setup data directory + dataDir = *dataDirFlag + if err := os.MkdirAll(dataDir, 0700); err != nil { + log.Fatalf("[INIT] Failed to create data directory: %v", err) } - torDialer = dialer - log.Printf("[FOG] Tor proxy: %s", TorSocksProxyAddr) - // Initialize PKI - if err := InitializePKI(); err != nil { - log.Fatalf("[FOG] PKI initialization failed: %v", err) - } + statsFile = filepath.Join(dataDir, "stats.json") - // Update local node address with the public name - // This is what we'll use in SMTP EHLO - localNode.Address = *name - - log.Printf("[FOG] Node identity: %s", localNode.Address) - - // Load PKI directory - if *pkiFile != "" { - if err := pkiDirectory.LoadFromFile(*pkiFile); err != nil { - log.Printf("[PKI] Warning: Failed to load PKI file: %v", err) + // 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) } - if *pkiURL != "" { - if err := pkiDirectory.LoadFromURL(*pkiURL); err != nil { - log.Printf("[PKI] Warning: Failed to load PKI from URL: %v", err) + // Show nodes + pkiDirectory = NewPKIDirectory() + if *showNodes { + if *pkiFileFlag != "" { + if err := pkiDirectory.LoadFromFile(*pkiFileFlag); err != nil { + log.Fatalf("[PKI] Failed to load: %v", err) + } + } + + 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("---") } + os.Exit(0) } - // Check if Sphinx should be enabled - if *enableSphinx { - if pkiDirectory.HealthyCount() >= SphinxHopCount { - sphinxEnabled.Store(true) - log.Printf("[FOG] Mode: Sphinx multi-hop ENABLED (%d healthy nodes)", pkiDirectory.HealthyCount()) + // 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 { - log.Printf("[FOG] Warning: Sphinx requested but only %d healthy nodes (need %d)", - pkiDirectory.HealthyCount(), SphinxHopCount) - log.Printf("[FOG] Mode: Direct relay (will auto-upgrade when nodes available)") + nodeAddress = *nodeAddrFlag } + + // 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]) + 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 { - log.Printf("[FOG] Mode: Direct relay") + nodeAddr = *nodeAddrFlag } - // Initialize mail queue - mailQueue = make(chan *Envelope, 1000) + InitializeLocalNode(nodeAddr) + InitializeTor() + + enableSphinx.Store(*enableSphinxFlag) + pkiURL = *pkiURLFlag + + if *enableSphinxFlag { + log.Printf("[INIT] Sphinx ENABLED with AES-256-GCM") + log.Printf("[INIT] Variable-hop routing: %d-%d hops (random)", + MinSphinxHops, MaxSphinxHops) + + if *pkiFileFlag != "" { + pkiDirectory.LoadFromFile(*pkiFileFlag) + } - // Start Sphinx node server if enabled - if *enableSphinx { + if pkiURL != "" { + pkiDirectory.LoadFromURL(pkiURL) + shutdownWg.Add(1) + go PKIRefreshWorker() + } + + shutdownWg.Add(1) + go HealthCheckWorker() + + // Start Sphinx node server shutdownWg.Add(1) - if err := StartSphinxNodeServer(*nodeAddr); err != nil { + if err := StartSphinxNodeServer(nodeAddr); err != nil { log.Fatalf("[SPHINX] Failed to start node server: %v", err) } - } - // Start relay workers - for i := 0; i < RelayWorkerCount; i++ { + // Start mixnet batch worker shutdownWg.Add(1) - go relayWorker(i) + go MixnetBatchWorker() + } else { + log.Printf("[INIT] Sphinx DISABLED (direct relay only)") } - // Start background workers - shutdownWg.Add(1) - go RotatePKIKeys() - - shutdownWg.Add(1) - go PaddingUpdateWorker() + // Start workers + for i := 0; i < WorkerCount; i++ { + shutdownWg.Add(1) + go RelayWorker(i) + } shutdownWg.Add(1) - go statsMonitor() + go StatsMonitor() + // Key rotation every 30 days shutdownWg.Add(1) - go NodeHealthChecker() - - if *pkiURL != "" { - shutdownWg.Add(1) - go PKIRefreshWorker(*pkiURL) - } + go RotatePKIKeys() // Start SMTP server - server := NewSMTPServer(*name, *addr) + server := NewSMTPServer(*serverName, *smtpAddr) - // Handle shutdown + // Handle signals sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { <-sigChan log.Printf("[FOG] Shutdown signal received") - close(shutdownSignal) + shutdownCancel() if server.listener != nil { server.listener.Close() } @@ -1677,11 +2005,12 @@ func main() { shutdownWg.Wait() - // Save PKI on shutdown - if *pkiFile != "" { - if err := pkiDirectory.SaveToFile(*pkiFile); err != nil { - log.Printf("[PKI] Warning: Failed to save PKI: %v", err) - } + // Save final state + if *pkiFileFlag != "" { + pkiDirectory.SaveToFile(*pkiFileFlag) + } + if statsFile != "" { + stats.SaveToFile(statsFile) } log.Printf("[FOG] Shutdown complete") |
