owpengram-server/internal/rpc/router.go

951 lines
35 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"encoding/hex"
"fmt"
"sync"
"time"
"go.uber.org/zap"
"golang.org/x/sync/singleflight"
"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/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
tlTypeNames map[uint32]string
)
// Config 是 Router 所需的服务端信息。
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 / invokeAfter*,并兜底未注册 RPC。
type Router struct {
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
availableReactionDocuments availableReactionDocumentMapCache
emojiStickers *emojiStickerIndex
notifySettings *notifySettingsCache
stickerCatalog *stickerCatalogCache
transientPrivateBigReactions transientPrivateBigReactionCache
accountSettings *accountSettingsCache
// webPageResolveSem 是链接预览异步解析的并发信号量(有界):发送后把 pending 占位
// 解析为卡片并就地替换。满则丢弃任务(消息留 pending。nil=未启用(测试可直接调
// resolvePendingWebPage 同步验证)。
webPageResolveSem chan struct{}
}
type clientInfoSessionKey struct {
rawAuthKeyID [8]byte
sessionID int64
}
type clientSessionInfo struct {
layer int
clientInfo ClientInfo
hasClientInfo bool
authorizationChecked bool
}
type authUserCacheEntry struct {
userID int64
found bool
}
// New 创建 Router由各业务域自行注册其 RPC handlerregisterHelp/Auth/Users/Updates
func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
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)
r.registerAuth(d)
r.registerUsers(d)
r.registerUpdates(d)
r.registerAccount(d)
r.registerMessages(d)
r.registerStickers(d)
r.registerChannels(d)
r.registerUpload(d)
r.registerPhotos(d)
r.registerFolders(d)
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 / 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)
if err != nil {
return nil, internalErr()
}
if hasUserID {
ctx = WithUserID(ctx, userID)
}
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 {
cached = id
hasCached = true
}
} else if id, ok := r.deps.Sessions.AuthKeyID(sessionID); ok {
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)
if err != nil {
return [8]byte{}, err
}
if ok {
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)
} else {
r.deps.Sessions.BindAuthKey(sessionID, effective)
}
}
}
func (r *Router) effectiveUserID(ctx context.Context, rawAuthKeyID, authKeyID [8]byte, sessionID int64) (int64, bool, error) {
if userID, ok := UserIDFrom(ctx); ok {
if scoped, ok := r.scopedSessions(); ok {
scoped.BindUserForAuthKey(rawAuthKeyID, sessionID, userID)
} else if r.deps.Sessions != nil {
r.deps.Sessions.BindUser(sessionID, userID)
}
return userID, true, nil
}
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
}
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 {
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
}
}
}
if found {
if scoped, ok := r.deps.Sessions.(ScopedSessionBinder); ok {
scoped.BindUserForAuthKey(rawAuthKeyID, sessionID, userID)
} else {
r.deps.Sessions.BindUser(sessionID, userID)
}
r.announceSessionOnline(ctx, userID)
} else {
if scoped, ok := r.deps.Sessions.(ScopedSessionBinder); ok {
scoped.BindUserForAuthKey(rawAuthKeyID, sessionID, 0)
} else {
r.deps.Sessions.BindUser(sessionID, 0)
}
}
}
return userID, found, nil
}
func (r *Router) lookupAuthUser(ctx context.Context, authKeyID [8]byte) (int64, bool, error) {
if userID, found, ok := r.cachedAuthUser(authKeyID); ok {
return userID, found, nil
}
key := string(authKeyID[:])
v, err, _ := r.authUserSF.Do(key, func() (any, error) {
if userID, found, ok := r.cachedAuthUser(authKeyID); ok {
return authUserCacheEntry{userID: userID, found: found}, nil
}
userID, found, err := r.deps.Auth.UserID(ctx, authKeyID)
if err != nil {
return authUserCacheEntry{}, err
}
r.setAuthUserCache(authKeyID, userID, found)
return authUserCacheEntry{userID: userID, found: found}, nil
})
if err != nil {
return 0, false, err
}
entry := v.(authUserCacheEntry)
return entry.userID, entry.found, nil
}
func (r *Router) cachedAuthUser(authKeyID [8]byte) (int64, bool, bool) {
r.authUserMu.RLock()
defer r.authUserMu.RUnlock()
entry, ok := r.authUsers[authKeyID]
if !ok {
return 0, false, false
}
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}
}
func (r *Router) invalidateAuthUserCache(authKeyID [8]byte) {
r.authUserMu.Lock()
delete(r.authUsers, authKeyID)
r.authUserMu.Unlock()
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) {
if r.deps.Sessions == nil {
return nil, false
}
scoped, ok := r.deps.Sessions.(ScopedSessionBinder)
return scoped, ok
}
func (r *Router) dispatch(ctx context.Context, b *bin.Buffer, depth int) (bin.Encoder, error) {
if depth > maxWrapperDepth {
return nil, wrapperTooDeepErr()
}
id, err := b.PeekID()
if err != nil {
return nil, err
}
switch id {
case tg.InvokeWithLayerRequestTypeID:
if err := b.ConsumeID(id); err != nil {
return nil, err
}
layer, err := b.Int()
if err != nil {
return nil, fmt.Errorf("decode invokeWithLayer layer: %w", err)
}
// query 紧跟 layerbuffer 剩余即内层请求。
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:
req := &tg.InitConnectionRequest{Query: &rawObject{}}
if err := req.Decode(b); err != nil {
return nil, fmt.Errorf("decode initConnection: %w", err)
}
info := ClientInfo{
APIID: req.APIID,
DeviceModel: req.DeviceModel,
SystemVersion: req.SystemVersion,
AppVersion: req.AppVersion,
SystemLangCode: req.SystemLangCode,
LangPack: req.LangPack,
LangCode: req.LangCode,
}
ctx = WithClientInfo(ctx, info)
r.rememberClientInfo(ctx, info)
r.log.Debug("initConnection",
zap.Int("api_id", req.APIID),
zap.String("device", req.DeviceModel),
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 {
return nil, fmt.Errorf("initConnection query: unexpected type %T", req.Query)
}
return r.dispatch(ctx, &bin.Buffer{Buf: inner.data}, depth+1)
default:
// 入站兼容统一入口(先于鉴权门/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))
}
r.log.Info("RPC inner handled", fields...)
} else {
r.log.Debug("RPC inner handled", fields...)
}
return enc, err
}
}
func tlTypeName(id uint32) string {
tlTypeNamesOnce.Do(func() {
names := tg.NamesMap()
tlTypeNames = make(map[uint32]string, len(names))
for name, typeID := range names {
tlTypeNames[typeID] = name
}
})
if name, ok := tlTypeNames[id]; ok {
return name
}
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
}
sessionID, ok := SessionIDFrom(ctx)
if !ok {
return
}
r.clientInfoMu.Lock()
defer r.clientInfoMu.Unlock()
if r.clientInfo == nil {
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)
}
}
func (r *Router) rememberClientSessionInfo(ctx context.Context, sessionInfo clientSessionInfo) {
rawAuthKeyID, ok := RawAuthKeyIDFrom(ctx)
if !ok {
return
}
sessionID, ok := SessionIDFrom(ctx)
if !ok {
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("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)),
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))
}
if rawAuthKeyID, ok := RawAuthKeyIDFrom(ctx); ok {
fields = append(fields, zap.String("raw_auth_key_id", hex.EncodeToString(rawAuthKeyID[:])))
}
if authKeyID, ok := AuthKeyIDFrom(ctx); ok {
fields = append(fields, zap.String("auth_key_id", hex.EncodeToString(authKeyID[:])))
}
if userID, ok := UserIDFrom(ctx); ok {
fields = append(fields, zap.Int64("user_id", userID))
}
return fields
}
// rawObject 在解码 wrapper 时按原样捕获内层 query 的 TL 字节,供递归分发。
// 它实现 bin.ObjectEncode/Decode但只搬运字节、不解释内容。
type rawObject struct {
data []byte
}
func (o *rawObject) Decode(b *bin.Buffer) error {
o.data = append(o.data[:0], b.Buf...)
b.Skip(len(b.Buf))
return nil
}
func (o *rawObject) Encode(b *bin.Buffer) error {
b.Put(o.data)
return nil
}