mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-14 08:24:59 +03:00
Compare commits
10 Commits
7.23.1
...
e1a5c360ac
| Author | SHA1 | Date | |
|---|---|---|---|
| e1a5c360ac | |||
| 0c570fcb38 | |||
| a236fab27c | |||
| f8e1186095 | |||
| ca186e2636 | |||
| 1e8ba9ad9b | |||
| 31e32f174e | |||
| bf376a8fac | |||
| 5a9a9b7ab1 | |||
| e7a9aa66dc |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>7.23.1</Version>
|
<Version>7.23.2</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Generated
+10
-1
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user