Compare commits

...

10 Commits

Author SHA1 Message Date
2dust e1a5c360ac Optimize timeout logic, try fix
https://github.com/2dust/v2rayN/issues/9681
2026-07-06 11:49:54 +08:00
2dust 0c570fcb38 Add Legacy Protect Tip 2026-07-06 10:18:31 +08:00
2dust a236fab27c up 7.23.2 2026-07-04 16:48:24 +08:00
2dust f8e1186095 Bug fix and Optimize profile items query with batching and ordering
Prevent SQLite variables limit issue by implementing chunked/batched queries.
2026-07-02 10:35:17 +08:00
Жора Змейкин ca186e2636 fix: clarify bind interface availability (#9660) 2026-07-02 09:21:59 +08:00
JieXu 1e8ba9ad9b Update README.md (#9654)
* Update README.md

* Update build-linux.yml

* Update build.yml

* Update Directory.Packages.props
2026-07-02 09:20:46 +08:00
DHR60 31e32f174e Fix (#9648) 2026-07-02 09:20:14 +08:00
dr mike bf376a8fac fix(hysteria2): emit https server_url for sing-box realm outbound (#9642)
sing-box requires realm.server_url as a full URL (https://host:port).
Bare host:port caused "missing host in realm server_url" on import.
Also forward stun_servers and clear ResolveRealm success message.

Fixes #9635
2026-06-28 11:05:41 +08:00
DHR60 5a9a9b7ab1 Wait For Proxy Port (#9640)
Co-authored-by: MrTeeett <mrteeett@gmail.com>
Co-authored-by: drownrat <drownrat@users.noreply.github.com>
2026-06-28 09:44:34 +08:00
dependabot[bot] e7a9aa66dc Bump actions/setup-dotnet from 5.3.0 to 5.4.0 (#9636)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.3.0 to 5.4.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.3.0...v5.4.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-27 10:40:34 +08:00
29 changed files with 279 additions and 67 deletions
+2 -2
View File
@@ -214,7 +214,7 @@ jobs:
QCOW2_IMAGE: debian13-loong64.qcow2 QCOW2_IMAGE: debian13-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.3 QEMU_VERSION: 10.2.4
steps: steps:
- name: Prepare host tools - name: Prepare host tools
shell: bash shell: bash
@@ -364,7 +364,7 @@ jobs:
QCOW2_IMAGE: fedora43-loong64.qcow2 QCOW2_IMAGE: fedora43-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.3 QEMU_VERSION: 10.2.4
steps: steps:
- name: Prepare host tools - name: Prepare host tools
shell: bash shell: bash
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
dotnet --list-sdks 2>$null; $LASTEXITCODE=0 dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET 10.0.1xx - name: Setup .NET 10.0.1xx
uses: actions/setup-dotnet@v5.3.0 uses: actions/setup-dotnet@v5.4.0
with: with:
dotnet-version: 10.0.1xx dotnet-version: 10.0.1xx
+4 -1
View File
@@ -22,6 +22,7 @@ jobs:
case( case(
inputs.target == 'macos', 'macos-latest', inputs.target == 'macos', 'macos-latest',
inputs.target == 'linux', 'ubuntu-24.04', inputs.target == 'linux', 'ubuntu-24.04',
inputs.target == 'windows', 'windows-latest',
'ubuntu-latest' 'ubuntu-latest'
) )
}} }}
@@ -68,15 +69,17 @@ jobs:
dotnet --list-sdks 2>$null; $LASTEXITCODE=0 dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v5.3.0 uses: actions/setup-dotnet@v5.4.0
with: with:
dotnet-version: '10.0.1xx' dotnet-version: '10.0.1xx'
- name: Build v2rayN - name: Build v2rayN
shell: bash
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
- name: Build AmazTool - name: Build AmazTool
shell: bash
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
fetch-depth: '0' fetch-depth: '0'
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v5.3.0 uses: actions/setup-dotnet@v5.4.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
+7
View File
@@ -23,6 +23,13 @@ Download the latest release here:
[https://github.com/2dust/v2rayN/releases](https://github.com/2dust/v2rayN/releases) [https://github.com/2dust/v2rayN/releases](https://github.com/2dust/v2rayN/releases)
> [!TIP]
> v2rayN is the desktop version. For the mobile version, please visit the v2rayNG \
> v2rayN 是电脑版,手机版请访问 v2rayNG
>
> https://github.com/2dust/v2rayNG
--- ---
## Documentation / 使用文档 ## Documentation / 使用文档
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.23.1</Version> <Version>7.23.2</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
+3 -3
View File
@@ -7,8 +7,8 @@
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.17" /> <PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.17" /> <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.18" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" /> <PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
@@ -27,7 +27,7 @@
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.3" /> <PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" /> <PackageVersion Include="sqlite-net-e" Version="1.11.0" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.2" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" /> <PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
@@ -1,8 +1,10 @@
using AwesomeAssertions; using AwesomeAssertions;
using ServiceLib.Common; using ServiceLib.Common;
using ServiceLib.Enums; using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager; using ServiceLib.Manager;
using ServiceLib.Models; using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig; using ServiceLib.Services.CoreConfig;
using Xunit; using Xunit;
@@ -557,4 +559,37 @@ public class CoreConfigSingboxServiceTests
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global)); cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct)); cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
} }
[Fact]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{
var shareLink =
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreTypeItem =
[
new CoreTypeItem { ConfigType = EConfigType.Hysteria2, CoreType = ECoreType.sing_box }
];
CoreConfigTestFactory.BindAppManagerConfig(config);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2");
proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public");
proxy.realm.realm_id.Should().Be("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull();
}
} }
@@ -1,4 +1,6 @@
using AwesomeAssertions; using AwesomeAssertions;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models.Dto;
using Xunit; using Xunit;
namespace ServiceLib.Tests.Fmt; namespace ServiceLib.Tests.Fmt;
@@ -58,4 +60,32 @@ public class HyRealmTests
uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com"); uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
uri.Should().EndWith("#remark"); uri.Should().EndWith("#remark");
} }
[Fact]
public void ToServerUrl_ShouldIncludeSchemeForSingbox()
{
var realm = new HyRealm(
IsHttp: false,
Token: "public",
RendezvousHost: "realm.hy2.io",
RendezvousPort: 443,
RealmName: "my-realm-id",
StunList: ["turn.cloudflare.com:3478"]
);
realm.ToServerUrl().Should().Be("https://realm.hy2.io:443");
}
[Fact]
public void ResolveRealm_Issue9635_ShouldProduceHttpsServerUrl()
{
var str = "hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var resolved = Hysteria2Fmt.ResolveRealm(str, out _);
resolved.Should().NotBeNull();
HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
realm!.ToServerUrl().Should().StartWith("https://");
realm.ToServerUrl().Should().Contain("realm.hy2.io");
realm.StunList.Should().Contain("turn.cloudflare.com:3478");
}
} }
+1
View File
@@ -92,6 +92,7 @@ public class Global
public const string LinuxBash = "/bin/bash"; public const string LinuxBash = "/bin/bash";
public const string StringTrue = "true"; public const string StringTrue = "true";
public const string StringFalse = "false"; public const string StringFalse = "false";
public const int SqliteMaxBatchSize = 10000;
public const string SingboxDirectDNSTag = "direct_dns"; public const string SingboxDirectDNSTag = "direct_dns";
public const string SingboxRemoteDNSTag = "remote_dns"; public const string SingboxRemoteDNSTag = "remote_dns";
@@ -38,7 +38,7 @@ public static class ConnectionHandler
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
responseTime = await GetRealPingTime(webProxy, 10); responseTime = await GetRealPingTime(webProxy);
if (responseTime > 0) if (responseTime > 0)
{ {
break; break;
@@ -66,7 +66,7 @@ public static class ConnectionHandler
/// <summary> /// <summary>
/// Measures response time by sending HTTP requests through proxy. /// Measures response time by sending HTTP requests through proxy.
/// </summary> /// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout) public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9)
{ {
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1; var responseTime = -1;
@@ -77,7 +77,8 @@ public static class ConnectionHandler
using var client = new HttpClient(new SocketsHttpHandler() using var client = new HttpClient(new SocketsHttpHandler()
{ {
Proxy = webProxy, Proxy = webProxy,
UseProxy = webProxy != null UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(3)
}); });
List<int> oneTime = []; List<int> oneTime = [];
@@ -110,6 +110,7 @@ public class Hysteria2Fmt : BaseFmt
Hy2RealmUrl = realm.ToUri(), Hy2RealmUrl = realm.ToUri(),
}); });
msg = string.Empty;
return item; return item;
} }
+22 -16
View File
@@ -14,6 +14,8 @@ public class DownloaderHelper
return null; return null;
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
Uri uri = new(url); Uri uri = new(url);
//Authorization Header //Authorization Header
var headers = new WebHeaderCollection(); var headers = new WebHeaderCollection();
@@ -27,12 +29,12 @@ public class DownloaderHelper
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
Headers = headers, Headers = headers,
UserAgent = userAgent, UserAgent = userAgent,
ConnectTimeout = timeout * 1000, ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };
await using var downloader = new Downloader.DownloadService(downloadOpt); await using var downloader = new Downloader.DownloadService(downloadOpt);
@@ -45,7 +47,9 @@ public class DownloaderHelper
}; };
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
using StreamReader reader = new(stream); using StreamReader reader = new(stream);
downloadOpt = null; downloadOpt = null;
@@ -60,15 +64,16 @@ public class DownloaderHelper
throw new ArgumentNullException(nameof(url)); throw new ArgumentNullException(nameof(url));
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
ConnectTimeout= timeout * 1000, ConnectTimeout= connectTimeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };
var lastUpdateTime = DateTime.Now; var lastUpdateTime = DateTime.Now;
@@ -116,7 +121,7 @@ public class DownloaderHelper
}; };
//progress.Report("......"); //progress.Report("......");
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
cts.CancelAfter(timeout * 1000); cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token); await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
downloadOpt = null; downloadOpt = null;
@@ -137,15 +142,16 @@ public class DownloaderHelper
File.Delete(fileName); File.Delete(fileName);
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
ConnectTimeout= timeout * 1000, ConnectTimeout= connectTimeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };
var progressPercentage = 0; var progressPercentage = 0;
+23 -13
View File
@@ -244,9 +244,26 @@ public sealed class AppManager
{ {
return []; return [];
} }
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId)) if (ids.Count <= Global.SqliteMaxBatchSize)
.ToListAsync(); {
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId))
.ToListAsync();
}
var items = new List<ProfileItem>();
for (var size = 0; size < ids.Count; size += Global.SqliteMaxBatchSize)
{
var chunk = ids.Skip(size).Take(Global.SqliteMaxBatchSize).ToList();
var chunkItems = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => chunk.Contains(it.IndexId))
.ToListAsync();
items.AddRange(chunkItems);
}
return items;
} }
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds) public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
@@ -257,18 +274,11 @@ public sealed class AppManager
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds) public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
{ {
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList(); var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (idList.Count == 0) var items = await GetProfileItemsByIndexIds(ids);
{
return [];
}
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => idList.Contains(it.IndexId))
.ToListAsync();
var itemMap = items.ToDictionary(it => it.IndexId); var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(itemMap.GetValueOrDefault) return ids.Select(itemMap.GetValueOrDefault)
.Where(item => item != null) .Where(item => item != null)
.ToList(); .ToList();
} }
+70
View File
@@ -90,6 +90,7 @@ public class CoreManager
} }
await CoreStart(mainContext); await CoreStart(mainContext);
await WaitForProxyPort(preContext);
await CoreStartPreService(preContext); await CoreStartPreService(preContext);
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType; AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
@@ -213,6 +214,75 @@ public class CoreManager
await _updateFunc?.Invoke(notify, msg); await _updateFunc?.Invoke(notify, msg);
} }
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
{
if (preContext is null)
{
return;
}
if (!preContext.AppConfig.TunModeItem.EnableTun)
{
return;
}
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
var rootToken = rootCts.Token;
var port = preContext.Node.Port;
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
var buf = new byte[2];
while (!rootToken.IsCancellationRequested)
{
using var tcp = new TcpClient();
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
var linkedToken = linkedCts.Token;
try
{
await tcp.ConnectAsync(Global.Loopback, port, linkedToken);
var stream = tcp.GetStream();
await stream.WriteAsync(greeting, linkedToken);
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
if (read == 2 && buf[0] == 0x05)
{
return;
}
}
catch (OperationCanceledException)
{
if (!rootToken.IsCancellationRequested)
{
continue;
}
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
return;
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
// Connection refused, proxy not ready yet, wait 50ms before retrying
try
{
await Task.Delay(50, rootToken);
}
catch (OperationCanceledException)
{
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
return;
}
}
catch
{
// Ignore other exceptions and continue
}
}
}
#endregion Private #endregion Private
#region Process #region Process
@@ -257,6 +257,7 @@ public class HyRealm4Sbox
public string? server_url { get; set; } public string? server_url { get; set; }
public string? token { get; set; } public string? token { get; set; }
public string? realm_id { get; set; } public string? realm_id { get; set; }
public List<string>? stun_servers { get; set; }
} }
public class Server4Sbox : BaseServer4Sbox public class Server4Sbox : BaseServer4Sbox
@@ -289,16 +289,7 @@ public class BalancersStrategySettings4Ray
{ {
public int? expected { get; set; } public int? expected { get; set; }
public string? maxRTT { get; set; } public string? maxRTT { get; set; }
public float? tolerance { get; set; } public double? tolerance { get; set; }
public List<string>? baselines { get; set; }
public List<BalancersStrategySettingsCosts4Ray>? costs { get; set; }
}
public class BalancersStrategySettingsCosts4Ray
{
public bool? regexp { get; set; }
public string? match { get; set; }
public float? value { get; set; }
} }
public class Observatory4Ray public class Observatory4Ray
+9
View File
@@ -15,6 +15,15 @@ public record HyRealm(
{ {
public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost; public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost;
/// <summary>
/// sing-box realm.server_url requires a scheme (https:// or http://).
/// </summary>
public string ToServerUrl()
{
var scheme = IsHttp ? "http" : "https";
return $"{scheme}://{RendezvousHostPort}";
}
public static bool TryParse(string? str, out HyRealm? realm) public static bool TryParse(string? str, out HyRealm? realm)
{ {
realm = null; realm = null;
+10 -1
View File
@@ -3339,6 +3339,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 If enabled, use sing-box TUN; otherwise, use xray TUN. 的本地化字符串。
/// </summary>
public static string TbLegacyProtectTip {
get {
return ResourceManager.GetString("TbLegacyProtectTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Address (IPv4, IPv6) 的本地化字符串。 /// 查找类似 Address (IPv4, IPv6) 的本地化字符串。
/// </summary> /// </summary>
@@ -3862,7 +3871,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode 的本地化字符串。 /// 查找类似 For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled. 的本地化字符串。
/// </summary> /// </summary>
public static string TbSettingsBindInterfaceTip { public static string TbSettingsBindInterfaceTip {
get { get {
+4 -1
View File
@@ -1750,7 +1750,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Bind Interface</value> <value>Bind Interface</value>
</data> </data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve"> <data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value> <value>For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled.</value>
</data> </data>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value> <value>PreSharedKey</value>
@@ -1812,4 +1812,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbGeckoPacketSize" xml:space="preserve"> <data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko Packet Size (min/max)</value> <value>Gecko Packet Size (min/max)</value>
</data> </data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>If enabled, use sing-box TUN; otherwise, use xray TUN.</value>
</data>
</root> </root>
+1 -1
View File
@@ -1747,7 +1747,7 @@
<value>Привязать интерфейс</value> <value>Привязать интерфейс</value>
</data> </data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve"> <data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value> <value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value>
</data> </data>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>Общий ключ (PSK)</value> <value>Общий ключ (PSK)</value>
@@ -1809,4 +1809,7 @@
<data name="TbGeckoPacketSize" xml:space="preserve"> <data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko 包大小 (最小/最大)</value> <value>Gecko 包大小 (最小/最大)</value>
</data> </data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>启用则使用 sing-box TUN ,否则使用 xray TUN</value>
</data>
</root> </root>
@@ -1794,4 +1794,7 @@
<data name="TbEnabletDnsViaProxy" xml:space="preserve"> <data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS 透過 Bridge</value> <value>DNS 透過 Bridge</value>
</data> </data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value>
</data>
</root> </root>
@@ -275,9 +275,10 @@ public partial class CoreConfigSingboxService
{ {
var realm4Sbox = new HyRealm4Sbox() var realm4Sbox = new HyRealm4Sbox()
{ {
server_url = realm.RendezvousHostPort, server_url = realm.ToServerUrl(),
token = realm.Token, token = realm.Token,
realm_id = realm.RealmName, realm_id = realm.RealmName,
stun_servers = realm.StunList?.Count > 0 ? realm.StunList : null,
}; };
outbound.realm = realm4Sbox; outbound.realm = realm4Sbox;
outbound.server = null; outbound.server = null;
@@ -89,22 +89,25 @@ public partial class CoreConfigV2rayService
EMultipleLoad.RoundRobin => "roundRobin", EMultipleLoad.RoundRobin => "roundRobin",
EMultipleLoad.LeastPing => "leastPing", EMultipleLoad.LeastPing => "leastPing",
EMultipleLoad.LeastLoad => "leastLoad", EMultipleLoad.LeastLoad => "leastLoad",
EMultipleLoad.Fallback => "leastLoad",
_ => "roundRobin", _ => "roundRobin",
}; };
var balancerTag = $"{selector}{Global.BalancerTagSuffix}"; var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
var balancer = new BalancersItem4Ray var balancer = new BalancersItem4Ray
{ {
selector = [selector], selector = [selector],
strategy = new() strategy = strategyType == "leastLoad" ? new()
{ {
type = strategyType, type = strategyType,
settings = new() settings = new()
{ {
expected = 1, expected = 1,
tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null,
maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null,
}, },
}, } : null,
tag = balancerTag, tag = balancerTag,
fallbackTag = multipleLoad == EMultipleLoad.Fallback ? Global.DirectTag : null, fallbackTag = _coreConfig.outbounds?.FirstOrDefault(o => o.tag.StartsWith(selector))?.tag,
}; };
_coreConfig.routing.balancers ??= []; _coreConfig.routing.balancers ??= [];
_coreConfig.routing.balancers.Add(balancer); _coreConfig.routing.balancers.Add(balancer);
+17 -7
View File
@@ -109,9 +109,10 @@ public class DownloadService
/// </summary> /// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent) public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
{ {
var timeout = 15;
try try
{ {
var result1 = await DownloadStringAsync(url, webProxy, userAgent, 15); var result1 = await DownloadStringAsync(url, webProxy, userAgent, timeout);
if (result1.IsNotEmpty()) if (result1.IsNotEmpty())
{ {
return result1; return result1;
@@ -129,7 +130,7 @@ public class DownloadService
try try
{ {
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, 15); var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, timeout);
if (result2.IsNotEmpty()) if (result2.IsNotEmpty())
{ {
return result2; return result2;
@@ -155,11 +156,18 @@ public class DownloadService
{ {
try try
{ {
var client = new HttpClient(new SocketsHttpHandler() var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var handler = new SocketsHttpHandler
{ {
Proxy = webProxy, Proxy = webProxy,
UseProxy = webProxy != null UseProxy = webProxy != null,
}); ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
};
using var client = new HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
if (userAgent.IsNullOrEmpty()) if (userAgent.IsNullOrEmpty())
{ {
@@ -175,8 +183,9 @@ public class DownloadService
} }
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
var result = await client.GetStringAsync(url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); cts.CancelAfter(TimeSpan.FromSeconds(timeout));
return result;
return await client.GetStringAsync(url, cts.Token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -187,6 +196,7 @@ public class DownloadService
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
} }
} }
return null; return null;
} }
@@ -420,7 +420,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<int> DoRealPing(ServerTestItem it) private async Task<int> DoRealPing(ServerTestItem it)
{ {
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, 10); var responseTime = await ConnectionHandler.GetRealPingTime(webProxy);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
@@ -1022,6 +1022,13 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbLegacyProtectTip}"
TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="9" Grid.Row="9"
@@ -1285,6 +1285,14 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbLegacyProtectTip}"
TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="9" Grid.Row="9"