Compare commits

...

42 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore(deps): update NLog to 6.2.0

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

* Update package-rhel-loong.sh

* Update package-debian-riscv.sh

* Update package-debian-loong.sh
2026-08-16 10:08:08 +08:00
2dust
e0ad2c46e6 up 7.24.7 2026-08-15 10:14:52 +08:00
2dust
230a2f6773 Bug fix
https://github.com/2dust/v2rayN/issues/9936
2026-08-14 12:08:43 +08:00
白龙
e7cc4c832e Fix tray server selection being cleared after refresh (#9927) (#9928)
* Fix tray server selection after refresh (#9927)

* Update StatusBarViewModel.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-08-14 10:38:27 +08:00
DHR60
fb7033aab5 Add hy2 ech support (#9954) 2026-08-14 10:22:31 +08:00
DHR60
b6cc3913ac Perf hy2 fmt (#9953) 2026-08-14 10:22:10 +08:00
DHR60
bc06a5dc21 Fix (#9951)
* Fix

* Fix

* Add HandleSafe
2026-08-14 10:20:58 +08:00
Chen, Ting-An
9c1951178c fix(i18n): polish Traditional Chinese DNS settings (#9947)
Replace Simplified Chinese and ambiguous translations in the DNS and full configuration template settings with natural Taiwan terminology. Clarify related descriptions without changing resource keys or application behavior.
2026-08-14 10:20:16 +08:00
DHR60
b3df42b9ba Sync finalmask order (#9943) 2026-08-14 10:19:56 +08:00
liuclare
e01717d832 Narrow the TUN self-address drop rule to single addresses (#9935)
The rule added in #9897 takes the TUN inbound's `address` verbatim as `ip_cidr`,
so a /30 or /126 interface prefix becomes the match range.

sing-tun derives the TUN's DNS entry from the address right after the interface's
own and hands it to the system resolver: Windows through luid.SetDNS in
tun_windows.go, Linux through systemd-resolved in tun_linux.go, both guarded only
by AutoRoute && !EXP_DisableDNSHijack. HasNextAddress keeps that address inside
the interface prefix, every preset in Global.TunIPv4Address is a /30 and every
IPv6 preset a /126, and the sing-box system stack rejects single-address
prefixes, so there is no configuration where it falls outside.

Queries from the system resolver then hit the drop rule and time out with no
response and no ICMP. Name resolution fails for the whole system while the proxy
path itself stays healthy, which makes it read as a DNS outage rather than a
routing rule. Reported in #9934 and #9926.

Matching each address on its own keeps what #9897 set out to block - the loop it
diagnosed was addressed to the interface address itself - and leaves the DNS
entry to sing-box.

Also restores the two regression tests #9897 came with, removed by eff58459
(#9817) while its implementation and template fix stayed in place.
ShouldRejectTrafficToTunOwnAddresses now asserts the single-address form and
additionally pins the prefix length, so it covers both the loop it was written
for and the resolver address it must not cover.

Verified on Linux by running sing-box directly from a generated config, changing
only this rule's prefix length between runs:

  ip_cidr ["172.18.0.1/30"]   getent hosts www.google.com -> empty, 3/3
  ip_cidr ["172.18.0.1/32"]   getent hosts www.google.com -> resolved, 3/3

dig against a public resolver, naked-IP HTTPS and the local mixed port were
unaffected in both runs. End to end, a build of this branch emits
drop ip_cidr ["172.18.0.1/32"] and system resolution works while its TUN is up.

Co-authored-by: liuclare <177657698+liuclare@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:09:04 +08:00
DHR60
8b4063b44b Set tun "route only" to true (#9933) 2026-08-09 20:53:00 +08:00
DHR60
3136f573dd Revert "Fix Windows TUN restart issue (#9925)" (#9932)
This reverts commit f347fe1c03.
2026-08-09 20:52:36 +08:00
liuclare
e101b1d7b0 Always route IPv6 into Xray TUN regardless of EnableIPv6Address (#9930)
* Always route IPv6 into Xray TUN regardless of EnableIPv6Address

EnableIPv6Address controls whether the TUN interface is assigned an IPv6
address, but it also gated whether ::/0 was added to autoSystemRoutingTable.
With the default (false), IPv6 had no route pointing at the TUN device and
followed the system default route instead, leaving the tunnel unproxied and
exposing the host's real IPv6 address.

The embedded template SampleTunInbound already declares both families; the
generated config discarded it. #9843 restored ::/0 only inside the
EnableIPv6Address == true branch, so the false branch still leaks.

Route both families unconditionally and let the option control only the
interface address. The same conditional existed a second time in the
RouteExcludeAddress branch and is fixed as well.

Fixes #9929

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Add regression tests for IPv6 routing in the Xray TUN inbound

Both assertions fail on 31044f44 and pass with the fix:

  Tun_ShouldRouteIPv6IntoTunnel(enableIPv6Address: False)
    Expected collection {"0.0.0.0/0"} to contain "::/0".

  TunRouteExcludeAddress_ShouldIncludeIPv6Ranges
    Expected collection {...44 IPv4 ranges...} to have an item matching x.Contains(:).

The theory also covers enableIPv6Address: true, which passes on both revisions,
so the tests only fail while the defect is present. The gateway count assertion
pins the intended split of responsibilities: EnableIPv6Address governs the
interface address, never the routing table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: liuclare <177657698+liuclare@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:15:18 +08:00
2dust
31044f449d up 7.24.6 2026-08-08 20:38:18 +08:00
2dust
eacf8b0784 Reuse inbound config and fix tun sniffing flag
https://github.com/2dust/v2rayN/issues/9921
2026-08-08 20:36:18 +08:00
Mangoo
f347fe1c03 Fix Windows TUN restart issue (#9925) 2026-08-08 20:20:41 +08:00
JieXu
71208eb711 Update build-windows-x86.yml (#9924) 2026-08-08 20:01:13 +08:00
tt2563
339eaa6bfd Update ResUI.zh-Hant.resx (#9923)
* Update ResUI.zh-Hant.resx

Update Traditional Chinese translation

* Fix typo in outbound/endpoint message
2026-08-08 19:58:55 +08:00
Miheichev Aleksandr Sergeevich
717ef7f0a7 i18n(ru): add missing translations and remove trailing periods (#9916)
* i18n(ru): add missing Russian translations

Translate the 10 strings missing from ResUI.ru.resx (proxy dial
resolution strategy, Happy Eyeballs, IPv4/IPv6 address labels, and
the custom outbound group). Russian now has full key parity with
ResUI.resx (580/580); placeholder consistency verified for all keys.

Technical terms and config values (outbound, endpoint, UseIP, Happy
Eyeballs) are kept untranslated, matching the zh-Hans locale.

* i18n(ru): remove trailing periods from translations

Align with the Russian UI convention of omitting sentence-final
periods in labels, tooltips and messages (32 values). Inner
punctuation and ellipses are kept; no wording changes.

* i18n(ru): translate TbFakeIPTips left in English

The key existed in ResUI.ru.resx but its value was the untranslated
English text. Translate it following the established glossary (FakeIP
and sing-box remain untranslated).
2026-08-07 14:19:59 +08:00
2dust
4a9d83f9dc Fix duplicated wording in UI resource strings
https://github.com/2dust/v2rayN/pull/9914
2026-08-07 14:13:32 +08:00
JieXu
1ac14d0903 Update package-osx.sh (#9913)
* Update Directory.Packages.props

* Update package-osx.sh
2026-08-07 14:07:44 +08:00
Miheichev Aleksandr Sergeevich
08d423e1b4 docs: link minimum OS requirements from README (#9912)
Replace the requirements table with a single bilingual line under
"Supported Platforms" pointing to the wiki page (Release files
introduction), per the review feedback in the PR: the README stays
minimal and the wiki remains the single source of truth.
2026-08-07 14:07:22 +08:00
2dust
9638b4c368 Fix custom config import
https://github.com/2dust/v2rayN/issues/9902
2026-08-06 09:29:33 +08:00
Miheichev Aleksandr Sergeevich
1034ce6e02 chore(deps): update NuGet dependencies (#9832)
* chore(deps): bump Avalonia.Desktop, CliWrap, Semi.Avalonia and Repobot.SQLite

- Avalonia.Desktop          12.1.0    -> 12.1.1
- CliWrap                   3.10.2    -> 3.10.4
- Semi.Avalonia             12.1.0    -> 12.1.0.1
- Semi.Avalonia.DataGrid    12.1.0    -> 12.1.0.1
- Repobot.SQLite.Unofficial 3.53.3.10 -> 3.53.4

Left unchanged on purpose:

- ReactiveUI.Avalonia: nuget.org reports 14.7.1 as the highest version, but it
  is unlisted and belongs to the old versioning line (netstandard2.0/net6.0/
  net7.0, Avalonia >= 11.0.0, ReactiveUI >= 19.4.1). Moving to it would revert
  both #9148 and #9678.
- SkiaSharp.NativeAssets.Linux: 4.151.0 is available, but Avalonia.Skia 12.1.1
  resolves the managed SkiaSharp to 3.119.4. Pairing 4.x native assets with a
  3.x managed binding risks native entry point failures on Linux; 3.119.4 is
  the latest release on the 3.x branch.

* chore(deps): bump ReactiveUI to 24.1.0 and ReactiveUI.SourceGenerators to 3.2.0

- ReactiveUI                  24.0.0 -> 24.1.0
- ReactiveUI.WPF              24.0.0 -> 24.1.0
- ReactiveUI.SourceGenerators 3.1.0  -> 3.2.0

ReactiveUI and ReactiveUI.WPF move together: ReactiveUI.WPF 24.1.0 requires
ReactiveUI >= 24.1.0. ReactiveUI.Avalonia 12.1.0 declares a ReactiveUI >= 24.0.0
minimum, so it resolves against 24.1.0 without changes; ReactiveUI.Primitives
(7.1.0) and Splat (20.2.0) are unaffected.

* chore(deps): bump Avalonia.Controls.DataGrid and ReactiveUI.Avalonia

- Avalonia.Controls.DataGrid 12.1.0 -> 12.1.2
- ReactiveUI.Avalonia        12.1.0 -> 12.1.1

ReactiveUI.Avalonia 12.1.1 requires Avalonia >= 12.1.1 and ReactiveUI >= 24.1.0,
both of which are already in place after the previous commits on this branch.
2026-08-06 08:24:21 +08:00
82 changed files with 1456 additions and 849 deletions

View File

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

View File

@@ -52,7 +52,7 @@ jobs:
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET 10.0.1xx
uses: actions/setup-dotnet@v6.0.0
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.1xx
@@ -146,7 +146,7 @@ jobs:
$xrayTag = "v$xrayVer"
$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"
Write-Host "Bundled Xray version: $xrayVer"

View File

@@ -69,10 +69,10 @@ jobs:
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET
uses: actions/setup-dotnet@v6.0.0
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.1xx'
dotnet-version: 10.0.1xx
- name: Build v2rayN
shell: bash
working-directory: ./v2rayN
@@ -89,7 +89,7 @@ jobs:
find ${{ matrix.arch }} -type f -name '*.pdb' -delete
- name: Upload build artifacts
uses: actions/upload-artifact@v7.0.1
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.arch }}
path: ${{ matrix.arch }}

View File

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

View File

@@ -50,6 +50,8 @@ Read the Wiki for usage guides and configuration details.
| Linux | ✅ | - | ✅ | ✅ | ✅ |
| 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 签名校验

5
global.json Normal file
View File

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

View File

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

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
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_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"

View File

@@ -54,7 +54,7 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
<key>NSHighResolutionCapable</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>13.7</string>
<string>13.6</string>
</dict>
</plist>
EOF

View File

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

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
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_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"

View File

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

View File

@@ -6,32 +6,30 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.0" />
<PackageVersion Include="CliWrap" Version="3.10.2" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
<PackageVersion Include="CliWrap" Version="3.10.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="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="24.0.0" />
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.1.0" />
<PackageVersion Include="ReactiveUI.WPF" Version="24.0.0" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0" />
<PackageVersion Include="ReactiveUI" Version="24.1.0" />
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
<PackageVersion Include="ReactiveUI.WPF" Version="24.1.0" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0" />
<PackageVersion Include="NLog" Version="6.1.4" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
<PackageVersion Include="NLog" Version="6.2.0" />
<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="TUnit" Version="1.65.38" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />

View File

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

View File

@@ -251,4 +251,12 @@ internal static class CoreConfigTestFactory
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
return config;
}
public static Config CreateConfigWithTun(ECoreType coreType, bool enableIPv6Address)
{
var config = CreateConfig(coreType);
config.TunModeItem.EnableTun = true;
config.TunModeItem.EnableIPv6Address = enableIPv6Address;
return config;
}
}

View File

@@ -1,19 +1,9 @@
using AwesomeAssertions;
using ServiceLib.Common;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager;
using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig;
using Xunit;
namespace ServiceLib.Tests.CoreConfig.Singbox;
public class CoreConfigSingboxServiceTests
{
[Fact]
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
[Test]
public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -22,17 +12,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await result.Data.Should().NotBeNull();
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
singboxConfig.Should().NotBeNull();
singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
await singboxConfig.Should().NotBeNull();
await singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
await singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
}
[Fact]
public void GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
[Test]
public async Task GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -46,18 +36,100 @@ public class CoreConfigSingboxServiceTests
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())!;
cfg.inbounds.Should().Contain(i =>
await cfg.inbounds.Should().Contain(i =>
i.type == nameof(EInboundProtocol.mixed)
&& i.listen == Global.Loopback
&& 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]
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
[Test]
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);
config.CoreBasicItem.BindInterface = "eth0";
@@ -71,16 +143,16 @@ public class CoreConfigSingboxServiceTests
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 proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.bind_interface.Should().Be("eth0");
proxy.detour.Should().BeNullOrEmpty();
await proxy.bind_interface.Should().BeEqualTo("eth0");
await proxy.detour.Should().BeNull().Or.BeEmpty();
}
[Fact]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
[Test]
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -97,17 +169,17 @@ public class CoreConfigSingboxServiceTests
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())!;
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");
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 == Global.ProxyTag && o.type == "selector");
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
}
[Fact]
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
[Test]
public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -124,18 +196,18 @@ public class CoreConfigSingboxServiceTests
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())!;
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));
cfg.outbounds.Should().Contain(o =>
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o =>
o.tag == Global.ProxyTag &&
(o.detour ?? string.Empty).StartsWith("chain-proxy-1-", StringComparison.Ordinal));
}
[Fact]
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
[Test]
public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -157,18 +229,18 @@ public class CoreConfigSingboxServiceTests
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())!;
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");
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));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
}
[Fact]
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
[Test]
public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -190,25 +262,25 @@ public class CoreConfigSingboxServiceTests
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())!;
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");
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 == Global.ProxyTag && o.type == "selector");
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
await 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-2-", 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
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
.All(o => (o.detour ?? string.Empty).StartsWith("chain-proxy-1-group-", StringComparison.Ordinal));
allCloneDetoursPointToGroupBranches.Should().BeTrue();
await allCloneDetoursPointToGroupBranches.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
[Test]
public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -244,24 +316,24 @@ public class CoreConfigSingboxServiceTests
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 hasDirectRule = cfg.route.rules.Any(r =>
r.domain != null
&& r.domain.Contains("direct.example.com")
&& r.outbound == Global.DirectTag);
hasDirectRule.Should().BeTrue();
await hasDirectRule.Should().BeTrue();
var hasBlockRule = cfg.route.rules.Any(r =>
r.domain != null
&& r.domain.Contains("block.example.com")
&& r.action == "reject");
hasBlockRule.Should().BeTrue();
await hasBlockRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
[Test]
public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -293,21 +365,21 @@ public class CoreConfigSingboxServiceTests
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 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 =>
r.domain != null
&& r.domain.Contains("route.example.com")
&& (r.outbound ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
hasRouteRule.Should().BeTrue();
await hasRouteRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
[Test]
public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
{
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(
ECoreType.sing_box,
@@ -338,7 +410,7 @@ public class CoreConfigSingboxServiceTests
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 hasExpectedRule = cfg.dns.rules?.Any(r =>
@@ -347,11 +419,11 @@ public class CoreConfigSingboxServiceTests
&& r.rule_set?.Contains("geosite-cn") == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeTrue();
await hasExpectedRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
[Test]
public async Task GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
{
var bootstrapDns = "8.8.8.8";
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns);
@@ -362,17 +434,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
config.SimpleDNSItem.BootstrapDNS.Should().Be(bootstrapDns);
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await config.SimpleDNSItem.BootstrapDNS.Should().BeEqualTo(bootstrapDns);
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var bootstrapServer = cfg.dns.servers?.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag);
bootstrapServer.Should().NotBeNull();
(bootstrapServer?.server ?? string.Empty).Should().Contain(bootstrapDns);
var bootstrapServer = cfg.dns?.servers.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag);
await bootstrapServer.Should().NotBeNull();
await bootstrapServer!.server.Should().Contain(bootstrapDns);
}
[Fact]
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
[Test]
public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.SimpleDNSItem.DirectDNS = "1.1.1.1";
@@ -405,14 +477,14 @@ public class CoreConfigSingboxServiceTests
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())!;
cfg.dns.final.Should().Be(Global.SingboxDirectDNSTag);
await cfg.dns.final.Should().BeEqualTo(Global.SingboxDirectDNSTag);
}
[Fact]
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
[Test]
public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
{
var config =
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();
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 hasExpectedRule = cfg.dns.rules?.Any(r =>
r.server == Global.SingboxDirectDNSTag
&& r.ip_cidr?.Contains("192.168.0.0/16") == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeFalse();
await hasExpectedRule.Should().BeFalse();
}
[Theory]
[InlineData("geosite:cn", "geosite-cn")]
[InlineData("geosite:geolocation-cn", "geosite-geolocation-cn")]
[InlineData("geosite:tld-cn", "geosite-tld-cn")]
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag,
[Test]
[Arguments("geosite:cn", "geosite-cn")]
[Arguments("geosite:geolocation-cn", "geosite-geolocation-cn")]
[Arguments("geosite:tld-cn", "geosite-tld-cn")]
public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag,
string expectedRuleSetTag)
{
var config =
@@ -484,7 +556,7 @@ public class CoreConfigSingboxServiceTests
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 hasExpectedRule = cfg.dns.rules?.Any(r =>
@@ -492,11 +564,11 @@ public class CoreConfigSingboxServiceTests
&& r.ip_cidr?.Contains("192.168.0.0/16") == true
&& r.rule_set?.Contains(expectedRuleSetTag) == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeTrue();
await hasExpectedRule.Should().BeTrue();
}
[Fact]
public void GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
[Test]
public async Task GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
@@ -508,21 +580,21 @@ public class CoreConfigSingboxServiceTests
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 hostsServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxHostsDNSTag);
hostsServer.Should().NotBeNull();
hostsServer!.predefined.Should().ContainKey("resolver.example");
hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1");
await hostsServer.Should().NotBeNull();
await hostsServer!.predefined.Should().ContainKey("resolver.example");
await hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1");
var directServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxDirectDNSTag);
directServer.Should().NotBeNull();
directServer!.domain_resolver.Should().Be(Global.SingboxHostsDNSTag);
await directServer.Should().NotBeNull();
await directServer!.domain_resolver.Should().BeEqualTo(Global.SingboxHostsDNSTag);
}
[Fact]
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
[Test]
public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -551,22 +623,22 @@ public class CoreConfigSingboxServiceTests
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())!;
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);
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.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 == Global.SingboxLocalDNSTag);
await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
}
[Fact]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
[Test]
public async Task GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{
var shareLink =
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull();
await node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
@@ -579,22 +651,22 @@ public class CoreConfigSingboxServiceTests
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 proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2");
proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public");
proxy.realm.realm_id.Should().Be("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull();
await proxy.type.Should().BeEqualTo("hysteria2");
await proxy.realm.Should().NotBeNull();
await proxy.realm!.server_url.Should().StartWith("https://");
await proxy.realm.server_url.Should().Contain("realm.hy2.io");
await proxy.realm.token.Should().BeEqualTo("public");
await proxy.realm.realm_id.Should().BeEqualTo("my-realm-id");
await proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
await proxy.server.Should().BeNull();
}
[Fact]
public void GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress()
[Test]
public async Task GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress()
{
// 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.
@@ -612,23 +684,24 @@ public class CoreConfigSingboxServiceTests
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 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 prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]);
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");
}
}
[Fact]
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
[Test]
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -649,17 +722,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await result.Data.Should().NotBeNull();
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);
proxyOutbound.Should().NotBeNull();
proxyOutbound!.type.Should().Be("shadowsocks");
proxyOutbound.server.Should().Be("1.2.3.4");
proxyOutbound.server_port.Should().Be(8388);
proxyOutbound.method.Should().Be("aes-128-gcm");
proxyOutbound.password.Should().Be("custom_password");
await proxyOutbound.Should().NotBeNull();
await proxyOutbound!.type.Should().BeEqualTo("shadowsocks");
await proxyOutbound.server.Should().BeEqualTo("1.2.3.4");
await proxyOutbound.server_port.Should().BeEqualTo(8388);
await proxyOutbound.method.Should().BeEqualTo("aes-128-gcm");
await proxyOutbound.password.Should().BeEqualTo("custom_password");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,44 +1,39 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Manager;
using Xunit;
namespace ServiceLib.Tests.Manager;
public class CoreManagerTests
{
[Theory]
[InlineData(ECoreType.sing_box)]
[InlineData(ECoreType.mihomo)]
[InlineData(ECoreType.Xray)]
public void ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
[Test]
[Arguments(ECoreType.sing_box)]
[Arguments(ECoreType.mihomo)]
[Arguments(ECoreType.Xray)]
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]
public void ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate()
[Test]
public async Task ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate()
{
// 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
// disabled must never elevate, and a launch whose snapshot has TUN enabled must
// elevate regardless of later changes to the live config.
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.sing_box, isNonWindows: true).Should().BeFalse();
await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
}
[Fact]
public void ShouldRunAsSudo_OnWindows_ShouldNotElevate()
[Test]
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]
[InlineData(ECoreType.v2fly)]
[InlineData(ECoreType.hysteria)]
[InlineData(null)]
public void ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
[Test]
[Arguments(ECoreType.v2fly)]
[Arguments(ECoreType.hysteria)]
[Arguments(null)]
public async Task ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
{
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
}
}

