diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2025-10-09 13:44:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-09 13:44:59 +0200 |
| commit | 553942f60b0395aa42313401f881b20826dd8dfd (patch) | |
| tree | 522ce31a1e396bc3d2680567e5aef22d92a0b93f | |
| parent | 755aa84e49ab4fe19a8643a7193bf29d051a94ba (diff) | |
| download | fog-553942f60b0395aa42313401f881b20826dd8dfd.tar.gz fog-553942f60b0395aa42313401f881b20826dd8dfd.tar.xz fog-553942f60b0395aa42313401f881b20826dd8dfd.zip | |
Create pluto2.go
| -rw-r--r-- | pluto2.go | 851 |
1 files changed, 851 insertions, 0 deletions
diff --git a/pluto2.go b/pluto2.go new file mode 100644 index 0000000..137c8e0 --- /dev/null +++ b/pluto2.go @@ -0,0 +1,851 @@ +package main + +import ( + "bytes" + "crypto/rand" + "crypto/sha512" + "encoding/base64" + "encoding/binary" + "errors" + "flag" + "fmt" + "io" + "log" + "math" + "math/big" + "net" + "net/smtp" + "net/textproto" + "regexp" + "strings" + "sync" + "time" + + "golang.org/x/net/proxy" +) + +const ( + TorSocksProxyAddr = "127.0.0.1:9050" + RelayWorkerCount = 5 + DeliveryTimeout = 30 * time.Second + MixnetBatchWindow = 30 * time.Second + CoverTrafficInterval = 15 * 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 +) + +var ( + emailRegExp *regexp.Regexp + localPartRegex *regexp.Regexp + domainRegex *regexp.Regexp +) + +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])?)*$`) +} + +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") + } + if !localPartRegex.MatchString(localPart) { + return errors.New("local part contains invalid characters") + } + return nil +} + +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") + } + for _, label := range labels { + if len(label) == 0 || len(label) > 63 { + return errors.New("domain label must be 1-63 characters") + } + } + 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 '@'") + } + 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") + } + if !isOnionDomain(name) { + return errors.New("server name must be a .onion domain") + } + onionName := strings.TrimSuffix(strings.ToLower(name), ".onion") + if len(onionName) != 56 { + return errors.New("server name must be a v3 .onion address (56 characters)") + } + validOnionV3 := regexp.MustCompile(`^[a-z2-7]{56}$`) + if !validOnionV3.MatchString(onionName) { + return errors.New("invalid v3 .onion address format") + } + return nil +} + +func hasMXRecords(domain string) bool { + 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() { + mxRecords, err := net.LookupMX(domain) + hasMX = err == nil && len(mxRecords) > 0 + done <- true + }() + select { + case <-done: + return hasMX + case <-time.After(MXLookupTimeout): + return false + } +} + +type MessageIDCache struct { + cache map[string]time.Time + mu sync.RWMutex +} + +func NewMessageIDCache() *MessageIDCache { + cache := &MessageIDCache{cache: make(map[string]time.Time)} + go cache.cleanupLoop() + return cache +} + +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) +} + +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 +} + +func (c *MessageIDCache) cleanupLoop() { + ticker := time.NewTicker(1 * time.Hour) + defer ticker.Stop() + for range 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() + } +} + +type RateLimiter struct { + requests map[string][]time.Time + mu sync.RWMutex +} + +func NewRateLimiter() *RateLimiter { + limiter := &RateLimiter{requests: make(map[string][]time.Time)} + go limiter.cleanupLoop() + return limiter +} + +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) + } + } + if len(validRequests) >= RateLimitPerIP { + rl.requests[clientIP] = validRequests + return false + } + validRequests = append(validRequests, now) + rl.requests[clientIP] = validRequests + return true +} + +func (rl *RateLimiter) cleanupLoop() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for range ticker.C { + rl.mu.Lock() + now := time.Now() + cutoff := now.Add(-RateLimitWindow) + 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() + } +} + +func ApplyAdaptivePadding(data []byte) []byte { + originalLen := len(data) + targetSize := ((originalLen / PaddingSizeUnit) + 1) * PaddingSizeUnit + if originalLen >= targetSize { + return data + } + padded := make([]byte, targetSize) + copy(padded, data) + paddingLen := targetSize - originalLen + padding := make([]byte, paddingLen) + rand.Read(padding) + copy(padded[originalLen:], padding) + return padded +} + +func RandomDelay() { + delayNs := MinDelay.Nanoseconds() + int64(cryptoRandInt63n(MaxDelay.Nanoseconds()-MinDelay.Nanoseconds())) + time.Sleep(time.Duration(delayNs)) +} + +func cryptoRandInt63n(n int64) int64 { + if n <= 0 { + return 0 + } + max := big.NewInt(n) + result, _ := rand.Int(rand.Reader, max) + return result.Int64() +} + +type MixnetBatcher struct { + batch []*Envelope + batchMu sync.Mutex + outputQueue chan *Envelope + stopChan chan struct{} +} + +func NewMixnetBatcher(outputQueue chan *Envelope) *MixnetBatcher { + batcher := &MixnetBatcher{ + batch: make([]*Envelope, 0), + outputQueue: outputQueue, + stopChan: make(chan struct{}), + } + go batcher.batchLoop() + return batcher +} + +func (mb *MixnetBatcher) Add(envelope *Envelope) { + mb.batchMu.Lock() + mb.batch = append(mb.batch, envelope) + mb.batchMu.Unlock() +} + +func (mb *MixnetBatcher) batchLoop() { + ticker := time.NewTicker(MixnetBatchWindow) + defer ticker.Stop() + for { + select { + case <-ticker.C: + mb.processBatch() + case <-mb.stopChan: + return + } + } +} + +func (mb *MixnetBatcher) processBatch() { + mb.batchMu.Lock() + if len(mb.batch) == 0 { + mb.batchMu.Unlock() + return + } + batch := make([]*Envelope, len(mb.batch)) + copy(batch, mb.batch) + mb.batch = make([]*Envelope, 0) + mb.batchMu.Unlock() + shuffleBatch(batch) + for _, env := range batch { + RandomDelay() + select { + case mb.outputQueue <- env: + default: + } + } +} + +func shuffleBatch(batch []*Envelope) { + for i := len(batch) - 1; i > 0; i-- { + j := int(cryptoRandInt63n(int64(i + 1))) + batch[i], batch[j] = batch[j], batch[i] + } +} + +func (mb *MixnetBatcher) Stop() { + close(mb.stopChan) +} + +type CoverTrafficGenerator struct { + outputQueue chan *Envelope + stopChan chan struct{} +} + +func NewCoverTrafficGenerator(outputQueue chan *Envelope) *CoverTrafficGenerator { + gen := &CoverTrafficGenerator{ + outputQueue: outputQueue, + stopChan: make(chan struct{}), + } + go gen.generateLoop() + return gen +} + +func (ctg *CoverTrafficGenerator) generateLoop() { + ticker := time.NewTicker(CoverTrafficInterval) + defer ticker.Stop() + firstNames := []string{"john", "mary", "david", "sarah", "mike", "emma", "james", "lisa", "robert", "anna"} + lastNames := []string{"smith", "johnson", "williams", "brown", "jones", "garcia", "miller", "davis", "rodriguez", "martinez"} + for { + select { + case <-ticker.C: + firstName := firstNames[cryptoRandInt63n(int64(len(firstNames)))] + lastName := lastNames[cryptoRandInt63n(int64(len(lastNames)))] + randomNum := cryptoRandInt63n(9999) + var dummyAddr string + switch cryptoRandInt63n(3) { + case 0: + dummyAddr = fmt.Sprintf("%s.%s%d@gmail.com", firstName, lastName, randomNum) + case 1: + dummyAddr = fmt.Sprintf("%s%s%d@gmail.com", firstName, lastName, randomNum) + default: + dummyAddr = fmt.Sprintf("%s.%s@gmail.com", firstName, lastName) + } + dummyData := make([]byte, 1024+int(cryptoRandInt63n(2048))) + rand.Read(dummyData) + envelope := &Envelope{ + MessageFrom: fmt.Sprintf("noreply%d@notifications.com", cryptoRandInt63n(999)), + MessageTo: dummyAddr, + MessageData: bytes.NewReader(dummyData), + ReceivedAt: time.Now(), + RetryCount: 0, + IsCoverTraffic: true, + } + select { + case ctg.outputQueue <- envelope: + default: + } + case <-ctg.stopChan: + return + } + } +} + +func (ctg *CoverTrafficGenerator) Stop() { + close(ctg.stopChan) +} + +type Server struct { + Name string + Addr string + Handler Handler + MessageIDCache *MessageIDCache + RateLimiter *RateLimiter + MixnetBatcher *MixnetBatcher + CoverTraffic *CoverTrafficGenerator +} + +type conn struct { + remoteAddr string + server *Server + rwc net.Conn + text *textproto.Conn + fromAgent string + mailFrom string + mailTo []string + mailData *bytes.Buffer + helloReceived bool + quitSent bool + mu sync.Mutex +} + +type Envelope struct { + MessageFrom string + MessageTo string + MessageData io.Reader + ReceivedAt time.Time + RetryCount int + MessageID string + IsCoverTraffic bool +} + +type HandlerFunc func(envelope *Envelope) error + +func (f HandlerFunc) ServeSMTP(envelope *Envelope) error { + return f(envelope) +} + +type Handler interface { + ServeSMTP(envelope *Envelope) error +} + +var ( + mailQueue chan *Envelope + mailQueueMutex sync.Mutex +) + +func (srv *Server) newConn(rwc net.Conn) (*conn, error) { + c := &conn{ + remoteAddr: rwc.RemoteAddr().String(), + server: srv, + rwc: rwc, + text: textproto.NewConn(rwc), + mailTo: make([]string, 0), + } + return c, nil +} + +func (srv *Server) ListenAndServe() error { + if srv.Name == "" { + srv.Name = "localhost" + } + addr := srv.Addr + if addr == "" { + addr = ":2525" + } + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + return srv.Serve(ln) +} + +func (srv *Server) Serve(l net.Listener) error { + defer l.Close() + var tempDelay time.Duration + for { + rw, e := l.Accept() + if e != nil { + if ne, ok := e.(net.Error); ok && ne.Temporary() { + if tempDelay == 0 { + tempDelay = 5 * time.Millisecond + } else { + tempDelay *= 2 + } + if max := 1 * time.Second; tempDelay > max { + tempDelay = max + } + time.Sleep(tempDelay) + continue + } + return e + } + tempDelay = 0 + c, err := srv.newConn(rw) + if err != nil { + continue + } + go c.serve() + } +} + +func (c *conn) serve() { + clientIP := extractIP(c.remoteAddr) + if !c.server.RateLimiter.Allow(clientIP) { + RandomDelay() + c.text.Close() + c.rwc.Close() + return + } + RandomDelay() + err := c.text.PrintfLine("%d %s ESMTP", 220, c.server.Name) + if err != nil { + return + } + for !c.quitSent && err == nil { + err = c.readCommand() + } + c.text.Close() + c.rwc.Close() +} + +func extractIP(addr string) string { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return addr + } + return host +} + +func (c *conn) resetSession() { + c.mailFrom = "" + c.mailTo = make([]string, 0) + c.mailData = 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 SplitAddress(address string) (string, string, error) { + localPart, domain, err := ValidateEmailAddress(address) + if err != nil { + return "", "", err + } + if !isOnionDomain(domain) { + return "", "", errors.New("only .onion domains are allowed for relay") + } + return localPart, domain, nil +} + +func extractDomainFromAddress(address string) string { + sepInd := strings.LastIndex(address, "@") + if sepInd == -1 { + return "" + } + return address[sepInd+1:] +} + +func (c *conn) readCommand() error { + s, err := c.text.ReadLine() + if err != nil { + return err + } + RandomDelay() + parts := strings.Split(s, " ") + if len(parts) <= 0 { + return c.text.PrintfLine("%d %s", 500, "Command not recognized") + } + parts[0] = strings.ToUpper(parts[0]) + switch parts[0] { + case "HELO", "EHLO": + if len(parts) < 2 { + return c.text.PrintfLine("%d %s", 501, "Not enough arguments") + } + c.fromAgent = parts[1] + c.resetSession() + c.helloReceived = true + responses := []string{ + fmt.Sprintf("%d-%s", 250, c.server.Name), + fmt.Sprintf("%d-%s", 250, "PIPELINING"), + fmt.Sprintf("%d %s", 250, "SMTPUTF8"), + } + for i, resp := range responses { + if i == len(responses)-1 { + resp = strings.Replace(resp, "-", " ", 1) + } + if err := c.text.PrintfLine(resp); err != nil { + return err + } + } + return nil + case "MAIL": + if c.mailFrom != "" { + return c.text.PrintfLine("%d %s", 503, "MAIL command already received") + } + if len(parts) < 2 { + return c.text.PrintfLine("%d %s", 501, "Not enough arguments") + } + if !strings.HasPrefix(strings.ToUpper(parts[1]), "FROM:") { + return c.text.PrintfLine("%d %s", 501, "MAIL command must be immediately succeeded by 'FROM:'") + } + from := strings.TrimPrefix(parts[1], "FROM:") + from = strings.TrimPrefix(from, "from:") + from = strings.Trim(from, "<>") + if !emailRegExp.MatchString(from) { + return c.text.PrintfLine("%d %s", 501, "MAIL command contained invalid address") + } + _, _, err := ValidateEmailAddress(from) + if err != nil { + return c.text.PrintfLine("%d %s", 501, "Invalid email address format") + } + c.mailFrom = from + return c.text.PrintfLine("%d %s", 250, "Ok") + case "RCPT": + if c.mailFrom == "" { + return c.text.PrintfLine("%d %s", 503, "Bad sequence of commands") + } + if len(parts) < 2 { + return c.text.PrintfLine("%d %s", 501, "Not enough arguments") + } + if !strings.HasPrefix(strings.ToUpper(parts[1]), "TO:") { + return c.text.PrintfLine("%d %s", 501, "RCPT command must be immediately succeeded by 'TO:'") + } + to := strings.TrimPrefix(parts[1], "TO:") + to = strings.TrimPrefix(to, "to:") + to = strings.Trim(to, "<>") + if !emailRegExp.MatchString(to) { + return c.text.PrintfLine("%d %s", 501, "RCPT command contained invalid address") + } + _, domain, err := ValidateEmailAddress(to) + if err != nil { + return c.text.PrintfLine("%d %s", 501, "Invalid email address format") + } + if !isOnionDomain(domain) { + if !hasMXRecords(domain) { + return c.text.PrintfLine("%d %s", 550, "Mailbox unavailable") + } + } + c.mailTo = append(c.mailTo, to) + return c.text.PrintfLine("%d %s", 250, "Ok") + case "DATA": + if len(c.mailTo) == 0 || c.mailFrom == "" { + return c.text.PrintfLine("%d %s", 503, "Bad sequence of commands") + } + if err := c.text.PrintfLine("%d %s", 354, "End data with <CR><LF>.<CR><LF>"); err != nil { + return err + } + data, err := c.text.ReadDotBytes() + if err != nil { + return err + } + c.mailData = bytes.NewBuffer(data) + messageID := generateMessageID() + if c.server.MessageIDCache.Has(messageID) { + return c.text.PrintfLine("%d %s", 554, "Duplicate message detected") + } + c.server.MessageIDCache.Add(messageID) + for _, recipient := range c.mailTo { + env := &Envelope{ + MessageFrom: c.mailFrom, + MessageTo: recipient, + MessageData: bytes.NewReader(c.mailData.Bytes()), + ReceivedAt: time.Now(), + RetryCount: 0, + MessageID: messageID, + IsCoverTraffic: false, + } + if err := c.server.Handler.ServeSMTP(env); err != nil { + return c.text.PrintfLine("%d %s", 554, "Transaction failed") + } + } + c.resetSession() + return c.text.PrintfLine("%d %s", 250, "OK") + case "RSET": + c.resetSession() + return c.text.PrintfLine("%d %s", 250, "Ok") + case "VRFY", "EXPN", "HELP", "NOOP": + return c.text.PrintfLine("%d %s", 250, "OK") + case "QUIT": + c.quitSent = true + return c.text.PrintfLine("%d %s", 221, "Bye") + default: + return c.text.PrintfLine("%d %s", 500, "Command not recognized") + } +} + +func generateMessageID() string { + randomBytes := make([]byte, 32) + rand.Read(randomBytes) + timestamp := make([]byte, 8) + binary.BigEndian.PutUint64(timestamp, uint64(time.Now().UnixNano())) + combined := append(randomBytes, timestamp...) + h := sha512.Sum512(combined) + return base64.URLEncoding.EncodeToString(h[:32]) +} + +type SimpleHandler struct { + batcher *MixnetBatcher +} + +func (h *SimpleHandler) ServeSMTP(envelope *Envelope) error { + h.batcher.Add(envelope) + return nil +} + +func smtpRelay(envelope *Envelope) error { + if envelope.IsCoverTraffic { + return nil + } + _, domain, err := SplitAddress(envelope.MessageTo) + if err != nil { + return fmt.Errorf("invalid recipient address: %w", err) + } + targetAddr := net.JoinHostPort(domain, "25") + jitter := time.Duration(cryptoRandInt63n(int64(5 * time.Second))) + time.Sleep(jitter) + dialer := &net.Dialer{Timeout: DeliveryTimeout} + torDialer, err := proxy.SOCKS5("tcp", TorSocksProxyAddr, nil, dialer) + if err != nil { + return fmt.Errorf("failed to create Tor dialer: %w", err) + } + conn, err := torDialer.Dial("tcp", targetAddr) + if err != nil { + return fmt.Errorf("failed to connect to relay target: %w", err) + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(DeliveryTimeout)) + client, err := smtp.NewClient(conn, domain) + if err != nil { + return fmt.Errorf("failed to create SMTP client: %w", err) + } + defer client.Close() + if err := client.Mail(envelope.MessageFrom); err != nil { + return fmt.Errorf("MAIL FROM failed: %w", err) + } + if err := client.Rcpt(envelope.MessageTo); err != nil { + return fmt.Errorf("RCPT TO failed: %w", err) + } + wc, err := client.Data() + if err != nil { + return fmt.Errorf("DATA command failed: %w", err) + } + defer wc.Close() + if _, err := io.Copy(wc, envelope.MessageData); err != nil { + return fmt.Errorf("message transfer failed: %w", err) + } + return nil +} + +func queueEnvelope(envelope *Envelope) bool { + mailQueueMutex.Lock() + defer mailQueueMutex.Unlock() + select { + case mailQueue <- envelope: + return true + default: + return false + } +} + +func StartRelayWorkers(queue chan *Envelope, workerCount int) { + for i := 0; i < workerCount; i++ { + go func(id int) { + log.Printf("Relay worker %d started", id) + for env := range queue { + err := smtpRelay(env) + if err != nil { + domain := extractDomainFromAddress(env.MessageTo) + if isOnionDomain(domain) && env.RetryCount < 9 { + env.RetryCount++ + backoff := time.Duration(math.Pow(2, float64(env.RetryCount))) * time.Second + jitter := time.Duration(cryptoRandInt63n(int64(backoff / 2))) + time.Sleep(backoff + jitter) + queueEnvelope(env) + } + } + } + }(i) + } +} + +func main() { + listenAddr := flag.String("addr", "127.0.0.1:2525", "Listen address") + serverName := flag.String("name", "abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuv.onion", "Server v3 .onion hostname") + flag.Parse() + if err := validateServerName(*serverName); err != nil { + log.Fatalf("Invalid server name: %v", err) + } + mailQueue = make(chan *Envelope, 100) + StartRelayWorkers(mailQueue, RelayWorkerCount) + messageIDCache := NewMessageIDCache() + rateLimiter := NewRateLimiter() + mixnetBatcher := NewMixnetBatcher(mailQueue) + coverTraffic := NewCoverTrafficGenerator(mailQueue) + server := &Server{ + Name: *serverName, + Addr: *listenAddr, + MessageIDCache: messageIDCache, + RateLimiter: rateLimiter, + MixnetBatcher: mixnetBatcher, + CoverTraffic: coverTraffic, + } + handler := &SimpleHandler{batcher: mixnetBatcher} + server.Handler = handler + log.Printf("Pluto2 SMTP Relay Server") + log.Printf("========================") + log.Printf("Server: %s", server.Name) + log.Printf("Listen: %s", server.Addr) + log.Printf("Tor proxy: %s", TorSocksProxyAddr) + log.Printf("") + log.Printf("Security: Replay protection, Rate limiting, Timing attacks, Size correlation, Mixnet batching, Cover traffic") + log.Printf("Validation: RFC-compliant email, v3 .onion server only") + log.Printf("Policy: FROM any valid, TO .onion + clearnet(MX), RELAY .onion only") + log.Printf("") + listener, err := net.Listen("tcp", server.Addr) + if err != nil { + log.Fatalf("Failed to create listener: %v", err) + } + if err := server.Serve(listener); err != nil { + log.Fatalf("Server failed: %v", err) + } +} |
