Update MTProxy checker documentation and logic to clarify probing modes and improve error handling. Modify README and Docker documentation to reflect changes in the -probe flag behavior, including detailed descriptions of exit codes and timeout handling. Refactor connection handling to enhance robustness and align with Telethon's approach.
This commit is contained in:
+18
-16
@@ -18,9 +18,6 @@ import (
|
||||
// ErrProxyClosed indicates the peer closed the TCP connection during the check (Telethon #1134 style).
|
||||
var ErrProxyClosed = errors.New("mtproxy closed connection after initial payload")
|
||||
|
||||
// ErrNoDataAfterHeader is returned in ProbeFast when the proxy sends nothing after the init payload within the wait window.
|
||||
var ErrNoDataAfterHeader = errors.New("no data from proxy after mtproxy header (timeout)")
|
||||
|
||||
// Check runs Fake-TLS/dd handshake and MTProxy init; further steps depend on opts.Probe (see Options). opts nil => ProbeFast.
|
||||
func Check(ctx context.Context, host string, port int, parsed *secret.Parsed, dcID int16, opts *Options) error {
|
||||
conn, err := dialTCP(ctx, host, port)
|
||||
@@ -54,6 +51,7 @@ func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
||||
if _, err := conn.Write(ch.Record); err != nil {
|
||||
return fmt.Errorf("write client hello: %w", err)
|
||||
}
|
||||
// ServerHello читаем с сырого conn (ровно длина записи), как в tdesktop Read — без readahead.
|
||||
resp, err := faketls.ReadServerHello(conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read server hello: %w", err)
|
||||
@@ -69,11 +67,12 @@ func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
||||
if err := faketls.WriteTLSApplicationData(conn, hdr); err != nil {
|
||||
return fmt.Errorf("write mtproxy header: %w", err)
|
||||
}
|
||||
if o.Probe == ProbeFast {
|
||||
return waitPostPayload(conn)
|
||||
}
|
||||
// После init один bufio на чтение TLS 0x17 (Telethon/telemt: не смешивать conn.Read и буфер).
|
||||
br := bufio.NewReader(conn)
|
||||
if err := tgquick.DrainPostInitEE(ctx, br, conn, dec, 2*time.Second); err != nil {
|
||||
if o.Probe == ProbeFast {
|
||||
return waitPostPayload(br, conn)
|
||||
}
|
||||
if err := tgquick.DrainPostInitEE(ctx, br, conn, dec, 5*time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, br); err != nil {
|
||||
@@ -94,7 +93,7 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
||||
return fmt.Errorf("write mtproxy header: %w", err)
|
||||
}
|
||||
if o.Probe == ProbeFast {
|
||||
return waitPostPayload(conn)
|
||||
return waitPostPayload(conn, conn)
|
||||
}
|
||||
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, nil); err != nil {
|
||||
if errors.Is(err, tgquick.ErrPeerClosed) {
|
||||
@@ -105,29 +104,32 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitPostPayload(conn net.Conn) error {
|
||||
// waitPostPayload после init повторяет логику Telethon TcpMTProxy._connect (#1134):
|
||||
// ждём появления данных или таймаут, затем считаем успехом только если соединение не закрыто сразу после payload.
|
||||
// Наличие входящих байтов не обязательно — часть прокси молчит до первого MTProto от клиента.
|
||||
func waitPostPayload(r io.Reader, c net.Conn) error {
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
buf := make([]byte, 4096)
|
||||
for time.Now().Before(deadline) {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
||||
n, err := conn.Read(buf)
|
||||
_ = c.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
||||
n, err := r.Read(buf)
|
||||
if n > 0 {
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
_ = c.SetReadDeadline(time.Time{})
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
_ = c.SetReadDeadline(time.Time{})
|
||||
return ErrProxyClosed
|
||||
}
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() {
|
||||
continue
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
_ = c.SetReadDeadline(time.Time{})
|
||||
return err
|
||||
}
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
return ErrNoDataAfterHeader
|
||||
_ = c.SetReadDeadline(time.Time{})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import "strings"
|
||||
type ProbeMode int
|
||||
|
||||
const (
|
||||
// ProbeFast: handshake + init, then any inbound data within a short window (legacy behavior; fast).
|
||||
// ProbeFast: handshake + init, then short wait like Telethon TcpMTProxy (#1134): OK if proxy does not close immediately.
|
||||
ProbeFast ProbeMode = iota
|
||||
// ProbeDeep: MTProto req_pq and expect resPQ from Telegram DC through the tunnel (stricter; slower).
|
||||
ProbeDeep
|
||||
|
||||
@@ -16,9 +16,6 @@ func Classify(err error) (exitCode int, message string) {
|
||||
if errors.Is(err, checker.ErrProxyClosed) {
|
||||
return 3, err.Error()
|
||||
}
|
||||
if errors.Is(err, checker.ErrNoDataAfterHeader) {
|
||||
return 1, err.Error()
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return 4, "timeout"
|
||||
}
|
||||
|
||||
@@ -81,10 +81,14 @@ func InitHeader(secret16 []byte, dcID int16) (header64 []byte, enc cipher.Stream
|
||||
binary.LittleEndian.PutUint16(dcBytes, uint16(dcID))
|
||||
copy(buf[60:62], dcBytes)
|
||||
|
||||
encChunk := make([]byte, 8)
|
||||
copy(encChunk, buf[56:64])
|
||||
enc.XORKeyStream(encChunk, encChunk)
|
||||
copy(buf[56:64], encChunk)
|
||||
// Как в Telethon MTProxyIO.init_header: encrypt(bytes(random)) на всём 64-байтном буфере,
|
||||
// на провод уходит только random[0:56] как есть и random[56:64] = out[56:64].
|
||||
// CTR enc должен продвинуться на 64 байта — иначе req_pq и чтение DC расходятся с прокси.
|
||||
work := make([]byte, 64)
|
||||
copy(work, buf)
|
||||
out := make([]byte, 64)
|
||||
enc.XORKeyStream(out, work)
|
||||
copy(buf[56:64], out[56:64])
|
||||
|
||||
return buf, enc, dec, nil
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ func isResPQ(mtInner []byte) bool {
|
||||
// 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 {
|
||||
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
||||
end := time.Now().Add(maxWait)
|
||||
if d, ok := ctx.Deadline(); ok && d.Before(end) {
|
||||
end = d
|
||||
@@ -139,6 +140,9 @@ func DrainPostInitEE(ctx context.Context, br *bufio.Reader, conn net.Conn, dec c
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(d))
|
||||
if err := sniffCleartextHTTP(br); err != nil {
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
chunk, err := readNextTLS17Payload(br)
|
||||
@@ -219,6 +223,12 @@ func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, tls
|
||||
var rerr error
|
||||
if tlsBR != nil {
|
||||
if err := sniffCleartextHTTP(tlsBR); err != nil {
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
if time.Now().Before(deadline) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
chunk, rerr = readNextTLS17Payload(tlsBR)
|
||||
|
||||
Reference in New Issue
Block a user