View File

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

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace ServiceLib.Common;
@@ -134,4 +135,30 @@ public static class Extension
.Replace("\r", 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;
}
}
}

View File

@@ -1,4 +1,3 @@
using System.Collections.Specialized;
using System.Security.Principal;
using CliWrap;
using CliWrap.Buffered;
@@ -753,11 +752,11 @@ public class Utils
return false;
}
public static int GetFreePort(int defaultPort = 0)
public static int GetFreePort(int defaultPort)
{
try
{
if (!(defaultPort == 0 || Utils.PortInUse(defaultPort)))
if (!PortInUse(defaultPort))
{
return defaultPort;
}

View File

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

View File

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

View File

@@ -115,10 +115,10 @@ public class NodeValidator
if (item.GetNetwork() is nameof(ETransport.ws)
&& item.EchConfigList.IsNullOrEmpty()
&& item.GetAlpn()?.FirstOrDefault() == "h3")
&& item.GetAlpn()?.FirstOrDefault() is "h3" or "h2")
{
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
@@ -131,14 +131,6 @@ public class NodeValidator
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
&& item.GetAllowInsecure()
&& !isCertProvided
@@ -147,6 +139,10 @@ public class NodeValidator
&& item.GetAllowInsecure()
&& !isCertProvided))
{
if (coreType == ECoreType.Xray)
{
v.Warning(ResUI.MsgAllowInsecureDeprecated);
}
v.Warning(ResUI.MsgInsecureConfiguration);
}
}

