238 lines
9.2 KiB
Go
238 lines
9.2 KiB
Go
package mtprotoedge
|
||
|
||
import (
|
||
"context"
|
||
"encoding/binary"
|
||
"errors"
|
||
"fmt"
|
||
|
||
"go.uber.org/zap"
|
||
|
||
"github.com/iamxvbaba/td/bin"
|
||
"github.com/iamxvbaba/td/crypto"
|
||
"github.com/iamxvbaba/td/exchange"
|
||
"github.com/iamxvbaba/td/mt"
|
||
"github.com/iamxvbaba/td/proto/codec"
|
||
"github.com/iamxvbaba/td/transport"
|
||
|
||
"telesrv/internal/store"
|
||
)
|
||
|
||
// emptyAuthKeyID 是未加密消息(密钥交换)的 auth_key_id(全零)。
|
||
var emptyAuthKeyID [8]byte
|
||
|
||
// peekAuthKeyID 读取消息前 8 字节的 auth_key_id,不消费 buffer。
|
||
func peekAuthKeyID(b *bin.Buffer) (id [8]byte, err error) {
|
||
err = b.PeekN(id[:], len(id))
|
||
return id, err
|
||
}
|
||
|
||
// handleExchange 在收到 auth_key_id==0 的首帧后执行服务端 MTProto 密钥交换。
|
||
//
|
||
// first 是已读取的首帧(req_pq*),通过 bufferedConn 交还给 exchange 流程,
|
||
// 使其能从头读取握手消息。auth key + server salt 会在 DhGenOk 发出前落入
|
||
// AuthKeyStore;持久化失败时不向客户端确认握手成功。
|
||
func (s *Server) handleExchange(ctx context.Context, conn transport.Conn, first *bin.Buffer) (*bin.Buffer, error) {
|
||
if s.key.Zero() {
|
||
s.log.Error("Key exchange requested but server RSA key is not configured")
|
||
return nil, s.sendProtoError(ctx, conn, codec.CodeAuthKeyNotFound)
|
||
}
|
||
|
||
buffered := newBufferedConn(conn)
|
||
buffered.push(first)
|
||
|
||
// 给整个密钥交换设总时长上界。HandshakeIdleTimeout 只约束单次读 idle,对一个持续发包的
|
||
// 客户端无效——若客户端陷入「ResPQ→nonce 失步→重发 req_pq」的握手重启死循环,无界的
|
||
// serverExchange 会对每个 req_pq 盲回 ResPQ、永不收敛地空转刷日志/占 CPU。超时即放弃本次
|
||
// 握手并断开,客户端重连发起全新握手(无残留相位差)即恢复。
|
||
runCtx := ctx
|
||
if s.handshakeMaxDur > 0 {
|
||
var cancel context.CancelFunc
|
||
runCtx, cancel = context.WithTimeout(ctx, s.handshakeMaxDur)
|
||
defer cancel()
|
||
}
|
||
|
||
start := s.clock.Now()
|
||
res, err := s.runServerExchange(runCtx, buffered)
|
||
if err != nil {
|
||
// gotd v0.158:握手中读到非零 auth_key_id 帧(客户端用既有 auth key 而非重新交换)
|
||
// 经类型化 UnexpectedEncryptedError 暴露并随附原始帧(旧版仅靠错误文案匹配,升级后失效)。
|
||
// 把该帧当既有会话首帧 replay,切勿回 -404——TDesktop 会判定 temp key 被销毁、丢弃并
|
||
// 重跑密钥交换,引发重连/重交换风暴。
|
||
var encErr *exchange.UnexpectedEncryptedError
|
||
if errors.As(err, &encErr) {
|
||
replay := encErr.Frame
|
||
if len(replay) > 0 {
|
||
s.log.Debug("Key exchange interrupted by encrypted frame; replaying as existing session")
|
||
return &bin.Buffer{Buf: replay}, nil
|
||
}
|
||
}
|
||
// req_pq 帧数超界(客户端握手重启死循环):瞬断,促客户端重连发起全新握手。
|
||
if errors.Is(err, errTooManyHandshakeReqPQ) {
|
||
s.log.Info("Key exchange aborted: too many req_pq retries (client handshake restart loop)",
|
||
zap.Int("max", maxHandshakeReqPQ))
|
||
return nil, err
|
||
}
|
||
// 仅本握手的总时长上界到点(ctx 自身未取消):放弃并断开,促客户端重连。
|
||
if errors.Is(err, context.DeadlineExceeded) && ctx.Err() == nil {
|
||
s.log.Info("Key exchange aborted: exceeded max duration (possible client req_pq restart loop)",
|
||
zap.Duration("max", s.handshakeMaxDur))
|
||
return nil, err
|
||
}
|
||
var exErr *exchange.ServerExchangeError
|
||
if errors.As(err, &exErr) {
|
||
s.log.Info("Key exchange rejected", zap.Int32("code", exErr.Code), zap.Error(err))
|
||
return nil, s.sendProtoError(ctx, conn, exErr.Code)
|
||
}
|
||
return nil, fmt.Errorf("key exchange: %w", err)
|
||
}
|
||
|
||
s.metrics.HandshakeDone(s.clock.Now().Sub(start))
|
||
s.log.Info("Key exchange completed",
|
||
zap.Int64("auth_key_id", res.Key.IntID()),
|
||
zap.Int64("server_salt", res.ServerSalt),
|
||
zap.Duration("dur", s.clock.Now().Sub(start)),
|
||
)
|
||
|
||
return nil, nil
|
||
}
|
||
|
||
// authKeyData 把握手结果转换为 store 记录。
|
||
func authKeyData(key crypto.AuthKey, salt, createdAt int64, expiresAt int) store.AuthKeyData {
|
||
return store.AuthKeyData{
|
||
ID: key.ID,
|
||
Value: [256]byte(key.Value),
|
||
ServerSalt: salt,
|
||
CreatedAt: createdAt,
|
||
ExpiresAt: expiresAt,
|
||
}
|
||
}
|
||
|
||
// sendProtoError 向客户端发送 transport 级协议错误(-code)。
|
||
func (s *Server) sendProtoError(ctx context.Context, conn transport.Conn, code int32) error {
|
||
var buf bin.Buffer
|
||
buf.PutInt32(-code)
|
||
|
||
ctx, cancel := context.WithTimeout(ctx, s.writeTimeout)
|
||
defer cancel()
|
||
if err := conn.Send(ctx, &buf); err != nil {
|
||
return fmt.Errorf("send proto error %d: %w", code, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// sendTerminalProtoError serializes a bare transport error after the authenticated
|
||
// outbound actor has stopped. A direct write while the actor is still draining can
|
||
// otherwise interleave an encrypted update/result after -404 on the same socket.
|
||
func (s *Server) sendTerminalProtoError(ctx context.Context, c *Conn, code int32) error {
|
||
if c == nil {
|
||
return errors.New("send terminal protocol error without logical connection")
|
||
}
|
||
c.beginTerminalShutdown()
|
||
if !c.waitOutboundShutdownUntil(forceCloseBatchTimeout) {
|
||
c.closeTransport()
|
||
return errors.New("outbound writer did not stop before terminal protocol error")
|
||
}
|
||
if c.transport == nil {
|
||
return ErrConnClosed
|
||
}
|
||
return s.sendProtoError(ctx, c.transport, code)
|
||
}
|
||
|
||
// maxHandshakeReqPQ 是一次密钥交换内允许的 req_pq(_multi) 帧数上界。正常握手只发 1 个
|
||
// req_pq(含个别客户端的「fake+真」也就 2 个);客户端因 nonce 失步陷入「收到 ResPQ→立刻
|
||
// 重启握手换 nonce 重发 req_pq」死循环时,会在同一连接上无限发 req_pq,而委托给 gotd 的
|
||
// serverExchange 会对每个都盲回 ResPQ、永不收敛(见 docs/client-compat-notes.md 的握手风暴)。
|
||
// 超过此上界即在 telesrv 传输层瞬断该连接,促客户端重连发起全新握手(无残留相位差)即恢复。
|
||
// 留足余量(8)容纳少量正常重连重启。它与 HandshakeMaxDuration 总时长上界互补(按次/按时)。
|
||
const maxHandshakeReqPQ = 8
|
||
|
||
// errTooManyHandshakeReqPQ 表示一次握手内 req_pq 帧数超过 maxHandshakeReqPQ(疑似客户端握手
|
||
// 重启死循环)。从 bufferedConn.Recv 抛出,经 serverExchange 透传回 handleExchange 断开连接。
|
||
var errTooManyHandshakeReqPQ = errors.New("too many req_pq frames in one handshake (client restart loop)")
|
||
|
||
// bufferedConn 包装 transport.Conn,可把已读取的帧重新交给后续 Recv。
|
||
//
|
||
// 用于密钥交换:serveConn 已读首帧用于 peek auth_key_id,再 push 回来交给 exchange。
|
||
type bufferedConn struct {
|
||
transport.Conn
|
||
pending []bin.Buffer
|
||
reqPQCount int // 本次握手已见 req_pq(_multi) 帧数;只在握手期访问(Recv 单 goroutine)
|
||
}
|
||
|
||
func newBufferedConn(conn transport.Conn) *bufferedConn {
|
||
return &bufferedConn{Conn: conn}
|
||
}
|
||
|
||
func (c *bufferedConn) push(b *bin.Buffer) {
|
||
if b == nil {
|
||
return
|
||
}
|
||
// serveConn is synchronously blocked in handleExchange, so the first frame's
|
||
// backing remains stable until the exchange returns. Keep a slice view instead
|
||
// of copying an attacker-sized transport frame.
|
||
buf := b.Buf
|
||
b.Buf = nil // transfer ownership; serveConn must not pin the frame after next Recv releases it
|
||
c.pending = append(c.pending, bin.Buffer{Buf: buf})
|
||
}
|
||
|
||
// Recv 优先返回已 push 的帧(FIFO),耗尽后读取底层连接。
|
||
func (c *bufferedConn) Recv(ctx context.Context, b *bin.Buffer) error {
|
||
for {
|
||
if len(c.pending) > 0 {
|
||
e := c.pending[0]
|
||
c.pending[0] = bin.Buffer{}
|
||
c.pending = c.pending[1:]
|
||
b.ResetTo(e.Buf)
|
||
} else {
|
||
if err := c.Conn.Recv(ctx, b); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if isUnencryptedMsgsAckFrame(b) {
|
||
// The ack is intentionally ignored during exchange. Drop its transport
|
||
// backing and shrink the retained high-water charge before the next Recv;
|
||
// otherwise a large trailing frame can consume global admission budget for
|
||
// the rest of a CPU-heavy key exchange even though no backing remains live.
|
||
b.Buf = nil
|
||
retainInboundFrameBackings(c.Conn, b)
|
||
continue
|
||
}
|
||
// req_pq 计数上界:仅在握手期生效(bufferedConn 只用于密钥交换),且 payload id 探测
|
||
// 与上面的 msgs_ack 跳过同量级开销,不碰加密消息热路径。超界即瞬断,止住握手死循环。
|
||
if isUnencryptedReqPQFrame(b) {
|
||
c.reqPQCount++
|
||
if c.reqPQCount > maxHandshakeReqPQ {
|
||
return errTooManyHandshakeReqPQ
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// unencryptedPayloadID 返回未加密消息(auth_key_id==0)内层 TL payload 的 type id。
|
||
// 非未加密消息 / 解码失败时 ok=false。
|
||
func unencryptedPayloadID(frame *bin.Buffer) (uint32, bool) {
|
||
if frame == nil || len(frame.Buf) < 24 {
|
||
return 0, false
|
||
}
|
||
if binary.LittleEndian.Uint64(frame.Buf[:8]) != 0 {
|
||
return 0, false
|
||
}
|
||
dataLen := int64(int32(binary.LittleEndian.Uint32(frame.Buf[16:20])))
|
||
if dataLen < 4 || dataLen > int64(len(frame.Buf)-20) {
|
||
return 0, false
|
||
}
|
||
return binary.LittleEndian.Uint32(frame.Buf[20:24]), true
|
||
}
|
||
|
||
func isUnencryptedMsgsAckFrame(frame *bin.Buffer) bool {
|
||
id, ok := unencryptedPayloadID(frame)
|
||
return ok && id == mt.MsgsAckTypeID
|
||
}
|
||
|
||
func isUnencryptedReqPQFrame(frame *bin.Buffer) bool {
|
||
id, ok := unencryptedPayloadID(frame)
|
||
return ok && (id == mt.ReqPqRequestTypeID || id == mt.ReqPqMultiRequestTypeID)
|
||
}
|