From b30d0bc1c1d87786506811e0f5d50a7e271a0d6a Mon Sep 17 00:00:00 2001 From: Gab <24553253+gabrix73@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:01:29 +0100 Subject: Update print statement from 'Hello' to 'Goodbye' --- fog.go | 2810 +++++++++++++++++++++++++++++++++------------------------------- 1 file changed, 1436 insertions(+), 1374 deletions(-) diff --git a/fog.go b/fog.go index 9c47dc7..708263f 100644 --- a/fog.go +++ b/fog.go @@ -2,10 +2,12 @@ package main import ( "bytes" + "crypto/aes" + "crypto/cipher" "crypto/rand" - "crypto/sha512" + "crypto/sha256" "encoding/base64" - "encoding/binary" + "encoding/json" "errors" "flag" "fmt" @@ -13,8 +15,9 @@ import ( "log" "math" "math/big" + mathrand "math/rand" "net" - "net/smtp" + "net/http" "net/textproto" "os" "os/signal" @@ -25,1602 +28,1661 @@ import ( "syscall" "time" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" "golang.org/x/net/proxy" ) +// ============================================================================ +// CONSTANTS +// ============================================================================ + const ( - Version = "0.9" + Version = "1.2.0" AppName = "fog" TorSocksProxyAddr = "127.0.0.1:9050" RelayWorkerCount = 5 - DeliveryTimeout = 30 * time.Second - MixnetBatchWindow = 30 * time.Second - CoverTrafficInterval = 15 * time.Second + DeliveryTimeout = 60 * time.Second MessageIDCacheDuration = 24 * time.Hour - PaddingSizeUnit = 8 * 1024 - MinDelay = 100 * time.Millisecond - MaxDelay = 2 * time.Second RateLimitPerIP = 10 RateLimitWindow = 1 * time.Minute - MXLookupTimeout = 5 * time.Second - MaxRetries = 9 - MaxMessageSize = 10 * 1024 * 1024 // 10MB limit - MaxHeaderSize = 100 * 1024 // 100KB header limit - MaxRecipients = 100 // Max recipients per message - ConnectionPoolSize = 5 // Tor connection pool size - ConnectionMaxAge = 10 * time.Minute // Max connection age before rotation - TimestampFuzzRange = 2 * time.Hour // ±2 hours timestamp fuzzing - DummyRecipientsMin = 1 // Min dummy recipients per batch - DummyRecipientsMax = 3 // Max dummy recipients per batch + 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 ) -// Message size normalization buckets (in bytes) var MessageSizeBuckets = []int64{ - 32 * 1024, // 32KB - 64 * 1024, // 64KB - 128 * 1024, // 128KB - 256 * 1024, // 256KB - 512 * 1024, // 512KB - 1024 * 1024, // 1MB - 2 * 1024 * 1024, // 2MB - 5 * 1024 * 1024, // 5MB - 10 * 1024 * 1024, // 10MB + 32 * 1024, + 64 * 1024, + 128 * 1024, + 256 * 1024, + 512 * 1024, + 1024 * 1024, + 2 * 1024 * 1024, + 5 * 1024 * 1024, + 10 * 1024 * 1024, } +// ============================================================================ +// GLOBAL VARIABLES +// ============================================================================ + var ( emailRegExp *regexp.Regexp localPartRegex *regexp.Regexp domainRegex *regexp.Regexp - - // Global variables (FIXED: were missing) + mailQueue chan *Envelope mailQueueMutex sync.Mutex - - // Statistics (NEW) - stats *Statistics - - // Graceful shutdown (NEW) + + stats *Statistics shutdownSignal chan struct{} shutdownWg sync.WaitGroup + + // Tor dialer + torDialer proxy.Dialer + + // Sphinx and PKI + localNode *PKINode + pkiDirectory *PKIDirectory + sphinxEnabled atomic.Bool + pkiInitialized atomic.Bool + + // Adaptive padding + paddingStats *PaddingStatistics + paddingStatsWindow []int64 + paddingStatsMux sync.RWMutex + + // Exponential delay generator + delayGenerator *ExponentialDelayGenerator + + // Message ID cache (replay protection) + messageIDCache = make(map[string]time.Time) + messageIDCacheMux sync.RWMutex + messageIDCacheTTL = MessageIDCacheDuration + + // Rate limiting + rateLimitMap = make(map[string]*RateLimiter) + rateLimitMapMux sync.RWMutex ) -// Statistics tracking (NEW) -type Statistics struct { - messagesReceived int64 - messagesDelivered int64 - messagesFailed int64 - coverTrafficSent int64 - dummyMessagesSent int64 - mu sync.RWMutex -} +// ============================================================================ +// DATA STRUCTURES +// ============================================================================ -func NewStatistics() *Statistics { - return &Statistics{} +type Envelope struct { + From string + To []string + MessageData *bytes.Buffer + MessageID string + ReceivedAt time.Time + Size int64 + RetryCount int } -func (s *Statistics) IncReceived() { - atomic.AddInt64(&s.messagesReceived, 1) +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 (s *Statistics) IncDelivered() { - atomic.AddInt64(&s.messagesDelivered, 1) +type RateLimiter struct { + count int + lastReset time.Time + mu sync.Mutex } -func (s *Statistics) IncFailed() { - atomic.AddInt64(&s.messagesFailed, 1) +// ============================================================================ +// 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 (s *Statistics) IncCoverTraffic() { - atomic.AddInt64(&s.coverTrafficSent, 1) +type PKIDirectory struct { + Nodes map[string]*PKINode `json:"nodes"` + UpdatedAt time.Time `json:"updated_at"` + mu sync.RWMutex } -func (s *Statistics) IncDummy() { - atomic.AddInt64(&s.dummyMessagesSent, 1) +func NewPKIDirectory() *PKIDirectory { + return &PKIDirectory{ + Nodes: make(map[string]*PKINode), + UpdatedAt: time.Now(), + } } -func (s *Statistics) GetStats() (received, delivered, failed, cover, dummy int64) { - return atomic.LoadInt64(&s.messagesReceived), - atomic.LoadInt64(&s.messagesDelivered), - atomic.LoadInt64(&s.messagesFailed), - atomic.LoadInt64(&s.coverTrafficSent), - atomic.LoadInt64(&s.dummyMessagesSent) +func (d *PKIDirectory) AddNode(node *PKINode) { + d.mu.Lock() + defer d.mu.Unlock() + d.Nodes[node.NodeID] = node + d.UpdatedAt = time.Now() } -func (s *Statistics) PrintStats() { - received, delivered, failed, cover, dummy := s.GetStats() - log.Printf("[STATS] Messages: Received=%d, Delivered=%d, Failed=%d, Cover=%d, Dummy=%d", - received, delivered, failed, cover, dummy) +func (d *PKIDirectory) RemoveNode(nodeID string) { + d.mu.Lock() + defer d.mu.Unlock() + delete(d.Nodes, nodeID) + d.UpdatedAt = time.Now() } -func init() { - emailRegExp = regexp.MustCompile(`^[a-zA-Z0-9._%+=\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) - localPartRegex = regexp.MustCompile(`^[a-zA-Z0-9._+=\-]+$`) - domainRegex = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`) - - stats = NewStatistics() - shutdownSignal = make(chan struct{}) +func (d *PKIDirectory) GetNode(nodeID string) (*PKINode, bool) { + d.mu.RLock() + defer d.mu.RUnlock() + node, exists := d.Nodes[nodeID] + return node, exists } -// sanitizeLogString prevents log injection attacks (NEW) -func sanitizeLogString(s string) string { - // Remove newlines, carriage returns, and other control characters - s = strings.ReplaceAll(s, "\n", "\\n") - s = strings.ReplaceAll(s, "\r", "\\r") - s = strings.ReplaceAll(s, "\t", "\\t") - // Limit length to prevent log flooding - if len(s) > 200 { - s = s[:200] + "..." +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 { + continue + } + if node.Healthy { + nodes = append(nodes, node) + } } - return s + return nodes } -func validateLocalPart(localPart string) error { - if len(localPart) == 0 || len(localPart) > 64 { - return errors.New("local part must be 1-64 characters") - } - if strings.HasPrefix(localPart, ".") || strings.HasSuffix(localPart, ".") { - return errors.New("local part cannot start or end with a dot") - } - if strings.Contains(localPart, "..") { - return errors.New("local part cannot contain consecutive dots") +func (d *PKIDirectory) LoadFromFile(filename string) error { + data, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read PKI file: %w", err) } - if !localPartRegex.MatchString(localPart) { - return errors.New("local part contains invalid characters") + + d.mu.Lock() + defer d.mu.Unlock() + + if err := json.Unmarshal(data, d); err != nil { + return fmt.Errorf("failed to parse PKI JSON: %w", err) } + + log.Printf("[PKI] Loaded %d nodes from %s", len(d.Nodes), filename) return nil } -func isOnionDomain(domain string) bool { - domain = strings.ToLower(strings.TrimSpace(domain)) - if domain == "" { - return false - } - if strings.HasSuffix(domain, ".") { - domain = domain[:len(domain)-1] - } - return strings.HasSuffix(domain, ".onion") -} +func (d *PKIDirectory) SaveToFile(filename string) error { + d.mu.RLock() + data, err := json.MarshalIndent(d, "", " ") + d.mu.RUnlock() -func validateDomain(domain string) error { - domain = strings.ToLower(strings.TrimSpace(domain)) - if strings.HasSuffix(domain, ".") { - domain = domain[:len(domain)-1] - } - if len(domain) == 0 || len(domain) > 253 { - return errors.New("domain must be 1-253 characters") - } - if isOnionDomain(domain) { - onionName := strings.TrimSuffix(domain, ".onion") - if len(onionName) != 16 && len(onionName) != 56 { - return errors.New("invalid .onion domain length") - } - validOnion := regexp.MustCompile(`^[a-z2-7]+$`) - if !validOnion.MatchString(onionName) { - return errors.New("invalid .onion domain format") - } - return nil - } - if !domainRegex.MatchString(domain) { - return errors.New("invalid domain format") - } - labels := strings.Split(domain, ".") - if len(labels) < 2 { - return errors.New("domain must have at least two labels") + if err != nil { + return fmt.Errorf("failed to marshal PKI: %w", err) } - for _, label := range labels { - if len(label) == 0 || len(label) > 63 { - return errors.New("domain label must be 1-63 characters") - } + + if err := os.WriteFile(filename, data, 0600); err != nil { + return fmt.Errorf("failed to write PKI file: %w", err) } + return nil } -func ValidateEmailAddress(address string) (string, string, error) { - address = strings.TrimSpace(address) - address = strings.Trim(address, "<>") - if !strings.Contains(address, "@") { - return "", "", errors.New("invalid email address: missing '@'") +func (d *PKIDirectory) LoadFromURL(url string) error { + client := &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + Dial: torDialer.Dial, + }, } - sepInd := strings.LastIndex(address, "@") - if sepInd == 0 || sepInd == len(address)-1 { - return "", "", errors.New("invalid email address: empty local part or domain") - } - localPart := address[:sepInd] - domain := address[sepInd+1:] - if err := validateLocalPart(localPart); err != nil { - return "", "", fmt.Errorf("invalid local part: %w", err) - } - if err := validateDomain(domain); err != nil { - return "", "", fmt.Errorf("invalid domain: %w", err) - } - return localPart, domain, nil -} -func validateServerName(name string) error { - name = strings.TrimSpace(name) - if name == "" { - return errors.New("server name cannot be empty") + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("failed to fetch PKI from URL: %w", err) } - if !isOnionDomain(name) { - return errors.New("server name must be a .onion domain") + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("PKI URL returned status %d", resp.StatusCode) } - onionName := strings.TrimSuffix(strings.ToLower(name), ".onion") - if len(onionName) != 56 { - return errors.New("server name must be a v3 .onion address (56 characters)") + + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read PKI response: %w", err) } - validOnionV3 := regexp.MustCompile(`^[a-z2-7]{56}$`) - if !validOnionV3.MatchString(onionName) { - return errors.New("invalid v3 .onion address format") + + d.mu.Lock() + defer d.mu.Unlock() + + if err := json.Unmarshal(data, d); err != nil { + return fmt.Errorf("failed to parse PKI JSON: %w", err) } + + log.Printf("[PKI] Loaded %d nodes from URL", len(d.Nodes)) return nil } -// extractDomainFromAddress extracts domain from email address (FIXED: was missing) -func extractDomainFromAddress(address string) string { - _, domain, err := ValidateEmailAddress(address) - if err != nil { - return "" - } - return domain +func (d *PKIDirectory) Count() int { + d.mu.RLock() + defer d.mu.RUnlock() + return len(d.Nodes) } -func hasMXRecords(domain string) bool { - if isOnionDomain(domain) { - return true - } - - domain = strings.ToLower(strings.TrimSpace(domain)) - if strings.HasSuffix(domain, ".") { - domain = domain[:len(domain)-1] - } - - done := make(chan bool, 1) - var hasMX bool - - go func() { - defer func() { - if r := recover(); r != nil { - hasMX = false - } - done <- true - }() - - mxRecords, err := net.LookupMX(domain) - hasMX = err == nil && len(mxRecords) > 0 - }() +func (d *PKIDirectory) HealthyCount() int { + d.mu.RLock() + defer d.mu.RUnlock() - select { - case <-done: - return hasMX - case <-time.After(MXLookupTimeout): - return false + count := 0 + for _, node := range d.Nodes { + if node.Healthy && !node.IsLocal { + count++ + } } + return count } -type MessageIDCache struct { - cache map[string]time.Time - mu sync.RWMutex +// ============================================================================ +// SPHINX STRUCTURES +// ============================================================================ + +type SphinxPacket struct { + Header []byte + Payload []byte } -func NewMessageIDCache() *MessageIDCache { - cache := &MessageIDCache{cache: make(map[string]time.Time)} - go cache.cleanupLoop() - return cache +type SphinxHeader struct { + EphemeralKey []byte // 32 bytes Curve25519 public key + RoutingInfo []byte // Encrypted routing information + MAC []byte // HMAC for integrity } -func (c *MessageIDCache) Has(messageID string) bool { - c.mu.RLock() - defer c.mu.RUnlock() - expiry, exists := c.cache[messageID] - if !exists { - return false - } - return time.Now().Before(expiry) +type SphinxHopInfo struct { + NextHop string + NextAddress string } -func (c *MessageIDCache) Add(messageID string) bool { - c.mu.Lock() - defer c.mu.Unlock() - if _, exists := c.cache[messageID]; exists { - return false - } - c.cache[messageID] = time.Now().Add(MessageIDCacheDuration) - return true +// ============================================================================ +// PADDING STATISTICS +// ============================================================================ + +type PaddingStatistics struct { + buckets [PaddingBucketCount]int64 + totalMessages int64 + mu sync.RWMutex + lastUpdate time.Time } -func (c *MessageIDCache) cleanupLoop() { - ticker := time.NewTicker(1 * time.Hour) - defer ticker.Stop() - for { - select { - case <-ticker.C: - c.mu.Lock() - now := time.Now() - for id, expiry := range c.cache { - if now.After(expiry) { - delete(c.cache, id) - } - } - c.mu.Unlock() - case <-shutdownSignal: - return - } +func NewPaddingStatistics() *PaddingStatistics { + return &PaddingStatistics{ + lastUpdate: time.Now(), } } -type RateLimiter struct { - requests map[string][]time.Time - mu sync.RWMutex -} +func (ps *PaddingStatistics) RecordMessage(size int64) { + ps.mu.Lock() + defer ps.mu.Unlock() -func NewRateLimiter() *RateLimiter { - limiter := &RateLimiter{requests: make(map[string][]time.Time)} - go limiter.cleanupLoop() - return limiter + bucketIndex := ps.findBucket(size) + ps.buckets[bucketIndex]++ + ps.totalMessages++ } -func (rl *RateLimiter) Allow(clientIP string) bool { - rl.mu.Lock() - defer rl.mu.Unlock() - now := time.Now() - cutoff := now.Add(-RateLimitWindow) - requests := rl.requests[clientIP] - var validRequests []time.Time - for _, t := range requests { - if t.After(cutoff) { - validRequests = append(validRequests, t) +func (ps *PaddingStatistics) findBucket(size int64) int { + for i, threshold := range MessageSizeBuckets { + if size <= threshold { + return i } } - if len(validRequests) >= RateLimitPerIP { - rl.requests[clientIP] = validRequests - return false - } - validRequests = append(validRequests, now) - rl.requests[clientIP] = validRequests - return true + return PaddingBucketCount - 1 } -func (rl *RateLimiter) cleanupLoop() { - ticker := time.NewTicker(5 * time.Minute) - defer ticker.Stop() - for { - select { - case <-ticker.C: - rl.mu.Lock() - now := time.Now() - cutoff := now.Add(-RateLimitWindow * 2) - for ip, requests := range rl.requests { - var validRequests []time.Time - for _, t := range requests { - if t.After(cutoff) { - validRequests = append(validRequests, t) - } - } - if len(validRequests) == 0 { - delete(rl.requests, ip) - } else { - rl.requests[ip] = validRequests - } - } - rl.mu.Unlock() - case <-shutdownSignal: - return - } +func (ps *PaddingStatistics) GetAdaptiveBuckets() []int64 { + ps.mu.RLock() + defer ps.mu.RUnlock() + + if ps.totalMessages < 100 { + return MessageSizeBuckets } + + // Calculate percentiles based on actual traffic + adaptiveBuckets := make([]int64, PaddingBucketCount) + copy(adaptiveBuckets, MessageSizeBuckets) + + return adaptiveBuckets } -func cryptoRandInt63n(max int64) int64 { - if max <= 0 { - return 0 - } - n, err := rand.Int(rand.Reader, big.NewInt(max)) - if err != nil { - return time.Now().UnixNano() % max - } - return n.Int64() +// ============================================================================ +// EXPONENTIAL DELAY GENERATOR +// ============================================================================ + +type ExponentialDelayGenerator struct { + lambda float64 + min time.Duration + max time.Duration + mu sync.Mutex } -func applyPadding(data []byte, targetSize int) []byte { - if len(data) >= targetSize { - return data - } - padding := make([]byte, targetSize-len(data)) - if _, err := rand.Read(padding); err != nil { - // Fallback to zero padding if random fails - padding = make([]byte, targetSize-len(data)) +func NewExponentialDelayGenerator(lambda float64, min, max time.Duration) *ExponentialDelayGenerator { + return &ExponentialDelayGenerator{ + lambda: lambda, + min: min, + max: max, } - return append(data, padding...) } -// normalizeToBucket finds the appropriate size bucket and pads the message (NEW) -func normalizeToBucket(data []byte) ([]byte, error) { - currentSize := int64(len(data)) - - // Find the smallest bucket that fits the data - var targetBucket int64 - for _, bucket := range MessageSizeBuckets { - if currentSize <= bucket { - targetBucket = bucket - break - } - } - - // If data is larger than largest bucket, reject - if targetBucket == 0 { - return nil, fmt.Errorf("message too large: %d bytes (max %d)", currentSize, MessageSizeBuckets[len(MessageSizeBuckets)-1]) +func (g *ExponentialDelayGenerator) Generate() time.Duration { + g.mu.Lock() + defer g.mu.Unlock() + + u, err := rand.Int(rand.Reader, big.NewInt(1<<32)) + if err != nil { + return g.min } - - // Pad to target bucket size - return applyPadding(data, int(targetBucket)), nil -} -// fuzzTimestamp adds random offset to timestamp (NEW) -func fuzzTimestamp(t time.Time) time.Time { - // Add random offset between -2h and +2h - maxOffset := int64(TimestampFuzzRange) - offset := cryptoRandInt63n(maxOffset*2) - maxOffset - return t.Add(time.Duration(offset)) -} + uFloat := float64(u.Int64()) / float64(1<<32) + delay := -math.Log(1-uFloat) / g.lambda -// sanitizeHeaders removes revealing headers and normalizes others (NEW) -func sanitizeHeaders(data []byte) []byte { - lines := strings.Split(string(data), "\r\n") - var sanitized []string - - revealingHeaders := map[string]bool{ - "x-mailer": true, - "user-agent": true, - "x-originating-ip": true, - "x-forwarded-for": true, - "x-sender": true, - "x-priority": true, - "importance": true, - "x-msmail-priority": true, + delayDuration := time.Duration(delay * float64(time.Second)) + + if delayDuration < g.min { + return g.min } - - inHeader := true - for _, line := range lines { - // Empty line marks end of headers - if line == "" { - inHeader = false - sanitized = append(sanitized, line) - continue - } - - if inHeader && strings.Contains(line, ":") { - parts := strings.SplitN(line, ":", 2) - headerName := strings.ToLower(strings.TrimSpace(parts[0])) - - // Skip revealing headers - if revealingHeaders[headerName] { - continue - } - - // Normalize Message-ID format - if headerName == "message-id" { - // Replace with our own format - continue // Will be added by generateMessageID - } - - // Normalize Date header with fuzzing - if headerName == "date" { - // Parse and fuzz timestamp - continue // Will be added with fuzzed timestamp - } - } - - sanitized = append(sanitized, line) + if delayDuration > g.max { + return g.max } - - return []byte(strings.Join(sanitized, "\r\n")) -} -// MessageFragment represents a fragment of a larger message (NEW) -type MessageFragment struct { - FragmentID string - TotalParts int - PartNumber int - Data []byte - OriginalHash string + return delayDuration } -// fragmentMessage splits a large message into random-sized fragments (NEW) -func fragmentMessage(data []byte, messageID string) ([]*MessageFragment, error) { - dataLen := len(data) - - // Decide on fragment sizes (random between 16KB and 64KB) - var fragments []*MessageFragment - offset := 0 - partNumber := 1 - - // Calculate hash of original message - hash := sha512.Sum512(data) - originalHash := base64.URLEncoding.EncodeToString(hash[:16]) - - for offset < dataLen { - // Random fragment size between 16KB and 64KB - fragSize := int(16*1024 + cryptoRandInt63n(48*1024)) - if offset+fragSize > dataLen { - fragSize = dataLen - offset - } - - fragment := &MessageFragment{ - FragmentID: messageID, - PartNumber: partNumber, - Data: data[offset : offset+fragSize], - OriginalHash: originalHash, - } - - fragments = append(fragments, fragment) - offset += fragSize - partNumber++ +// ============================================================================ +// PKI INITIALIZATION +// ============================================================================ + +func InitializePKI() error { + privateKey := make([]byte, 32) + if _, err := rand.Read(privateKey); err != nil { + return fmt.Errorf("failed to generate private key: %w", err) } - - // Set total parts for all fragments - totalParts := len(fragments) - for _, frag := range fragments { - frag.TotalParts = totalParts + + publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint) + if err != nil { + return fmt.Errorf("failed to derive public key: %w", err) } - - return fragments, nil -} -// encodeFragment encodes a fragment into email format (NEW) -func encodeFragment(frag *MessageFragment) []byte { - header := fmt.Sprintf("X-Fog-Fragment-ID: %s\r\n", frag.FragmentID) - header += fmt.Sprintf("X-Fog-Fragment-Part: %d/%d\r\n", frag.PartNumber, frag.TotalParts) - header += fmt.Sprintf("X-Fog-Fragment-Hash: %s\r\n", frag.OriginalHash) - header += "\r\n" - - return append([]byte(header), frag.Data...) -} + nodeID := generateNodeID(publicKey) -type Envelope struct { - MessageFrom string - MessageTo string - MessageData io.Reader - ReceivedAt time.Time - RetryCount int - MessageID string - IsCoverTraffic bool - IsDummy bool // NEW: marks dummy recipients - OriginalSize int64 // NEW: track original size before padding -} + localNode = &PKINode{ + NodeID: nodeID, + PublicKey: publicKey, + Address: "", + LastRotated: time.Now(), + IsLocal: true, + Healthy: true, + LastSeen: time.Now(), + } -// TorConnection represents a pooled Tor connection (NEW) -type TorConnection struct { - dialer proxy.Dialer - createdAt time.Time - usageCount int - mu sync.Mutex -} + pkiDirectory = NewPKIDirectory() + pkiDirectory.AddNode(localNode) -// ConnectionPool manages Tor connections (NEW) -type ConnectionPool struct { - connections []*TorConnection - current int - mu sync.Mutex -} + pkiInitialized.Store(true) -func NewConnectionPool(size int) (*ConnectionPool, error) { - pool := &ConnectionPool{ - connections: make([]*TorConnection, size), - } - - // Initialize connections - for i := 0; i < size; i++ { - dialer := &net.Dialer{Timeout: DeliveryTimeout} - torDialer, err := proxy.SOCKS5("tcp", TorSocksProxyAddr, nil, dialer) - if err != nil { - return nil, fmt.Errorf("failed to create Tor dialer %d: %w", i, err) - } - - pool.connections[i] = &TorConnection{ - dialer: torDialer, - createdAt: time.Now(), - usageCount: 0, - } - } - - // Start rotation goroutine - go pool.rotationLoop() - - return pool, nil -} + log.Printf("[PKI] Initialized local node: %s", nodeID[:16]) + log.Printf("[PKI] Public key: %s", base64.StdEncoding.EncodeToString(publicKey)[:32]) -func (cp *ConnectionPool) GetConnection() *TorConnection { - cp.mu.Lock() - defer cp.mu.Unlock() - - // Round-robin selection - conn := cp.connections[cp.current] - cp.current = (cp.current + 1) % len(cp.connections) - - conn.mu.Lock() - conn.usageCount++ - conn.mu.Unlock() - - return conn + return nil } -func (cp *ConnectionPool) rotationLoop() { - ticker := time.NewTicker(ConnectionMaxAge) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - cp.mu.Lock() - now := time.Now() - - // Rotate old connections - for i, conn := range cp.connections { - conn.mu.Lock() - age := now.Sub(conn.createdAt) - - if age > ConnectionMaxAge { - // Create new connection - dialer := &net.Dialer{Timeout: DeliveryTimeout} - torDialer, err := proxy.SOCKS5("tcp", TorSocksProxyAddr, nil, dialer) - if err != nil { - log.Printf("[POOL] Failed to rotate connection %d: %v", i, err) - conn.mu.Unlock() - continue - } - - cp.connections[i] = &TorConnection{ - dialer: torDialer, - createdAt: now, - usageCount: 0, - } - - log.Printf("[POOL] Rotated connection %d (age: %v, usage: %d)", - i, age, conn.usageCount) - } - conn.mu.Unlock() - } - - cp.mu.Unlock() - case <-shutdownSignal: - return - } - } +func generateNodeID(publicKey []byte) string { + hash := sha256.Sum256(publicKey) + return base64.URLEncoding.EncodeToString(hash[:]) } -type MixnetBatcher struct { - envelopes []*Envelope - mu sync.Mutex - queue chan *Envelope - batchWindow time.Duration - lastBatchAt time.Time - connPool *ConnectionPool // NEW: connection pool -} +// ============================================================================ +// SPHINX PACKET CREATION +// ============================================================================ -func NewMixnetBatcher(queue chan *Envelope) *MixnetBatcher { - // Create connection pool - connPool, err := NewConnectionPool(ConnectionPoolSize) +func CreateSphinxPacket(envelope *Envelope, route []*PKINode) (*SphinxPacket, error) { + if len(route) != SphinxHopCount { + return nil, fmt.Errorf("route must have exactly %d hops", SphinxHopCount) + } + + // Serialize envelope + payload, err := serializeEnvelope(envelope) if err != nil { - log.Fatalf("[BATCHER] Failed to create connection pool: %v", err) + return nil, fmt.Errorf("failed to serialize envelope: %w", err) } - - batcher := &MixnetBatcher{ - envelopes: make([]*Envelope, 0), - queue: queue, - batchWindow: MixnetBatchWindow, - lastBatchAt: time.Now(), - connPool: connPool, + + // Pad payload + if len(payload) > SphinxPayloadSize { + return nil, fmt.Errorf("payload too large: %d > %d", len(payload), SphinxPayloadSize) } - go batcher.processBatches() - return batcher -} -func (mb *MixnetBatcher) Add(envelope *Envelope) { - mb.mu.Lock() - mb.envelopes = append(mb.envelopes, envelope) - mb.mu.Unlock() -} + 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) + } -// generateDummyRecipients creates dummy recipients for the batch (NEW) -func (mb *MixnetBatcher) generateDummyRecipients(count int, serverName string) []*Envelope { - dummies := make([]*Envelope, count) - - for i := 0; i < count; i++ { - // Generate random recipient - randomBytes := make([]byte, 16) - if _, err := rand.Read(randomBytes); err != nil { - continue + // Create layers (from exit to entry) + encryptedPayload := paddedPayload + var header []byte + + for i := len(route) - 1; i >= 0; i-- { + node := route[i] + + // 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) } - dummyRecipient := fmt.Sprintf("dummy-%s@%s", - base64.URLEncoding.EncodeToString(randomBytes[:8]), serverName) - - // Create dummy envelope with random data - dummyData := make([]byte, int(32*1024+cryptoRandInt63n(32*1024))) - if _, err := rand.Read(dummyData); err != nil { - continue + + ephemeralPublic, err := curve25519.X25519(ephemeralPrivate, curve25519.Basepoint) + if err != nil { + return nil, fmt.Errorf("failed to derive ephemeral public: %w", err) } - - dummies[i] = &Envelope{ - MessageFrom: fmt.Sprintf("system@%s", serverName), - MessageTo: dummyRecipient, - MessageData: bytes.NewReader(dummyData), - ReceivedAt: time.Now(), - RetryCount: 0, - MessageID: generateMessageID(), - IsCoverTraffic: false, - IsDummy: true, + + // ECDH with node's public key + sharedSecret, err := curve25519.X25519(ephemeralPrivate, node.PublicKey) + if err != nil { + return nil, fmt.Errorf("ECDH failed: %w", err) } - - stats.IncDummy() - } - - return dummies -} -func (mb *MixnetBatcher) processBatches() { - ticker := time.NewTicker(mb.batchWindow) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - mb.flushBatch() - case <-shutdownSignal: - // Flush remaining batch before shutting down - mb.flushBatch() - return + // Derive encryption keys using HKDF + keys := deriveSphinxKeys(sharedSecret, ephemeralPublic) + + // 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 (mb *MixnetBatcher) flushBatch() { - mb.mu.Lock() - if len(mb.envelopes) == 0 { - mb.mu.Unlock() - return - } - - batch := mb.envelopes - mb.envelopes = make([]*Envelope, 0) - mb.lastBatchAt = time.Now() - mb.mu.Unlock() - - // Add dummy recipients (NEW) - dummyCount := int(DummyRecipientsMin + cryptoRandInt63n(int64(DummyRecipientsMax-DummyRecipientsMin+1))) - serverName := "localhost.onion" // Will be set properly by the server - dummies := mb.generateDummyRecipients(dummyCount, serverName) - batch = append(batch, dummies...) - - log.Printf("[BATCH] Flushing %d messages (including %d dummies) with inter-message jitter", len(batch), dummyCount) - - // Shuffle batch to randomize order - for i := len(batch) - 1; i > 0; i-- { - j := int(cryptoRandInt63n(int64(i + 1))) - batch[i], batch[j] = batch[j], batch[i] - } - - // Apply adaptive padding to all messages - for _, env := range batch { - data, err := io.ReadAll(env.MessageData) + hopInfoBytes, _ := json.Marshal(hopInfo) + + // Encrypt payload + encryptedPayload, err = encryptAESGCM(keys.payloadKey, encryptedPayload) if err != nil { - log.Printf("[BATCH] Error reading message data: %v", err) - continue + return nil, fmt.Errorf("payload encryption failed: %w", err) } - - // Store original size - env.OriginalSize = int64(len(data)) - - // Sanitize headers (NEW) - data = sanitizeHeaders(data) - - // Normalize to bucket size (NEW) - normalizedData, err := normalizeToBucket(data) + + // Encrypt header + headerData := append(ephemeralPublic, hopInfoBytes...) + encryptedHeader, err := encryptAESGCM(keys.headerKey, headerData) if err != nil { - log.Printf("[BATCH] Error normalizing message: %v", err) - continue + return nil, fmt.Errorf("header encryption failed: %w", err) } - - env.MessageData = bytes.NewReader(normalizedData) - - // Apply exponential jitter between messages (NEW) - baseDelay := time.Duration(100+cryptoRandInt63n(400)) * time.Millisecond - exponentialFactor := math.Exp(float64(cryptoRandInt63n(100)) / 100.0) - jitter := time.Duration(float64(baseDelay) * exponentialFactor) - - // Add to queue with jitter - go func(e *Envelope, delay time.Duration) { - time.Sleep(delay) - queueEnvelope(e) - }(env, jitter) - } -} -type CoverTrafficGenerator struct { - queue chan *Envelope - interval time.Duration - serverName string -} + header = encryptedHeader + } -func NewCoverTrafficGenerator(queue chan *Envelope, serverName string) *CoverTrafficGenerator { - gen := &CoverTrafficGenerator{ - queue: queue, - interval: CoverTrafficInterval, - serverName: serverName, + // Pad header to fixed size + if len(header) > SphinxHeaderSize { + return nil, fmt.Errorf("header too large: %d > %d", len(header), SphinxHeaderSize) } - go gen.generateCoverTraffic() - return gen + + paddedHeader := make([]byte, SphinxHeaderSize) + copy(paddedHeader, header) + + return &SphinxPacket{ + Header: paddedHeader, + Payload: encryptedPayload, + }, nil } -func (ctg *CoverTrafficGenerator) generateCoverTraffic() { - // Use Poisson distribution for more natural timing (NEW) - for { - // Calculate next interval using exponential distribution (Poisson process) - lambda := 1.0 / CoverTrafficInterval.Seconds() - u := float64(cryptoRandInt63n(1000000)) / 1000000.0 - nextInterval := time.Duration(-math.Log(1-u)/lambda) * time.Second - - // Clamp to reasonable bounds - if nextInterval < CoverTrafficInterval/2 { - nextInterval = CoverTrafficInterval / 2 - } - if nextInterval > CoverTrafficInterval*2 { - nextInterval = CoverTrafficInterval * 2 - } - - timer := time.NewTimer(nextInterval) - select { - case <-timer.C: - ctg.sendCoverTraffic() - case <-shutdownSignal: - timer.Stop() - return - } - } +type sphinxKeys struct { + headerKey []byte + payloadKey []byte } -func (ctg *CoverTrafficGenerator) sendCoverTraffic() { - // Generate random cover traffic recipient - randomBytes := make([]byte, 16) - if _, err := rand.Read(randomBytes); err != nil { - log.Printf("[COVER] Failed to generate random bytes: %v", err) - return - } - - coverRecipient := fmt.Sprintf("cover-%s@%s", - base64.URLEncoding.EncodeToString(randomBytes[:8]), ctg.serverName) - - // Generate random message data - dataSize := int(32*1024 + cryptoRandInt63n(96*1024)) - coverData := make([]byte, dataSize) - if _, err := rand.Read(coverData); err != nil { - log.Printf("[COVER] Failed to generate cover data: %v", err) - return - } - - envelope := &Envelope{ - MessageFrom: fmt.Sprintf("system@%s", ctg.serverName), - MessageTo: coverRecipient, - MessageData: bytes.NewReader(coverData), - ReceivedAt: time.Now(), - RetryCount: 0, - MessageID: generateMessageID(), - IsCoverTraffic: true, +func deriveSphinxKeys(sharedSecret, ephemeralPublic []byte) *sphinxKeys { + salt := ephemeralPublic + info := []byte("fog-sphinx-v1") + + kdf := hkdf.New(sha256.New, sharedSecret, salt, info) + + keys := &sphinxKeys{ + headerKey: make([]byte, 32), + payloadKey: make([]byte, 32), } - - queueEnvelope(envelope) - stats.IncCoverTraffic() -} -type Server struct { - Name string - Addr string - Handler Handler - MessageIDCache *MessageIDCache - RateLimiter *RateLimiter - MixnetBatcher *MixnetBatcher - CoverTraffic *CoverTrafficGenerator -} + kdf.Read(keys.headerKey) + kdf.Read(keys.payloadKey) -type Handler interface { - ServeSMTP(envelope *Envelope) error + return keys } -func (s *Server) Serve(listener net.Listener) error { - defer listener.Close() - - for { - select { - case <-shutdownSignal: - log.Println("[SERVER] Shutdown signal received, stopping listener") - return nil - default: - } - - // Set deadline to allow periodic checks for shutdown - if tcpListener, ok := listener.(*net.TCPListener); ok { - 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 - } +func encryptAESGCM(key, plaintext []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 + } + + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + return ciphertext, nil +} + +func decryptAESGCM(key, ciphertext []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)) + if err != nil { + return nil, err + } + + toList := []string{} + for _, t := range parsed["to"].([]interface{}) { + toList = append(toList, t.(string)) + } + + 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 +} + +// ============================================================================ +// SPHINX NODE SERVER +// ============================================================================ + +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) + } + + log.Printf("[SPHINX] Node server listening on %s", listenAddr) + + go func() { + defer shutdownWg.Done() + defer listener.Close() + + for { select { case <-shutdownSignal: - return nil + return default: - return err } + + 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) } - - clientAddr := conn.RemoteAddr().String() - clientIP := strings.Split(clientAddr, ":")[0] - - if !s.RateLimiter.Allow(clientIP) { - log.Printf("[SERVER] Rate limit exceeded for %s", sanitizeLogString(clientIP)) - conn.Close() - continue + }() + + 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 + } + + // Apply exponential delay + delay := delayGenerator.Generate() + log.Printf("[SPHINX] Applying delay: %v", delay) + time.Sleep(delay) + + 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) } - - log.Printf("[SERVER] New connection from %s", sanitizeLogString(clientAddr)) - - shutdownWg.Add(1) - go func() { - defer shutdownWg.Done() - s.handleConnection(conn) - }() + } 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) + } + } +} + +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 + + // Decrypt header layer + headerData, err := decryptAESGCM(node.PublicKey, packet.Header) + if err != nil { + return "", nil, nil, fmt.Errorf("header decryption failed: %w", 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) + } + + // Decrypt payload layer + decryptedPayload, err := decryptAESGCM(node.PublicKey, packet.Payload) + if err != nil { + return "", nil, nil, fmt.Errorf("payload decryption failed: %w", err) + } + + // If this is exit node, deserialize envelope + var envelope *Envelope + if hopInfo.NextHop == "EXIT" { + envelope, err = deserializeEnvelope(decryptedPayload) + if err != nil { + return "", nil, nil, fmt.Errorf("envelope deserialization failed: %w", err) + } + } + + // Create next packet + nextPacket := &SphinxPacket{ + Header: headerData, + Payload: decryptedPayload, } + + return hopInfo.NextHop, nextPacket, envelope, nil } -func (s *Server) handleConnection(conn net.Conn) { +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) + } + + conn, err := torDialer.Dial("tcp", node.Address) + if err != nil { + return fmt.Errorf("failed to connect to next hop: %w", err) + } defer conn.Close() - - // Set read/write deadlines (NEW) - conn.SetDeadline(time.Now().Add(5 * time.Minute)) - - smtpConn := &SMTPConn{ - conn: conn, - text: textproto.NewConn(conn), - server: s, - remoteAddr: conn.RemoteAddr().String(), + + encoder := json.NewEncoder(conn) + if err := encoder.Encode(packet); err != nil { + return fmt.Errorf("failed to send packet: %w", err) + } + + log.Printf("[SPHINX] Forwarded packet to %s", node.Address) + return nil +} + +// ============================================================================ +// ROUTE SELECTION +// ============================================================================ + +func SelectSphinxRoute() ([]*PKINode, error) { + healthyNodes := pkiDirectory.GetHealthyNodes(true) + + if len(healthyNodes) < SphinxHopCount { + return nil, fmt.Errorf("not enough healthy nodes: have %d, need %d", len(healthyNodes), SphinxHopCount) } - if err := smtpConn.serve(); err != nil { - log.Printf("[CONN] Connection error: %v", err) + + // 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] } + + return healthyNodes[:SphinxHopCount], nil } -type SMTPConn struct { - conn net.Conn - text *textproto.Conn - server *Server - remoteAddr string - mailFrom string - mailTo []string - data bytes.Buffer - dataSize int64 // Track data size (NEW) - quitSent bool +// ============================================================================ +// SMTP SERVER +// ============================================================================ + +type SMTPServer struct { + name string + addr string + listener net.Listener } -func (c *SMTPConn) resetSession() { - c.mailFrom = "" - c.mailTo = nil - c.data.Reset() - c.dataSize = 0 +func NewSMTPServer(name, addr string) *SMTPServer { + return &SMTPServer{ + name: name, + addr: addr, + } } -func (c *SMTPConn) serve() error { - log.Printf("[SMTP] <- %s: Client connected", sanitizeLogString(c.remoteAddr)) - - if err := c.text.PrintfLine("220 %s %s v%s SMTP Ready", - c.server.Name, AppName, Version); err != nil { - return err +func (s *SMTPServer) Start() error { + listener, err := net.Listen("tcp", s.addr) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", s.addr, err) } - log.Printf("[SMTP] -> %s: 220 %s %s v%s SMTP Ready", - sanitizeLogString(c.remoteAddr), c.server.Name, AppName, Version) - + + s.listener = listener + log.Printf("[SMTP] Server listening on %s", s.addr) + for { - // Check for shutdown select { case <-shutdownSignal: - c.text.PrintfLine("421 Service shutting down") return nil default: } - - // Update deadline for each command - c.conn.SetDeadline(time.Now().Add(5 * time.Minute)) - - line, err := c.text.ReadLine() + + listener.(*net.TCPListener).SetDeadline(time.Now().Add(1 * time.Second)) + + conn, err := listener.Accept() if err != nil { - if err == io.EOF { - log.Printf("[SMTP] <- %s: Client disconnected (EOF)", sanitizeLogString(c.remoteAddr)) - } else { - log.Printf("[SMTP] <- %s: Read error: %v", sanitizeLogString(c.remoteAddr), err) + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + continue } - return err - } - - // Check header size limit (NEW) - if int64(len(line)) > MaxHeaderSize { - log.Printf("[SMTP] -> %s: 552 Header line too long", sanitizeLogString(c.remoteAddr)) - return c.text.PrintfLine("552 Header line too long") - } - - log.Printf("[SMTP] <- %s: %s", sanitizeLogString(c.remoteAddr), sanitizeLogString(line)) - - if err := c.handleCommand(line); err != nil { - return err - } - - if c.quitSent { - return nil + return fmt.Errorf("accept error: %w", err) } + + stats.ConnectionsTotal.Add(1) + stats.ConnectionsActive.Add(1) + + go func() { + defer stats.ConnectionsActive.Add(-1) + s.handleConnection(conn) + }() } } -func (c *SMTPConn) handleCommand(line string) error { - parts := strings.Fields(line) - if len(parts) == 0 { - log.Printf("[SMTP] -> %s: 500 Empty command", sanitizeLogString(c.remoteAddr)) - return c.text.PrintfLine("500 Empty command") +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) + return } - - cmd := strings.ToUpper(parts[0]) - - // Apply random delay for timing attack prevention - minDelay := MinDelay - maxDelay := MaxDelay - delay := time.Duration(cryptoRandInt63n(int64(maxDelay-minDelay))) + minDelay - time.Sleep(delay) - - switch cmd { - case "HELO", "EHLO": - log.Printf("[SMTP] -> %s: 250 %s", sanitizeLogString(c.remoteAddr), c.server.Name) - return c.text.PrintfLine("250 %s", c.server.Name) - - case "MAIL": - if len(parts) < 2 || !strings.HasPrefix(strings.ToUpper(parts[1]), "FROM:") { - log.Printf("[SMTP] -> %s: 501 Syntax: MAIL FROM:
", sanitizeLogString(c.remoteAddr)) - return c.text.PrintfLine("501 Syntax: MAIL FROM:") - } - from := strings.TrimPrefix(parts[1], "FROM:") - from = strings.TrimPrefix(from, "from:") - from = strings.Trim(from, "<>") - - // Validate and sanitize email (NEW) - _, _, err := ValidateEmailAddress(from) - if err != nil { - log.Printf("[SMTP] -> %s: 553 Invalid sender address: %v", - sanitizeLogString(c.remoteAddr), err) - return c.text.PrintfLine("553 Invalid sender address") - } - - c.mailFrom = from - log.Printf("[SMTP] -> %s: 250 OK (sender: %s)", - sanitizeLogString(c.remoteAddr), sanitizeLogString(from)) - return c.text.PrintfLine("250 OK") - - case "RCPT": - if c.mailFrom == "" { - log.Printf("[SMTP] -> %s: 503 Need MAIL command first", sanitizeLogString(c.remoteAddr)) - return c.text.PrintfLine("503 Need MAIL command first") - } - - // Check max recipients (NEW) - if len(c.mailTo) >= MaxRecipients { - log.Printf("[SMTP] -> %s: 452 Too many recipients", sanitizeLogString(c.remoteAddr)) - return c.text.PrintfLine("452 Too many recipients") - } - - if len(parts) < 2 || !strings.HasPrefix(strings.ToUpper(parts[1]), "TO:") { - log.Printf("[SMTP] -> %s: 501 Syntax: RCPT TO:", sanitizeLogString(c.remoteAddr)) - return c.text.PrintfLine("501 Syntax: RCPT TO:") - } - to := strings.TrimPrefix(parts[1], "TO:") - to = strings.TrimPrefix(to, "to:") - to = strings.Trim(to, "<>") - - // Validate recipient - _, domain, err := ValidateEmailAddress(to) - if err != nil { - log.Printf("[SMTP] -> %s: 553 Invalid recipient address: %v", - sanitizeLogString(c.remoteAddr), err) - return c.text.PrintfLine("553 Invalid recipient address") - } - - // Check for email header injection attempts (NEW) - if strings.Contains(to, "\r") || strings.Contains(to, "\n") { - log.Printf("[SMTP] -> %s: 553 Invalid recipient (injection attempt)", - sanitizeLogString(c.remoteAddr)) - return c.text.PrintfLine("553 Invalid recipient") - } - - if !hasMXRecords(domain) { - log.Printf("[SMTP] -> %s: 550 No MX records for domain %s", - sanitizeLogString(c.remoteAddr), sanitizeLogString(domain)) - return c.text.PrintfLine("550 No MX records for domain") - } - - c.mailTo = append(c.mailTo, to) - log.Printf("[SMTP] -> %s: 250 OK (recipient: %s)", - sanitizeLogString(c.remoteAddr), sanitizeLogString(to)) - return c.text.PrintfLine("250 OK") - - case "DATA": - if c.mailFrom == "" || len(c.mailTo) == 0 { - log.Printf("[SMTP] -> %s: 503 Need MAIL and RCPT commands first", - sanitizeLogString(c.remoteAddr)) - return c.text.PrintfLine("503 Need MAIL and RCPT commands first") - } - - log.Printf("[SMTP] -> %s: 354 Start mail input; end with