feat(mail): ING-09 Rate-Limiting auf Protokollebene für IMAP/POP3/SMTP
Neues Paket mail/internal/ratelimit: Token-Bucket je (Mandant, Quelle)-Schlüssel. ConfigProvider/StaticConfig liefern Burst/Nachfüllrate je Mandant mit Fallback auf eine Default-Konfiguration (Akzeptanzkriterium 2/3: begrenzt, konfigurierbar, je Mandant getrennt). Limiter.Allow liefert bei Ablehnung eine konkrete positive Wartezeit statt nur false — Grundlage für die geforderte definierte Ablehnung MIT Wartezeit-Hinweis statt Verbindungsabbruch (Akzeptanzkriterium 3). IMAP LOGIN und POP3 PASS begrenzen Anmeldeversuche pro (Mandant, Quell-IP). SMTP MAIL FROM begrenzt die Annahmerate pro (Mandant, Absenderadresse+Quell-IP). Bei Überschreitung: POP3 -ERR, IMAP NO, SMTP 451 (temporärer Fehlercode) mit Wartezeit in Sekunden, Verbindung bleibt in allen drei Fällen offen und weiter nutzbar. loginLimiter/ acceptLimiter sind optional (nil = deaktiviert, Rückwärtskompatibilität zu ING-01..ING-08) — neue Konstruktoren NewServerWithGuardTLSLoggerAndRateLimit (IMAP/POP3) und NewServerWithMaxMessageBytesTLSLoggerAndRateLimit (SMTP). Jeder Server bekommt eine tenantID, konsistent mit dem in ING-10 etablierten Muster "ein Server je Mandant". Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt, jeweils in IMAP, POP3 und SMTP einzeln: 20 reale Versuche bei Burst=5 -> exakt 5 akzeptiert, 15 mit Wartezeit-Hinweis abgelehnt, keine Verbindung getrennt; legitime Nutzung unterhalb der Schwelle unbeeinträchtigt; zwei Mandanten mit unterschiedlichem Burst über denselben Limiter unabhängig und exakt wirksam (2 vs. 8 akzeptierte Versuche). go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~31 Pakete) regressionsfrei getestet — insbesondere QA-07-Lasttests bleiben grün, da Rate-Limiting standardmäßig deaktiviert ist.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protolog"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/ratelimit"
|
||||
)
|
||||
|
||||
// phaseAuthorization/phaseTransaction sind die protoguard-Phasen dieser
|
||||
@@ -46,29 +47,48 @@ type Session struct {
|
||||
|
||||
log *protolog.SessionLogger // ING-08, nie nil (aber log.Event() ist nil-sicher)
|
||||
|
||||
// tenantID/loginLimiter: Rate-Limiting für PASS-Versuche (ING-09).
|
||||
// loginLimiter nil bedeutet: kein Rate-Limiting (Rückwärtskompatibilität
|
||||
// zu ING-01..ING-08).
|
||||
tenantID string
|
||||
loginLimiter *ratelimit.Limiter
|
||||
|
||||
state State
|
||||
pendingUsername string // nach USER, vor erfolgreichem PASS
|
||||
username string // nach erfolgreichem PASS
|
||||
deleted map[int]bool
|
||||
}
|
||||
|
||||
func newSession(conn net.Conn, auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config, logger *slog.Logger) *Session {
|
||||
func newSession(conn net.Conn, auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config, logger *slog.Logger, tenantID string, loginLimiter *ratelimit.Limiter) *Session {
|
||||
_, alreadyTLS := conn.(*tls.Conn)
|
||||
return &Session{
|
||||
conn: conn,
|
||||
reader: bufio.NewReaderSize(conn, maxCommandLineBytes),
|
||||
writer: bufio.NewWriter(conn),
|
||||
auth: auth,
|
||||
store: store,
|
||||
guard: protoguard.New(guardCfg),
|
||||
tlsConfig: tlsConfig,
|
||||
tlsActive: alreadyTLS,
|
||||
log: protolog.NewSessionLogger(logger, "pop3"),
|
||||
state: Authorization,
|
||||
deleted: map[int]bool{},
|
||||
conn: conn,
|
||||
reader: bufio.NewReaderSize(conn, maxCommandLineBytes),
|
||||
writer: bufio.NewWriter(conn),
|
||||
auth: auth,
|
||||
store: store,
|
||||
guard: protoguard.New(guardCfg),
|
||||
tlsConfig: tlsConfig,
|
||||
tlsActive: alreadyTLS,
|
||||
log: protolog.NewSessionLogger(logger, "pop3"),
|
||||
tenantID: tenantID,
|
||||
loginLimiter: loginLimiter,
|
||||
state: Authorization,
|
||||
deleted: map[int]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
// sourceAddr liefert die Quell-IP dieser Verbindung ohne Portanteil,
|
||||
// für das Rate-Limiting (ING-09) und als Schlüssel gegenüber dem
|
||||
// Limiter stabil pro Client.
|
||||
func (s *Session) sourceAddr() string {
|
||||
host, _, err := net.SplitHostPort(s.conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
return s.conn.RemoteAddr().String()
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// currentPhase liefert die protoguard-Phase des aktuellen Sitzungszustands.
|
||||
func (s *Session) currentPhase() protoguard.Phase {
|
||||
if s.state == Authorization {
|
||||
|
||||
Reference in New Issue
Block a user