diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 35e426e..2249d65 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -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) } diff --git a/internal/tgquick/probe.go b/internal/tgquick/probe.go index e2c7289..f31176b 100644 --- a/internal/tgquick/probe.go +++ b/internal/tgquick/probe.go @@ -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 {