fix: sync temp auth key expiry boundaries

This commit is contained in:
A 2026-07-13 23:04:15 +08:00
parent 305e8a0008
commit 20a310f6ca
50 changed files with 3626 additions and 335 deletions

View file

@ -369,6 +369,7 @@ func (s *Server) buildConn(tc transport.Conn, lease *physicalTransportLease, key
msgID: proto.NewMessageIDGen(s.clock.Now),
writeTimeout: s.writeTimeout,
metrics: s.metrics,
now: s.clock.Now,
authKeyID: key.ID,
authKeyHex: hex.EncodeToString(key.ID[:]),
sessionID: sessionID,
@ -770,6 +771,19 @@ func (s *Server) serveConn(ctx context.Context, raw transport.Conn, remote, loca
// 撤销由 SessionManager 主动 Close 连接保证失效,不依赖被动的“下一帧 -404”。
// 尚未进入 SessionManager 的 bad-salt provisional 会在 handleEncrypted 建立 activation claim
// 后精确复查一次,既把撤销与激活线性化,也不把 salt storm 放大成 PG 写风暴。
// temporary key 的绝对 expiry 缓存在 Conn 上,逐帧只做内存比较;到期必须在
// RPC 前返回 -404让官方客户端仅轮换 temp key。绝不能落到 Router 后退化为
// raw business identity再以会触发整账号退出的 401 结束。
if current != nil && current.authKeyID == authKeyID && authKeyProtocolUnavailable(current.authKeyExpiresAt, s.clock.Now()) {
s.log.Info("Rejecting unavailable temporary or legacy auth key",
zap.String("auth_key_id", current.authKeyHex),
zap.Int("expires_at", current.authKeyExpiresAt),
)
if err := s.sendTerminalProtoError(ctx, current, codec.CodeAuthKeyNotFound); err != nil {
return err
}
return nil
}
var fetchedKey *store.AuthKeyData
if current == nil || current.authKeyID != authKeyID {
d, found, err := s.authKeys.Get(ctx, authKeyID)
@ -777,17 +791,35 @@ func (s *Server) serveConn(ctx context.Context, raw transport.Conn, remote, loca
return fmt.Errorf("lookup auth key: %w", err)
}
if !found {
writer := transport.Conn(conn)
var sendErr error
if current != nil {
writer = current.transport
sendErr = s.sendTerminalProtoError(ctx, current, codec.CodeAuthKeyNotFound)
} else {
sendErr = s.sendProtoError(ctx, conn, codec.CodeAuthKeyNotFound)
}
if err := s.sendProtoError(ctx, writer, codec.CodeAuthKeyNotFound); err != nil {
return err
if sendErr != nil {
return sendErr
}
// -404 对 TDesktop 是 terminal key failure继续保留 socket 只会允许
// 同一客户端反复触发 AuthKeyStore 查询。回包一次后立即断开。
return nil
}
if authKeyProtocolUnavailable(d.ExpiresAt, s.clock.Now()) {
s.log.Info("Rejecting unavailable temporary or legacy auth key",
zap.String("auth_key_id", hex.EncodeToString(d.ID[:])),
zap.Int("expires_at", d.ExpiresAt),
)
var sendErr error
if current != nil {
sendErr = s.sendTerminalProtoError(ctx, current, codec.CodeAuthKeyNotFound)
} else {
sendErr = s.sendProtoError(ctx, conn, codec.CodeAuthKeyNotFound)
}
if sendErr != nil {
return sendErr
}
return nil
}
fetchedKey = &d
}
@ -806,6 +838,12 @@ func (s *Server) serveConn(ctx context.Context, raw transport.Conn, remote, loca
}
}
func authKeyProtocolUnavailable(expiresAt int, now time.Time) bool {
// -1 is migration 0086's explicit legacy-unknown sentinel. Reject it once
// instead of guessing permanent and allowing account authorization on a temp key.
return expiresAt < 0 || (expiresAt > 0 && int64(expiresAt) <= now.Unix())
}
// maxRetainedConnBuffer keeps normal upload/download frames allocation-free while preventing one
// exceptional near-16MiB transport frame from pinning that capacity for the lifetime of a long
// connection. RPC bodies that outlive dispatch already own a budgeted Copy.