Compare commits

...

19 Commits

Author SHA1 Message Date
2dust
af0eb9ed14 Code clean 2026-08-22 17:32:26 +08:00
DHR60
0436081436 Revert "fix(tun): automatically exclude proxy node IPs and resolved domains from TUN routing to prevent loops (#9974) (#10012)" (#10013)
This reverts commit 2be63d1655.
2026-08-22 15:02:24 +08:00
2dust
2a89a2e597 up 7.24.8 2026-08-22 15:01:25 +08:00
Mangoo
2be63d1655 fix(tun): automatically exclude proxy node IPs and resolved domains from TUN routing to prevent loops (#9974) (#10012) 2026-08-22 14:49:27 +08:00
DHR60
74bd28bbd0 Fix test (#10007) 2026-08-22 10:25:57 +08:00
DHR60
be501cbd2a Fix dependabot path (#10008) 2026-08-22 10:20:41 +08:00
Miheichev Aleksandr Sergeevich
e20b4dbfab chore(deps): update CliWrap and TUnit, drop unused test pins (#10005)
* chore(deps): update CliWrap and TUnit

- CliWrap                  3.10.4      -> 3.10.5
- TUnit                    1.65.0      -> 1.65.38
- TUnit.Assertions.Should  1.65.0-beta -> 1.65.38-beta

TUnit.Assertions.Should is versioned in lockstep with TUnit and published with
a -beta suffix. Its 1.65.0-beta package depends on TUnit.Assertions 1.65.0-beta,
so it has to move together with TUnit to keep the whole TUnit stack resolved on
a single version (TUnit.Assertions 1.65.38 after this change).

* chore(deps): drop unused AwesomeAssertions and xunit.v3 pins

Since the migration to TUnit (c9e843aa) no project references AwesomeAssertions
or xunit.v3 any more, so these two PackageVersion entries no longer pin
anything: neither package appears in any project.assets.json, directly or
transitively.

Verified by restoring, building and publishing the whole solution with and
without the two entries: the resolved package graph (769 entries across all
projects) and the published output of every CI target - v2rayN win-x64,
v2rayN.Desktop win-x64 and linux-x64, AmazTool win-x64, all self-contained -
are byte-identical, and ServiceLib.Tests stays at 69/69.
2026-08-22 10:19:29 +08:00
DHR60
728cbe70f2 Add block AAAA query support (#10004) 2026-08-22 10:18:48 +08:00
DHR60
26a86f9bd2 Fix (#10002) 2026-08-22 10:17:09 +08:00
DHR60
c66c55b05e Fix dnd (#10003)
* Fix dnd

* Fix
2026-08-22 10:13:21 +08:00
DHR60
ebb4bd5daa Optimize file download (#9952)
* Optimize file download

* Optimize func call
2026-08-21 14:15:54 +08:00
dayepao
14e27f3d23 fix: preserve Mihomo Reality short-id as YAML string (#9964) 2026-08-20 10:47:24 +08:00
DHR60
2d514ff480 Fix (#9991) 2026-08-19 13:48:59 +08:00
DHR60
11bd52b6a5 Fix (#9987) 2026-08-19 09:56:38 +08:00
JieXu
3b3fe8def3 Update build.yml (#9986)
* Update build.yml

* Update build-windows-x86.yml
2026-08-19 09:56:21 +08:00
DHR60
c9e843aa3b TUnit (#9981) 2026-08-19 09:54:51 +08:00
DHR60
7be264c65c Log with caller (#9976) 2026-08-17 14:33:33 +08:00
Miheichev Aleksandr Sergeevich
5e6660aeee chore(deps): update xunit.v3 and NLog, and drop the VSTest packages xunit replaces (#9975)
* chore(deps): update xunit.v3 to 4.0.0 and drop the VSTest packages

xunit.v3 4.0.0 moves from Microsoft.Testing.Platform v1 to v2, and MTP v2 drops the VSTest bridge on the .NET 10 SDK, so anything routed through VSTest now fails before a single test runs.

Rather than bridging back to VSTest, the two VSTest-era packages are removed. The test project is already an executable carrying xunit's own in-process runner, so Microsoft.NET.Test.Sdk and xunit.runner.visualstudio have nothing left to contribute, and without them no opt-in file is needed anywhere.

No source or test changes are required: every 4.0.0 breaking change is in the extensibility and runner APIs, and the suite uses only [Fact], [Theory] and [InlineData].

* ci: run the tests directly and let versions float on their major

test.yml requested the 8.0.x SDK while every project targets net10.0, which an 8.0 SDK cannot build (NETSDK1045), and it invoked dotnet test, which needs the VSTest bridge that MTP v2 has dropped. It now runs the test executable, which needs no adapter and no test SDK.

All three setup-dotnet steps ask for 10.x with quality ga, so a new .NET 10 patch or feature band is picked up automatically while previews and release candidates stay out of builds. setup-dotnet and upload-artifact were the only actions pinned to an exact patch; they now track their major tag like the other seven.

* chore(deps): update NLog to 6.2.0

A minor release with no API change on the surface this project uses. Verified beyond compilation: Logging.Setup builds its FileTarget, and both SaveLog overloads write through it at runtime with the expected layout.
2026-08-17 14:33:18 +08:00
JieXu
5b27f4836f Update package-rhel-riscv.sh (#9971)
* Update package-rhel-riscv.sh

* Update package-rhel-loong.sh

* Update package-debian-riscv.sh

* Update package-debian-loong.sh
2026-08-16 10:08:08 +08:00
62 changed files with 1038 additions and 683 deletions

View File

@@ -6,6 +6,6 @@ updates:
interval: "daily" interval: "daily"
- package-ecosystem: "nuget" - package-ecosystem: "nuget"
directory: "/" directory: "/v2rayN"
schedule: schedule:
interval: "daily" interval: "daily"

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@v6.0.0 uses: actions/setup-dotnet@v6
with: with:
dotnet-version: 10.0.1xx dotnet-version: 10.0.1xx

View File

@@ -69,10 +69,10 @@ 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@v6.0.0 uses: actions/setup-dotnet@v6
with: with:
dotnet-version: '10.0.1xx' dotnet-version: 10.0.1xx
- name: Build v2rayN - name: Build v2rayN
shell: bash shell: bash
working-directory: ./v2rayN working-directory: ./v2rayN
@@ -89,7 +89,7 @@ jobs:
find ${{ matrix.arch }} -type f -name '*.pdb' -delete find ${{ matrix.arch }} -type f -name '*.pdb' -delete
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v7.0.1 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.arch }} name: ${{ matrix.arch }}
path: ${{ matrix.arch }} path: ${{ matrix.arch }}

View File

@@ -9,6 +9,10 @@ on:
- 'v2rayN/ServiceLib/Handler/Fmt/**' - 'v2rayN/ServiceLib/Handler/Fmt/**'
- '.github/workflows/test.yml' - '.github/workflows/test.yml'
permissions:
checks: write
pull-requests: write
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -20,10 +24,17 @@ jobs:
fetch-depth: '0' fetch-depth: '0'
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v6.0.0 uses: actions/setup-dotnet@v6
with: with:
dotnet-version: '8.0.x' dotnet-version: '10.x'
dotnet-quality: 'ga'
- name: Test Code - name: Test Code
working-directory: ./v2rayN run: dotnet test --project ./v2rayN/ServiceLib.Tests -c Release --results-directory ./TestResults -- --report-trx
run: dotnet test ./ServiceLib.Tests
- name: Comment PR with results
if: always()
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: ./TestResults/*.trx
comment_mode: failures

5
global.json Normal file
View File

@@ -0,0 +1,5 @@
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}

View File

@@ -13,8 +13,8 @@ PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0" DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.110" DOTNET_LOONGARCH_VERSION="10.0.111"
DOTNET_LOONGARCH_TAG="v10.0.110-loongarch64" DOTNET_LOONGARCH_TAG="v10.0.111-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_RISCV_VERSION="10.0.110" DOTNET_RISCV_VERSION="10.0.111"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"

View File

@@ -12,8 +12,8 @@ MIN_KERNEL="6.12"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild" RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_LOONGARCH_VERSION="10.0.110" DOTNET_LOONGARCH_VERSION="10.0.111"
DOTNET_LOONGARCH_TAG="v10.0.110-loongarch64" DOTNET_LOONGARCH_TAG="v10.0.111-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild" RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_RISCV_VERSION="10.0.110" DOTNET_RISCV_VERSION="10.0.111"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.24.7</Version> <Version>7.24.8</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -9,13 +9,11 @@
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" /> <PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
<PackageVersion Include="CliWrap" Version="3.10.4" /> <PackageVersion Include="CliWrap" Version="3.10.5" />
<PackageVersion Include="Downloader" Version="5.9.5" /> <PackageVersion Include="Downloader" Version="5.9.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" /> <PackageVersion Include="QRCoder" Version="1.8.0" />
@@ -25,15 +23,15 @@
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" /> <PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
<PackageVersion Include="NLog" Version="6.1.4" /> <PackageVersion Include="NLog" Version="6.2.0" />
<PackageVersion Include="sqlite-net-e" Version="1.11.285" /> <PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="TUnit" Version="1.65.38" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
<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.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" /> <PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" /> <PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,15 +1,8 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Handler.Builder;
using ServiceLib.Helper;
using ServiceLib.Models;
using Xunit;
namespace ServiceLib.Tests.CoreConfig.Context; namespace ServiceLib.Tests.CoreConfig.Context;
public class CoreConfigContextBuilderTests public class CoreConfigContextBuilderTests
{ {
[Fact] [Test]
public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError() public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError()
{ {
var config = CoreConfigTestFactory.CreateConfig(); var config = CoreConfigTestFactory.CreateConfig();
@@ -27,13 +20,13 @@ public class CoreConfigContextBuilderTests
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false); var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
validatorResult.Success.Should().BeFalse(); await validatorResult.Success.Should().BeFalse();
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg)); await validatorResult.Errors.Should().Contain(ContainsCycleDependencyMessage);
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId); await context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId); await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
} }
[Fact] [Test]
public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError() public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError()
{ {
var config = CoreConfigTestFactory.CreateConfig(); var config = CoreConfigTestFactory.CreateConfig();
@@ -53,14 +46,14 @@ public class CoreConfigContextBuilderTests
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false); var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
validatorResult.Success.Should().BeFalse(); await validatorResult.Success.Should().BeFalse();
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg)); await validatorResult.Errors.Should().Contain(ContainsCycleDependencyMessage);
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId); await context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId); await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupC.IndexId); await context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
} }
[Fact] [Test]
public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild() public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild()
{ {
var config = CoreConfigTestFactory.CreateConfig(); var config = CoreConfigTestFactory.CreateConfig();
@@ -80,14 +73,14 @@ public class CoreConfigContextBuilderTests
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false); var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
validatorResult.Success.Should().BeTrue(); await validatorResult.Success.Should().BeTrue();
validatorResult.Errors.Should().BeEmpty(); await validatorResult.Errors.Should().BeEmpty();
validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg)); await validatorResult.Warnings.Should().Contain(ContainsCycleDependencyMessage);
context.AllProxiesMap.Should().ContainKey(leaf.IndexId); await context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
context.AllProxiesMap.Should().ContainKey(groupA.IndexId); await context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId); await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
groupA.GetProtocolExtra().ChildItems.Should().Be(leaf.IndexId); await groupA.GetProtocolExtra().ChildItems.Should().BeEqualTo(leaf.IndexId);
} }
private static string NewId(string prefix) private static string NewId(string prefix)

View File

@@ -1,19 +1,9 @@
using AwesomeAssertions;
using ServiceLib.Common;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager;
using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig;
using Xunit;
namespace ServiceLib.Tests.CoreConfig.Singbox; namespace ServiceLib.Tests.CoreConfig.Singbox;
public class CoreConfigSingboxServiceTests public class CoreConfigSingboxServiceTests
{ {
[Fact] [Test]
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig() public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -22,17 +12,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull(); await result.Data.Should().NotBeNull();
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString()); var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
singboxConfig.Should().NotBeNull(); await singboxConfig.Should().NotBeNull();
singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks"); await singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed)); await singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
} }
[Fact] [Test]
public void GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound() public async Task GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -46,18 +36,18 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.inbounds.Should().Contain(i => await cfg.inbounds.Should().Contain(i =>
i.type == nameof(EInboundProtocol.mixed) i.type == nameof(EInboundProtocol.mixed)
&& i.listen == Global.Loopback && i.listen == Global.Loopback
&& i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks)); && i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks));
cfg.inbounds.Should().Contain(i => i.type == "tun"); await cfg.inbounds.Should().Contain(i => i.type == "tun");
} }
[Fact] [Test]
public void GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules() public async Task GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
{ {
// The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast). // The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast).
// They are deserialized into List<Rule4Sbox>, so a schema mismatch in the // They are deserialized into List<Rule4Sbox>, so a schema mismatch in the
@@ -75,22 +65,22 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.route.rules.Should().Contain( await cfg.route.rules.Should().Contain(
r => r.action == "reject" r => r.action == "reject"
&& r.network != null && r.network.Contains("udp") && r.network != null && r.network.Contains("udp")
&& r.port != null && r.port.Contains(5353), && r.port != null && r.port.Contains(5353),
"the embedded tun rules must reject mDNS/NetBIOS noise"); "the embedded tun rules must reject mDNS/NetBIOS noise");
cfg.route.rules.Should().Contain( await cfg.route.rules.Should().Contain(
r => r.action == "reject" r => r.action == "reject"
&& r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"), && r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"),
"the embedded tun rules must reject multicast traffic"); "the embedded tun rules must reject multicast traffic");
} }
[Fact] [Test]
public void GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses() public async Task GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
{ {
// Regression test: traffic addressed to the TUN interface's own addresses must // Regression test: traffic addressed to the TUN interface's own addresses must
// never reach an outbound. auto_route hijacks the default route, so `direct` // never reach an outbound. auto_route hijacks the default route, so `direct`
@@ -111,17 +101,19 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var tun = cfg.inbounds.First(i => i.type == "tun"); var tun = cfg.inbounds.First(i => i.type == "tun");
tun.address.Should().NotBeNullOrEmpty(); //tun.address.Should().NotBeNullOrEmpty();
await tun.address.Should().NotBeNull();
await tun.address.Should().NotBeEmpty();
foreach (var address in tun.address!) foreach (var address in tun.address!)
{ {
var self = IPAddress.Parse(address.Split('/').First()); var self = IPAddress.Parse(address.Split('/').First());
var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32; var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
var expected = $"{self}/{hostBits}"; var expected = $"{self}/{hostBits}";
cfg.route.rules.Should().Contain( await cfg.route.rules.Should().Contain(
r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(expected), r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(expected),
$"traffic to the TUN's own address '{address}' must be rejected, not routed"); $"traffic to the TUN's own address '{address}' must be rejected, not routed");
} }
@@ -131,12 +123,13 @@ public class CoreConfigSingboxServiceTests
// here leaves room for it, so a prefix match would drop system name lookups too. // here leaves room for it, so a prefix match would drop system name lookups too.
var dropRule = cfg.route.rules.First(r => var dropRule = cfg.route.rules.First(r =>
r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0); r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0);
dropRule.ip_cidr!.Should().OnlyContain(c => //dropRule.ip_cidr!.Should().OnlyContain(c =>
c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal)); // c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
await dropRule.ip_cidr.Should().All(c => c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
} }
[Fact] [Test]
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface() public async Task GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreBasicItem.BindInterface = "eth0"; config.CoreBasicItem.BindInterface = "eth0";
@@ -150,16 +143,16 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag); var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.bind_interface.Should().Be("eth0"); await proxy.bind_interface.Should().BeEqualTo("eth0");
proxy.detour.Should().BeNullOrEmpty(); await proxy.detour.Should().BeNull().Or.BeEmpty();
} }
[Fact] [Test]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector() public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -176,17 +169,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
} }
[Fact] [Test]
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain() public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -203,18 +196,18 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks"); await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => await cfg.outbounds.Should().Contain(o =>
o.tag == Global.ProxyTag && o.tag == Global.ProxyTag &&
(o.detour ?? string.Empty).StartsWith("chain-proxy-1-", StringComparison.Ordinal)); (o.detour ?? string.Empty).StartsWith("chain-proxy-1-", StringComparison.Ordinal));
} }
[Fact] [Test]
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds() public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -236,18 +229,18 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
} }
[Fact] [Test]
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches() public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -269,25 +262,25 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)); var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal));
proxyCloneCount.Should().Be(2); await proxyCloneCount.Should().BeEqualTo(2);
var allCloneDetoursPointToGroupBranches = cfg.outbounds var allCloneDetoursPointToGroupBranches = cfg.outbounds
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)) .Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
.All(o => (o.detour ?? string.Empty).StartsWith("chain-proxy-1-group-", StringComparison.Ordinal)); .All(o => (o.detour ?? string.Empty).StartsWith("chain-proxy-1-group-", StringComparison.Ordinal));
allCloneDetoursPointToGroupBranches.Should().BeTrue(); await allCloneDetoursPointToGroupBranches.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules() public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -323,24 +316,24 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasDirectRule = cfg.route.rules.Any(r => var hasDirectRule = cfg.route.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("direct.example.com") && r.domain.Contains("direct.example.com")
&& r.outbound == Global.DirectTag); && r.outbound == Global.DirectTag);
hasDirectRule.Should().BeTrue(); await hasDirectRule.Should().BeTrue();
var hasBlockRule = cfg.route.rules.Any(r => var hasBlockRule = cfg.route.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("block.example.com") && r.domain.Contains("block.example.com")
&& r.action == "reject"); && r.action == "reject");
hasBlockRule.Should().BeTrue(); await hasBlockRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound() public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -372,21 +365,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}"; var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}";
cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
var hasRouteRule = cfg.route.rules.Any(r => var hasRouteRule = cfg.route.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("route.example.com") && r.domain.Contains("route.example.com")
&& (r.outbound ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal)); && (r.outbound ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
hasRouteRule.Should().BeTrue(); await hasRouteRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule() public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
{ {
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs( var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(
ECoreType.sing_box, ECoreType.sing_box,
@@ -417,7 +410,7 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasExpectedRule = cfg.dns.rules?.Any(r => var hasExpectedRule = cfg.dns.rules?.Any(r =>
@@ -426,11 +419,11 @@ public class CoreConfigSingboxServiceTests
&& r.rule_set?.Contains("geosite-cn") == true && r.rule_set?.Contains("geosite-cn") == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false; && r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeTrue(); await hasExpectedRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver() public async Task GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
{ {
var bootstrapDns = "8.8.8.8"; var bootstrapDns = "8.8.8.8";
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns); var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns);
@@ -441,17 +434,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
config.SimpleDNSItem.BootstrapDNS.Should().Be(bootstrapDns); await config.SimpleDNSItem.BootstrapDNS.Should().BeEqualTo(bootstrapDns);
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var bootstrapServer = cfg.dns.servers?.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag); var bootstrapServer = cfg.dns?.servers.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag);
bootstrapServer.Should().NotBeNull(); await bootstrapServer.Should().NotBeNull();
(bootstrapServer?.server ?? string.Empty).Should().Contain(bootstrapDns); await bootstrapServer!.server.Should().Contain(bootstrapDns);
} }
[Fact] [Test]
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns() public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.SimpleDNSItem.DirectDNS = "1.1.1.1"; config.SimpleDNSItem.DirectDNS = "1.1.1.1";
@@ -484,14 +477,14 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.dns.final.Should().Be(Global.SingboxDirectDNSTag); await cfg.dns.final.Should().BeEqualTo(Global.SingboxDirectDNSTag);
} }
[Fact] [Test]
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule() public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
{ {
var config = var config =
CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn"); CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn");
@@ -521,21 +514,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasExpectedRule = cfg.dns.rules?.Any(r => var hasExpectedRule = cfg.dns.rules?.Any(r =>
r.server == Global.SingboxDirectDNSTag r.server == Global.SingboxDirectDNSTag
&& r.ip_cidr?.Contains("192.168.0.0/16") == true && r.ip_cidr?.Contains("192.168.0.0/16") == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false; && r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeFalse(); await hasExpectedRule.Should().BeFalse();
} }
[Theory] [Test]
[InlineData("geosite:cn", "geosite-cn")] [Arguments("geosite:cn", "geosite-cn")]
[InlineData("geosite:geolocation-cn", "geosite-geolocation-cn")] [Arguments("geosite:geolocation-cn", "geosite-geolocation-cn")]
[InlineData("geosite:tld-cn", "geosite-tld-cn")] [Arguments("geosite:tld-cn", "geosite-tld-cn")]
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag, public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag,
string expectedRuleSetTag) string expectedRuleSetTag)
{ {
var config = var config =
@@ -563,7 +556,7 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasExpectedRule = cfg.dns.rules?.Any(r => var hasExpectedRule = cfg.dns.rules?.Any(r =>
@@ -571,11 +564,11 @@ public class CoreConfigSingboxServiceTests
&& r.ip_cidr?.Contains("192.168.0.0/16") == true && r.ip_cidr?.Contains("192.168.0.0/16") == true
&& r.rule_set?.Contains(expectedRuleSetTag) == true && r.rule_set?.Contains(expectedRuleSetTag) == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false; && r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeTrue(); await hasExpectedRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver() public async Task GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1"; config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
@@ -587,21 +580,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hostsServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxHostsDNSTag); var hostsServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxHostsDNSTag);
hostsServer.Should().NotBeNull(); await hostsServer.Should().NotBeNull();
hostsServer!.predefined.Should().ContainKey("resolver.example"); await hostsServer!.predefined.Should().ContainKey("resolver.example");
hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1"); await hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1");
var directServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxDirectDNSTag); var directServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxDirectDNSTag);
directServer.Should().NotBeNull(); await directServer.Should().NotBeNull();
directServer!.domain_resolver.Should().Be(Global.SingboxHostsDNSTag); await directServer!.domain_resolver.Should().BeEqualTo(Global.SingboxHostsDNSTag);
} }
[Fact] [Test]
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver() public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -630,22 +623,22 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8"); await cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag); await cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global)); await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct)); await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
} }
[Fact] [Test]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl() public async Task GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{ {
var shareLink = 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"; "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 _); var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull(); await node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box; node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
@@ -658,22 +651,22 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag); var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2"); await proxy.type.Should().BeEqualTo("hysteria2");
proxy.realm.Should().NotBeNull(); await proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://"); await proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io"); await proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public"); await proxy.realm.token.Should().BeEqualTo("public");
proxy.realm.realm_id.Should().Be("my-realm-id"); await proxy.realm.realm_id.Should().BeEqualTo("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478"); await proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull(); await proxy.server.Should().BeNull();
} }
[Fact] [Test]
public void GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress() public async Task GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress()
{ {
// Regression test for #9820: sing-box fails with "need one more IPv6 address in // Regression test for #9820: sing-box fails with "need one more IPv6 address in
// first prefix for system stack" when the TUN inbound uses a /128 IPv6 prefix. // first prefix for system stack" when the TUN inbound uses a /128 IPv6 prefix.
@@ -691,23 +684,24 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var tun = cfg.inbounds.First(i => i.type == "tun"); var tun = cfg.inbounds.First(i => i.type == "tun");
tun.address.Should().NotBeNullOrEmpty(); await tun.address.Should().NotBeNull();
await tun.address.Should().NotBeEmpty();
foreach (var address in tun.address!) foreach (var address in tun.address!)
{ {
var prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]); var prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]);
var isIpv6 = address.Contains(':'); var isIpv6 = address.Contains(':');
prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30, await prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
$"'{address}' must leave room for the peer address the system stack derives"); $"'{address}' must leave room for the peer address the system stack derives");
} }
} }
[Fact] [Test]
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson() public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -728,17 +722,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull(); await result.Data.Should().NotBeNull();
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString()); var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
cfg.Should().NotBeNull(); await cfg.Should().NotBeNull();
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag); var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
proxyOutbound.Should().NotBeNull(); await proxyOutbound.Should().NotBeNull();
proxyOutbound!.type.Should().Be("shadowsocks"); await proxyOutbound!.type.Should().BeEqualTo("shadowsocks");
proxyOutbound.server.Should().Be("1.2.3.4"); await proxyOutbound.server.Should().BeEqualTo("1.2.3.4");
proxyOutbound.server_port.Should().Be(8388); await proxyOutbound.server_port.Should().BeEqualTo(8388);
proxyOutbound.method.Should().Be("aes-128-gcm"); await proxyOutbound.method.Should().BeEqualTo("aes-128-gcm");
proxyOutbound.password.Should().Be("custom_password"); await proxyOutbound.password.Should().BeEqualTo("custom_password");
} }
} }

View File

@@ -1,16 +1,9 @@
using AwesomeAssertions;
using ServiceLib.Common;
using ServiceLib.Enums;
using ServiceLib.Models;
using ServiceLib.Services.CoreConfig;
using Xunit;
namespace ServiceLib.Tests.CoreConfig.V2ray; namespace ServiceLib.Tests.CoreConfig.V2ray;
public class CoreConfigV2rayServiceTests public class CoreConfigV2rayServiceTests
{ {
[Fact] [Test]
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig() public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -19,17 +12,17 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
result.Data.Should().NotBeNull(); await result.Data.Should().NotBeNull();
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString()); var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
v2rayConfig.Should().NotBeNull(); await v2rayConfig.Should().NotBeNull();
v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess"); await v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess");
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed)); await v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
} }
[Fact] [Test]
public void GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings() public async Task GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -42,27 +35,27 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http"); var outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http");
outbound.settings.address?.ToString().Should().Be("proxy.example.com"); await outbound.settings.address!.ToString().Should().BeEqualTo("proxy.example.com");
outbound.settings.port.Should().Be(8080); await outbound.settings.port.Should().BeEqualTo(8080);
outbound.settings.user.Should().Be("user"); await outbound.settings.user.Should().BeEqualTo("user");
outbound.settings.pass.Should().Be("pass"); await outbound.settings.pass.Should().BeEqualTo("pass");
outbound.settings.level.Should().Be(1); await outbound.settings.level.Should().BeEqualTo(1);
outbound.settings.headers.Should().NotBeNull(); await outbound.settings.headers.Should().NotBeNull();
var headers = JsonUtils.ParseJson(outbound.settings.headers.ToString()); var headers = JsonUtils.ParseJson(outbound.settings.headers!.ToString());
headers["User-Agent"]!.GetValue<string>().Should().Be("v2rayN"); await headers["User-Agent"]!.GetValue<string>().Should().BeEqualTo("v2rayN");
headers["Set-Cookie"]!.AsArray() await headers["Set-Cookie"]!.AsArray()
.Select(item => item!.GetValue<string>()) .Select(item => item!.GetValue<string>())
.Should().Equal("a=1", "b=2"); .Should().BeEquivalentTo(["a=1", "b=2"]);
outbound.settings.servers.Should().BeNull(); await outbound.settings.servers.Should().BeNull();
outbound.settings.vnext.Should().BeNull(); await outbound.settings.vnext.Should().BeNull();
} }
[Fact] [Test]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer() public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -79,17 +72,17 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
cfg.routing.balancers.Should().NotBeNull(); await cfg.routing.balancers.Should().NotBeNull();
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
} }
[Fact] [Test]
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain() public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -105,21 +98,21 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
var hasDialerChain = cfg.outbounds.Any(o => var hasDialerChain = cfg.outbounds.Any(o =>
o.tag == Global.ProxyTag o.tag == Global.ProxyTag
&& o.streamSettings is not null && o.streamSettings is not null
&& o.streamSettings.sockopt is not null && o.streamSettings.sockopt is not null
&& (o.streamSettings.sockopt.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-", && (o.streamSettings.sockopt.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-",
StringComparison.Ordinal)); StringComparison.Ordinal));
hasDialerChain.Should().BeTrue(); await hasDialerChain.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds() public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -140,18 +133,18 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
cfg.routing.balancers.Should().NotBeNull(); await cfg.routing.balancers.Should().NotBeNull();
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
} }
[Fact] [Test]
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches() public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -173,27 +166,27 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)); var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal));
proxyCloneCount.Should().Be(2); await proxyCloneCount.Should().BeEqualTo(2);
var allCloneDialersPointToGroupBranches = cfg.outbounds var allCloneDialersPointToGroupBranches = cfg.outbounds
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)) .Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
.All(o => (o.streamSettings?.sockopt?.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-group-", .All(o => (o.streamSettings?.sockopt?.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-group-",
StringComparison.Ordinal)); StringComparison.Ordinal));
allCloneDialersPointToGroupBranches.Should().BeTrue(); await allCloneDialersPointToGroupBranches.Should().BeTrue();
cfg.routing.balancers.Should().NotBeNull(); await cfg.routing.balancers.Should().NotBeNull();
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
} }
[Fact] [Test]
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules() public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -229,24 +222,24 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var hasDirectRule = cfg.routing.rules.Any(r => var hasDirectRule = cfg.routing.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("full:direct.example.com") && r.domain.Contains("full:direct.example.com")
&& r.outboundTag == Global.DirectTag); && r.outboundTag == Global.DirectTag);
hasDirectRule.Should().BeTrue(); await hasDirectRule.Should().BeTrue();
var hasBlockRule = cfg.routing.rules.Any(r => var hasBlockRule = cfg.routing.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("full:block.example.com") && r.domain.Contains("full:block.example.com")
&& r.outboundTag == Global.BlockTag); && r.outboundTag == Global.BlockTag);
hasBlockRule.Should().BeTrue(); await hasBlockRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound() public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -278,20 +271,20 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}"; var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}";
cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
var hasRouteRule = cfg.routing.rules.Any(r => var hasRouteRule = cfg.routing.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("full:route.example.com") && r.domain.Contains("full:route.example.com")
&& (r.outboundTag ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal)); && (r.outboundTag ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
hasRouteRule.Should().BeTrue(); await hasRouteRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer() public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer()
{ {
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -320,7 +313,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
@@ -335,11 +328,11 @@ public class CoreConfigV2rayServiceTests
&& s.domains?.Contains("geosite:cn") == true && s.domains?.Contains("geosite:cn") == true
&& s.expectedIPs?.Contains("192.168.0.0/16") == true && s.expectedIPs?.Contains("192.168.0.0/16") == true
&& s.expectedIPs?.Contains("geoip:cn") == true); && s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedServer.Should().BeTrue(); await hasExpectedServer.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains() public async Task GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains()
{ {
var bootstrapDns = "8.8.8.8"; var bootstrapDns = "8.8.8.8";
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.Xray, bootstrapDns); var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.Xray, bootstrapDns);
@@ -352,7 +345,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
@@ -366,11 +359,11 @@ public class CoreConfigV2rayServiceTests
s.address == bootstrapDns s.address == bootstrapDns
&& s.domains?.Contains("full:dns-direct.example") == true && s.domains?.Contains("full:dns-direct.example") == true
&& s.domains?.Contains("full:dns-remote.example") == true); && s.domains?.Contains("full:dns-remote.example") == true);
hasBootstrapServer.Should().BeTrue(); await hasBootstrapServer.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers() public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.SimpleDNSItem.DirectDNS = "1.1.1.1"; config.SimpleDNSItem.DirectDNS = "1.1.1.1";
@@ -403,7 +396,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
var dnsServers = dns.servers var dnsServers = dns.servers
@@ -415,14 +408,14 @@ public class CoreConfigV2rayServiceTests
var hasDirectFallback = dnsServers.Any(s => var hasDirectFallback = dnsServers.Any(s =>
(s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal) (s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal)
&& s.address == "1.1.1.1"); && s.address == "1.1.1.1");
hasDirectFallback.Should().BeTrue(); await hasDirectFallback.Should().BeTrue();
var hasRemoteFallback = dnsServers.Any(s => s.address == "9.9.9.9"); var hasRemoteFallback = dnsServers.Any(s => s.address == "9.9.9.9");
hasRemoteFallback.Should().BeFalse(); await hasRemoteFallback.Should().BeFalse();
} }
[Fact] [Test]
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs() public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs()
{ {
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -451,7 +444,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
var dnsServers = dns.servers var dnsServers = dns.servers
@@ -463,14 +456,14 @@ public class CoreConfigV2rayServiceTests
var hasExpectedIPs = dnsServers.Any(s => var hasExpectedIPs = dnsServers.Any(s =>
s.expectedIPs?.Contains("192.168.0.0/16") == true s.expectedIPs?.Contains("192.168.0.0/16") == true
|| s.expectedIPs?.Contains("geoip:cn") == true); || s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedIPs.Should().BeFalse(); await hasExpectedIPs.Should().BeFalse();
} }
[Theory] [Test]
[InlineData("geosite:cn")] [Arguments("geosite:cn")]
[InlineData("geosite:geolocation-cn")] [Arguments("geosite:geolocation-cn")]
[InlineData("geosite:tld-cn")] [Arguments("geosite:tld-cn")]
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag) public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag)
{ {
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -496,7 +489,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
var dnsServers = dns.servers var dnsServers = dns.servers
@@ -510,11 +503,11 @@ public class CoreConfigV2rayServiceTests
&& s.domains?.Contains(domainTag) == true && s.domains?.Contains(domainTag) == true
&& s.expectedIPs?.Contains("192.168.0.0/16") == true && s.expectedIPs?.Contains("192.168.0.0/16") == true
&& s.expectedIPs?.Contains("geoip:cn") == true); && s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedServer.Should().BeTrue(); await hasExpectedServer.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts() public async Task GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1"; config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
@@ -525,17 +518,17 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
dns.hosts.Should().NotBeNull(); await dns.hosts.Should().NotBeNull();
dns.hosts!.Should().ContainKey("resolver.example"); await dns.hosts!.Should().ContainKey("resolver.example");
JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1"); await JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1");
} }
[Fact] [Test]
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig() public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -556,24 +549,24 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8"); await JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8");
dns.hosts.Should().NotBeNull(); await dns.hosts.Should().NotBeNull();
dns.hosts!.Should().ContainKey("raw.example"); await dns.hosts!.Should().ContainKey("raw.example");
JsonUtils.Serialize(dns.hosts!["raw.example"]).Should().Contain("1.1.1.1"); await JsonUtils.Serialize(dns.hosts!["raw.example"]).Should().Contain("1.1.1.1");
var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom"); var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom");
directOutbound.Should().NotBeNull(); await directOutbound.Should().NotBeNull();
directOutbound!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4"); await directOutbound!.streamSettings.sockopt!.domainStrategy.Should().BeEqualTo("UseIPv4");
} }
[Theory] [Test]
[InlineData(false)] [Arguments(false)]
[InlineData(true)] [Arguments(true)]
public void GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address) public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address)
{ {
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address); var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -583,20 +576,20 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull(); await tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0"); await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0"); await tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0");
// EnableIPv6Address governs the interface address only, never the routing table. // EnableIPv6Address governs the interface address only, never the routing table.
tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1); await tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
} }
[Fact] [Test]
public void GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges() public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
{ {
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
config.TunModeItem.EnableIPv6Address = false; config.TunModeItem.EnableIPv6Address = false;
@@ -607,16 +600,16 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull(); await tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':')); await tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':'));
} }
[Fact] [Test]
public void GenerateClientConfigContent_TunRouteExcludeAddress() public async Task GenerateClientConfigContent_TunRouteExcludeAddress()
{ {
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -626,20 +619,20 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull(); await tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0"); await tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32"); await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31"); await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
} }
[Fact] [Test]
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson() public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -666,14 +659,14 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull(); await result.Data.Should().NotBeNull();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString()); var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
cfg.Should().NotBeNull(); await cfg.Should().NotBeNull();
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag); var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
proxyOutbound.Should().NotBeNull(); await proxyOutbound.Should().NotBeNull();
proxyOutbound!.protocol.Should().Be("shadowsocks"); await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks");
proxyOutbound.settings.servers.Should().NotBeNull(); await proxyOutbound.settings.servers.Should().NotBeNull();
} }
} }

View File

@@ -1,103 +1,99 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models;
using Xunit;
namespace ServiceLib.Tests.Fmt; namespace ServiceLib.Tests.Fmt;
public class FmtHandlerTests public class FmtHandlerTests
{ {
[Fact] [Test]
public void GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields()
{ {
var source = CreateVmessProfile(); var source = CreateVmessProfile();
var resolved = ExportThenImport(source); var resolved = await ExportThenImport(source);
resolved.ConfigType.Should().Be(EConfigType.VMess); await resolved.ConfigType.Should().BeEqualTo(EConfigType.VMess);
resolved.Remarks.Should().Be(source.Remarks); await resolved.Remarks.Should().BeEqualTo(source.Remarks);
resolved.Address.Should().Be(source.Address); await resolved.Address.Should().BeEqualTo(source.Address);
resolved.Port.Should().Be(source.Port); await resolved.Port.Should().BeEqualTo(source.Port);
resolved.Password.Should().Be(source.Password); await resolved.Password.Should().BeEqualTo(source.Password);
resolved.GetProtocolExtra().AlterId.Should().Be(source.GetProtocolExtra().AlterId); await resolved.GetProtocolExtra().AlterId.Should().BeEqualTo(source.GetProtocolExtra().AlterId);
} }
[Fact] [Test]
public void GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields()
{ {
var source = CreateVlessProfile(); var source = CreateVlessProfile();
var resolved = ExportThenImport(source); var resolved = await ExportThenImport(source);
resolved.ConfigType.Should().Be(EConfigType.VLESS); await resolved.ConfigType.Should().BeEqualTo(EConfigType.VLESS);
resolved.Remarks.Should().Be(source.Remarks); await resolved.Remarks.Should().BeEqualTo(source.Remarks);
resolved.Address.Should().Be(source.Address); await resolved.Address.Should().BeEqualTo(source.Address);
resolved.Port.Should().Be(source.Port); await resolved.Port.Should().BeEqualTo(source.Port);
resolved.Password.Should().Be(source.Password); await resolved.Password.Should().BeEqualTo(source.Password);
resolved.GetProtocolExtra().VlessEncryption.Should().Be(Global.None); await resolved.GetProtocolExtra().VlessEncryption.Should().BeEqualTo(Global.None);
} }
[Fact] [Test]
public void GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields()
{ {
var source = CreateShadowsocksProfile(); var source = CreateShadowsocksProfile();
var resolved = ExportThenImport(source); var resolved = await ExportThenImport(source);
resolved.ConfigType.Should().Be(EConfigType.Shadowsocks); await resolved.ConfigType.Should().BeEqualTo(EConfigType.Shadowsocks);
resolved.Remarks.Should().Be(source.Remarks); await resolved.Remarks.Should().BeEqualTo(source.Remarks);
resolved.Address.Should().Be(source.Address); await resolved.Address.Should().BeEqualTo(source.Address);
resolved.Port.Should().Be(source.Port); await resolved.Port.Should().BeEqualTo(source.Port);
resolved.Password.Should().Be(source.Password); await resolved.Password.Should().BeEqualTo(source.Password);
resolved.GetProtocolExtra().SsMethod.Should().Be(source.GetProtocolExtra().SsMethod); await resolved.GetProtocolExtra().SsMethod.Should().BeEqualTo(source.GetProtocolExtra().SsMethod);
} }
[Fact] [Test]
public void GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields()
{ {
var source = CreateSocksProfile(); var source = CreateSocksProfile();
var resolved = ExportThenImport(source); var resolved = await ExportThenImport(source);
resolved.ConfigType.Should().Be(EConfigType.SOCKS); await resolved.ConfigType.Should().BeEqualTo(EConfigType.SOCKS);
resolved.Remarks.Should().Be(source.Remarks); await resolved.Remarks.Should().BeEqualTo(source.Remarks);
resolved.Address.Should().Be(source.Address); await resolved.Address.Should().BeEqualTo(source.Address);
resolved.Port.Should().Be(source.Port); await resolved.Port.Should().BeEqualTo(source.Port);
resolved.Username.Should().Be(source.Username); await resolved.Username.Should().BeEqualTo(source.Username);
resolved.Password.Should().Be(source.Password); await resolved.Password.Should().BeEqualTo(source.Password);
} }
[Fact] [Test]
public void ResolveConfig_UnsupportedProtocol_ShouldReturnNull() public async Task ResolveConfig_UnsupportedProtocol_ShouldReturnNull()
{ {
var resolved = FmtHandler.ResolveConfig("not-a-share-uri", out var msg); var resolved = FmtHandler.ResolveConfig("not-a-share-uri", out var msg);
resolved.Should().BeNull(); await resolved.Should().BeNull();
msg.Should().NotBeNullOrWhiteSpace(); await msg.Should().NotBeNull();
await msg.Should().NotBeEmpty();
} }
[Fact] [Test]
public void GetShareUri_UnsupportedConfigType_ShouldReturnNull() public async Task GetShareUri_UnsupportedConfigType_ShouldReturnNull()
{ {
var item = new ProfileItem { ConfigType = EConfigType.PolicyGroup, Remarks = "group", }; var item = new ProfileItem { ConfigType = EConfigType.PolicyGroup, Remarks = "group", };
var uri = FmtHandler.GetShareUri(item); var uri = FmtHandler.GetShareUri(item);
uri.Should().BeNull(); await uri.Should().BeNull();
} }
private static ProfileItem ExportThenImport(ProfileItem source) private static async Task<ProfileItem> ExportThenImport(ProfileItem source)
{ {
var uri = FmtHandler.GetShareUri(source); var uri = FmtHandler.GetShareUri(source);
uri.Should().NotBeNullOrWhiteSpace(); await uri.Should().NotBeNull();
uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should() await uri.Should().NotBeEmpty();
await uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should()
.BeTrue(); .BeTrue();
var resolved = FmtHandler.ResolveConfig(uri, out var msg); var resolved = FmtHandler.ResolveConfig(uri, out var msg);
resolved.Should().NotBeNull($"uri: {uri}, msg: {msg}"); await resolved.Should().NotBeNull().Because($"uri: {uri}, msg: {msg}");
return resolved!; return resolved!;
} }

View File

@@ -1,32 +1,27 @@
using AwesomeAssertions;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models.Dto;
using Xunit;
namespace ServiceLib.Tests.Fmt; namespace ServiceLib.Tests.Fmt;
public class HyRealmTests public class HyRealmTests
{ {
[Fact] [Test]
public void TryParse_ShouldParseValidRealm() public async Task TryParse_ShouldParseValidRealm()
{ {
var str = "realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478"; var str = "realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478";
var result = HyRealm.TryParse(str, out var realm); var result = HyRealm.TryParse(str, out var realm);
result.Should().BeTrue(); await result.Should().BeTrue();
realm.Should().NotBeNull(); await realm.Should().NotBeNull();
realm.IsHttp.Should().BeFalse(); await realm.IsHttp.Should().BeFalse();
realm.Token.Should().Be("public"); await realm.Token.Should().BeEqualTo("public");
realm.RendezvousHost.Should().Be("realm.hy2.io"); await realm.RendezvousHost.Should().BeEqualTo("realm.hy2.io");
realm.RendezvousPort.Should().Be(443); await realm.RendezvousPort.Should().BeEqualTo(443);
realm.RealmName.Should().Be("57f9be7c-2810-4f5b-8cb9-260bc84d6c90"); await realm.RealmName.Should().BeEqualTo("57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
realm.StunList.Should().HaveCount(2); await realm.StunList.Should().HaveCount(2);
realm.StunList.Should().Contain("example.stun:3478"); await realm.StunList.Should().Contain("example.stun:3478");
realm.StunList.Should().Contain("example2.stun:3478"); await realm.StunList.Should().Contain("example2.stun:3478");
} }
[Fact] [Test]
public void ToUri_ShouldGenerateValidUri() public async Task ToUri_ShouldGenerateValidUri()
{ {
var realm = new HyRealm( var realm = new HyRealm(
IsHttp: false, IsHttp: false,
@@ -37,32 +32,32 @@ public class HyRealmTests
StunList: ["example.stun:3478", "example2.stun:3478"] StunList: ["example.stun:3478", "example2.stun:3478"]
); );
var uri = realm.ToUri(); var uri = realm.ToUri();
uri.Should().Contain("realm://public@realm.hy2.io"); await uri.Should().Contain("realm://public@realm.hy2.io");
uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90"); await uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
uri.Should().Contain("stun=example.stun:3478"); await uri.Should().Contain("stun=example.stun:3478");
uri.Should().Contain("stun=example2.stun:3478"); await uri.Should().Contain("stun=example2.stun:3478");
} }
[Fact] [Test]
public void GetShareUriAndResolveConfig_Hy2Realm_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Hy2Realm_ShouldRoundTripBasicFields()
{ {
var str = "hysteria2+realm://mytoken@rendezvous.example.com/my-cabin-1f3a8c2e9b?auth=your_password&insecure=1&pinSHA256=deadbeef#remark"; var str = "hysteria2+realm://mytoken@rendezvous.example.com/my-cabin-1f3a8c2e9b?auth=your_password&insecure=1&pinSHA256=deadbeef#remark";
var resolved = Hysteria2Fmt.ResolveRealm(str, out var msg); var resolved = Hysteria2Fmt.ResolveRealm(str, out var msg);
resolved.Should().NotBeNull(); await resolved.Should().NotBeNull();
resolved.Password.Should().Be("your_password"); await resolved.Password.Should().BeEqualTo("your_password");
var result = HyRealm.TryParse(resolved.GetProtocolExtra().Hy2RealmUrl, out var realm); var result = HyRealm.TryParse(resolved.GetProtocolExtra().Hy2RealmUrl, out var realm);
result.Should().BeTrue(); await result.Should().BeTrue();
realm.Should().NotBeNull(); await realm.Should().NotBeNull();
realm.Token.Should().Be("mytoken"); await realm.Token.Should().BeEqualTo("mytoken");
// To uri // To uri
var uri = Hysteria2Fmt.ToUri(resolved); var uri = Hysteria2Fmt.ToUri(resolved);
uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com"); await uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
uri.Should().EndWith("#remark"); await uri.Should().EndWith("#remark");
} }
[Fact] [Test]
public void ToServerUrl_ShouldIncludeSchemeForSingbox() public async Task ToServerUrl_ShouldIncludeSchemeForSingbox()
{ {
var realm = new HyRealm( var realm = new HyRealm(
IsHttp: false, IsHttp: false,
@@ -73,19 +68,19 @@ public class HyRealmTests
StunList: ["turn.cloudflare.com:3478"] StunList: ["turn.cloudflare.com:3478"]
); );
realm.ToServerUrl().Should().Be("https://realm.hy2.io:443"); await realm.ToServerUrl().Should().BeEqualTo("https://realm.hy2.io:443");
} }
[Fact] [Test]
public void ResolveRealm_Issue9635_ShouldProduceHttpsServerUrl() public async Task 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 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 _); var resolved = Hysteria2Fmt.ResolveRealm(str, out _);
resolved.Should().NotBeNull(); await resolved.Should().NotBeNull();
HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue(); await HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
realm!.ToServerUrl().Should().StartWith("https://"); await realm!.ToServerUrl().Should().StartWith("https://");
realm.ToServerUrl().Should().Contain("realm.hy2.io"); await realm.ToServerUrl().Should().Contain("realm.hy2.io");
realm.StunList.Should().Contain("turn.cloudflare.com:3478"); await realm.StunList.Should().Contain("turn.cloudflare.com:3478");
} }
} }

View File

@@ -1,15 +1,11 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Tests.CoreConfig; using ServiceLib.Tests.CoreConfig;
using Xunit;
namespace ServiceLib.Tests.Fmt; namespace ServiceLib.Tests.Fmt;
public class InnerFmtTests public class InnerFmtTests
{ {
[Fact] [Test]
public void ToUriAndResolve_ShouldRoundTripPolicyGroupReferences() public async Task ToUriAndResolve_ShouldRoundTripPolicyGroupReferences()
{ {
var childA = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "child-a", "child-a"); var childA = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "child-a", "child-a");
var childB = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "child-b", "child-b"); var childB = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "child-b", "child-b");
@@ -19,19 +15,20 @@ public class InnerFmtTests
var uri = InnerFmt.ToUri([group, childA, childB]); var uri = InnerFmt.ToUri([group, childA, childB]);
uri.Should().NotBeNullOrWhiteSpace(); await uri.Should().NotBeNull();
await uri.Should().NotBeEmpty();
var resolved = InnerFmt.Resolve(uri!, "sub-123"); var resolved = InnerFmt.Resolve(uri!, "sub-123");
resolved.Should().NotBeNull(); await resolved.Should().NotBeNull();
resolved.Should().HaveCount(3); await resolved.Should().HaveCount(3);
var resolvedGroup = resolved!.Single(x => x.Remarks == group.Remarks); var resolvedGroup = resolved!.Single(x => x.Remarks == group.Remarks);
var resolvedChildA = resolved.Single(x => x.Remarks == childA.Remarks); var resolvedChildA = resolved.Single(x => x.Remarks == childA.Remarks);
var resolvedChildB = resolved.Single(x => x.Remarks == childB.Remarks); var resolvedChildB = resolved.Single(x => x.Remarks == childB.Remarks);
resolvedGroup.ConfigType.Should().Be(EConfigType.PolicyGroup); await resolvedGroup.ConfigType.Should().BeEqualTo(EConfigType.PolicyGroup);
resolvedGroup.GetProtocolExtra().SubChildItems.Should().Be("sub-123"); await resolvedGroup.GetProtocolExtra().SubChildItems.Should().BeEqualTo("sub-123");
resolvedGroup.GetProtocolExtra().ChildItems.Should().Be($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}"); await resolvedGroup.GetProtocolExtra().ChildItems.Should().BeEqualTo($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}");
} }
} }

View File

@@ -1,13 +1,9 @@
using AwesomeAssertions;
using ServiceLib.Handler.Fmt;
using Xunit;
namespace ServiceLib.Tests.Fmt; namespace ServiceLib.Tests.Fmt;
public class WireguardFmtTests public class WireguardFmtTests
{ {
[Fact] [Test]
public void ResolveConfig_ShouldParsePeersAndIgnoreInlineComments() public async Task ResolveConfig_ShouldParsePeersAndIgnoreInlineComments()
{ {
const string config = const string config =
""" """
@@ -29,19 +25,19 @@ public class WireguardFmtTests
var resolved = WireguardFmt.ResolveConfig(config); var resolved = WireguardFmt.ResolveConfig(config);
resolved.Should().NotBeNull(); await resolved.Should().NotBeNull();
resolved.Should().HaveCount(2); await resolved.Should().HaveCount(2);
var first = resolved![0]; var first = resolved![0];
first.Address.Should().Be("2001:db8::1"); await first.Address.Should().BeEqualTo("2001:db8::1");
first.Port.Should().Be(51820); await first.Port.Should().BeEqualTo(51820);
first.Password.Should().Be("interface-private-key"); await first.Password.Should().BeEqualTo("interface-private-key");
first.GetProtocolExtra().WgReserved.Should().Be("1, 2, 3"); await first.GetProtocolExtra().WgReserved.Should().BeEqualTo("1, 2, 3");
first.GetProtocolExtra().WgInterfaceAddress.Should().Be("10.0.0.2/32, fd00::2/128"); await first.GetProtocolExtra().WgInterfaceAddress.Should().BeEqualTo("10.0.0.2/32, fd00::2/128");
first.GetProtocolExtra().WgMtu.Should().Be(1420); await first.GetProtocolExtra().WgMtu.Should().BeEqualTo(1420);
var second = resolved[1]; var second = resolved[1];
second.Address.Should().Be("example.com"); await second.Address.Should().BeEqualTo("example.com");
second.Port.Should().Be(12345); await second.Port.Should().BeEqualTo(12345);
} }
} }

View File

@@ -1,44 +1,39 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Manager;
using Xunit;
namespace ServiceLib.Tests.Manager; namespace ServiceLib.Tests.Manager;
public class CoreManagerTests public class CoreManagerTests
{ {
[Theory] [Test]
[InlineData(ECoreType.sing_box)] [Arguments(ECoreType.sing_box)]
[InlineData(ECoreType.mihomo)] [Arguments(ECoreType.mihomo)]
[InlineData(ECoreType.Xray)] [Arguments(ECoreType.Xray)]
public void ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType) public async Task ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
{ {
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue(); await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue();
} }
[Fact] [Test]
public void ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate() public async Task ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate()
{ {
// Regression guard for the macOS TUN failure: the elevation decision must follow // Regression guard for the macOS TUN failure: the elevation decision must follow
// the context snapshot that generated the config. A launch whose snapshot has TUN // the context snapshot that generated the config. A launch whose snapshot has TUN
// disabled must never elevate, and a launch whose snapshot has TUN enabled must // disabled must never elevate, and a launch whose snapshot has TUN enabled must
// elevate regardless of later changes to the live config. // elevate regardless of later changes to the live config.
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse(); await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse();
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse(); await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
} }
[Fact] [Test]
public void ShouldRunAsSudo_OnWindows_ShouldNotElevate() public async Task ShouldRunAsSudo_OnWindows_ShouldNotElevate()
{ {
CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse(); await CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse();
} }
[Theory] [Test]
[InlineData(ECoreType.v2fly)] [Arguments(ECoreType.v2fly)]
[InlineData(ECoreType.hysteria)] [Arguments(ECoreType.hysteria)]
[InlineData(null)] [Arguments(null)]
public void ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType) public async Task ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
{ {
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse(); await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
} }
} }

View File

@@ -1,23 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AwesomeAssertions" /> <PackageReference Include="TUnit" />
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="TUnit.Assertions.Should" />
<PackageReference Include="xunit.runner.visualstudio"> </ItemGroup>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.v3" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ServiceLib\ServiceLib.csproj" /> <ProjectReference Include="..\ServiceLib\ServiceLib.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Using Include="TUnit.Assertions.Should" />
<Using Include="TUnit.Assertions.Should.Extensions" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace ServiceLib.Common; namespace ServiceLib.Common;
@@ -138,7 +139,10 @@ public static class Extension
public static async Task<TOutput> HandleSafe<TInput, TOutput>( public static async Task<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction, this Interaction<TInput, TOutput> interaction,
TInput input, TInput input,
TOutput defaultValue = default!) TOutput defaultValue = default!,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{ {
try try
{ {
@@ -146,12 +150,14 @@ public static class Extension
} }
catch (UnhandledInteractionException<TInput, TOutput> ex) catch (UnhandledInteractionException<TInput, TOutput> ex)
{ {
Logging.SaveLog($"Unhandled interaction exception for input: {input}", ex); var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
Logging.SaveLog(title, ex);
return defaultValue; return defaultValue;
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog($"Exception occurred while handling interaction for input: {input}", ex); var title = $"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}";
Logging.SaveLog(title, ex);
return defaultValue; return defaultValue;
} }
} }

View File

@@ -1,4 +1,3 @@
using System.Collections.Specialized;
using System.Security.Principal; using System.Security.Principal;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;

View File

@@ -5,6 +5,7 @@ public sealed class EventChannel<T>
private readonly Signal<T> _signal = new(); private readonly Signal<T> _signal = new();
private readonly Lock _gate = new(); private readonly Lock _gate = new();
private readonly IObservable<T> _observable; private readonly IObservable<T> _observable;
public EventChannel() public EventChannel()
{ {
_observable = _signal.Synchronize(_gate); _observable = _signal.Synchronize(_gate);

View File

@@ -1,5 +1,3 @@
using System.ComponentModel.DataAnnotations;
namespace ServiceLib.Handler.Builder; namespace ServiceLib.Handler.Builder;
public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult) public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult)
@@ -205,7 +203,7 @@ public class CoreConfigContextBuilder
Context = preSocksResult.Context with Context = preSocksResult.Context with
{ {
ProtectDomainList = ProtectDomainList =
[.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []], [.. nodeContext.ProtectDomainList, .. preSocksResult.Context.ProtectDomainList],
ProtectCoreTypeList = protectCoreTypeList, ProtectCoreTypeList = protectCoreTypeList,
}, },
}; };
@@ -386,7 +384,7 @@ public class CoreConfigContextBuilder
{ {
var echQuerySni = node.Sni; var echQuerySni = node.Sni;
if (node.StreamSecurity == Global.StreamSecurity if (node.StreamSecurity == Global.StreamSecurity
&& node.EchConfigList?.Contains("://") == true) && node.EchConfigList.Contains("://"))
{ {
var idx = node.EchConfigList.IndexOf('+'); var idx = node.EchConfigList.IndexOf('+');
echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni; echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni;

View File

@@ -115,10 +115,10 @@ public class NodeValidator
if (item.GetNetwork() is nameof(ETransport.ws) if (item.GetNetwork() is nameof(ETransport.ws)
&& item.EchConfigList.IsNullOrEmpty() && item.EchConfigList.IsNullOrEmpty()
&& item.GetAlpn()?.FirstOrDefault() == "h3") && item.GetAlpn()?.FirstOrDefault() is "h3" or "h2")
{ {
v.Warning( v.Warning(
"WebSocket but ALPN is set to h3, the core may ignore the ALPN setting or cause unexpected issues."); $"WebSocket but ALPN is set to {item.Alpn}, the core may ignore the ALPN setting or cause unexpected issues.");
} }
// TLS & Security // TLS & Security
@@ -131,14 +131,6 @@ public class NodeValidator
isCertProvided = false; isCertProvided = false;
} }
// Check for deprecated allowInsecure property when TLS is enabled
if (item.GetAllowInsecure()
&& item.Cert.IsNullOrEmpty()
&& item.CertSha.IsNullOrEmpty())
{
v.Warning(ResUI.MsgAllowInsecureDeprecated);
}
if ((coreType == ECoreType.Xray if ((coreType == ECoreType.Xray
&& item.GetAllowInsecure() && item.GetAllowInsecure()
&& !isCertProvided && !isCertProvided
@@ -147,6 +139,10 @@ public class NodeValidator
&& item.GetAllowInsecure() && item.GetAllowInsecure()
&& !isCertProvided)) && !isCertProvided))
{ {
if (coreType == ECoreType.Xray)
{
v.Warning(ResUI.MsgAllowInsecureDeprecated);
}
v.Warning(ResUI.MsgInsecureConfiguration); v.Warning(ResUI.MsgInsecureConfiguration);
} }
} }

View File

@@ -115,6 +115,7 @@ public static class ConfigHandler
config.ConstItem ??= new ConstItem(); config.ConstItem ??= new ConstItem();
config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
config.SimpleDNSItem.BlockAAAAQuery ??= false;
config.SimpleDNSItem.FakeIPRange ??= Global.FakeIPRanges.FirstOrDefault(); config.SimpleDNSItem.FakeIPRange ??= Global.FakeIPRanges.FirstOrDefault();
config.SimpleDNSItem.GlobalFakeIp ??= true; config.SimpleDNSItem.GlobalFakeIp ??= true;
config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault(); config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault();
@@ -1797,21 +1798,18 @@ public static class ConfigHandler
{ {
ECoreType.Xray => V2rayFmt.ResolveToCustom(strData, subRemarks), ECoreType.Xray => V2rayFmt.ResolveToCustom(strData, subRemarks),
ECoreType.sing_box => SingboxFmt.ResolveToCustom(strData, subRemarks), ECoreType.sing_box => SingboxFmt.ResolveToCustom(strData, subRemarks),
_ => null _ => null,
}; };
if (lstProfiles is not null) if ((lstProfiles?.Count ?? 0) == 0)
{ {
if (lstProfiles.Count == 0) return -1;
{ }
return -1;
}
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub); var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
if (count > 0) if (count > 0)
{ {
return count; return count;
}
} }
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType); return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);

View File

@@ -1,5 +1,3 @@
using System.Collections.Specialized;
namespace ServiceLib.Handler.Fmt; namespace ServiceLib.Handler.Fmt;
public class BaseFmt public class BaseFmt

View File

@@ -166,15 +166,19 @@ public class Hysteria2Fmt : BaseFmt
} }
if (item.CertSha.IsNullOrEmpty()) if (item.CertSha.IsNullOrEmpty())
{ {
item.CertSha = GetQueryDecoded(query, "pinSHA256"); var pinSHA256 = GetQueryDecoded(query, "pinSHA256");
// NOTE: item.CertSha = pinSHA256;
// To accommodate Xray changes, if (!pinSHA256.IsNullOrEmpty())
// some providers issue self-signed cert links with `insecure = false` and a certificate fingerprint, {
// breaking interoperability between Xray, official Hysteria 2 client, and sing-box. // NOTE:
// Since this won't compromise the overall security model, // To accommodate Xray changes,
// `insecure = true` is automatically set when a fingerprint is detected, // some providers issue self-signed cert links with `insecure = false` and a certificate fingerprint,
// and the value is restored when generating configurations. // breaking interoperability between Xray, official Hysteria 2 client, and sing-box.
item.AllowInsecure = Global.StringTrue; // Since this won't compromise the overall security model,
// `insecure = true` is automatically set when a fingerprint is detected,
// and the value is restored when generating configurations.
item.AllowInsecure = Global.StringTrue;
}
} }
item.EchConfigList = GetQueryDecoded(query, "ech"); item.EchConfigList = GetQueryDecoded(query, "ech");
item.SetProtocolExtra(item.GetProtocolExtra() with item.SetProtocolExtra(item.GetProtocolExtra() with

View File

@@ -34,9 +34,9 @@ public class SingboxFmt : BaseFmt
if (!isOutbound) if (!isOutbound)
{ {
var fullProfile = ResolveFull(jsonObject, subRemarks); var fullProfile = ResolveFull(jsonObject, subRemarks);
profileList.Add(fullProfile);
if (fullProfile is not null) if (fullProfile is not null)
{ {
profileList.Add(fullProfile);
return profileList; return profileList;
} }
} }

View File

@@ -34,9 +34,9 @@ public class V2rayFmt : BaseFmt
if (!isOutbound) if (!isOutbound)
{ {
var fullProfile = ResolveFull(jsonObject, subRemarks); var fullProfile = ResolveFull(jsonObject, subRemarks);
profileList.Add(fullProfile);
if (fullProfile is not null) if (fullProfile is not null)
{ {
profileList.Add(fullProfile);
return profileList; return profileList;
} }
} }

View File

@@ -55,8 +55,6 @@ public class DownloaderHelper
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token); await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
using StreamReader reader = new(stream); using StreamReader reader = new(stream);
downloadOpt = null;
return await reader.ReadToEndAsync(cts.Token); return await reader.ReadToEndAsync(cts.Token);
} }
@@ -128,72 +126,150 @@ public class DownloaderHelper
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout)); 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;
} }
public async Task DownloadFileAsync(IWebProxy? webProxy, string url, string fileName, IProgress<double> progress, int timeout) public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
{ {
if (url.IsNullOrEmpty()) ArgumentNullException.ThrowIfNull(request);
if (request.FilePath.IsNullOrEmpty())
{ {
throw new ArgumentNullException(nameof(url)); throw new ArgumentNullException(nameof(request.FilePath));
} }
if (fileName.IsNullOrEmpty()) if (File.Exists(request.FilePath))
{ {
throw new ArgumentNullException(nameof(fileName)); File.Delete(request.FilePath);
}
if (File.Exists(fileName))
{
File.Delete(fileName);
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5); var state = new FileDownloadState
{
Request = request,
};
var requestConfiguration = new RequestConfiguration() var requestConfiguration = new RequestConfiguration()
{ {
ConnectTimeout = connectTimeout * 1000, ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
Proxy = webProxy Proxy = webProxy,
}; };
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000, ChunkCount = 100,
MaxTryAgainOnFailure = 2, MinimumChunkSize = 8 * 1024 * 1024, // 8 MB
MinimumSizeOfChunking = 8 * 1024 * 1024, // 8 MB
ParallelDownload = true,
ParallelCount = 4,
RequestConfiguration = requestConfiguration, RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration), CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
}; };
var progressPercentage = 0;
var hasValue = false;
await using var downloader = new Downloader.DownloadService(downloadOpt); await using var downloader = new Downloader.DownloadService(downloadOpt);
downloader.DownloadStarted += (sender, value) => progress?.Report(0); downloader.DownloadStarted += (sender, value) =>
{
state = state with
{
TotalBytes = value.TotalBytesToReceive,
};
onProgress.Invoke(state);
};
downloader.DownloadProgressChanged += (sender, value) => downloader.DownloadProgressChanged += (sender, value) =>
{ {
hasValue = true; state = state with
var percent = (int)value.ProgressPercentage;// Convert.ToInt32((totalRead * 1d) / (total * 1d) * 100);
if (progressPercentage != percent && percent % 10 == 0)
{ {
progressPercentage = percent; DownloadedBytes = value.ReceivedBytesSize,
progress.Report(percent); TotalBytes = value.TotalBytesToReceive,
} SpeedBytesPerSecond = value.BytesPerSecondSpeed,
};
onProgress.Invoke(state);
}; };
downloader.DownloadFileCompleted += (sender, value) => downloader.DownloadFileCompleted += (sender, value) =>
{ {
if (progress != null) state = state with
{ {
if (hasValue && value.Error == null) Completed = true,
{ Error = value.Error,
progress.Report(101); };
} onProgress.Invoke(state);
else if (value.Error != null)
{
throw value.Error;
}
}
}; };
using var cts = new CancellationTokenSource(); await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken);
await downloader.DownloadFileTaskAsync(url, fileName, cts.Token); }
downloadOpt = null; public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
{
if (requests is not { Count: > 0 })
{
throw new ArgumentNullException(nameof(requests));
}
var states = new FileDownloadState[requests.Count];
for (var i = 0; i < requests.Count; i++)
{
states[i] = new FileDownloadState
{
Request = requests[i],
};
}
var readOnlyStates = new ReadOnlyMemory<FileDownloadState>(states);
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
Proxy = webProxy,
KeepAlive = true,
};
using var socketsHttpHandler = GetSocketsHttpHandler(requestConfiguration);
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = 4,
//CancellationToken = cancellationToken,
};
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) =>
{
var request = requests[index];
var downloadOpt = new DownloadConfiguration()
{
RequestConfiguration = requestConfiguration,
// ReSharper disable once AccessToDisposedClosure
CustomHttpMessageHandlerFactory = () => socketsHttpHandler,
};
await using var downloader = new Downloader.DownloadService(downloadOpt);
downloader.DownloadStarted += (sender, value) =>
{
states[index] = states[index] with
{
DownloadedBytes = 0,
TotalBytes = value.TotalBytesToReceive,
SpeedBytesPerSecond = 0,
Completed = false,
};
onProgress.Invoke(readOnlyStates);
};
downloader.DownloadProgressChanged += (sender, value) =>
{
states[index] = states[index] with
{
DownloadedBytes = value.ReceivedBytesSize,
TotalBytes = value.TotalBytesToReceive,
SpeedBytesPerSecond = value.BytesPerSecondSpeed,
Completed = false,
};
onProgress.Invoke(readOnlyStates);
};
downloader.DownloadFileCompleted += (sender, value) =>
{
var newState = states[index] with { Completed = true };
if (value.Error != null)
{
newState = newState with { Error = value.Error };
}
states[index] = newState;
onProgress.Invoke(readOnlyStates);
};
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, parallelCancellationToken);
});
} }
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/SocketClient.cs#L45 // https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/SocketClient.cs#L45
@@ -213,7 +289,7 @@ public class DownloaderHelper
PooledConnectionIdleTimeout = config.KeepAliveTimeout, PooledConnectionIdleTimeout = config.KeepAliveTimeout,
PooledConnectionLifetime = Timeout.InfiniteTimeSpan, PooledConnectionLifetime = Timeout.InfiniteTimeSpan,
EnableMultipleHttp2Connections = true, EnableMultipleHttp2Connections = true,
ConnectTimeout = TimeSpan.FromMilliseconds(config.ConnectTimeout) ConnectTimeout = TimeSpan.FromMilliseconds(config.ConnectTimeout),
}; };
// Set up the SslClientAuthenticationOptions for custom certificate validation // Set up the SslClientAuthenticationOptions for custom certificate validation

View File

@@ -279,6 +279,7 @@ public class SimpleDNSItem
public bool? GlobalFakeIp { get; set; } public bool? GlobalFakeIp { get; set; }
public string? FakeIPRange { get; set; } public string? FakeIPRange { get; set; }
public bool? BlockBindingQuery { get; set; } public bool? BlockBindingQuery { get; set; }
public bool? BlockAAAAQuery { get; set; }
public string? DirectDNS { get; set; } public string? DirectDNS { get; set; }
public string? RemoteDNS { get; set; } public string? RemoteDNS { get; set; }
public string? BootstrapDNS { get; set; } public string? BootstrapDNS { get; set; }

View File

@@ -241,6 +241,7 @@ public class Dns4Ray
public List<object> servers { get; set; } public List<object> servers { get; set; }
public bool? serveStale { get; set; } public bool? serveStale { get; set; }
public bool? enableParallelQuery { get; set; } public bool? enableParallelQuery { get; set; }
public string? queryStrategy { get; set; }
public string? tag { get; set; } public string? tag { get; set; }
} }

View File

@@ -0,0 +1,22 @@
namespace ServiceLib.Models.Dto;
public record FileDownloadState
{
public required FileDownloadRequest Request { get; init; }
public long DownloadedBytes { get; init; } = 0;
public long TotalBytes { get; init; } = 0;
public double SpeedBytesPerSecond { get; init; } = 0;
public bool Completed { get; init; } = false;
public Exception? Error { get; init; }
public bool IsFailed => Error != null;
}
public record FileDownloadRequest
{
public required string FileUrl { get; init; }
public required string FilePath { get; init; }
public string? DisplayFileName { get; init; }
public string FileName => DisplayFileName ?? Path.GetFileName(FilePath);
}

View File

@@ -2769,6 +2769,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Block AAAA Queries 的本地化字符串。
/// </summary>
public static string TbBlockAAAAQueries {
get {
return ResourceManager.GetString("TbBlockAAAAQueries", resourceCulture);
}
}
/// <summary>
/// 查找类似 Block IPv6 queries when enabled 的本地化字符串。
/// </summary>
public static string TbBlockAAAAQueriesTips {
get {
return ResourceManager.GetString("TbBlockAAAAQueriesTips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Block SVCB and HTTPS Queries 的本地化字符串。 /// 查找类似 Block SVCB and HTTPS Queries 的本地化字符串。
/// </summary> /// </summary>
@@ -2779,7 +2797,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Block ECH and HTTP/3 availability checks when enabled 的本地化字符串。 /// 查找类似 Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray 的本地化字符串。
/// </summary> /// </summary>
public static string TbBlockSVCBHTTPSQueriesTips { public static string TbBlockSVCBHTTPSQueriesTips {
get { get {
@@ -5031,6 +5049,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Xray Only 的本地化字符串。
/// </summary>
public static string TbXrayOnly {
get {
return ResourceManager.GetString("TbXrayOnly", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。 /// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
/// </summary> /// </summary>

View File

@@ -1456,7 +1456,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value> <value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value> <value>Please fill in the correct config template</value>

View File

@@ -1453,7 +1453,7 @@
<value>DNS personnalisé activé ; la configuration de cette page sera ignorée</value> <value>DNS personnalisé activé ; la configuration de cette page sera ignorée</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Une fois activé, bloque les requêtes ECH et de disponibilité HTTP/3</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Veuillez saisir un modèle de configuration valide</value> <value>Veuillez saisir un modèle de configuration valide</value>

View File

@@ -1456,7 +1456,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value> <value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value> <value>Please fill in the correct config template</value>

View File

@@ -1456,7 +1456,7 @@
<value>DNS kustom diaktifkan, pengaturan halaman ini tidak berlaku</value> <value>DNS kustom diaktifkan, pengaturan halaman ini tidak berlaku</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Blokir pemeriksaan ketersediaan ECH dan HTTP/3 saat diaktifkan</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Isi template konfigurasi yang benar</value> <value>Isi template konfigurasi yang benar</value>

View File

@@ -1462,7 +1462,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value> <value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value> <value>Please fill in the correct config template</value>
@@ -1860,4 +1860,13 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="LvCustomCoreType" xml:space="preserve"> <data name="LvCustomCoreType" xml:space="preserve">
<value>Custom config core</value> <value>Custom config core</value>
</data> </data>
</root> <data name="TbXrayOnly" xml:space="preserve">
<value>Xray Only</value>
</data>
<data name="TbBlockAAAAQueries" xml:space="preserve">
<value>Block AAAA Queries</value>
</data>
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
<value>Block IPv6 queries when enabled</value>
</data>
</root>

View File

@@ -1462,7 +1462,7 @@
<value>Включён пользовательский DNS — настройки на этой странице не применяются</value> <value>Включён пользовательский DNS — настройки на этой странице не применяются</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>При включении блокирует проверки доступности ECH и HTTP/3</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Пожалуйста, заполните корректный шаблон конфигурации</value> <value>Пожалуйста, заполните корректный шаблон конфигурации</value>

View File

@@ -1459,7 +1459,7 @@
<value>自定义 DNS 已启用,此页面配置将无效</value> <value>自定义 DNS 已启用,此页面配置将无效</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>开启后将阻止 ECH 和 HTTP/3 可用性查询</value> <value>开启后将阻止 ECH 和 HTTP/3 可用性查询。Xray 固定开启</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>请填写正确的配置模板</value> <value>请填写正确的配置模板</value>
@@ -1861,4 +1861,13 @@
<data name="LvCustomCoreType" xml:space="preserve"> <data name="LvCustomCoreType" xml:space="preserve">
<value>自定义配置核心</value> <value>自定义配置核心</value>
</data> </data>
<data name="TbXrayOnly" xml:space="preserve">
<value>仅 Xray</value>
</data>
<data name="TbBlockAAAAQueries" xml:space="preserve">
<value>阻止 AAAA 查询</value>
</data>
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
<value>开启后将阻止 IPv6 查询</value>
</data>
</root> </root>

View File

@@ -1459,7 +1459,7 @@
<value>已啟用自訂 DNS此頁面的設定將不會生效</value> <value>已啟用自訂 DNS此頁面的設定將不會生效</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>啟用後將封鎖 ECH 與 HTTP/3 可用性查詢</value> <value>啟用後將封鎖 ECH 與 HTTP/3 可用性查詢。Xray 固定啟用</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>請填寫正確的設定範本</value> <value>請填寫正確的設定範本</value>
@@ -1861,4 +1861,4 @@
<data name="LvCustomCoreType" xml:space="preserve"> <data name="LvCustomCoreType" xml:space="preserve">
<value>自訂設定核心</value> <value>自訂設定核心</value>
</data> </data>
</root> </root>

View File

@@ -1,3 +1,6 @@
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace ServiceLib.Services.CoreConfig; namespace ServiceLib.Services.CoreConfig;
/// <summary> /// <summary>
@@ -127,7 +130,27 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
Logging.SaveLog($"{_tag}-Mixin", ex); Logging.SaveLog($"{_tag}-Mixin", ex);
} }
// Mihomo parses plain values such as 815458e4 as floats, so quote REALITY short IDs.
var originalRealityShortIds = new List<(Dictionary<object, object> RealityOptions, string ShortId)>();
if (fileContent.GetValueOrDefault("proxies") is List<object> proxies)
{
foreach (var proxy in proxies.OfType<Dictionary<object, object>>())
{
if (proxy.GetValueOrDefault("reality-opts") is Dictionary<object, object> realityOptions
&& realityOptions.GetValueOrDefault("short-id") is string shortId
&& !shortId.StartsWith(tagYamlStr2, StringComparison.Ordinal))
{
originalRealityShortIds.Add((realityOptions, shortId));
realityOptions["short-id"] = new YamlScalarNode(shortId) { Style = ScalarStyle.DoubleQuoted };
}
}
}
var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3); var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3);
foreach (var (realityOptions, shortId) in originalRealityShortIds)
{
realityOptions["short-id"] = shortId;
}
await File.WriteAllTextAsync(fileName, txtFileNew); await File.WriteAllTextAsync(fileName, txtFileNew);
//check again //check again
if (!File.Exists(fileName)) if (!File.Exists(fileName))

View File

@@ -125,7 +125,8 @@ public partial class CoreConfigSingboxService
fullConfigTemplateNode["outbounds"] = customOutboundsNode; fullConfigTemplateNode["outbounds"] = customOutboundsNode;
// Process endpoints // Process endpoints
if (fullConfigTemplateNode["endpoints"] is JsonArray { Count: > 0 } coreConfigEndpointsNode) var coreConfigEndpointsNode = coreConfigNode?["endpoints"] as JsonArray ?? [];
if (coreConfigEndpointsNode is { Count: > 0 })
{ {
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? []; var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
foreach (var endpoint in coreConfigEndpointsNode) foreach (var endpoint in coreConfigEndpointsNode)

View File

@@ -282,13 +282,23 @@ public partial class CoreConfigSingboxService
{ {
query_type = [1, 28], // A and AAAA query_type = [1, 28], // A and AAAA
}, },
fakeipFilterRule fakeipFilterRule,
] ],
}; };
_coreConfig.dns.rules.Add(rule4Fake); _coreConfig.dns.rules.Add(rule4Fake);
} }
if (simpleDnsItem.BlockAAAAQuery == true)
{
_coreConfig.dns.rules.Add(new()
{
query_type = [28],
action = "predefined",
rcode = "NOERROR",
});
}
var routing = context.RoutingItem; var routing = context.RoutingItem;
if (routing == null) if (routing == null)
{ {

View File

@@ -82,6 +82,11 @@ public partial class CoreConfigV2rayService
dnsItem.serveStale = simpleDnsItem?.ServeStale is true ? true : null; dnsItem.serveStale = simpleDnsItem?.ServeStale is true ? true : null;
dnsItem.enableParallelQuery = simpleDnsItem?.ParallelQuery is true ? true : null; dnsItem.enableParallelQuery = simpleDnsItem?.ParallelQuery is true ? true : null;
if (simpleDnsItem.BlockAAAAQuery == true)
{
dnsItem.queryStrategy = "UseIPv4";
}
// DNS routing // DNS routing
var directDnsTags = dnsItem.servers var directDnsTags = dnsItem.servers
.Select(server => .Select(server =>

View File

@@ -42,21 +42,88 @@ public class DownloadService
/// <summary> /// <summary>
/// Downloads a file and reports progress through events. /// Downloads a file and reports progress through events.
/// </summary> /// </summary>
public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout) public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, TimeSpan connectTimeout)
{ {
try try
{ {
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {url}")); UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}"));
var progress = new Progress<double>();
progress.ProgressChanged += (sender, value) => UpdateCompleted?.Invoke(this, new UpdateResult(value > 100, $"...{value}%"));
var webProxy = await GetWebProxy(blProxy); var webProxy = await GetWebProxy(blProxy);
await DownloaderHelper.Instance.DownloadFileAsync(webProxy, await DownloaderHelper.Instance.DownloadFileAsync(webProxy,
url, request,
fileName, OnProgress,
progress, connectTimeout);
downloadTimeout);
void OnProgress(FileDownloadState state)
{
UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}"));
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
Error?.Invoke(this, new ErrorEventArgs(ex));
if (ex.InnerException != null)
{
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
}
}
}
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, TimeSpan connectTimeout)
{
try
{
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}"));
var webProxy = await GetWebProxy(blProxy);
await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy,
requests,
OnProgress,
connectTimeout);
void OnProgress(ReadOnlyMemory<FileDownloadState> states)
{
var span = states.Span;
var completedCount = 0;
var downloadingStates = new List<FileDownloadState>();
foreach (ref readonly var item in span)
{
if (item.Completed)
{
completedCount++;
}
else if (item.TotalBytes > 0)
{
downloadingStates.Add(item);
}
}
var totalSpeed = downloadingStates.Sum(x => x.SpeedBytesPerSecond);
var totalDownloadedBytes = downloadingStates.Sum(x => x.DownloadedBytes);
var totalTotalBytes = downloadingStates.Sum(x => x.TotalBytes);
var downloadingFileName = string.Join(", ", downloadingStates.Select(x => x.Request.FileName));
var allCompleted = completedCount == span.Length;
if (allCompleted)
{
// check and throw errors if any
FileDownloadState? failedState = null;
foreach (ref readonly var item in span)
{
if (!item.IsFailed)
{
continue;
}
failedState = item;
break;
}
if (failedState?.Error != null)
{
throw failedState.Error;
}
}
UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}"));
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -37,9 +37,9 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN)); await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN));
await UpdateFunc(false, result.Msg); await UpdateFunc(false, result.Msg);
url = result.Url.ToString(); url = result.Url!;
fileName = Utils.GetTempPath(Utils.GetGuid()); fileName = Utils.GetTempPath(Utils.GetGuid());
await downloadHandle.DownloadFileAsync(url, fileName, true, _timeout); await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, true, TimeSpan.FromSeconds(_timeout));
} }
else else
{ {
@@ -86,10 +86,10 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type)); await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type));
await UpdateFunc(false, result.Msg); await UpdateFunc(false, result.Msg);
url = result.Url.ToString(); url = result.Url!;
var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url); var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url);
fileName = Utils.GetTempPath(Utils.GetGuid() + ext); fileName = Utils.GetTempPath(Utils.GetGuid() + ext);
await downloadHandle.DownloadFileAsync(url, fileName, true, _timeout); await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, true, TimeSpan.FromSeconds(_timeout));
} }
else else
{ {
@@ -139,9 +139,13 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
public async Task UpdateGeoFileAll() public async Task UpdateGeoFileAll()
{ {
await UpdateGeoFiles(); var requests = new List<FileDownloadRequest>();
await UpdateOtherFiles(); requests.AddRange(GetGeoFilesRequest());
await UpdateSrsFileAll(); requests.AddRange(GetOtherFilesRequest());
requests.AddRange(await GetSrsFileAllRequest());
// NOTE: srs files are more small, so we reverse the order to ensure a good download experience for the user.
requests.Reverse();
await DownloadGeoFiles(requests);
await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
} }
@@ -363,41 +367,53 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
#region Geo private #region Geo private
private async Task UpdateGeoFiles() private List<FileDownloadRequest> GetGeoFilesRequest()
{ {
var geoUrl = string.IsNullOrEmpty(_config?.ConstItem.GeoSourceUrl) var geoUrl = string.IsNullOrEmpty(_config?.ConstItem.GeoSourceUrl)
? Global.GeoUrl ? Global.GeoUrl
: _config.ConstItem.GeoSourceUrl; : _config.ConstItem.GeoSourceUrl;
List<string> files = ["geosite", "geoip"]; List<string> files = ["geosite", "geoip"];
foreach (var geoName in files) return
{ [
var fileName = $"{geoName}.dat"; .. from geoName in files
var targetPath = Utils.GetBinPath($"{fileName}"); let fileName = $"{geoName}.dat"
var url = string.Format(geoUrl, geoName); let targetPath = Utils.GetBinPath($"{fileName}")
let url = string.Format(geoUrl, geoName)
await DownloadGeoFile(url, fileName, targetPath); select new FileDownloadRequest()
} {
FileUrl = url,
FilePath = targetPath,
DisplayFileName = fileName,
},
];
} }
private async Task UpdateOtherFiles() private List<FileDownloadRequest> GetOtherFilesRequest()
{ {
//If it is not in China area, no update is required //If it is not in China area, no update is required
if (_config.ConstItem.GeoSourceUrl.IsNotEmpty()) if (_config.ConstItem.GeoSourceUrl.IsNotEmpty())
{ {
return; return [];
} }
foreach (var url in Global.OtherGeoUrls) return
{ [
var fileName = Path.GetFileName(url); .. Global.OtherGeoUrls.Select(url =>
var targetPath = Utils.GetBinPath($"{fileName}"); {
var fileName = Path.GetFileName(url);
await DownloadGeoFile(url, fileName, targetPath); var targetPath = Utils.GetBinPath($"{fileName}");
} return new FileDownloadRequest()
{
FileUrl = url,
FilePath = targetPath,
DisplayFileName = fileName,
};
}),
];
} }
private async Task UpdateSrsFileAll() private async Task<List<FileDownloadRequest>> GetSrsFileAllRequest()
{ {
var geoipFiles = new List<string>(); var geoipFiles = new List<string>();
var geoSiteFiles = new List<string>(); var geoSiteFiles = new List<string>();
@@ -432,15 +448,12 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
Directory.CreateDirectory(path); Directory.CreateDirectory(path);
} }
foreach (var item in geoipFiles.Distinct()) return
{ [
await UpdateSrsFile("geoip", item); .. geoipFiles.Distinct().Select(f => (type: "geoip", file: f))
} .Concat(geoSiteFiles.Distinct().Select(f => (type: "geosite", file: f)))
.Select(item => GetSrsFileRequest(item.type, item.file)),
foreach (var item in geoSiteFiles.Distinct()) ];
{
await UpdateSrsFile("geosite", item);
}
} }
private void AddPrefixedItems(List<string>? items, string prefix, List<string> output) private void AddPrefixedItems(List<string>? items, string prefix, List<string> output)
@@ -500,7 +513,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
} }
} }
private async Task UpdateSrsFile(string type, string srsName) private FileDownloadRequest GetSrsFileRequest(string type, string srsName)
{ {
var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl) var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl)
? Global.SingboxRulesetUrl ? Global.SingboxRulesetUrl
@@ -510,33 +523,57 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName); var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName);
var url = string.Format(srsUrl, type, $"{type}-{srsName}", srsName); var url = string.Format(srsUrl, type, $"{type}-{srsName}", srsName);
await DownloadGeoFile(url, fileName, targetPath); return new FileDownloadRequest()
{
FileUrl = url,
FilePath = targetPath,
DisplayFileName = fileName,
};
} }
private async Task DownloadGeoFile(string url, string fileName, string targetPath) private async Task DownloadGeoFiles(List<FileDownloadRequest> requests)
{ {
var tmpFileName = Utils.GetTempPath(Utils.GetGuid()); var tmpFilePathDict = new Dictionary<string, string>();
var tmpFileRequests = new List<FileDownloadRequest>();
foreach (var request in requests)
{
var tmpFilePath = Utils.GetTempPath(Utils.GetGuid());
tmpFilePathDict[request.FilePath] = tmpFilePath;
tmpFileRequests.Add(request with
{
FilePath = tmpFilePath,
});
}
DownloadService downloadHandle = new(); DownloadService downloadHandle = new();
downloadHandle.UpdateCompleted += (sender2, args) => downloadHandle.UpdateCompleted += (sender2, args) =>
{ {
if (args.Success) if (args.Success)
{ {
_ = UpdateFunc(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName)); //_ = UpdateFunc(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName));
try foreach (var request in requests)
{ {
if (File.Exists(tmpFileName)) try
{ {
File.Copy(tmpFileName, targetPath, true); //if (File.Exists(tmpFileName))
//{
// File.Copy(tmpFileName, targetPath, true);
File.Delete(tmpFileName); // File.Delete(tmpFileName);
//await UpdateFunc(true, ""); // //await UpdateFunc(true, "");
//}
var tmpFileName = tmpFilePathDict[request.FilePath];
if (File.Exists(tmpFileName))
{
File.Copy(tmpFileName, request.FilePath, true);
File.Delete(tmpFileName);
}
}
catch (Exception ex)
{
_ = UpdateFunc(false, ex.Message);
} }
}
catch (Exception ex)
{
_ = UpdateFunc(false, ex.Message);
} }
} }
else else
@@ -549,7 +586,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
_ = UpdateFunc(false, args.GetException().Message); _ = UpdateFunc(false, args.GetException().Message);
}; };
await downloadHandle.DownloadFileAsync(url, tmpFileName, true, _timeout); await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, true, TimeSpan.FromSeconds(_timeout));
} }
#endregion Geo private #endregion Geo private

View File

@@ -9,6 +9,7 @@ public partial class DNSSettingViewModel : MyReactiveObject, ICloseable
[Reactive] public partial bool FakeIP { get; set; } [Reactive] public partial bool FakeIP { get; set; }
[Reactive] public partial string FakeIPRange { get; set; } [Reactive] public partial string FakeIPRange { get; set; }
[Reactive] public partial bool BlockBindingQuery { get; set; } [Reactive] public partial bool BlockBindingQuery { get; set; }
[Reactive] public partial bool BlockAAAAQuery { get; set; }
[Reactive] public partial string DirectDNS { get; set; } [Reactive] public partial string DirectDNS { get; set; }
[Reactive] public partial string RemoteDNS { get; set; } [Reactive] public partial string RemoteDNS { get; set; }
[Reactive] public partial string BootstrapDNS { get; set; } [Reactive] public partial string BootstrapDNS { get; set; }
@@ -74,6 +75,7 @@ public partial class DNSSettingViewModel : MyReactiveObject, ICloseable
FakeIP = item.FakeIP ?? false; FakeIP = item.FakeIP ?? false;
FakeIPRange = item.FakeIPRange ?? string.Empty; FakeIPRange = item.FakeIPRange ?? string.Empty;
BlockBindingQuery = item.BlockBindingQuery ?? false; BlockBindingQuery = item.BlockBindingQuery ?? false;
BlockAAAAQuery = item.BlockAAAAQuery ?? false;
DirectDNS = item.DirectDNS ?? string.Empty; DirectDNS = item.DirectDNS ?? string.Empty;
RemoteDNS = item.RemoteDNS ?? string.Empty; RemoteDNS = item.RemoteDNS ?? string.Empty;
BootstrapDNS = item.BootstrapDNS ?? string.Empty; BootstrapDNS = item.BootstrapDNS ?? string.Empty;
@@ -108,6 +110,7 @@ public partial class DNSSettingViewModel : MyReactiveObject, ICloseable
_config.SimpleDNSItem.FakeIP = FakeIP; _config.SimpleDNSItem.FakeIP = FakeIP;
_config.SimpleDNSItem.FakeIPRange = FakeIPRange; _config.SimpleDNSItem.FakeIPRange = FakeIPRange;
_config.SimpleDNSItem.BlockBindingQuery = BlockBindingQuery; _config.SimpleDNSItem.BlockBindingQuery = BlockBindingQuery;
_config.SimpleDNSItem.BlockAAAAQuery = BlockAAAAQuery;
_config.SimpleDNSItem.DirectDNS = DirectDNS; _config.SimpleDNSItem.DirectDNS = DirectDNS;
_config.SimpleDNSItem.RemoteDNS = RemoteDNS; _config.SimpleDNSItem.RemoteDNS = RemoteDNS;
_config.SimpleDNSItem.BootstrapDNS = BootstrapDNS; _config.SimpleDNSItem.BootstrapDNS = BootstrapDNS;

View File

@@ -3,7 +3,7 @@ namespace ServiceLib.ViewModels;
public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
public Interaction<RxVoid, RxVoid> ProfilesFocusInteraction { get; } = new(); public Interaction<RxVoid, RxVoid> ProfilesFocusInteraction { get; } = new();
#region private prop #region private prop

View File

@@ -9,6 +9,7 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
[Reactive] [Reactive]
public partial string CustomCoreType { get; set; } public partial string CustomCoreType { get; set; }
[Reactive] [Reactive]
public partial string PrevProfile { get; set; } public partial string PrevProfile { get; set; }

View File

@@ -16,7 +16,7 @@ internal static class MacAppUtils
public static bool IsWindowMiniaturized(Window window) public static bool IsWindowMiniaturized(Window window)
=> window.TryGetPlatformHandle() is IMacOSTopLevelPlatformHandle { NSWindow: not 0 } handle => window.TryGetPlatformHandle() is IMacOSTopLevelPlatformHandle { NSWindow: not 0 } handle
&& objc_msgSend_bool(handle.NSWindow, sel_registerName("isMiniaturized")); && objc_msgSend_bool(handle.NSWindow, sel_registerName("isMiniaturized"));
[DllImport(LibObjC)] [DllImport(LibObjC)]
private static extern nint objc_getClass(string name); private static extern nint objc_getClass(string name);
@@ -29,7 +29,7 @@ internal static class MacAppUtils
[DllImport(LibObjC, EntryPoint = "objc_msgSend")] [DllImport(LibObjC, EntryPoint = "objc_msgSend")]
[return: MarshalAs(UnmanagedType.I1)] [return: MarshalAs(UnmanagedType.I1)]
private static extern bool objc_msgSend_bool(nint receiver, nint selector); private static extern bool objc_msgSend_bool(nint receiver, nint selector);
[DllImport(LibObjC, EntryPoint = "objc_msgSend")] [DllImport(LibObjC, EntryPoint = "objc_msgSend")]
private static extern void objc_msgSend(nint receiver, nint selector, nint argument); private static extern void objc_msgSend(nint receiver, nint selector, nint argument);
} }

View File

@@ -1,4 +1,3 @@
using ServiceLib.Models.Entities;
using System.Diagnostics; using System.Diagnostics;
using v2rayN.Desktop.Manager; using v2rayN.Desktop.Manager;
using v2rayN.Desktop.ViewModels; using v2rayN.Desktop.ViewModels;

View File

@@ -1265,8 +1265,10 @@
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertSha256Tips}" TextWrapping="Wrap">
TextWrapping="Wrap" /> <Run Text="{x:Static resx:ResUI.TbCertSha256Tips}" />
<Run FontWeight="Bold" Text="{x:Static resx:ResUI.TbXrayOnly}" />
</TextBlock>
<TextBox <TextBox
x:Name="txtCertSha256Pinning" x:Name="txtCertSha256Pinning"
Width="400" Width="400"

View File

@@ -1,5 +1,4 @@
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;

View File

@@ -242,7 +242,7 @@
x:Name="gridAdvancedDNSSettings" x:Name="gridAdvancedDNSSettings"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
ColumnDefinitions="Auto,Auto,*" ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,*"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,*">
<TextBlock <TextBlock
x:Name="txtAdvancedDNSSettingsInvalid" x:Name="txtAdvancedDNSSettingsInvalid"
@@ -338,17 +338,38 @@
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbBlockAAAAQueries}" />
<ToggleSwitch
x:Name="togBlockAAAAQuery"
Grid.Row="5"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
<TextBlock
Grid.Row="5"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbBlockAAAAQueriesTips}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="6"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" /> Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" />
<ComboBox <ComboBox
x:Name="cmbDirectExpectedIPs" x:Name="cmbDirectExpectedIPs"
Grid.Row="5" Grid.Row="6"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
IsEditable="True" /> IsEditable="True" />
<TextBlock <TextBlock
Grid.Row="5" Grid.Row="6"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -356,7 +377,7 @@
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="6" Grid.Row="7"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -365,7 +386,7 @@
<TextBox <TextBox
x:Name="txtHosts" x:Name="txtHosts"
Grid.Row="7" Grid.Row="8"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"

View File

@@ -35,6 +35,7 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FakeIPRange, v => v.cmbFakeIPRange.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.FakeIPRange, v => v.cmbFakeIPRange.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BlockAAAAQuery, v => v.togBlockAAAAQuery.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BootstrapDNS, v => v.cmbBootstrapDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.BootstrapDNS, v => v.cmbBootstrapDNS.Text).DisposeWith(disposables);

View File

@@ -28,6 +28,8 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
lstProfiles.SetValue(DragDrop.AllowDropProperty, true); lstProfiles.SetValue(DragDrop.AllowDropProperty, true);
lstProfiles.AddHandler(PointerPressedEvent, LstProfiles_PointerPressed, RoutingStrategies.Bubble, true); lstProfiles.AddHandler(PointerPressedEvent, LstProfiles_PointerPressed, RoutingStrategies.Bubble, true);
lstProfiles.AddHandler(PointerMovedEvent, LstProfiles_PointerMoved, RoutingStrategies.Bubble, true);
lstProfiles.AddHandler(PointerReleasedEvent, LstProfiles_PointerReleased, RoutingStrategies.Bubble, true);
lstProfiles.AddHandler(DragDrop.DragOverEvent, LstProfiles_DragOver, RoutingStrategies.Bubble); lstProfiles.AddHandler(DragDrop.DragOverEvent, LstProfiles_DragOver, RoutingStrategies.Bubble);
lstProfiles.AddHandler(DragDrop.DropEvent, LstProfiles_Drop, RoutingStrategies.Bubble); lstProfiles.AddHandler(DragDrop.DropEvent, LstProfiles_Drop, RoutingStrategies.Bubble);
} }
@@ -435,31 +437,66 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
#region Drag and Drop #region Drag and Drop
private static readonly DataFormat<object> LstProfilesRowFormat = private static readonly DataFormat<ProfileItemModel> LstProfilesRowFormat =
DataFormat.CreateInProcessFormat<object>("LstProfilesRow"); DataFormat.CreateInProcessFormat<ProfileItemModel>("LstProfilesRow");
private (Point, PointerPressedEventArgs)? _dragStartPoint;
private async void LstProfiles_PointerPressed(object? sender, PointerPressedEventArgs e) private void LstProfiles_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(this).Properties;
if (properties.IsLeftButtonPressed)
{
_dragStartPoint = (e.GetPosition(this), e);
}
}
private async void LstProfiles_PointerMoved(object? sender, PointerEventArgs e)
{ {
try try
{ {
if (e.Source is not Visual visualSource) if (_dragStartPoint == null)
{ {
return; return;
} }
var properties = e.GetCurrentPoint(this).Properties;
if (!properties.IsLeftButtonPressed)
{
_dragStartPoint = null;
return;
}
var currentPoint = e.GetPosition(this);
var startPoint = _dragStartPoint.Value.Item1;
var delta = startPoint - currentPoint;
var threshold = new Vector(4, 4);
if (!(Math.Abs(delta.X) >= threshold.X) && !(Math.Abs(delta.Y) >= threshold.Y))
{
return;
}
var dragStartEventArgs = _dragStartPoint.Value.Item2;
_dragStartPoint = null;
if (e.Source is not Visual visualSource)
{
return;
}
var row = visualSource.FindAncestorOfType<DataGridRow>(true); var row = visualSource.FindAncestorOfType<DataGridRow>(true);
if (row?.DataContext == null) if (row?.DataContext == null)
{ {
return; return;
} }
if (e.GetCurrentPoint(row).Properties.IsLeftButtonPressed) e.Handled = true;
{
var dragData = new DataTransfer(); var dragData = new DataTransfer();
var item = DataTransferItem.Create(LstProfilesRowFormat, row.DataContext); var item = DataTransferItem.Create(LstProfilesRowFormat, row.DataContext as ProfileItemModel);
dragData.Add(item);
await DragDrop.DoDragDropAsync(e, dragData, DragDropEffects.Move); dragData.Add(item);
}
await DragDrop.DoDragDropAsync(dragStartEventArgs, dragData, DragDropEffects.Move);
} }
catch catch
{ {
@@ -467,6 +504,11 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
} }
} }
private void LstProfiles_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
_dragStartPoint = null;
}
private void LstProfiles_DragOver(object? sender, DragEventArgs e) private void LstProfiles_DragOver(object? sender, DragEventArgs e)
{ {
if (!e.DataTransfer.Contains(LstProfilesRowFormat)) if (!e.DataTransfer.Contains(LstProfilesRowFormat))

View File

@@ -1593,8 +1593,10 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCertSha256Tips}" TextWrapping="Wrap">
TextWrapping="Wrap" /> <Run Text="{x:Static resx:ResUI.TbCertSha256Tips}" />
<Run FontWeight="Bold" Text="{x:Static resx:ResUI.TbXrayOnly}" />
</TextBlock>
<TextBox <TextBox
x:Name="txtCertSha256Pinning" x:Name="txtCertSha256Pinning"
Width="400" Width="400"

View File

@@ -278,6 +278,7 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -387,17 +388,39 @@
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbBlockAAAAQueries}" />
<ToggleButton
x:Name="togBlockAAAAQuery"
Grid.Row="5"
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="5"
Grid.Column="2"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbBlockAAAAQueriesTips}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="6"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" /> Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" />
<ComboBox <ComboBox
x:Name="cmbDirectExpectedIPs" x:Name="cmbDirectExpectedIPs"
Grid.Row="5" Grid.Row="6"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
IsEditable="True" IsEditable="True"
Style="{StaticResource DefComboBox}" /> Style="{StaticResource DefComboBox}" />
<TextBlock <TextBlock
Grid.Row="5" Grid.Row="6"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -406,7 +429,7 @@
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="6" Grid.Row="7"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
@@ -415,7 +438,7 @@
Text="{x:Static resx:ResUI.TbDNSHostsConfig}" /> Text="{x:Static resx:ResUI.TbDNSHostsConfig}" />
<TextBox <TextBox
x:Name="txtHosts" x:Name="txtHosts"
Grid.Row="7" Grid.Row="8"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"

View File

@@ -31,6 +31,7 @@ public partial class DNSSettingWindow
this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FakeIPRange, v => v.cmbFakeIPRange.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.FakeIPRange, v => v.cmbFakeIPRange.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BlockAAAAQuery, v => v.togBlockAAAAQuery.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BootstrapDNS, v => v.cmbBootstrapDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.BootstrapDNS, v => v.cmbBootstrapDNS.Text).DisposeWith(disposables);