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.
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user