diff --git a/README.md b/README.md index adc47d7b4..9d9f3b6b7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Built as an enhanced fork of the original X-UI project, 3X-UI adds broader proto - **Multi-protocol inbounds** — VLESS, VMess, Trojan, Shadowsocks, WireGuard, AmneziaWG, TUIC v5, Hysteria2, MTProto, HTTP, SOCKS (Mixed), Dokodemo-door / Tunnel, and TUN. - **Modern transports & security** — TCP (Raw), mKCP, WebSocket, gRPC, HTTPUpgrade, and XHTTP, secured with TLS, XTLS, and REALITY. - **AmneziaWG built in** — DPI-resistant WireGuard runs inside the panel on a userspace network stack, with no kernel module, DKMS, or extra packages to install. -- **TUIC v5 sidecar** — High-performance QUIC-based proxy with native UDP relay traffic metering, 0-RTT handshakes, and BBR congestion control. +- **Native TUIC v5 server** — In-process Go QUIC server with Xray routing and per-client traffic accounting; BBR and New Reno are available server-side. CUBIC is preserved in the client profile but currently falls back to New Reno on the server. - **MTProto proxies** — per-client FakeTLS secrets, ad-tags, and quotas, applied live without dropping existing connections. - **Fallbacks** — serve multiple protocols on a single port (e.g. VLESS and Trojan on 443) using Xray's fallback support. - **Per-client management** — traffic quotas, expiry dates, IP limits with trusted-address exemptions, HWID device limits, scheduled renewal cycles, live online status, and one-click share links, QR codes, and subscriptions. diff --git a/README.ru_RU.md b/README.ru_RU.md index 3a64ab15b..0544871d4 100644 --- a/README.ru_RU.md +++ b/README.ru_RU.md @@ -29,7 +29,7 @@ - **Многопротокольные входящие подключения** — VLESS, VMess, Trojan, Shadowsocks, WireGuard, AmneziaWG, TUIC v5, Hysteria2, MTProto, HTTP, SOCKS (Mixed), Dokodemo-door / Tunnel и TUN. - **Современные транспорты и безопасность** — TCP (Raw), mKCP, WebSocket, gRPC, HTTPUpgrade и XHTTP, защищённые с помощью TLS, XTLS и REALITY. - **Встроенный AmneziaWG** — устойчивый к DPI WireGuard работает прямо в панели на сетевом стеке в пространстве пользователя: без модуля ядра, DKMS и дополнительных пакетов. -- **Встроенный TUIC v5** — высокопроизводительный прокси на базе QUIC с нативным учётом трафика через UDP-релей, 0-RTT рукопожатиями и контролем перегрузок BBR. +- **Нативный TUIC v5** — Go QUIC-сервер работает внутри процесса панели. Трафик маршрутизируется через Xray, а учёт ведётся по клиентам. На сервере доступны BBR и New Reno; CUBIC сохраняется в профиле клиента, но на сервере пока использует New Reno. - **MTProto-прокси** — секреты FakeTLS, ad-tag и квоты для каждого клиента применяются на лету, не разрывая существующие соединения. - **Fallback** — обслуживание нескольких протоколов на одном порту (например, VLESS и Trojan на 443) с помощью функции fallback в Xray. - **Управление по каждому клиенту** — квоты трафика, даты истечения, лимиты IP с исключениями для доверенных адресов, лимиты устройств (HWID), запланированные циклы продления, статус «онлайн» в реальном времени, а также ссылки для общего доступа, QR-коды и подписки в один клик. diff --git a/docs/content/docs/en/config/tuic.mdx b/docs/content/docs/en/config/tuic.mdx index 7232a9f65..e2098101d 100644 --- a/docs/content/docs/en/config/tuic.mdx +++ b/docs/content/docs/en/config/tuic.mdx @@ -15,16 +15,16 @@ unstable networks. ## Key settings -### Server & QUIC parameters +### Server, QUIC & client-profile parameters | Field | Description | | --- | --- | | **Port** | UDP port for incoming client QUIC connections. | | **Certificate & Key** | Full TLS certificate chain and private key. QUIC mandates TLS encryption; self-signed certificates or valid Let's Encrypt / ACME certs are supported. | -| **SNI** | Server Name Indication matching your TLS certificate domain name. | -| **Congestion Control** | QUIC congestion control algorithm: `bbr` (recommended for high throughput), `cubic`, or `new_reno`. The server runs `bbr` or `new_reno`; `cubic` is sent to clients but served as `new_reno`. | +| **SNI** | Client-profile Server Name Indication. Set it to the domain covered by the server certificate; this field does not configure the listener certificate. | +| **Congestion Control** | QUIC congestion control algorithm used in the server setting and exported client profile: `bbr`, `cubic`, or `new_reno`. The server runs BBR or New Reno; when CUBIC is selected, the client profile keeps CUBIC while this server currently falls back to New Reno. | | **ALPN** | Application-Layer Protocol Negotiation tokens (default: `h3`). | -| **UDP Relay Mode** | Packet encapsulation mode: `native` (QUIC datagrams, recommended) or `quic`. | +| **UDP Relay Mode** | Client-profile packet mode: `native` (QUIC datagrams) or `quic` (unidirectional streams). The server accepts both modes regardless of this exported preference. | | **Zero-RTT Handshake** | Enables 0-RTT connection resumption to eliminate initial handshake round-trips for returning clients. | | **Authentication Timeout** | Maximum time (seconds) allowed for client authentication before disconnecting (default: `3s`). | | **Max Idle Time** | Inactivity timeout (seconds) before closing idle QUIC connections (default: `15s`). | @@ -105,6 +105,8 @@ tuic://:@:?congestion_control=bbr&alpn=h3&sni=vpn.ex - **Native in-process Go engine**: TUIC v5 runs 100% natively in Go within the 3x-ui process. No external binaries or sidecars to download or maintain. - **Full Xray routing & cascading**: Decrypted traffic passes directly through Xray's routing engine. Inbound tags (`in--udp`) work seamlessly with routing rules, domain/IP blocks, and cascading to any outbound proxy (VLESS, Shadowsocks, WARP, etc.). - **Per-client traffic limits & expiration**: Individual traffic quotas (`totalGB`) and expiration timestamps (`expiryTime`) are tracked and enforced for each client. + - **Live speed & traffic totals**: Native TUIC client counters are sampled every 10 seconds and sent to the panel for per-client live speed. Xray meters inbound totals through the loopback relay; TUIC's client-speed event does not add inbound totals again. + - **UDP resource bounds**: Each QUIC connection can hold up to 256 active UDP associations. Idle associations are closed after five minutes. This is a per-connection limit, not a node-wide association cap. TUIC uses a reserved loopback SOCKS relay port in `64001–65000`; 3x-ui checks it against managed inbound and relay ports. - **Zero-downtime client updates**: Adding, modifying, or disabling clients updates the in-memory user registry instantly without restarting the UDP port or interrupting existing client sessions. - **Deployment**: A TUIC inbound can be created on, or cloned to, a sub-node. The node's own panel runs the TUIC server, so the node must run panel v3.8.0 or newer; the master refuses an older node. diff --git a/docs/content/docs/ru/config/tuic.mdx b/docs/content/docs/ru/config/tuic.mdx index ab01e5ccf..939a78e27 100644 --- a/docs/content/docs/ru/config/tuic.mdx +++ b/docs/content/docs/ru/config/tuic.mdx @@ -14,16 +14,16 @@ icon: Zap ## Ключевые параметры -### Параметры сервера и QUIC +### Параметры сервера, QUIC и клиентского профиля | Поле | Описание | | --- | --- | | **Порт** | UDP-порт для входящих QUIC-соединений клиентов. | | **Сертификат и ключ** | Полная цепочка SSL-сертификата и приватный ключ. Протокол QUIC требует обязательного шифрования TLS; поддерживаются сертификаты Let's Encrypt / ACME или самоподписанные. | -| **SNI** | Имя сервера (Server Name Indication), совпадающее с доменным именем в сертификате. | -| **Контроль перегрузок** | Алгоритм контроля перегрузок QUIC: `bbr` (рекомендуется для максимальной скорости), `cubic` или `new_reno`. Сервер работает с `bbr` или `new_reno`; `cubic` передаётся клиентам, но на сервере применяется как `new_reno`. | +| **SNI** | Server Name Indication для профиля клиента. Укажите домен, покрытый сертификатом сервера; это поле не настраивает сертификат listener'а. | +| **Контроль перегрузок** | Алгоритм QUIC в настройках сервера и экспортируемом профиле: `bbr`, `cubic` или `new_reno`. Сервер использует BBR или New Reno; при выборе CUBIC клиентский профиль сохраняет CUBIC, а сервер пока применяет New Reno. | | **ALPN** | Токены протоколов уровня приложений (по умолчанию: `h3`). | -| **Режим UDP Relay** | Режим инкапсуляции пакетов: `native` (QUIC datagrams, рекомендуется) или `quic`. | +| **Режим UDP Relay** | Режим UDP в профиле клиента: `native` (QUIC datagrams) или `quic` (однонаправленные потоки). Сервер принимает оба режима независимо от этого значения. | | **Zero-RTT Handshake** | Включает 0-RTT возобновление сессий для мгновенного повторного подключения клиентов без ожидания завершения рукопожатия. | | **Таймаут аутентификации** | Максимальное время (в секундах) на прохождение аутентификации клиентом (по умолчанию: `3s`). | | **Максимальный простой** | Таймаут бездействия (в секундах) перед закрытием неактивных QUIC-соединений (по умолчанию: `15s`). | @@ -104,6 +104,8 @@ tuic://:@:?congestion_control=bbr&alpn=h3&sni=vpn.ex - **Нативный Go-движок**: TUIC v5 работает на 100% нативно на Go внутри процесса 3x-ui. Никаких внешних сторонних бинарников скачивать не требуется. - **Маршрутизация и каскады в Xray**: Трафик проходит через движок маршрутизации Xray. Теги инбаундов (`in--udp`) полноценно участвуют в правилах маршрутизации (Routing Rules), блокировках geosite/geoip и перенаправлении в любые аутбаунды (VLESS, Shadowsocks, WARP и др.). - **Персональные квоты трафика**: Лимиты трафика (`totalGB`) и сроки действия (`expiryTime`) учитываются и применяются индивидуально для каждого клиента. + - **Скорость и общий трафик**: Нативные счётчики клиентов TUIC опрашиваются раз в 10 секунд и передаются в панель для отображения скорости. Xray отдельно считает общий трафик инбаунда через локальный relay; событие скорости TUIC повторно его не начисляет. + - **Ограничения UDP**: На одно QUIC-соединение допускается до 256 активных UDP-ассоциаций. Неактивные ассоциации закрываются через пять минут. Это лимит на соединение, а не общий лимит узла. TUIC использует выделенный локальный SOCKS-порт из диапазона `64001–65000`; 3x-ui проверяет его конфликты с управляемыми инбаундами и relay-портами. - **Горячее обновление без обрыва связи**: Добавление, редактирование или отключение клиентов обновляет реестр пользователей в памяти без перезапуска порта и без сброса активных сессий других пользователей. - **Развёртывание**: Инбаунд TUIC можно создать на дочернем узле или клонировать туда. TUIC-сервер запускает панель самого узла, поэтому на узле нужна панель v3.8.0 или новее; более старый узел главная панель отклоняет. diff --git a/docs/public/openapi.json b/docs/public/openapi.json index 298f96690..43feed637 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -16027,15 +16027,9 @@ }, { "type": "traffic", - "summary": "Live traffic deltas plus online, per-node and last-online maps. Local polls send traffics/clientTraffics; node polls send nodeTraffics.", + "summary": "Live traffic deltas plus online, per-node and last-online maps. TUIC also sends source-tagged client deltas with their sampling interval for live speed.", "payloadSchema": { "type": "object", - "required": [ - "onlineClients", - "onlineByGuid", - "activeInbounds", - "lastOnlineMap" - ], "properties": { "traffics": { "type": "array", @@ -16049,6 +16043,18 @@ "$ref": "#/components/schemas/ClientTraffic" } }, + "clientTrafficSource": { + "type": "string", + "enum": [ + "xray", + "tuic" + ], + "description": "Present for native TUIC samples; omitted Xray samples default to xray." + }, + "clientTrafficIntervalMs": { + "type": "integer", + "description": "Sampling interval used to calculate client speed, in milliseconds." + }, "nodeTraffics": { "type": "array", "nullable": true, @@ -16092,13 +16098,24 @@ { "required": [ "traffics", - "clientTraffics" + "clientTraffics", + "onlineClients", + "onlineByGuid", + "activeInbounds", + "lastOnlineMap" ] }, { "required": [ "nodeTraffics" ] + }, + { + "required": [ + "clientTraffics", + "clientTrafficSource", + "clientTrafficIntervalMs" + ] } ] }, diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 298f96690..43feed637 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -16027,15 +16027,9 @@ }, { "type": "traffic", - "summary": "Live traffic deltas plus online, per-node and last-online maps. Local polls send traffics/clientTraffics; node polls send nodeTraffics.", + "summary": "Live traffic deltas plus online, per-node and last-online maps. TUIC also sends source-tagged client deltas with their sampling interval for live speed.", "payloadSchema": { "type": "object", - "required": [ - "onlineClients", - "onlineByGuid", - "activeInbounds", - "lastOnlineMap" - ], "properties": { "traffics": { "type": "array", @@ -16049,6 +16043,18 @@ "$ref": "#/components/schemas/ClientTraffic" } }, + "clientTrafficSource": { + "type": "string", + "enum": [ + "xray", + "tuic" + ], + "description": "Present for native TUIC samples; omitted Xray samples default to xray." + }, + "clientTrafficIntervalMs": { + "type": "integer", + "description": "Sampling interval used to calculate client speed, in milliseconds." + }, "nodeTraffics": { "type": "array", "nullable": true, @@ -16092,13 +16098,24 @@ { "required": [ "traffics", - "clientTraffics" + "clientTraffics", + "onlineClients", + "onlineByGuid", + "activeInbounds", + "lastOnlineMap" ] }, { "required": [ "nodeTraffics" ] + }, + { + "required": [ + "clientTraffics", + "clientTrafficSource", + "clientTrafficIntervalMs" + ] } ] }, diff --git a/frontend/src/hooks/useClients.ts b/frontend/src/hooks/useClients.ts index 61d62508a..199a47a2e 100644 --- a/frontend/src/hooks/useClients.ts +++ b/frontend/src/hooks/useClients.ts @@ -98,6 +98,8 @@ export interface ClientSpeedEntry { down: number; } +type ClientSpeedSource = 'xray' | 'tuic'; + type ClientStatRow = ClientTraffic & { email?: string }; export function sameSpeedMap( @@ -299,7 +301,31 @@ export function useClients(options: UseClientsOptions = {}) { // settings request still lets the page fall back and render. const settingsReady = defaultsQuery.isFetched; - const [clientSpeed, setClientSpeed] = useState>({}); + const [clientSpeedBySource, setClientSpeedBySource] = useState< + Partial>> + >({}); + const clientSpeedExpiryTimers = useRef>>({}); + const clientSpeedSourceVersions = useRef>({ xray: 0, tuic: 0 }); + const clientSpeed = useMemo(() => { + const combined: Record = {}; + for (const source of Object.values(clientSpeedBySource)) { + if (!source) continue; + for (const [email, speed] of Object.entries(source)) { + const current = combined[email] ?? { up: 0, down: 0 }; + combined[email] = { up: current.up + speed.up, down: current.down + speed.down }; + } + } + return combined; + }, [clientSpeedBySource]); + + useEffect( + () => () => { + for (const timer of Object.values(clientSpeedExpiryTimers.current)) { + if (timer !== undefined) window.clearTimeout(timer); + } + }, + [], + ); const summary = listQuery.data?.summary ?? DEFAULT_SUMMARY; const invalidateAll = useCallback(() => { @@ -725,6 +751,8 @@ export function useClients(options: UseClientsOptions = {}) { const p = payload as { onlineClients?: string[]; clientTraffics?: { email: string; up: number; down: number }[]; + clientTrafficSource?: 'xray' | 'tuic'; + clientTrafficIntervalMs?: number; }; if (Array.isArray(p.onlineClients)) { queryClient.setQueryData(keys.clients.onlines(), p.onlineClients); @@ -736,17 +764,45 @@ export function useClients(options: UseClientsOptions = {}) { // dropped and an unchanged result returns the previous object — which lets // React bail out of the update instead of re-rendering the table. const next: Record = {}; + const source = p.clientTrafficSource === 'tuic' ? 'tuic' : 'xray'; + const sampleIntervalMs = + typeof p.clientTrafficIntervalMs === 'number' && + Number.isFinite(p.clientTrafficIntervalMs) && + p.clientTrafficIntervalMs > 0 + ? p.clientTrafficIntervalMs + : TRAFFIC_POLL_INTERVAL_S * 1000; + const sampleIntervalSeconds = sampleIntervalMs / 1000; for (const ct of p.clientTraffics) { if (!ct || !ct.email) continue; const up = ct.up || 0; const down = ct.down || 0; if (up === 0 && down === 0) continue; + const current = next[ct.email] ?? { up: 0, down: 0 }; next[ct.email] = { - up: up / TRAFFIC_POLL_INTERVAL_S, - down: down / TRAFFIC_POLL_INTERVAL_S, + up: current.up + up / sampleIntervalSeconds, + down: current.down + down / sampleIntervalSeconds, }; } - setClientSpeed((prev) => (sameSpeedMap(prev, next) ? prev : next)); + setClientSpeedBySource((prev) => + sameSpeedMap(prev[source] ?? {}, next) ? prev : { ...prev, [source]: next }, + ); + + const version = ++clientSpeedSourceVersions.current[source]; + const previousTimer = clientSpeedExpiryTimers.current[source]; + if (previousTimer !== undefined) window.clearTimeout(previousTimer); + clientSpeedExpiryTimers.current[source] = window.setTimeout( + () => { + if (clientSpeedSourceVersions.current[source] !== version) return; + delete clientSpeedExpiryTimers.current[source]; + setClientSpeedBySource((prev) => { + if (!prev[source]) return prev; + const nextSources = { ...prev }; + delete nextSources[source]; + return nextSources; + }); + }, + Math.min(sampleIntervalMs * 2, 120_000), + ); } }, [queryClient], diff --git a/frontend/src/pages/api-docs/websocket-events.ts b/frontend/src/pages/api-docs/websocket-events.ts index 997d93449..3549a5b5d 100644 --- a/frontend/src/pages/api-docs/websocket-events.ts +++ b/frontend/src/pages/api-docs/websocket-events.ts @@ -120,10 +120,18 @@ const statusPayloadSchema = { const trafficPayloadSchema = { type: 'object', - required: ['onlineClients', 'onlineByGuid', 'activeInbounds', 'lastOnlineMap'], properties: { traffics: { type: 'array', items: { $ref: '#/components/schemas/Traffic' } }, clientTraffics: { type: 'array', items: { $ref: '#/components/schemas/ClientTraffic' } }, + clientTrafficSource: { + type: 'string', + enum: ['xray', 'tuic'], + description: 'Present for native TUIC samples; omitted Xray samples default to xray.', + }, + clientTrafficIntervalMs: { + type: 'integer', + description: 'Sampling interval used to calculate client speed, in milliseconds.', + }, nodeTraffics: { type: 'array', nullable: true, @@ -134,7 +142,20 @@ const trafficPayloadSchema = { activeInbounds: stringArrayMap, lastOnlineMap: timestampMap, }, - oneOf: [{ required: ['traffics', 'clientTraffics'] }, { required: ['nodeTraffics'] }], + oneOf: [ + { + required: [ + 'traffics', + 'clientTraffics', + 'onlineClients', + 'onlineByGuid', + 'activeInbounds', + 'lastOnlineMap', + ], + }, + { required: ['nodeTraffics'] }, + { required: ['clientTraffics', 'clientTrafficSource', 'clientTrafficIntervalMs'] }, + ], }; const clientStatsPayloadSchema = { @@ -205,7 +226,7 @@ export function buildWebSocketEvents( { type: 'traffic', summary: - 'Live traffic deltas plus online, per-node and last-online maps. Local polls send traffics/clientTraffics; node polls send nodeTraffics.', + 'Live traffic deltas plus online, per-node and last-online maps. TUIC also sends source-tagged client deltas with their sampling interval for live speed.', payloadSchema: trafficPayloadSchema, example: { type: 'traffic', diff --git a/frontend/src/pages/inbounds/form/protocols/tuic.tsx b/frontend/src/pages/inbounds/form/protocols/tuic.tsx index ee577dca9..cc6876732 100644 --- a/frontend/src/pages/inbounds/form/protocols/tuic.tsx +++ b/frontend/src/pages/inbounds/form/protocols/tuic.tsx @@ -166,7 +166,7 @@ export default function TuicFields() { - +