Update MTProxy checker documentation and logic to clarify probing modes and improve error handling. Modify README and Docker documentation to reflect changes in the -probe flag behavior, including detailed descriptions of exit codes and timeout handling. Refactor connection handling to enhance robustness and align with Telethon's approach.
This commit is contained in:
@@ -3,3 +3,4 @@
|
|||||||
*.exe
|
*.exe
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
telemt_ref/
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ 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), `-probe fast|deep` (по умолчанию **`fast`** — быстрая проверка; `deep` — MTProto до DC).
|
Флаги: `-timeout` (по умолчанию 15s), `-dc-id` (по умолчанию 2), `-probe fast|deep` (по умолчанию **`fast`** — как Telethon TcpMTProxy после init; **`deep`** — `req_pq`/`resPQ` до DC).
|
||||||
|
|
||||||
Код выхода: `0` — OK (через прокси получен ответ Telegram DC на MTProto `req_pq` — `resPQ`), `1` — ошибка (в т.ч. нет валидного `resPQ`), `2` — неверные аргументы, `3` — прокси закрыл соединение после проверки, `4` — таймаут.
|
Код выхода: `0` — OK (**`fast`**: рукопожатие + init, прокси не рвёт TCP сразу; **`deep`**: плюс `resPQ` от DC), `1` — ошибка (в **`deep`** в т.ч. нет валидного `resPQ`), `2` — неверные аргументы, `3` — прокси закрыл соединение после проверки, `4` — таймаут.
|
||||||
|
|
||||||
**Docker:** [docs/docker.ru.md](docs/docker.ru.md) — один образ: **с аргументами** после образа — CLI; **без аргументов** — HTTP API (файл со списком `tg://`, интервал, опциональный whitelist IP).
|
**Docker:** [docs/docker.ru.md](docs/docker.ru.md) — один образ: **с аргументами** после образа — CLI; **без аргументов** — HTTP API (файл со списком `tg://`, интервал, опциональный whitelist IP).
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +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)")
|
probe := flag.String("probe", "fast", "fast: handshake+init, Telethon-style post-init wait (no immediate close); deep: req_pq/resPQ via DC")
|
||||||
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)")
|
||||||
|
|||||||
+4
-4
@@ -50,7 +50,7 @@ docker run -d --name mtproxy-api --restart unless-stopped -p 8080:8080 `
|
|||||||
| `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` если не задано) | Таймаут **всей** одной проверки; для `MTPROXY_PROBE=deep` нужен запас (TLS + drain + ответ DC) |
|
| `MTPROXY_CHECK_TIMEOUT` | `45s` (в демоне по умолчанию; CLI по-прежнему `15s` если не задано) | Таймаут **всей** одной проверки; для `MTPROXY_PROBE=deep` нужен запас (TLS + drain + ответ DC) |
|
||||||
| `MTPROXY_PROBE` | *(пусто)* → **`fast`** | `fast` — как в ранних релизах: рукопожатие + init + любой входящий байт. `deep` — `req_pq`/`resPQ` через DC (строже, дольше) |
|
| `MTPROXY_PROBE` | *(пусто)* → **`fast`** | `fast` — рукопожатие + init + короткое ожидание как в Telethon TcpMTProxy (#1134): OK, если прокси **не** рвёт TCP сразу после 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` **не** используется |
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ docker run -d --name mtproxy-api --restart unless-stopped -p 8080:8080 `
|
|||||||
| GET | `/health` | `200`, `{"status":"ok"}` |
|
| GET | `/health` | `200`, `{"status":"ok"}` |
|
||||||
| GET | `/api/v1/proxies` | `200`, JSON с полями `cycle_finished_at`, `next_check_after`, массив `proxies` |
|
| GET | `/api/v1/proxies` | `200`, JSON с полями `cycle_finished_at`, `next_check_after`, массив `proxies` |
|
||||||
|
|
||||||
Элемент `proxies[]`: `raw_line`, при успешном разборе — `url`, `ok`, `exit_code` (`0` OK — через туннель прокси получен ответ DC на MTProto `req_pq` (`resPQ`), как при реальной проверке клиентом; `1` ошибка проверки, в т.ч. нет `resPQ` за время ожидания; `2` ошибка разбора URL/секрета; `3` прокси закрыл соединение; `4` таймаут всего запроса), `error`, при необходимости `parse_error`, `checked_at`.
|
Элемент `proxies[]`: `raw_line`, при успешном разборе — `url`, `ok`, `exit_code` (`0` OK — при `MTPROXY_PROBE=fast`: рукопожатие + init и прокси не закрыл TCP сразу (как Telethon #1134); при **`deep`**: дополнительно получен `resPQ` от DC через туннель); `1` ошибка проверки, в т.ч. нет `resPQ` за время ожидания в `deep`; `2` ошибка разбора URL/секрета; `3` прокси закрыл соединение; `4` таймаут всего запроса), `error`, при необходимости `parse_error`, `checked_at`.
|
||||||
|
|
||||||
### Whitelist IP и Docker
|
### Whitelist IP и Docker
|
||||||
|
|
||||||
@@ -281,8 +281,8 @@ docker run --rm registry.example.com/owner/mtproxy_checker:latest \
|
|||||||
|
|
||||||
| Код | Значение |
|
| Код | Значение |
|
||||||
|-----|----------|
|
|-----|----------|
|
||||||
| 0 | Проверка прошла: Fake-TLS/dd + MTProxy init, затем MTProto `req_pq` и ответ `resPQ` от Telegram DC через прокси |
|
| 0 | **`fast`**: Fake-TLS/dd + MTProxy init, прокси не закрыл соединение сразу (как Telethon). **`deep`**: плюс подтверждён `resPQ` от DC |
|
||||||
| 1 | Ошибка (сеть, протокол; в т.ч. **нет валидного `resPQ`** — туннель до DC не подтверждён) |
|
| 1 | Ошибка (сеть, протокол; в **`deep`** — в т.ч. нет валидного `resPQ`) |
|
||||||
| 2 | Неверные аргументы CLI |
|
| 2 | Неверные аргументы CLI |
|
||||||
| 3 | Прокси закрыл TCP сразу после начального payload |
|
| 3 | Прокси закрыл TCP сразу после начального payload |
|
||||||
| 4 | Общий таймаут (`-timeout`) |
|
| 4 | Общий таймаут (`-timeout`) |
|
||||||
|
|||||||
+18
-16
@@ -18,9 +18,6 @@ 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")
|
||||||
|
|
||||||
// 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.
|
// 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 {
|
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)
|
||||||
@@ -54,6 +51,7 @@ func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
|||||||
if _, err := conn.Write(ch.Record); err != nil {
|
if _, err := conn.Write(ch.Record); err != nil {
|
||||||
return fmt.Errorf("write client hello: %w", err)
|
return fmt.Errorf("write client hello: %w", err)
|
||||||
}
|
}
|
||||||
|
// ServerHello читаем с сырого conn (ровно длина записи), как в tdesktop Read — без readahead.
|
||||||
resp, err := faketls.ReadServerHello(conn)
|
resp, err := faketls.ReadServerHello(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read server hello: %w", err)
|
return fmt.Errorf("read server hello: %w", err)
|
||||||
@@ -69,11 +67,12 @@ func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
|||||||
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 {
|
// После init один bufio на чтение TLS 0x17 (Telethon/telemt: не смешивать conn.Read и буфер).
|
||||||
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 o.Probe == ProbeFast {
|
||||||
|
return waitPostPayload(br, conn)
|
||||||
|
}
|
||||||
|
if err := tgquick.DrainPostInitEE(ctx, br, conn, dec, 5*time.Second); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, br); err != nil {
|
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, br); err != nil {
|
||||||
@@ -94,7 +93,7 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
|||||||
return fmt.Errorf("write mtproxy header: %w", err)
|
return fmt.Errorf("write mtproxy header: %w", err)
|
||||||
}
|
}
|
||||||
if o.Probe == ProbeFast {
|
if o.Probe == ProbeFast {
|
||||||
return waitPostPayload(conn)
|
return waitPostPayload(conn, 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) {
|
||||||
@@ -105,29 +104,32 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitPostPayload(conn net.Conn) error {
|
// waitPostPayload после init повторяет логику Telethon TcpMTProxy._connect (#1134):
|
||||||
|
// ждём появления данных или таймаут, затем считаем успехом только если соединение не закрыто сразу после payload.
|
||||||
|
// Наличие входящих байтов не обязательно — часть прокси молчит до первого MTProto от клиента.
|
||||||
|
func waitPostPayload(r io.Reader, c net.Conn) error {
|
||||||
deadline := time.Now().Add(2 * time.Second)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
for time.Now().Before(deadline) {
|
for time.Now().Before(deadline) {
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
_ = c.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
||||||
n, err := conn.Read(buf)
|
n, err := r.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = c.SetReadDeadline(time.Time{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, io.EOF) {
|
if errors.Is(err, io.EOF) {
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = c.SetReadDeadline(time.Time{})
|
||||||
return ErrProxyClosed
|
return ErrProxyClosed
|
||||||
}
|
}
|
||||||
var ne net.Error
|
var ne net.Error
|
||||||
if errors.As(err, &ne) && ne.Timeout() {
|
if errors.As(err, &ne) && ne.Timeout() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = c.SetReadDeadline(time.Time{})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = c.SetReadDeadline(time.Time{})
|
||||||
return ErrNoDataAfterHeader
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import "strings"
|
|||||||
type ProbeMode int
|
type ProbeMode int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ProbeFast: handshake + init, then any inbound data within a short window (legacy behavior; fast).
|
// ProbeFast: handshake + init, then short wait like Telethon TcpMTProxy (#1134): OK if proxy does not close immediately.
|
||||||
ProbeFast ProbeMode = iota
|
ProbeFast ProbeMode = iota
|
||||||
// ProbeDeep: MTProto req_pq and expect resPQ from Telegram DC through the tunnel (stricter; slower).
|
// ProbeDeep: MTProto req_pq and expect resPQ from Telegram DC through the tunnel (stricter; slower).
|
||||||
ProbeDeep
|
ProbeDeep
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,10 +81,14 @@ func InitHeader(secret16 []byte, dcID int16) (header64 []byte, enc cipher.Stream
|
|||||||
binary.LittleEndian.PutUint16(dcBytes, uint16(dcID))
|
binary.LittleEndian.PutUint16(dcBytes, uint16(dcID))
|
||||||
copy(buf[60:62], dcBytes)
|
copy(buf[60:62], dcBytes)
|
||||||
|
|
||||||
encChunk := make([]byte, 8)
|
// Как в Telethon MTProxyIO.init_header: encrypt(bytes(random)) на всём 64-байтном буфере,
|
||||||
copy(encChunk, buf[56:64])
|
// на провод уходит только random[0:56] как есть и random[56:64] = out[56:64].
|
||||||
enc.XORKeyStream(encChunk, encChunk)
|
// CTR enc должен продвинуться на 64 байта — иначе req_pq и чтение DC расходятся с прокси.
|
||||||
copy(buf[56:64], encChunk)
|
work := make([]byte, 64)
|
||||||
|
copy(work, buf)
|
||||||
|
out := make([]byte, 64)
|
||||||
|
enc.XORKeyStream(out, work)
|
||||||
|
copy(buf[56:64], out[56:64])
|
||||||
|
|
||||||
return buf, enc, dec, nil
|
return buf, enc, dec, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ func isResPQ(mtInner []byte) bool {
|
|||||||
// DrainPostInitEE reads inbound fake-TLS records after MTProxy init and before req_pq (Telethon TcpMTProxy waits for data ~2s).
|
// DrainPostInitEE reads inbound fake-TLS records after MTProxy init and before req_pq (Telethon TcpMTProxy waits for data ~2s).
|
||||||
// Consumes 0x17 payloads with dec so the CTR stream stays aligned with the server; discards MTProto frames until idle.
|
// Consumes 0x17 payloads with dec so the CTR stream stays aligned with the server; discards MTProto frames until idle.
|
||||||
func DrainPostInitEE(ctx context.Context, br *bufio.Reader, conn net.Conn, dec cipher.Stream, maxWait time.Duration) error {
|
func DrainPostInitEE(ctx context.Context, br *bufio.Reader, conn net.Conn, dec cipher.Stream, maxWait time.Duration) error {
|
||||||
|
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
||||||
end := time.Now().Add(maxWait)
|
end := time.Now().Add(maxWait)
|
||||||
if d, ok := ctx.Deadline(); ok && d.Before(end) {
|
if d, ok := ctx.Deadline(); ok && d.Before(end) {
|
||||||
end = d
|
end = d
|
||||||
@@ -139,6 +140,9 @@ func DrainPostInitEE(ctx context.Context, br *bufio.Reader, conn net.Conn, dec c
|
|||||||
}
|
}
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(d))
|
_ = conn.SetReadDeadline(time.Now().Add(d))
|
||||||
if err := sniffCleartextHTTP(br); err != nil {
|
if err := sniffCleartextHTTP(br); err != nil {
|
||||||
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
chunk, err := readNextTLS17Payload(br)
|
chunk, err := readNextTLS17Payload(br)
|
||||||
@@ -219,6 +223,12 @@ func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, tls
|
|||||||
var rerr error
|
var rerr error
|
||||||
if tlsBR != nil {
|
if tlsBR != nil {
|
||||||
if err := sniffCleartextHTTP(tlsBR); err != nil {
|
if err := sniffCleartextHTTP(tlsBR); err != nil {
|
||||||
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||||
|
if time.Now().Before(deadline) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
chunk, rerr = readNextTLS17Payload(tlsBR)
|
chunk, rerr = readNextTLS17Payload(tlsBR)
|
||||||
|
|||||||
Reference in New Issue
Block a user