Enhance MTProxy checker functionality by introducing a new -probe flag for selecting between fast and deep probing modes. Update README and Docker documentation to reflect this change, including details on timeout handling and exit codes for improved clarity.
This commit is contained in:
@@ -20,7 +20,7 @@ go build -o mtproxy_checker.exe ./cmd/mtproxy_checker
|
|||||||
.\mtproxy_checker.exe --server HOST --port PORT --secret HEX
|
.\mtproxy_checker.exe --server HOST --port PORT --secret HEX
|
||||||
```
|
```
|
||||||
|
|
||||||
Флаги: `-timeout` (по умолчанию 15s), `-dc-id` (по умолчанию 2).
|
Флаги: `-timeout` (по умолчанию 15s), `-dc-id` (по умолчанию 2), `-probe fast|deep` (по умолчанию **`fast`** — быстрая проверка; `deep` — MTProto до DC).
|
||||||
|
|
||||||
Код выхода: `0` — OK (через прокси получен ответ Telegram DC на MTProto `req_pq` — `resPQ`), `1` — ошибка (в т.ч. нет валидного `resPQ`), `2` — неверные аргументы, `3` — прокси закрыл соединение после проверки, `4` — таймаут.
|
Код выхода: `0` — OK (через прокси получен ответ Telegram DC на MTProto `req_pq` — `resPQ`), `1` — ошибка (в т.ч. нет валидного `resPQ`), `2` — неверные аргументы, `3` — прокси закрыл соединение после проверки, `4` — таймаут.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ func main() {
|
|||||||
|
|
||||||
func run() int {
|
func run() int {
|
||||||
timeout := flag.Duration("timeout", 15*time.Second, "overall TCP/handshake timeout")
|
timeout := flag.Duration("timeout", 15*time.Second, "overall TCP/handshake timeout")
|
||||||
|
probe := flag.String("probe", "fast", "fast: handshake+init+inbound byte; deep: MTProto req_pq/resPQ via DC (stricter, slower)")
|
||||||
dcID := flag.Int("dc-id", 2, "Telegram DC id (signed int16) embedded in MTProxy header")
|
dcID := flag.Int("dc-id", 2, "Telegram DC id (signed int16) embedded in MTProxy header")
|
||||||
server := flag.String("server", "", "proxy hostname (if not using tg:// positional)")
|
server := flag.String("server", "", "proxy hostname (if not using tg:// positional)")
|
||||||
portFlag := flag.Int("port", 0, "proxy port (if not using tg:// positional)")
|
portFlag := flag.Int("port", 0, "proxy port (if not using tg:// positional)")
|
||||||
@@ -61,7 +63,8 @@ func run() int {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err = checker.Check(ctx, host, port, parsed, int16(*dcID))
|
opts := &checker.Options{Probe: checker.ParseProbe(*probe)}
|
||||||
|
err = checker.Check(ctx, host, port, parsed, int16(*dcID), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, checker.ErrProxyClosed) {
|
if errors.Is(err, checker.ErrProxyClosed) {
|
||||||
fmt.Fprintf(os.Stderr, "FAIL: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FAIL: %v\n", err)
|
||||||
@@ -71,6 +74,11 @@ func run() int {
|
|||||||
fmt.Fprintf(os.Stderr, "FAIL: timeout\n")
|
fmt.Fprintf(os.Stderr, "FAIL: timeout\n")
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
|
var ne net.Error
|
||||||
|
if errors.As(err, &ne) && ne.Timeout() {
|
||||||
|
fmt.Fprintf(os.Stderr, "FAIL: timeout\n")
|
||||||
|
return 4
|
||||||
|
}
|
||||||
fmt.Fprintf(os.Stderr, "FAIL: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FAIL: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
listFile string
|
listFile string
|
||||||
checkInterval time.Duration
|
checkInterval time.Duration
|
||||||
httpAddr string
|
httpAddr string
|
||||||
checkTimeout time.Duration
|
checkTimeout time.Duration
|
||||||
dcID int16
|
dcID int16
|
||||||
|
probe checker.ProbeMode
|
||||||
allowedPrefixes []netip.Prefix
|
allowedPrefixes []netip.Prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,12 +80,14 @@ func loadConfig() (*config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
probe := checker.ParseProbe(os.Getenv("MTPROXY_PROBE"))
|
||||||
return &config{
|
return &config{
|
||||||
listFile: listFile,
|
listFile: listFile,
|
||||||
checkInterval: interval,
|
checkInterval: interval,
|
||||||
httpAddr: httpAddr,
|
httpAddr: httpAddr,
|
||||||
checkTimeout: checkTimeout,
|
checkTimeout: checkTimeout,
|
||||||
dcID: int16(dcParsed),
|
dcID: int16(dcParsed),
|
||||||
|
probe: probe,
|
||||||
allowedPrefixes: prefixes,
|
allowedPrefixes: prefixes,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -146,7 +149,7 @@ func readProxyLines(path string) ([]string, error) {
|
|||||||
return lines, nil
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOneLine(ctx context.Context, line string, dcID int16) proxyEntry {
|
func checkOneLine(ctx context.Context, line string, dcID int16, probe checker.ProbeMode) proxyEntry {
|
||||||
now := time.Now().UTC().Format(time.RFC3339)
|
now := time.Now().UTC().Format(time.RFC3339)
|
||||||
ent := proxyEntry{RawLine: line, CheckedAt: now}
|
ent := proxyEntry{RawLine: line, CheckedAt: now}
|
||||||
t, err := parseurl.ParseTGProxy(line)
|
t, err := parseurl.ParseTGProxy(line)
|
||||||
@@ -164,7 +167,7 @@ func checkOneLine(ctx context.Context, line string, dcID int16) proxyEntry {
|
|||||||
ent.Error = ent.ParseError
|
ent.Error = ent.ParseError
|
||||||
return ent
|
return ent
|
||||||
}
|
}
|
||||||
err = checker.Check(ctx, t.Host, t.Port, parsed, dcID)
|
err = checker.Check(ctx, t.Host, t.Port, parsed, dcID, &checker.Options{Probe: probe})
|
||||||
code, msg := checkresult.Classify(err)
|
code, msg := checkresult.Classify(err)
|
||||||
ent.ExitCode = code
|
ent.ExitCode = code
|
||||||
ent.OK = err == nil
|
ent.OK = err == nil
|
||||||
@@ -191,7 +194,7 @@ func runCycle(cfg *config, st *store) {
|
|||||||
entries := make([]proxyEntry, 0, len(lines))
|
entries := make([]proxyEntry, 0, len(lines))
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.checkTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.checkTimeout)
|
||||||
ent := checkOneLine(ctx, line, cfg.dcID)
|
ent := checkOneLine(ctx, line, cfg.dcID, cfg.probe)
|
||||||
cancel()
|
cancel()
|
||||||
entries = append(entries, ent)
|
entries = append(entries, ent)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -49,7 +49,8 @@ docker run -d --name mtproxy-api --restart unless-stopped -p 8080:8080 `
|
|||||||
| `MTPROXY_LIST_FILE` | `/data/proxies.txt` | Путь к файлу: одна `tg://` ссылка на строку; пустые строки и строки с `#` в начале пропускаются |
|
| `MTPROXY_LIST_FILE` | `/data/proxies.txt` | Путь к файлу: одна `tg://` ссылка на строку; пустые строки и строки с `#` в начале пропускаются |
|
||||||
| `MTPROXY_CHECK_INTERVAL` | `5m` | Интервал между циклами (`time.ParseDuration`, например `5m`, `1h`) |
|
| `MTPROXY_CHECK_INTERVAL` | `5m` | Интервал между циклами (`time.ParseDuration`, например `5m`, `1h`) |
|
||||||
| `MTPROXY_HTTP_ADDR` | `:8080` | Адрес прослушивания HTTP |
|
| `MTPROXY_HTTP_ADDR` | `:8080` | Адрес прослушивания HTTP |
|
||||||
| `MTPROXY_CHECK_TIMEOUT` | `45s` (в демоне по умолчанию; CLI по-прежнему `15s` если не задано) | Таймаут **всей** одной проверки: TCP + Fake-TLS + MTProxy init + drain + `req_pq`/`resPQ` до DC; при медленной сети увеличьте |
|
| `MTPROXY_CHECK_TIMEOUT` | `45s` (в демоне по умолчанию; CLI по-прежнему `15s` если не задано) | Таймаут **всей** одной проверки; для `MTPROXY_PROBE=deep` нужен запас (TLS + drain + ответ DC) |
|
||||||
|
| `MTPROXY_PROBE` | *(пусто)* → **`fast`** | `fast` — как в ранних релизах: рукопожатие + init + любой входящий байт. `deep` — `req_pq`/`resPQ` через DC (строже, дольше) |
|
||||||
| `MTPROXY_DC_ID` | `2` | DC id (аналог `-dc-id` CLI) |
|
| `MTPROXY_DC_ID` | `2` | DC id (аналог `-dc-id` CLI) |
|
||||||
| `MTPROXY_ALLOWED_IPS` | *(не задана)* | Если задана непустая строка — доступ к **всем** маршрутам только с перечисленных IP/CIDR; остальные получают **403** и JSON `{"error":"forbidden"}`. Формат: через запятую, пробелы допускаются: `192.168.1.10`, `10.0.0.0/8`, IPv6 и CIDR вида `2001:db8::/32`. Учитывается только **`RemoteAddr`** TCP-соединения; заголовок `X-Forwarded-For` **не** используется |
|
| `MTPROXY_ALLOWED_IPS` | *(не задана)* | Если задана непустая строка — доступ к **всем** маршрутам только с перечисленных IP/CIDR; остальные получают **403** и JSON `{"error":"forbidden"}`. Формат: через запятую, пробелы допускаются: `192.168.1.10`, `10.0.0.0/8`, IPv6 и CIDR вида `2001:db8::/32`. Учитывается только **`RemoteAddr`** TCP-соединения; заголовок `X-Forwarded-For` **не** используется |
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,25 +18,29 @@ import (
|
|||||||
// ErrProxyClosed indicates the peer closed the TCP connection during the check (Telethon #1134 style).
|
// ErrProxyClosed indicates the peer closed the TCP connection during the check (Telethon #1134 style).
|
||||||
var ErrProxyClosed = errors.New("mtproxy closed connection after initial payload")
|
var ErrProxyClosed = errors.New("mtproxy closed connection after initial payload")
|
||||||
|
|
||||||
// Check runs Fake-TLS/dd handshake, MTProxy init, then a minimal MTProto req_pq and expects resPQ from Telegram DC (same idea as the mobile client path).
|
// ErrNoDataAfterHeader is returned in ProbeFast when the proxy sends nothing after the init payload within the wait window.
|
||||||
func Check(ctx context.Context, host string, port int, parsed *secret.Parsed, dcID int16) error {
|
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)
|
conn, err := dialTCP(ctx, host, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("tcp dial: %w", err)
|
return fmt.Errorf("tcp dial: %w", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
|
o := effectiveOpts(opts)
|
||||||
switch parsed.Kind {
|
switch parsed.Kind {
|
||||||
case secret.KindEE:
|
case secret.KindEE:
|
||||||
return checkEE(ctx, conn, parsed, dcID)
|
return checkEE(ctx, conn, parsed, dcID, o)
|
||||||
case secret.KindDD:
|
case secret.KindDD:
|
||||||
return checkDD(ctx, conn, parsed, dcID)
|
return checkDD(ctx, conn, parsed, dcID, o)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown secret kind")
|
return fmt.Errorf("unknown secret kind")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16) error {
|
func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o *Options) error {
|
||||||
// ee-секрет хранит домен как «сырой» хвост (часто 0xd0 + ASCII hostname). В TLS SNI нужен только hostname,
|
// ee-секрет хранит домен как «сырой» хвост (часто 0xd0 + ASCII hostname). В TLS SNI нужен только hostname,
|
||||||
// как в официальном клиенте Telegram — иначе прокси сбрасывает соединение до ServerHello.
|
// как в официальном клиенте Telegram — иначе прокси сбрасывает соединение до ServerHello.
|
||||||
sni := faketls.SNIDomain(p.Domain)
|
sni := faketls.SNIDomain(p.Domain)
|
||||||
@@ -64,6 +69,9 @@ func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16) e
|
|||||||
if err := faketls.WriteTLSApplicationData(conn, hdr); err != nil {
|
if err := faketls.WriteTLSApplicationData(conn, hdr); err != nil {
|
||||||
return fmt.Errorf("write mtproxy header: %w", err)
|
return fmt.Errorf("write mtproxy header: %w", err)
|
||||||
}
|
}
|
||||||
|
if o.Probe == ProbeFast {
|
||||||
|
return waitPostPayload(conn)
|
||||||
|
}
|
||||||
br := bufio.NewReader(conn)
|
br := bufio.NewReader(conn)
|
||||||
if err := tgquick.DrainPostInitEE(ctx, br, conn, dec, 2*time.Second); err != nil {
|
if err := tgquick.DrainPostInitEE(ctx, br, conn, dec, 2*time.Second); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -77,7 +85,7 @@ func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16) error {
|
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)
|
hdr, enc, dec, err := mtproxy.InitHeader(p.Key, dcID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("mtproxy header: %w", err)
|
return fmt.Errorf("mtproxy header: %w", err)
|
||||||
@@ -85,6 +93,9 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16) e
|
|||||||
if _, err := conn.Write(hdr); err != nil {
|
if _, err := conn.Write(hdr); err != nil {
|
||||||
return fmt.Errorf("write mtproxy header: %w", err)
|
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 err := tgquick.VerifyResPQ(ctx, conn, enc, dec, nil); err != nil {
|
||||||
if errors.Is(err, tgquick.ErrPeerClosed) {
|
if errors.Is(err, tgquick.ErrPeerClosed) {
|
||||||
return fmt.Errorf("%w", ErrProxyClosed)
|
return fmt.Errorf("%w", ErrProxyClosed)
|
||||||
@@ -93,3 +104,30 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16) e
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package checker
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ProbeMode selects how strictly we validate after MTProxy init.
|
||||||
|
type ProbeMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ProbeFast: handshake + init, then any inbound data within a short window (legacy behavior; fast).
|
||||||
|
ProbeFast ProbeMode = iota
|
||||||
|
// ProbeDeep: MTProto req_pq and expect resPQ from Telegram DC through the tunnel (stricter; slower).
|
||||||
|
ProbeDeep
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options configures Check. Nil or zero value uses ProbeFast.
|
||||||
|
type Options struct {
|
||||||
|
Probe ProbeMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseProbe maps env/flag strings: "", "fast" -> ProbeFast; "deep" -> ProbeDeep.
|
||||||
|
func ParseProbe(s string) ProbeMode {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||||
|
case "deep", "respq", "dc":
|
||||||
|
return ProbeDeep
|
||||||
|
default:
|
||||||
|
return ProbeFast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func effectiveOpts(opts *Options) *Options {
|
||||||
|
if opts == nil {
|
||||||
|
return &Options{Probe: ProbeFast}
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@ func Classify(err error) (exitCode int, message string) {
|
|||||||
if errors.Is(err, checker.ErrProxyClosed) {
|
if errors.Is(err, checker.ErrProxyClosed) {
|
||||||
return 3, err.Error()
|
return 3, err.Error()
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, checker.ErrNoDataAfterHeader) {
|
||||||
|
return 1, err.Error()
|
||||||
|
}
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
return 4, "timeout"
|
return 4, "timeout"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user