View File

@@ -115,6 +115,7 @@ public static class ConfigHandler
config.ConstItem ??= new ConstItem();
config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
config.SimpleDNSItem.BlockAAAAQuery ??= false;
config.SimpleDNSItem.FakeIPRange ??= Global.FakeIPRanges.FirstOrDefault();
config.SimpleDNSItem.GlobalFakeIp ??= true;
config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault();
@@ -586,7 +587,6 @@ public static class ConfigHandler
return 0;
}
public static async Task<int> AddCustomOutboundServer(Config config, ProfileItem profileItem, bool blDelete, bool toFile = true)
{
var fileName = profileItem.Address;
@@ -1739,8 +1739,16 @@ public static class ConfigHandler
SubItem? subItem)
{
var subRemarks = subItem?.Remarks;
// Safe Mode: Only allow full configuration if it's not from a subscription
var lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks);
// Prioritize using complete custom parsing, followed by custom outbound parsing.
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)
{
lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
@@ -1750,7 +1758,7 @@ public static class ConfigHandler
return -1;
}
var count = await AddCustomOutboundServers(config, lstProfiles, subid, isSub);
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
if (count > 0)
{
return count;
@@ -1786,31 +1794,28 @@ public static class ConfigHandler
var subRemarks = subItem.Remarks;
var customCoreType = subItem.CustomCoreType!.Value;
List<ProfileItem>? lstProfiles = customCoreType switch
var lstProfiles = customCoreType switch
{
ECoreType.Xray => V2rayFmt.ResolveToCustom(strData, subRemarks),
ECoreType.sing_box => SingboxFmt.ResolveToCustom(strData, subRemarks),
_ => null
_ => null,
};
if (lstProfiles is not null)
if ((lstProfiles?.Count ?? 0) == 0)
{
if (lstProfiles.Count == 0)
{
return -1;
}
return -1;
}
var count = await AddCustomOutboundServers(config, lstProfiles, subid, isSub);
if (count > 0)
{
return count;
}
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
if (count > 0)
{
return count;
}
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);
}
private static async Task<int> AddCustomOutboundServers(
private static async Task<int> AddBatchCustomServers(
Config config,
List<ProfileItem> lstProfiles,
string subid,
@@ -1821,9 +1826,20 @@ public static class ConfigHandler
{
it.Subid = subid;
it.IsSub = isSub;
if (await AddCustomOutboundServer(config, it, true) == 0)
if (it.ConfigType == EConfigType.Custom)
{
count++;
if (await AddCustomServer(config, it, true) == 0)
{
count++;
}
}
else
{
if (await AddCustomOutboundServer(config, it, true) == 0)
{
count++;
}
}
}
return count;

View File

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

View File

@@ -1,5 +1,3 @@
using System.Collections.Specialized;
namespace ServiceLib.Handler.Fmt;
public class Hysteria2Fmt : BaseFmt
@@ -168,8 +166,21 @@ public class Hysteria2Fmt : BaseFmt
}
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
{
Ports = GetQueryDecoded(query, "mport"),
@@ -212,6 +223,10 @@ public class Hysteria2Fmt : BaseFmt
var sha = item.CertSha;
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
}
if (!item.EchConfigList.IsNullOrEmpty())
{
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
}
var protocolExtraItem = item.GetProtocolExtra();
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())

