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
+46 -46
View File
@@ -1,46 +1,46 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="FluentValidation" Version="12.1.1" /> <PackageVersion Include="FluentValidation" Version="12.1.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.11" /> <PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.11" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
<PackageVersion <PackageVersion
Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore"
Version="10.0.9" Version="10.0.9"
/> />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.OpenApi" Version="2.9.0" /> <PackageVersion Include="Microsoft.OpenApi" Version="2.9.0" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.16.7" /> <PackageVersion Include="Scalar.AspNetCore" Version="2.16.7" />
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" /> <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9"> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageVersion> </PackageVersion>
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.9" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.9" />
<PackageVersion <PackageVersion
Include="Microsoft.Extensions.DependencyInjection.Abstractions" Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="10.0.9" Version="10.0.9"
/> />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" /> <PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
<PackageVersion Include="Telegram.Bot" Version="22.10.1" /> <PackageVersion Include="Telegram.Bot" Version="22.10.1" />
<PackageVersion Include="ThreeXui.Net" Version="1.0.1" /> <PackageVersion Include="ThreeXui.Net" Version="1.0.2" />
<!-- Тестирование (M8) --> <!-- Тестирование (M8) -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="xunit" Version="2.9.3" /> <PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageVersion Include="NSubstitute" Version="5.3.0" /> <PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.2.0" /> <PackageVersion Include="Testcontainers.PostgreSql" Version="4.2.0" />
<!-- InMemory, не Sqlite/Npgsql — модель использует Postgres-специфичные типы (uuid[], jsonb), <!-- InMemory, не Sqlite/Npgsql — модель использует Postgres-специфичные типы (uuid[], jsonb),
не имеющие реляционных аналогов; InMemory игнорирует HasColumnType и не требует их маппинга. --> не имеющие реляционных аналогов; InMemory игнорирует HasColumnType и не требует их маппинга. -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -95,6 +95,13 @@ public interface IXuiPanelGateway
CancellationToken cancellationToken CancellationToken cancellationToken
); );
/// <summary>
/// Реализация (<c>XuiPanelGateway</c>) передаёт в ThreeXui.Net (1.0.2+)
/// <c>ForcedFingerprint: "firefox"</c> (клиентские приложения выдают TLS-fingerprint панели,
/// если fp не задан явно в tls/reality-ссылке) и для транспорта <c>xhttp</c> —
/// <c>ForcedPacketEncoding: "xudp"</c> (без него часть клиентов не может корректно релеить UDP
/// поверх xhttp); для остальных транспортов <c>ForcedPacketEncoding</c> не передаётся.
/// </summary>
Task<Result<string>> BuildConnectionStringAsync( Task<Result<string>> BuildConnectionStringAsync(
Node node, Node node,
Inbound inbound, Inbound inbound,
@@ -1,7 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Application.Common.Models; using PnvPanel.Application.Common.Models;
@@ -239,12 +237,12 @@ internal sealed class XuiPanelGateway(
inbound.Port, inbound.Port,
publicHost, publicHost,
node.BaseAddress.ToString(), node.BaseAddress.ToString(),
remoteInbound remoteInbound,
ForcedFingerprint: ForcedFingerprint,
ForcedPacketEncoding: IsXhttpTransport(remoteInbound.StreamSettings) ? "xudp" : null
); );
return Result.Success( return Result.Success(builder.Build(request));
ForceFingerprint(builder.Build(request), inbound.Protocol, ForcedFingerprint)
);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -259,65 +257,31 @@ internal sealed class XuiPanelGateway(
/// <summary> /// <summary>
/// Клиентские приложения выдают TLS-fingerprint панели, если fp не задан явно в /// Клиентские приложения выдают TLS-fingerprint панели, если fp не задан явно в
/// share-ссылке — подставляем "firefox" поверх того, что вернул ThreeXui.Net /// share-ссылке — подставляем "firefox" через <see cref="XuiConnectionStringRequest.ForcedFingerprint"/>
/// (сам параметр берётся из streamSettings ноды, per-nod настройки не даём). /// (ThreeXui.Net 1.0.2+; применяется библиотекой только к tls/reality-ссылкам, игнорируется иначе).
/// Применимо только к tls/reality-ссылкам (vless/trojan/vmess); shadowsocks TLS
/// не использует.
/// </summary> /// </summary>
private const string ForcedFingerprint = "firefox"; private const string ForcedFingerprint = "firefox";
private static string ForceFingerprint(string link, VpnProtocol protocol, string fingerprint) => /// <summary>
protocol switch /// packetEncoding=xudp через <see cref="XuiConnectionStringRequest.ForcedPacketEncoding"/> нужен
{ /// только транспорту xhttp — остальные транспорты его не используют, добавлять не за чем.
VpnProtocol.Vless or VpnProtocol.Trojan => ForceQueryFingerprint(link, fingerprint), /// </summary>
VpnProtocol.Vmess => ForceVmessFingerprint(link, fingerprint), private static bool IsXhttpTransport(string? streamSettingsJson)
_ => link,
};
private static string ForceQueryFingerprint(string link, string fingerprint)
{ {
var hashIndex = link.IndexOf('#'); if (string.IsNullOrWhiteSpace(streamSettingsJson))
var head = hashIndex >= 0 ? link[..hashIndex] : link; return false;
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;
try try
{ {
var json = Encoding.UTF8.GetString(Convert.FromBase64String(link[prefix.Length..])); using var doc = JsonDocument.Parse(streamSettingsJson);
var node = JsonNode.Parse(json)?.AsObject(); return doc.RootElement.ValueKind == JsonValueKind.Object
if (node is null || string.IsNullOrEmpty(node["tls"]?.GetValue<string>())) && doc.RootElement.TryGetProperty("network", out var network)
return link; && network.ValueKind == JsonValueKind.String
&& string.Equals(network.GetString(), "xhttp", StringComparison.OrdinalIgnoreCase);
node["fp"] = fingerprint;
var updatedJson = node.ToJsonString();
return prefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(updatedJson));
} }
catch (Exception) catch (JsonException)
{ {
// Неожиданный формат vmess-пейлоада — возвращаем ссылку как есть. return false;
return link;
} }
} }
+6 -4
View File
@@ -173,10 +173,12 @@ POST /api/configs
- Синхронизация инбаундов ноды (`SyncNodeCommandHandler`) реконсилирует пропажу инбаунда с панели: - Синхронизация инбаундов ноды (`SyncNodeCommandHandler`) реконсилирует пропажу инбаунда с панели:
без привязанных конфигов запись удаляется, с конфигами — помечается `IsAvailable=false` (детали и без привязанных конфигов запись удаляется, с конфигами — помечается `IsAvailable=false` (детали и
инварианты — [domain-model.md](domain-model.md#inbound--прокси-inbound-на-ноде)). инварианты — [domain-model.md](domain-model.md#inbound--прокси-inbound-на-ноде)).
- `BuildConnectionStringAsync` поверх ссылки из `ThreeXui.Net` принудительно подставляет - `BuildConnectionStringAsync` передаёт в `ThreeXui.Net` (`XuiConnectionStringRequest.ForcedFingerprint:
`fp=firefox` (TLS-fingerprint клиента) для tls/reality-ссылок vless/trojan/vmess, независимо "firefox"`) принудительный TLS-fingerprint клиента для tls/reality-ссылок vless/trojan/vmess,
от того, что задано в `streamSettings` ноды; shadowsocks (без TLS) и ссылки без security не независимо от того, что задано в `streamSettings` ноды; shadowsocks (без TLS) и ссылки без security
трогает. Правится в одном месте — применяется и к сайту, и к боту, и к подписке. библиотека не трогает. Для транспорта `xhttp` дополнительно передаётся
`ForcedPacketEncoding: "xudp"` (не 3x-ui-настройка, а требование части клиентских приложений).
Правится в одном месте — применяется и к сайту, и к боту, и к подписке.
- **Дрейф с 3x-ui активно не реконсилируется**: `TrafficSyncService` при недоступной ноде или - **Дрейф с 3x-ui активно не реконсилируется**: `TrafficSyncService` при недоступной ноде или
при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо