Highly-opinionated (ex-bullshit-free) MTPROTO proxy for Telegram. If you use v1.0 or upgrade broke you proxy, please read the chapter Version 2
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. // Package dcprobe verifies that a TCP endpoint is a real Telegram DC by
  2. // performing the unauthenticated first step of the MTProto handshake
  3. // (req_pq_multi -> resPQ) on top of mtg's existing obfuscated2 transport.
  4. //
  5. // No auth_key is generated; no long-lived state is introduced. Two TL
  6. // messages, one round-trip. A generic listener cannot fake the reply
  7. // because it must echo back our random nonce in resPQ.
  8. //
  9. // References:
  10. // - https://core.telegram.org/mtproto/auth_key (handshake step 1)
  11. // - https://core.telegram.org/schema/mtproto (TL schema)
  12. // - https://core.telegram.org/mtproto/mtproto-transports#padded-intermediate
  13. package dcprobe
  14. import (
  15. "bytes"
  16. "context"
  17. "crypto/rand"
  18. "encoding/binary"
  19. "errors"
  20. "fmt"
  21. "io"
  22. "net"
  23. "time"
  24. "github.com/9seconds/mtg/v2/essentials"
  25. "github.com/9seconds/mtg/v2/mtglib/obfuscation"
  26. )
  27. // MTProto wire constants (https://core.telegram.org/schema/mtproto).
  28. //
  29. // req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
  30. // resPQ#05162463 nonce:int128 server_nonce:int128 pq:string
  31. // server_public_key_fingerprints:Vector<long> = ResPQ;
  32. const (
  33. ctorReqPQMulti uint32 = 0xbe7e8ef1
  34. ctorResPQ uint32 = 0x05162463
  35. // Minimum legal resPQ frame: 20-byte unencrypted-message envelope +
  36. // 4-byte ctor + 16-byte nonce echo. Anything below cannot be a resPQ.
  37. minResPQFrame = 20 + 4 + 16
  38. // Upper bound: real resPQ replies are ~84 bytes (envelope + ~64-byte
  39. // payload). 256 is comfortable headroom; anything beyond is hostile or
  40. // not Telegram.
  41. maxResPQFrame = 256
  42. )
  43. // Probe sends req_pq_multi over an obfuscated2 + padded-intermediate transport
  44. // and verifies that the peer replies with a matching resPQ.
  45. //
  46. // conn must be a freshly opened reliable byte stream (typically TCP) to a
  47. // Telegram DC, but a SOCKS/proxy-wrapped net.Conn works just as well — Probe
  48. // adapts whatever it gets to the half-close interface mtg's obfuscator
  49. // requires. Probe does NOT close conn — the caller does. dc is the DC number
  50. // (1..5) that gets baked into the obfuscated2 handshake frame.
  51. //
  52. // The returned duration is the round-trip from "first byte sent after the
  53. // obfs handshake" to "resPQ frame fully read".
  54. func Probe(ctx context.Context, conn net.Conn, dc int) (time.Duration, error) {
  55. if deadline, ok := ctx.Deadline(); ok {
  56. _ = conn.SetDeadline(deadline)
  57. defer func() { _ = conn.SetDeadline(time.Time{}) }()
  58. }
  59. // Honour ctx cancellation as well as its deadline: a parent ctx that is
  60. // canceled (without an earlier deadline expiring) would otherwise let
  61. // Probe block on an in-flight Read until the deadline. Forcing the
  62. // deadline to "now" makes the next syscall return an i/o timeout error
  63. // that Probe wraps and surfaces.
  64. stop := context.AfterFunc(ctx, func() {
  65. _ = conn.SetDeadline(time.Now())
  66. })
  67. defer stop()
  68. // 1. obfuscated2 handshake. Empty Secret = no MTProxy secret mixing,
  69. // which is how mtg itself talks to a DC (see mtglib/proxy.go).
  70. obfsConn, err := obfuscation.Obfuscator{}.SendHandshake(adaptConn(conn), dc)
  71. if err != nil {
  72. return 0, fmt.Errorf("obfuscated2 handshake: %w", err)
  73. }
  74. // 2. build req_pq_multi TL payload: 4-byte LE constructor + 16-byte nonce.
  75. var nonce [16]byte
  76. if _, err := rand.Read(nonce[:]); err != nil {
  77. return 0, fmt.Errorf("read nonce: %w", err)
  78. }
  79. tlBody := make([]byte, 4+16)
  80. binary.LittleEndian.PutUint32(tlBody[:4], ctorReqPQMulti)
  81. copy(tlBody[4:], nonce[:])
  82. // 3. wrap in an MTProto unencrypted message envelope (per
  83. // https://core.telegram.org/mtproto/description#unencrypted-message):
  84. // auth_key_id:long(=0) | message_id:long | message_data_length:int | message_data:bytes
  85. // Without this envelope the DC silently drops the connection.
  86. msg := make([]byte, 8+8+4+len(tlBody))
  87. // auth_key_id = 0 (already zeroed by make)
  88. binary.LittleEndian.PutUint64(msg[8:16], generateMessageID())
  89. binary.LittleEndian.PutUint32(msg[16:20], uint32(len(tlBody)))
  90. copy(msg[20:], tlBody)
  91. // 4. wrap in a padded-intermediate frame: length(LE) + msg.
  92. // Padding is allowed [0..15] but not required when len(msg) % 4 == 0.
  93. frame := make([]byte, 4+len(msg))
  94. binary.LittleEndian.PutUint32(frame[:4], uint32(len(msg)))
  95. copy(frame[4:], msg)
  96. start := time.Now()
  97. if _, err := obfsConn.Write(frame); err != nil {
  98. return 0, fmt.Errorf("write req_pq_multi: %w", err)
  99. }
  100. // 5. read padded-intermediate reply: length, then that many bytes.
  101. // The reply is itself an MTProto unencrypted message (same envelope as
  102. // what we sent), so we must skip 20 bytes to get to the resPQ TL.
  103. var lenBuf [4]byte
  104. if _, err := io.ReadFull(obfsConn, lenBuf[:]); err != nil {
  105. return 0, fmt.Errorf("read frame length: %w", err)
  106. }
  107. respLen := binary.LittleEndian.Uint32(lenBuf[:])
  108. if respLen < minResPQFrame {
  109. return 0, fmt.Errorf("%w: resPQ frame too short (%d bytes)", ErrNotTelegram, respLen)
  110. }
  111. if respLen > maxResPQFrame {
  112. return 0, fmt.Errorf("%w: resPQ frame too large (%d bytes, max %d)", ErrNotTelegram, respLen, maxResPQFrame)
  113. }
  114. resp := make([]byte, respLen)
  115. if _, err := io.ReadFull(obfsConn, resp); err != nil {
  116. return 0, fmt.Errorf("read resPQ frame: %w", err)
  117. }
  118. rtt := time.Since(start)
  119. // 6. unwrap the MTProto envelope: skip auth_key_id(8) + message_id(8) +
  120. // message_data_length(4) = 20 bytes.
  121. tlResp := resp[20:]
  122. // 7. verify constructor and nonce echo. We deliberately do not parse
  123. // server_nonce, pq, or fingerprints — they are not needed to prove
  124. // the peer can speak MTProto.
  125. if got := binary.LittleEndian.Uint32(tlResp[:4]); got != ctorResPQ {
  126. return rtt, fmt.Errorf("%w: got constructor 0x%08x, want resPQ 0x%08x", ErrNotTelegram, got, ctorResPQ)
  127. }
  128. if !bytes.Equal(tlResp[4:4+16], nonce[:]) {
  129. return rtt, fmt.Errorf("%w: nonce echo mismatch", ErrNotTelegram)
  130. }
  131. return rtt, nil
  132. }
  133. // generateMessageID returns an MTProto message_id roughly synchronized with
  134. // server time, with the lower 2 bits cleared (client-to-server requests).
  135. // See https://core.telegram.org/mtproto/description#message-identifier-msg-id.
  136. func generateMessageID() uint64 {
  137. nano := uint64(time.Now().UnixNano())
  138. sec := nano / 1_000_000_000
  139. nsInSec := nano % 1_000_000_000
  140. subsec := (nsInSec << 32) / 1_000_000_000
  141. id := (sec << 32) | subsec
  142. return id &^ 3
  143. }
  144. // ErrNotTelegram is returned (wrapped) when the peer's reply is not a
  145. // well-formed resPQ matching our nonce. Use errors.Is to distinguish
  146. // "the TCP connection was OK but the peer is not a Telegram DC" from
  147. // transport errors.
  148. var ErrNotTelegram = errors.New("peer did not respond with a matching resPQ")
  149. // adaptConn returns conn as essentials.Conn if it already satisfies the
  150. // interface (typically *net.TCPConn), otherwise wraps it with no-op
  151. // CloseRead/CloseWrite. mtg's obfuscator only ever calls Read/Write/Close,
  152. // so the no-ops are safe.
  153. func adaptConn(conn net.Conn) essentials.Conn {
  154. if ec, ok := conn.(essentials.Conn); ok {
  155. return ec
  156. }
  157. return halfCloseShim{Conn: conn}
  158. }
  159. type halfCloseShim struct{ net.Conn }
  160. func (halfCloseShim) CloseRead() error { return nil }
  161. func (halfCloseShim) CloseWrite() error { return nil }