View File

@@ -34,9 +34,9 @@ public class SingboxFmt : BaseFmt
if (!isOutbound)
{
var fullProfile = ResolveFull(jsonObject, subRemarks);
profileList.Add(fullProfile);
if (fullProfile is not null)
{
profileList.Add(fullProfile);
return profileList;
}
}
@@ -76,6 +76,7 @@ public class SingboxFmt : BaseFmt
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem
{
ConfigType = EConfigType.Custom,
CoreType = ECoreType.sing_box,
Address = fileName,
Remarks = subRemarks ?? "singbox_custom",

View File

@@ -34,9 +34,9 @@ public class V2rayFmt : BaseFmt
if (!isOutbound)
{
var fullProfile = ResolveFull(jsonObject, subRemarks);
profileList.Add(fullProfile);
if (fullProfile is not null)
{
profileList.Add(fullProfile);
return profileList;
}
}
@@ -77,6 +77,7 @@ public class V2rayFmt : BaseFmt
var profileItem = new ProfileItem
{
ConfigType = EConfigType.Custom,
CoreType = ECoreType.Xray,
Address = fileName,
Remarks = jsonObject["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -142,7 +142,7 @@
<value>Failed to get the default configuration</value>
</data>
<data name="FailedImportedCustomServer" xml:space="preserve">
<value>Failed to import custom configuration Configuration</value>
<value>Failed to import configuration</value>
</data>
<data name="FailedReadConfiguration" xml:space="preserve">
<value>Failed to read configuration file</value>
@@ -283,7 +283,7 @@
<value>Configuration successful. {0}</value>
</data>
<data name="SuccessfullyImportedCustomServer" xml:space="preserve">
<value>Custom configuration Configuration imported successfully</value>
<value>Custom configuration imported successfully</value>
</data>
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
<value>{0} Configurations have been imported from clipboard</value>
@@ -385,7 +385,7 @@
<value>All</value>
</data>
<data name="FillServerAddressCustom" xml:space="preserve">
<value>Please browse to import Configuration configuration</value>
<value>Please browse to import configuration</value>
</data>
<data name="Speedtesting" xml:space="preserve">
<value>Testing...</value>
@@ -1462,7 +1462,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value>
<value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value>
@@ -1860,4 +1860,13 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="LvCustomCoreType" xml:space="preserve">
<value>Custom config core</value>
</data>
<data name="TbXrayOnly" xml:space="preserve">
<value>Xray Only</value>
</data>
<data name="TbBlockAAAAQueries" xml:space="preserve">
<value>Block AAAA Queries</value>
</data>
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
<value>Block IPv6 queries when enabled</value>
</data>
</root>

View File

@@ -268,7 +268,7 @@
<value>Сначала выберите сервер</value>
</data>
<data name="RemoveDuplicateServerResult" xml:space="preserve">
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}.</value>
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}</value>
</data>
<data name="RemoveServer" xml:space="preserve">
<value>Вы уверены, что хотите удалить сервер?</value>
@@ -1048,7 +1048,7 @@
<value>HTTP-заголовки</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк.</value>
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк</value>
</data>
<data name="LvPrevProfile" xml:space="preserve">
<value>Псевдоним предыдущего прокси</value>
@@ -1120,28 +1120,28 @@
<value>Длина фрагмента</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию.</value>
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Интервал фрагментации</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию.</value>
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Макс. разделение</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию.</value>
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Резервная задержка</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию.</value>
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100).</value>
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100)</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
@@ -1351,7 +1351,7 @@
<value>Пароль sudo системы</value>
</data>
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска.</value>
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска</value>
</data>
<data name="TransportHeaderType5" xml:space="preserve">
<value>XHTTP-режим</value>
@@ -1411,7 +1411,7 @@
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
</data>
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
<value>Неверный пароль, попробуйте ещё раз.</value>
<value>Неверный пароль, попробуйте ещё раз</value>
</data>
<data name="TbMldsa65Verify" xml:space="preserve">
<value>Mldsa65Verify</value>
@@ -1462,7 +1462,7 @@
<value>Включён пользовательский DNS — настройки на этой странице не применяются</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>При включении блокирует проверки доступности ECH и HTTP/3</value>
<value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Пожалуйста, заполните корректный шаблон конфигурации</value>
@@ -1492,7 +1492,7 @@
<value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value>
</data>
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value>
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную</value>
</data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Начинается разбор и обработка содержимого подписки</value>
@@ -1501,7 +1501,7 @@
<value>Выбрать профиль</value>
</data>
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Добавьте хотя бы одну конфигурацию</value>
@@ -1588,7 +1588,7 @@
<value>Привязанный сертификат (заполните любое из полей)
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>Получить сертификат</value>
@@ -1642,10 +1642,10 @@
<value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value>
</data>
<data name="TbDirectResolveStrategyTips" xml:space="preserve">
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль.</value>
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль</value>
</data>
<data name="TbRemoteResolveStrategyTips" xml:space="preserve">
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль.</value>
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль</value>
</data>
<data name="TbHopInt7" xml:space="preserve">
<value>Интервал смены портов (Port Hopping)</value>
@@ -1660,37 +1660,37 @@
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел.</value>
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен.</value>
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен.</value>
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен.</value>
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>У группы {0} нет допустимых дочерних узлов.</value>
<value>У группы {0} нет допустимых дочерних узлов</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел.</value>
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел.</value>
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Предыдущий прокси подписки {0} не найден. Пропущено.</value>
<value>Предыдущий прокси подписки {0} не найден. Пропущено</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Следующий прокси подписки {0} не найден. Пропущено.</value>
<value>Следующий прокси подписки {0} не найден. Пропущено</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>Сгенерировать группу политик</value>
@@ -1756,7 +1756,7 @@
<value>Привязать интерфейс</value>
</data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value>
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>Общий ключ (PSK)</value>
@@ -1780,13 +1780,13 @@
<value>Доступно обновление</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя.</value>
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Адреса, исключаемые из маршрутизации</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>Разделяйте запятыми (,).</value>
<value>Разделяйте запятыми (,)</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
@@ -1798,22 +1798,22 @@
<value>Включить финальную фрагментацию (Final Fragment)</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку.</value>
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS через Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM).</value>
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM)</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Некорректный Realm URL.</value>
<value>Некорректный Realm URL</value>
</data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>Введите корректный JSON заголовков HTTP-запроса.</value>
<value>Введите корректный JSON заголовков HTTP-запроса</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Формат: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
@@ -1822,12 +1822,42 @@
<value>Размер пакета Gecko (мин/макс)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>Если включено, используется sing-box TUN; иначе — xray TUN.</value>
<value>Если включено, используется sing-box TUN; иначе — xray TUN</value>
</data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>Поставщик корневых сертификатов</value>
</data>
<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>
</root>

