Files
mtproxy_checker/internal/tgquick/probe.go
T

244 lines
6.6 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 tgquick performs a minimal MTProto step (req_pq → resPQ) through an established MTProxy tunnel,
// matching Telethon's RandomizedIntermediate + MTProxyIO framing.
package tgquick
import (
"bufio"
"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")
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 03 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
}
// VerifyResPQ sends req_pq and waits for resPQ on the MTProxy byte stream (decrypt with dec, encrypt with enc).
// If eeTLS is true, each MTProxy chunk is wrapped in TLS 1.2 application records (0x17) as in checkEE.
func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, eeTLS bool) 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 eeTLS {
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(15 * time.Second)
}
var acc []byte
var br *bufio.Reader
if eeTLS {
br = bufio.NewReader(conn)
}
const maxAcc = 256 << 10
for time.Now().Before(deadline) {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
chunkDeadline := time.Now().Add(3 * time.Second)
if chunkDeadline.After(deadline) {
chunkDeadline = deadline
}
_ = conn.SetReadDeadline(chunkDeadline)
var chunk []byte
var rerr error
if eeTLS {
chunk, rerr = readNextTLS17Payload(br)
} 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)
}
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 readNextTLS17Payload(br *bufio.Reader) ([]byte, error) {
const maxSkips = 128
for range maxSkips {
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 {
return nil, fmt.Errorf("unexpected tls record version 0x%04x (type 0x%02x); if type is 0x48 ('H') the peer may be speaking HTTP on this socket", 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
}