diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props index c246b5e..1ad7929 100644 --- a/backend/Directory.Packages.props +++ b/backend/Directory.Packages.props @@ -1,46 +1,46 @@ - - - true - - - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - + + + true + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/IXuiPanelGateway.cs b/backend/src/PnvPanel.Application/Common/Interfaces/IXuiPanelGateway.cs index 6b81f8c..351c02f 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/IXuiPanelGateway.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/IXuiPanelGateway.cs @@ -95,6 +95,13 @@ public interface IXuiPanelGateway CancellationToken cancellationToken ); + /// + /// Реализация (XuiPanelGateway) передаёт в ThreeXui.Net (1.0.2+) + /// ForcedFingerprint: "firefox" (клиентские приложения выдают TLS-fingerprint панели, + /// если fp не задан явно в tls/reality-ссылке) и для транспорта xhttp — + /// ForcedPacketEncoding: "xudp" (без него часть клиентов не может корректно релеить UDP + /// поверх xhttp); для остальных транспортов ForcedPacketEncoding не передаётся. + /// Task> BuildConnectionStringAsync( Node node, Inbound inbound, diff --git a/backend/src/PnvPanel.Infrastructure/Xui/XuiPanelGateway.cs b/backend/src/PnvPanel.Infrastructure/Xui/XuiPanelGateway.cs index 353e9af..1cb160e 100644 --- a/backend/src/PnvPanel.Infrastructure/Xui/XuiPanelGateway.cs +++ b/backend/src/PnvPanel.Infrastructure/Xui/XuiPanelGateway.cs @@ -1,7 +1,5 @@ using System.Collections.Concurrent; -using System.Text; using System.Text.Json; -using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Common.Models; @@ -239,12 +237,12 @@ internal sealed class XuiPanelGateway( inbound.Port, publicHost, node.BaseAddress.ToString(), - remoteInbound + remoteInbound, + ForcedFingerprint: ForcedFingerprint, + ForcedPacketEncoding: IsXhttpTransport(remoteInbound.StreamSettings) ? "xudp" : null ); - return Result.Success( - ForceFingerprint(builder.Build(request), inbound.Protocol, ForcedFingerprint) - ); + return Result.Success(builder.Build(request)); } catch (Exception ex) { @@ -259,65 +257,31 @@ internal sealed class XuiPanelGateway( /// /// Клиентские приложения выдают TLS-fingerprint панели, если fp не задан явно в - /// share-ссылке — подставляем "firefox" поверх того, что вернул ThreeXui.Net - /// (сам параметр берётся из streamSettings ноды, per-nod настройки не даём). - /// Применимо только к tls/reality-ссылкам (vless/trojan/vmess); shadowsocks TLS - /// не использует. + /// share-ссылке — подставляем "firefox" через + /// (ThreeXui.Net 1.0.2+; применяется библиотекой только к tls/reality-ссылкам, игнорируется иначе). /// private const string ForcedFingerprint = "firefox"; - private static string ForceFingerprint(string link, VpnProtocol protocol, string fingerprint) => - protocol switch - { - VpnProtocol.Vless or VpnProtocol.Trojan => ForceQueryFingerprint(link, fingerprint), - VpnProtocol.Vmess => ForceVmessFingerprint(link, fingerprint), - _ => link, - }; - - private static string ForceQueryFingerprint(string link, string fingerprint) + /// + /// packetEncoding=xudp через нужен + /// только транспорту xhttp — остальные транспорты его не используют, добавлять не за чем. + /// + private static bool IsXhttpTransport(string? streamSettingsJson) { - var hashIndex = link.IndexOf('#'); - var head = hashIndex >= 0 ? link[..hashIndex] : link; - var fragment = hashIndex >= 0 ? link[hashIndex..] : string.Empty; - - var queryIndex = head.IndexOf('?'); - if (queryIndex < 0) - return link; - - var baseUri = head[..queryIndex]; - var query = head[(queryIndex + 1)..]; - if (!query.Contains("security=tls") && !query.Contains("security=reality")) - return link; - - var parts = query - .Split('&', StringSplitOptions.RemoveEmptyEntries) - .Where(p => !p.StartsWith("fp=", StringComparison.Ordinal)) - .Append($"fp={fingerprint}"); - - return $"{baseUri}?{string.Join('&', parts)}{fragment}"; - } - - private static string ForceVmessFingerprint(string link, string fingerprint) - { - const string prefix = "vmess://"; - if (!link.StartsWith(prefix, StringComparison.Ordinal)) - return link; + if (string.IsNullOrWhiteSpace(streamSettingsJson)) + return false; try { - var json = Encoding.UTF8.GetString(Convert.FromBase64String(link[prefix.Length..])); - var node = JsonNode.Parse(json)?.AsObject(); - if (node is null || string.IsNullOrEmpty(node["tls"]?.GetValue())) - return link; - - node["fp"] = fingerprint; - var updatedJson = node.ToJsonString(); - return prefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(updatedJson)); + using var doc = JsonDocument.Parse(streamSettingsJson); + return doc.RootElement.ValueKind == JsonValueKind.Object + && doc.RootElement.TryGetProperty("network", out var network) + && network.ValueKind == JsonValueKind.String + && string.Equals(network.GetString(), "xhttp", StringComparison.OrdinalIgnoreCase); } - catch (Exception) + catch (JsonException) { - // Неожиданный формат vmess-пейлоада — возвращаем ссылку как есть. - return link; + return false; } } diff --git a/docs/architecture.md b/docs/architecture.md index 3c29b15..34d6c46 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -173,10 +173,12 @@ POST /api/configs - Синхронизация инбаундов ноды (`SyncNodeCommandHandler`) реконсилирует пропажу инбаунда с панели: без привязанных конфигов запись удаляется, с конфигами — помечается `IsAvailable=false` (детали и инварианты — [domain-model.md](domain-model.md#inbound--прокси-inbound-на-ноде)). -- `BuildConnectionStringAsync` поверх ссылки из `ThreeXui.Net` принудительно подставляет - `fp=firefox` (TLS-fingerprint клиента) для tls/reality-ссылок vless/trojan/vmess, независимо - от того, что задано в `streamSettings` ноды; shadowsocks (без TLS) и ссылки без security не - трогает. Правится в одном месте — применяется и к сайту, и к боту, и к подписке. +- `BuildConnectionStringAsync` передаёт в `ThreeXui.Net` (`XuiConnectionStringRequest.ForcedFingerprint: + "firefox"`) принудительный TLS-fingerprint клиента для tls/reality-ссылок vless/trojan/vmess, + независимо от того, что задано в `streamSettings` ноды; shadowsocks (без TLS) и ссылки без security + библиотека не трогает. Для транспорта `xhttp` дополнительно передаётся + `ForcedPacketEncoding: "xudp"` (не 3x-ui-настройка, а требование части клиентских приложений). + Правится в одном месте — применяется и к сайту, и к боту, и к подписке. - **Дрейф с 3x-ui активно не реконсилируется**: `TrafficSyncService` при недоступной ноде или при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо