187 lines
6.1 KiB
Go
187 lines
6.1 KiB
Go
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
|
||
}
|