mirror of
https://github.com/2dust/v2rayN.git
synced 2026-08-19 13:42:04 +03:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d514ff480 | ||
|
|
11bd52b6a5 | ||
|
|
3b3fe8def3 | ||
|
|
c9e843aa3b | ||
|
|
7be264c65c | ||
|
|
5e6660aeee | ||
|
|
5b27f4836f | ||
|
|
e0ad2c46e6 | ||
|
|
230a2f6773 | ||
|
|
e7cc4c832e | ||
|
|
fb7033aab5 | ||
|
|
b6cc3913ac | ||
|
|
bc06a5dc21 | ||
|
|
9c1951178c | ||
|
|
b3df42b9ba | ||
|
|
e01717d832 | ||
|
|
8b4063b44b | ||
|
|
3136f573dd | ||
|
|
e101b1d7b0 | ||
|
|
31044f449d | ||
|
|
eacf8b0784 | ||
|
|
f347fe1c03 | ||
|
|
71208eb711 | ||
|
|
339eaa6bfd | ||
|
|
717ef7f0a7 | ||
|
|
4a9d83f9dc | ||
|
|
1ac14d0903 | ||
|
|
08d423e1b4 | ||
|
|
9638b4c368 | ||
|
|
1034ce6e02 |
4
.github/workflows/build-windows-x86.yml
vendored
4
.github/workflows/build-windows-x86.yml
vendored
@@ -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
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
$xrayTag = "v$xrayVer"
|
$xrayTag = "v$xrayVer"
|
||||||
$singTag = "v$singVer"
|
$singTag = "v$singVer"
|
||||||
|
|
||||||
$xrayUrl = "https://github.com/XTLS/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
|
$xrayUrl = "https://github.com/autorepobot/Xray/releases/download/$xrayTag/Xray-windows-32.zip"
|
||||||
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
|
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
|
||||||
|
|
||||||
Write-Host "Bundled Xray version: $xrayVer"
|
Write-Host "Bundled Xray version: $xrayVer"
|
||||||
|
|||||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -69,9 +69,9 @@ 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
|
||||||
@@ -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 }}
|
||||||
|
|||||||
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
@@ -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,18 @@ 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
|
working-directory: ./v2rayN
|
||||||
run: dotnet test ./ServiceLib.Tests
|
run: dotnet test ./ServiceLib.Tests -c Release --no-build --results-directory ./TestResults -- --report-trx
|
||||||
|
|
||||||
|
- name: Comment PR with results
|
||||||
|
if: always()
|
||||||
|
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||||
|
with:
|
||||||
|
files: ./v2rayN/TestResults/*.trx
|
||||||
|
comment_mode: failures
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ Read the Wiki for usage guides and configuration details.
|
|||||||
| Linux | ✅ | - | ✅ | ✅ | ✅ |
|
| Linux | ✅ | - | ✅ | ✅ | ✅ |
|
||||||
| macOS | ✅ | - | ✅ | - | - |
|
| macOS | ✅ | - | ✅ | - | - |
|
||||||
|
|
||||||
|
Minimum OS requirements: [Release files introduction](https://github.com/2dust/v2rayN/wiki/Release-files-introduction) / 最低系统要求:[发布文件介绍](https://github.com/2dust/v2rayN/wiki/Release-files-introduction)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## GPG Verification / GPG 签名校验
|
## GPG Verification / GPG 签名校验
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
|
|||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>13.7</string>
|
<string>13.6</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>7.24.5</Version>
|
<Version>7.24.7</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -6,32 +6,32 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
|
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
|
<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="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.0" />
|
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
|
||||||
<PackageVersion Include="CliWrap" Version="3.10.2" />
|
<PackageVersion Include="CliWrap" Version="3.10.4" />
|
||||||
<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" />
|
||||||
<PackageVersion Include="ReactiveUI" Version="24.0.0" />
|
<PackageVersion Include="ReactiveUI" Version="24.1.0" />
|
||||||
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.1.0" />
|
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
|
||||||
<PackageVersion Include="ReactiveUI.WPF" Version="24.0.0" />
|
<PackageVersion Include="ReactiveUI.WPF" Version="24.1.0" />
|
||||||
<PackageVersion Include="Semi.Avalonia" Version="12.1.0" />
|
<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" />
|
<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.3.10" />
|
<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.0" />
|
||||||
|
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.0-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="4.0.0" />
|
||||||
<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" />
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -251,4 +251,12 @@ internal static class CoreConfigTestFactory
|
|||||||
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
|
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Config CreateConfigWithTun(ECoreType coreType, bool enableIPv6Address)
|
||||||
|
{
|
||||||
|
var config = CreateConfig(coreType);
|
||||||
|
config.TunModeItem.EnableTun = true;
|
||||||
|
config.TunModeItem.EnableIPv6Address = enableIPv6Address;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,100 @@ 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_BindInterface_ShouldUseDialBindInterface()
|
public async Task GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
|
||||||
|
{
|
||||||
|
// The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast).
|
||||||
|
// They are deserialized into List<Rule4Sbox>, so a schema mismatch in the
|
||||||
|
// embedded template makes JsonUtils.Deserialize return null and silently
|
||||||
|
// drops every one of them.
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
config.TunModeItem.EnableTun = true;
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
IsTunEnabled = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
|
||||||
|
await cfg.route.rules.Should().Contain(
|
||||||
|
r => r.action == "reject"
|
||||||
|
&& r.network != null && r.network.Contains("udp")
|
||||||
|
&& r.port != null && r.port.Contains(5353),
|
||||||
|
"the embedded tun rules must reject mDNS/NetBIOS noise");
|
||||||
|
await cfg.route.rules.Should().Contain(
|
||||||
|
r => r.action == "reject"
|
||||||
|
&& r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"),
|
||||||
|
"the embedded tun rules must reject multicast traffic");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
|
||||||
|
{
|
||||||
|
// Regression test: traffic addressed to the TUN interface's own addresses must
|
||||||
|
// never reach an outbound. auto_route hijacks the default route, so `direct`
|
||||||
|
// writes such a packet straight back into the TUN, which routes it to the
|
||||||
|
// outbound again - an infinite loop that pins a CPU core. Observed in the wild
|
||||||
|
// with WebRTC ICE connectivity checks against the TUN's own fc00::/7 ULA
|
||||||
|
// address, sustaining ~8k packets/s out of the TUN interface.
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
config.TunModeItem.EnableTun = true;
|
||||||
|
config.TunModeItem.EnableIPv6Address = true;
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
IsTunEnabled = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
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!)
|
||||||
|
{
|
||||||
|
var self = IPAddress.Parse(address.Split('/').First());
|
||||||
|
var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
|
||||||
|
var expected = $"{self}/{hostBits}";
|
||||||
|
await cfg.route.rules.Should().Contain(
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The match has to stay on the addresses themselves. sing-tun derives the TUN's DNS
|
||||||
|
// entry from the address right after the interface's own, and every prefix offered
|
||||||
|
// here leaves room for it, so a prefix match would drop system name lookups too.
|
||||||
|
var dropRule = cfg.route.rules.First(r =>
|
||||||
|
r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0);
|
||||||
|
//dropRule.ip_cidr!.Should().OnlyContain(c =>
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
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";
|
||||||
@@ -71,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);
|
||||||
@@ -97,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);
|
||||||
@@ -124,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);
|
||||||
@@ -157,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);
|
||||||
@@ -190,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);
|
||||||
@@ -244,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);
|
||||||
@@ -293,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,
|
||||||
@@ -338,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 =>
|
||||||
@@ -347,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);
|
||||||
@@ -362,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";
|
||||||
@@ -405,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");
|
||||||
@@ -442,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 =
|
||||||
@@ -484,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 =>
|
||||||
@@ -492,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";
|
||||||
@@ -508,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);
|
||||||
@@ -551,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);
|
||||||
@@ -579,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.
|
||||||
@@ -612,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);
|
||||||
@@ -649,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,22 +549,67 @@ 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Test]
|
||||||
public void GenerateClientConfigContent_TunRouteExcludeAddress()
|
[Arguments(false)]
|
||||||
|
[Arguments(true)]
|
||||||
|
public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address)
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
await result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||||
|
|
||||||
|
await tunInbound.Should().NotBeNull();
|
||||||
|
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
|
||||||
|
await tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0");
|
||||||
|
|
||||||
|
// EnableIPv6Address governs the interface address only, never the routing table.
|
||||||
|
await tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
|
||||||
|
config.TunModeItem.EnableIPv6Address = false;
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
await result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||||
|
|
||||||
|
await tunInbound.Should().NotBeNull();
|
||||||
|
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':'));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GenerateClientConfigContent_TunRouteExcludeAddress()
|
||||||
{
|
{
|
||||||
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
|
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
|
||||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
@@ -581,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);
|
||||||
@@ -621,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,21 @@
|
|||||||
<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">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="xunit.v3" />
|
|
||||||
</ItemGroup>
|
</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>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace ServiceLib.Common;
|
namespace ServiceLib.Common;
|
||||||
|
|
||||||
@@ -134,4 +135,30 @@ public static class Extension
|
|||||||
.Replace("\r", replacement)
|
.Replace("\r", replacement)
|
||||||
.Replace("\n", replacement);
|
.Replace("\n", replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
|
||||||
|
this Interaction<TInput, TOutput> interaction,
|
||||||
|
TInput input,
|
||||||
|
TOutput defaultValue = default!,
|
||||||
|
[CallerMemberName] string memberName = "",
|
||||||
|
[CallerFilePath] string filePath = "",
|
||||||
|
[CallerLineNumber] int lineNumber = 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await interaction.Handle(input);
|
||||||
|
}
|
||||||
|
catch (UnhandledInteractionException<TInput, TOutput> ex)
|
||||||
|
{
|
||||||
|
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
|
||||||
|
Logging.SaveLog(title, ex);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var title = $"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}";
|
||||||
|
Logging.SaveLog(title, ex);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -753,11 +753,11 @@ public class Utils
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetFreePort(int defaultPort = 0)
|
public static int GetFreePort(int defaultPort)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!(defaultPort == 0 || Utils.PortInUse(defaultPort)))
|
if (!PortInUse(defaultPort))
|
||||||
{
|
{
|
||||||
return defaultPort;
|
return defaultPort;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,7 +586,6 @@ public static class ConfigHandler
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async Task<int> AddCustomOutboundServer(Config config, ProfileItem profileItem, bool blDelete, bool toFile = true)
|
public static async Task<int> AddCustomOutboundServer(Config config, ProfileItem profileItem, bool blDelete, bool toFile = true)
|
||||||
{
|
{
|
||||||
var fileName = profileItem.Address;
|
var fileName = profileItem.Address;
|
||||||
@@ -1739,8 +1738,16 @@ public static class ConfigHandler
|
|||||||
SubItem? subItem)
|
SubItem? subItem)
|
||||||
{
|
{
|
||||||
var subRemarks = subItem?.Remarks;
|
var subRemarks = subItem?.Remarks;
|
||||||
// Safe Mode: Only allow full configuration if it's not from a subscription
|
// Prioritize using complete custom parsing, followed by custom outbound parsing.
|
||||||
var lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks);
|
var lstProfiles = V2rayFmt.ResolveToCustom(strData, subRemarks);
|
||||||
|
if (lstProfiles.Count == 0)
|
||||||
|
{
|
||||||
|
lstProfiles = SingboxFmt.ResolveToCustom(strData, subRemarks);
|
||||||
|
}
|
||||||
|
if (lstProfiles.Count == 0)
|
||||||
|
{
|
||||||
|
lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks);
|
||||||
|
}
|
||||||
if (lstProfiles.Count == 0)
|
if (lstProfiles.Count == 0)
|
||||||
{
|
{
|
||||||
lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
|
lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
|
||||||
@@ -1750,7 +1757,7 @@ public static class ConfigHandler
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = await AddCustomOutboundServers(config, lstProfiles, subid, isSub);
|
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||||
if (count > 0)
|
if (count > 0)
|
||||||
{
|
{
|
||||||
return count;
|
return count;
|
||||||
@@ -1786,7 +1793,7 @@ public static class ConfigHandler
|
|||||||
var subRemarks = subItem.Remarks;
|
var subRemarks = subItem.Remarks;
|
||||||
var customCoreType = subItem.CustomCoreType!.Value;
|
var customCoreType = subItem.CustomCoreType!.Value;
|
||||||
|
|
||||||
List<ProfileItem>? lstProfiles = customCoreType switch
|
var lstProfiles = customCoreType switch
|
||||||
{
|
{
|
||||||
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),
|
||||||
@@ -1800,7 +1807,7 @@ public static class ConfigHandler
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = await AddCustomOutboundServers(config, lstProfiles, subid, isSub);
|
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||||
if (count > 0)
|
if (count > 0)
|
||||||
{
|
{
|
||||||
return count;
|
return count;
|
||||||
@@ -1810,7 +1817,7 @@ public static class ConfigHandler
|
|||||||
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);
|
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<int> AddCustomOutboundServers(
|
private static async Task<int> AddBatchCustomServers(
|
||||||
Config config,
|
Config config,
|
||||||
List<ProfileItem> lstProfiles,
|
List<ProfileItem> lstProfiles,
|
||||||
string subid,
|
string subid,
|
||||||
@@ -1821,11 +1828,22 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
it.Subid = subid;
|
it.Subid = subid;
|
||||||
it.IsSub = isSub;
|
it.IsSub = isSub;
|
||||||
|
|
||||||
|
if (it.ConfigType == EConfigType.Custom)
|
||||||
|
{
|
||||||
|
if (await AddCustomServer(config, it, true) == 0)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (await AddCustomOutboundServer(config, it, true) == 0)
|
if (await AddCustomOutboundServer(config, it, true) == 0)
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.Collections.Specialized;
|
|
||||||
|
|
||||||
namespace ServiceLib.Handler.Fmt;
|
namespace ServiceLib.Handler.Fmt;
|
||||||
|
|
||||||
public class Hysteria2Fmt : BaseFmt
|
public class Hysteria2Fmt : BaseFmt
|
||||||
@@ -168,8 +166,21 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
}
|
}
|
||||||
if (item.CertSha.IsNullOrEmpty())
|
if (item.CertSha.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
var pinSHA256 = GetQueryDecoded(query, "pinSHA256");
|
||||||
|
item.CertSha = pinSHA256;
|
||||||
|
if (!pinSHA256.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
// NOTE:
|
||||||
|
// To accommodate Xray changes,
|
||||||
|
// 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.
|
||||||
|
// 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.SetProtocolExtra(item.GetProtocolExtra() with
|
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||||
{
|
{
|
||||||
Ports = GetQueryDecoded(query, "mport"),
|
Ports = GetQueryDecoded(query, "mport"),
|
||||||
@@ -212,6 +223,10 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
var sha = item.CertSha;
|
var sha = item.CertSha;
|
||||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
||||||
}
|
}
|
||||||
|
if (!item.EchConfigList.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
||||||
|
}
|
||||||
var protocolExtraItem = item.GetProtocolExtra();
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
|
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||||
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public class SingboxFmt : BaseFmt
|
|||||||
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
|
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
|
||||||
var profileItem = new ProfileItem
|
var profileItem = new ProfileItem
|
||||||
{
|
{
|
||||||
|
ConfigType = EConfigType.Custom,
|
||||||
CoreType = ECoreType.sing_box,
|
CoreType = ECoreType.sing_box,
|
||||||
Address = fileName,
|
Address = fileName,
|
||||||
Remarks = subRemarks ?? "singbox_custom",
|
Remarks = subRemarks ?? "singbox_custom",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ public class V2rayFmt : BaseFmt
|
|||||||
|
|
||||||
var profileItem = new ProfileItem
|
var profileItem = new ProfileItem
|
||||||
{
|
{
|
||||||
|
ConfigType = EConfigType.Custom,
|
||||||
CoreType = ECoreType.Xray,
|
CoreType = ECoreType.Xray,
|
||||||
Address = fileName,
|
Address = fileName,
|
||||||
Remarks = jsonObject["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom",
|
Remarks = jsonObject["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom",
|
||||||
|
|||||||
6
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
6
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
@@ -178,7 +178,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Failed to import custom configuration Configuration 的本地化字符串。
|
/// 查找类似 Failed to import configuration 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string FailedImportedCustomServer {
|
public static string FailedImportedCustomServer {
|
||||||
get {
|
get {
|
||||||
@@ -268,7 +268,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Please browse to import Configuration configuration 的本地化字符串。
|
/// 查找类似 Please browse to import configuration 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string FillServerAddressCustom {
|
public static string FillServerAddressCustom {
|
||||||
get {
|
get {
|
||||||
@@ -2653,7 +2653,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Custom configuration Configuration imported successfully 的本地化字符串。
|
/// 查找类似 Custom configuration imported successfully 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string SuccessfullyImportedCustomServer {
|
public static string SuccessfullyImportedCustomServer {
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
<value>Failed to get the default configuration</value>
|
<value>Failed to get the default configuration</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FailedImportedCustomServer" xml:space="preserve">
|
<data name="FailedImportedCustomServer" xml:space="preserve">
|
||||||
<value>Failed to import custom configuration Configuration</value>
|
<value>Failed to import configuration</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FailedReadConfiguration" xml:space="preserve">
|
<data name="FailedReadConfiguration" xml:space="preserve">
|
||||||
<value>Failed to read configuration file</value>
|
<value>Failed to read configuration file</value>
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
<value>Configuration successful. {0}</value>
|
<value>Configuration successful. {0}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SuccessfullyImportedCustomServer" xml:space="preserve">
|
<data name="SuccessfullyImportedCustomServer" xml:space="preserve">
|
||||||
<value>Custom configuration Configuration imported successfully</value>
|
<value>Custom configuration imported successfully</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
|
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
|
||||||
<value>{0} Configurations have been imported from clipboard</value>
|
<value>{0} Configurations have been imported from clipboard</value>
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
<value>All</value>
|
<value>All</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillServerAddressCustom" xml:space="preserve">
|
<data name="FillServerAddressCustom" xml:space="preserve">
|
||||||
<value>Please browse to import Configuration configuration</value>
|
<value>Please browse to import configuration</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Speedtesting" xml:space="preserve">
|
<data name="Speedtesting" xml:space="preserve">
|
||||||
<value>Testing...</value>
|
<value>Testing...</value>
|
||||||
|
|||||||
@@ -268,7 +268,7 @@
|
|||||||
<value>Сначала выберите сервер</value>
|
<value>Сначала выберите сервер</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveDuplicateServerResult" xml:space="preserve">
|
<data name="RemoveDuplicateServerResult" xml:space="preserve">
|
||||||
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}.</value>
|
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveServer" xml:space="preserve">
|
<data name="RemoveServer" xml:space="preserve">
|
||||||
<value>Вы уверены, что хотите удалить сервер?</value>
|
<value>Вы уверены, что хотите удалить сервер?</value>
|
||||||
@@ -1048,7 +1048,7 @@
|
|||||||
<value>HTTP-заголовки</value>
|
<value>HTTP-заголовки</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TipHttpOutboundHeaders" xml:space="preserve">
|
<data name="TipHttpOutboundHeaders" xml:space="preserve">
|
||||||
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк.</value>
|
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvPrevProfile" xml:space="preserve">
|
<data name="LvPrevProfile" xml:space="preserve">
|
||||||
<value>Псевдоним предыдущего прокси</value>
|
<value>Псевдоним предыдущего прокси</value>
|
||||||
@@ -1120,28 +1120,28 @@
|
|||||||
<value>Длина фрагмента</value>
|
<value>Длина фрагмента</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию.</value>
|
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
<value>Интервал фрагментации</value>
|
<value>Интервал фрагментации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию.</value>
|
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
<value>Макс. разделение</value>
|
<value>Макс. разделение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию.</value>
|
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
<value>Резервная задержка</value>
|
<value>Резервная задержка</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию.</value>
|
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillFragmentParameterError" xml:space="preserve">
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100).</value>
|
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
|
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
|
||||||
@@ -1351,7 +1351,7 @@
|
|||||||
<value>Пароль sudo системы</value>
|
<value>Пароль sudo системы</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
|
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
|
||||||
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска.</value>
|
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportHeaderType5" xml:space="preserve">
|
<data name="TransportHeaderType5" xml:space="preserve">
|
||||||
<value>XHTTP-режим</value>
|
<value>XHTTP-режим</value>
|
||||||
@@ -1411,7 +1411,7 @@
|
|||||||
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
|
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
|
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
|
||||||
<value>Неверный пароль, попробуйте ещё раз.</value>
|
<value>Неверный пароль, попробуйте ещё раз</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbMldsa65Verify" xml:space="preserve">
|
<data name="TbMldsa65Verify" xml:space="preserve">
|
||||||
<value>Mldsa65Verify</value>
|
<value>Mldsa65Verify</value>
|
||||||
@@ -1492,7 +1492,7 @@
|
|||||||
<value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value>
|
<value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||||
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value>
|
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||||
<value>Начинается разбор и обработка содержимого подписки</value>
|
<value>Начинается разбор и обработка содержимого подписки</value>
|
||||||
@@ -1501,7 +1501,7 @@
|
|||||||
<value>Выбрать профиль</value>
|
<value>Выбрать профиль</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFakeIPTips" xml:space="preserve">
|
<data name="TbFakeIPTips" xml:space="preserve">
|
||||||
<value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
|
<value>Применяется глобально по умолчанию; встроенная фильтрация FakeIP есть только в sing-box</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>Добавьте хотя бы одну конфигурацию</value>
|
<value>Добавьте хотя бы одну конфигурацию</value>
|
||||||
@@ -1588,7 +1588,7 @@
|
|||||||
<value>Привязанный сертификат (заполните любое из полей)
|
<value>Привязанный сертификат (заполните любое из полей)
|
||||||
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
|
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
|
||||||
|
|
||||||
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
|
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFetchCert" xml:space="preserve">
|
<data name="TbFetchCert" xml:space="preserve">
|
||||||
<value>Получить сертификат</value>
|
<value>Получить сертификат</value>
|
||||||
@@ -1642,10 +1642,10 @@
|
|||||||
<value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value>
|
<value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDirectResolveStrategyTips" xml:space="preserve">
|
<data name="TbDirectResolveStrategyTips" xml:space="preserve">
|
||||||
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль.</value>
|
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRemoteResolveStrategyTips" xml:space="preserve">
|
<data name="TbRemoteResolveStrategyTips" xml:space="preserve">
|
||||||
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль.</value>
|
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbHopInt7" xml:space="preserve">
|
<data name="TbHopInt7" xml:space="preserve">
|
||||||
<value>Интервал смены портов (Port Hopping)</value>
|
<value>Интервал смены портов (Port Hopping)</value>
|
||||||
@@ -1660,37 +1660,37 @@
|
|||||||
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
|
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
|
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
|
||||||
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел.</value>
|
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupCycleDependency" xml:space="preserve">
|
<data name="MsgGroupCycleDependency" xml:space="preserve">
|
||||||
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен.</value>
|
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
|
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
|
||||||
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
|
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupChildNodeError" xml:space="preserve">
|
<data name="MsgGroupChildNodeError" xml:space="preserve">
|
||||||
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен.</value>
|
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
|
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
|
||||||
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
|
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
|
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
|
||||||
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен.</value>
|
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
|
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
|
||||||
<value>У группы {0} нет допустимых дочерних узлов.</value>
|
<value>У группы {0} нет допустимых дочерних узлов</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
|
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
|
||||||
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел.</value>
|
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
|
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
|
||||||
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел.</value>
|
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
|
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
|
||||||
<value>Предыдущий прокси подписки {0} не найден. Пропущено.</value>
|
<value>Предыдущий прокси подписки {0} не найден. Пропущено</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
|
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
|
||||||
<value>Следующий прокси подписки {0} не найден. Пропущено.</value>
|
<value>Следующий прокси подписки {0} не найден. Пропущено</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuGenGroupServer" xml:space="preserve">
|
<data name="menuGenGroupServer" xml:space="preserve">
|
||||||
<value>Сгенерировать группу политик</value>
|
<value>Сгенерировать группу политик</value>
|
||||||
@@ -1756,7 +1756,7 @@
|
|||||||
<value>Привязать интерфейс</value>
|
<value>Привязать интерфейс</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||||
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value>
|
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>Общий ключ (PSK)</value>
|
<value>Общий ключ (PSK)</value>
|
||||||
@@ -1780,13 +1780,13 @@
|
|||||||
<value>Доступно обновление</value>
|
<value>Доступно обновление</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя.</value>
|
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
||||||
<value>Адреса, исключаемые из маршрутизации</value>
|
<value>Адреса, исключаемые из маршрутизации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
|
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
|
||||||
<value>Разделяйте запятыми (,).</value>
|
<value>Разделяйте запятыми (,)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
|
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
|
||||||
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
|
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
|
||||||
@@ -1798,22 +1798,22 @@
|
|||||||
<value>Включить финальную фрагментацию (Final Fragment)</value>
|
<value>Включить финальную фрагментацию (Final Fragment)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку.</value>
|
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||||
<value>DNS через Bridge</value>
|
<value>DNS через Bridge</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||||
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM).</value>
|
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbHy2RealmUrl" xml:space="preserve">
|
<data name="TbHy2RealmUrl" xml:space="preserve">
|
||||||
<value>Realm URL</value>
|
<value>Realm URL</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
||||||
<value>Некорректный Realm URL.</value>
|
<value>Некорректный Realm URL</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
|
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
|
||||||
<value>Введите корректный JSON заголовков HTTP-запроса.</value>
|
<value>Введите корректный JSON заголовков HTTP-запроса</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
||||||
<value>Формат: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
<value>Формат: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
||||||
@@ -1822,12 +1822,42 @@
|
|||||||
<value>Размер пакета Gecko (мин/макс)</value>
|
<value>Размер пакета Gecko (мин/макс)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbLegacyProtectTip" xml:space="preserve">
|
<data name="TbLegacyProtectTip" xml:space="preserve">
|
||||||
<value>Если включено, используется sing-box TUN; иначе — xray TUN.</value>
|
<value>Если включено, используется sing-box TUN; иначе — xray TUN</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRootCertificateProvider" xml:space="preserve">
|
<data name="TbRootCertificateProvider" xml:space="preserve">
|
||||||
<value>Поставщик корневых сертификатов</value>
|
<value>Поставщик корневых сертификатов</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRootCertificateProviderTip" xml:space="preserve">
|
<data name="TbRootCertificateProviderTip" xml:space="preserve">
|
||||||
<value>Применяется только к загрузкам и сетевым запросам графического интерфейса v2rayN. Не влияет на проверку сертификатов ядром.</value>
|
<value>Применяется только к загрузкам и сетевым запросам графического интерфейса v2rayN. Не влияет на проверку сертификатов ядром</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
|
||||||
|
<value>Стратегия разрешения DNS при подключении к прокси</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
|
||||||
|
<value>Не рекомендуется; может вызвать петли маршрутизации</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableHappyEyeballs" xml:space="preserve">
|
||||||
|
<value>Включить Happy Eyeballs</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
|
||||||
|
<value>Требуется стратегия UseIP. При включении одновременно устанавливаются соединения по IPv4 и IPv6 и автоматически выбирается более быстрый доступный путь</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbIpv6Address" xml:space="preserve">
|
||||||
|
<value>Адрес IPv6</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbIpv4Address" xml:space="preserve">
|
||||||
|
<value>Адрес IPv4</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuAddCustomOutboundServer" xml:space="preserve">
|
||||||
|
<value>Добавить пользовательский outbound</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
|
||||||
|
<value>Файл пользовательского outbound {0} не найден: {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbCustomOutboundTip" xml:space="preserve">
|
||||||
|
<value>Для xray/sing-box поддерживается только один outbound/endpoint</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvCustomCoreType" xml:space="preserve">
|
||||||
|
<value>Ядро пользовательской конфигурации</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1417,7 +1417,7 @@
|
|||||||
<value>新增 [Anytls] 節點</value>
|
<value>新增 [Anytls] 節點</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRemoteDNS" xml:space="preserve">
|
<data name="TbRemoteDNS" xml:space="preserve">
|
||||||
<value>遠程 DNS</value>
|
<value>遠端 DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDomesticDNS" xml:space="preserve">
|
<data name="TbDomesticDNS" xml:space="preserve">
|
||||||
<value>直連 DNS</value>
|
<value>直連 DNS</value>
|
||||||
@@ -1429,16 +1429,16 @@
|
|||||||
<value>代理目標解析策略</value>
|
<value>代理目標解析策略</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
||||||
<value>新增常用 DNS Hosts</value>
|
<value>新增常用 DNS 主機對應</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFakeIP" xml:space="preserve">
|
<data name="TbFakeIP" xml:space="preserve">
|
||||||
<value>FakeIP</value>
|
<value>FakeIP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbBlockSVCBHTTPSQueries" xml:space="preserve">
|
<data name="TbBlockSVCBHTTPSQueries" xml:space="preserve">
|
||||||
<value>阻止 SVCB 和 HTTPS 查詢</value>
|
<value>封鎖 SVCB 與 HTTPS 查詢</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDNSHostsConfig" xml:space="preserve">
|
<data name="TbDNSHostsConfig" xml:space="preserve">
|
||||||
<value>DNS Hosts:(“網域名稱1 ip1 ip2” 一行一個)</value>
|
<value>DNS 主機對應:(每行一組「網域名稱1 ip1 ip2」)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ThBasicDNSSettings" xml:space="preserve">
|
<data name="ThBasicDNSSettings" xml:space="preserve">
|
||||||
<value>DNS 基礎設定</value>
|
<value>DNS 基礎設定</value>
|
||||||
@@ -1447,49 +1447,49 @@
|
|||||||
<value>DNS 進階設定</value>
|
<value>DNS 進階設定</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
|
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
|
||||||
<value>校驗相應地區域名 IP</value>
|
<value>驗證區域網域名稱的 IP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
|
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
|
||||||
<value>配置後,會對相應地區域名(如 geosite:cn - geoip:cn)的返回 IP 進行校驗,僅返回期望 IP</value>
|
<value>設定後,系統會驗證區域網域名稱(例如 geosite:cn - geoip:cn)所傳回的 IP,並只傳回預期的 IP。</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbCustomDNSEnable" xml:space="preserve">
|
<data name="TbCustomDNSEnable" xml:space="preserve">
|
||||||
<value>啟用自訂 DNS</value>
|
<value>啟用自訂 DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
|
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
|
||||||
<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 可用性查詢</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||||
<value>請填寫正確的配置範本</value>
|
<value>請填寫正確的設定範本</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuFullConfigTemplate" xml:space="preserve">
|
<data name="menuFullConfigTemplate" xml:space="preserve">
|
||||||
<value>完整配置範本設定</value>
|
<value>完整設定範本</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFullConfigTemplateEnable" xml:space="preserve">
|
<data name="TbFullConfigTemplateEnable" xml:space="preserve">
|
||||||
<value>啟用完整配置範本</value>
|
<value>啟用完整設定範本</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRayFullConfigTemplate" xml:space="preserve">
|
<data name="TbRayFullConfigTemplate" xml:space="preserve">
|
||||||
<value>v2ray 完整配置範本</value>
|
<value>v2ray 完整設定範本</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRayFullConfigTemplateDesc" xml:space="preserve">
|
<data name="TbRayFullConfigTemplateDesc" xml:space="preserve">
|
||||||
<value>僅添加出站配置,routing.balancers 和 routing.rules.outboundTag,點擊查看文檔</value>
|
<value>僅新增出站設定、routing.balancers 與 routing.rules.outboundTag。點選以查看說明文件</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
|
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
|
||||||
<value>不添加非代理協定出站</value>
|
<value>不新增非代理協定出站</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
|
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
|
||||||
<value>設定上游代理 tag</value>
|
<value>設定上游代理標籤</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSBFullConfigTemplate" xml:space="preserve">
|
<data name="TbSBFullConfigTemplate" xml:space="preserve">
|
||||||
<value>sing-box 完整配置範本</value>
|
<value>sing-box 完整設定範本</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSBFullConfigTemplateDesc" xml:space="preserve">
|
<data name="TbSBFullConfigTemplateDesc" xml:space="preserve">
|
||||||
<value>僅添加出站和端點配置,點擊查看文檔</value>
|
<value>僅新增出站與端點設定。點選以查看說明文件</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||||
<value>此功能供高級用戶和有特殊需求的用戶使用。 啟用此功能後,將忽略 Core 的基礎設定,DNS 設定 ,路由設定。你需要保證系統代理的埠和流量統計等功能的配置正確,一切都由你來設定。</value>
|
<value>此功能適合進階使用者與有特殊需求的使用者。啟用後,Core 基礎設定、DNS 設定與路由設定將被忽略。請確認系統代理連接埠、流量統計及其他相關設定皆正確;所有項目都必須由您自行設定。</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||||
<value>開始解析和處理訂閱內容</value>
|
<value>開始解析和處理訂閱內容</value>
|
||||||
@@ -1498,10 +1498,10 @@
|
|||||||
<value>選擇節點</value>
|
<value>選擇節點</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFakeIPTips" xml:space="preserve">
|
<data name="TbFakeIPTips" xml:space="preserve">
|
||||||
<value>默認全局生效,僅在 sing-box 中內置 FakeIP 過濾。</value>
|
<value>預設會套用至全域;內建 FakeIP 過濾僅支援 sing-box。</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>請至少添加一個節點</value>
|
<value>請至少新增一個設定檔</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
||||||
<value>策略組</value>
|
<value>策略組</value>
|
||||||
@@ -1627,7 +1627,7 @@
|
|||||||
<value>提供過期快取(Serve Stale)</value>
|
<value>提供過期快取(Serve Stale)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbParallelQuery" xml:space="preserve">
|
<data name="TbParallelQuery" xml:space="preserve">
|
||||||
<value>并行查詢</value>
|
<value>並行查詢</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDomesticDNSTips" xml:space="preserve">
|
<data name="TbDomesticDNSTips" xml:space="preserve">
|
||||||
<value>預設僅在路由期間進行解析時調用</value>
|
<value>預設僅在路由期間進行解析時調用</value>
|
||||||
@@ -1849,4 +1849,16 @@
|
|||||||
<data name="TbIpv4Address" xml:space="preserve">
|
<data name="TbIpv4Address" xml:space="preserve">
|
||||||
<value>Ipv4 位址</value>
|
<value>Ipv4 位址</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuAddCustomOutboundServer" xml:space="preserve">
|
||||||
|
<value>新增自訂出站</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
|
||||||
|
<value>自訂出站 {0} 的檔案未找到:{1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbCustomOutboundTip" xml:space="preserve">
|
||||||
|
<value>僅支援 xray/sing-box 的單一 outbound/endpoint</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvCustomCoreType" xml:space="preserve">
|
||||||
|
<value>自訂設定核心</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -21,26 +21,27 @@ public partial class CoreConfigSingboxService
|
|||||||
};
|
};
|
||||||
_coreConfig.inbounds.Add(inbound);
|
_coreConfig.inbounds.Add(inbound);
|
||||||
|
|
||||||
|
var inboundConf = _config.Inbound.First();
|
||||||
inbound.listen_port = listenPort;
|
inbound.listen_port = listenPort;
|
||||||
|
|
||||||
if (_config.Inbound.First().SecondLocalPortEnabled)
|
if (inboundConf.SecondLocalPortEnabled)
|
||||||
{
|
{
|
||||||
var inbound2 = BuildInbound(inbound, EInboundProtocol.socks2, true);
|
var inbound2 = BuildInbound(inbound, EInboundProtocol.socks2, true);
|
||||||
_coreConfig.inbounds.Add(inbound2);
|
_coreConfig.inbounds.Add(inbound2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_config.Inbound.First().AllowLANConn)
|
if (inboundConf.AllowLANConn)
|
||||||
{
|
{
|
||||||
if (_config.Inbound.First().NewPort4LAN)
|
if (inboundConf.NewPort4LAN)
|
||||||
{
|
{
|
||||||
var inbound3 = BuildInbound(inbound, EInboundProtocol.socks3, true);
|
var inbound3 = BuildInbound(inbound, EInboundProtocol.socks3, true);
|
||||||
inbound3.listen = listen;
|
inbound3.listen = listen;
|
||||||
_coreConfig.inbounds.Add(inbound3);
|
_coreConfig.inbounds.Add(inbound3);
|
||||||
|
|
||||||
//auth
|
//auth
|
||||||
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
|
if (inboundConf.User.IsNotEmpty() && inboundConf.Pass.IsNotEmpty())
|
||||||
{
|
{
|
||||||
inbound3.users = new() { new() { username = _config.Inbound.First().User, password = _config.Inbound.First().Pass } };
|
inbound3.users = new() { new() { username = inboundConf.User, password = inboundConf.Pass } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -48,12 +48,18 @@ public partial class CoreConfigSingboxService
|
|||||||
// packet straight back into the TUN, which hands it to the outbound again -
|
// packet straight back into the TUN, which hands it to the outbound again -
|
||||||
// an infinite loop that pins a CPU core. Drop instead of rejecting so no
|
// an infinite loop that pins a CPU core. Drop instead of rejecting so no
|
||||||
// ICMP unreachable is generated back towards the same addresses.
|
// ICMP unreachable is generated back towards the same addresses.
|
||||||
|
//
|
||||||
|
// Match each address on its own, not the prefix it carries. On Linux sing-tun
|
||||||
|
// registers Inet4Address[0].Addr().Next() with systemd-resolved as a "~." DNS
|
||||||
|
// upstream, and every prefix offered here is a /30 or /126, so carrying the
|
||||||
|
// prefix through would cover that resolver address too and drop every system
|
||||||
|
// name lookup along with the loop.
|
||||||
var tunAddresses = _coreConfig.inbounds.FirstOrDefault(i => i.type == "tun")?.address;
|
var tunAddresses = _coreConfig.inbounds.FirstOrDefault(i => i.type == "tun")?.address;
|
||||||
if (tunAddresses?.Count > 0)
|
if (tunAddresses?.Count > 0)
|
||||||
{
|
{
|
||||||
_coreConfig.route.rules.Add(new()
|
_coreConfig.route.rules.Add(new()
|
||||||
{
|
{
|
||||||
ip_cidr = [.. tunAddresses],
|
ip_cidr = [.. tunAddresses.Select(ToSingleAddressPrefix)],
|
||||||
action = "reject",
|
action = "reject",
|
||||||
method = "drop",
|
method = "drop",
|
||||||
});
|
});
|
||||||
@@ -284,6 +290,14 @@ public partial class CoreConfigSingboxService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ToSingleAddressPrefix(string address)
|
||||||
|
{
|
||||||
|
var addr = address.Split('/').First();
|
||||||
|
return IPAddress.TryParse(addr, out var ip)
|
||||||
|
? $"{addr}/{(ip.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32)}"
|
||||||
|
: address;
|
||||||
|
}
|
||||||
|
|
||||||
private List<string> BuildRoutingDirectExe()
|
private List<string> BuildRoutingDirectExe()
|
||||||
{
|
{
|
||||||
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -9,37 +9,38 @@ public partial class CoreConfigV2rayService
|
|||||||
var listen = "0.0.0.0";
|
var listen = "0.0.0.0";
|
||||||
var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||||
_coreConfig.inbounds = [];
|
_coreConfig.inbounds = [];
|
||||||
var inbound = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks, true);
|
var inboundConf = _config.Inbound.First();
|
||||||
|
var inbound = BuildInbound(inboundConf, EInboundProtocol.socks, true);
|
||||||
var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort;
|
var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort;
|
||||||
|
|
||||||
if (!context.IsTunEnabled || !isUsingLocalMixedPort)
|
if (!context.IsTunEnabled || !isUsingLocalMixedPort)
|
||||||
{
|
{
|
||||||
_coreConfig.inbounds.Add(inbound);
|
_coreConfig.inbounds.Add(inbound);
|
||||||
|
|
||||||
if (_config.Inbound.First().SecondLocalPortEnabled)
|
if (inboundConf.SecondLocalPortEnabled)
|
||||||
{
|
{
|
||||||
var inbound2 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks2, true);
|
var inbound2 = BuildInbound(inboundConf, EInboundProtocol.socks2, true);
|
||||||
_coreConfig.inbounds.Add(inbound2);
|
_coreConfig.inbounds.Add(inbound2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_config.Inbound.First().AllowLANConn)
|
if (inboundConf.AllowLANConn)
|
||||||
{
|
{
|
||||||
if (_config.Inbound.First().NewPort4LAN)
|
if (inboundConf.NewPort4LAN)
|
||||||
{
|
{
|
||||||
var inbound3 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks3, true);
|
var inbound3 = BuildInbound(inboundConf, EInboundProtocol.socks3, true);
|
||||||
inbound3.listen = listen;
|
inbound3.listen = listen;
|
||||||
_coreConfig.inbounds.Add(inbound3);
|
_coreConfig.inbounds.Add(inbound3);
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
|
if (inboundConf.User.IsNotEmpty() && inboundConf.Pass.IsNotEmpty())
|
||||||
{
|
{
|
||||||
inbound3.settings.auth = "password";
|
inbound3.settings.auth = "password";
|
||||||
inbound3.settings.accounts =
|
inbound3.settings.accounts =
|
||||||
[
|
[
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
user = _config.Inbound.First().User,
|
user = inboundConf.User,
|
||||||
pass = _config.Inbound.First().Pass,
|
pass = inboundConf.Pass,
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
@@ -66,12 +67,14 @@ public partial class CoreConfigV2rayService
|
|||||||
|
|
||||||
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
|
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
|
||||||
tunInbound.settings.gateway = [address];
|
tunInbound.settings.gateway = [address];
|
||||||
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
|
// Route both families into the tunnel regardless of EnableIPv6Address. That option only
|
||||||
|
// controls whether the interface gets an IPv6 address; leaving ::/0 out of the routing
|
||||||
|
// table makes IPv6 follow the system default route and bypass the tunnel entirely.
|
||||||
|
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0", "::/0"];
|
||||||
if (_config.TunModeItem.EnableIPv6Address == true)
|
if (_config.TunModeItem.EnableIPv6Address == true)
|
||||||
{
|
{
|
||||||
var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
|
var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
|
||||||
tunInbound.settings.gateway.Add(address6);
|
tunInbound.settings.gateway.Add(address6);
|
||||||
tunInbound.settings.autoSystemRoutingTable.Add("::/0");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
||||||
@@ -80,6 +83,7 @@ public partial class CoreConfigV2rayService
|
|||||||
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
||||||
}
|
}
|
||||||
tunInbound.sniffing = inbound.sniffing;
|
tunInbound.sniffing = inbound.sniffing;
|
||||||
|
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
|
||||||
tunInbound.sniffing.routeOnly = true;
|
tunInbound.sniffing.routeOnly = true;
|
||||||
|
|
||||||
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
||||||
@@ -117,16 +121,9 @@ public partial class CoreConfigV2rayService
|
|||||||
includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList();
|
includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList();
|
||||||
includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList();
|
includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList();
|
||||||
|
|
||||||
if (_config.TunModeItem.EnableIPv6Address)
|
|
||||||
{
|
|
||||||
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
|
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
|
||||||
.Concat(includeListV6.Select(x => x.ToString())).ToList();
|
.Concat(includeListV6.Select(x => x.ToString())).ToList();
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_coreConfig.inbounds.Add(tunInbound);
|
_coreConfig.inbounds.Add(tunInbound);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -512,6 +512,7 @@ public partial class CoreConfigV2rayService
|
|||||||
settings = new MaskSettings4Ray { value = kcpSeed },
|
settings = new MaskSettings4Ray { value = kcpSeed },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
kcpFinalmask.udp?.Reverse();
|
||||||
streamSettings.kcpSettings = kcpSettings;
|
streamSettings.kcpSettings = kcpSettings;
|
||||||
streamSettings.finalmask = kcpFinalmask;
|
streamSettings.finalmask = kcpFinalmask;
|
||||||
break;
|
break;
|
||||||
@@ -666,6 +667,7 @@ public partial class CoreConfigV2rayService
|
|||||||
version = 2,
|
version = 2,
|
||||||
auth = _node.Password,
|
auth = _node.Password,
|
||||||
};
|
};
|
||||||
|
hy2Finalmask.udp?.Reverse();
|
||||||
streamSettings.finalmask = hy2Finalmask;
|
streamSettings.finalmask = hy2Finalmask;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public partial class AddServer2ViewModel : MyReactiveObject, ICloseable
|
|||||||
|
|
||||||
BrowseServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
BrowseServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
{
|
{
|
||||||
var fileName = await BrowseConfigFileInteraction.Handle(RxVoid.Default);
|
var fileName = await BrowseConfigFileInteraction.HandleSafe(RxVoid.Default);
|
||||||
if (fileName.IsNullOrEmpty())
|
if (fileName.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
|
|
||||||
#endregion Menu
|
#endregion Menu
|
||||||
|
|
||||||
private readonly SynchronizationContext _uiContext = SynchronizationContext.Current;
|
|
||||||
|
|
||||||
#region Init
|
#region Init
|
||||||
|
|
||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
@@ -302,7 +300,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||||
.Subscribe(async blShow =>
|
.Subscribe(async blShow =>
|
||||||
{
|
{
|
||||||
await ShowHideWindowInteraction.Handle(blShow);
|
await ShowHideWindowInteraction.HandleSafe(blShow);
|
||||||
});
|
});
|
||||||
|
|
||||||
StatusBarViewModel.SetDefaultServerRequested
|
StatusBarViewModel.SetDefaultServerRequested
|
||||||
@@ -413,14 +411,25 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
private async Task RefreshServersDispatcherAsync()
|
private async Task RefreshServersDispatcherAsync()
|
||||||
{
|
{
|
||||||
//await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
|
//await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
|
||||||
_uiContext?.Post(_ => _ = RefreshServers(), null);
|
await Signal.FromAsync(async () =>
|
||||||
|
{
|
||||||
|
await RefreshServers();
|
||||||
|
return RxVoid.Default;
|
||||||
|
})
|
||||||
|
.SubscribeOn(RxSchedulers.MainThreadScheduler)
|
||||||
|
.ToTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshSubscriptions()
|
private async Task RefreshSubscriptions()
|
||||||
{
|
{
|
||||||
//await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler);
|
//await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler);
|
||||||
|
await Signal.FromAsync(async () =>
|
||||||
_uiContext?.Post(_ => _ = ProfilesViewModel.RefreshSubscriptions(), null);
|
{
|
||||||
|
await ProfilesViewModel.RefreshSubscriptions();
|
||||||
|
return RxVoid.Default;
|
||||||
|
})
|
||||||
|
.SubscribeOn(RxSchedulers.MainThreadScheduler)
|
||||||
|
.ToTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Servers && Groups
|
#endregion Servers && Groups
|
||||||
@@ -467,7 +476,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
var stringData = clipboardData;
|
var stringData = clipboardData;
|
||||||
if (clipboardData == null)
|
if (clipboardData == null)
|
||||||
{
|
{
|
||||||
var result = await ReadTextFromClipboardInteraction.Handle(RxVoid.Default);
|
var result = await ReadTextFromClipboardInteraction.HandleSafe(RxVoid.Default);
|
||||||
if (result.IsNullOrEmpty())
|
if (result.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
||||||
@@ -490,7 +499,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
|
|
||||||
public async Task AddServerViaScanAsync()
|
public async Task AddServerViaScanAsync()
|
||||||
{
|
{
|
||||||
var result = await ScanScreenInteraction.Handle(RxVoid.Default);
|
var result = await ScanScreenInteraction.HandleSafe(RxVoid.Default);
|
||||||
await ScanScreenResult(result);
|
await ScanScreenResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +511,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
|
|
||||||
public async Task AddServerViaImageAsync()
|
public async Task AddServerViaImageAsync()
|
||||||
{
|
{
|
||||||
var imageFileName = await BrowseImageFileInteraction.Handle(RxVoid.Default);
|
var imageFileName = await BrowseImageFileInteraction.HandleSafe(RxVoid.Default);
|
||||||
await AddScanResultAsync(imageFileName);
|
await AddScanResultAsync(imageFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,10 +700,12 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
//{
|
//{
|
||||||
// await ClashProxiesViewModel.ProxiesReload();
|
// await ClashProxiesViewModel.ProxiesReload();
|
||||||
//}, RxSchedulers.MainThreadScheduler);
|
//}, RxSchedulers.MainThreadScheduler);
|
||||||
RxSchedulers.MainThreadScheduler.Schedule(async () =>
|
await Signal.FromAsync(async () =>
|
||||||
{
|
{
|
||||||
await ClashProxiesViewModel.ProxiesReload();
|
await ClashProxiesViewModel.ProxiesReload();
|
||||||
});
|
return RxVoid.Default;
|
||||||
|
}).SubscribeOn(RxSchedulers.MainThreadScheduler)
|
||||||
|
.ToTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
ReloadResult(showClashUI);
|
ReloadResult(showClashUI);
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public partial class MsgViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await DispatcherShowMsgInteraction.Handle(sb.ToString());
|
await DispatcherShowMsgInteraction.HandleSafe(sb.ToString());
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -138,13 +138,7 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
|
|||||||
|
|
||||||
await RefreshServers();
|
await RefreshServers();
|
||||||
|
|
||||||
try
|
await ProfilesFocusInteraction.HandleSafe(RxVoid.Default);
|
||||||
{
|
|
||||||
await ProfilesFocusInteraction.Handle(RxVoid.Default);
|
|
||||||
}
|
|
||||||
catch (UnhandledInteractionException<RxVoid, RxVoid>)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ServerFilterChanged(bool c)
|
private async Task ServerFilterChanged(bool c)
|
||||||
|
|||||||
@@ -345,13 +345,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
|
|
||||||
await RefreshServers();
|
await RefreshServers();
|
||||||
|
|
||||||
try
|
await ProfilesFocusInteraction.HandleSafe(RxVoid.Default);
|
||||||
{
|
|
||||||
await ProfilesFocusInteraction.Handle(RxVoid.Default);
|
|
||||||
}
|
|
||||||
catch (UnhandledInteractionException<RxVoid, RxVoid>)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ServerFilterChanged(bool c)
|
private async Task ServerFilterChanged(bool c)
|
||||||
@@ -395,13 +389,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
SelectedProfile = selected ?? lstModel.First();
|
SelectedProfile = selected ?? lstModel.First();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
await DispatcherRefreshServersBizInteraction.HandleSafe(RxVoid.Default);
|
||||||
{
|
|
||||||
await DispatcherRefreshServersBizInteraction.Handle(RxVoid.Default);
|
|
||||||
}
|
|
||||||
catch (UnhandledInteractionException<RxVoid, RxVoid>)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshSubscriptions()
|
public async Task RefreshSubscriptions()
|
||||||
@@ -419,7 +407,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
|
|
||||||
public async Task AdjustMainLvColWidth()
|
public async Task AdjustMainLvColWidth()
|
||||||
{
|
{
|
||||||
await AdjustMainLvColWidthInteraction.Handle(RxVoid.Default);
|
await AdjustMainLvColWidthInteraction.HandleSafe(RxVoid.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
|
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
|
||||||
@@ -535,7 +523,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -556,7 +544,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
|
|
||||||
private async Task RemoveDuplicateServer()
|
private async Task RemoveDuplicateServer()
|
||||||
{
|
{
|
||||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -631,7 +619,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ShareServerInteraction.Handle(url);
|
await ShareServerInteraction.HandleSafe(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GenGroupAllServer()
|
private async Task GenGroupAllServer()
|
||||||
@@ -799,13 +787,13 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await SetClipboardDataInteraction.Handle((string)result.Data);
|
await SetClipboardDataInteraction.HandleSafe((string)result.Data);
|
||||||
NoticeManager.Instance.SendMessage(ResUI.OperationSuccess);
|
NoticeManager.Instance.SendMessage(ResUI.OperationSuccess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await SaveFileDialogInteraction.Handle(item);
|
await SaveFileDialogInteraction.HandleSafe(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,11 +842,11 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
if (blEncode)
|
if (blEncode)
|
||||||
{
|
{
|
||||||
await SetClipboardDataInteraction.Handle(Utils.Base64Encode(sb.ToString()));
|
await SetClipboardDataInteraction.HandleSafe(Utils.Base64Encode(sb.ToString()));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await SetClipboardDataInteraction.Handle(sb.ToString());
|
await SetClipboardDataInteraction.HandleSafe(sb.ToString());
|
||||||
}
|
}
|
||||||
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
|
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
|
||||||
}
|
}
|
||||||
@@ -881,7 +869,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
|
|
||||||
if (!result.IsNullOrEmpty())
|
if (!result.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
await SetClipboardDataInteraction.Handle(result);
|
await SetClipboardDataInteraction.HandleSafe(result);
|
||||||
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
|
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -925,7 +913,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,21 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
|
|||||||
[Reactive]
|
[Reactive]
|
||||||
public partial bool AutoSort { get; set; }
|
public partial bool AutoSort { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public partial string OutboundTag { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public partial string Remarks { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public partial string Port { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public partial string Network { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public partial bool Enabled { get; set; }
|
||||||
|
|
||||||
public ReactiveCommand<RxVoid, RxVoid> SelectProfileCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> SelectProfileCmd { get; }
|
||||||
public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
|
||||||
|
|
||||||
@@ -57,6 +72,11 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
|
|||||||
IP = Utils.List2String(SelectedSource.Ip, true);
|
IP = Utils.List2String(SelectedSource.Ip, true);
|
||||||
Process = Utils.List2String(SelectedSource.Process, true);
|
Process = Utils.List2String(SelectedSource.Process, true);
|
||||||
RuleType = SelectedSource.RuleType?.ToString();
|
RuleType = SelectedSource.RuleType?.ToString();
|
||||||
|
OutboundTag = SelectedSource.OutboundTag;
|
||||||
|
Remarks = SelectedSource.Remarks;
|
||||||
|
Port = SelectedSource.Port;
|
||||||
|
Network = SelectedSource.Network;
|
||||||
|
Enabled = SelectedSource.Enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveRulesAsync()
|
private async Task SaveRulesAsync()
|
||||||
@@ -80,6 +100,11 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
|
|||||||
SelectedSource.Protocol = ProtocolItems?.ToList();
|
SelectedSource.Protocol = ProtocolItems?.ToList();
|
||||||
SelectedSource.InboundTag = InboundTagItems?.ToList();
|
SelectedSource.InboundTag = InboundTagItems?.ToList();
|
||||||
SelectedSource.RuleType = RuleType.IsNullOrEmpty() ? null : Enum.Parse<ERuleType>(RuleType);
|
SelectedSource.RuleType = RuleType.IsNullOrEmpty() ? null : Enum.Parse<ERuleType>(RuleType);
|
||||||
|
SelectedSource.OutboundTag = OutboundTag;
|
||||||
|
SelectedSource.Remarks = Remarks;
|
||||||
|
SelectedSource.Port = Port;
|
||||||
|
SelectedSource.Network = Network;
|
||||||
|
SelectedSource.Enabled = Enabled;
|
||||||
|
|
||||||
var hasRule = SelectedSource.Domain?.Count > 0
|
var hasRule = SelectedSource.Domain?.Count > 0
|
||||||
|| SelectedSource.Ip?.Count > 0
|
|| SelectedSource.Ip?.Count > 0
|
||||||
@@ -110,8 +135,7 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
|
|||||||
var profileItem = await profileSelectViewModel.GetProfileItem();
|
var profileItem = await profileSelectViewModel.GetProfileItem();
|
||||||
if (profileItem != null)
|
if (profileItem != null)
|
||||||
{
|
{
|
||||||
SelectedSource.OutboundTag = profileItem.Remarks;
|
OutboundTag = profileItem.Remarks;
|
||||||
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
|||||||
});
|
});
|
||||||
ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () =>
|
ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
{
|
{
|
||||||
var fileName = await BrowseRulesFileInteraction.Handle(RxVoid.Default);
|
var fileName = await BrowseRulesFileInteraction.HandleSafe(RxVoid.Default);
|
||||||
await ImportRulesFromFileAsync(fileName);
|
await ImportRulesFromFileAsync(fileName);
|
||||||
});
|
});
|
||||||
ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
|
ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
@@ -156,7 +156,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
|||||||
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
|
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
|||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
};
|
};
|
||||||
await SetClipboardDataInteraction.Handle(JsonUtils.Serialize(lst, options));
|
await SetClipboardDataInteraction.HandleSafe(JsonUtils.Serialize(lst, options));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
|||||||
var stringData = clipboardData;
|
var stringData = clipboardData;
|
||||||
if (clipboardData == null)
|
if (clipboardData == null)
|
||||||
{
|
{
|
||||||
var result = await ReadTextFromClipboardInteraction.Handle(RxVoid.Default);
|
var result = await ReadTextFromClipboardInteraction.HandleSafe(RxVoid.Default);
|
||||||
if (result.IsNullOrEmpty())
|
if (result.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
||||||
@@ -315,7 +315,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
|||||||
private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData)
|
private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData)
|
||||||
{
|
{
|
||||||
var blReplace = false;
|
var blReplace = false;
|
||||||
if (await ShowYesNoInteraction.Handle(ResUI.AddBatchRoutingRulesYesNo) == false)
|
if (await ShowYesNoInteraction.HandleSafe(ResUI.AddBatchRoutingRulesYesNo) == false)
|
||||||
{
|
{
|
||||||
blReplace = true;
|
blReplace = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ public partial class RoutingSettingViewModel : MyReactiveObject
|
|||||||
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
|
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ public partial class StatusBarViewModel : MyReactiveObject
|
|||||||
sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}");
|
sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}");
|
||||||
sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}");
|
sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}");
|
||||||
|
|
||||||
await SetClipboardDataInteraction.Handle(sb.ToString());
|
await SetClipboardDataInteraction.HandleSafe(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddServerViaClipboard()
|
private async Task AddServerViaClipboard()
|
||||||
@@ -297,21 +297,14 @@ public partial class StatusBarViewModel : MyReactiveObject
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var models = new List<ComboItem>();
|
var models = lstModel.Select(it => new ComboItem { ID = it.IndexId, Text = it.GetSummary() }).ToList();
|
||||||
BlServers = true;
|
|
||||||
foreach (var it in lstModel)
|
|
||||||
{
|
|
||||||
var name = it.GetSummary();
|
|
||||||
|
|
||||||
var item = new ComboItem() { ID = it.IndexId, Text = name };
|
BlServers = true;
|
||||||
models.Add(item);
|
|
||||||
if (_config.IndexId == it.IndexId)
|
|
||||||
{
|
|
||||||
SelectedServer = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Servers.Clear();
|
Servers.Clear();
|
||||||
Servers.AddRange(models);
|
Servers.AddRange(models);
|
||||||
|
|
||||||
|
// Update the ItemsSource before SelectedItem so a collection reset does not clear the tray selection.
|
||||||
|
SelectedServer = models.FirstOrDefault(it => it.ID == _config.IndexId) ?? new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ServerSelectedChanged(bool c)
|
private void ServerSelectedChanged(bool c)
|
||||||
@@ -389,14 +382,7 @@ public partial class StatusBarViewModel : MyReactiveObject
|
|||||||
|
|
||||||
if (blChange)
|
if (blChange)
|
||||||
{
|
{
|
||||||
try
|
await DispatcherRefreshIconInteraction.HandleSafe(RxVoid.Default);
|
||||||
{
|
|
||||||
await DispatcherRefreshIconInteraction.Handle(RxVoid.Default);
|
|
||||||
}
|
|
||||||
catch (UnhandledInteractionException<RxVoid, RxVoid>)
|
|
||||||
{
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +418,7 @@ public partial class StatusBarViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
|
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
|
||||||
ReloadRequested.Publish();
|
ReloadRequested.Publish();
|
||||||
await DispatcherRefreshIconInteraction.Handle(RxVoid.Default);
|
await DispatcherRefreshIconInteraction.HandleSafe(RxVoid.Default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +455,7 @@ public partial class StatusBarViewModel : MyReactiveObject
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var password = await PasswordInputInteraction.Handle(RxVoid.Default);
|
var password = await PasswordInputInteraction.HandleSafe(RxVoid.Default);
|
||||||
if (password.IsNullOrEmpty())
|
if (password.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
_config.TunModeItem.EnableTun = false;
|
_config.TunModeItem.EnableTun = false;
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
|||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public partial string CustomCoreType { get; set; }
|
public partial string CustomCoreType { get; set; }
|
||||||
|
[Reactive]
|
||||||
|
public partial string PrevProfile { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public partial string NextProfile { get; set; }
|
||||||
|
|
||||||
public ReactiveCommand<RxVoid, RxVoid> SelectPrevProfileCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> SelectPrevProfileCmd { get; }
|
||||||
public ReactiveCommand<RxVoid, RxVoid> SelectNextProfileCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> SelectNextProfileCmd { get; }
|
||||||
@@ -23,8 +28,7 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
|||||||
var profileItem = await SelectProfileAsync();
|
var profileItem = await SelectProfileAsync();
|
||||||
if (profileItem != null)
|
if (profileItem != null)
|
||||||
{
|
{
|
||||||
SelectedSource?.PrevProfile = profileItem.Remarks;
|
PrevProfile = profileItem.Remarks;
|
||||||
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SelectNextProfileCmd = ReactiveCommand.CreateFromTask(async () =>
|
SelectNextProfileCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
@@ -32,8 +36,7 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
|||||||
var profileItem = await SelectProfileAsync();
|
var profileItem = await SelectProfileAsync();
|
||||||
if (profileItem != null)
|
if (profileItem != null)
|
||||||
{
|
{
|
||||||
SelectedSource?.NextProfile = profileItem.Remarks;
|
NextProfile = profileItem.Remarks;
|
||||||
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
|
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
@@ -43,6 +46,8 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
|||||||
|
|
||||||
SelectedSource = subItem.Id.IsNullOrEmpty() ? subItem : JsonUtils.DeepCopy(subItem);
|
SelectedSource = subItem.Id.IsNullOrEmpty() ? subItem : JsonUtils.DeepCopy(subItem);
|
||||||
CustomCoreType = SelectedSource.CustomCoreType?.ToString() ?? string.Empty;
|
CustomCoreType = SelectedSource.CustomCoreType?.ToString() ?? string.Empty;
|
||||||
|
PrevProfile = SelectedSource.PrevProfile;
|
||||||
|
NextProfile = SelectedSource.NextProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveSubAsync()
|
private async Task SaveSubAsync()
|
||||||
@@ -72,6 +77,8 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
|
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
|
||||||
|
SelectedSource.PrevProfile = PrevProfile;
|
||||||
|
SelectedSource.NextProfile = NextProfile;
|
||||||
|
|
||||||
if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0)
|
if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public partial class SubSettingViewModel : MyReactiveObject
|
|||||||
}, canEditRemove);
|
}, canEditRemove);
|
||||||
SubShareCmd = ReactiveCommand.CreateFromTask(async () =>
|
SubShareCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
{
|
{
|
||||||
await ShareSubInteraction.Handle(SelectedSource?.Url);
|
await ShareSubInteraction.HandleSafe(SelectedSource?.Url);
|
||||||
}, canEditRemove);
|
}, canEditRemove);
|
||||||
|
|
||||||
_ = Init();
|
_ = Init();
|
||||||
@@ -84,7 +84,7 @@ public partial class SubSettingViewModel : MyReactiveObject
|
|||||||
|
|
||||||
private async Task DeleteSubAsync()
|
private async Task DeleteSubAsync()
|
||||||
{
|
{
|
||||||
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
|
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
|
|||||||
.Subscribe(InitializeData)
|
.Subscribe(InitializeData)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Port, v => v.txtPort.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Domain, v => v.txtDomain.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Domain, v => v.txtDomain.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.IP, v => v.txtIP.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.IP, v => v.txtIP.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Process, v => v.txtProcess.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Process, v => v.txtProcess.Text).DisposeWith(disposables);
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
|||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.SelectedValue).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.SelectedValue).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.PrevProfile, v => v.txtPrevProfile.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.PrevProfile, v => v.txtPrevProfile.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.SelectedValue).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.SelectedValue).DisposeWith(disposables);
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ public partial class RoutingRuleDetailsWindow
|
|||||||
.Subscribe(InitializeData)
|
.Subscribe(InitializeData)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Port, v => v.txtPort.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Network, v => v.cmbNetwork.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Domain, v => v.txtDomain.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Domain, v => v.txtDomain.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.IP, v => v.txtIP.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.IP, v => v.txtIP.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Process, v => v.txtProcess.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Process, v => v.txtProcess.Text).DisposeWith(disposables);
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ public partial class SubEditWindow
|
|||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.PrevProfile, v => v.txtPrevProfile.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.PrevProfile, v => v.txtPrevProfile.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.Text).DisposeWith(disposables);
|
||||||
|
|||||||
Reference in New Issue
Block a user