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
|
package checker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"mtproxy_checker/internal/faketls"
|
"mtproxy_checker/internal/faketls"
|
||||||
"mtproxy_checker/internal/mtproxy"
|
"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 {
|
if err := faketls.WriteTLSApplicationData(conn, hdr); err != nil {
|
||||||
return fmt.Errorf("write mtproxy header: %w", err)
|
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) {
|
if errors.Is(err, tgquick.ErrPeerClosed) {
|
||||||
return fmt.Errorf("%w", ErrProxyClosed)
|
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 {
|
if _, err := conn.Write(hdr); err != nil {
|
||||||
return fmt.Errorf("write mtproxy header: %w", err)
|
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) {
|
if errors.Is(err, tgquick.ErrPeerClosed) {
|
||||||
return fmt.Errorf("%w", ErrProxyClosed)
|
return fmt.Errorf("%w", ErrProxyClosed)
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-10
@@ -4,6 +4,7 @@ package tgquick
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
crand "crypto/rand"
|
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.
|
// ErrPeerClosed is returned when the remote side closes TCP during the MTProto probe.
|
||||||
var ErrPeerClosed = errors.New("connection closed by peer during 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 {
|
func newMessageID() int64 {
|
||||||
t := time.Now().UnixNano()
|
t := time.Now().UnixNano()
|
||||||
return (t / 4) &^ 3
|
return (t / 4) &^ 3
|
||||||
@@ -105,9 +109,60 @@ func isResPQ(mtInner []byte) bool {
|
|||||||
return binary.LittleEndian.Uint32(body[0:4]) == tlResPQ
|
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).
|
// 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.
|
// 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, eeTLS bool) error {
|
func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, tlsBR *bufio.Reader) error {
|
||||||
plain, err := buildReqPQ()
|
plain, err := buildReqPQ()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("build req_pq: %w", err)
|
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...)
|
wire := append([]byte(nil), ri...)
|
||||||
enc.XORKeyStream(wire, wire)
|
enc.XORKeyStream(wire, wire)
|
||||||
if eeTLS {
|
if tlsBR != nil {
|
||||||
if err := faketls.WriteTLSApplicationData(conn, wire); err != nil {
|
if err := faketls.WriteTLSApplicationData(conn, wire); err != nil {
|
||||||
return fmt.Errorf("write tls app (req_pq): %w", err)
|
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 acc []byte
|
||||||
var br *bufio.Reader
|
|
||||||
if eeTLS {
|
|
||||||
br = bufio.NewReader(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxAcc = 256 << 10
|
const maxAcc = 256 << 10
|
||||||
for time.Now().Before(deadline) {
|
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)
|
_ = conn.SetReadDeadline(chunkDeadline)
|
||||||
var chunk []byte
|
var chunk []byte
|
||||||
var rerr error
|
var rerr error
|
||||||
if eeTLS {
|
if tlsBR != nil {
|
||||||
chunk, rerr = readNextTLS17Payload(br)
|
if err := sniffCleartextHTTP(tlsBR); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chunk, rerr = readNextTLS17Payload(tlsBR)
|
||||||
} else {
|
} else {
|
||||||
chunk, rerr = readAtMost(conn, 65536)
|
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) {
|
if errors.Is(rerr, io.EOF) {
|
||||||
return fmt.Errorf("%w", ErrPeerClosed)
|
return fmt.Errorf("%w", ErrPeerClosed)
|
||||||
}
|
}
|
||||||
|
if errors.Is(rerr, ErrCleartextHTTP) {
|
||||||
|
return rerr
|
||||||
|
}
|
||||||
return fmt.Errorf("read mtproxy stream: %w", rerr)
|
return fmt.Errorf("read mtproxy stream: %w", rerr)
|
||||||
}
|
}
|
||||||
if len(chunk) == 0 {
|
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.
|
// 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).
|
// 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.
|
// 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) {
|
func readNextTLS17Payload(br *bufio.Reader) ([]byte, error) {
|
||||||
const maxSkips = 128
|
const maxSkips = 128
|
||||||
for range maxSkips {
|
for range maxSkips {
|
||||||
|
if err := sniffCleartextHTTP(br); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
h := make([]byte, 5)
|
h := make([]byte, 5)
|
||||||
if _, err := io.ReadFull(br, h); err != nil {
|
if _, err := io.ReadFull(br, h); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ver := binary.BigEndian.Uint16(h[1:3])
|
ver := binary.BigEndian.Uint16(h[1:3])
|
||||||
if ver != 0x0303 && ver != 0x0301 {
|
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]))
|
n := int(binary.BigEndian.Uint16(h[3:5]))
|
||||||
if n <= 0 || n > 1<<20 {
|
if n <= 0 || n > 1<<20 {
|
||||||
|
|||||||
Reference in New Issue
Block a user