diff --git a/changelog.md b/changelog.md index de669045..1eb5e33b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # B4 - Bye Bye Big Bro +## [1.73.0] - 2026-07-05 + +- FIXED: **A phone that dropped off mobile data could leave a Telegram proxy connection stuck** - when a mobile client backgrounded Telegram or lost signal, its connection frequently died without a clean close, yet b4 held the half-dead session and its link out to Telegram open for many minutes, in some cases until the app was reopened, so returning to Telegram could mean waiting on a dead socket instead of a clean reconnect. +- FIXED: **A stalled link out to Telegram over a WebSocket bridge could wedge a proxy session** - if that link stalled mid-transfer, tearing the session down could itself block for many minutes on the stuck write, holding the client's slot and both connections until a kernel timeout expired. +- ADDED: **Tunable MTProto connection timeouts** - Settings → MTProto exposes a TCP user timeout and an idle timeout for the proxy, for unusual carrier networks or router kernels where the built-in defaults (about two minutes and five minutes) need adjusting or turning off. +- ADDED: **API to list active MTProto sessions and clients** - two HTTP endpoints for integrations: GET /api/mtproto/sessions returns one entry per live client connection (client IP and port, the Telegram destination, and when it connected and was last active), and GET /api/mtproto/active-clients groups those by secret (active connection count and the distinct client IPs using each one). The API exposed only a per-secret connection count, with no way to see the client addresses behind it. + ## [1.72.0] - 2026-07-02 - ADDED: **A separate MTProto secret for each person** - the MTProto proxy can now hold several named secrets instead of one shared code. Each one can be named (for example, per person), switched on or off on its own, and shared as its own link or QR code, so access can be given or taken away one person at a time. diff --git a/changelog_ru.md b/changelog_ru.md index 01eed064..911f2311 100644 --- a/changelog_ru.md +++ b/changelog_ru.md @@ -1,5 +1,12 @@ # B4 - Bye Bye Big Bro +## [1.73.0] - 2026-07-05 + +- ИСПРАВЛЕНО: **Телефон, отвалившийся от мобильного интернета, мог оставить подключение к прокси Telegram зависшим** - когда мобильный клиент сворачивал Telegram или терял сигнал, его соединение часто обрывалось без чистого закрытия, но b4 удерживал полумёртвую сессию и свой канал до Telegram открытыми много минут, иногда до повторного открытия приложения, поэтому при возврате в Telegram можно было упереться в мёртвый сокет вместо чистого переподключения. +- ИСПРАВЛЕНО: **Зависший канал до Telegram через WebSocket-мост мог заклинить сессию прокси** - если этот канал застревал посреди передачи, само закрытие сессии могло заблокироваться на много минут на застрявшей записи, удерживая слот клиента и оба соединения до истечения таймаута ядра. +- ДОБАВЛЕНО: **Настраиваемые таймауты подключений MTProto** - в «Настройки → MTProto» появились TCP user timeout и таймаут простоя для прокси, на случай нестандартных операторских сетей или ядер роутеров, где встроенные значения по умолчанию (около двух и пяти минут) нужно подправить или отключить. +- ДОБАВЛЕНО: **API для просмотра активных сессий и клиентов MTProto** - два HTTP-эндпоинта для интеграций: GET /api/mtproto/sessions возвращает по одной записи на каждое живое клиентское соединение (IP и порт клиента, адрес назначения в Telegram, время подключения и последней активности), а GET /api/mtproto/active-clients группирует их по секрету (число активных соединений и различные IP клиентов, использующих каждый). API отдавал только число соединений на секрет, без возможности увидеть адреса клиентов за ним. + ## [1.72.0] - 2026-07-02 - ДОБАВЛЕНО: **Отдельный секрет MTProto для каждого человека** - прокси MTProto теперь может хранить несколько именованных секретов вместо одного общего кода. Каждому можно задать имя (например, по человеку), включить или выключить его отдельно и поделиться им отдельной ссылкой или QR-кодом, поэтому доступ можно выдавать или отзывать по одному человеку. diff --git a/docs/src/pages/swagger.tsx b/docs/src/pages/swagger.tsx index 1d2fdb84..63e11207 100644 --- a/docs/src/pages/swagger.tsx +++ b/docs/src/pages/swagger.tsx @@ -25,19 +25,74 @@ function buildSwaggerHtml(specUrl: string, server: string): string {
diff --git a/docs/static/swagger-versions/index.json b/docs/static/swagger-versions/index.json index 0f746bb4..3a95c0eb 100644 --- a/docs/static/swagger-versions/index.json +++ b/docs/static/swagger-versions/index.json @@ -1,4 +1,6 @@ [ + "v1.73.0" +, "v1.71.0" , "v1.67.2" diff --git a/docs/static/swagger-versions/v1.73.0.json b/docs/static/swagger-versions/v1.73.0.json new file mode 100644 index 00000000..4f9e7e50 --- /dev/null +++ b/docs/static/swagger-versions/v1.73.0.json @@ -0,0 +1,5695 @@ +{ + "swagger": "2.0", + "info": { + "description": "B4 network packet processor REST API", + "title": "B4 API", + "contact": {}, + "version": "1.73.0" + }, + "basePath": "/api", + "paths": { + "/ai/chat": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Streams a Server-Sent Events response with delta/done/error events for a chat conversation.", + "consumes": [ + "application/json" + ], + "produces": [ + "text/event-stream" + ], + "tags": [ + "AI" + ], + "summary": "Stream an AI chat completion", + "parameters": [ + { + "description": "Chat request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.aiChatRequest" + } + } + ], + "responses": { + "200": { + "description": "SSE stream", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "AI manager not initialized or provider unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/ai/explain": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Streams a Server-Sent Events response with delta/done/error events.", + "consumes": [ + "application/json" + ], + "produces": [ + "text/event-stream" + ], + "tags": [ + "AI" + ], + "summary": "Stream an AI explanation for a config setting", + "parameters": [ + { + "description": "Explain request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.aiExplainRequest" + } + } + ], + "responses": { + "200": { + "description": "SSE stream", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "AI manager not initialized or provider unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/ai/models": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "List models for an AI provider", + "parameters": [ + { + "type": "string", + "description": "Provider id (openai, anthropic, ollama). Defaults to configured provider.", + "name": "provider", + "in": "query" + }, + { + "type": "string", + "description": "Override endpoint URL (used when provider matches the configured one)", + "name": "endpoint", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "428": { + "description": "API key missing for provider", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "502": { + "description": "Upstream provider error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "AI manager not initialized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/ai/secrets": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "List stored AI secret refs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "503": { + "description": "AI manager not initialized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Save an AI provider secret", + "parameters": [ + { + "description": "Secret ref and key", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.aiSecretBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Delete an AI provider secret", + "parameters": [ + { + "type": "string", + "description": "Secret ref to remove", + "name": "ref", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/ai/status": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns whether the AI assistant is enabled, configured, and ready to serve requests.", + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Get AI assistant status", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.aiStatusResponse" + } + } + } + } + }, + "/asn": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ASN" + ], + "summary": "Get all ASN entries", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/config.AsnInfo" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ASN" + ], + "summary": "Create or update ASN entry", + "parameters": [ + { + "description": "ASN info", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.AsnInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/config.AsnInfo" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ASN" + ], + "summary": "Delete ASN entry", + "parameters": [ + { + "type": "string", + "description": "ASN ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/asn/lookup": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ASN" + ], + "summary": "Lookup ASN by IP address", + "parameters": [ + { + "type": "string", + "description": "IP address", + "name": "ip", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/config.AsnInfo" + } + } + } + } + }, + "/auth/check": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Check authentication status", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/auth/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Login with credentials", + "parameters": [ + { + "description": "Login credentials (username, password)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object" + } + } + } + } + }, + "/auth/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Logout and invalidate token", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/backup": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/gzip" + ], + "tags": [ + "Backup" + ], + "summary": "Download configuration backup", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + } + } + }, + "/backup/restore": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Backup" + ], + "summary": "Restore configuration from backup", + "parameters": [ + { + "type": "file", + "description": "Backup tar.gz file", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/capture/clear": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Capture" + ], + "summary": "Clear all captures", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/capture/delete": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Capture" + ], + "summary": "Delete a capture", + "parameters": [ + { + "type": "string", + "description": "Protocol (tls or quic)", + "name": "protocol", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Domain name", + "name": "domain", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/capture/download": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Capture" + ], + "summary": "Download a capture file", + "parameters": [ + { + "type": "string", + "description": "Filename", + "name": "file", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + } + } + }, + "/capture/generate": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Capture" + ], + "summary": "Generate capture payload", + "parameters": [ + { + "description": "Capture request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.CaptureRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/capture/list": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Capture" + ], + "summary": "List all captures", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "/capture/probe": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Capture" + ], + "summary": "Probe domain for capture", + "parameters": [ + { + "description": "Capture request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.CaptureRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/capture/upload": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Capture" + ], + "summary": "Upload a capture file", + "parameters": [ + { + "type": "file", + "description": "Capture binary file", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Domain name", + "name": "domain", + "in": "formData", + "required": true + }, + { + "type": "string", + "default": "tls", + "description": "Protocol (tls or quic)", + "name": "protocol", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/config": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Config" + ], + "summary": "Get full configuration with statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ConfigResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Config" + ], + "summary": "Update configuration", + "parameters": [ + { + "description": "Updated configuration", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.Config" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ConfigResponse" + } + } + } + } + }, + "/config/reset": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Resets configuration to defaults, preserving sets, web server settings and geo file paths.", + "produces": [ + "application/json" + ], + "tags": [ + "Config" + ], + "summary": "Reset configuration to defaults", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ConfigResponse" + } + } + } + } + }, + "/detector/cancel/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Detector" + ], + "summary": "Cancel detector suite", + "parameters": [ + { + "type": "string", + "description": "Suite ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/detector/history": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Detector" + ], + "summary": "Get detector history", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "/detector/history/clear": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Detector" + ], + "summary": "Clear detector history", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/detector/history/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Detector" + ], + "summary": "Delete detector history entry", + "parameters": [ + { + "type": "string", + "description": "Entry ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/detector/start": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Detector" + ], + "summary": "Start detection suite", + "parameters": [ + { + "description": "Detector request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.DetectorRequest" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/handler.DetectorResponse" + } + } + } + } + }, + "/detector/status/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Detector" + ], + "summary": "Get detector status", + "parameters": [ + { + "type": "string", + "description": "Suite ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/devices": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Get all devices", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.DevicesResponse" + } + } + } + } + }, + "/devices/{mac}/vendor": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Get device vendor info", + "parameters": [ + { + "type": "string", + "description": "MAC address", + "name": "mac", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.VendorInfo" + } + } + } + } + }, + "/discovery/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Add discovery preset as a new set", + "parameters": [ + { + "description": "Set configuration", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.SetConfig" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/discovery/cache/clear": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Clear discovery cache", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/discovery/cancel/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Cancel discovery", + "parameters": [ + { + "type": "string", + "description": "Suite ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/discovery/current": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Get current running discovery", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/discovery/history": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Get discovery history", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "/discovery/history/clear": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Clear discovery history", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/discovery/history/{domain}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Delete discovery history entry", + "parameters": [ + { + "type": "string", + "description": "Domain name", + "name": "domain", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/discovery/similar": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Find sets with similar configuration", + "parameters": [ + { + "description": "Set to compare", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.SetConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "/discovery/start": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Start domain discovery", + "parameters": [ + { + "description": "Discovery request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.DiscoveryRequest" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/handler.DiscoveryResponse" + } + } + } + } + }, + "/discovery/status/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discovery" + ], + "summary": "Get discovery status", + "parameters": [ + { + "type": "string", + "description": "Suite ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/dns": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "DNS" + ], + "summary": "Get public DNS servers by country", + "parameters": [ + { + "type": "string", + "description": "Country code (default: us)", + "name": "country", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.PublicDNSServer" + } + } + } + } + } + }, + "/geodat/download": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Geodat" + ], + "summary": "Download geodat files", + "parameters": [ + { + "description": "Download request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.GeodatDownloadRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GeodatDownloadResponse" + } + } + } + } + }, + "/geodat/info": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Geodat" + ], + "summary": "Get geodat file info", + "parameters": [ + { + "type": "string", + "description": "File path", + "name": "path", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/geodat/sources": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Geodat" + ], + "summary": "List available geodat sources", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.GeodatSource" + } + } + } + } + } + }, + "/geodat/upload": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Geodat" + ], + "summary": "Upload geodat file", + "parameters": [ + { + "type": "file", + "description": "Geodat file (.dat or .db)", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "File type (geosite or geoip)", + "name": "type", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Destination directory path", + "name": "destination_path", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/geoip": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "GeoIP" + ], + "summary": "List geoip categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GeoipResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "GeoIP" + ], + "summary": "Add IP/CIDR blocks to a set", + "parameters": [ + { + "description": "CIDR blocks to add", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.AddGeoIpRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.AddIpResponse" + } + } + } + } + }, + "/geosite": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Geosite" + ], + "summary": "List geosite categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.GeositeResponse" + } + } + } + } + }, + "/geosite/category": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Geosite" + ], + "summary": "Preview geosite category domains", + "parameters": [ + { + "type": "string", + "description": "Category tag name", + "name": "tag", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/geosite/domain": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Geosite" + ], + "summary": "Add domain to a set via geosite", + "parameters": [ + { + "description": "Domain to add", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.AddDomainRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.AddDomainResponse" + } + } + } + } + }, + "/integration/ipinfo": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Integration" + ], + "summary": "Query IPInfo API for IP details", + "parameters": [ + { + "type": "string", + "description": "IP address", + "name": "ip", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/integration/ripestat": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Integration" + ], + "summary": "Query RIPE network info for IP", + "parameters": [ + { + "type": "string", + "description": "IP address", + "name": "ip", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/integration/ripestat/asn": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Integration" + ], + "summary": "Query RIPE ASN announced prefixes", + "parameters": [ + { + "type": "string", + "description": "ASN number", + "name": "asn", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/logs/trace/download": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Streams the most recently finished trace file as a plain-text attachment.", + "produces": [ + "text/plain" + ], + "tags": [ + "Logs" + ], + "summary": "Download the last log trace file", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "No trace file available", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/logs/trace/start": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Captures all log output to a file until stopped or the max duration elapses. The file is prefixed with build info and a full system diagnostics snapshot. Only one session may run at a time.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Start a log trace session", + "parameters": [ + { + "description": "Optional note describing the session", + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/handler.TraceStartRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.TraceStatusResponse" + } + }, + "409": { + "description": "A trace session is already running", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/logs/trace/status": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Reports whether a trace is active, its captured line count and start time, and whether a finished trace is available to download.", + "produces": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Get log trace session status", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.TraceStatusResponse" + } + } + } + } + }, + "/logs/trace/stop": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Finalizes the current trace file (writes footer, flushes, closes) and makes it available for download.", + "produces": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Stop the active log trace session", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.TraceStatusResponse" + } + }, + "400": { + "description": "No trace session is running", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/metrics": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Metrics" + ], + "summary": "Get full metrics snapshot", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/metrics/reset": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Metrics" + ], + "summary": "Reset metrics statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/metrics/summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Metrics" + ], + "summary": "Get metrics summary", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/mtproto/active-clients": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Aggregated per-secret view: active connection count, the distinct client IPs currently using each secret, and last activity.", + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "List active MTProto clients per secret", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "/mtproto/config": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "Get MTProto configuration", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "Update MTProto configuration", + "parameters": [ + { + "description": "MTProto configuration", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.MTProtoConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mtproto/generate-secret": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "Generate MTProto secret", + "parameters": [ + { + "description": "fake_sni field required", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mtproto/refresh-dcs": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "Refresh MTProto DCs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mtproto/sessions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "One entry per live client connection, including client IP/port, upstream destination and activity timestamps.", + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "List active MTProto sessions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "/mtproto/test-ws": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "Probe MTProto upstream transports", + "parameters": [ + { + "description": "optional overrides: upstream_mode, ws_custom_domain, ws_endpoint_host, cfworker_domain, cfproxy_enabled, dc_relay, dc", + "name": "body", + "in": "body", + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/sets": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "List all sets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/config.SetConfig" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Create a new set", + "parameters": [ + { + "description": "Set configuration", + "name": "set", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.SetConfig" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/config.SetConfig" + } + } + } + } + }, + "/sets/batch-delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Batch delete sets", + "parameters": [ + { + "description": "Set IDs to delete", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/sets/batch-set-enabled": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Batch enable/disable sets", + "parameters": [ + { + "description": "Set IDs and enabled flag", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/sets/check-domain": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Check which sets match a domain", + "parameters": [ + { + "type": "string", + "description": "Domain to check", + "name": "domain", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Set ID to exclude", + "name": "exclude", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "/sets/reorder": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Reorder sets", + "parameters": [ + { + "description": "Ordered set IDs", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/sets/targeted-domains": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "List all targeted domains from enabled sets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/sets/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Get a set by ID", + "parameters": [ + { + "type": "string", + "description": "Set ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/config.SetConfig" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Update a set", + "parameters": [ + { + "type": "string", + "description": "Set ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated set configuration", + "name": "set", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.SetConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/config.SetConfig" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Delete a set", + "parameters": [ + { + "type": "string", + "description": "Set ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/sets/{id}/add-domain": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sets" + ], + "summary": "Add domain to a set", + "parameters": [ + { + "type": "string", + "description": "Set ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Domain object", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/socks5/config": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SOCKS5" + ], + "summary": "Get SOCKS5 configuration", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SOCKS5" + ], + "summary": "Update SOCKS5 configuration", + "parameters": [ + { + "description": "SOCKS5 configuration", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.Socks5Config" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/system/cache": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Get cache statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "string" + } + } + } + } + }, + "/system/diagnostics": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Get system diagnostics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.DiagnosticsResponse" + } + } + } + } + }, + "/system/info": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Get system information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.SystemInfo" + } + } + } + } + }, + "/system/restart": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Restart the service", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RestartResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.RestartResponse" + } + } + } + } + }, + "/system/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Start update process", + "parameters": [ + { + "description": "Update request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.UpdateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.UpdateResponse" + } + } + } + } + }, + "/version": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Get version information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.VersionInfo" + } + } + } + } + }, + "/watchdog/check": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Schedules an out-of-band check for a domain that is already present in the watchdog list. The domain may be passed as a bare host or a full URL; both forms are matched against the stored list.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Watchdog" + ], + "summary": "Force an immediate watchdog check", + "parameters": [ + { + "description": "Domain to force-check", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.WatchdogDomainRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.WatchdogActionResponse" + } + }, + "400": { + "description": "domain is required", + "schema": { + "type": "string" + } + }, + "404": { + "description": "domain not in watchdog list", + "schema": { + "type": "string" + } + }, + "405": { + "description": "Method not allowed", + "schema": { + "type": "string" + } + }, + "503": { + "description": "watchdog is not running", + "schema": { + "type": "string" + } + } + } + } + }, + "/watchdog/disable": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Turns the watchdog off and persists the change in the configuration. No further domain checks are performed until it is re-enabled.", + "produces": [ + "application/json" + ], + "tags": [ + "Watchdog" + ], + "summary": "Disable the watchdog", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.WatchdogActionResponse" + } + }, + "405": { + "description": "Method not allowed", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to save configuration", + "schema": { + "type": "string" + } + } + } + } + }, + "/watchdog/domains": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Adds a domain to the list of monitored targets. Duplicates (including different URL forms that resolve to the same host) are rejected. The configuration is persisted and pushed to running services.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Watchdog" + ], + "summary": "Add a domain to the watchdog list", + "parameters": [ + { + "description": "Domain to add", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.WatchdogDomainRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.WatchdogActionResponse" + } + }, + "400": { + "description": "domain is required", + "schema": { + "type": "string" + } + }, + "405": { + "description": "Method not allowed", + "schema": { + "type": "string" + } + }, + "409": { + "description": "domain already in watchdog list", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to save configuration", + "schema": { + "type": "string" + } + } + } + } + }, + "/watchdog/domains/{domain}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Removes a domain from the monitored list. The path parameter may be either the exact stored value or the bare host extracted from a stored URL.", + "produces": [ + "application/json" + ], + "tags": [ + "Watchdog" + ], + "summary": "Remove a domain from the watchdog list", + "parameters": [ + { + "type": "string", + "description": "Domain or host to remove", + "name": "domain", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.WatchdogActionResponse" + } + }, + "400": { + "description": "domain is required", + "schema": { + "type": "string" + } + }, + "404": { + "description": "domain not found in watchdog list", + "schema": { + "type": "string" + } + }, + "405": { + "description": "Method not allowed", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to save configuration", + "schema": { + "type": "string" + } + } + } + } + }, + "/watchdog/enable": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Turns the watchdog on and persists the change in the configuration. Monitoring of configured domains resumes on the next tick.", + "produces": [ + "application/json" + ], + "tags": [ + "Watchdog" + ], + "summary": "Enable the watchdog", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.WatchdogActionResponse" + } + }, + "405": { + "description": "Method not allowed", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to save configuration", + "schema": { + "type": "string" + } + } + } + } + }, + "/watchdog/status": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns the current enabled state of the watchdog and the status of each monitored domain (last check, failures, cooldown, matched set, etc.).", + "produces": [ + "application/json" + ], + "tags": [ + "Watchdog" + ], + "summary": "Get watchdog status", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/watchdog.WatchdogState" + } + }, + "405": { + "description": "Method not allowed", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "ai.Message": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/ai.Role" + } + } + }, + "ai.Role": { + "type": "string", + "enum": [ + "system", + "user", + "assistant" + ], + "x-enum-varnames": [ + "RoleSystem", + "RoleUser", + "RoleAssistant" + ] + }, + "config.AIConfig": { + "type": "object", + "properties": { + "api_key_ref": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "max_tokens": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "temperature": { + "type": "number" + }, + "timeout_sec": { + "type": "integer" + } + } + }, + "config.ApiConfig": { + "type": "object", + "properties": { + "ipinfo_token": { + "type": "string" + } + } + }, + "config.AsnInfo": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prefixes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "config.ComboFragConfig": { + "type": "object", + "properties": { + "decoy_enabled": { + "type": "boolean" + }, + "extension_split": { + "type": "boolean" + }, + "fake_per_seg_count": { + "description": "Number of fake packets per segment (default 1)", + "type": "integer" + }, + "fake_per_seg_count_max": { + "description": "Max for randomization (0 = use fixed)", + "type": "integer" + }, + "fake_per_segment": { + "type": "boolean" + }, + "first_byte_split": { + "type": "boolean" + }, + "first_delay_ms": { + "type": "integer" + }, + "first_delay_ms_max": { + "type": "integer" + }, + "jitter_max_us": { + "type": "integer" + }, + "jitter_max_us_max": { + "type": "integer" + }, + "shuffle_mode": { + "description": "\"middle\", \"full\", \"reverse\"", + "type": "string" + } + } + }, + "config.Config": { + "type": "object", + "properties": { + "queue": { + "$ref": "#/definitions/config.QueueConfig" + }, + "sets": { + "type": "array", + "items": { + "$ref": "#/definitions/config.SetConfig" + } + }, + "system": { + "$ref": "#/definitions/config.SystemConfig" + }, + "version": { + "type": "integer" + } + } + }, + "config.DNSConfig": { + "type": "object", + "properties": { + "doh_url": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "fragment_query": { + "type": "boolean" + }, + "target_dns": { + "type": "string" + } + } + }, + "config.DesyncConfig": { + "type": "object", + "properties": { + "count": { + "description": "Number of desync packets", + "type": "integer" + }, + "mode": { + "description": "\"off\" \"rst\", \"fin\", \"ack\", \"combo\", \"full\"", + "type": "string" + }, + "post_desync": { + "description": "Send fake RST after ClientHello", + "type": "boolean" + }, + "ttl": { + "description": "TTL for desync packets", + "type": "integer" + } + } + }, + "config.Device": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "is_manual": { + "type": "boolean" + }, + "mac": { + "type": "string" + }, + "mss_clamp": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "selected": { + "type": "boolean" + } + } + }, + "config.DevicesConfig": { + "type": "object", + "properties": { + "devices": { + "type": "array", + "items": { + "$ref": "#/definitions/config.Device" + } + }, + "enabled": { + "type": "boolean" + }, + "vendor_lookup": { + "type": "boolean" + }, + "wisb": { + "type": "boolean" + } + } + }, + "config.DiscoveryConfig": { + "type": "object", + "properties": { + "config_propagate_ms": { + "type": "integer" + }, + "discovery_flow_mark": { + "type": "integer" + }, + "discovery_injected_mark": { + "type": "integer" + }, + "discovery_timeout": { + "type": "integer" + }, + "reference_dns": { + "type": "array", + "items": { + "type": "string" + } + }, + "reference_domain": { + "type": "string" + }, + "validation_tries": { + "type": "integer" + }, + "watchdog": { + "$ref": "#/definitions/config.WatchdogConfig" + } + } + }, + "config.DisorderFragConfig": { + "type": "object", + "properties": { + "fake_per_seg_count": { + "description": "Number of fake packets per segment (default 1)", + "type": "integer" + }, + "fake_per_seg_count_max": { + "description": "Max for randomization (0 = use fixed)", + "type": "integer" + }, + "fake_per_segment": { + "type": "boolean" + }, + "max_jitter_us": { + "type": "integer" + }, + "min_jitter_us": { + "type": "integer" + }, + "shuffle_mode": { + "description": "\"full\", \"reverse\"", + "type": "string" + } + } + }, + "config.DuplicateConfig": { + "type": "object", + "properties": { + "count": { + "description": "Number of packet copies to send (original is dropped)", + "type": "integer" + }, + "enabled": { + "type": "boolean" + } + } + }, + "config.EscalateConfig": { + "type": "object", + "properties": { + "rst_threshold": { + "description": "0 -\u003e 3", + "type": "integer" + }, + "rst_window_sec": { + "description": "0 -\u003e 30", + "type": "integer" + }, + "to": { + "description": "ID of next set to use after this set is detected as blocked for a destination", + "type": "string" + }, + "ttl_sec": { + "description": "0 -\u003e 3600", + "type": "integer" + } + } + }, + "config.FakingConfig": { + "type": "object", + "properties": { + "custom_payload": { + "type": "string" + }, + "payload_domain": { + "type": "string" + }, + "payload_file": { + "type": "string" + }, + "seq_offset": { + "type": "integer" + }, + "sni": { + "type": "boolean" + }, + "sni_mutation": { + "$ref": "#/definitions/config.SNIMutationConfig" + }, + "sni_seq_length": { + "type": "integer" + }, + "sni_type": { + "type": "integer" + }, + "strategy": { + "type": "string" + }, + "tcp_md5": { + "description": "Enable TCP MD5 option insertion", + "type": "boolean" + }, + "timestamp_decrease": { + "description": "Amount to decrease TCP timestamp option", + "type": "integer" + }, + "tls_mod": { + "description": "e.g. [\"rnd\", \"dupsid\"]", + "type": "array", + "items": { + "type": "string" + } + }, + "ttl": { + "type": "integer" + } + } + }, + "config.FragmentationConfig": { + "type": "object", + "properties": { + "combo": { + "$ref": "#/definitions/config.ComboFragConfig" + }, + "disorder": { + "$ref": "#/definitions/config.DisorderFragConfig" + }, + "middle_sni": { + "type": "boolean" + }, + "oob_char": { + "description": "Character for OOB data", + "type": "integer" + }, + "oob_position": { + "description": "Position for OOB (0=disabled)", + "type": "integer" + }, + "oob_position_max": { + "description": "max for randomization (0 = use fixed)", + "type": "integer" + }, + "reverse_order": { + "type": "boolean" + }, + "seq_overlap_length": { + "type": "integer" + }, + "seq_overlap_pattern": { + "type": "array", + "items": { + "type": "string" + } + }, + "sni_position": { + "type": "integer" + }, + "sni_position_max": { + "description": "max for randomization (0 = use fixed)", + "type": "integer" + }, + "strategy": { + "description": "Values: \"tcp\", \"ip\", \"oob\", \"tls\", \"disorder\", \"extsplit\", \"firstbyte\", \"combo\", \"none\"", + "type": "string" + }, + "strategy_pool": { + "type": "array", + "items": { + "type": "string" + } + }, + "tlsrec_pos": { + "description": "where to split TLS record", + "type": "integer" + }, + "tlsrec_pos_max": { + "description": "max for randomization (0 = use fixed)", + "type": "integer" + } + } + }, + "config.IPBlockDetectConfig": { + "type": "object", + "properties": { + "cache_blocked_ips": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "retransmit_threshold": { + "type": "integer" + }, + "timeout_ms": { + "type": "integer" + } + } + }, + "config.IncomingConfig": { + "type": "object", + "properties": { + "fake_count": { + "type": "integer" + }, + "fake_ttl": { + "type": "integer" + }, + "max": { + "description": "threshold max (KB), if 0 or eq MinKB -\u003e uses MinKB", + "type": "integer" + }, + "min": { + "description": "threshold min (KB)", + "type": "integer" + }, + "mode": { + "description": "\"off\", \"fake\", \"reset\", \"fin\", \"desync\"", + "type": "string" + }, + "strategy": { + "description": "\"badsum\", \"badseq\", \"badack\", \"rand\", \"all\"", + "type": "string" + } + } + }, + "config.Logging": { + "type": "object", + "properties": { + "directory": { + "description": "Directory is the base folder for all b4 log files (errors.log, update.log, ...).\nEmpty disables file logging entirely.", + "type": "string" + }, + "instaflush": { + "type": "boolean" + }, + "level": { + "$ref": "#/definitions/log.Level" + }, + "syslog": { + "type": "boolean" + } + } + }, + "config.MSSClampConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "size": { + "description": "MSS value in bytes (e.g., 88)", + "type": "integer" + } + } + }, + "config.MTProtoConfig": { + "type": "object", + "properties": { + "bind_address": { + "type": "string" + }, + "cfproxy_enabled": { + "description": "enable Cloudflare-proxied fallback WS domains (rescues DCs the network blocks)", + "type": "boolean" + }, + "cfproxy_url": { + "description": "URL to refresh CF-proxy domain list; empty = built-in default", + "type": "string" + }, + "cfworker_domain": { + "description": "user's Cloudflare Worker domain(s) (workers.dev), comma-separated; free per-user WS relay tried before the shared CF pool", + "type": "string" + }, + "dc_fallback_enabled": { + "description": "fetch the DC IP list from DCFallbackURL when Telegram's official endpoint is blocked", + "type": "boolean" + }, + "dc_fallback_url": { + "description": "fallback source for the Telegram DC list; empty = built-in default", + "type": "string" + }, + "dc_relay": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "fake_sni": { + "type": "string" + }, + "idle_timeout_sec": { + "type": "integer" + }, + "max_connections": { + "description": "max concurrent client connections; 0 = default (2048)", + "type": "integer" + }, + "port": { + "type": "integer" + }, + "secret": { + "description": "legacy single secret; mirrors the first enabled entry in Secrets for backward compatibility", + "type": "string" + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/definitions/config.MTProtoSecret" + } + }, + "tcp_user_timeout_sec": { + "type": "integer" + }, + "upstream_mode": { + "type": "string" + }, + "ws_custom_domain": { + "type": "string" + }, + "ws_endpoint_host": { + "type": "string" + } + } + }, + "config.MTProtoSecret": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "config.MasqueradeConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "interfaces": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "config.QueueConfig": { + "type": "object", + "properties": { + "devices": { + "$ref": "#/definitions/config.DevicesConfig" + }, + "interfaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "ipv4": { + "type": "boolean" + }, + "ipv6": { + "type": "boolean" + }, + "mark": { + "description": "Main injected packets mark", + "type": "integer" + }, + "mode": { + "type": "string" + }, + "mss_clamp": { + "$ref": "#/definitions/config.MSSClampConfig" + }, + "start_num": { + "type": "integer" + }, + "tcp_conn_bytes_limit": { + "type": "integer" + }, + "threads": { + "type": "integer" + }, + "tun": { + "$ref": "#/definitions/config.TUNConfig" + }, + "udp_conn_bytes_limit": { + "type": "integer" + } + } + }, + "config.RSTProtectionConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "ttl_tolerance": { + "type": "integer" + } + } + }, + "config.RoutingConfig": { + "type": "object", + "properties": { + "block_action": { + "type": "string" + }, + "egress_interface": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "fwmark": { + "type": "integer" + }, + "ip_ttl_seconds": { + "type": "integer" + }, + "mode": { + "type": "string" + }, + "source_interfaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "table": { + "type": "integer" + }, + "upstream": { + "$ref": "#/definitions/config.UpstreamProxyConfig" + } + } + }, + "config.SNIMutationConfig": { + "type": "object", + "properties": { + "fake_ext_count": { + "type": "integer" + }, + "fake_snis": { + "description": "Additional SNIs to inject", + "type": "array", + "items": { + "type": "string" + } + }, + "grease_count": { + "type": "integer" + }, + "mode": { + "description": "\"off\", \"duplicate\", \"grease\", \"padding\", \"reorder\", \"full\"", + "type": "string" + }, + "padding_size": { + "type": "integer" + } + } + }, + "config.SetConfig": { + "type": "object", + "properties": { + "dns": { + "$ref": "#/definitions/config.DNSConfig" + }, + "enabled": { + "type": "boolean" + }, + "escalate": { + "$ref": "#/definitions/config.EscalateConfig" + }, + "faking": { + "$ref": "#/definitions/config.FakingConfig" + }, + "fragmentation": { + "$ref": "#/definitions/config.FragmentationConfig" + }, + "id": { + "type": "string" + }, + "mss_clamp": { + "$ref": "#/definitions/config.MSSClampConfig" + }, + "name": { + "type": "string" + }, + "routing": { + "$ref": "#/definitions/config.RoutingConfig" + }, + "targets": { + "$ref": "#/definitions/config.TargetsConfig" + }, + "tcp": { + "$ref": "#/definitions/config.TCPConfig" + }, + "udp": { + "$ref": "#/definitions/config.UDPConfig" + } + } + }, + "config.Socks5Config": { + "type": "object", + "properties": { + "bind_address": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "udp_read_timeout": { + "type": "integer" + }, + "udp_timeout": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "config.SystemConfig": { + "type": "object", + "properties": { + "ai": { + "$ref": "#/definitions/config.AIConfig" + }, + "api": { + "$ref": "#/definitions/config.ApiConfig" + }, + "checker": { + "$ref": "#/definitions/config.DiscoveryConfig" + }, + "geo": { + "$ref": "#/definitions/geodat.GeoDatConfig" + }, + "logging": { + "$ref": "#/definitions/config.Logging" + }, + "memory_limit": { + "type": "string" + }, + "mtproto": { + "$ref": "#/definitions/config.MTProtoConfig" + }, + "pprof": { + "type": "boolean" + }, + "socks5": { + "$ref": "#/definitions/config.Socks5Config" + }, + "tables": { + "$ref": "#/definitions/config.TablesConfig" + }, + "timezone": { + "type": "string" + }, + "web_server": { + "$ref": "#/definitions/config.WebServerConfig" + } + } + }, + "config.TCPConfig": { + "type": "object", + "properties": { + "conn_bytes_limit": { + "type": "integer" + }, + "desync": { + "$ref": "#/definitions/config.DesyncConfig" + }, + "dport_filter": { + "description": "comma separated list of ports and port ranges, e.g. \"80,443,5222\"", + "type": "string" + }, + "drop_sack": { + "type": "boolean" + }, + "duplicate": { + "$ref": "#/definitions/config.DuplicateConfig" + }, + "incoming": { + "$ref": "#/definitions/config.IncomingConfig" + }, + "ip_block_detect": { + "$ref": "#/definitions/config.IPBlockDetectConfig" + }, + "rst_protection": { + "$ref": "#/definitions/config.RSTProtectionConfig" + }, + "seg2delay": { + "type": "integer" + }, + "seg2delay_max": { + "type": "integer" + }, + "syn_fake": { + "type": "boolean" + }, + "syn_fake_len": { + "type": "integer" + }, + "syn_ttl": { + "type": "integer" + }, + "win": { + "$ref": "#/definitions/config.WinConfig" + } + } + }, + "config.TUNConfig": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "address_v6": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "out_gateway": { + "type": "string" + }, + "out_interface": { + "type": "string" + }, + "route_table": { + "type": "integer" + } + } + }, + "config.TablesConfig": { + "type": "object", + "properties": { + "engine": { + "type": "string" + }, + "masquerade": { + "$ref": "#/definitions/config.MasqueradeConfig" + }, + "monitor_interval": { + "type": "integer" + }, + "skip_setup": { + "type": "boolean" + } + } + }, + "config.TargetsConfig": { + "type": "object", + "properties": { + "domain_only": { + "type": "boolean" + }, + "geoip_categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "geosite_categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_version": { + "description": "\"4\", \"6\", or \"\" (match any)", + "type": "string" + }, + "sni_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "source_devices": { + "type": "array", + "items": { + "type": "string" + } + }, + "source_devices_exclude": { + "type": "boolean" + }, + "tls": { + "description": "\"1.2\", \"1.3\", or \"\" (match any)", + "type": "string" + } + } + }, + "config.UDPConfig": { + "type": "object", + "properties": { + "conn_bytes_limit": { + "type": "integer" + }, + "dport_filter": { + "description": "can be a comma separated list of ports and port ranges, e.g. \"80,443,1000-2000\"", + "type": "string" + }, + "fake_len": { + "type": "integer" + }, + "fake_payload_file": { + "description": "\"\" = zero fill, \"@quic_initial\" = generate fresh QUIC Initial per packet, \"@preset:quic1\"/\"@preset:quic2\" = bundled presets, otherwise capture filename relative to config dir (e.g. \"captures/quic_youtube_com.bin\")", + "type": "string" + }, + "fake_seq_length": { + "type": "integer" + }, + "faking_strategy": { + "type": "string" + }, + "filter_quic": { + "type": "string" + }, + "filter_stun": { + "type": "boolean" + }, + "mode": { + "type": "string" + }, + "seg2delay": { + "type": "integer" + }, + "seg2delay_max": { + "type": "integer" + } + } + }, + "config.UpstreamProxyConfig": { + "type": "object", + "properties": { + "fail_open": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "udp": { + "type": "boolean" + }, + "use_domain": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "config.WatchdogConfig": { + "type": "object", + "properties": { + "cooldown_sec": { + "type": "integer" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "failure_interval": { + "type": "integer" + }, + "interval_sec": { + "type": "integer" + }, + "max_retries": { + "type": "integer" + }, + "timeout_sec": { + "type": "integer" + } + } + }, + "config.WebServerConfig": { + "type": "object", + "properties": { + "bind_address": { + "type": "string" + }, + "language": { + "type": "string" + }, + "password": { + "type": "string" + }, + "password_set": { + "type": "boolean" + }, + "port": { + "type": "integer" + }, + "tls_cert": { + "type": "string" + }, + "tls_key": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "config.WinConfig": { + "type": "object", + "properties": { + "mode": { + "description": "\"off\", \"oscillate\", \"zero\", \"random\", \"escalate\"", + "type": "string" + }, + "values": { + "description": "Custom window values", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "geodat.GeoAutoUpdateConfig": { + "type": "object", + "properties": { + "interval": { + "type": "string" + }, + "last_run": { + "type": "string" + }, + "on_startup": { + "type": "boolean" + } + } + }, + "geodat.GeoDatConfig": { + "type": "object", + "properties": { + "auto_update": { + "$ref": "#/definitions/geodat.GeoAutoUpdateConfig" + }, + "ipdat_path": { + "type": "string" + }, + "ipdat_url": { + "type": "string" + }, + "sitedat_path": { + "type": "string" + }, + "sitedat_url": { + "type": "string" + } + } + }, + "handler.AddDomainRequest": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "set_id": { + "type": "string" + }, + "set_name": { + "type": "string" + } + } + }, + "handler.AddDomainResponse": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "manual_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "total_domains": { + "type": "integer" + } + } + }, + "handler.AddGeoIpRequest": { + "type": "object", + "properties": { + "cidr": { + "type": "array", + "items": { + "type": "string" + } + }, + "set_id": { + "type": "string" + }, + "set_name": { + "type": "string" + } + } + }, + "handler.AddIpResponse": { + "type": "object", + "properties": { + "manual_cidrs": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "total_cidrs": { + "type": "integer" + } + } + }, + "handler.CaptureRequest": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "protocol": { + "description": "\"tls\", \"quic\", or \"both\"", + "type": "string" + } + } + }, + "handler.ConfigResponse": { + "type": "object", + "properties": { + "available_ifaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + }, + "queue": { + "$ref": "#/definitions/config.QueueConfig" + }, + "sets": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SetWithStats" + } + }, + "success": { + "type": "boolean" + }, + "system": { + "$ref": "#/definitions/config.SystemConfig" + }, + "version": { + "type": "integer" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handler.DetectorRequest": { + "type": "object", + "properties": { + "tests": { + "description": "\"dns\", \"dns-availability\", \"domains\", \"tcp\", \"sni\", \"telegram\"", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handler.DetectorResponse": { + "type": "object", + "properties": { + "estimated_tests": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "tests": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handler.DeviceInfo": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "is_manual": { + "type": "boolean" + }, + "is_online": { + "type": "boolean" + }, + "is_private": { + "type": "boolean" + }, + "mac": { + "type": "string" + }, + "mss_clamp": { + "type": "integer" + }, + "vendor": { + "type": "string" + } + } + }, + "handler.DevicesResponse": { + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "devices": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DeviceInfo" + } + }, + "router_ips": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + } + } + }, + "handler.DiagB4": { + "type": "object", + "properties": { + "build_date": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "config_path": { + "type": "string" + }, + "memory_mb": { + "type": "string" + }, + "pid": { + "type": "integer" + }, + "running": { + "type": "boolean" + }, + "service_manager": { + "type": "string" + }, + "uptime": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "handler.DiagCapability": { + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "detail": { + "type": "string" + }, + "missing": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "packages": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handler.DiagEngine": { + "type": "object", + "properties": { + "mode": { + "type": "string" + }, + "tun": { + "$ref": "#/definitions/handler.DiagTUN" + } + } + }, + "handler.DiagFirewall": { + "type": "object", + "properties": { + "backend": { + "type": "string" + }, + "flow_offload": { + "type": "string" + }, + "nfqueue_works": { + "type": "boolean" + }, + "rule_groups": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagRuleGroup" + } + } + } + }, + "handler.DiagGeodata": { + "type": "object", + "properties": { + "geoip_configured": { + "type": "boolean" + }, + "geoip_path": { + "type": "string" + }, + "geoip_size": { + "type": "string" + }, + "geosite_configured": { + "type": "boolean" + }, + "geosite_path": { + "type": "string" + }, + "geosite_size": { + "type": "string" + }, + "total_domains": { + "type": "integer" + }, + "total_ips": { + "type": "integer" + } + } + }, + "handler.DiagInterface": { + "type": "object", + "properties": { + "addrs": { + "type": "array", + "items": { + "type": "string" + } + }, + "mac": { + "type": "string" + }, + "mtu": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "up": { + "type": "boolean" + } + } + }, + "handler.DiagKernel": { + "type": "object", + "properties": { + "capabilities": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagCapability" + } + }, + "modules": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagModule" + } + } + } + }, + "handler.DiagModule": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handler.DiagMount": { + "type": "object", + "properties": { + "available": { + "type": "string" + }, + "path": { + "type": "string" + }, + "writable": { + "type": "boolean" + } + } + }, + "handler.DiagNetwork": { + "type": "object", + "properties": { + "interfaces": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagInterface" + } + } + } + }, + "handler.DiagPaths": { + "type": "object", + "properties": { + "binary": { + "type": "string" + }, + "config": { + "type": "string" + }, + "data_dir": { + "type": "string" + }, + "error_log": { + "type": "string" + }, + "geoip": { + "type": "string" + }, + "geosite": { + "type": "string" + } + } + }, + "handler.DiagRuleGroup": { + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": "string" + } + } + }, + "handler.DiagSystem": { + "type": "object", + "properties": { + "arch": { + "type": "string" + }, + "cpu_cores": { + "type": "integer" + }, + "distro": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "is_docker": { + "type": "boolean" + }, + "kernel": { + "type": "string" + }, + "mem_avail_mb": { + "type": "integer" + }, + "mem_total_mb": { + "type": "integer" + }, + "os": { + "type": "string" + } + } + }, + "handler.DiagTUN": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "address_v6": { + "type": "string" + }, + "capture": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_up": { + "type": "boolean" + }, + "forward_errors": { + "type": "integer" + }, + "ipv6_dropped": { + "type": "integer" + }, + "mtu": { + "type": "integer" + }, + "out_gateway": { + "type": "string" + }, + "out_interface": { + "type": "string" + }, + "packets_forwarded": { + "type": "integer" + }, + "reply_capture": { + "type": "boolean" + }, + "resolved_src": { + "type": "string" + }, + "route_table": { + "type": "integer" + }, + "running": { + "type": "boolean" + } + } + }, + "handler.DiagTool": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "found": { + "type": "boolean" + }, + "name": { + "type": "string" + } + } + }, + "handler.DiagTools": { + "type": "object", + "properties": { + "firewall": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagTool" + } + }, + "optional": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagTool" + } + }, + "required": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagTool" + } + } + } + }, + "handler.Diagnostics": { + "type": "object", + "properties": { + "b4": { + "$ref": "#/definitions/handler.DiagB4" + }, + "engine": { + "$ref": "#/definitions/handler.DiagEngine" + }, + "firewall": { + "$ref": "#/definitions/handler.DiagFirewall" + }, + "geodata": { + "$ref": "#/definitions/handler.DiagGeodata" + }, + "kernel": { + "$ref": "#/definitions/handler.DiagKernel" + }, + "network": { + "$ref": "#/definitions/handler.DiagNetwork" + }, + "paths": { + "$ref": "#/definitions/handler.DiagPaths" + }, + "storage": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagMount" + } + }, + "system": { + "$ref": "#/definitions/handler.DiagSystem" + }, + "tools": { + "$ref": "#/definitions/handler.DiagTools" + } + } + }, + "handler.DiagnosticsResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.Diagnostics" + }, + "success": { + "type": "boolean" + } + } + }, + "handler.DiscoveryRequest": { + "type": "object", + "properties": { + "check_url": { + "type": "string" + }, + "check_urls": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_version": { + "description": "\"auto\", \"ipv4\", \"ipv6\"", + "type": "string" + }, + "payload_files": { + "type": "array", + "items": { + "type": "string" + } + }, + "skip_cache": { + "type": "boolean" + }, + "skip_dns": { + "type": "boolean" + }, + "tls_version": { + "description": "\"auto\", \"tls12\", \"tls13\"", + "type": "string" + }, + "validation_tries": { + "type": "integer" + } + } + }, + "handler.DiscoveryResponse": { + "type": "object", + "properties": { + "check_url": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "estimated_tests": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "handler.GeodatDownloadRequest": { + "type": "object", + "properties": { + "destination_path": { + "type": "string" + }, + "geoip_url": { + "type": "string" + }, + "geosite_url": { + "type": "string" + } + } + }, + "handler.GeodatDownloadResponse": { + "type": "object", + "properties": { + "geoip_path": { + "type": "string" + }, + "geoip_size": { + "type": "integer" + }, + "geosite_path": { + "type": "string" + }, + "geosite_size": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "handler.GeodatSource": { + "type": "object", + "properties": { + "geoip_url": { + "type": "string" + }, + "geosite_url": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "handler.GeoipResponse": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handler.GeositeResponse": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handler.PublicDNSServer": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "country_id": { + "type": "string" + }, + "dnssec": { + "type": "boolean" + }, + "ip": { + "type": "string" + }, + "name": { + "type": "string" + }, + "reliability": { + "type": "number" + } + } + }, + "handler.RestartResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "restart_command": { + "type": "string" + }, + "service_manager": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "handler.SetStatistics": { + "type": "object", + "properties": { + "geoip_category_breakdown": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "geoip_ips": { + "type": "integer" + }, + "geosite_category_breakdown": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "geosite_domains": { + "type": "integer" + }, + "manual_domains": { + "type": "integer" + }, + "manual_ips": { + "type": "integer" + }, + "total_domains": { + "type": "integer" + }, + "total_ips": { + "type": "integer" + } + } + }, + "handler.SetWithStats": { + "type": "object", + "properties": { + "dns": { + "$ref": "#/definitions/config.DNSConfig" + }, + "enabled": { + "type": "boolean" + }, + "escalate": { + "$ref": "#/definitions/config.EscalateConfig" + }, + "faking": { + "$ref": "#/definitions/config.FakingConfig" + }, + "fragmentation": { + "$ref": "#/definitions/config.FragmentationConfig" + }, + "id": { + "type": "string" + }, + "mss_clamp": { + "$ref": "#/definitions/config.MSSClampConfig" + }, + "name": { + "type": "string" + }, + "routing": { + "$ref": "#/definitions/config.RoutingConfig" + }, + "stats": { + "$ref": "#/definitions/handler.SetStatistics" + }, + "targets": { + "$ref": "#/definitions/config.TargetsConfig" + }, + "tcp": { + "$ref": "#/definitions/config.TCPConfig" + }, + "udp": { + "$ref": "#/definitions/config.UDPConfig" + } + } + }, + "handler.SystemInfo": { + "type": "object", + "properties": { + "arch": { + "type": "string" + }, + "can_restart": { + "type": "boolean" + }, + "is_docker": { + "type": "boolean" + }, + "os": { + "type": "string" + }, + "service_manager": { + "type": "string" + } + } + }, + "handler.TraceStartRequest": { + "type": "object", + "properties": { + "note": { + "type": "string" + } + } + }, + "handler.TraceStatusResponse": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "downloadName": { + "type": "string" + }, + "downloadReady": { + "type": "boolean" + }, + "level": { + "type": "string" + }, + "lines": { + "type": "integer" + }, + "maxSeconds": { + "type": "integer" + }, + "note": { + "type": "string" + }, + "startedAt": { + "type": "string" + } + } + }, + "handler.UpdateRequest": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + } + }, + "handler.UpdateResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "service_manager": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "update_command": { + "type": "string" + } + } + }, + "handler.VendorInfo": { + "type": "object", + "properties": { + "company": { + "type": "string" + } + } + }, + "handler.VersionInfo": { + "type": "object", + "properties": { + "build_date": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "handler.WatchdogActionResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "added example.com to watchdog" + }, + "success": { + "type": "boolean", + "example": true + } + } + }, + "handler.WatchdogDomainRequest": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "example": "example.com" + } + } + }, + "handler.aiChatRequest": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.Message" + } + }, + "system": { + "type": "string" + } + } + }, + "handler.aiExplainRequest": { + "type": "object", + "properties": { + "context_json": { + "type": "string" + }, + "field_doc": { + "type": "string" + }, + "field_label": { + "type": "string" + }, + "language": { + "type": "string" + }, + "question": { + "type": "string" + }, + "topic": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "handler.aiSecretBody": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "ref": { + "type": "string" + } + } + }, + "handler.aiStatusResponse": { + "type": "object", + "properties": { + "api_key_ref": { + "type": "string" + }, + "available_providers": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "has_key": { + "type": "boolean" + }, + "model": { + "type": "string" + }, + "not_ready_reason": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "ready": { + "type": "boolean" + } + } + }, + "log.Level": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "LevelError", + "LevelInfo", + "LevelTrace", + "LevelDebug" + ] + }, + "watchdog.DomainStatus": { + "type": "object", + "properties": { + "consecutive_failures": { + "type": "integer" + }, + "cooldown_until": { + "type": "string" + }, + "display_domain": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "interval_sec": { + "type": "integer" + }, + "last_check": { + "type": "string" + }, + "last_error": { + "type": "string" + }, + "last_failure": { + "type": "string" + }, + "last_heal": { + "type": "string" + }, + "last_speed": { + "type": "number" + }, + "matched_set": { + "type": "string" + }, + "matched_set_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "watchdog.WatchdogState": { + "type": "object", + "properties": { + "domains": { + "type": "array", + "items": { + "$ref": "#/definitions/watchdog.DomainStatus" + } + }, + "enabled": { + "type": "boolean" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Enter \"Bearer {token}\" to authorize", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/static/swagger.json b/docs/static/swagger.json index cfa67e9b..4f9e7e50 100644 --- a/docs/static/swagger.json +++ b/docs/static/swagger.json @@ -4,7 +4,7 @@ "description": "B4 network packet processor REST API", "title": "B4 API", "contact": {}, - "version": "1.71.0" + "version": "1.73.0" }, "basePath": "/api", "paths": { @@ -2230,6 +2230,34 @@ } } }, + "/mtproto/active-clients": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Aggregated per-secret view: active connection count, the distinct client IPs currently using each secret, and last activity.", + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "List active MTProto clients per secret", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, "/mtproto/config": { "get": { "security": [ @@ -2355,6 +2383,34 @@ } } }, + "/mtproto/sessions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "One entry per live client connection, including client IP/port, upstream destination and activity timestamps.", + "produces": [ + "application/json" + ], + "tags": [ + "MTProto" + ], + "summary": "List active MTProto sessions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, "/mtproto/test-ws": { "post": { "security": [ @@ -3877,6 +3933,9 @@ "fake_sni": { "type": "string" }, + "idle_timeout_sec": { + "type": "integer" + }, "max_connections": { "description": "max concurrent client connections; 0 = default (2048)", "type": "integer" @@ -3885,8 +3944,18 @@ "type": "integer" }, "secret": { + "description": "legacy single secret; mirrors the first enabled entry in Secrets for backward compatibility", "type": "string" }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/definitions/config.MTProtoSecret" + } + }, + "tcp_user_timeout_sec": { + "type": "integer" + }, "upstream_mode": { "type": "string" }, @@ -3898,6 +3967,23 @@ } } }, + "config.MTProtoSecret": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, "config.MasqueradeConfig": { "type": "object", "properties": { @@ -4227,6 +4313,9 @@ "config.TargetsConfig": { "type": "object", "properties": { + "domain_only": { + "type": "boolean" + }, "geoip_categories": { "type": "array", "items": { @@ -4261,6 +4350,9 @@ "type": "string" } }, + "source_devices_exclude": { + "type": "boolean" + }, "tls": { "description": "\"1.2\", \"1.3\", or \"\" (match any)", "type": "string" @@ -4612,12 +4704,18 @@ "is_manual": { "type": "boolean" }, + "is_online": { + "type": "boolean" + }, "is_private": { "type": "boolean" }, "mac": { "type": "string" }, + "mss_clamp": { + "type": "integer" + }, "vendor": { "type": "string" } @@ -4678,6 +4776,32 @@ } } }, + "handler.DiagCapability": { + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "detail": { + "type": "string" + }, + "missing": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "packages": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "handler.DiagEngine": { "type": "object", "properties": { @@ -4764,6 +4888,12 @@ "handler.DiagKernel": { "type": "object", "properties": { + "capabilities": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.DiagCapability" + } + }, "modules": { "type": "array", "items": { diff --git a/src/config/migration.go b/src/config/migration.go index 3a1d424f..bd5df813 100644 --- a/src/config/migration.go +++ b/src/config/migration.go @@ -8,89 +8,41 @@ import ( "path/filepath" "strings" - "github.com/daniellavrushin/b4/geodat" "github.com/daniellavrushin/b4/log" "github.com/google/uuid" ) type MigrationFunc func(*Config, map[string]interface{}) error -var ( - CurrentConfigVersion = len(migrationRegistry) +const ( + CurrentConfigVersion = 51 MinSupportedVersion = 0 ) var migrationRegistry = map[int]MigrationFunc{ - 0: migrateV0to1, // Add enabled field to sets - 1: migrateV1to2, - 2: migrateV2to3, - 3: migrateV3to4, - 4: migrateV4to5, - 5: migrateV5to6, - 6: migrateV6to7, // Add TCP syn TTL and drop SACK settings - 7: migrateV7to8, // Add DNS redirect settings - 8: migrateV8to9, - 9: migrateV9to10, - 10: migrateV10to11, - 11: migrateV11to12, - 12: migrateV12to13, - 13: migrateV13to14, - 14: migrateV14to15, // Flatten TCP desync settings into nested struct - 15: migrateV15to16, // Add TCP Incoming config - 16: migrateV16to17, - 17: migrateV17to18, // Add TCP packet duplication config - 18: migrateV18to19, // Add TLS certificate/key to web server config - 19: migrateV19to20, // Add vendor lookup option to devices config - 20: migrateV20to21, // Add SOCKS5 proxy server config - 21: migrateV21to22, // Add NAT masquerade config - 22: migrateV22to23, // Add TCP MSS clamping config - 23: migrateV23to24, // Add multidisorder (fake per segment) and new payload types - 24: migrateV24to25, // Remove main set, move ConnBytesLimit to queue config - 25: migrateV25to26, // Add TLS version filter to targets - 26: migrateV26to27, // Add tables engine config - 27: migrateV27to28, // Add per-set routing config - 28: migrateV28to29, // Add position ranges and strategy pool - 29: migrateV29to30, // Add MTProto proxy config - 30: migrateV30to31, // Add TCP IP block detection config - 31: migrateV31to32, // Add watchdog config - 32: migrateV32to33, // Add TCP RST protection config - 33: migrateV33to34, // Add manual devices to device config - 34: migrateV34to35, // Add routing mode and upstream proxy config - 35: migrateV35to36, // Add fragmentation seq overlap length - 36: migrateV36to37, // Add UDP fake_payload_file field - 37: migrateV37to38, // Add MTProto upstream transport (WS) fields - 38: migrateV38to39, // Add system.memory_limit - 39: migrateV39to40, // Add geo auto_update config - 40: migrateV40to41, // Add MTProto CF-proxy fallback config - 41: migrateV41to42, // Add MTProto CF Worker domain config - 42: migrateV42to43, // Add per-set routing block action - 43: migrateV43to44, // Add MTProto DC-list fallback source config - 44: migrateV44to45, // Add per-set DNS-over-HTTPS redirect target - 45: migrateV45to46, // Replace logging.error_file with logging.directory - 46: migrateV46to47, // Drop the hardcoded legacy MTProto WS endpoint host (empty now falls back to it) - 47: migrateV47to48, // Add TUN engine mode and config - 48: migrateV48to49, // Convert masquerade to nested config (multi-interface) - 49: migrateV49to50, // Move the single MTProto secret into the secrets list - 50: migrateV50to51, // Add per-set domain-only matching target + 14: migrateV14to15, + 24: migrateV24to25, + 33: migrateV33to34, + 45: migrateV45to46, + 46: migrateV46to47, + 48: migrateV48to49, + 49: migrateV49to50, } -func migrateV50to51(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v50->v51: Adding per-set domain-only matching target") - for _, set := range c.Sets { - set.Targets.DomainOnly = false - set.Targets.SourceDevicesExclude = false - } - return nil -} - -func migrateV49to50(c *Config, _ map[string]interface{}) error { +func migrateV49to50(c *Config, raw map[string]interface{}) error { log.Tracef("Migration v49->v50: Moving single MTProto secret into secrets list") m := &c.System.MTProto - if len(m.Secrets) == 0 && strings.TrimSpace(m.Secret) != "" { + if len(m.Secrets) > 0 { + return nil + } + system, _ := raw["system"].(map[string]interface{}) + mt, _ := system["mtproto"].(map[string]interface{}) + legacy, _ := mt["secret"].(string) + if strings.TrimSpace(legacy) != "" { m.Secrets = []MTProtoSecret{{ ID: uuid.NewString(), Name: "default", - Secret: m.Secret, + Secret: strings.TrimSpace(legacy), Enabled: true, }} } @@ -107,13 +59,6 @@ func migrateV48to49(c *Config, raw map[string]interface{}) error { return nil } -func migrateV47to48(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v47->v48: Adding TUN engine mode and config") - c.Queue.Mode = DefaultConfig.Queue.Mode - c.Queue.TUN = DefaultConfig.Queue.TUN - return nil -} - func migrateV46to47(c *Config, _ map[string]interface{}) error { log.Tracef("Migration v46->v47: Dropping hardcoded legacy MTProto WS endpoint host (empty falls back to the same edge)") if c.System.MTProto.WSEndpointHost == TGWSEndpointHost { @@ -143,93 +88,6 @@ func migrateV45to46(c *Config, raw map[string]interface{}) error { return nil } -func migrateV44to45(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v44->v45: Adding per-set DNS-over-HTTPS redirect target") - for _, set := range c.Sets { - set.DNS.DoHURL = DefaultSetConfig.DNS.DoHURL - } - return nil -} - -func migrateV43to44(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v43->v44: Adding MTProto DC-list fallback source config") - c.System.MTProto.DCFallbackEnabled = true - c.System.MTProto.DCFallbackURL = DefaultConfig.System.MTProto.DCFallbackURL - return nil -} - -func migrateV42to43(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v42->v43: Adding per-set routing block action") - for _, set := range c.Sets { - if set.Routing.BlockAction == "" { - set.Routing.BlockAction = DefaultSetConfig.Routing.BlockAction - } - } - return nil -} - -func migrateV41to42(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v41->v42: Adding MTProto CF Worker domain config") - c.System.MTProto.CFWorkerDomain = "" - return nil -} - -func migrateV40to41(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v40->v41: Adding MTProto CF-proxy fallback config") - c.System.MTProto.CFProxyEnabled = true - c.System.MTProto.CFProxyURL = DefaultConfig.System.MTProto.CFProxyURL - return nil -} - -func migrateV39to40(c *Config, _ map[string]interface{}) error { - c.System.Geo.AutoUpdate = geodat.GeoAutoUpdateConfig{} - for _, set := range c.Sets { - set.MSSClamp = MSSClampConfig{} - } - return nil -} - -func migrateV38to39(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v38->v39: Adding system.memory_limit") - c.System.MemoryLimit = DefaultConfig.System.MemoryLimit - return nil -} - -func migrateV37to38(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v37->v38: Adding MTProto upstream transport (WS) fields") - c.System.MTProto.UpstreamMode = "auto" - c.System.MTProto.WSCustomDomain = "" - c.System.MTProto.WSEndpointHost = "149.154.167.220" - return nil -} - -func migrateV36to37(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v36->v37: Adding UDP fake_payload_file field") - for _, set := range c.Sets { - set.UDP.FakePayloadFile = DefaultSetConfig.UDP.FakePayloadFile - } - return nil -} - -func migrateV35to36(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v35->v36: Adding fragmentation seq overlap length") - for _, set := range c.Sets { - set.Fragmentation.SeqOverlapLength = DefaultSetConfig.Fragmentation.SeqOverlapLength - } - return nil -} - -func migrateV34to35(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v34->v35: Adding routing mode and upstream proxy config") - for _, set := range c.Sets { - if set.Routing.Mode == "" { - set.Routing.Mode = RoutingModeInterface - } - set.Routing.Upstream.UseDomain = true - } - return nil -} - func migrateV33to34(c *Config, raw map[string]interface{}) error { log.Tracef("Migration v33->v34: Unifying device model") @@ -353,74 +211,6 @@ func loadAliasesForMigration(configPath string) map[string]string { return aliases } -func migrateV32to33(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v32->v33: Adding TCP RST protection config") - for _, set := range c.Sets { - set.TCP.RSTProtection = DefaultSetConfig.TCP.RSTProtection - } - return nil -} - -func migrateV31to32(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v31->v32: Adding watchdog config") - c.System.Checker.Watchdog = DefaultConfig.System.Checker.Watchdog - return nil -} - -func migrateV30to31(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v30->v31: Adding TCP IP block detection config") - for _, set := range c.Sets { - set.TCP.IPBlockDetect = DefaultSetConfig.TCP.IPBlockDetect - } - return nil -} - -func migrateV29to30(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v29->v30: Adding MTProto proxy config") - c.System.MTProto = DefaultConfig.System.MTProto - return nil -} - -func migrateV28to29(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v28->v29: Adding position ranges and strategy pool") - for _, set := range c.Sets { - if set.Fragmentation.StrategyPool == nil { - set.Fragmentation.StrategyPool = []string{} - } - } - return nil -} - -func migrateV27to28(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v27->v28: Adding per-set routing config") - for _, set := range c.Sets { - if set.Routing.SourceInterfaces == nil { - set.Routing.SourceInterfaces = []string{} - } - if set.Routing.IPTTLSeconds <= 0 { - set.Routing.IPTTLSeconds = DefaultSetConfig.Routing.IPTTLSeconds - } - } - return nil -} - -// Migration: v26 -> v27 (add tables engine config) -func migrateV26to27(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v26->v27: Adding tables engine config") - c.System.Timezone = DefaultConfig.System.Timezone - c.System.Tables.Engine = DefaultConfig.System.Tables.Engine - return nil -} - -// Migration: v25 -> v26 (add TLS version filter to targets) -func migrateV25to26(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v25->v26: Adding TLS version filter to targets") - for _, set := range c.Sets { - set.Targets.TLSVersion = "" - } - return nil -} - // Migration: v24 -> v25 (remove main set, move ConnBytesLimit to queue config) func migrateV24to25(c *Config, _ map[string]interface{}) error { log.Tracef("Migration v24->v25: Removing main set, moving ConnBytesLimit to queue config") @@ -448,73 +238,6 @@ func migrateV24to25(c *Config, _ map[string]interface{}) error { return nil } -func migrateV23to24(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v23->v24: Adding multidisorder (fake per segment) to combo/disorder configs") - for _, set := range c.Sets { - set.Fragmentation.Combo.FakePerSegment = DefaultSetConfig.Fragmentation.Combo.FakePerSegment - set.Fragmentation.Combo.FakePerSegCount = DefaultSetConfig.Fragmentation.Combo.FakePerSegCount - set.Fragmentation.Disorder.FakePerSegment = DefaultSetConfig.Fragmentation.Disorder.FakePerSegment - set.Fragmentation.Disorder.FakePerSegCount = DefaultSetConfig.Fragmentation.Disorder.FakePerSegCount - } - return nil -} - -func migrateV22to23(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v22->v23: Adding queue-level MSS clamping config") - c.Queue.MSSClamp = DefaultConfig.Queue.MSSClamp - return nil -} - -func migrateV21to22(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v21->v22: Adding NAT masquerade config") - c.System.Tables.Masquerade = DefaultConfig.System.Tables.Masquerade - return nil -} - -func migrateV20to21(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v20->v21: Adding SOCKS5 proxy server config") - c.System.Socks5 = DefaultConfig.System.Socks5 - return nil -} - -func migrateV19to20(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v19->v20: Adding vendor lookup option to devices config") - c.Queue.Devices.VendorLookup = false - return nil -} - -func migrateV18to19(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v18->v19: Adding TLS certificate/key fields to web server config") - // TLS fields default to empty strings (TLS disabled), no action needed - return nil -} - -func migrateV17to18(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v17->v18: Adding TCP packet duplication config") - - for _, set := range c.Sets { - set.TCP.Duplicate = DefaultSetConfig.TCP.Duplicate - } - return nil -} - -func migrateV16to17(c *Config, _ map[string]interface{}) error { - - for _, set := range c.Sets { - set.Faking.TimestampDecrease = DefaultSetConfig.Faking.TimestampDecrease - } - return nil -} - -func migrateV15to16(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v15->v16: Adding TCP Incoming config") - - for _, set := range c.Sets { - set.TCP.Incoming = DefaultSetConfig.TCP.Incoming - } - return nil -} - func migrateV14to15(c *Config, raw map[string]interface{}) error { sets, _ := raw["sets"].([]interface{}) @@ -558,158 +281,6 @@ func migrateV14to15(c *Config, raw map[string]interface{}) error { return nil } -// Migration: v13 -> v14 (add fragmentation strategy field) -func migrateV13to14(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v13->v14: Adding fragmentation strategy field") - - c.System.WebServer.BindAddress = DefaultConfig.System.WebServer.BindAddress - for _, set := range c.Sets { - set.TCP.Desync.PostDesync = DefaultSetConfig.TCP.Desync.PostDesync - set.Fragmentation.Combo.DecoyEnabled = DefaultSetConfig.Fragmentation.Combo.DecoyEnabled - } - return nil -} - -// Migration: v12 -> v13 (add payload file/data to faking config) -func migrateV12to13(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v12->v13: Adding TLSMod to faking config") - - for _, set := range c.Sets { - set.Faking.TLSMod = DefaultSetConfig.Faking.TLSMod - } - return nil -} - -// Migration: v11 -> v12 (add TCP FilterSYN setting) -func migrateV11to12(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v11->v12: Adding TCP FilterSYN setting") - - for _, set := range c.Sets { - set.Faking.PayloadFile = DefaultSetConfig.Faking.PayloadFile - set.Faking.PayloadData = DefaultSetConfig.Faking.PayloadData - - set.Fragmentation.SeqOverlapPattern = DefaultSetConfig.Fragmentation.SeqOverlapPattern - set.Fragmentation.SeqOverlapBytes = DefaultSetConfig.Fragmentation.SeqOverlapBytes - } - return nil -} - -// Migration: v10 -> v11 (add devices config to queue) -func migrateV10to11(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v10->v11: Adding devices config to queue") - - c.Queue.Devices = DefaultConfig.Queue.Devices - return nil -} - -// Migration: v9 -> v10 (add error log file setting) -func migrateV9to10(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v9->v10: Adding error log file setting") - - c.Queue.Interfaces = []string{} - return nil - -} - -// Migration: v8 -> v9 -func migrateV8to9(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v8->v9: No changes, placeholder migration") - c.System.Logging.Directory = DefaultConfig.System.Logging.Directory - return nil -} - -// Migration: v7 -> v8 (add DNS redirect settings) -func migrateV7to8(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v7->v8: Adding DNS redirect settings") - - for _, set := range c.Sets { - set.DNS = DefaultSetConfig.DNS - } - c.System.Checker.ReferenceDNS = DefaultConfig.System.Checker.ReferenceDNS - return nil -} - -// Migration: v6 -> v7 (add TCP syn TTL and drop SACK settings) -func migrateV6to7(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v6->v7: Adding TCP syn TTL and drop SACK settings") - - for _, set := range c.Sets { - set.TCP.SynTTL = DefaultSetConfig.TCP.SynTTL - } - return nil -} - -// Migration: v5 -> v6 (add reference domain to discovery config) -func migrateV5to6(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v5->v6: Initializing missing fields with default values") - - for _, set := range c.Sets { - set.Fragmentation.Combo = DefaultSetConfig.Fragmentation.Combo - set.Fragmentation.Disorder = DefaultSetConfig.Fragmentation.Disorder - // set.Fragmentation.Overlap = DefaultSetConfig.Fragmentation.Overlap - } - return nil -} - -// Migration: v4 -> v5 (add reference domain to discovery config) -func migrateV4to5(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v4->v5: Adding reference domain to discovery config") - - c.System.Checker.ReferenceDomain = "max.ru" - - return nil -} - -// Migration: v0 -> v1 (add enabled field to sets) -func migrateV0to1(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v0->v1: Adding 'enabled' field to all sets") - - for _, set := range c.Sets { - set.Enabled = true - } - - return nil -} - -func migrateV1to2(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v1->v2: Renaming sni_reverse to reverse_order") - - for _, set := range c.Sets { - set.Fragmentation.ReverseOrder = DefaultSetConfig.Fragmentation.ReverseOrder - set.Fragmentation.OOBChar = DefaultSetConfig.Fragmentation.OOBChar - set.Fragmentation.OOBPosition = DefaultSetConfig.Fragmentation.OOBPosition - set.Fragmentation.TLSRecordPosition = DefaultSetConfig.Fragmentation.TLSRecordPosition - } - - return nil -} - -func migrateV2to3(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v2->v3: Adding TCP desync/window settings and SNI mutation") - - for _, set := range c.Sets { - // TCP desync settings - set.TCP.Desync = DefaultSetConfig.TCP.Desync - - // TCP window manipulation - set.TCP.Win = DefaultSetConfig.TCP.Win - - // SNI mutation - set.Faking.SNIMutation = DefaultSetConfig.Faking.SNIMutation - } - - return nil -} - -func migrateV3to4(c *Config, _ map[string]interface{}) error { - log.Tracef("Migration v3->v4: Initializing missing fields with default values") - - c.System.Checker.ConfigPropagateMs = DefaultConfig.System.Checker.ConfigPropagateMs - c.System.Checker.DiscoveryTimeoutSec = DefaultConfig.System.Checker.DiscoveryTimeoutSec - - return nil -} - // discoverConfigPath checks well-known locations for a config file. func discoverConfigPath() string { candidates := []string{ @@ -794,9 +365,13 @@ func (c *Config) LoadWithMigration(path string) (bool, error) { migrated = true log.Infof("Config version %d is older than current version %d, migrating", c.Version, CurrentConfigVersion) + backupConfigBeforeMigration(path, data, c.Version) if err := c.applyMigrations(c.Version, rawJSON); err != nil { return false, err } + } else if c.Version > CurrentConfigVersion { + log.Warnf("Config version %d is newer than the version %d supported by this binary; unknown settings will be dropped on the next save", + c.Version, CurrentConfigVersion) } c.System.Geo.SanitizePaths(filepath.Dir(c.ConfigPath)) @@ -808,6 +383,15 @@ func (c *Config) LoadWithMigration(path string) (bool, error) { return migrated, nil } +func backupConfigBeforeMigration(path string, data []byte, fromVersion int) { + backupPath := fmt.Sprintf("%s.v%d.bak", path, fromVersion) + if err := os.WriteFile(backupPath, data, 0644); err != nil { + log.Warnf("Failed to back up config to %s before migration: %v", backupPath, err) + return + } + log.Infof("Backed up pre-migration config to %s", backupPath) +} + func (c *Config) migratePasswordHash() bool { p := c.System.WebServer.Password if p == "" || IsHashedPassword(p) { @@ -827,7 +411,8 @@ func (c *Config) applyMigrations(startVersion int, rawJSON map[string]interface{ for v := startVersion; v < CurrentConfigVersion; v++ { migrationFunc, exists := migrationRegistry[v] if !exists { - return fmt.Errorf("no migration path from version %d to %d", v, v+1) + c.Version = v + 1 + continue } log.Infof("Applying migration: v%d -> v%d", v, v+1) diff --git a/src/config/migration_test.go b/src/config/migration_test.go index 0c4705b1..b7df384f 100644 --- a/src/config/migration_test.go +++ b/src/config/migration_test.go @@ -91,6 +91,15 @@ func TestLoadWithMigration(t *testing.T) { if len(cfg.Sets) > 0 && !cfg.Sets[0].Enabled { t.Error("migration should set Enabled=true") } + + backupPath := path + ".v0.bak" + backup, err := os.ReadFile(backupPath) + if err != nil { + t.Fatalf("expected pre-migration backup at %s: %v", backupPath, err) + } + if string(backup) != v0Json { + t.Error("backup should contain the original pre-migration config") + } }) t.Run("current version skips migration", func(t *testing.T) { @@ -145,17 +154,14 @@ func TestDiscoverConfigPath(t *testing.T) { } func TestApplyMigrations(t *testing.T) { - t.Run("v0 to v1 sets enabled", func(t *testing.T) { + t.Run("versions without a registered migration are skipped", func(t *testing.T) { cfg := NewConfig() - set := NewSetConfig() - set.Enabled = false - cfg.Sets = []*SetConfig{&set} if err := cfg.applyMigrations(0, map[string]interface{}{}); err != nil { t.Fatalf("migration failed: %v", err) } - if !cfg.Sets[0].Enabled { - t.Error("v0->v1 should set Enabled=true") + if cfg.Version != CurrentConfigVersion { + t.Errorf("expected version %d, got %d", CurrentConfigVersion, cfg.Version) } }) @@ -193,4 +199,49 @@ func TestApplyMigrations(t *testing.T) { }) } }) + + t.Run("v49 to v50 moves legacy mtproto secret into secrets list", func(t *testing.T) { + t.Run("legacy secret migrated from raw", func(t *testing.T) { + cfg := NewConfig() + cfg.System.MTProto.Secrets = nil + raw := map[string]interface{}{"system": map[string]interface{}{"mtproto": map[string]interface{}{"secret": "ee-legacy-secret"}}} + if err := migrateV49to50(&cfg, raw); err != nil { + t.Fatalf("migration failed: %v", err) + } + if len(cfg.System.MTProto.Secrets) != 1 { + t.Fatalf("want 1 migrated secret, got %d", len(cfg.System.MTProto.Secrets)) + } + s := cfg.System.MTProto.Secrets[0] + if s.Secret != "ee-legacy-secret" { + t.Errorf("secret: want %q, got %q", "ee-legacy-secret", s.Secret) + } + if !s.Enabled { + t.Error("migrated secret should be enabled") + } + if s.ID == "" { + t.Error("migrated secret should get an ID") + } + }) + t.Run("existing secrets are not overwritten", func(t *testing.T) { + cfg := NewConfig() + cfg.System.MTProto.Secrets = []MTProtoSecret{{ID: "keep", Secret: "ee-existing", Enabled: true}} + raw := map[string]interface{}{"system": map[string]interface{}{"mtproto": map[string]interface{}{"secret": "ee-legacy-secret"}}} + if err := migrateV49to50(&cfg, raw); err != nil { + t.Fatalf("migration failed: %v", err) + } + if len(cfg.System.MTProto.Secrets) != 1 || cfg.System.MTProto.Secrets[0].Secret != "ee-existing" { + t.Errorf("existing secrets should be untouched, got %+v", cfg.System.MTProto.Secrets) + } + }) + t.Run("no legacy secret is a no-op", func(t *testing.T) { + cfg := NewConfig() + cfg.System.MTProto.Secrets = nil + if err := migrateV49to50(&cfg, map[string]interface{}{}); err != nil { + t.Fatalf("migration failed: %v", err) + } + if len(cfg.System.MTProto.Secrets) != 0 { + t.Errorf("want 0 secrets, got %d", len(cfg.System.MTProto.Secrets)) + } + }) + }) } diff --git a/src/config/mtproto.go b/src/config/mtproto.go index 21165493..2ea406f7 100644 --- a/src/config/mtproto.go +++ b/src/config/mtproto.go @@ -10,19 +10,13 @@ type MTProtoSecret struct { } func (m *MTProtoConfig) EffectiveSecrets() []MTProtoSecret { - if len(m.Secrets) > 0 { - out := make([]MTProtoSecret, 0, len(m.Secrets)) - for _, s := range m.Secrets { - if s.Enabled && strings.TrimSpace(s.Secret) != "" { - out = append(out, s) - } + out := make([]MTProtoSecret, 0, len(m.Secrets)) + for _, s := range m.Secrets { + if s.Enabled && strings.TrimSpace(s.Secret) != "" { + out = append(out, s) } - return out } - if strings.TrimSpace(m.Secret) != "" { - return []MTProtoSecret{{ID: "legacy", Secret: m.Secret, Enabled: true}} - } - return nil + return out } func (m *MTProtoConfig) FirstEnabledSecret() string { diff --git a/src/config/types.go b/src/config/types.go index 344dc41c..4e9fda71 100644 --- a/src/config/types.go +++ b/src/config/types.go @@ -272,20 +272,21 @@ type AIConfig struct { } type MTProtoConfig struct { - Enabled bool `json:"enabled"` - Port int `json:"port"` - BindAddress string `json:"bind_address"` - MaxConnections int `json:"max_connections"` // max concurrent client connections; 0 = default (2048) - Secret string `json:"secret"` // legacy single secret; mirrors the first enabled entry in Secrets for backward compatibility - Secrets []MTProtoSecret `json:"secrets,omitempty"` - FakeSNI string `json:"fake_sni"` - DCRelay string `json:"dc_relay"` - UpstreamMode string `json:"upstream_mode"` - WSCustomDomain string `json:"ws_custom_domain"` - WSEndpointHost string `json:"ws_endpoint_host"` - CFProxyEnabled bool `json:"cfproxy_enabled"` // enable Cloudflare-proxied fallback WS domains (rescues DCs the network blocks) - CFProxyURL string `json:"cfproxy_url"` // URL to refresh CF-proxy domain list; empty = built-in default - CFWorkerDomain string `json:"cfworker_domain"` // user's Cloudflare Worker domain(s) (workers.dev), comma-separated; free per-user WS relay tried before the shared CF pool + Enabled bool `json:"enabled"` + Port int `json:"port"` + BindAddress string `json:"bind_address"` + MaxConnections int `json:"max_connections"` // max concurrent client connections; 0 = default (2048) + TCPUserTimeoutSec int `json:"tcp_user_timeout_sec"` + IdleTimeoutSec int `json:"idle_timeout_sec"` + Secrets []MTProtoSecret `json:"secrets,omitempty"` + FakeSNI string `json:"fake_sni"` + DCRelay string `json:"dc_relay"` + UpstreamMode string `json:"upstream_mode"` + WSCustomDomain string `json:"ws_custom_domain"` + WSEndpointHost string `json:"ws_endpoint_host"` + CFProxyEnabled bool `json:"cfproxy_enabled"` // enable Cloudflare-proxied fallback WS domains (rescues DCs the network blocks) + CFProxyURL string `json:"cfproxy_url"` // URL to refresh CF-proxy domain list; empty = built-in default + CFWorkerDomain string `json:"cfworker_domain"` // user's Cloudflare Worker domain(s) (workers.dev), comma-separated; free per-user WS relay tried before the shared CF pool DCFallbackEnabled bool `json:"dc_fallback_enabled"` // fetch the DC IP list from DCFallbackURL when Telegram's official endpoint is blocked DCFallbackURL string `json:"dc_fallback_url"` // fallback source for the Telegram DC list; empty = built-in default diff --git a/src/config/validation.go b/src/config/validation.go index 7d4253f0..5cdabd39 100644 --- a/src/config/validation.go +++ b/src/config/validation.go @@ -374,6 +374,16 @@ func (c *Config) checkPortCollisions(v *validator) { map[string]any{"value": mc, "min": 0, "max": 100000}, "max_connections must be between 0 (default) and 100000 (got %d)", mc) } + if ut := c.System.MTProto.TCPUserTimeoutSec; ut < -1 || ut > 86400 { + v.addf("system.mtproto.tcp_user_timeout_sec", "out_of_range", + map[string]any{"value": ut, "min": -1, "max": 86400}, + "tcp_user_timeout_sec must be -1 (disable), 0 (default 120), or up to 86400 (got %d)", ut) + } + if it := c.System.MTProto.IdleTimeoutSec; it < -1 || it > 86400 { + v.addf("system.mtproto.idle_timeout_sec", "out_of_range", + map[string]any{"value": it, "min": -1, "max": 86400}, + "idle_timeout_sec must be -1 (disable), 0 (default 300), or up to 86400 (got %d)", it) + } } for i := 0; i < len(refs); i++ { for j := i + 1; j < len(refs); j++ { diff --git a/src/http/handler/mtproto.go b/src/http/handler/mtproto.go index d1a0cc4f..718d232b 100644 --- a/src/http/handler/mtproto.go +++ b/src/http/handler/mtproto.go @@ -5,8 +5,10 @@ import ( "io" "net" "net/http" + "sort" "strconv" "strings" + "time" "github.com/daniellavrushin/b4/config" "github.com/daniellavrushin/b4/log" @@ -19,6 +21,121 @@ func (api *API) RegisterMTProtoApi() { api.mux.HandleFunc("/api/mtproto/config", api.handleMTProtoConfig) api.mux.HandleFunc("/api/mtproto/refresh-dcs", api.handleMTProtoRefreshDCs) api.mux.HandleFunc("/api/mtproto/test-ws", api.handleMTProtoTestWS) + api.mux.HandleFunc("/api/mtproto/sessions", api.handleMTProtoSessions) + api.mux.HandleFunc("/api/mtproto/active-clients", api.handleMTProtoActiveClients) +} + +func mtprotoSessions() []mtproto.SessionInfo { + if p, ok := globalMTProtoServer.(interface { + Sessions() []mtproto.SessionInfo + }); ok { + return p.Sessions() + } + return nil +} + +// @Summary List active MTProto sessions +// @Description One entry per live client connection, including client IP/port, upstream destination and activity timestamps. +// @Tags MTProto +// @Produce json +// @Success 200 {array} object +// @Security BearerAuth +// @Router /mtproto/sessions [get] +func (api *API) handleMTProtoSessions(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + type sessionOut struct { + ID string `json:"id"` + Name string `json:"name"` + ClientIP string `json:"client_ip"` + ClientPort int `json:"client_port"` + Destination string `json:"destination"` + ConnectedAt string `json:"connected_at"` + LastSeen string `json:"last_seen"` + } + sessions := mtprotoSessions() + out := make([]sessionOut, 0, len(sessions)) + for _, s := range sessions { + out = append(out, sessionOut{ + ID: s.ID, + Name: s.Name, + ClientIP: s.ClientIP, + ClientPort: s.ClientPort, + Destination: s.Destination, + ConnectedAt: s.ConnectedAt.UTC().Format(time.RFC3339), + LastSeen: s.LastSeen.UTC().Format(time.RFC3339), + }) + } + sort.Slice(out, func(i, j int) bool { + return out[i].ConnectedAt < out[j].ConnectedAt + }) + sendResponse(w, out) +} + +// @Summary List active MTProto clients per secret +// @Description Aggregated per-secret view: active connection count, the distinct client IPs currently using each secret, and last activity. +// @Tags MTProto +// @Produce json +// @Success 200 {array} object +// @Security BearerAuth +// @Router /mtproto/active-clients [get] +func (api *API) handleMTProtoActiveClients(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + type clientOut struct { + ID string `json:"id"` + Name string `json:"name"` + ActiveConnections int `json:"active_connections"` + ActiveIPs []string `json:"active_ips"` + ActiveIPCount int `json:"active_ip_count"` + LastSeen string `json:"last_seen"` + } + + type agg struct { + out clientOut + ips []string + seenIP map[string]struct{} + lastSeen time.Time + } + order := make([]string, 0) + byID := make(map[string]*agg) + + for _, s := range mtprotoSessions() { + a := byID[s.ID] + if a == nil { + a = &agg{ + out: clientOut{ID: s.ID, Name: s.Name, ActiveIPs: []string{}}, + seenIP: make(map[string]struct{}), + } + byID[s.ID] = a + order = append(order, s.ID) + } + a.out.ActiveConnections++ + if s.ClientIP != "" { + if _, ok := a.seenIP[s.ClientIP]; !ok { + a.seenIP[s.ClientIP] = struct{}{} + a.ips = append(a.ips, s.ClientIP) + } + } + if s.LastSeen.After(a.lastSeen) { + a.lastSeen = s.LastSeen + } + } + + out := make([]clientOut, 0, len(order)) + for _, id := range order { + a := byID[id] + sort.Strings(a.ips) + a.out.ActiveIPs = a.ips + a.out.ActiveIPCount = len(a.ips) + a.out.LastSeen = a.lastSeen.UTC().Format(time.RFC3339) + out = append(out, a.out) + } + sendResponse(w, out) } // @Summary Probe MTProto upstream transports @@ -243,21 +360,7 @@ func (api *API) updateMTProtoConfig(w http.ResponseWriter, r *http.Request) { } } - req.Secret = strings.TrimSpace(req.Secret) - if req.Secret != "" { - if _, err := mtproto.ParseSecret(req.Secret); err != nil { - writeJsonError(w, http.StatusBadRequest, "Invalid secret: "+err.Error()) - return - } - } - - // Keep the legacy single-secret field mirroring the first enabled entry so - // older clients and the share-link path stay consistent. - if len(req.Secrets) > 0 { - req.Secret = req.FirstEnabledSecret() - } - - hasSecret := req.Secret != "" || len(req.EffectiveSecrets()) > 0 + hasSecret := len(req.EffectiveSecrets()) > 0 if req.Enabled && !hasSecret && req.FakeSNI == "" { writeJsonError(w, http.StatusBadRequest, "At least one secret or a fake SNI domain is required when enabled") return diff --git a/src/http/handler/mtproto_test.go b/src/http/handler/mtproto_test.go new file mode 100644 index 00000000..a4888678 --- /dev/null +++ b/src/http/handler/mtproto_test.go @@ -0,0 +1,176 @@ +package handler + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/daniellavrushin/b4/config" + "github.com/daniellavrushin/b4/mtproto" +) + +type stubMTProtoServer struct { + sessions []mtproto.SessionInfo +} + +func (s *stubMTProtoServer) UpdateConfig(*config.Config) {} + +func (s *stubMTProtoServer) Sessions() []mtproto.SessionInfo { return s.sessions } + +func newMTProtoTestMux(t *testing.T, srv ConfigRefresher) *http.ServeMux { + t.Helper() + prev := globalMTProtoServer + globalMTProtoServer = srv + t.Cleanup(func() { globalMTProtoServer = prev }) + + cfg := config.NewConfig() + api := &API{cfgPtr: testCfgPtr(&cfg)} + mux := http.NewServeMux() + api.mux = mux + api.RegisterMTProtoApi() + return mux +} + +func TestHandleMTProtoSessions(t *testing.T) { + t0 := time.Date(2026, 7, 2, 10, 0, 0, 0, time.UTC) + stub := &stubMTProtoServer{sessions: []mtproto.SessionInfo{ + { + ID: "a", Name: "Max", ClientIP: "85.233.150.240", ClientPort: 42378, + Destination: "149.154.167.220:443", ConnectedAt: t0, LastSeen: t0.Add(15 * time.Second), + }, + }} + mux := newMTProtoTestMux(t, stub) + + req := httptest.NewRequest(http.MethodGet, "/api/mtproto/sessions", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rec.Code) + } + + var out []map[string]any + if err := json.NewDecoder(rec.Body).Decode(&out); err != nil { + t.Fatalf("decode: %v", err) + } + if len(out) != 1 { + t.Fatalf("expected 1 session, got %d", len(out)) + } + got := out[0] + want := map[string]any{ + "id": "a", "name": "Max", "client_ip": "85.233.150.240", + "client_port": float64(42378), "destination": "149.154.167.220:443", + "connected_at": "2026-07-02T10:00:00Z", "last_seen": "2026-07-02T10:00:15Z", + } + for k, v := range want { + if got[k] != v { + t.Errorf("field %q = %v, want %v", k, got[k], v) + } + } +} + +func TestHandleMTProtoSessionsMethodNotAllowed(t *testing.T) { + mux := newMTProtoTestMux(t, &stubMTProtoServer{}) + req := httptest.NewRequest(http.MethodPost, "/api/mtproto/sessions", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusMethodNotAllowed { + t.Fatalf("expected 405, got %d", rec.Code) + } +} + +func TestHandleMTProtoSessionsEmptyWhenNoServer(t *testing.T) { + prev := globalMTProtoServer + globalMTProtoServer = nil + t.Cleanup(func() { globalMTProtoServer = prev }) + + cfg := config.NewConfig() + api := &API{cfgPtr: testCfgPtr(&cfg)} + mux := http.NewServeMux() + api.mux = mux + api.RegisterMTProtoApi() + + req := httptest.NewRequest(http.MethodGet, "/api/mtproto/sessions", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rec.Code) + } + var out []map[string]any + if err := json.NewDecoder(rec.Body).Decode(&out); err != nil { + t.Fatalf("decode: %v", err) + } + if len(out) != 0 { + t.Fatalf("expected empty array, got %d entries", len(out)) + } +} + +func TestHandleMTProtoActiveClients(t *testing.T) { + t0 := time.Date(2026, 7, 2, 10, 0, 0, 0, time.UTC) + stub := &stubMTProtoServer{sessions: []mtproto.SessionInfo{ + {ID: "a", Name: "Max", ClientIP: "85.233.150.240", ClientPort: 42378, ConnectedAt: t0, LastSeen: t0.Add(15 * time.Second)}, + {ID: "a", Name: "Max", ClientIP: "85.233.150.240", ClientPort: 42379, ConnectedAt: t0, LastSeen: t0.Add(18 * time.Second)}, + {ID: "a", Name: "Max", ClientIP: "178.130.140.98", ClientPort: 55120, ConnectedAt: t0, LastSeen: t0.Add(17 * time.Second)}, + {ID: "b", Name: "Ivan", ClientIP: "10.0.0.5", ClientPort: 5000, ConnectedAt: t0, LastSeen: t0.Add(1 * time.Second)}, + }} + mux := newMTProtoTestMux(t, stub) + + req := httptest.NewRequest(http.MethodGet, "/api/mtproto/active-clients", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rec.Code) + } + + type clientOut struct { + ID string `json:"id"` + Name string `json:"name"` + ActiveConnections int `json:"active_connections"` + ActiveIPs []string `json:"active_ips"` + ActiveIPCount int `json:"active_ip_count"` + LastSeen string `json:"last_seen"` + } + var out []clientOut + if err := json.NewDecoder(rec.Body).Decode(&out); err != nil { + t.Fatalf("decode: %v", err) + } + if len(out) != 2 { + t.Fatalf("expected 2 clients, got %d", len(out)) + } + + byID := make(map[string]clientOut, len(out)) + for _, c := range out { + byID[c.ID] = c + } + + a, ok := byID["a"] + if !ok { + t.Fatalf("client a missing") + } + if a.ActiveConnections != 3 { + t.Errorf("active_connections = %d, want 3", a.ActiveConnections) + } + if a.ActiveIPCount != 2 { + t.Errorf("active_ip_count = %d, want 2", a.ActiveIPCount) + } + wantIPs := []string{"178.130.140.98", "85.233.150.240"} + if len(a.ActiveIPs) != len(wantIPs) { + t.Fatalf("active_ips = %v, want %v", a.ActiveIPs, wantIPs) + } + for i, ip := range wantIPs { + if a.ActiveIPs[i] != ip { + t.Errorf("active_ips[%d] = %q, want %q (expected sorted)", i, a.ActiveIPs[i], ip) + } + } + if a.LastSeen != "2026-07-02T10:00:18Z" { + t.Errorf("last_seen = %q, want the most recent activity 2026-07-02T10:00:18Z", a.LastSeen) + } + + if b := byID["b"]; b.ActiveConnections != 1 || b.ActiveIPCount != 1 { + t.Errorf("client b unexpected: conns=%d ips=%d", b.ActiveConnections, b.ActiveIPCount) + } +} diff --git a/src/http/ui/src/components/settings/MTProto.tsx b/src/http/ui/src/components/settings/MTProto.tsx index df123e42..bba40531 100644 --- a/src/http/ui/src/components/settings/MTProto.tsx +++ b/src/http/ui/src/components/settings/MTProto.tsx @@ -258,6 +258,24 @@ export const MTProtoSettings = ({ config, onChange }: MTProtoSettingsProps) => { disabled={!config.system.mtproto?.enabled} helperText={t("settings.MTProto.maxConnectionsHelp")} /> +