View File

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

View File

@@ -1417,7 +1417,7 @@
<value>新增 [Anytls] 節點</value>
</data>
<data name="TbRemoteDNS" xml:space="preserve">
<value>遠 DNS</value>
<value>遠 DNS</value>
</data>
<data name="TbDomesticDNS" xml:space="preserve">
<value>直連 DNS</value>
@@ -1429,16 +1429,16 @@
<value>代理目標解析策略</value>
</data>
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>新增常用 DNS Hosts</value>
<value>新增常用 DNS 主機對應</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>
<data name="TbBlockSVCBHTTPSQueries" xml:space="preserve">
<value>阻止 SVCB HTTPS 查詢</value>
<value>封鎖 SVCB HTTPS 查詢</value>
</data>
<data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts網域名稱1 ip1 ip2” 一行一個</value>
<value>DNS 主機對應:(每行一組「網域名稱1 ip1 ip2</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve">
<value>DNS 基礎設定</value>
@@ -1447,49 +1447,49 @@
<value>DNS 進階設定</value>
</data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>校驗相應地區域名 IP</value>
<value>驗證區域網域名稱的 IP</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>配置後,會對相應地區域名(如 geosite:cn - geoip:cn的返回 IP 進行校驗,僅返回期望 IP</value>
<value>設定後,系統會驗證區域網域名稱(例如 geosite:cn - geoip:cn所傳回的 IP並只傳回預期的 IP</value>
</data>
<data name="TbCustomDNSEnable" xml:space="preserve">
<value>啟用自訂 DNS</value>
</data>
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>自訂 DNS 已啟用,此頁面配置將無效</value>
<value>已啟用自訂 DNS,此頁面的設定將不會生效</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>開啟後將阻止 ECH HTTP/3 可用性查詢</value>
<value>啟用後將封鎖 ECH HTTP/3 可用性查詢。Xray 固定啟用</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>請填寫正確的配置範本</value>
<value>請填寫正確的設定範本</value>
</data>
<data name="menuFullConfigTemplate" xml:space="preserve">
<value>完整配置範本設定</value>
<value>完整設定範本</value>
</data>
<data name="TbFullConfigTemplateEnable" xml:space="preserve">
<value>啟用完整配置範本</value>
<value>啟用完整設定範本</value>
</data>
<data name="TbRayFullConfigTemplate" xml:space="preserve">
<value>v2ray 完整配置範本</value>
<value>v2ray 完整設定範本</value>
</data>
<data name="TbRayFullConfigTemplateDesc" xml:space="preserve">
<value>僅添加出站配置,routing.balancers routing.rules.outboundTag,點擊查看文檔</value>
<value>僅新增出站設定、routing.balancers routing.rules.outboundTag。點選以查看說明文件</value>
</data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>不添加非代理協定出站</value>
<value>不新增非代理協定出站</value>
</data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>設定上游代理 tag</value>
<value>設定上游代理標籤</value>
</data>
<data name="TbSBFullConfigTemplate" xml:space="preserve">
<value>sing-box 完整配置範本</value>
<value>sing-box 完整設定範本</value>
</data>
<data name="TbSBFullConfigTemplateDesc" xml:space="preserve">
<value>僅添加出站端點配置,點擊查看文檔</value>
<value>僅新增出站端點設定。點選以查看說明文件</value>
</data>
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>此功能供高級用戶和有特殊需求的用戶使用。 啟用此功能後,將忽略 Core 基礎設定DNS 設定 路由設定。你需要保證系統代理的埠和流量統計等功能的配置正確,一切都由你來設定。</value>
<value>此功能適合進階使用者與有特殊需求的使用者。啟用後,Core 基礎設定DNS 設定路由設定將被忽略。請確認系統代理連接埠、流量統計及其他相關設定皆正確;所有項目都必須由您自行設定。</value>
</data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>開始解析和處理訂閱內容</value>
@@ -1498,10 +1498,10 @@
<value>選擇節點</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>默認全局生效,僅在 sing-box 中內置 FakeIP 過濾。</value>
<value>預設會套用至全域;內建 FakeIP 過濾僅支援 sing-box。</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>請至少添加一個節點</value>
<value>請至少新增一個設定檔</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>策略組</value>
@@ -1627,7 +1627,7 @@
<value>提供過期快取Serve Stale</value>
</data>
<data name="TbParallelQuery" xml:space="preserve">
<value>行查詢</value>
<value>行查詢</value>
</data>
<data name="TbDomesticDNSTips" xml:space="preserve">
<value>預設僅在路由期間進行解析時調用</value>
@@ -1849,4 +1849,16 @@
<data name="TbIpv4Address" xml:space="preserve">
<value>Ipv4 位址</value>
</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>

View File

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

View File

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

View File

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

View File

@@ -21,26 +21,27 @@ public partial class CoreConfigSingboxService
};
_coreConfig.inbounds.Add(inbound);
var inboundConf = _config.Inbound.First();
inbound.listen_port = listenPort;
if (_config.Inbound.First().SecondLocalPortEnabled)
if (inboundConf.SecondLocalPortEnabled)
{
var inbound2 = BuildInbound(inbound, EInboundProtocol.socks2, true);
_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);
inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3);
//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

View File

@@ -48,12 +48,18 @@ public partial class CoreConfigSingboxService
// 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
// 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;
if (tunAddresses?.Count > 0)
{
_coreConfig.route.rules.Add(new()
{
ip_cidr = [.. tunAddresses],
ip_cidr = [.. tunAddresses.Select(ToSingleAddressPrefix)],
action = "reject",
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()
{
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

View File

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

View File

@@ -9,37 +9,38 @@ public partial class CoreConfigV2rayService
var listen = "0.0.0.0";
var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
_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;
if (!context.IsTunEnabled || !isUsingLocalMixedPort)
{
_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);
}
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;
_coreConfig.inbounds.Add(inbound3);
// 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.accounts =
[
new()
{
user = _config.Inbound.First().User,
pass = _config.Inbound.First().Pass,
user = inboundConf.User,
pass = inboundConf.Pass,
},
];
@@ -66,20 +67,23 @@ public partial class CoreConfigV2rayService
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
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)
{
var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
tunInbound.settings.gateway.Add(address6);
tunInbound.settings.autoSystemRoutingTable.Add("::/0");
}
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (!bindInterface.IsNullOrEmpty())
{
tunInbound.settings.autoOutboundsInterface = bindInterface;
}
tunInbound.sniffing = inbound.sniffing;
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
tunInbound.sniffing.routeOnly = true;
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
@@ -117,15 +121,8 @@ public partial class CoreConfigV2rayService
includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList();
includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList();
if (_config.TunModeItem.EnableIPv6Address)
{
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
.Concat(includeListV6.Select(x => x.ToString())).ToList();
}
else
{
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList();
}
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
.Concat(includeListV6.Select(x => x.ToString())).ToList();
}
_coreConfig.inbounds.Add(tunInbound);

View File

@@ -512,6 +512,7 @@ public partial class CoreConfigV2rayService
settings = new MaskSettings4Ray { value = kcpSeed },
});
}
kcpFinalmask.udp?.Reverse();
streamSettings.kcpSettings = kcpSettings;
streamSettings.finalmask = kcpFinalmask;
break;
@@ -666,6 +667,7 @@ public partial class CoreConfigV2rayService
version = 2,
auth = _node.Password,
};
hy2Finalmask.udp?.Reverse();
streamSettings.finalmask = hy2Finalmask;
break;

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ public partial class AddServer2ViewModel : MyReactiveObject, ICloseable
BrowseServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
var fileName = await BrowseConfigFileInteraction.Handle(RxVoid.Default);
var fileName = await BrowseConfigFileInteraction.HandleSafe(RxVoid.Default);
if (fileName.IsNullOrEmpty())
{
return;

View File

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

View File

@@ -85,8 +85,6 @@ public partial class MainWindowViewModel : MyReactiveObject
#endregion Menu
private readonly SynchronizationContext _uiContext = SynchronizationContext.Current;
#region Init
public MainWindowViewModel()
@@ -302,7 +300,7 @@ public partial class MainWindowViewModel : MyReactiveObject
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blShow =>
{
await ShowHideWindowInteraction.Handle(blShow);
await ShowHideWindowInteraction.HandleSafe(blShow);
});
StatusBarViewModel.SetDefaultServerRequested
@@ -413,14 +411,25 @@ public partial class MainWindowViewModel : MyReactiveObject
private async Task RefreshServersDispatcherAsync()
{
//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()
{
//await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler);
_uiContext?.Post(_ => _ = ProfilesViewModel.RefreshSubscriptions(), null);
await Signal.FromAsync(async () =>
{
await ProfilesViewModel.RefreshSubscriptions();
return RxVoid.Default;
})
.SubscribeOn(RxSchedulers.MainThreadScheduler)
.ToTask();
}
#endregion Servers && Groups
@@ -467,7 +476,7 @@ public partial class MainWindowViewModel : MyReactiveObject
var stringData = clipboardData;
if (clipboardData == null)
{
var result = await ReadTextFromClipboardInteraction.Handle(RxVoid.Default);
var result = await ReadTextFromClipboardInteraction.HandleSafe(RxVoid.Default);
if (result.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
@@ -490,7 +499,7 @@ public partial class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaScanAsync()
{
var result = await ScanScreenInteraction.Handle(RxVoid.Default);
var result = await ScanScreenInteraction.HandleSafe(RxVoid.Default);
await ScanScreenResult(result);
}
@@ -502,7 +511,7 @@ public partial class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaImageAsync()
{
var imageFileName = await BrowseImageFileInteraction.Handle(RxVoid.Default);
var imageFileName = await BrowseImageFileInteraction.HandleSafe(RxVoid.Default);
await AddScanResultAsync(imageFileName);
}
@@ -691,10 +700,12 @@ public partial class MainWindowViewModel : MyReactiveObject
//{
// await ClashProxiesViewModel.ProxiesReload();
//}, RxSchedulers.MainThreadScheduler);
RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await ClashProxiesViewModel.ProxiesReload();
});
await Signal.FromAsync(async () =>
{
await ClashProxiesViewModel.ProxiesReload();
return RxVoid.Default;
}).SubscribeOn(RxSchedulers.MainThreadScheduler)
.ToTask();
}
ReloadResult(showClashUI);

View File

@@ -74,7 +74,7 @@ public partial class MsgViewModel : MyReactiveObject
{
try
{
await DispatcherShowMsgInteraction.Handle(sb.ToString());
await DispatcherShowMsgInteraction.HandleSafe(sb.ToString());
}
catch (Exception)
{

View File

@@ -3,7 +3,7 @@ namespace ServiceLib.ViewModels;
public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
{
public event EventHandler? RequestClose;
public Interaction<RxVoid, RxVoid> ProfilesFocusInteraction { get; } = new();
#region private prop
@@ -138,13 +138,7 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
await RefreshServers();
try
{
await ProfilesFocusInteraction.Handle(RxVoid.Default);
}
catch (UnhandledInteractionException<RxVoid, RxVoid>)
{
}
await ProfilesFocusInteraction.HandleSafe(RxVoid.Default);
}
private async Task ServerFilterChanged(bool c)

View File

@@ -345,13 +345,7 @@ public partial class ProfilesViewModel : MyReactiveObject
await RefreshServers();
try
{
await ProfilesFocusInteraction.Handle(RxVoid.Default);
}
catch (UnhandledInteractionException<RxVoid, RxVoid>)
{
}
await ProfilesFocusInteraction.HandleSafe(RxVoid.Default);
}
private async Task ServerFilterChanged(bool c)
@@ -395,13 +389,7 @@ public partial class ProfilesViewModel : MyReactiveObject
SelectedProfile = selected ?? lstModel.First();
}
try
{
await DispatcherRefreshServersBizInteraction.Handle(RxVoid.Default);
}
catch (UnhandledInteractionException<RxVoid, RxVoid>)
{
}
await DispatcherRefreshServersBizInteraction.HandleSafe(RxVoid.Default);
}
public async Task RefreshSubscriptions()
@@ -419,7 +407,7 @@ public partial class ProfilesViewModel : MyReactiveObject
public async Task AdjustMainLvColWidth()
{
await AdjustMainLvColWidthInteraction.Handle(RxVoid.Default);
await AdjustMainLvColWidthInteraction.HandleSafe(RxVoid.Default);
}
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
@@ -535,7 +523,7 @@ public partial class ProfilesViewModel : MyReactiveObject
{
return;
}
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
{
return;
}
@@ -556,7 +544,7 @@ public partial class ProfilesViewModel : MyReactiveObject
private async Task RemoveDuplicateServer()
{
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
{
return;
}
@@ -631,7 +619,7 @@ public partial class ProfilesViewModel : MyReactiveObject
return;
}
await ShareServerInteraction.Handle(url);
await ShareServerInteraction.HandleSafe(url);
}
private async Task GenGroupAllServer()
@@ -799,13 +787,13 @@ public partial class ProfilesViewModel : MyReactiveObject
}
else
{
await SetClipboardDataInteraction.Handle((string)result.Data);
await SetClipboardDataInteraction.HandleSafe((string)result.Data);
NoticeManager.Instance.SendMessage(ResUI.OperationSuccess);
}
}
else
{
await SaveFileDialogInteraction.Handle(item);
await SaveFileDialogInteraction.HandleSafe(item);
}
}
@@ -854,11 +842,11 @@ public partial class ProfilesViewModel : MyReactiveObject
{
if (blEncode)
{
await SetClipboardDataInteraction.Handle(Utils.Base64Encode(sb.ToString()));
await SetClipboardDataInteraction.HandleSafe(Utils.Base64Encode(sb.ToString()));
}
else
{
await SetClipboardDataInteraction.Handle(sb.ToString());
await SetClipboardDataInteraction.HandleSafe(sb.ToString());
}
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
}
@@ -881,7 +869,7 @@ public partial class ProfilesViewModel : MyReactiveObject
if (!result.IsNullOrEmpty())
{
await SetClipboardDataInteraction.Handle(result);
await SetClipboardDataInteraction.HandleSafe(result);
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
}
else
@@ -925,7 +913,7 @@ public partial class ProfilesViewModel : MyReactiveObject
return;
}
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
{
return;
}

