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`) просто пропускает его в этом цикле
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо