chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -13,10 +13,30 @@ import (
"github.com/gotd/td/bin"
"github.com/gotd/td/clock"
"github.com/gotd/td/tg"
"telesrv/internal/compat/layerwire"
"telesrv/internal/domain"
"telesrv/internal/observability/dbtrace"
)
// maxWrapperDepth 限制 invokeWithLayer/initConnection 等 wrapper 的嵌套深度,防御恶意构造。
const maxWrapperDepth = 4
// maxWrapperDepth 限制 invokeWithLayer/initConnection/invokeAfter 等 wrapper 的嵌套深度,防御恶意构造。
// 合法客户端的最深包装来自 gotd 数据连接模式的握手初始化:
// invokeWithLayer → invokeWithoutUpdates → initConnection → invokeWithoutUpdates → query深度 5
// 官方服务器同样接受。取 8 留余量,仍是小常量、不削弱对无界嵌套的防御。
const maxWrapperDepth = 8
const maxInvokeAfterMsgIDs = 128
// tempResolveResult 是 effectiveAuthKeyID 里 temp→perm 解析经 singleflight 共享的结果。
type tempResolveResult struct {
perm [8]byte
ok bool
}
const (
authKeyResolveSingleflightPrefix = "resolve:"
authClientInfoSingleflightPrefix = "authinfo:"
)
var (
tlTypeNamesOnce sync.Once
@ -28,26 +48,91 @@ type Config struct {
DC int
IP string // 对外公布的 DC IP写入 DCOptions
Port int // 对外公布的 DC 端口
InstanceID string // 进程内唯一标识,用于跨实例 ephemeral push 去重。
OutboundPushTimeout time.Duration
SendRateLimit int
SendRateWindow time.Duration
// CatchupRateLimit/CatchupRateWindow 限制 difference 类 catch-up RPCgetChannelDifference /
// getPeerDialogs的每用户频率设计 Phase 2 / §10.3nudge 被消费后客户端会触发这两类
// catch-up放开大群 nudge 全速前需 FLOOD_WAIT 兜底防风暴打爆 PG。两类各自独立计数、共用同一
// 阈值。<=0 关闭(默认行为不变)。
CatchupRateLimit int
CatchupRateWindow time.Duration
// ChannelNudgeMaxTargets 是一次 fan-out 的 >cap nudge 目标上限(设计 Phase 0b 限速兜底);
// <=0 用内置默认 defaultChannelNudgeMaxTargets。
ChannelNudgeMaxTargets int
// CallSignalingMaxBytes 是 phone.sendSignalingData 单条载荷上限;<=0 不限制。
CallSignalingMaxBytes int
// CallForceRelay 强制私聊通话 p2p_allowed=false调试 TURN 中继路径)。
CallForceRelay bool
// GroupCallMaxParticipants 是群通话单房间参与者上限;<=0 不限制。
GroupCallMaxParticipants int
// TempKeyResolveCacheTTL 是 PFS temp→perm auth key 解析的进程内缓存有效期。>0 时同一 temp key
// 在 TTL 内复用上次解析、跳过每帧 ResolveAuthKey 的 PG 查询0默认/测试)关闭=每帧重校验。
// 显式撤销会删除协议 auth key、清缓存并断开活跃连接TTL 只影响自然过期或异常路径下的
// 下一次重新解析。re-bind 由 onAuthBindTempAuthKey 显式失效,避免跨账号串号。
TempKeyResolveCacheTTL time.Duration
// TempKeyResolveCacheMaxEntries 是 temp→perm 解析缓存容量;<=0 用内置默认。
TempKeyResolveCacheMaxEntries int
}
// Router 把解密后的 RPC 请求按 TypeID 路由到 typed handlertg.ServerDispatcher
//
// handler 输入输出均为 gotd/td/tg 类型,各业务域的 handler
// 与注册见 help.go / auth.go / users.go / updates.go。Router 本身只负责协议外壳:
// 剥离 invokeWithLayer / initConnection / invokeWithoutUpdates并兜底未注册 RPC。
// 剥离 invokeWithLayer / initConnection / invokeWithoutUpdates / invokeAfter*,并兜底未注册 RPC。
type Router struct {
cfg Config
log *zap.Logger
clock clock.Clock
deps Deps
dispatcher *tg.ServerDispatcher
clientInfoMu sync.RWMutex
clientInfo map[clientInfoSessionKey]ClientInfo
authUserMu sync.RWMutex
authUsers map[[8]byte]authUserCacheEntry
authUserSF singleflight.Group
presence *presenceTracker
cfg Config
log *zap.Logger
clock clock.Clock
deps Deps
dispatcher *tg.ServerDispatcher
clientInfoMu sync.RWMutex
clientInfo map[clientInfoSessionKey]clientSessionInfo
authInfo map[[8]byte]clientSessionInfo
authUserMu sync.RWMutex
authUsers map[[8]byte]authUserCacheEntry
authUserSF singleflight.Group
mediaCountSF singleflight.Group
dialogsPinnedSF singleflight.Group
channelFullBotSF singleflight.Group
presence *presenceTracker
callbacks *callbackRegistry
inlines *inlineRegistry
webviews *webViewRegistry
loginTokens *loginTokenRegistry
instanceID string
channelFanout *channelFanoutDispatcher
// presenceCandidateCache 缓存 presence fan-out 的候选 peer 集合(联系人 私聊对端,
// online 过滤前),按 userID 短 TTL零值 sync.Map 即可用,无需构造器初始化。候选集变动
// 很慢(加好友/开新私聊),短 TTL 内复用避免 updateStatus 每次重跑 ~25-30 条 hydration 查询。
presenceCandidateCache sync.Map // userID(int64) -> *presenceCandidateEntry
// botStatus 永久缓存 userID->是否 bot。bot 标志按账号不可变BotFather 注册即定,普通用户永不变 bot
// 故可无 TTL 缓存。userIsBot 在 PFS 连接上被 announceSessionOnline 每 RPC 调用,不缓存则每次一发
// Users.ByID 重投影——开群洪峰 ~50 并发时退化成 ~300ms herd既拖尾延迟也飙 PG CPU。零值即可用。
botStatus sync.Map // userID(int64) -> bool
// lastSeenPersist 记录每个 user 最近一次 last_seen 落库时刻unix用于写去抖
// updateStatus 高频续期时数秒内只落一次 DB。
lastSeenPersist sync.Map // userID(int64) -> int64(unix)
// tempKeyResolveCache 缓存 rawTempKeyID -> resolved perm带过期容量有界。
tempKeyResolveCache *tempKeyResolveCache
storyProjectionCache *storyProjectionCache
storyPinnedCache *storyPinnedAvailableCache
storyPinnedListCache *storyPinnedStoriesCache
channelFullBotCache *channelFullBotInfoCache
userFullProjectionCache *userFullProjectionCache
peerSettingsProjectionCache *peerSettingsProjectionCache
channelFullProjectionCache *channelFullProjectionCache
emojiStickers *emojiStickerIndex
notifySettings *notifySettingsCache
stickerCatalog *stickerCatalogCache
accountSettings *accountSettingsCache
// webPageResolveSem 是链接预览异步解析的并发信号量(有界):发送后把 pending 占位
// 解析为卡片并就地替换。满则丢弃任务(消息留 pending。nil=未启用(测试可直接调
// resolvePendingWebPage 同步验证)。
webPageResolveSem chan struct{}
}
type clientInfoSessionKey struct {
@ -55,6 +140,13 @@ type clientInfoSessionKey struct {
sessionID int64
}
type clientSessionInfo struct {
layer int
clientInfo ClientInfo
hasClientInfo bool
authorizationChecked bool
}
type authUserCacheEntry struct {
userID int64
found bool
@ -62,7 +154,13 @@ type authUserCacheEntry struct {
// New 创建 Router由各业务域自行注册其 RPC handlerregisterHelp/Auth/Users/Updates
func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
r := &Router{cfg: cfg, log: log, clock: clk, deps: deps, presence: newPresenceTracker()}
instanceID := cfg.InstanceID
if instanceID == "" {
instanceID = fmt.Sprintf("%016x", randomNonZeroInt64())
}
r := &Router{cfg: cfg, log: log, clock: clk, deps: deps, presence: newPresenceTracker(), callbacks: newCallbackRegistry(), inlines: newInlineRegistry(botInlineQueryTTL, deps.Inline), webviews: newWebViewRegistry(webViewSessionTTL, deps.Inline), loginTokens: newLoginTokenRegistry(), tempKeyResolveCache: newTempKeyResolveCache(cfg.TempKeyResolveCacheMaxEntries), storyProjectionCache: newStoryProjectionCache(clk.Now), storyPinnedCache: newStoryPinnedAvailableCache(clk.Now), storyPinnedListCache: newStoryPinnedStoriesCache(clk.Now), channelFullBotCache: newChannelFullBotInfoCache(clk.Now), userFullProjectionCache: newUserFullProjectionCache(clk.Now), peerSettingsProjectionCache: newPeerSettingsProjectionCache(clk.Now), channelFullProjectionCache: newChannelFullProjectionCache(clk.Now), emojiStickers: newEmojiStickerIndex(clk.Now), notifySettings: newNotifySettingsCache(clk.Now), stickerCatalog: newStickerCatalogCache(clk.Now), accountSettings: newAccountSettingsCache(clk.Now), instanceID: instanceID}
r.channelFanout = newChannelFanoutDispatcher(r, defaultChannelFanoutShards, defaultChannelFanoutBuffer)
r.webPageResolveSem = make(chan struct{}, webPageResolveConcurrency)
d := tg.NewServerDispatcher(r.fallback)
r.registerHelp(d)
@ -78,24 +176,29 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
r.registerContacts(d)
r.registerLangpack(d)
r.registerStories(d)
r.registerPhone(d)
r.registerEncrypted(d)
r.registerPayments(d)
r.registerStats(d)
r.registerPremium(d)
r.registerAiCompose(d)
r.registerBots(d)
r.dispatcher = d
return r
}
// Dispatch 路由一条 RPC 请求:先剥离 invokeWithLayer / initConnection /
// invokeWithoutUpdates 等 wrapper注入 layer / 客户端信息到 ctx
// invokeWithoutUpdates / invokeAfter* 等 wrapper注入 layer / 客户端信息到 ctx
// 再按 TypeID 路由到 typed handler。满足 mtprotoedge.RPCHandler。
func (r *Router) Dispatch(ctx context.Context, authKeyID [8]byte, sessionID int64, b *bin.Buffer) (bin.Encoder, error) {
preStart := r.clock.Now()
ctx = WithRawAuthKeyID(ctx, authKeyID)
effectiveAuthKeyID, err := r.effectiveAuthKeyID(ctx, authKeyID, sessionID)
if err != nil {
return nil, internalErr()
}
tAuth := r.clock.Now()
ctx = WithAuthKeyID(ctx, effectiveAuthKeyID)
ctx = WithSessionID(ctx, sessionID)
userID, hasUserID, err := r.effectiveUserID(ctx, authKeyID, effectiveAuthKeyID, sessionID)
@ -105,22 +208,105 @@ func (r *Router) Dispatch(ctx context.Context, authKeyID [8]byte, sessionID int6
if hasUserID {
ctx = WithUserID(ctx, userID)
}
if info, ok := r.clientInfoForSession(ctx); ok {
ctx = WithClientInfo(ctx, info)
tUser := r.clock.Now()
info, hasClientMetadata := r.clientSessionInfo(ctx)
if hasUserID {
if authInfo, ok := r.clientSessionInfoFromAuthorization(ctx, userID, effectiveAuthKeyID, info); ok {
info = mergeClientSessionInfo(info, authInfo)
hasClientMetadata = true
r.rememberClientSessionInfo(ctx, info)
}
}
// 前置鉴权阶段auth key 解析 / user 重校验 / client info慢路径告警超阈值才记避免刷屏。
// 常驻观测——正常应 ≪50ms再现 >50ms 时按 auth_resolve / user_resolve / client_info 三段拆分
// 即可定位(历史上 client_info ~1s 的根因是 DSN 用 localhost 触发 IPv6 连接回退,已改 127.0.0.1)。
if r.log != nil {
if tInfo := r.clock.Now(); tInfo.Sub(preStart) > 50*time.Millisecond {
id, _ := b.PeekID()
r.log.Info("slow pre-handler",
zap.String("method", tlTypeName(id)),
zap.Duration("pre_total", tInfo.Sub(preStart)),
zap.Duration("auth_resolve", tAuth.Sub(preStart)),
zap.Duration("user_resolve", tUser.Sub(tAuth)),
zap.Duration("client_info", tInfo.Sub(tUser)),
zap.Int64("session_id", sessionID),
)
}
}
if hasClientMetadata {
if info.layer != 0 {
ctx = WithLayer(ctx, info.layer)
}
if info.hasClientInfo {
ctx = WithClientInfo(ctx, info.clientInfo)
}
}
return r.dispatch(ctx, b, 0)
}
func (r *Router) effectiveAuthKeyID(ctx context.Context, rawAuthKeyID [8]byte, sessionID int64) ([8]byte, error) {
var (
cached [8]byte
hasCached bool
)
if r.deps.Sessions != nil {
if scoped, ok := r.deps.Sessions.(ScopedSessionBinder); ok {
if id, ok := scoped.AuthKeyIDForSession(rawAuthKeyID, sessionID); ok {
return id, nil
cached = id
hasCached = true
}
} else if id, ok := r.deps.Sessions.AuthKeyID(sessionID); ok {
return id, nil
cached = id
hasCached = true
}
}
if hasCached {
if cached == rawAuthKeyID || r.deps.Auth == nil {
return cached, nil
}
// temp→perm 解析缓存PFS 连接每帧都要解析一次 temp keyResolveAuthKey 打 PG。TTL 内复用
// 上次解析、跳过 DB。仅当缓存的 perm 仍等于 session binder 当前 perm 才用rebind 会改 binder
// 且 onAuthBindTempAuthKey / 授权撤销都会显式 Delete 缓存,双保险防跨账号串号和被踢滞后)。
ttl := r.cfg.TempKeyResolveCacheTTL
if ttl > 0 {
if perm, ok := r.tempKeyResolveCache.Get(rawAuthKeyID, cached, r.clock.Now()); ok {
return perm, nil
}
}
// cold burst 下并发 temp-key 解析用 singleflight 合并:同一 temp key 的 N 个并发 RPC 只打
// 1 次 PG ResolveAuthKey其余共享避免开群/重连首帧 ~50 并发 herd曾让 auth_resolve 飙到
// ~1s。解析结果 + 缓存写入在 SF 内幂等共享session 绑定仍每 caller 各自做(按 session
// 顺序调用不合并SF 仅合并真并发),「每帧重校验」语义与固化测试 resolveCount 不变。
v, sfErr, _ := r.authUserSF.Do(authKeyResolveSingleflightPrefix+string(rawAuthKeyID[:]), func() (any, error) {
if ttl > 0 {
if perm, ok := r.tempKeyResolveCache.Get(rawAuthKeyID, cached, r.clock.Now()); ok {
return tempResolveResult{perm: perm, ok: true}, nil
}
}
resolved, ok, err := r.deps.Auth.ResolveAuthKey(ctx, rawAuthKeyID)
if err != nil {
return tempResolveResult{}, err
}
if ok && ttl > 0 {
r.tempKeyResolveCache.Store(rawAuthKeyID, resolved, r.clock.Now().Add(ttl), r.clock.Now())
}
return tempResolveResult{perm: resolved, ok: ok}, nil
})
if sfErr != nil {
return [8]byte{}, sfErr
}
out := v.(tempResolveResult)
if out.ok {
if out.perm != cached {
r.bindEffectiveAuthKey(rawAuthKeyID, sessionID, out.perm)
}
return out.perm, nil
}
r.tempKeyResolveCache.Delete(rawAuthKeyID)
r.invalidateAuthUserCache(cached)
r.bindEffectiveAuthKey(rawAuthKeyID, sessionID, rawAuthKeyID)
return rawAuthKeyID, nil
}
effective := rawAuthKeyID
if r.deps.Auth != nil {
resolved, ok, err := r.deps.Auth.ResolveAuthKey(ctx, rawAuthKeyID)
@ -131,6 +317,11 @@ func (r *Router) effectiveAuthKeyID(ctx context.Context, rawAuthKeyID [8]byte, s
effective = resolved
}
}
r.bindEffectiveAuthKey(rawAuthKeyID, sessionID, effective)
return effective, nil
}
func (r *Router) bindEffectiveAuthKey(rawAuthKeyID [8]byte, sessionID int64, effective [8]byte) {
if r.deps.Sessions != nil {
if scoped, ok := r.deps.Sessions.(ScopedSessionBinder); ok {
scoped.BindAuthKeyForSession(rawAuthKeyID, sessionID, effective)
@ -138,7 +329,6 @@ func (r *Router) effectiveAuthKeyID(ctx context.Context, rawAuthKeyID [8]byte, s
r.deps.Sessions.BindAuthKey(sessionID, effective)
}
}
return effective, nil
}
func (r *Router) effectiveUserID(ctx context.Context, rawAuthKeyID, authKeyID [8]byte, sessionID int64) (int64, bool, error) {
@ -153,26 +343,51 @@ func (r *Router) effectiveUserID(ctx context.Context, rawAuthKeyID, authKeyID [8
if r.deps.Sessions != nil {
if scoped, ok := r.deps.Sessions.(ScopedSessionBinder); ok {
if userID, resolved := scoped.UserIDResolvedForAuthKey(rawAuthKeyID, sessionID); resolved {
if userID == 0 {
if cachedUserID, ok := r.positiveCachedAuthUser(authKeyID); ok {
scoped.BindUserForAuthKey(rawAuthKeyID, sessionID, cachedUserID)
r.announceSessionOnline(ctx, cachedUserID)
return cachedUserID, true, nil
}
}
return userID, userID != 0, nil
}
} else if userID, resolved := r.deps.Sessions.UserIDResolved(sessionID); resolved {
if userID == 0 {
if cachedUserID, ok := r.positiveCachedAuthUser(authKeyID); ok {
r.deps.Sessions.BindUser(sessionID, cachedUserID)
r.announceSessionOnline(ctx, cachedUserID)
return cachedUserID, true, nil
}
}
return userID, userID != 0, nil
}
}
if r.deps.Auth == nil {
return 0, false, nil
}
userID, found, err := r.lookupAuthUser(ctx, authKeyID)
var (
userID int64
found bool
err error
)
userID, found, err = r.lookupAuthUser(ctx, authKeyID)
if err != nil {
return 0, false, err
}
if r.deps.Sessions != nil {
if scoped, ok := r.deps.Sessions.(ScopedSessionBinder); ok {
if cachedUserID, resolved := scoped.UserIDResolvedForAuthKey(rawAuthKeyID, sessionID); resolved {
return cachedUserID, cachedUserID != 0, nil
if cachedUserID != 0 || !found {
return cachedUserID, cachedUserID != 0, nil
}
}
} else {
if cachedUserID, resolved := r.deps.Sessions.UserIDResolved(sessionID); resolved {
if cachedUserID != 0 || !found {
return cachedUserID, cachedUserID != 0, nil
}
}
} else if cachedUserID, resolved := r.deps.Sessions.UserIDResolved(sessionID); resolved {
return cachedUserID, cachedUserID != 0, nil
}
if found {
if scoped, ok := r.deps.Sessions.(ScopedSessionBinder); ok {
@ -225,12 +440,23 @@ func (r *Router) cachedAuthUser(authKeyID [8]byte) (int64, bool, bool) {
return entry.userID, entry.found, true
}
func (r *Router) positiveCachedAuthUser(authKeyID [8]byte) (int64, bool) {
userID, found, ok := r.cachedAuthUser(authKeyID)
if !ok || !found || userID == 0 {
return 0, false
}
return userID, true
}
func (r *Router) setAuthUserCache(authKeyID [8]byte, userID int64, found bool) {
r.authUserMu.Lock()
defer r.authUserMu.Unlock()
if r.authUsers == nil {
r.authUsers = make(map[[8]byte]authUserCacheEntry)
}
if _, exists := r.authUsers[authKeyID]; !exists {
evictMapEntryIfFullLocked(r.authUsers, maxAuthUsersCached)
}
r.authUsers[authKeyID] = authUserCacheEntry{userID: userID, found: found}
}
@ -238,7 +464,13 @@ func (r *Router) invalidateAuthUserCache(authKeyID [8]byte) {
r.authUserMu.Lock()
delete(r.authUsers, authKeyID)
r.authUserMu.Unlock()
r.authUserSF.Forget(string(authKeyID[:]))
r.clientInfoMu.Lock()
delete(r.authInfo, authKeyID)
r.clientInfoMu.Unlock()
key := string(authKeyID[:])
r.authUserSF.Forget(key)
r.authUserSF.Forget(authKeyResolveSingleflightPrefix + key)
r.authUserSF.Forget(authClientInfoSingleflightPrefix + key)
}
func (r *Router) scopedSessions() (ScopedSessionBinder, bool) {
@ -269,12 +501,41 @@ func (r *Router) dispatch(ctx context.Context, b *bin.Buffer, depth int) (bin.En
return nil, fmt.Errorf("decode invokeWithLayer layer: %w", err)
}
// query 紧跟 layerbuffer 剩余即内层请求。
return r.dispatch(WithLayer(ctx, layer), b, depth+1)
ctx = WithLayer(ctx, layer)
r.rememberClientLayer(ctx, layer)
return r.dispatch(ctx, b, depth+1)
case tg.InvokeWithoutUpdatesRequestTypeID:
if err := b.ConsumeID(id); err != nil {
return nil, err
}
return r.dispatch(withInvokeWithoutUpdates(ctx), b, depth+1)
case tg.InvokeAfterMsgRequestTypeID:
if err := b.ConsumeID(id); err != nil {
return nil, err
}
if _, err := b.Long(); err != nil {
return nil, fmt.Errorf("decode invokeAfterMsg msg_id: %w", err)
}
return r.dispatch(ctx, b, depth+1)
case tg.InvokeAfterMsgsRequestTypeID:
if err := b.ConsumeID(id); err != nil {
return nil, err
}
msgIDs, err := b.VectorHeader()
if err != nil {
return nil, fmt.Errorf("decode invokeAfterMsgs msg_ids: %w", err)
}
if msgIDs > maxInvokeAfterMsgIDs {
return nil, fmt.Errorf("decode invokeAfterMsgs msg_ids: too many ids %d", msgIDs)
}
for i := 0; i < msgIDs; i++ {
if _, err := b.Long(); err != nil {
return nil, fmt.Errorf("decode invokeAfterMsgs msg_ids[%d]: %w", i, err)
}
}
return r.dispatch(ctx, b, depth+1)
case tg.InitConnectionRequestTypeID:
@ -296,8 +557,9 @@ func (r *Router) dispatch(ctx context.Context, b *bin.Buffer, depth int) (bin.En
r.log.Debug("initConnection",
zap.Int("api_id", req.APIID),
zap.String("device", req.DeviceModel),
zap.String("app", req.AppVersion),
zap.String("app_version", req.AppVersion),
zap.Int("layer", LayerFrom(ctx)),
zap.String("client_type", string(ClientTypeFrom(ctx))),
)
inner, ok := req.Query.(*rawObject)
if !ok {
@ -306,17 +568,57 @@ func (r *Router) dispatch(ctx context.Context, b *bin.Buffer, depth int) (bin.En
return r.dispatch(ctx, &bin.Buffer{Buf: inner.data}, depth+1)
default:
if enc, ok, err := r.dispatchCompat(ctx, b, id); ok {
return enc, err
// 入站兼容统一入口(先于鉴权门/dispatcherlayerwire 把老客户端请求升级为
// canonical(227) 形态——①官方层漂移生成表flag-gated 新增→换 4 字节 id
// ②客户端构造器漂移client_aliases 纯换 id / client-drift.tl 通用 body 变换:
// 插 flags、按 kind 补默认、类型转换、改名映射)。替代了原先散落在 rpc 的
// dispatchCompat + 各 handleLegacy* 解码器。提前到鉴权门之前,使后续一切只面对 227
// 形态鉴权白名单、dispatcher 均无需再认旧构造器 id
if id != 0 {
clientDrift := layerwire.IsClientDrift(id)
if up, ok, err := layerwire.UpgradeInbound(id, b); ok {
if err != nil {
return nil, err
}
b = up
newID, err := b.PeekID()
if err != nil {
return nil, err
}
id = newID
if clientDrift {
// 客户端漂移多来自未完整 initConnection 的 DrKLO按既有行为在
// 类型/层未知时兜底为 androidwithAndroidCompatMetadata 自带 unknown 守卫)。
ctx = r.withAndroidCompatMetadata(ctx)
}
}
}
if r.deps.Auth != nil {
if _, ok := UserIDFrom(ctx); !ok && !rpcAllowedWithoutAuthorization(id) {
fields := append([]zap.Field{
zap.String("method", tlTypeName(id)),
zap.String("type_id", fmt.Sprintf("%#x", id)),
}, r.contextLogFields(ctx)...)
r.log.Info("RPC rejected before authorization", fields...)
return nil, authKeyUnregisteredErr()
}
}
// 任何未包 invokeWithoutUpdates 的已登录 RPC 都把当前 session 视为 updates
// 接收者。仅靠 updates.getState/getDifference 置位会漏掉 DrKLO 热恢复:
// 它重连后不重建同步基线pts 在进程内存里),只发普通业务请求,置位
// 永不发生时主动推送会一直暂存直至超时丢弃,表现为另一端消息不再实时同步。
r.maybeMarkSessionReceivesUpdates(ctx)
dbBefore := dbtrace.SnapshotFromContext(ctx)
start := time.Now()
enc, err := r.dispatcher.Handle(ctx, b)
dur := time.Since(start)
dbDelta := dbtrace.SnapshotFromContext(ctx).Sub(dbBefore)
fields := append([]zap.Field{
zap.String("method", tlTypeName(id)),
zap.String("type_id", fmt.Sprintf("%#x", id)),
zap.Duration("dur", dur),
}, r.contextLogFields(ctx)...)
fields = dbtrace.AppendZapFields(fields, "handler_", dbDelta)
if err != nil || dur > 100*time.Millisecond {
if err != nil {
fields = append(fields, zap.Error(err))
@ -343,7 +645,60 @@ func tlTypeName(id uint32) string {
return fmt.Sprintf("%#x", id)
}
// maxClientInfoEntries / maxAuthInfoEntries 是客户端元数据缓存的容量上限兜底。
// 条目含客户端可控字符串且 session_id 由客户端任意生成,无上限时恶意客户端
// 在单连接上反复换 session_id / 轮换 temp auth key 可线速膨胀直至 OOM。
// 达到上限后驱逐任意旧条目:受害条目只损失 layer/clientType 缓存,
// 下一次 initConnection 或 authorization 回填即恢复。
const (
maxClientInfoEntries = 1 << 16
maxAuthInfoEntries = 1 << 16
// maxAuthUsersCached 给 authUsers 授权缓存设容量上界,与 clientInfo/authInfo 一致。
// 原本无任何上限:设备轮换 temp 键而不显式登出时每个新 authKeyID 永久累积一条,
// 只靠 logout/reset 的显式 invalidate 清理。达上限驱逐任意旧条目,下次按需回查回填。
maxAuthUsersCached = 1 << 16
)
func (r *Router) rememberClientInfo(ctx context.Context, info ClientInfo) {
info = normalizeClientInfo(info)
layer := LayerFrom(ctx)
r.mutateClientSessionInfo(ctx, func(sessionInfo *clientSessionInfo) {
sessionInfo.clientInfo = info
sessionInfo.hasClientInfo = true
if layer != 0 {
sessionInfo.layer = layer
}
})
}
func (r *Router) rememberClientLayer(ctx context.Context, layer int) {
r.mutateClientSessionInfo(ctx, func(sessionInfo *clientSessionInfo) {
sessionInfo.layer = layer
})
}
// NegotiatedLayer returns the TL layer the given session negotiated via
// invokeWithLayer/initConnection. It is keyed first by (auth_key, session) then
// falls back to the stable auth_key — so a reconnect with a new session_id still
// inherits the layer within the process lifetime. ok=false means no layer was
// ever observed (cold connection, or the in-memory entry was evicted): callers
// MUST NOT overwrite a connection's last-known layer in that case, only treat it
// as canonical (227) when no value was ever recorded.
func (r *Router) NegotiatedLayer(authKeyID [8]byte, sessionID int64) (int, bool) {
r.clientInfoMu.RLock()
defer r.clientInfoMu.RUnlock()
if info, ok := r.clientInfo[clientInfoSessionKey{rawAuthKeyID: authKeyID, sessionID: sessionID}]; ok && info.layer != 0 {
return info.layer, true
}
if info, ok := r.authInfo[authKeyID]; ok && info.layer != 0 {
return info.layer, true
}
return currentClientLayer, false
}
// mutateClientSessionInfo 在单个临界区内完成「读旧值-修改-写回」,避免
// RLock 读出与 Lock 写回之间被并发写覆盖的窗口。
func (r *Router) mutateClientSessionInfo(ctx context.Context, mutate func(*clientSessionInfo)) {
rawAuthKeyID, ok := RawAuthKeyIDFrom(ctx)
if !ok {
return
@ -355,37 +710,211 @@ func (r *Router) rememberClientInfo(ctx context.Context, info ClientInfo) {
r.clientInfoMu.Lock()
defer r.clientInfoMu.Unlock()
if r.clientInfo == nil {
r.clientInfo = make(map[clientInfoSessionKey]ClientInfo)
r.clientInfo = make(map[clientInfoSessionKey]clientSessionInfo)
}
sessionKey := clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID}
sessionInfo, exists := r.clientInfo[sessionKey]
mutate(&sessionInfo)
if !exists {
evictMapEntryIfFullLocked(r.clientInfo, maxClientInfoEntries)
}
r.clientInfo[sessionKey] = sessionInfo
r.rememberAuthClientInfoLocked(rawAuthKeyID, sessionInfo)
if authKeyID, ok := AuthKeyIDFrom(ctx); ok {
r.rememberAuthClientInfoLocked(authKeyID, sessionInfo)
}
r.clientInfo[clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID}] = info
}
func (r *Router) clientInfoForSession(ctx context.Context) (ClientInfo, bool) {
func (r *Router) rememberClientSessionInfo(ctx context.Context, sessionInfo clientSessionInfo) {
rawAuthKeyID, ok := RawAuthKeyIDFrom(ctx)
if !ok {
return ClientInfo{}, false
return
}
sessionID, ok := SessionIDFrom(ctx)
if !ok {
return ClientInfo{}, false
return
}
r.clientInfoMu.Lock()
defer r.clientInfoMu.Unlock()
if r.clientInfo == nil {
r.clientInfo = make(map[clientInfoSessionKey]clientSessionInfo)
}
sessionKey := clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID}
if _, exists := r.clientInfo[sessionKey]; !exists {
evictMapEntryIfFullLocked(r.clientInfo, maxClientInfoEntries)
}
r.clientInfo[sessionKey] = mergeClientSessionInfo(r.clientInfo[sessionKey], sessionInfo)
r.rememberAuthClientInfoLocked(rawAuthKeyID, sessionInfo)
if authKeyID, ok := AuthKeyIDFrom(ctx); ok {
r.rememberAuthClientInfoLocked(authKeyID, sessionInfo)
}
}
func (r *Router) rememberAuthClientInfoLocked(authKeyID [8]byte, info clientSessionInfo) {
if r.authInfo == nil {
r.authInfo = make(map[[8]byte]clientSessionInfo)
}
if _, exists := r.authInfo[authKeyID]; !exists {
evictMapEntryIfFullLocked(r.authInfo, maxAuthInfoEntries)
}
current := r.authInfo[authKeyID]
r.authInfo[authKeyID] = mergeClientSessionInfo(current, info)
}
// forgetClientSessionInfo 随连接下线移除该 session 的元数据缓存条目,并清掉以该 raw
// auth_key 为键的 authInfo 兜底条目,使 authInfo 收敛到活跃 raw auth key主导的单
// session/key 场景下严格回收)。共享同一 raw auth_key 的其它 session 若仍在线,会在下一次
// initConnection/authorization 回填——authInfo 只是廉价的元数据兜底。temp→perm 解析后以
// 业务 perm key 为键的条目仍靠容量上限兜底SessionOffline 不带业务 key无法在此精确清理
func (r *Router) forgetClientSessionInfo(rawAuthKeyID [8]byte, sessionID int64) {
r.clientInfoMu.Lock()
delete(r.clientInfo, clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID})
delete(r.authInfo, rawAuthKeyID)
r.clientInfoMu.Unlock()
}
func evictMapEntryIfFullLocked[K comparable, V any](m map[K]V, limit int) {
if len(m) < limit {
return
}
for k := range m {
delete(m, k)
return
}
}
func (r *Router) clientSessionInfo(ctx context.Context) (clientSessionInfo, bool) {
rawAuthKeyID, ok := RawAuthKeyIDFrom(ctx)
if !ok {
return clientSessionInfo{}, false
}
sessionID, ok := SessionIDFrom(ctx)
if !ok {
return clientSessionInfo{}, false
}
r.clientInfoMu.RLock()
defer r.clientInfoMu.RUnlock()
info, ok := r.clientInfo[clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID}]
if authInfo, authOK := r.authInfo[rawAuthKeyID]; authOK {
info = mergeClientSessionInfo(info, authInfo)
ok = true
}
if authKeyID, hasAuthKeyID := AuthKeyIDFrom(ctx); hasAuthKeyID {
if authInfo, authOK := r.authInfo[authKeyID]; authOK {
info = mergeClientSessionInfo(info, authInfo)
ok = true
}
}
return info, ok
}
func (r *Router) cachedResolvedAuthClientInfo(authKeyID [8]byte) (clientSessionInfo, bool) {
r.clientInfoMu.RLock()
defer r.clientInfoMu.RUnlock()
info, ok := r.authInfo[authKeyID]
if !ok || clientSessionInfoNeedsAuthorization(info) {
return clientSessionInfo{}, false
}
return info, true
}
func mergeClientSessionInfo(base, fallback clientSessionInfo) clientSessionInfo {
if base.layer == 0 {
base.layer = fallback.layer
}
if !base.hasClientInfo && fallback.hasClientInfo {
base.clientInfo = fallback.clientInfo
base.hasClientInfo = true
}
if fallback.authorizationChecked {
base.authorizationChecked = true
}
return base
}
func (r *Router) clientSessionInfoFromAuthorization(ctx context.Context, userID int64, authKeyID [8]byte, current clientSessionInfo) (clientSessionInfo, bool) {
if !clientSessionInfoNeedsAuthorization(current) || r.deps.Auth == nil || userID == 0 {
return clientSessionInfo{}, false
}
v, err, _ := r.authUserSF.Do(authClientInfoSingleflightPrefix+string(authKeyID[:]), func() (any, error) {
if cached, ok := r.cachedResolvedAuthClientInfo(authKeyID); ok {
return cached, nil
}
item, found, err := r.deps.Auth.Authorization(ctx, authKeyID)
if err != nil {
return clientSessionInfo{}, err
}
if !found || item.UserID != userID || item.PasswordPending {
return clientSessionInfo{authorizationChecked: true}, nil
}
return clientSessionInfoFromAuthorizationRecord(item, current), nil
})
if err != nil {
return clientSessionInfo{}, false
}
return v.(clientSessionInfo), true
}
func clientSessionInfoFromAuthorizationRecord(item domain.Authorization, current clientSessionInfo) clientSessionInfo {
info := clientSessionInfo{
layer: item.Layer,
authorizationChecked: true,
clientInfo: ClientInfo{
APIID: item.APIID,
DeviceModel: item.DeviceModel,
SystemVersion: item.SystemVersion,
AppVersion: item.AppVersion,
Type: ClientType(item.Platform),
},
}
info.clientInfo = normalizeClientInfo(info.clientInfo)
info.hasClientInfo = info.clientInfo.ClientType() != ClientTypeUnknown ||
info.clientInfo.DeviceModel != "" ||
info.clientInfo.SystemVersion != "" ||
info.clientInfo.AppVersion != "" ||
info.clientInfo.APIID != 0
if info.layer == 0 {
if current.layer != 0 {
info.layer = current.layer
} else if info.clientInfo.ClientType() != ClientTypeUnknown {
info.layer = currentClientLayer
}
}
return info
}
func clientSessionInfoNeedsAuthorization(info clientSessionInfo) bool {
if info.authorizationChecked {
return false
}
return info.layer == 0 || !info.hasClientInfo || info.clientInfo.ClientType() == ClientTypeUnknown
}
// fallback 处理未注册的 RPC记录到 compatibility trace落兼容矩阵
// 返回 NOT_IMPLEMENTED rpc_error 让客户端继续运行而非断连。
func (r *Router) fallback(ctx context.Context, b *bin.Buffer) (bin.Encoder, error) {
// DrKLO 12.8.1 的 theme 方法构造器比 gotd schema 新,dispatcher 匹配不上;
// 在落到「未实现」前先按 DrKLO 字段序手动解码处理。
if enc, handled, err := r.tryLegacyThemeRPC(ctx, b); handled {
return enc, err
}
id, _ := b.PeekID()
fields := append([]zap.Field{zap.String("type_id", fmt.Sprintf("%#x", id))}, r.contextLogFields(ctx)...)
fields := append([]zap.Field{
zap.String("method", tlTypeName(id)),
zap.String("type_id", fmt.Sprintf("%#x", id)),
}, r.contextLogFields(ctx)...)
r.log.Warn("Unhandled RPC (compatibility trace)", fields...)
return nil, notImplementedErr()
}
func (r *Router) contextLogFields(ctx context.Context) []zap.Field {
fields := []zap.Field{zap.Int("layer", LayerFrom(ctx))}
fields := []zap.Field{
zap.Int("layer", LayerFrom(ctx)),
zap.String("client_type", string(ClientTypeFrom(ctx))),
}
if info, ok := ClientInfoFrom(ctx); ok && info.AppVersion != "" {
fields = append(fields, zap.String("app_version", info.AppVersion))
}
if sessionID, ok := SessionIDFrom(ctx); ok {
fields = append(fields, zap.Int64("session_id", sessionID))
}