Enhance MTProxy connection handling by introducing DrainPostInitEE function to read and discard fake-TLS records after initialization. Update VerifyResPQ to accept a bufio.Reader for improved error handling and support for cleartext HTTP detection. Refactor checkEE and checkDD functions to utilize the new logic, ensuring robust response verification.
Publish mtproxy_checker Docker image / test (push) Successful in 7s
Publish mtproxy_checker Docker image / build-and-push (push) Successful in 53s

This commit is contained in:
Denozordec
2026-04-11 02:00:09 +07:00
parent bb1f71a6b2
commit 6a6183a994
2 changed files with 96 additions and 12 deletions
+8 -2
View File
@@ -1,10 +1,12 @@
package checker
import (
"bufio"
"context"
"errors"
"fmt"
"net"
"time"
"mtproxy_checker/internal/faketls"
"mtproxy_checker/internal/mtproxy"
@@ -62,7 +64,11 @@ func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16) e
if err := faketls.WriteTLSApplicationData(conn, hdr); err != nil {
return fmt.Errorf("write mtproxy header: %w", err)
}
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, true); err != nil {
br := bufio.NewReader(conn)
if err := tgquick.DrainPostInitEE(ctx, br, conn, dec, 2*time.Second); err != nil {
return err
}
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, br); err != nil {
if errors.Is(err, tgquick.ErrPeerClosed) {
return fmt.Errorf("%w", ErrProxyClosed)
}
@@ -79,7 +85,7 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16) e
if _, err := conn.Write(hdr); err != nil {
return fmt.Errorf("write mtproxy header: %w", err)
}
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, false); err != nil {
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, nil); err != nil {
if errors.Is(err, tgquick.ErrPeerClosed) {
return fmt.Errorf("%w", ErrProxyClosed)
}
+88 -10
View File
@@ -4,6 +4,7 @@ package tgquick
import (
"bufio"
"bytes"
"context"
"crypto/cipher"
crand "crypto/rand"
@@ -29,6 +30,9 @@ var ErrNoResPQ = errors.New("no resPQ from telegram through proxy (tunnel may be
// 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
@@ -105,9 +109,60 @@ func isResPQ(mtInner []byte) bool {
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)
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 := 200 * time.Millisecond
if rem := time.Until(end); 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).
// 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 {
// 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)
@@ -118,7 +173,7 @@ func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, eeT
}
wire := append([]byte(nil), ri...)
enc.XORKeyStream(wire, wire)
if eeTLS {
if tlsBR != nil {
if err := faketls.WriteTLSApplicationData(conn, wire); err != nil {
return fmt.Errorf("write tls app (req_pq): %w", err)
}
@@ -134,10 +189,6 @@ func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, eeT
}
var acc []byte
var br *bufio.Reader
if eeTLS {
br = bufio.NewReader(conn)
}
const maxAcc = 256 << 10
for time.Now().Before(deadline) {
@@ -153,8 +204,11 @@ func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, eeT
_ = conn.SetReadDeadline(chunkDeadline)
var chunk []byte
var rerr error
if eeTLS {
chunk, rerr = readNextTLS17Payload(br)
if tlsBR != nil {
if err := sniffCleartextHTTP(tlsBR); err != nil {
return err
}
chunk, rerr = readNextTLS17Payload(tlsBR)
} else {
chunk, rerr = readAtMost(conn, 65536)
}
@@ -168,6 +222,9 @@ func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, eeT
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 {
@@ -197,16 +254,37 @@ func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, eeT
// 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 {
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])
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 {