110 lines
3.3 KiB
Go
110 lines
3.3 KiB
Go
package checker
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"time"
|
||
|
||
"mtproxy_checker/internal/faketls"
|
||
"mtproxy_checker/internal/mtproxy"
|
||
"mtproxy_checker/internal/secret"
|
||
)
|
||
|
||
// ErrProxyClosed indicates the MTProxy dropped the TCP connection right after the probe (Telethon #1134 style).
|
||
var ErrProxyClosed = errors.New("mtproxy closed connection after initial payload")
|
||
|
||
// ErrNoDataAfterHeader indicates the proxy accepted the probe but sent no application data back within the wait window.
|
||
// Treating silence as success matched some servers but often disagrees with Telegram client "unavailable".
|
||
var ErrNoDataAfterHeader = errors.New("no data from proxy after mtproxy header (timeout)")
|
||
|
||
// Check runs the handshake probe for the given parsed secret.
|
||
func Check(ctx context.Context, host string, port int, parsed *secret.Parsed, dcID int16) error {
|
||
conn, err := dialTCP(ctx, host, port)
|
||
if err != nil {
|
||
return fmt.Errorf("tcp dial: %w", err)
|
||
}
|
||
defer conn.Close()
|
||
|
||
switch parsed.Kind {
|
||
case secret.KindEE:
|
||
return checkEE(conn, parsed, dcID)
|
||
case secret.KindDD:
|
||
return checkDD(conn, parsed, dcID)
|
||
default:
|
||
return fmt.Errorf("unknown secret kind")
|
||
}
|
||
}
|
||
|
||
func checkEE(conn net.Conn, p *secret.Parsed, dcID int16) 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, _, _, 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)
|
||
}
|
||
return waitPostPayload(conn)
|
||
}
|
||
|
||
func checkDD(conn net.Conn, p *secret.Parsed, dcID int16) error {
|
||
hdr, _, _, 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)
|
||
}
|
||
return waitPostPayload(conn)
|
||
}
|
||
|
||
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
|
||
}
|