134 lines
4.0 KiB
Go
134 lines
4.0 KiB
Go
package checker
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"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")
|
||
|
||
// 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)
|
||
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)
|
||
}
|
||
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)
|
||
}
|
||
if o.Probe == ProbeFast {
|
||
return waitPostPayload(conn)
|
||
}
|
||
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)
|
||
}
|
||
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)
|
||
}
|
||
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, nil); err != nil {
|
||
if errors.Is(err, tgquick.ErrPeerClosed) {
|
||
return fmt.Errorf("%w", ErrProxyClosed)
|
||
}
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func waitPostPayload(conn 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)
|
||
if n > 0 {
|
||
_ = conn.SetReadDeadline(time.Time{})
|
||
return nil
|
||
}
|
||
if err != nil {
|
||
if errors.Is(err, io.EOF) {
|
||
_ = conn.SetReadDeadline(time.Time{})
|
||
return ErrProxyClosed
|
||
}
|
||
var ne net.Error
|
||
if errors.As(err, &ne) && ne.Timeout() {
|
||
continue
|
||
}
|
||
_ = conn.SetReadDeadline(time.Time{})
|
||
return err
|
||
}
|
||
}
|
||
_ = conn.SetReadDeadline(time.Time{})
|
||
return ErrNoDataAfterHeader
|
||
}
|