Files

187 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package checker
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net"
"syscall"
"time"
"mtproxy_checker/internal/faketls"
"mtproxy_checker/internal/mtproxy"
"mtproxy_checker/internal/secret"
"mtproxy_checker/internal/tgquick"
)
// ErrProxyClosed indicates the peer closed the TCP connection during the check (Telethon #1134 style).
var ErrProxyClosed = errors.New("mtproxy closed connection after initial payload")
// 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)
if err != nil {
return fmt.Errorf("tcp dial: %w", err)
}
defer conn.Close()
o := effectiveOpts(opts)
switch parsed.Kind {
case secret.KindEE:
return checkEE(ctx, conn, parsed, dcID, o)
case secret.KindDD:
return checkDD(ctx, conn, parsed, dcID, o)
default:
return fmt.Errorf("unknown secret kind")
}
}
func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o *Options) error {
// ee-секрет хранит домен как «сырой» хвост (часто 0xd0 + ASCII hostname). В TLS SNI нужен только hostname,
// как в официальном клиенте Telegram — иначе прокси сбрасывает соединение до ServerHello.
sni := faketls.SNIDomain(p.Domain)
if len(sni) == 0 {
sni = p.Domain
}
ch, err := faketls.BuildTdesktopClientHello(p.Key, sni)
if err != nil {
return fmt.Errorf("fake-tls client hello: %w", err)
}
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)
}
if err := faketls.VerifyServerHelloTdesktop(resp, p.Key, ch.RandomField); err != nil {
return fmt.Errorf("verify server hello: %w", err)
}
hdr, enc, dec, err := mtproxy.InitHeader(p.Key, dcID)
if err != nil {
return fmt.Errorf("mtproxy header: %w", err)
}
if err := faketls.WriteTLSApplicationData(conn, hdr); err != nil {
return fmt.Errorf("write mtproxy header: %w", err)
}
// После init один bufio на чтение TLS 0x17 (Telethon/telemt: не смешивать conn.Read и буфер).
br := bufio.NewReader(conn)
if o.Probe == ProbeFast {
return waitPostPayload(br, conn)
}
if err := tgquick.DrainPostInitEE(ctx, br, conn, dec, 5*time.Second); err != nil {
return err
}
strict := o.Probe == ProbeDeepStrict
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, br, strict); err != nil {
if errors.Is(err, tgquick.ErrPeerClosed) {
return fmt.Errorf("%w", ErrProxyClosed)
}
return err
}
return nil
}
func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o *Options) error {
hdr, enc, dec, err := mtproxy.InitHeader(p.Key, dcID)
if err != nil {
return fmt.Errorf("mtproxy header: %w", err)
}
if _, err := conn.Write(hdr); err != nil {
return fmt.Errorf("write mtproxy header: %w", err)
}
if o.Probe == ProbeFast {
return waitPostPayload(conn, conn)
}
strict := o.Probe == ProbeDeepStrict
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, nil, strict); err != nil {
if errors.Is(err, tgquick.ErrPeerClosed) {
return fmt.Errorf("%w", ErrProxyClosed)
}
return err
}
return nil
}
// isPeerClosedReadErr reports RST/half-close style errors from Read (not timeouts).
func isPeerClosedReadErr(err error) bool {
if err == nil {
return false
}
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
return false
}
return errors.Is(err, syscall.ECONNRESET) ||
errors.Is(err, syscall.ECONNABORTED) ||
errors.Is(err, syscall.EPIPE) ||
errors.Is(err, syscall.ENOTCONN)
}
// waitPostPayload после init повторяет логику Telethon TcpMTProxy._connect (#1134):
// ждём появления данных или таймаут, затем считаем успехом только если соединение не закрыто сразу после payload.
// Наличие входящих байтов не обязательно — часть прокси молчит до первого MTProto от клиента.
// После окна idle дополнительно читаем короткими интервалами: часть прокси шлёт FIN/RST сразу после init,
// но не в первых 200ms цикла — без этого fast давал бы ложный OK.
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) {
_ = c.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
n, err := r.Read(buf)
if n > 0 {
_ = c.SetReadDeadline(time.Time{})
return nil
}
if err != nil {
if errors.Is(err, io.EOF) {
_ = c.SetReadDeadline(time.Time{})
return ErrProxyClosed
}
if isPeerClosedReadErr(err) {
_ = c.SetReadDeadline(time.Time{})
return ErrProxyClosed
}
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
continue
}
_ = c.SetReadDeadline(time.Time{})
return err
}
}
// Отложенное закрытие со стороны прокси (после того как «молчали» в основном окне).
const probeEach = 400 * time.Millisecond
const probeRounds = 4
for probes := 0; probes < probeRounds; probes++ {
_ = c.SetReadDeadline(time.Now().Add(probeEach))
n, err := r.Read(buf)
if n > 0 {
_ = c.SetReadDeadline(time.Time{})
return nil
}
if err != nil {
if errors.Is(err, io.EOF) {
_ = c.SetReadDeadline(time.Time{})
return ErrProxyClosed
}
if isPeerClosedReadErr(err) {
_ = c.SetReadDeadline(time.Time{})
return ErrProxyClosed
}
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
continue
}
_ = c.SetReadDeadline(time.Time{})
return err
}
}
_ = c.SetReadDeadline(time.Time{})
return nil
}