Enhance MTProxy checker by introducing a new -probe mode deep-strict for stricter response validation. Update README and Docker documentation to clarify probing modes, exit codes, and the new MTPROXY_TDLIB_HELPER and MTPROXY_TDLIB_TIMEOUT environment variables for external helper integration. Refactor connection handling and error reporting to improve robustness and clarity in response verification.
This commit is contained in:
@@ -20,12 +20,14 @@ 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), `-dc-ids` (например `1,2,3,4,5` — проверка **каждого** DC по очереди; `-timeout` на **один** DC; общий лимит времени умножается на число DC; код `0`, если **хотя бы один** DC прошёл; список удавшихся DC печатается в stderr), `-probe fast|deep` (по умолчанию **`fast`** — как Telethon TcpMTProxy после init; **`deep`** — `req_pq`/`resPQ` до DC).
|
Флаги: `-timeout` (по умолчанию 15s), `-dc-id` (по умолчанию 2), `-dc-ids` (например `1,2,3,4,5` — проверка **каждого** DC по очереди; `-timeout` на **один** DC; общий лимит времени умножается на число DC; код `0`, если **хотя бы один** DC прошёл; список удавшихся DC печатается в stderr), `-probe fast|deep|deep-strict` (по умолчанию **`fast`** — как Telethon TcpMTProxy после init; **`deep`** — после init отправляется `req_pq`, успех если от DC пришло **любое** валидное unencrypted MTProto-сообщение (часто `resPQ`, но не только); **`deep-strict`** — как раньше `deep`, ответ должен быть именно `resPQ`).
|
||||||
|
|
||||||
Код выхода: `0` — OK (**`fast`**: рукопожатие + init, прокси не рвёт TCP сразу; **`deep`**: плюс `resPQ` от DC); при **нескольких** DC — `0`, если любой из них OK. `1` — ошибка (в **`deep`** в т.ч. нет валидного `resPQ`), `2` — неверные аргументы, `3` — прокси закрыл соединение после проверки, `4` — таймаут.
|
Код выхода: `0` — OK (**`fast`**: рукопожатие + init, прокси не рвёт TCP сразу; **`deep`**: плюс ответ DC после `req_pq`); при **нескольких** DC — `0`, если любой из них OK. `1` — ошибка (в **`deep`** в т.ч. нет ответа DC в ожидаемом виде; в **`deep-strict`** — нет `resPQ`), `2` — неверные аргументы, `3` — прокси закрыл соединение после проверки (в **`fast`** в т.ч. отложенный FIN/RST после init — ловится дополнительным коротким чтением после окна Telethon), `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).
|
||||||
|
|
||||||
|
**HTTP API и TDLib:** в JSON каждой строки прокси поля **`standard`** (наш TCP/MTProxy чек) и **`tdlib`** (опционально: внешний helper TDLib `addProxy`+`pingProxy`). Рекомендуется нативный бинарник **`tdlib_ping`** (`cmd/tdlib_ping`, официальный JSON API TDLib через `libtdjson`). Альтернатива на Node — `contrib/tdlib-ping`. Задаётся `MTPROXY_TDLIB_HELPER` и `MTPROXY_TDLIB_TIMEOUT`; helper вызывается только в режиме **`fast`**. Базовый Docker-образ остаётся без TDLib/Node — helper подключают своим слоем образа или volume.
|
||||||
|
|
||||||
### Если в Telegram прокси «Available», а утилита падает на `read server hello`
|
### Если в Telegram прокси «Available», а утилита падает на `read server hello`
|
||||||
|
|
||||||
Для `ee` в TLS SNI должен идти **только ASCII hostname** из хвоста секрета; в коде это `SNIDomain`. Ответ сервера читается по схеме из Telegram Desktop (`mtproto_tls_socket`), а не по старому фиксированному формату telethon.
|
Для `ee` в TLS SNI должен идти **только ASCII hostname** из хвоста секрета; в коде это `SNIDomain`. Ответ сервера читается по схеме из Telegram Desktop (`mtproto_tls_socket`), а не по старому фиксированному формату telethon.
|
||||||
|
|||||||
@@ -22,7 +22,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, Telethon-style post-init wait (no immediate close); deep: req_pq/resPQ via DC")
|
probe := flag.String("probe", "fast", "fast: handshake+init, Telethon-style post-init wait (no immediate close); deep: req_pq + any unencrypted DC reply; deep-strict: must be resPQ")
|
||||||
dcID := flag.Int("dc-id", 2, "Telegram DC id (signed int16) embedded in MTProxy header (ignored if -dc-ids is set)")
|
dcID := flag.Int("dc-id", 2, "Telegram DC id (signed int16) embedded in MTProxy header (ignored if -dc-ids is set)")
|
||||||
dcIDsFlag := flag.String("dc-ids", "", "comma-separated DC ids to probe in order (e.g. 1,2,3,4,5); OK if any succeeds; timeout is per-DC")
|
dcIDsFlag := flag.String("dc-ids", "", "comma-separated DC ids to probe in order (e.g. 1,2,3,4,5); OK if any succeeds; timeout is per-DC")
|
||||||
server := flag.String("server", "", "proxy hostname (if not using tg:// positional)")
|
server := flag.String("server", "", "proxy hostname (if not using tg:// positional)")
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ type config struct {
|
|||||||
dcIDs []int16
|
dcIDs []int16
|
||||||
probe checker.ProbeMode
|
probe checker.ProbeMode
|
||||||
allowedPrefixes []netip.Prefix
|
allowedPrefixes []netip.Prefix
|
||||||
|
tdlibHelper string
|
||||||
|
tdlibTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() (*config, error) {
|
func loadConfig() (*config, error) {
|
||||||
@@ -90,6 +92,15 @@ func loadConfig() (*config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
probe := checker.ParseProbe(os.Getenv("MTPROXY_PROBE"))
|
probe := checker.ParseProbe(os.Getenv("MTPROXY_PROBE"))
|
||||||
|
tdlibHelper := strings.TrimSpace(os.Getenv("MTPROXY_TDLIB_HELPER"))
|
||||||
|
tdlibTimeoutStr := strings.TrimSpace(os.Getenv("MTPROXY_TDLIB_TIMEOUT"))
|
||||||
|
if tdlibTimeoutStr == "" {
|
||||||
|
tdlibTimeoutStr = "45s"
|
||||||
|
}
|
||||||
|
tdlibTimeout, err := time.ParseDuration(tdlibTimeoutStr)
|
||||||
|
if err != nil || tdlibTimeout <= 0 {
|
||||||
|
return nil, fmt.Errorf("MTPROXY_TDLIB_TIMEOUT: invalid duration %q", tdlibTimeoutStr)
|
||||||
|
}
|
||||||
return &config{
|
return &config{
|
||||||
listFile: listFile,
|
listFile: listFile,
|
||||||
checkInterval: interval,
|
checkInterval: interval,
|
||||||
@@ -98,6 +109,8 @@ func loadConfig() (*config, error) {
|
|||||||
dcIDs: dcIDs,
|
dcIDs: dcIDs,
|
||||||
probe: probe,
|
probe: probe,
|
||||||
allowedPrefixes: prefixes,
|
allowedPrefixes: prefixes,
|
||||||
|
tdlibHelper: tdlibHelper,
|
||||||
|
tdlibTimeout: tdlibTimeout,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,14 +121,32 @@ type dcProbeResult struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type standardProbe struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
ExitCode int `json:"exit_code"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
ParseError string `json:"parse_error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tdlibProbe struct {
|
||||||
|
SkippedReason string `json:"skipped_reason,omitempty"`
|
||||||
|
Ran bool `json:"ran"`
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
ExitCode int `json:"exit_code,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
DurationMs int64 `json:"duration_ms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type proxyEntry struct {
|
type proxyEntry struct {
|
||||||
RawLine string `json:"raw_line"`
|
RawLine string `json:"raw_line"`
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
|
CheckedAt string `json:"checked_at,omitempty"`
|
||||||
|
Standard standardProbe `json:"standard"`
|
||||||
|
TDLib tdlibProbe `json:"tdlib"`
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
ExitCode int `json:"exit_code"`
|
ExitCode int `json:"exit_code"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
ParseError string `json:"parse_error,omitempty"`
|
ParseError string `json:"parse_error,omitempty"`
|
||||||
CheckedAt string `json:"checked_at,omitempty"`
|
|
||||||
DCs []dcProbeResult `json:"dcs,omitempty"`
|
DCs []dcProbeResult `json:"dcs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +216,13 @@ func aggregateExitFromDCs(dcs []dcProbeResult) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOneLine(ctx context.Context, line string, dcIDs []int16, perDCTimeout time.Duration, probe checker.ProbeMode) proxyEntry {
|
func checkOneLine(ctx context.Context, line string, dcIDs []int16, perDCTimeout time.Duration, probe checker.ProbeMode, tdlibHelper string, tdlibTimeout time.Duration) (ent 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}
|
||||||
|
defer func() {
|
||||||
|
ent.Standard = standardProbe{OK: ent.OK, ExitCode: ent.ExitCode, Error: ent.Error, ParseError: ent.ParseError}
|
||||||
|
ent.TDLib = runTDLibBlock(line, tdlibHelper, tdlibTimeout, probe, ent)
|
||||||
|
}()
|
||||||
if len(dcIDs) == 0 {
|
if len(dcIDs) == 0 {
|
||||||
ent.ExitCode = 2
|
ent.ExitCode = 2
|
||||||
ent.Error = "no DC ids configured"
|
ent.Error = "no DC ids configured"
|
||||||
@@ -210,8 +245,8 @@ func checkOneLine(ctx context.Context, line string, dcIDs []int16, perDCTimeout
|
|||||||
}
|
}
|
||||||
if len(dcIDs) == 1 {
|
if len(dcIDs) == 1 {
|
||||||
dcCtx, cancel := context.WithTimeout(ctx, perDCTimeout)
|
dcCtx, cancel := context.WithTimeout(ctx, perDCTimeout)
|
||||||
defer cancel()
|
|
||||||
err = checker.Check(dcCtx, t.Host, t.Port, parsed, dcIDs[0], &checker.Options{Probe: probe})
|
err = checker.Check(dcCtx, t.Host, t.Port, parsed, dcIDs[0], &checker.Options{Probe: probe})
|
||||||
|
cancel()
|
||||||
code, msg := checkresult.Classify(err)
|
code, msg := checkresult.Classify(err)
|
||||||
ent.ExitCode = code
|
ent.ExitCode = code
|
||||||
ent.OK = err == nil
|
ent.OK = err == nil
|
||||||
@@ -262,6 +297,8 @@ func runCycle(cfg *config, st *store) {
|
|||||||
ParseError: err.Error(),
|
ParseError: err.Error(),
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
CheckedAt: finished.Format(time.RFC3339),
|
CheckedAt: finished.Format(time.RFC3339),
|
||||||
|
Standard: standardProbe{OK: false, ExitCode: 2, Error: err.Error(), ParseError: err.Error()},
|
||||||
|
TDLib: tdlibProbe{SkippedReason: "cycle aborted (list file error)", Ran: false},
|
||||||
}}, finished, time.Now().Add(cfg.checkInterval))
|
}}, finished, time.Now().Add(cfg.checkInterval))
|
||||||
log.Printf("read list file: %v", err)
|
log.Printf("read list file: %v", err)
|
||||||
return
|
return
|
||||||
@@ -272,8 +309,11 @@ func runCycle(cfg *config, st *store) {
|
|||||||
if len(cfg.dcIDs) > 1 {
|
if len(cfg.dcIDs) > 1 {
|
||||||
total = cfg.checkTimeout * time.Duration(len(cfg.dcIDs))
|
total = cfg.checkTimeout * time.Duration(len(cfg.dcIDs))
|
||||||
}
|
}
|
||||||
|
if cfg.probe == checker.ProbeFast && strings.TrimSpace(cfg.tdlibHelper) != "" {
|
||||||
|
total += cfg.tdlibTimeout
|
||||||
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), total)
|
ctx, cancel := context.WithTimeout(context.Background(), total)
|
||||||
ent := checkOneLine(ctx, line, cfg.dcIDs, cfg.checkTimeout, cfg.probe)
|
ent := checkOneLine(ctx, line, cfg.dcIDs, cfg.checkTimeout, cfg.probe, cfg.tdlibHelper, cfg.tdlibTimeout)
|
||||||
cancel()
|
cancel()
|
||||||
entries = append(entries, ent)
|
entries = append(entries, ent)
|
||||||
}
|
}
|
||||||
@@ -353,7 +393,11 @@ func run() error {
|
|||||||
|
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("listening on %s, list=%s interval=%s dcs=%v", cfg.httpAddr, cfg.listFile, cfg.checkInterval, cfg.dcIDs)
|
if cfg.tdlibHelper != "" {
|
||||||
|
log.Printf("listening on %s, list=%s interval=%s dcs=%v tdlib_helper=%s tdlib_timeout=%s", cfg.httpAddr, cfg.listFile, cfg.checkInterval, cfg.dcIDs, cfg.tdlibHelper, cfg.tdlibTimeout)
|
||||||
|
} else {
|
||||||
|
log.Printf("listening on %s, list=%s interval=%s dcs=%v", cfg.httpAddr, cfg.listFile, cfg.checkInterval, cfg.dcIDs)
|
||||||
|
}
|
||||||
errCh <- srv.ListenAndServe()
|
errCh <- srv.ListenAndServe()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"mtproxy_checker/internal/checker"
|
||||||
|
"mtproxy_checker/internal/tdlibexec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runTDLibBlock(proxyLine, helper string, timeout time.Duration, probe checker.ProbeMode, ent proxyEntry) tdlibProbe {
|
||||||
|
if probe != checker.ProbeFast {
|
||||||
|
return tdlibProbe{SkippedReason: "tdlib runs only with probe fast (or empty MTPROXY_PROBE)", Ran: false}
|
||||||
|
}
|
||||||
|
if ent.ParseError != "" {
|
||||||
|
return tdlibProbe{SkippedReason: "skipped: standard parse error", Ran: false}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(helper) == "" {
|
||||||
|
return tdlibProbe{SkippedReason: "MTPROXY_TDLIB_HELPER not set", Ran: false}
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
res, err := tdlibexec.Run(ctx, helper, proxyLine)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||||
|
return tdlibProbe{Ran: true, OK: false, ExitCode: 4, Error: err.Error(), DurationMs: res.DurationMs}
|
||||||
|
}
|
||||||
|
return tdlibProbe{Ran: true, OK: false, ExitCode: 1, Error: err.Error(), DurationMs: res.DurationMs}
|
||||||
|
}
|
||||||
|
return tdlibProbe{Ran: true, OK: res.OK, ExitCode: res.ExitCode, Error: res.Error, DurationMs: res.DurationMs}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# tdlib_ping — официальный JSON-интерфейс TDLib
|
||||||
|
|
||||||
|
Бинарник вызывает **`td_create_client_id` / `td_send` / `td_receive` / `td_execute`** из [`td/telegram/td_json_client.h`](https://github.com/tdlib/td/blob/master/td/telegram/td_json_client.h) (рекомендуемый multi-client API), без Node.js.
|
||||||
|
|
||||||
|
Контракт stdout — одна строка JSON для `mtproxy_checkerd`:
|
||||||
|
|
||||||
|
`{"ok":true,"error":"","exit_code":0}`
|
||||||
|
|
||||||
|
## Сборка
|
||||||
|
|
||||||
|
Linux (Debian/Ubuntu), установите заголовки и библиотеку, например:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install -y libtdjson-dev # имя пакета может отличаться по дистрибутиву
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
go build -tags=tdlib -o tdlib_ping ./cmd/tdlib_ping
|
||||||
|
```
|
||||||
|
|
||||||
|
macOS: `brew install tdlib`, затем при необходимости `export PKG_CONFIG_PATH=...` если `pkg-config --libs tdlib` находит `-ltdjson`.
|
||||||
|
|
||||||
|
Без `libtdjson` линковка завершится ошибкой — это ожидаемо.
|
||||||
|
|
||||||
|
Сборка **без** тега (как часть `go build ./...`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build ./cmd/tdlib_ping
|
||||||
|
```
|
||||||
|
|
||||||
|
получится заглушка, которая печатает JSON с `exit_code: 2` и пояснением — чтобы репозиторий собирался без TDLib.
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Назначение |
|
||||||
|
|------------|--------------|------------|
|
||||||
|
| `MTPROXY_TD_API_ID` | `12345` | Замените на значение с [my.telegram.org](https://my.telegram.org) |
|
||||||
|
| `MTPROXY_TD_API_HASH` | демо-строка из примера TDLib | Замените на свой `api_hash` |
|
||||||
|
| `MTPROXY_TDLIB_PING_MS` | `45000` | Общий дедлайн цикла (мс) |
|
||||||
|
| `MTPROXY_TDLIB_DATABASE_DIR` | пусто | Если задан — постоянный каталог для `database_directory` / `files_directory` (подкаталоги `db/` и `files/` создаются автоматически). Иначе — временный каталог на каждый запуск |
|
||||||
|
| `MTPROXY_TD_USE_TEST_DC` | пусто | Если `1` — `use_test_dc: true` в `setTdlibParameters` (иногда помогает обойти ожидание телефона на «чистой» БД) |
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tdlib_ping 'tg://proxy?server=HOST&port=PORT&secret=HEX'
|
||||||
|
```
|
||||||
|
|
||||||
|
Если **`MTPROXY_TDLIB_DATABASE_DIR`** не задан, каталоги БД TDLib создаются под уникальным префиксом во временном каталоге на каждый запуск (и удаляются после выхода).
|
||||||
|
|
||||||
|
## Ограничение
|
||||||
|
|
||||||
|
При первом запуске с «чистой» БД TDLib может перейти в **`authorizationStateWaitPhoneNumber`**. Тогда helper завершится с ошибкой в JSON — нужен уже проинициализированный `database_directory` или рабочий сценарий входа. Для типичного мониторинга MTProxy на выделенной машине обычно достаточно повторных запусков с фиксированным каталогом (`MTPROXY_TDLIB_DATABASE_DIR`, см. таблицу выше).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build !tdlib || !cgo
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
_ = enc.Encode(map[string]interface{}{
|
||||||
|
"ok": false,
|
||||||
|
"error": "tdlib_ping: build with CGO and -tags=tdlib; requires libtdjson (see cmd/tdlib_ping/README.md)",
|
||||||
|
"exit_code": 2,
|
||||||
|
})
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
//go:build tdlib && cgo
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -ltdjson
|
||||||
|
#cgo CFLAGS: -I/usr/include -I/usr/local/include
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <td/telegram/td_json_client.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"mtproxy_checker/internal/parseurl"
|
||||||
|
"mtproxy_checker/internal/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
outJSON(false, "usage: tdlib_ping <tg://proxy?...>", 2)
|
||||||
|
}
|
||||||
|
run(os.Args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func outJSON(ok bool, msg string, code int) {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
_ = enc.Encode(map[string]interface{}{
|
||||||
|
"ok": ok, "error": msg, "exit_code": code,
|
||||||
|
})
|
||||||
|
if ok {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
switch code {
|
||||||
|
case 1:
|
||||||
|
os.Exit(1)
|
||||||
|
case 4:
|
||||||
|
os.Exit(4)
|
||||||
|
default:
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(proxyURL string) {
|
||||||
|
deadlineMs, _ := strconv.Atoi(os.Getenv("MTPROXY_TDLIB_PING_MS"))
|
||||||
|
if deadlineMs <= 0 {
|
||||||
|
deadlineMs = 45000
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(time.Duration(deadlineMs) * time.Millisecond)
|
||||||
|
|
||||||
|
t, err := parseurl.ParseTGProxy(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
outJSON(false, err.Error(), 1)
|
||||||
|
}
|
||||||
|
parsed, err := secret.Parse(t.Secret)
|
||||||
|
if err != nil {
|
||||||
|
outJSON(false, err.Error(), 1)
|
||||||
|
}
|
||||||
|
secHex := mtprotoSecretHex(parsed)
|
||||||
|
|
||||||
|
apiID := int64(12345)
|
||||||
|
if v := strings.TrimSpace(os.Getenv("MTPROXY_TD_API_ID")); v != "" {
|
||||||
|
if n, e := strconv.ParseInt(v, 10, 32); e == nil {
|
||||||
|
apiID = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiHash := strings.TrimSpace(os.Getenv("MTPROXY_TD_API_HASH"))
|
||||||
|
if apiHash == "" {
|
||||||
|
apiHash = "0123456789abcdef0123456789abcdef"
|
||||||
|
}
|
||||||
|
useTest := strings.TrimSpace(os.Getenv("MTPROXY_TD_USE_TEST_DC")) == "1"
|
||||||
|
|
||||||
|
dbBase := strings.TrimSpace(os.Getenv("MTPROXY_TDLIB_DATABASE_DIR"))
|
||||||
|
var cleanupTemp string
|
||||||
|
if dbBase == "" {
|
||||||
|
cleanupTemp, err = os.MkdirTemp("", "mtproxy_tdlib_")
|
||||||
|
if err != nil {
|
||||||
|
outJSON(false, err.Error(), 2)
|
||||||
|
}
|
||||||
|
dbBase = cleanupTemp
|
||||||
|
defer func() { _ = os.RemoveAll(cleanupTemp) }()
|
||||||
|
}
|
||||||
|
|
||||||
|
verb := `{"@type":"setLogVerbosityLevel","new_verbosity_level":1}`
|
||||||
|
if cs := C.CString(verb); cs != nil {
|
||||||
|
_ = C.td_execute(cs)
|
||||||
|
C.free(unsafe.Pointer(cs))
|
||||||
|
}
|
||||||
|
|
||||||
|
clientID := int(C.td_create_client_id())
|
||||||
|
defer closeClient(clientID)
|
||||||
|
|
||||||
|
if !waitAuthState(clientID, "authorizationStateWaitTdlibParameters", deadline) {
|
||||||
|
outJSON(false, "timeout waiting for authorizationStateWaitTdlibParameters", 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"@type": "setTdlibParameters",
|
||||||
|
"use_test_dc": useTest,
|
||||||
|
"database_directory": dbBase + "/db",
|
||||||
|
"files_directory": dbBase + "/files",
|
||||||
|
"use_file_database": true,
|
||||||
|
"use_chat_info_database": true,
|
||||||
|
"use_message_database": true,
|
||||||
|
"use_secret_chats": true,
|
||||||
|
"api_id": apiID,
|
||||||
|
"api_hash": apiHash,
|
||||||
|
"system_language_code": "en",
|
||||||
|
"device_model": "mtproxy-tdlib-ping",
|
||||||
|
"application_version": "1.0",
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dbBase+"/db", 0o700); err != nil {
|
||||||
|
outJSON(false, err.Error(), 2)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dbBase+"/files", 0o700); err != nil {
|
||||||
|
outJSON(false, err.Error(), 2)
|
||||||
|
}
|
||||||
|
tdSend(clientID, params)
|
||||||
|
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
ev := tdReceive(1.0)
|
||||||
|
if ev == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if typ, _ := ev["@type"].(string); typ == "error" {
|
||||||
|
outJSON(false, fmt.Sprintf("tdlib: %v", ev["message"]), 2)
|
||||||
|
}
|
||||||
|
switch authStateType(ev) {
|
||||||
|
case "authorizationStateWaitEncryptionKey":
|
||||||
|
tdSend(clientID, map[string]interface{}{
|
||||||
|
"@type": "checkDatabaseEncryptionKey", "encryption_key": "",
|
||||||
|
})
|
||||||
|
case "authorizationStateReady":
|
||||||
|
goto doProxy
|
||||||
|
case "authorizationStateWaitPhoneNumber":
|
||||||
|
outJSON(false, "TDLib authorizationStateWaitPhoneNumber: use a persistent MTPROXY_TDLIB_DATABASE_DIR after TDLib login, or set MTPROXY_TD_USE_TEST_DC=1", 2)
|
||||||
|
case "authorizationStateClosed":
|
||||||
|
outJSON(false, "unexpected authorizationStateClosed before ping", 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outJSON(false, "timeout in TDLib authorization", 4)
|
||||||
|
|
||||||
|
doProxy:
|
||||||
|
addBody := map[string]interface{}{
|
||||||
|
"@type": "addProxy",
|
||||||
|
"server": t.Host, "port": t.Port, "enable": true,
|
||||||
|
"type": map[string]interface{}{
|
||||||
|
"@type": "proxyTypeMtproto", "secret": secHex,
|
||||||
|
},
|
||||||
|
"@extra": "addproxy",
|
||||||
|
}
|
||||||
|
tdSend(clientID, addBody)
|
||||||
|
|
||||||
|
var proxyID int64
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
ev := tdReceive(1.0)
|
||||||
|
if ev == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchExtra(ev, "addproxy") {
|
||||||
|
if typ, _ := ev["@type"].(string); typ == "error" {
|
||||||
|
outJSON(false, fmt.Sprintf("addProxy: %v", ev["message"]), 2)
|
||||||
|
}
|
||||||
|
if typ, _ := ev["@type"].(string); typ == "proxy" {
|
||||||
|
if idf, ok := ev["id"].(float64); ok {
|
||||||
|
proxyID = int64(idf)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outJSON(false, fmt.Sprintf("addProxy unexpected: %#v", ev), 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if proxyID == 0 {
|
||||||
|
outJSON(false, "timeout waiting for addProxy response", 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
tdSend(clientID, map[string]interface{}{
|
||||||
|
"@type": "pingProxy", "proxy_id": proxyID, "@extra": "pingpx",
|
||||||
|
})
|
||||||
|
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
ev := tdReceive(1.0)
|
||||||
|
if ev == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchExtra(ev, "pingpx") {
|
||||||
|
if typ, _ := ev["@type"].(string); typ == "ok" {
|
||||||
|
outJSON(true, "", 0)
|
||||||
|
}
|
||||||
|
if typ, _ := ev["@type"].(string); typ == "error" {
|
||||||
|
outJSON(false, fmt.Sprintf("pingProxy: %v", ev["message"]), 2)
|
||||||
|
}
|
||||||
|
outJSON(false, fmt.Sprintf("pingProxy unexpected: %#v", ev), 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outJSON(false, "timeout waiting for pingProxy", 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mtprotoSecretHex(p *secret.Parsed) string {
|
||||||
|
var full []byte
|
||||||
|
switch p.Kind {
|
||||||
|
case secret.KindDD:
|
||||||
|
full = append([]byte{0xdd}, p.Key...)
|
||||||
|
case secret.KindEE:
|
||||||
|
full = append(append([]byte{0xee}, p.Key...), p.Domain...)
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(full)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authStateType(m map[string]interface{}) string {
|
||||||
|
if m == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if m["@type"] != "updateAuthorizationState" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
v, _ := m["authorization_state"].(map[string]interface{})
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
t, _ := v["@type"].(string)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchExtra(m map[string]interface{}, want string) bool {
|
||||||
|
if m == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ext, ok := m["@extra"].(string)
|
||||||
|
return ok && ext == want
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitAuthState(clientID int, want string, deadline time.Time) bool {
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
ev := tdReceive(1.0)
|
||||||
|
if ev == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if authStateType(ev) == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func tdSend(clientID int, v interface{}) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
outJSON(false, err.Error(), 2)
|
||||||
|
}
|
||||||
|
cs := C.CString(string(b))
|
||||||
|
defer C.free(unsafe.Pointer(cs))
|
||||||
|
C.td_send(C.int(clientID), cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tdReceive(maxSec float64) map[string]interface{} {
|
||||||
|
cs := C.td_receive(C.double(maxSec))
|
||||||
|
if cs == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s := C.GoString(cs)
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(s), &m); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeClient(clientID int) {
|
||||||
|
tdSend(clientID, map[string]interface{}{"@type": "close"})
|
||||||
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
ev := tdReceive(0.5)
|
||||||
|
if ev == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if authStateType(ev) == "authorizationStateClosed" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# TDLib helper для `mtproxy_checkerd` (Node.js)
|
||||||
|
|
||||||
|
> Предпочтительный вариант — нативный **`tdlib_ping`** в корне репозитория (`cmd/tdlib_ping`, `libtdjson`, официальный JSON API TDLib). Этот каталог остаётся как **legacy** на Node + `prebuilt-tdlib`, если так удобнее в образе.
|
||||||
|
|
||||||
|
Внешний процесс: **TDLib** (`addProxy` + `pingProxy`), как в [telegram-mtproto-proxy-checker](https://github.com/AmirTahaMim/telegram-mtproto-proxy-checker). В stdout печатается **одна строка JSON** — её парсит `mtproxy_checkerd`.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd contrib/tdlib-ping
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск вручную
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ping.js 'tg://proxy?server=HOST&port=PORT&secret=HEX'
|
||||||
|
```
|
||||||
|
|
||||||
|
Успех: строка `{"ok":true,"error":"","exit_code":0}` и код выхода `0`.
|
||||||
|
Ошибка секрета: `exit_code` `1`.
|
||||||
|
Прочие ошибки: `exit_code` `2` (или `4` при таймауте внутри скрипта).
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
Базовый образ `mtproxy_checker` остаётся без Node/TDLib. Соберите свой слой: установите Node 18+, скопируйте `contrib/tdlib-ping`, выполните `npm install`, задайте:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MTPROXY_TDLIB_HELPER=/usr/local/bin/mtproxy-tdlib-ping
|
||||||
|
MTPROXY_TDLIB_TIMEOUT=45s
|
||||||
|
```
|
||||||
|
|
||||||
|
Обёртка-скрипт `mtproxy-tdlib-ping`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/bin/sh
|
||||||
|
exec node /opt/tdlib-ping/ping.js "$1"
|
||||||
|
```
|
||||||
|
|
||||||
|
TDLib проверка выполняется только при **`MTPROXY_PROBE=fast`** (или пусто).
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "mtproxy-checker-tdlib-ping",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "TDLib addProxy+pingProxy helper for mtproxy_checkerd (stdout: one JSON line)",
|
||||||
|
"main": "ping.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"prebuilt-tdlib": "^0.1008059.0",
|
||||||
|
"tdl": "^7.4.1",
|
||||||
|
"tdl-tdlib-addon": "^1.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* TDLib addProxy + pingProxy; prints one JSON line to stdout for mtproxy_checkerd.
|
||||||
|
* Based on flow from https://github.com/AmirTahaMim/telegram-mtproto-proxy-checker (MIT).
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
const { Client } = require('tdl');
|
||||||
|
const { TDLib } = require('tdl-tdlib-addon');
|
||||||
|
const tdl = require('tdl');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { getTdjson } = require('prebuilt-tdlib');
|
||||||
|
tdl.configure({ tdjson: getTdjson() });
|
||||||
|
} catch (_) {
|
||||||
|
/* system tdjson */
|
||||||
|
}
|
||||||
|
|
||||||
|
function outJson(ok, error, exitCode) {
|
||||||
|
console.log(JSON.stringify({ ok, error: error || '', exit_code: exitCode }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProxyUrl(url) {
|
||||||
|
const tgPattern = /^tg:\/\/proxy\?/;
|
||||||
|
const httpsPattern = /^https?:\/\/(www\.)?t\.me\/proxy\?/;
|
||||||
|
if (!tgPattern.test(url) && !httpsPattern.test(url)) {
|
||||||
|
throw new Error('Invalid proxy URL format');
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams(url.split('?')[1]);
|
||||||
|
const server = params.get('server');
|
||||||
|
const port = parseInt(params.get('port'), 10);
|
||||||
|
const secret = params.get('secret');
|
||||||
|
if (!server || !port || !secret) {
|
||||||
|
throw new Error('Missing server, port, or secret');
|
||||||
|
}
|
||||||
|
if (isNaN(port) || port < 1 || port > 65535) {
|
||||||
|
throw new Error('Invalid port');
|
||||||
|
}
|
||||||
|
return { server, port, secret };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSecret(secret) {
|
||||||
|
const hexPattern = /^[0-9a-fA-F]+$/;
|
||||||
|
if (hexPattern.test(secret)) {
|
||||||
|
if (secret.length % 2 !== 0) throw new Error('INVALID_SECRET');
|
||||||
|
return Buffer.from(secret, 'hex').toString('hex').toLowerCase();
|
||||||
|
}
|
||||||
|
let normalized = secret.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const padding = normalized.length % 4;
|
||||||
|
if (padding !== 0) normalized += '='.repeat(4 - padding);
|
||||||
|
return Buffer.from(normalized, 'base64').toString('hex').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractErrorMessage(error) {
|
||||||
|
if (error.response && error.response._ === 'error') {
|
||||||
|
return `Error ${error.response.code}: ${error.response.message || ''}`;
|
||||||
|
}
|
||||||
|
if (error.message) return error.message;
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyProxy(server, port, hexSecret, timeoutMs) {
|
||||||
|
const base = path.join(os.tmpdir(), `mtproxy_tdlib_${process.pid}_${Date.now()}`);
|
||||||
|
const tdlib = new TDLib();
|
||||||
|
const apiId = parseInt(process.env.MTPROXY_TD_API_ID || '12345', 10);
|
||||||
|
const apiHash = process.env.MTPROXY_TD_API_HASH || '0123456789abcdef0123456789abcdef';
|
||||||
|
const client = new Client(tdlib, {
|
||||||
|
apiId,
|
||||||
|
apiHash,
|
||||||
|
useTestDc: false,
|
||||||
|
databaseDirectory: path.join(base, 'db'),
|
||||||
|
filesDirectory: path.join(base, 'files'),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: extractErrorMessage(e), code: 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let addProxyResult;
|
||||||
|
try {
|
||||||
|
addProxyResult = await client.invoke({
|
||||||
|
_: 'addProxy',
|
||||||
|
server,
|
||||||
|
port,
|
||||||
|
enable: true,
|
||||||
|
type: { _: 'proxyTypeMtproto', secret: hexSecret },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const msg = extractErrorMessage(error);
|
||||||
|
if (msg.includes('INVALID_SECRET') || /secret/i.test(msg)) {
|
||||||
|
return { ok: false, error: 'INVALID_SECRET', code: 1 };
|
||||||
|
}
|
||||||
|
return { ok: false, error: msg, code: 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addProxyResult._ !== 'proxy') {
|
||||||
|
return { ok: false, error: 'addProxy did not return proxy', code: 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyId = addProxyResult.id;
|
||||||
|
const pingPromise = client.invoke({ _: 'pingProxy', proxy_id: proxyId });
|
||||||
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('TIMEOUT')), timeoutMs)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([pingPromise, timeoutPromise]);
|
||||||
|
return { ok: true, error: '', code: 0 };
|
||||||
|
} catch (error) {
|
||||||
|
const msg = extractErrorMessage(error);
|
||||||
|
const code = msg.includes('TIMEOUT') || msg.includes('timeout') ? 4 : 2;
|
||||||
|
return { ok: false, error: msg, code: code > 4 ? 2 : code };
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await client.close();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const url = process.argv[2];
|
||||||
|
if (!url) {
|
||||||
|
outJson(false, 'usage: node ping.js <tg:// or https://t.me/proxy?...>', 2);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
const timeoutMs = parseInt(process.env.MTPROXY_TDLIB_PING_MS || '45000', 10) || 45000;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { server, port, secret } = parseProxyUrl(url);
|
||||||
|
const hexSecret = normalizeSecret(secret);
|
||||||
|
const r = await verifyProxy(server, port, hexSecret, timeoutMs);
|
||||||
|
outJson(r.ok, r.error, r.code);
|
||||||
|
process.exit(r.code);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === 'INVALID_SECRET' || e.message.includes('INVALID_SECRET')) {
|
||||||
|
outJson(false, 'INVALID_SECRET', 1);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
outJson(false, e.message || String(e), 2);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
outJson(false, e.message || String(e), 2);
|
||||||
|
process.exit(2);
|
||||||
|
});
|
||||||
+8
-5
@@ -50,10 +50,12 @@ 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` если не задано) | Таймаут **одной** попытки к выбранному DC; для `MTPROXY_PROBE=deep` нужен запас (TLS + drain + ответ DC). Если задан `MTPROXY_DC_IDS` с несколькими DC, общий бюджет цикла на строку ≈ `таймаут × число_DC` |
|
| `MTPROXY_CHECK_TIMEOUT` | `45s` (в демоне по умолчанию; CLI по-прежнему `15s` если не задано) | Таймаут **одной** попытки к выбранному DC; для `MTPROXY_PROBE=deep` нужен запас (TLS + drain + ответ DC). Если задан `MTPROXY_DC_IDS` с несколькими DC, общий бюджет цикла на строку ≈ `таймаут × число_DC` |
|
||||||
| `MTPROXY_PROBE` | *(пусто)* → **`fast`** | `fast` — рукопожатие + init + короткое ожидание как в Telethon TcpMTProxy (#1134): OK, если прокси **не** рвёт TCP сразу после init (входящие байты не обязательны). `deep` — `req_pq`/`resPQ` через DC (строже, дольше) |
|
| `MTPROXY_PROBE` | *(пусто)* → **`fast`** | `fast` — рукопожатие + init + короткое ожидание как в Telethon TcpMTProxy (#1134): OK, если прокси **не** рвёт TCP сразу после init (входящие байты не обязательны); после основного 2s-окна — ещё несколько коротких `Read`, чтобы поймать **отложенный** FIN/RST (код выхода `3`, как в `deep`). `deep` — после init отправляется `req_pq`, OK если от DC пришло **любое** валидное unencrypted MTProto-сообщение (туннель реально несёт ответ DC; ICMP «ping» до DC через MTProxy невозможен). `deep-strict` — как раньше: ответ должен быть именно `resPQ` |
|
||||||
| `MTPROXY_DC_ID` | `2` | Один DC id (аналог `-dc-id` CLI), **игнорируется**, если задан непустой `MTPROXY_DC_IDS` |
|
| `MTPROXY_DC_ID` | `2` | Один DC id (аналог `-dc-id` CLI), **игнорируется**, если задан непустой `MTPROXY_DC_IDS` |
|
||||||
| `MTPROXY_DC_IDS` | *(пусто)* | Список DC через запятую, например `1,2,3,4,5`: для каждой строки прокси выполняется отдельная проверка на каждый DC **по очереди**. В JSON у записи появляется массив `dcs[]` с результатом по каждому DC. Поле `ok` у строки — **true, если хотя бы один DC прошёл** (как при «есть живой путь к Telegram» при переборе DC) |
|
| `MTPROXY_DC_IDS` | *(пусто)* | Список DC через запятую, например `1,2,3,4,5`: для каждой строки прокси выполняется отдельная проверка на каждый DC **по очереди**. В JSON у записи появляется массив `dcs[]` с результатом по каждому DC. Поле `ok` у строки — **true, если хотя бы один DC прошёл** (как при «есть живой путь к Telegram» при переборе DC) |
|
||||||
| `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` **не** используется |
|
||||||
|
| `MTPROXY_TDLIB_HELPER` | *(пусто)* | Путь к исполняемому **внешнему** helper: один аргумент — строка `tg://` с прокси. В stdout — строка JSON `{"ok":bool,"error":"…","exit_code":int}`. Рекомендуется нативный `tdlib_ping` (см. `cmd/tdlib_ping/README.md`); при необходимости — обёртка `exec node …/ping.js` из `contrib/tdlib-ping`. Запускается **только** при `MTPROXY_PROBE=fast` (или пусто). Без TDLib в образе по умолчанию |
|
||||||
|
| `MTPROXY_TDLIB_TIMEOUT` | `45s` | Таймаут **одного** вызова helper на строку прокси |
|
||||||
|
|
||||||
### HTTP
|
### HTTP
|
||||||
|
|
||||||
@@ -62,7 +64,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 — при `MTPROXY_PROBE=fast`: рукопожатие + init и прокси не закрыл TCP сразу (как Telethon #1134); при **`deep`**: дополнительно получен `resPQ` от DC через туннель); `1` ошибка проверки, в т.ч. нет `resPQ` за время ожидания в `deep`; `2` ошибка разбора URL/секрета; `3` прокси закрыл соединение; `4` таймаут всего запроса), `error`, при необходимости `parse_error`, `checked_at`. Если задан `MTPROXY_DC_IDS` с **несколькими** DC, добавляется массив `dcs` (`dc`, `ok`, `exit_code`, `error` по каждому); агрегатный `ok` — true, если **хотя бы один** DC успешен.
|
Элемент `proxies[]`: поля **`standard`** (наша проверка: `ok`, `exit_code`, `error`, `parse_error`) и **`tdlib`** (результат helper: `ran`, при пропуске — `skipped_reason`; при запуске — `ok`, `exit_code`, `error`, `duration_ms`). Для совместимости дублируются корневые `ok`, `exit_code`, `error`, `parse_error` — те же значения, что и в `standard`. `raw_line`, при успешном разборе — `url`, `checked_at`. Смысл кодов в `standard`: при `MTPROXY_PROBE=fast` — рукопожатие + init (+ ловля отложенного закрытия); при **`deep`** / **`deep-strict`** — см. описание `MTPROXY_PROBE`. Если задан `MTPROXY_DC_IDS` с **несколькими** DC — массив `dcs`; корневой `ok` true, если **хотя бы один** DC успешен.
|
||||||
|
|
||||||
### Whitelist IP и Docker
|
### Whitelist IP и Docker
|
||||||
|
|
||||||
@@ -282,8 +284,8 @@ docker run --rm registry.example.com/owner/mtproxy_checker:latest \
|
|||||||
|
|
||||||
| Код | Значение |
|
| Код | Значение |
|
||||||
|-----|----------|
|
|-----|----------|
|
||||||
| 0 | **`fast`**: Fake-TLS/dd + MTProxy init, прокси не закрыл соединение сразу (как Telethon). **`deep`**: плюс подтверждён `resPQ` от DC |
|
| 0 | **`fast`**: Fake-TLS/dd + MTProxy init, прокси не закрыл соединение сразу (как Telethon). **`deep`**: плюс ответ DC после `req_pq` (валидное unencrypted MTProto). **`deep-strict`**: ответ должен быть `resPQ` |
|
||||||
| 1 | Ошибка (сеть, протокол; в **`deep`** — в т.ч. нет валидного `resPQ`) |
|
| 1 | Ошибка (сеть, протокол; в **`deep`** — нет ответа DC; в **`deep-strict`** — нет `resPQ`) |
|
||||||
| 2 | Неверные аргументы CLI |
|
| 2 | Неверные аргументы CLI |
|
||||||
| 3 | Прокси закрыл TCP сразу после начального payload |
|
| 3 | Прокси закрыл TCP сразу после начального payload |
|
||||||
| 4 | Общий таймаут (`-timeout`) |
|
| 4 | Общий таймаут (`-timeout`) |
|
||||||
@@ -294,7 +296,8 @@ docker run --rm registry.example.com/owner/mtproxy_checker:latest \
|
|||||||
|
|
||||||
- `connection refused` — порт закрыт или фильтр.
|
- `connection refused` — порт закрыт или фильтр.
|
||||||
- `FAIL: timeout` — нет ответа за `-timeout`.
|
- `FAIL: timeout` — нет ответа за `-timeout`.
|
||||||
- `no resPQ from telegram through proxy` — до DC достучаться не удалось или ответ не похож на `resPQ` (часто совпадает с «Недоступен» в клиенте).
|
- `no reply from telegram DC through proxy (timeout)` — в режиме **`deep`** за время ожидания не получено ни одного валидного unencrypted-ответа DC после `req_pq`.
|
||||||
|
- `no resPQ from telegram through proxy (tunnel may be broken)` — только в **`deep-strict`**: ответ не распознан как `resPQ`.
|
||||||
- Ошибки чтения/проверки ServerHello — несовместимый ответ или обрыв соединения.
|
- Ошибки чтения/проверки ServerHello — несовместимый ответ или обрыв соединения.
|
||||||
|
|
||||||
Для отладки без `--rm` можно посмотреть логи контейнера по id; с `--rm` контейнер удаляется сразу после выхода.
|
Для отладки без `--rm` можно посмотреть логи контейнера по id; с `--rm` контейнер удаляется сразу после выхода.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"mtproxy_checker/internal/faketls"
|
"mtproxy_checker/internal/faketls"
|
||||||
@@ -75,7 +76,8 @@ func checkEE(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
|||||||
if err := tgquick.DrainPostInitEE(ctx, br, conn, dec, 5*time.Second); err != nil {
|
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 {
|
strict := o.Probe == ProbeDeepStrict
|
||||||
|
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, br, strict); err != nil {
|
||||||
if errors.Is(err, tgquick.ErrPeerClosed) {
|
if errors.Is(err, tgquick.ErrPeerClosed) {
|
||||||
return fmt.Errorf("%w", ErrProxyClosed)
|
return fmt.Errorf("%w", ErrProxyClosed)
|
||||||
}
|
}
|
||||||
@@ -95,7 +97,8 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
|||||||
if o.Probe == ProbeFast {
|
if o.Probe == ProbeFast {
|
||||||
return waitPostPayload(conn, conn)
|
return waitPostPayload(conn, conn)
|
||||||
}
|
}
|
||||||
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, nil); err != nil {
|
strict := o.Probe == ProbeDeepStrict
|
||||||
|
if err := tgquick.VerifyResPQ(ctx, conn, enc, dec, nil, strict); err != nil {
|
||||||
if errors.Is(err, tgquick.ErrPeerClosed) {
|
if errors.Is(err, tgquick.ErrPeerClosed) {
|
||||||
return fmt.Errorf("%w", ErrProxyClosed)
|
return fmt.Errorf("%w", ErrProxyClosed)
|
||||||
}
|
}
|
||||||
@@ -104,9 +107,26 @@ func checkDD(ctx context.Context, conn net.Conn, p *secret.Parsed, dcID int16, o
|
|||||||
return nil
|
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):
|
// waitPostPayload после init повторяет логику Telethon TcpMTProxy._connect (#1134):
|
||||||
// ждём появления данных или таймаут, затем считаем успехом только если соединение не закрыто сразу после payload.
|
// ждём появления данных или таймаут, затем считаем успехом только если соединение не закрыто сразу после payload.
|
||||||
// Наличие входящих байтов не обязательно — часть прокси молчит до первого MTProto от клиента.
|
// Наличие входящих байтов не обязательно — часть прокси молчит до первого MTProto от клиента.
|
||||||
|
// После окна idle дополнительно читаем короткими интервалами: часть прокси шлёт FIN/RST сразу после init,
|
||||||
|
// но не в первых 200ms цикла — без этого fast давал бы ложный OK.
|
||||||
func waitPostPayload(r io.Reader, c net.Conn) error {
|
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)
|
||||||
@@ -122,6 +142,37 @@ func waitPostPayload(r io.Reader, c net.Conn) error {
|
|||||||
_ = c.SetReadDeadline(time.Time{})
|
_ = c.SetReadDeadline(time.Time{})
|
||||||
return ErrProxyClosed
|
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
|
var ne net.Error
|
||||||
if errors.As(err, &ne) && ne.Timeout() {
|
if errors.As(err, &ne) && ne.Timeout() {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ type ProbeMode int
|
|||||||
const (
|
const (
|
||||||
// ProbeFast: handshake + init, then short wait like Telethon TcpMTProxy (#1134): OK if proxy does not close immediately.
|
// 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 then any valid unencrypted MTProto reply from DC (tunnel carries DC traffic; not only resPQ).
|
||||||
ProbeDeep
|
ProbeDeep
|
||||||
|
// ProbeDeepStrict: req_pq and response must be resPQ#05162463 (legacy strict check).
|
||||||
|
ProbeDeepStrict
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options configures Check. Nil or zero value uses ProbeFast.
|
// Options configures Check. Nil or zero value uses ProbeFast.
|
||||||
@@ -17,9 +19,11 @@ type Options struct {
|
|||||||
Probe ProbeMode
|
Probe ProbeMode
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseProbe maps env/flag strings: "", "fast" -> ProbeFast; "deep" -> ProbeDeep.
|
// ParseProbe maps env/flag strings: "", "fast" -> ProbeFast; "deep" -> ProbeDeep; "deep-strict" -> ProbeDeepStrict.
|
||||||
func ParseProbe(s string) ProbeMode {
|
func ParseProbe(s string) ProbeMode {
|
||||||
switch strings.ToLower(strings.TrimSpace(s)) {
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||||
|
case "deep-strict", "strict-deep", "respq-strict":
|
||||||
|
return ProbeDeepStrict
|
||||||
case "deep", "respq", "dc":
|
case "deep", "respq", "dc":
|
||||||
return ProbeDeep
|
return ProbeDeep
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// Package tdlibexec runs an optional external helper (e.g. Node + TDLib) for proxy verification.
|
||||||
|
// Contract: helper argv = [helperPath, proxyURL]; stdout must contain a JSON line:
|
||||||
|
//
|
||||||
|
// {"ok":true,"error":"","exit_code":0}
|
||||||
|
package tdlibexec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result is the parsed outcome of a helper invocation.
|
||||||
|
type Result struct {
|
||||||
|
OK bool
|
||||||
|
ExitCode int
|
||||||
|
Error string
|
||||||
|
DurationMs int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonLine struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
ExitCode int `json:"exit_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes helperPath with a single argument proxyURL and parses the last JSON object from stdout.
|
||||||
|
func Run(ctx context.Context, helperPath, proxyURL string) (Result, error) {
|
||||||
|
start := time.Now()
|
||||||
|
ms := func() int64 { return time.Since(start).Milliseconds() }
|
||||||
|
|
||||||
|
if strings.TrimSpace(helperPath) == "" {
|
||||||
|
return Result{}, errors.New("empty helper path")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, helperPath, proxyURL)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
duration := ms()
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return Result{OK: false, ExitCode: 4, Error: "tdlib helper: " + ctx.Err().Error(), DurationMs: duration}, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
r, perr := parseStdout(stdout.Bytes(), stderr.Bytes(), duration)
|
||||||
|
if perr != nil {
|
||||||
|
return Result{OK: false, ExitCode: 1, Error: perr.Error(), DurationMs: duration}, nil
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ee *exec.ExitError
|
||||||
|
if errors.As(err, &ee) {
|
||||||
|
if r, perr := parseStdout(stdout.Bytes(), stderr.Bytes(), duration); perr == nil {
|
||||||
|
if r.ExitCode == 0 && !r.OK {
|
||||||
|
r.ExitCode = ee.ExitCode()
|
||||||
|
}
|
||||||
|
if r.ExitCode == 0 && !r.OK {
|
||||||
|
r.ExitCode = 2
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
msg := strings.TrimSpace(stdout.String())
|
||||||
|
if msg == "" {
|
||||||
|
msg = strings.TrimSpace(stderr.String())
|
||||||
|
}
|
||||||
|
if msg == "" {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
code := ee.ExitCode()
|
||||||
|
if code < 0 || code > 4 {
|
||||||
|
code = 2
|
||||||
|
}
|
||||||
|
return Result{OK: false, ExitCode: code, Error: msg, DurationMs: duration}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result{}, fmt.Errorf("tdlib helper: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStdout(stdout, stderr []byte, duration int64) (Result, error) {
|
||||||
|
lines := strings.Split(string(stdout), "\n")
|
||||||
|
for i := len(lines) - 1; i >= 0; i-- {
|
||||||
|
line := strings.TrimSpace(lines[i])
|
||||||
|
if line == "" || line[0] != '{' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var j jsonLine
|
||||||
|
if err := json.Unmarshal([]byte(line), &j); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if j.ExitCode == 0 && !j.OK {
|
||||||
|
j.ExitCode = 2
|
||||||
|
}
|
||||||
|
if j.Error == "" && !j.OK {
|
||||||
|
j.Error = "tdlib reported failure"
|
||||||
|
}
|
||||||
|
return Result{OK: j.OK, ExitCode: j.ExitCode, Error: j.Error, DurationMs: duration}, nil
|
||||||
|
}
|
||||||
|
if len(stderr) > 0 {
|
||||||
|
return Result{}, fmt.Errorf("no json in stdout: stderr=%s", strings.TrimSpace(string(stderr)))
|
||||||
|
}
|
||||||
|
return Result{}, fmt.Errorf("no json line in helper stdout")
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package tdlibexec
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseStdout(t *testing.T) {
|
||||||
|
r, err := parseStdout([]byte("noise\n{\"ok\":true,\"error\":\"\",\"exit_code\":0}\n"), nil, 42)
|
||||||
|
if err != nil || !r.OK || r.ExitCode != 0 || r.DurationMs != 42 {
|
||||||
|
t.Fatalf("got %+v err=%v", r, err)
|
||||||
|
}
|
||||||
|
r2, err := parseStdout([]byte(`{"ok":false,"error":"bad","exit_code":2}`), nil, 1)
|
||||||
|
if err != nil || r2.OK || r2.ExitCode != 2 || r2.Error != "bad" {
|
||||||
|
t.Fatalf("got %+v err=%v", r2, err)
|
||||||
|
}
|
||||||
|
if _, err := parseStdout([]byte("no json"), nil, 0); err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package tgquick performs a minimal MTProto step (req_pq → resPQ) through an established MTProxy tunnel,
|
// Package tgquick performs a minimal MTProto step (req_pq → ответ DC) through an established MTProxy tunnel,
|
||||||
// matching Telethon's RandomizedIntermediate + MTProxyIO framing.
|
// matching Telethon's RandomizedIntermediate + MTProxyIO framing.
|
||||||
package tgquick
|
package tgquick
|
||||||
|
|
||||||
@@ -24,9 +24,12 @@ const (
|
|||||||
maxRI = 1 << 20
|
maxRI = 1 << 20
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNoResPQ means no valid resPQ was received from Telegram through the tunnel.
|
// ErrNoResPQ means strict deep check did not see resPQ#05162463 from DC.
|
||||||
var ErrNoResPQ = errors.New("no resPQ from telegram through proxy (tunnel may be broken)")
|
var ErrNoResPQ = errors.New("no resPQ from telegram through proxy (tunnel may be broken)")
|
||||||
|
|
||||||
|
// ErrNoDCReply means relaxed deep check got no parsable unencrypted MTProto reply from DC before deadline.
|
||||||
|
var ErrNoDCReply = errors.New("no reply from telegram DC through proxy (timeout)")
|
||||||
|
|
||||||
// ErrPeerClosed is returned when the remote side closes TCP during the MTProto probe.
|
// ErrPeerClosed is returned when the remote side closes TCP during the MTProto probe.
|
||||||
var ErrPeerClosed = errors.New("connection closed by peer during mtproto probe")
|
var ErrPeerClosed = errors.New("connection closed by peer during mtproto probe")
|
||||||
|
|
||||||
@@ -109,6 +112,22 @@ func isResPQ(mtInner []byte) bool {
|
|||||||
return binary.LittleEndian.Uint32(body[0:4]) == tlResPQ
|
return binary.LittleEndian.Uint32(body[0:4]) == tlResPQ
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isUnencryptedDCEnvelope reports a valid MTProto unencrypted container (auth_key_id 0) with non-empty body.
|
||||||
|
// After req_pq the DC normally sends resPQ, but any well-formed unencrypted reply proves the tunnel carried DC traffic.
|
||||||
|
func isUnencryptedDCEnvelope(mtInner []byte) bool {
|
||||||
|
if len(mtInner) < 24 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if binary.LittleEndian.Uint64(mtInner[0:8]) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ml := int(binary.LittleEndian.Uint32(mtInner[16:20]))
|
||||||
|
if ml < 4 || ml > maxRI || 20+ml > len(mtInner) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -172,9 +191,10 @@ func DrainPostInitEE(ctx context.Context, br *bufio.Reader, conn net.Conn, dec c
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyResPQ sends req_pq and waits for resPQ on the MTProxy byte stream (decrypt with dec, encrypt with enc).
|
// VerifyResPQ sends req_pq and waits for a DC reply on the MTProxy byte stream (decrypt with dec, encrypt with enc).
|
||||||
|
// If strictResPQ is true, the first matching RI frame must be resPQ; otherwise any valid unencrypted MTProto message is enough.
|
||||||
// For ee (fake-TLS), pass the same bufio.Reader used after init (and DrainPostInitEE); for dd pass tlsBR=nil.
|
// For ee (fake-TLS), pass the same bufio.Reader used after init (and DrainPostInitEE); for dd pass tlsBR=nil.
|
||||||
func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, tlsBR *bufio.Reader) error {
|
func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, tlsBR *bufio.Reader, strictResPQ bool) error {
|
||||||
plain, err := buildReqPQ()
|
plain, err := buildReqPQ()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("build req_pq: %w", err)
|
return fmt.Errorf("build req_pq: %w", err)
|
||||||
@@ -263,14 +283,24 @@ func VerifyResPQ(ctx context.Context, conn net.Conn, enc, dec cipher.Stream, tls
|
|||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if isResPQ(frame) {
|
if strictResPQ {
|
||||||
|
if isResPQ(frame) {
|
||||||
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isUnencryptedDCEnvelope(frame) {
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
return ErrNoResPQ
|
if strictResPQ {
|
||||||
|
return ErrNoResPQ
|
||||||
|
}
|
||||||
|
return ErrNoDCReply
|
||||||
}
|
}
|
||||||
|
|
||||||
// readNextTLS17Payload reads TLS 1.2-style records from the wire (plaintext record headers).
|
// readNextTLS17Payload reads TLS 1.2-style records from the wire (plaintext record headers).
|
||||||
|
|||||||
@@ -34,3 +34,15 @@ func TestIsResPQ(t *testing.T) {
|
|||||||
t.Fatal("expected resPQ")
|
t.Fatal("expected resPQ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsUnencryptedDCEnvelope(t *testing.T) {
|
||||||
|
msgData := make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint32(msgData, 0x11223344)
|
||||||
|
w := wrapUnencrypted(msgData)
|
||||||
|
if !isUnencryptedDCEnvelope(w) {
|
||||||
|
t.Fatal("expected envelope for arbitrary constructor")
|
||||||
|
}
|
||||||
|
if isResPQ(w) {
|
||||||
|
t.Fatal("not resPQ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user