Implement forced TLS fingerprinting for specific VPN protocols
- Added functionality to enforce a default TLS fingerprint ("firefox") for vless, trojan, and vmess protocols when not explicitly set in the stream settings.
- Introduced methods to modify the connection string to include the fingerprint for TLS/reality links, ensuring compatibility with existing configurations.
- Updated documentation to reflect the new behavior regarding TLS fingerprint handling in the architecture overview.
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
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;
|
||||||
@@ -221,7 +223,9 @@ internal sealed class XuiPanelGateway(
|
|||||||
remoteInbound
|
remoteInbound
|
||||||
);
|
);
|
||||||
|
|
||||||
return Result.Success(builder.Build(request));
|
return Result.Success(
|
||||||
|
ForceFingerprint(builder.Build(request), inbound.Protocol, ForcedFingerprint)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -234,6 +238,70 @@ internal sealed class XuiPanelGateway(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Клиентские приложения выдают TLS-fingerprint панели, если fp не задан явно в
|
||||||
|
/// share-ссылке — подставляем "firefox" поверх того, что вернул ThreeXui.Net
|
||||||
|
/// (сам параметр берётся из streamSettings ноды, per-nod настройки не даём).
|
||||||
|
/// Применимо только к tls/reality-ссылкам (vless/trojan/vmess); shadowsocks TLS
|
||||||
|
/// не использует.
|
||||||
|
/// </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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Неожиданный формат vmess-пейлоада — возвращаем ссылку как есть.
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Result<IReadOnlyDictionary<string, ClientTrafficInfo>>> GetClientTrafficAsync(
|
public async Task<Result<IReadOnlyDictionary<string, ClientTrafficInfo>>> GetClientTrafficAsync(
|
||||||
Node node,
|
Node node,
|
||||||
string inboundRemoteId,
|
string inboundRemoteId,
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ POST /api/configs
|
|||||||
к ноде. Cookie-session и авто-переавторизация на 401 обеспечиваются самой `ThreeXui.Net`.
|
к ноде. Cookie-session и авто-переавторизация на 401 обеспечиваются самой `ThreeXui.Net`.
|
||||||
- Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу.
|
- Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу.
|
||||||
- Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды).
|
- Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды).
|
||||||
|
- `BuildConnectionStringAsync` поверх ссылки из `ThreeXui.Net` принудительно подставляет
|
||||||
|
`fp=firefox` (TLS-fingerprint клиента) для tls/reality-ссылок vless/trojan/vmess, независимо
|
||||||
|
от того, что задано в `streamSettings` ноды; shadowsocks (без TLS) и ссылки без security не
|
||||||
|
трогает. Правится в одном месте — применяется и к сайту, и к боту, и к подписке.
|
||||||
- **Дрейф с 3x-ui активно не реконсилируется**: `TrafficSyncService` при недоступной ноде или
|
- **Дрейф с 3x-ui активно не реконсилируется**: `TrafficSyncService` при недоступной ноде или
|
||||||
при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле
|
при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле
|
||||||
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо
|
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо
|
||||||
|
|||||||
Reference in New Issue
Block a user