View File

@@ -25,6 +25,21 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
[Reactive]
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> SaveCmd { get; }
@@ -57,6 +72,11 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
IP = Utils.List2String(SelectedSource.Ip, true);
Process = Utils.List2String(SelectedSource.Process, true);
RuleType = SelectedSource.RuleType?.ToString();
OutboundTag = SelectedSource.OutboundTag;
Remarks = SelectedSource.Remarks;
Port = SelectedSource.Port;
Network = SelectedSource.Network;
Enabled = SelectedSource.Enabled;
}
private async Task SaveRulesAsync()
@@ -80,6 +100,11 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
SelectedSource.Protocol = ProtocolItems?.ToList();
SelectedSource.InboundTag = InboundTagItems?.ToList();
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
|| SelectedSource.Ip?.Count > 0
@@ -110,8 +135,7 @@ public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
var profileItem = await profileSelectViewModel.GetProfileItem();
if (profileItem != null)
{
SelectedSource.OutboundTag = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
OutboundTag = profileItem.Remarks;
}
}
}

View File

@@ -48,7 +48,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
});
ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () =>
{
var fileName = await BrowseRulesFileInteraction.Handle(RxVoid.Default);
var fileName = await BrowseRulesFileInteraction.HandleSafe(RxVoid.Default);
await ImportRulesFromFileAsync(fileName);
});
ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -156,7 +156,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return;
}
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
{
return;
}
@@ -199,7 +199,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
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;
if (clipboardData == null)
{
var result = await ReadTextFromClipboardInteraction.Handle(RxVoid.Default);
var result = await ReadTextFromClipboardInteraction.HandleSafe(RxVoid.Default);
if (result.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
@@ -315,7 +315,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData)
{
var blReplace = false;
if (await ShowYesNoInteraction.Handle(ResUI.AddBatchRoutingRulesYesNo) == false)
if (await ShowYesNoInteraction.HandleSafe(ResUI.AddBatchRoutingRulesYesNo) == false)
{
blReplace = true;
}

View File

@@ -147,7 +147,7 @@ public partial class RoutingSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return;
}
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
{
return;
}

View File

@@ -243,7 +243,7 @@ public partial class StatusBarViewModel : MyReactiveObject
sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}");
sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}");
await SetClipboardDataInteraction.Handle(sb.ToString());
await SetClipboardDataInteraction.HandleSafe(sb.ToString());
}
private async Task AddServerViaClipboard()
@@ -297,21 +297,14 @@ public partial class StatusBarViewModel : MyReactiveObject
return;
}
var models = new List<ComboItem>();
BlServers = true;
foreach (var it in lstModel)
{
var name = it.GetSummary();
var models = lstModel.Select(it => new ComboItem { ID = it.IndexId, Text = it.GetSummary() }).ToList();
var item = new ComboItem() { ID = it.IndexId, Text = name };
models.Add(item);
if (_config.IndexId == it.IndexId)
{
SelectedServer = item;
}
}
BlServers = true;
Servers.Clear();
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)
@@ -389,14 +382,7 @@ public partial class StatusBarViewModel : MyReactiveObject
if (blChange)
{
try
{
await DispatcherRefreshIconInteraction.Handle(RxVoid.Default);
}
catch (UnhandledInteractionException<RxVoid, RxVoid>)
{
// Ignore
}
await DispatcherRefreshIconInteraction.HandleSafe(RxVoid.Default);
}
}
@@ -432,7 +418,7 @@ public partial class StatusBarViewModel : MyReactiveObject
{
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
ReloadRequested.Publish();
await DispatcherRefreshIconInteraction.Handle(RxVoid.Default);
await DispatcherRefreshIconInteraction.HandleSafe(RxVoid.Default);
}
}
@@ -469,7 +455,7 @@ public partial class StatusBarViewModel : MyReactiveObject
}
else
{
var password = await PasswordInputInteraction.Handle(RxVoid.Default);
var password = await PasswordInputInteraction.HandleSafe(RxVoid.Default);
if (password.IsNullOrEmpty())
{
_config.TunModeItem.EnableTun = false;

View File

@@ -10,6 +10,12 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
[Reactive]
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> SelectNextProfileCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
@@ -23,8 +29,7 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
var profileItem = await SelectProfileAsync();
if (profileItem != null)
{
SelectedSource?.PrevProfile = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
PrevProfile = profileItem.Remarks;
}
});
SelectNextProfileCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -32,8 +37,7 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
var profileItem = await SelectProfileAsync();
if (profileItem != null)
{
SelectedSource?.NextProfile = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
NextProfile = profileItem.Remarks;
}
});
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -43,6 +47,8 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
SelectedSource = subItem.Id.IsNullOrEmpty() ? subItem : JsonUtils.DeepCopy(subItem);
CustomCoreType = SelectedSource.CustomCoreType?.ToString() ?? string.Empty;
PrevProfile = SelectedSource.PrevProfile;
NextProfile = SelectedSource.NextProfile;
}
private async Task SaveSubAsync()
@@ -72,6 +78,8 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
}
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
SelectedSource.PrevProfile = PrevProfile;
SelectedSource.NextProfile = NextProfile;
if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0)
{

View File

@@ -40,7 +40,7 @@ public partial class SubSettingViewModel : MyReactiveObject
}, canEditRemove);
SubShareCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ShareSubInteraction.Handle(SelectedSource?.Url);
await ShareSubInteraction.HandleSafe(SelectedSource?.Url);
}, canEditRemove);
_ = Init();
@@ -84,7 +84,7 @@ public partial class SubSettingViewModel : MyReactiveObject
private async Task DeleteSubAsync()
{
if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
if (await ShowYesNoInteraction.HandleSafe(ResUI.RemoveServer) == false)
{
return;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,12 +26,12 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
.Subscribe(InitializeData)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.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.SelectedSource.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.SelectedSource.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.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Port, v => v.txtPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Network, v => v.cmbNetwork.SelectedValue).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.IP, v => v.txtIP.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Process, v => v.txtProcess.Text).DisposeWith(disposables);

View File

@@ -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.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.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.PrevProfile, v => v.txtPrevProfile.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.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.SelectedValue).DisposeWith(disposables);

View File

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

View File

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

View File

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

View File

@@ -23,11 +23,11 @@ public partial class RoutingRuleDetailsWindow
.Subscribe(InitializeData)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.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.SelectedSource.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.SelectedSource.Enabled, v => v.togEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Port, v => v.txtPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Network, v => v.cmbNetwork.Text).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.IP, v => v.txtIP.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Process, v => v.txtProcess.Text).DisposeWith(disposables);

View File

@@ -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.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.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.PrevProfile, v => v.txtPrevProfile.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.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.Text).DisposeWith(disposables);