335 lines
9.2 KiB
Go
335 lines
9.2 KiB
Go
// Package tgquick performs a minimal MTProto step (req_pq → resPQ) through an established MTProxy tunnel,
|
||
// matching Telethon's RandomizedIntermediate + MTProxyIO framing.
|
||
package tgquick
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"context"
|
||
"crypto/cipher"
|
||
crand "crypto/rand"
|
||
"encoding/binary"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"time"
|
||
|
||
"mtproxy_checker/internal/faketls"
|
||
)
|
||
|
||
const (
|
||
tlReqPQ = 0xd712e4be
|
||
tlResPQ = 0x05162463
|
||
maxRI = 1 << 20
|
||
)
|
||
|
||
// ErrNoResPQ means no valid resPQ was received from Telegram through the tunnel.
|
||
var ErrNoResPQ = errors.New("no resPQ from telegram through proxy (tunnel may be broken)")
|
||
|
||
// ErrPeerClosed is returned when the remote side closes TCP during the MTProto probe.
|
||
var ErrPeerClosed = errors.New("connection closed by peer during mtproto probe")
|
||
|
||
// ErrCleartextHTTP means the peer sent a plaintext HTTP line instead of continuing fake-TLS (often a front nginx 400/502).
|
||
var ErrCleartextHTTP = errors.New("cleartext http on socket (not fake-tls mtproxy payload)")
|
||
|
||
func newMessageID() int64 {
|
||
t := time.Now().UnixNano()
|
||
return (t / 4) &^ 3
|
||
}
|
||
|
||
func buildReqPQ() ([]byte, error) {
|
||
nonce := make([]byte, 16)
|
||
if _, err := io.ReadFull(crand.Reader, nonce); err != nil {
|
||
return nil, err
|
||
}
|
||
body := make([]byte, 4+16)
|
||
binary.LittleEndian.PutUint32(body, tlReqPQ)
|
||
copy(body[4:], nonce)
|
||
return wrapUnencrypted(body), nil
|
||
}
|
||
|
||
func wrapUnencrypted(msgData []byte) []byte {
|
||
out := make([]byte, 8+8+4+len(msgData))
|
||
// auth_key_id = 0
|
||
binary.LittleEndian.PutUint64(out[8:], uint64(newMessageID()))
|
||
binary.LittleEndian.PutUint32(out[16:], uint32(len(msgData)))
|
||
copy(out[20:], msgData)
|
||
return out
|
||
}
|
||
|
||
// randomizedIntermediateEncode packs payload with 0–3 random padding bytes (Telethon RandomizedIntermediatePacketCodec).
|
||
func randomizedIntermediateEncode(payload []byte) ([]byte, error) {
|
||
var rnd [1]byte
|
||
if _, err := crand.Read(rnd[:]); err != nil {
|
||
return nil, err
|
||
}
|
||
pad := int(rnd[0] % 4)
|
||
padding := make([]byte, pad)
|
||
if _, err := crand.Read(padding); err != nil {
|
||
return nil, err
|
||
}
|
||
body := append(append([]byte{}, payload...), padding...)
|
||
out := make([]byte, 4+len(body))
|
||
binary.LittleEndian.PutUint32(out, uint32(len(body)))
|
||
copy(out[4:], body)
|
||
return out, nil
|
||
}
|
||
|
||
func popRI(acc *[]byte) ([]byte, bool) {
|
||
a := *acc
|
||
if len(a) < 4 {
|
||
return nil, false
|
||
}
|
||
L := int(binary.LittleEndian.Uint32(a[0:4]))
|
||
if L < 0 || L > maxRI || len(a) < 4+L {
|
||
return nil, false
|
||
}
|
||
frame := a[4 : 4+L]
|
||
*acc = append([]byte(nil), a[4+L:]...)
|
||
pad := len(frame) % 4
|
||
if pad > 0 {
|
||
frame = frame[:len(frame)-pad]
|
||
}
|
||
return frame, true
|
||
}
|
||
|
||
func isResPQ(mtInner []byte) bool {
|
||
if len(mtInner) < 20 {
|
||
return false
|
||
}
|
||
if binary.LittleEndian.Uint64(mtInner[0:8]) != 0 {
|
||
return false
|
||
}
|
||
ml := int(binary.LittleEndian.Uint32(mtInner[16:20]))
|
||
if ml < 4 || ml > maxRI || 20+ml > len(mtInner) {
|
||
return false
|
||
}
|
||
body := mtInner[20 : 20+ml]
|
||
return binary.LittleEndian.Uint32(body[0:4]) == tlResPQ
|
||
}
|
||
|
||
// DrainPostInitEE reads inbound fake-TLS records after MTProxy init and before req_pq (Telethon TcpMTProxy waits for data ~2s).
|
||
// Consumes 0x17 payloads with dec so the CTR stream stays aligned with the server; discards MTProto frames until idle.
|
||
func DrainPostInitEE(ctx context.Context, br *bufio.Reader, conn net.Conn, dec cipher.Stream, maxWait time.Duration) error {
|
||
end := time.Now().Add(maxWait)
|
||
if d, ok := ctx.Deadline(); ok && d.Before(end) {
|
||
end = d
|
||
}
|
||
var acc []byte
|
||
const maxDrain = 256 << 10
|
||
drained := 0
|
||
for drained < maxDrain && time.Now().Before(end) {
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
default:
|
||
}
|
||
d := 500 * time.Millisecond
|
||
if rem := time.Until(end); rem < d {
|
||
d = rem
|
||
}
|
||
if cd, ok := ctx.Deadline(); ok {
|
||
if rem := time.Until(cd); rem < d {
|
||
d = rem
|
||
}
|
||
}
|
||
if d <= 0 {
|
||
return nil
|
||
}
|
||
_ = conn.SetReadDeadline(time.Now().Add(d))
|
||
if err := sniffCleartextHTTP(br); err != nil {
|
||
return err
|
||
}
|
||
chunk, err := readNextTLS17Payload(br)
|
||
if err != nil {
|
||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||
return nil
|
||
}
|
||
if errors.Is(err, ErrCleartextHTTP) {
|
||
return err
|
||
}
|
||
if errors.Is(err, io.EOF) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("post-init drain: %w", err)
|
||
}
|
||
dec.XORKeyStream(chunk, chunk)
|
||
drained += len(chunk)
|
||
acc = append(acc, chunk...)
|
||
for {
|
||
frame, ok := popRI(&acc)
|
||
if !ok {
|
||
break
|
||
}
|
||
_ = frame
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// VerifyResPQ sends req_pq and waits for resPQ on the MTProxy byte stream (decrypt with dec, encrypt with enc).
|
||
// For ee (fake-TLS), pass the same bufio.Reader used after init (and DrainPostInitEE); for dd pass tlsBR=nil.
|
||
func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, tlsBR *bufio.Reader) error {
|
||
plain, err := buildReqPQ()
|
||
if err != nil {
|
||
return fmt.Errorf("build req_pq: %w", err)
|
||
}
|
||
ri, err := randomizedIntermediateEncode(plain)
|
||
if err != nil {
|
||
return fmt.Errorf("randomized intermediate: %w", err)
|
||
}
|
||
wire := append([]byte(nil), ri...)
|
||
enc.XORKeyStream(wire, wire)
|
||
if tlsBR != nil {
|
||
if err := faketls.WriteTLSApplicationData(conn, wire); err != nil {
|
||
return fmt.Errorf("write tls app (req_pq): %w", err)
|
||
}
|
||
} else {
|
||
if _, err := conn.Write(wire); err != nil {
|
||
return fmt.Errorf("write req_pq: %w", err)
|
||
}
|
||
}
|
||
|
||
deadline, ok := ctx.Deadline()
|
||
if !ok {
|
||
deadline = time.Now().Add(60 * time.Second)
|
||
}
|
||
|
||
var acc []byte
|
||
|
||
const maxAcc = 256 << 10
|
||
// Один read ждёт ответ DC; 3s мало при нагрузке/гео — держим до 30s, но не дольше ctx.
|
||
const perReadSlack = 30 * time.Second
|
||
for time.Now().Before(deadline) {
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
default:
|
||
}
|
||
chunkDeadline := time.Now().Add(perReadSlack)
|
||
if chunkDeadline.After(deadline) {
|
||
chunkDeadline = deadline
|
||
}
|
||
if !chunkDeadline.After(time.Now().Add(50 * time.Millisecond)) {
|
||
return fmt.Errorf("%w: not enough time left waiting for resPQ", context.DeadlineExceeded)
|
||
}
|
||
_ = conn.SetReadDeadline(chunkDeadline)
|
||
var chunk []byte
|
||
var rerr error
|
||
if tlsBR != nil {
|
||
if err := sniffCleartextHTTP(tlsBR); err != nil {
|
||
return err
|
||
}
|
||
chunk, rerr = readNextTLS17Payload(tlsBR)
|
||
} else {
|
||
chunk, rerr = readAtMost(conn, 65536)
|
||
}
|
||
if rerr != nil {
|
||
if ne, ok := rerr.(net.Error); ok && ne.Timeout() {
|
||
if time.Now().Before(deadline) {
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
if errors.Is(rerr, io.EOF) {
|
||
return fmt.Errorf("%w", ErrPeerClosed)
|
||
}
|
||
if errors.Is(rerr, ErrCleartextHTTP) {
|
||
return rerr
|
||
}
|
||
return fmt.Errorf("read mtproxy stream: %w", rerr)
|
||
}
|
||
if len(chunk) == 0 {
|
||
continue
|
||
}
|
||
dec.XORKeyStream(chunk, chunk)
|
||
acc = append(acc, chunk...)
|
||
if len(acc) > maxAcc {
|
||
return fmt.Errorf("mtproxy read buffer overflow")
|
||
}
|
||
for {
|
||
frame, ok := popRI(&acc)
|
||
if !ok {
|
||
break
|
||
}
|
||
if isResPQ(frame) {
|
||
_ = conn.SetReadDeadline(time.Time{})
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
_ = conn.SetReadDeadline(time.Time{})
|
||
return ErrNoResPQ
|
||
}
|
||
|
||
// readNextTLS17Payload reads TLS 1.2-style records from the wire (plaintext record headers).
|
||
// Handshake (0x16) and ChangeCipherSpec (0x15) payloads are not MTProxy-CTR ciphertext — skip them.
|
||
// Only application data (0x17) inner bytes are passed through the MTProxy decrypt stream (caller XORs).
|
||
// Some proxies send extra handshake records after the fake ServerHello; clients skip them before MTProto.
|
||
func sniffCleartextHTTP(br *bufio.Reader) error {
|
||
p, err := br.Peek(8)
|
||
if len(p) >= 4 && bytes.HasPrefix(p, []byte("HTTP")) {
|
||
line := string(p)
|
||
if idx := bytes.IndexByte(p, '\n'); idx >= 0 {
|
||
line = string(p[:idx])
|
||
}
|
||
return fmt.Errorf("%w: %s", ErrCleartextHTTP, line)
|
||
}
|
||
if err != nil && !errors.Is(err, bufio.ErrBufferFull) && err != io.EOF {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func readNextTLS17Payload(br *bufio.Reader) ([]byte, error) {
|
||
const maxSkips = 128
|
||
for range maxSkips {
|
||
if err := sniffCleartextHTTP(br); err != nil {
|
||
return nil, err
|
||
}
|
||
h := make([]byte, 5)
|
||
if _, err := io.ReadFull(br, h); err != nil {
|
||
return nil, err
|
||
}
|
||
ver := binary.BigEndian.Uint16(h[1:3])
|
||
if ver != 0x0303 && ver != 0x0301 {
|
||
if h[0] == 'H' && h[1] == 'T' {
|
||
return nil, fmt.Errorf("%w: misaligned or non-tls data (type 0x%02x version 0x%04x)", ErrCleartextHTTP, h[0], ver)
|
||
}
|
||
return nil, fmt.Errorf("unexpected tls record version 0x%04x (type 0x%02x)", ver, h[0])
|
||
}
|
||
n := int(binary.BigEndian.Uint16(h[3:5]))
|
||
if n <= 0 || n > 1<<20 {
|
||
return nil, fmt.Errorf("invalid tls record length %d", n)
|
||
}
|
||
payload := make([]byte, n)
|
||
if _, err := io.ReadFull(br, payload); err != nil {
|
||
return nil, err
|
||
}
|
||
switch h[0] {
|
||
case 0x17: // application data — inner bytes are MTProxy-obfuscated
|
||
return payload, nil
|
||
case 0x16, 0x15: // more handshake / CCS after ServerHello — plaintext, do not advance CTR interpretation here
|
||
continue
|
||
case 0x14: // alert
|
||
return nil, fmt.Errorf("tls alert record from proxy (len=%d)", n)
|
||
default:
|
||
return nil, fmt.Errorf("unexpected tls record type 0x%02x (len=%d); 0x48 often means cleartext HTTP if the stream is misaligned", h[0], n)
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("too many non-application tls records before 0x17")
|
||
}
|
||
|
||
func readAtMost(conn net.Conn, max int) ([]byte, error) {
|
||
buf := make([]byte, max)
|
||
n, err := conn.Read(buf)
|
||
if n > 0 {
|
||
if err == io.EOF {
|
||
err = nil
|
||
}
|
||
return buf[:n], err
|
||
}
|
||
return nil, err
|
||
}
|