Update package versions and enhance IXuiPanelGateway interface
CI / Backend (build + test) (push) Failing after 55s
CI / Frontend (lint + typecheck + build) (push) Successful in 34s

- Updated the version of `ThreeXui.Net` to 1.0.2 in `Directory.Packages.props`.
- Enhanced the `IXuiPanelGateway` interface to include detailed documentation on the new `ForcedFingerprint` and `ForcedPacketEncoding` parameters for the `BuildConnectionStringAsync` method, clarifying their roles in client application interactions.
- Refactored `XuiPanelGateway` to implement the new parameters, ensuring compatibility with client requirements for TLS fingerprinting and packet encoding based on transport type.
- Updated architecture documentation to reflect changes in connection string handling and the implications for client applications.
This commit is contained in:
Leonid Pershin
2026-07-23 00:24:02 +03:00
parent 29291b5dec
commit fb320fbb31
4 changed files with 79 additions and 106 deletions
@@ -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(
/// <summary>
/// Клиентские приложения выдают TLS-fingerprint панели, если fp не задан явно в
/// share-ссылке — подставляем "firefox" поверх того, что вернул ThreeXui.Net
/// (сам параметр берётся из streamSettings ноды, per-nod настройки не даём).
/// Применимо только к tls/reality-ссылкам (vless/trojan/vmess); shadowsocks TLS
/// не использует.
/// share-ссылке — подставляем "firefox" через <see cref="XuiConnectionStringRequest.ForcedFingerprint"/>
/// (ThreeXui.Net 1.0.2+; применяется библиотекой только к tls/reality-ссылкам, игнорируется иначе).
/// </summary>
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)
/// <summary>
/// packetEncoding=xudp через <see cref="XuiConnectionStringRequest.ForcedPacketEncoding"/> нужен
/// только транспорту xhttp — остальные транспорты его не используют, добавлять не за чем.
/// </summary>
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<string>()))
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;
}
}