1
1
Fork 0
mirror of https://github.com/mjl-/mox.git synced 2025-04-21 21:40:01 +03:00

imapserver: try harder to get the user-agent (from the ID command) into the loginattempt

our previous approach was to hope clients did the ID command right after the
AUTHENTICATE command. with more extensions implemented, that's a stretch,
clients are doing other commands in between.

the new approach is to allow more commands, but wait at most 1 second. clients
are still assumed to send the ID command soon after authenticate. we also still
ensure login attempts are logged on connection teardown, so we aren't missing
any logging, just may get it slightly delayed. seems reasonable.

we now also keep the useragent value around, and we use when initializing the
login attempt. because the ID command can happen at any time, also before the
AUTHENTICATE command.
This commit is contained in:
Mechiel Lukkien 2025-02-24 09:38:17 +01:00
parent d27fc1e7fc
commit b56d6c4061
No known key found for this signature in database

View file

@ -207,10 +207,13 @@ type conn struct {
// ../rfc/5182:13 ../rfc/9051:4040
searchResult []store.UID
// Set during authentication, typically picked up by the ID command that
// immediately follows, or will be flushed after any other command after
// authentication instead.
loginAttempt *store.LoginAttempt
// userAgent is set by the ID command, which can happen at any time (before or
// after the authentication attempt we want to log it with).
userAgent string
// loginAttempt is set during authentication, typically picked up by the ID command
// that soon follows, or it will be flushed within 1s, or on connection teardown.
loginAttempt *store.LoginAttempt
loginAttemptTime time.Time
// Only set when connection has been authenticated. These can be set even when
// c.state is stateNotAuthenticated, for TLS client certificate authentication. In
@ -839,25 +842,20 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x
if c.loginAttempt != nil {
store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
c.loginAttempt = nil
c.loginAttemptTime = time.Time{}
}
}()
var storeLoginAttempt bool
for {
c.command()
c.xflush() // For flushing errors, or commands that did not flush explicitly.
// After an authentication command, we will have a c.loginAttempt. We typically get
// an "ID" command with the user-agent immediately after. So we wait for one more
// command after seeing a loginAttempt to gather it.
if storeLoginAttempt {
storeLoginAttempt = false
if c.loginAttempt != nil {
store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
c.loginAttempt = nil
}
} else if c.loginAttempt != nil {
storeLoginAttempt = true
// Flush login attempt if it hasn't already been flushed by an ID command within 1s
// after authentication.
if c.loginAttempt != nil && (c.loginAttempt.UserAgent != "" || time.Since(c.loginAttemptTime) >= time.Second) {
store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
c.loginAttempt = nil
c.loginAttemptTime = time.Time{}
}
}
}
@ -875,6 +873,7 @@ func (c *conn) newLoginAttempt(useTLS bool, authMech string) {
store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
c.loginAttempt = nil
}
c.loginAttemptTime = time.Now()
var state *tls.ConnectionState
if tc, ok := c.conn.(*tls.Conn); ok && useTLS {
@ -889,12 +888,13 @@ func (c *conn) newLoginAttempt(useTLS bool, authMech string) {
}
c.loginAttempt = &store.LoginAttempt{
RemoteIP: c.remoteIP.String(),
LocalIP: localIP,
TLS: store.LoginAttemptTLS(state),
Protocol: "imap",
AuthMech: authMech,
Result: store.AuthError, // Replaced by caller.
RemoteIP: c.remoteIP.String(),
LocalIP: localIP,
TLS: store.LoginAttemptTLS(state),
Protocol: "imap",
UserAgent: c.userAgent, // May still be empty, to be filled in later.
AuthMech: authMech,
Result: store.AuthError, // Replaced by caller.
}
}
@ -1799,12 +1799,15 @@ func (c *conn) cmdID(tag, cmd string, p *parser) {
}
p.xempty()
// The ID command is typically sent immediately after authentication. So we've
// prepared the LoginAttempt and write it now.
c.userAgent = strings.Join(values, " ")
// The ID command is typically sent soon after authentication. So we've prepared
// the LoginAttempt and write it now.
if c.loginAttempt != nil {
c.loginAttempt.UserAgent = strings.Join(values, " ")
c.loginAttempt.UserAgent = c.userAgent
store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
c.loginAttempt = nil
c.loginAttemptTime = time.Time{}
}
// We just log the client id.