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

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net"
"net/http"
"sync"
"time"
@ -33,6 +34,12 @@ import (
// 返回 *tgerr.Error 时连接层将其转为 rpc_error 回发;其他 error 视为连接级故障。
type RPCHandler interface {
Dispatch(ctx context.Context, authKeyID [8]byte, sessionID int64, b *bin.Buffer) (bin.Encoder, error)
// NegotiatedLayer returns the TL layer the session negotiated via
// invokeWithLayer and whether one was ever observed. Used to downgrade
// outbound objects for clients compiled on an older layer. ok=false means
// unknown (cold/evicted) — the caller must keep the connection's last-known
// layer rather than overwrite it.
NegotiatedLayer(authKeyID [8]byte, sessionID int64) (int, bool)
}
// Options 配置 Server。
@ -44,11 +51,24 @@ type Options struct {
// ObfuscatedTCP 先按 MTProto TCP obfuscation 解包,再自动探测 codec。
// Telegram Desktop 的 tcpo_only endpoint 会走这个 64 字节前缀流程。
ObfuscatedTCP bool
// WebSocket 在同一个 listener 上接受 MTProto over WebSocket(/apiws*)。
// 开启后仅在连接建立时读取前 4 字节做 HTTP/TCP 分流MTProto TCP
// 后续仍走原 ObfuscatedTCP + codec 热路径。
WebSocket bool
// WebSocketAllowedOrigins 是允许浏览器发起 WebSocket upgrade 的页面 origin。
// 空列表表示只接受无 Origin 的非浏览器客户端;"*" 表示允许所有来源(仅调试)。
WebSocketAllowedOrigins []string
// ReadTimeout 单次读取超时。默认 5m。
ReadTimeout time.Duration
// HandshakeIdleTimeout 是连接「建立 session 前」(握手 + 首个加密消息之前)的读超时,
// 比 ReadTimeout 短,用于快速回收握手后静默的半开 / 异常连接。默认 60s。
HandshakeIdleTimeout time.Duration
// HandshakeMaxDuration 是单次密钥交换serverExchange的总时长上界。HandshakeIdleTimeout
// 只约束「单次读 idle」对一个持续发包的客户端无效——若客户端陷入「收到 ResPQ→nonce 失步
// →重发 req_pq」的握手重启死循环见 docs/client-compat-notes.md无界的 serverExchange 会
// 对每个 req_pq 盲回 ResPQ、永不收敛地空转刷日志/占 CPU。本上界给整个握手设总预算超时即
// 放弃并断开,客户端重连发起全新握手(无残留相位差)即恢复。正常握手 <1s。默认 20s。
HandshakeMaxDuration time.Duration
// WriteTimeout 单次写入超时。默认 30s。
WriteTimeout time.Duration
// RPCMaxInflight 是单连接同时处理的 RPC 上限。默认 32。
@ -88,6 +108,9 @@ func (o *Options) setDefaults() {
if o.HandshakeIdleTimeout == 0 {
o.HandshakeIdleTimeout = 60 * time.Second
}
if o.HandshakeMaxDuration == 0 {
o.HandshakeMaxDuration = 20 * time.Second
}
if o.WriteTimeout == 0 {
o.WriteTimeout = 30 * time.Second
}
@ -129,8 +152,11 @@ type Server struct {
log *zap.Logger
codec func() transport.Codec
obfuscated bool
websocket bool
websocketOrigins []string
readTimeout time.Duration
handshakeTimeout time.Duration
handshakeMaxDur time.Duration
writeTimeout time.Duration
rpcInflight int
rpcQueueSize int
@ -148,8 +174,7 @@ type Server struct {
rand io.Reader
types *tmap.Map
// sessionUID 是本进程 server session 唯一标识,写入 new_session_created。
sessionUID int64
rpcResults *rpcResultCache
// onFrame 是测试钩子:收到一帧时回调其字节数;生产为 nil。
onFrame func(n int)
@ -166,8 +191,11 @@ func New(opts Options) *Server {
log: opts.Logger,
codec: opts.Codec,
obfuscated: opts.ObfuscatedTCP,
websocket: opts.WebSocket,
websocketOrigins: append([]string(nil), opts.WebSocketAllowedOrigins...),
readTimeout: opts.ReadTimeout,
handshakeTimeout: opts.HandshakeIdleTimeout,
handshakeMaxDur: opts.HandshakeMaxDuration,
writeTimeout: opts.WriteTimeout,
rpcInflight: opts.RPCMaxInflight,
rpcQueueSize: opts.RPCQueueSize,
@ -183,7 +211,7 @@ func New(opts Options) *Server {
clock: opts.Clock,
rand: opts.Rand,
types: tmap.New(tg.TypesMap(), mt.TypesMap(), proto.TypesMap()),
sessionUID: opts.Clock.Now().UnixNano(),
rpcResults: newRPCResultCache(opts.Clock.Now),
}
}
@ -214,49 +242,192 @@ func (s *Server) newConn(tc transport.Conn, key crypto.AuthKey, sessionID, salt
// Serve 在 ln 上运行 MTProto 连接循环,直到 ctx 取消或发生不可恢复错误。
// ctx 取消时优雅退出:关闭 listener 并等待在途连接处理结束。
func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
if s.websocket {
return s.serveMixed(ctx, ln)
}
return s.serveTCP(ctx, ln)
}
func (s *Server) serveTCP(ctx context.Context, ln net.Listener) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
transportListener := ln
if s.obfuscated {
transportListener = transport.ObfuscatedListener(ln)
}
l := newCompatTransportListener(s.codec, transportListener)
s.log.Info("Serving", zap.String("addr", ln.Addr().String()), zap.Int("dc", s.dc), zap.Bool("obfuscated_tcp", s.obfuscated))
defer s.log.Info("Stopped")
// ctx 取消时关闭 listener解除 Accept 阻塞。
return s.acceptLoop(ctx, ln, s.obfuscated)
}
func (s *Server) serveMixed(ctx context.Context, ln net.Listener) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// 嗅探(读首 4 字节做 HTTP/TCP 分流)的读超时必须对齐「建立 session 前」的读超时
// s.handshakeTimeout(默认 60s)——与 serveDetectedConn/serveConn 的 pre-session 读
// deadline 完全一致。一条尚未发出首帧的连接正处于「pre-session idle」状态合法 MTProto
// 客户端(如 DrKLO)会预开「暖」连接、在有请求前并不立即发送 obfuscated2 init。此前用
// minDuration(5s,...) 把嗅探压到 5s比非 mux 路径激进 12 倍,会把这些暖连接在 5s 误杀,
// 触发客户端 6s 重连风暴并误判「后端不健康」回退到外部 DNS。per-conn goroutine 模型已消解
// slow-loris 接入饥饿,故嗅探用满 handshakeTimeout 是安全的。
mux := newSamePortMux(ln, s.handshakeTimeout)
wsLn, wsHandler := transport.WebsocketListener(ln.Addr())
httpServer := &http.Server{
Handler: websocketRouteHandler(wsHandler, s.websocketOrigins),
ReadHeaderTimeout: minDuration(10*time.Second, s.handshakeTimeout),
BaseContext: func(net.Listener) context.Context {
return ctx
},
}
s.log.Info("Serving",
zap.String("addr", ln.Addr().String()),
zap.Int("dc", s.dc),
zap.Bool("obfuscated_tcp", s.obfuscated),
zap.Bool("websocket", true),
zap.Strings("websocket_origins", s.websocketOrigins),
)
defer s.log.Info("Stopped")
go func() {
<-ctx.Done()
_ = l.Close()
_ = mux.Close()
_ = httpServer.Close()
_ = wsLn.Close()
}()
errCh := make(chan error, 4)
var wg sync.WaitGroup
wg.Add(4)
// 分流器:窥探前 4 字节把 HTTP(WebSocket 升级) 与裸 MTProto TCP 拆开。
go func() {
defer wg.Done()
errCh <- mux.Serve(ctx)
}()
// 裸 MTProto TCP每条连接在自己的 goroutine 里完成去混淆 + codec 探测。
go func() {
defer wg.Done()
errCh <- s.acceptLoop(ctx, mux.TCP(), s.obfuscated)
}()
// WebSocketgotd 升级处理器已剥离 obfuscated2 并补回 codec tag这里只需探测 codec。
go func() {
defer wg.Done()
errCh <- s.acceptLoop(ctx, wsLn, false)
}()
go func() {
defer wg.Done()
if err := httpServer.Serve(mux.HTTP()); err != nil {
if errors.Is(err, http.ErrServerClosed) || errors.Is(err, net.ErrClosed) {
errCh <- nil
return
}
errCh <- fmt.Errorf("websocket http serve: %w", err)
return
}
errCh <- nil
}()
var firstErr error
for i := 0; i < 4; i++ {
if err := <-errCh; err != nil && firstErr == nil {
firstErr = err
cancel()
}
}
cancel()
_ = mux.Close()
_ = httpServer.Close()
_ = wsLn.Close()
wg.Wait()
return firstErr
}
// acceptLoop 接受裸连接,并为每条连接单独起 goroutine 完成「去混淆 + codec 探测 +
// serveConn」。探测在 accept 循环之外、带握手超时进行——慢/半开/坏 init 的客户端只占用
// 自己的 goroutine绝不阻塞其他连接的接入单条连接的握手失败也只关闭该连接不会拖垮
// 整个监听循环。obfuscated 为 true 时先走 obfuscated2 去混淆(裸 MTProto TCPWebSocket
// 连接传 falsegotd 升级处理器已完成去混淆)。
func (s *Server) acceptLoop(ctx context.Context, ln net.Listener, obfuscated bool) error {
go func() {
<-ctx.Done()
_ = ln.Close()
}()
var wg sync.WaitGroup
defer wg.Wait()
for {
conn, err := l.Accept()
raw, err := ln.Accept()
if err != nil {
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
return nil
}
if s.obfuscated && isClientDisconnect(err) {
s.log.Debug("Ignoring failed obfuscated accept", zap.Error(err))
continue
}
return fmt.Errorf("accept: %w", err)
}
wg.Add(1)
go func() {
defer wg.Done()
if err := s.serveConn(ctx, conn); err != nil && !isClientDisconnect(err) {
s.log.Info("Connection closed with error", zap.Error(err))
}
s.serveDetectedConn(ctx, raw, obfuscated)
}()
}
}
// serveDetectedConn 把一条裸连接提升为 transport.Conn去混淆 + codec 探测)后运行 MTProto
// 连接循环。提升过程的读取放在本 goroutine、且受握手读超时约束而非塞在 accept 循环里,
// 这样慢连接不会阻塞其他连接接入,去混淆/codec 握手本身也有时间上界。
func (s *Server) serveDetectedConn(ctx context.Context, raw net.Conn, obfuscated bool) {
// 握手读超时只覆盖去混淆 + codec 探测这一小段用真实墙钟时间SetReadDeadline 语义),
// 不走可能被测试注入的逻辑 clock。
if err := raw.SetReadDeadline(time.Now().Add(s.handshakeTimeout)); err != nil {
_ = raw.Close()
return
}
// 探测阶段若 ctx 取消,主动关闭 raw 解除阻塞读取——否则去混淆读会一直挂到握手超时,
// 把半开连接拖进优雅退出的等待里。探测结束即停掉该 watcher连接服务期由 serveConn
// 自己的 ctx watcher 接管。
promoted := make(chan struct{})
go func() {
select {
case <-ctx.Done():
_ = raw.Close()
case <-promoted:
}
}()
conn, err := s.promoteConn(raw, obfuscated)
close(promoted)
if err != nil {
// 去混淆/codec 探测失败(读超时、客户端中途断开、坏 init 等)只影响这一条连接,
// 记 debug 即可。
if !isClientDisconnect(err) {
s.log.Debug("Transport handshake failed", zap.Error(err))
}
_ = raw.Close()
return
}
// 探测完成,撤掉握手读超时;后续每帧读写由 serveConn / 传输层各自管理超时。
if err := raw.SetReadDeadline(time.Time{}); err != nil {
_ = conn.Close()
return
}
if err := s.serveConn(ctx, conn); err != nil && !isClientDisconnect(err) {
s.log.Info("Connection closed with error", zap.Error(err))
}
}
// promoteConn 复用与 listener 组合完全一致的「obfuscated2 去混淆 + codec 探测」管线,但针对
// 单条连接,使其可在 accept 循环之外执行。obfuscated 对 WebSocket 连接必须为 falsegotd
// 升级处理器已剥离 obfuscated2 并补回 codec tag
func (s *Server) promoteConn(raw net.Conn, obfuscated bool) (transport.Conn, error) {
var ln net.Listener = newSingleConnListener(raw)
if obfuscated {
ln = transport.ObfuscatedListener(ln)
}
return newCompatTransportListener(s.codec, ln).Accept()
}
// serveConn 处理单个传输连接:读帧并按 auth_key_id 分流。
//
// - auth_key_id == 0未加密的密钥交换起始消息执行握手并落地 auth key。
@ -322,18 +493,26 @@ func (s *Server) serveConn(ctx context.Context, conn transport.Conn) (err error)
continue
}
data, found, err := s.authKeys.Get(ctx, authKeyID)
if err != nil {
return fmt.Errorf("lookup auth key: %w", err)
}
if !found {
if err := s.sendProtoError(ctx, conn, codec.CodeAuthKeyNotFound); err != nil {
return err
// 已建立连接复用缓存密钥走快路径fetchedKey=nil避开每帧回查 AuthKeyStore——
// 这是 mtprotoedge 层最热的库访问点。密钥材料创建后不可变;销毁(destroy_auth_key)/
// 撤销由 SessionManager 主动 Close 连接保证失效,不依赖此被动回查。仅 destroy_auth_key
// 的发起连接置 keyDestroyed使其下一帧回落到 Get→AuthKeyNotFound维持原契约。
var fetchedKey *store.AuthKeyData
if current == nil || current.authKeyID != authKeyID || current.keyDestroyed.Load() {
d, found, err := s.authKeys.Get(ctx, authKeyID)
if err != nil {
return fmt.Errorf("lookup auth key: %w", err)
}
continue
if !found {
if err := s.sendProtoError(ctx, conn, codec.CodeAuthKeyNotFound); err != nil {
return err
}
continue
}
fetchedKey = &d
}
current, err = s.handleEncrypted(ctx, conn, cs, current, data, &b)
current, err = s.handleEncrypted(ctx, conn, cs, current, fetchedKey, &b)
if err != nil {
return err
}