Compare commits

...

109 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
2dust
5efff37acb up 7.24.5 2026-08-05 19:49:08 +08:00
2dust
e8cdd1cc02 Remove allowInsecure from the Xray Core
https://github.com/2dust/v2rayN/issues/9893
2026-08-05 18:48:39 +08:00
2dust
d381d429e4 Wait briefly after sudo process kills
https://github.com/2dust/v2rayN/issues/9863
Add a short 1-second delay after terminating core processes on Linux, macOS, and in CoreAdminManager so ports and process resources have time to settle before the next step runs.
2026-08-05 17:03:23 +08:00
DHR60
b27ae11d1e Adjust allow insecure fmt (#9888)
* Adjust allow insecure fmt

* Remove `VLESS` insecure fmt
2026-08-05 14:26:16 +08:00
DHR60
eff584597f Add custom outbound support (#9817)
* Add custom outbound support

* Add test

* Add inner fmt support

* Full config to outbounds

* Rename to `Outbound`

* AI optimized

* Fix

* Add bind interface placeholder

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-08-05 14:25:16 +08:00
2dust
ccf18ba0fb Bug fix
https://github.com/2dust/v2rayN/issues/9896
2026-08-05 11:28:46 +08:00
sparklelcm333
15ba2bdf93 fix: 修复回归的窗口启动时偏右下偏移 (#9875) 2026-08-05 10:38:51 +08:00
Tylerrrkd
e1e6c5ddb0 Fix infinite TUN routing loop on traffic to the TUN's own addresses (#9897)
* Restore the silently dropped embedded TUN routing rules

tun_singbox_rules declares "network": "udp" as a bare string, but
Rule4Sbox.network is a List<string>. System.Text.Json therefore throws on
the very first rule, and JsonUtils.Deserialize swallows it and returns
default. GenRouting only checks for null before AddRange, so the whole
embedded rule set is dropped without a trace.

Both rules it carries have thus never reached a generated config: the
NetBIOS/mDNS reject and the multicast reject. The mismatch predates the
sing-box 1.12 migration - the template has used the string form since the
rules file was introduced.

- declare network as an array so the template matches Rule4Sbox

* Drop traffic addressed to the TUN's own addresses

With auto_route the TUN steals the default route, so a packet whose
destination is the TUN interface's own address is handed to sing-box instead
of being looped back by the kernel. Routing then matches ip_is_private and
sends it to `direct`, whose interface is auto-detected as the TUN again, so
the packet is written straight back into the TUN and re-enters routing. The
loop never terminates and pins a CPU core.

Seen in the wild on macOS: a WebRTC client offered the TUN's own
fc00::172:18:0:1 ULA as an ICE candidate and the resulting STUN
connectivity checks sustained ~8k packets/s out of the interface at 800%
CPU, 21 GB written to the TUN over five hours - against 3 GB read, the
asymmetry that gives the loop away.

Nothing legitimate is addressed to those addresses, so reject them before
any outbound rule can match. Use method "drop" rather than the default
ICMP unreachable, whose destination would be the looping address itself.

- reject the TUN inbound's own addresses, taken from the generated inbound
  so the two cannot drift apart
2026-08-05 10:35:37 +08:00
DHR60
d924c6f557 ReactiveUI 24 (#9678)
* ReactiveUI.SourceGenerators

* Primitives

* Try fix
2026-08-01 10:08:43 +08:00
2dust
635a04d74c up 7.24.4 2026-07-30 20:43:20 +08:00
autorepobot
2e1bb23da7 Update build-windows-x86.yml (#9846) 2026-07-28 20:10:12 +08:00
JieXu
6f3bacd5b4 Update build-linux.yml (#9844)
* Update build-linux.yml

* Update build-linux.yml

* Update build-linux.yml
2026-07-28 10:12:11 +08:00
XXcipherX
074ce5de04 Fix missing IPv6 default route for Xray TUN (#9843)
* Fix IPv6 route for Xray TUN

* Update V2rayInboundService.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-28 10:11:20 +08:00
hyjhyj111
02df430172 Use the TUN context snapshot in remaining launch-path reads (#9831)
Follow-up to #9830: audit of all TunModeItem.EnableTun usages found
three more launch-path reads of the live mutable config where the
behavior must agree with the context snapshot that generated the
config:

- CoreConfigClashService (mihomo custom config): the tun section was
  decided from the live config while the mihomo launch elevation uses
  the snapshot; a mid-reload toggle could produce a config containing
  tun launched without sudo, the same failure fixed in #9830. The tun
  state is now passed in as a snapshot.
- CoreManager.LoadCore: the Windows RemoveTunDevice cleanup now checks
  the main/pre context snapshots.
- CoreManager.WaitForProxyPort: preContext.AppConfig is a shared live
  reference; use preContext.IsTunEnabled instead.

Reads that intentionally stay live: StatusBarViewModel (UI state
source), CoreConfigContextBuilder (the snapshot capture point),
GetPreSocksItem (called during snapshot construction, self-consistent),
and AppManager.StatePort2 (transient mid-reload skew only, self-heals
after reload).
2026-07-27 21:13:28 +08:00
2dust
611ce0fd2c Adjust MsgAllowInsecureDeprecated 2026-07-27 21:08:35 +08:00
2dust
a846e74e8f Fix
https://github.com/2dust/v2rayN/issues/9834
2026-07-27 14:08:30 +08:00
2dust
92c3df45ae up 7.24.3 2026-07-26 21:25:31 +08:00
hyjhyj111
ee7e21268a Fix TUN core elevation: decide sudo from the config context snapshot (#9830)
CoreManager.RunProcess decided sudo elevation from the live mutable
_config.TunModeItem.EnableTun while the launched config was generated
from the immutable CoreConfigContext snapshot. If the TUN state changed
while a reload was in flight, a core whose config contains a TUN
inbound could launch without elevation and die within the 100ms health
check ("Failed to run core"), with no stderr shown.

The elevation decision now follows context.IsTunEnabled /
preContext.IsTunEnabled, so the generated config and the launch mode
always agree. With legacy TUN protect, the sing-box pre-core hosting
TUN is elevated and the main core no longer runs as root needlessly.
2026-07-26 21:24:11 +08:00
2dust
dc216c2b02 Rename TunIp to TunIP 2026-07-26 21:07:36 +08:00
2dust
beffa919d3 Use /126 prefixes for TUN IPv6 addresses
https://github.com/2dust/v2rayN/pull/9830
2026-07-26 20:46:32 +08:00
2dust
a654826231 Enable legacy TUN protect by default 2026-07-26 20:46:01 +08:00
2dust
b01476d147 Fix tun inbound dns setting 2026-07-26 10:29:35 +08:00
2dust
cf8dd45abe up 7.24.2 2026-07-24 16:57:53 +08:00
2dust
9bba6f53f8 Update Directory.Packages.props 2026-07-24 16:57:22 +08:00
2dust
cd77f1d882 Code clean 2026-07-24 16:57:19 +08:00
mymyme
0427037638 fix: sanitize system proxy exceptions to avoid macOS ExceptionsList b… (#9815)
* fix: sanitize system proxy exceptions to avoid macOS ExceptionsList being dropped

On macOS/Linux the exception list is comma-separated and passed to networksetup. It was only stripped of spaces, so a stray newline or a trailing comma left a malformed/empty entry. macOS configd then silently rejects the whole ExceptionsList (scutil --proxy shows no ExceptionsList), so the bypass never takes effect. Split on ',' with TrimEntries + RemoveEmptyEntries and rejoin; behavior-preserving for well-formed input. Windows path (';') is left unchanged.

Note: not built locally (no .NET SDK on hand); relying on CI.

* Update SysProxyHandler.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-24 14:12:10 +08:00
2dust
e749b81ecf Add configurable TUN IPv4/IPv6 addresses
https://github.com/2dust/v2rayN/issues/9810
2026-07-24 10:13:08 +08:00
JieXu
fd2c942231 Update package-rhel-loong.sh (#9792)
* Update package-rhel-loong.sh

* Update package-debian-loong.sh

* Update Directory.Packages.props
2026-07-20 09:30:52 +08:00
tt2563
fa42eb670f Update ResUI.zh-Hant.resx (#9783)
Update Traditional Chinese translation
2026-07-19 09:54:45 +08:00
DHR60
0d42996573 Fix ui (#9789) 2026-07-19 09:54:26 +08:00
DHR60
a37772f12b Fakeip (#9786)
* Add fakeip range

* Xray fakedns
2026-07-18 17:47:14 +08:00
DHR60
531e0e9443 Fix (#9787) 2026-07-18 17:45:42 +08:00
2dust
984c77f0c4 up 7.24.1 2026-07-17 09:26:10 +08:00
2dust
b5e0be47a5 Code clean 2026-07-17 09:25:13 +08:00
dependabot[bot]
6df0e6de1b Bump actions/setup-dotnet from 5.4.0 to 6.0.0 (#9778)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.4.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.4.0...v6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-17 09:13:28 +08:00
autorepobot
2b5328665f Update package-rhel.sh (#9780)
* Update package-rhel.sh

* Update package-rhel-riscv.sh

* Update package-rhel-loong.sh

* Update package-debian.sh

* Update package-debian-riscv.sh

* Update package-debian-loong.sh

* Update package-debian-riscv.sh

* Update package-rhel-riscv.sh
2026-07-17 09:13:19 +08:00
DHR60
4584ecc5c9 Add HappyEyeballs (#9772)
* Add HappyEyeballs

* Clean up the code

* Fix
2026-07-17 09:12:50 +08:00
DHR60
d9dc2d7cf5 Fix (#9776) 2026-07-17 09:11:28 +08:00
DHR60
36f8565b7a Update cert (#9773) 2026-07-17 08:59:30 +08:00
DHR60
f55d8b2565 Sync xray (#9708)
* Sync xray fragment

* Sync xray freedom domainStrategy

* Add Strategy4ProxyDial
2026-07-16 18:55:06 +08:00
DHR60
09ea4890a7 Fix (#9771) 2026-07-16 17:37:17 +08:00
DHR60
5cc2aaba13 Fix (#9769) 2026-07-16 17:37:04 +08:00
DHR60
b8889bad86 Fix (#9768) 2026-07-16 17:36:51 +08:00
JieXu
5dd5b25869 Update Directory.Packages.props (#9766) 2026-07-16 11:48:57 +08:00
2dust
fafdb4a0b6 up 7.24.0 2026-07-16 11:42:28 +08:00
2dust
223642dd67 Fix
https://github.com/2dust/v2rayN/issues/9765
2026-07-16 11:39:47 +08:00
Miheichev Aleksandr Sergeevich
165e0bf9e9 chore(deps): update NuGet dependencies (#9745)
- Downloader 5.9.0 -> 5.9.4
- NLog 6.1.3 -> 6.1.4
- Repobot.SQLite.Unofficial 3.53.3 -> 3.53.3.8
- sqlite-net-e 1.11.0 -> 1.11.285
- YamlDotNet 18.0.0 -> 18.1.0

sqlite-net-e and Repobot.SQLite.Unofficial are bumped together: the newer
sqlite-net-e moves to SQLitePCLRaw.config.e_sqlite3 3.0.3.

SkiaSharp.NativeAssets.Linux intentionally stays at 3.119.4 to match the
managed SkiaSharp resolved through Avalonia.Skia 12.1.0; bumping it alone
to 4.x would compile but break rendering at runtime on Linux. It should
follow Avalonia when Avalonia itself moves to SkiaSharp 4.
ReactiveUI.Avalonia stays at 12.0.3: the numerically higher 14.x line
targets Avalonia 11 / ReactiveUI 19 and would be an effective downgrade
for the Avalonia 12 stack.
2026-07-15 19:12:56 +08:00
DHR60
e615314582 Xray tun route only (#9759) 2026-07-15 19:12:34 +08:00
DHR60
ca9978b625 Fix (#9758) 2026-07-15 18:12:55 +08:00
DHR60
74ab7ad097 Add ProtectCoreTypeList (#9756)
* Add ProtectCoreTypeList

* Add process path restrictions
2026-07-15 17:32:07 +08:00
DHR60
8f5cbad988 Xray precore for custom config (#9754)
* Xray precore for custom config

* Fix

* Custom Pre

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-15 17:23:21 +08:00
2dust
1ab9757498 Revert "fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移 (#9498)"
This reverts commit 8147b39723.
https://github.com/2dust/v2rayN/issues/9740
2026-07-14 17:27:30 +08:00
DHR60
50ab02e8a9 Desktop DragDropSort (#9734) 2026-07-14 09:57:00 +08:00
2dust
b9a1c129c4 Fixed the issue with RefreshIcon's call in the desktop version. 2026-07-14 09:52:01 +08:00
tt2563
996eb51f9b Update Traditional Chinese translation (#9742) 2026-07-14 09:11:48 +08:00
Miheichev Aleksandr Sergeevich
b9a58e7137 i18n(ru): translate newly added UI strings (#9733)
Add Russian translations for the 20 resource strings introduced up to
v7.23.4 that were missing from ResUI.ru.resx:

- allowInsecure deprecation warning and insecure-configuration message
- Xray HTTP outbound custom headers (label, tip, validation message)
- Hysteria2 Realm URL (label, format tip, validation message)
- TUN route exclude address (label, tip, invalid-address message)
- Final Fragment option and Root Certificate Provider (labels and tips)
- DNS via Bridge, Gecko Packet Size, Legacy Protect tip,
  Verify Peer Cert By Name, options-conflict message

Translated from the zh-Hans source and cross-checked against the English
resource. Existing Russian terminology conventions are preserved, and key
ordering mirrors the English resource file.
2026-07-14 09:11:14 +08:00
DHR60
9664ee380e ViewModel-First (#9621)
* Interaction

* Remove view action

* ViewModel-first

* MainGirdOrientation Hot Reload

* Fix avalonia preview

* Remove CloseWindowInteraction

* Fix

* Avoid threading issues

* Fix

* ProfilesSelect

* Try fix previewer

* Remove AppEvents.ProfilesRefreshRequested

* Remove AppEvents.SubscriptionsRefreshRequested

* Remove AppEvents.ProxiesReloadRequested

* Remove AppEvents.AdjustMainLvColWidthRequested

* Remove AppEvents.SetDefaultServerRequested

* Remove AppEvents.RoutingsMenuRefreshRequested

* Remove AppEvents.TestServerRequested

* Remove AppEvents.InboundDisplayRequested

* Remove AppEvents.SubscriptionsUpdateRequested

* Remove AppEvents.ShowHideWindowRequested

* Remove AppEvents.ReloadRequested

* Remove AppEvents.AddServerRequested

* Fix
2026-07-14 09:05:05 +08:00
DHR60
6f50206606 Update Avalonia to 12.1 (#9148) 2026-07-13 09:41:28 +08:00
2dust
1de83f96ed up 7.23.4 2026-07-11 17:44:19 +08:00
2dust
994bc1ca6a Code clean 2026-07-11 17:43:32 +08:00
2dust
3ca49dd9db Add option to hide IP info column 2026-07-11 16:05:31 +08:00
DHR60
a0bd8f9934 Remove fallbackTag (#9728) 2026-07-11 15:18:16 +08:00
006lp
88d59488fd feat: support custom headers for Xray HTTP outbounds (#9690)
* feat: support custom headers for Xray HTTP outbounds

* feat: simplify HTTP outbound headers input

* feat: validate JSON syntax in HTTP headers

  Keep HTTP outbound headers stored as raw text, while validating
  header names and values against RFC 9110/9112 syntax. Support string
  arrays for multi-value headers, reject duplicate JSON header keys, and
  clarify the invalid header JSON message.

* feat: refactor HTTP header parsing into utility

* Simplify code

* Simplify code

* Update CoreConfigV2rayServiceTests.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-11 14:31:17 +08:00
DHR60
2c70b018ce Revert "Fix" (#9727)
* Revert "Fix"

This reverts commit 45ab7503e3.

* Fix
2026-07-11 14:02:12 +08:00
Kurobac
b6ab428dfc Fix Linux tray tooltip identity (#9709)
Keep the tray tooltip text fixed to the application name on Linux so desktop environments do not treat server changes as tray identity changes. Non-Linux platforms continue to show the current server information.
2026-07-11 11:42:00 +08:00
2dust
45ab7503e3 Fix
https://github.com/2dust/v2rayN/issues/9699
2026-07-11 11:20:36 +08:00
2dust
1768b4a7ee Update winget-publish.yml 2026-07-09 20:51:04 +08:00
228 changed files with 9188 additions and 7772 deletions

View File

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

View File

@@ -1,3 +1,49 @@
/*
==============================================================================
THIRD-PARTY CA CERTIFICATE DATA ATTRIBUTION
==============================================================================
This component includes CA certificate data obtained from third-party sources.
1. Common CA Database (CCADB)
------------------------------------------------------------------------------
Website:
https://www.ccadb.org/
Data source:
https://ccadb.my.salesforce-sites.com/mozilla/IncludedRootsPEMTxt?TrustBitsInclude=Websites
License:
Community Data License Agreement Permissive, Version 2.0 (CDLA-2.0 Permissive)
The names and trademarks of CCADB contributors may not be used to endorse or
promote products derived from this data without prior written permission.
License text:
https://cdla.dev/permissive-2-0/
2. Chromium Root Store
------------------------------------------------------------------------------
Source:
Chromium Project
Data source:
https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/root_store.certs?format=TEXT
License:
BSD 3-Clause License
Copyright:
Copyright (c) The Chromium Authors
The Chromium Root Store data is distributed under the BSD 3-Clause License.
License text:
https://opensource.org/licenses/BSD-3-Clause
==============================================================================
*/
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
@@ -27,68 +73,23 @@ Console.WriteLine("\nAll done!");
async Task ProcessMozillaAsync(string outputDir) async Task ProcessMozillaAsync(string outputDir)
{ {
const string certdataUrl = "https://raw.githubusercontent.com/mozilla-firefox/firefox/refs/heads/release/security/nss/lib/ckfw/builtins/certdata.txt"; const string certdataUrl = "https://ccadb.my.salesforce-sites.com/mozilla/IncludedRootsPEMTxt?TrustBitsInclude=Websites";
var outputFile = Path.Combine(outputDir, "mozilla_roots_pem"); var outputFile = Path.Combine(outputDir, "mozilla_roots_pem");
Console.WriteLine("Downloading Mozilla certdata.txt..."); Console.WriteLine("Downloading Mozilla certdata.txt...");
var content = await client.GetStringAsync(certdataUrl); var content = await client.GetStringAsync(certdataUrl);
Console.WriteLine("Parsing MULTILINE_OCTAL to PEM..."); var pemMatches = Regex.Matches(content, @"(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)");
var pems = ParseMozillaCertData(content); var pems = new List<string>();
foreach (Match m in pemMatches)
{
pems.Add(m.Groups[1].Value.Trim());
}
await File.WriteAllTextAsync(outputFile, string.Join("\n\n", pems)); await File.WriteAllTextAsync(outputFile, string.Join("\n\n", pems));
Console.WriteLine($"Mozilla roots saved to: {outputFile} ({pems.Count} certificates)"); Console.WriteLine($"Mozilla roots saved to: {outputFile} ({pems.Count} certificates)");
} }
List<string> ParseMozillaCertData(string text)
{
var pems = new List<string>();
var matches = Regex.Matches(text,
@"CKA_VALUE MULTILINE_OCTAL\s*([\s\S]*?)\s*END",
RegexOptions.Multiline);
foreach (Match m in matches)
{
var octalBlock = m.Groups[1].Value.Trim();
var der = OctalToBytes(octalBlock);
if (der.Length > 0)
{
var pem = ConvertToPem(der);
pems.Add(pem);
}
}
return pems;
}
byte[] OctalToBytes(string octalBlock)
{
var bytes = new List<byte>();
var matches = Regex.Matches(octalBlock, @"\\(\d{1,3})");
foreach (Match m in matches)
{
bytes.Add((byte)Convert.ToInt32(m.Groups[1].Value, 8));
}
return bytes.ToArray();
}
string ConvertToPem(byte[] der)
{
var base64 = Convert.ToBase64String(der);
var sb = new StringBuilder();
sb.Append("-----BEGIN CERTIFICATE-----\n");
for (int i = 0; i < base64.Length; i += 64)
{
var length = Math.Min(64, base64.Length - i);
sb.Append(base64.Substring(i, length));
sb.Append("\n");
}
sb.Append("-----END CERTIFICATE-----\n");
return sb.ToString().TrimEnd();
}
async Task ProcessChromeAsync(string outputDir) async Task ProcessChromeAsync(string outputDir)
{ {
const string chromeCertsUrl = "https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/root_store.certs?format=TEXT"; const string chromeCertsUrl = "https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/root_store.certs?format=TEXT";

View File

@@ -205,7 +205,7 @@ jobs:
deb-loong64: deb-loong64:
name: build and release deb loong64 name: build and release deb loong64
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04-arm
env: env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2 QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
@@ -234,10 +234,10 @@ jobs:
- name: Download QEMU prebuild - name: Download QEMU prebuild
run: | run: |
set -euo pipefail set -euo pipefail
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz" wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
tar -xzf qemu-linux-x64-v3.tar.gz tar -xzf qemu-linux-arm64-v8.2.tar.gz
mkdir -p "$HOME/qemu-install" mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/" rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version "$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware - name: Download loong64 qcow2 and EFI firmware
shell: bash shell: bash
@@ -355,7 +355,7 @@ jobs:
rpm-loong64: rpm-loong64:
name: build and release rpm loong64 name: build and release rpm loong64
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04-arm
env: env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2 QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2
@@ -384,10 +384,10 @@ jobs:
- name: Download QEMU prebuild - name: Download QEMU prebuild
run: | run: |
set -euo pipefail set -euo pipefail
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz" wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
tar -xzf qemu-linux-x64-v3.tar.gz tar -xzf qemu-linux-arm64-v8.2.tar.gz
mkdir -p "$HOME/qemu-install" mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/" rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version "$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware - name: Download loong64 qcow2 and EFI firmware
shell: bash shell: bash

View File

@@ -52,7 +52,7 @@ jobs:
dotnet --list-sdks 2>$null; $LASTEXITCODE=0 dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET 10.0.1xx - name: Setup .NET 10.0.1xx
uses: actions/setup-dotnet@v5.4.0 uses: actions/setup-dotnet@v6
with: with:
dotnet-version: 10.0.1xx dotnet-version: 10.0.1xx
@@ -146,7 +146,7 @@ jobs:
$xrayTag = "v$xrayVer" $xrayTag = "v$xrayVer"
$singTag = "v$singVer" $singTag = "v$singVer"
$xrayUrl = "https://github.com/autorepobot/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip" $xrayUrl = "https://github.com/autorepobot/Xray/releases/download/$xrayTag/Xray-windows-32.zip"
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip" $singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
Write-Host "Bundled Xray version: $xrayVer" Write-Host "Bundled Xray version: $xrayVer"

View File

@@ -69,9 +69,9 @@ jobs:
dotnet --list-sdks 2>$null; $LASTEXITCODE=0 dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v5.4.0 uses: actions/setup-dotnet@v6
with: with:
dotnet-version: '10.0.1xx' dotnet-version: 10.0.1xx
- name: Build v2rayN - name: Build v2rayN
shell: bash shell: bash
@@ -83,8 +83,13 @@ jobs:
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
- name: Remove Debug Symbols
shell: bash
run: |
find ${{ matrix.arch }} -type f -name '*.pdb' -delete
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v7.0.1 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.arch }} name: ${{ matrix.arch }}
path: ${{ matrix.arch }} path: ${{ matrix.arch }}

View File

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

View File

@@ -23,8 +23,13 @@ jobs:
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1 $targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
$x64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-64\.zip' | Select -ExpandProperty browser_download_url $assets = $targetRelease | Select -ExpandProperty assets -First 1
$arm64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-arm64\.zip' | Select -ExpandProperty browser_download_url $x64InstallerUrl = $assets | Where-Object { $_.name -eq 'v2rayN-windows-64.zip' } | Select -ExpandProperty browser_download_url
$arm64InstallerUrl = $assets | Where-Object { $_.name -eq 'v2rayN-windows-arm64.zip' } | Select -ExpandProperty browser_download_url
if (-not $x64InstallerUrl -or -not $arm64InstallerUrl) {
throw "Could not find required installers in release assets."
}
$ver = $targetRelease.tag_name $ver = $targetRelease.tag_name

View File

@@ -50,6 +50,8 @@ Read the Wiki for usage guides and configuration details.
| Linux | ✅ | - | ✅ | ✅ | ✅ | | Linux | ✅ | - | ✅ | ✅ | ✅ |
| macOS | ✅ | - | ✅ | - | - | | macOS | ✅ | - | ✅ | - | - |
Minimum OS requirements: [Release files introduction](https://github.com/2dust/v2rayN/wiki/Release-files-introduction) / 最低系统要求:[发布文件介绍](https://github.com/2dust/v2rayN/wiki/Release-files-introduction)
--- ---
## GPG Verification / GPG 签名校验 ## GPG Verification / GPG 签名校验

5
global.json Normal file
View File

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

View File

@@ -13,8 +13,8 @@ PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0" DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.109" DOTNET_LOONGARCH_VERSION="10.0.111"
DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64" DOTNET_LOONGARCH_TAG="v10.0.111-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
@@ -597,7 +597,7 @@ package_binary() {
write_desktop_file "$stage" write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir" write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.2.0), coreutils (>= 9.5), bash (>= 5.2.26), libfreetype6 (>= 2.13)" extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
mkdir -p "$workdir/debian" mkdir -p "$workdir/debian"
cat > "$workdir/debian/control" <<EOF cat > "$workdir/debian/control" <<EOF

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_RISCV_VERSION="10.0.109" DOTNET_RISCV_VERSION="10.0.111"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -595,7 +595,7 @@ package_binary() {
write_desktop_file "$stage" write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir" write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.2.0), coreutils (>= 9.5), bash (>= 5.2.26), libfreetype6 (>= 2.13)" extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
mkdir -p "$workdir/debian" mkdir -p "$workdir/debian"
cat > "$workdir/debian/control" <<EOF cat > "$workdir/debian/control" <<EOF

View File

@@ -609,7 +609,7 @@ package_binary() {
write_desktop_file "$stage" write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir" write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.2.0), coreutils (>= 9.5), bash (>= 5.2.26), libfreetype6 (>= 2.13)" extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
mkdir -p "$workdir/debian" mkdir -p "$workdir/debian"
cat > "$workdir/debian/control" <<EOF cat > "$workdir/debian/control" <<EOF

View File

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

View File

@@ -12,8 +12,8 @@ MIN_KERNEL="6.12"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild" RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_LOONGARCH_VERSION="10.0.109" DOTNET_LOONGARCH_VERSION="10.0.111"
DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64" DOTNET_LOONGARCH_TAG="v10.0.111-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
@@ -513,9 +513,9 @@ Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.39 Requires: glibc >= 2.39
Requires: fontconfig >= 2.15.0 Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26 Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.2.0 Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 9.5 Requires: coreutils >= 9.4
Requires: bash >= 5.2.26 Requires: bash >= 5.2.21
Requires: freetype >= 2.13 Requires: freetype >= 2.13
%description %description

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild" RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_RISCV_VERSION="10.0.109" DOTNET_RISCV_VERSION="10.0.111"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -512,9 +512,9 @@ Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.39 Requires: glibc >= 2.39
Requires: fontconfig >= 2.15.0 Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26 Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.2.0 Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 9.5 Requires: coreutils >= 9.4
Requires: bash >= 5.2.26 Requires: bash >= 5.2.21
Requires: freetype >= 2.13 Requires: freetype >= 2.13
%description %description

View File

@@ -501,9 +501,9 @@ Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.39 Requires: glibc >= 2.39
Requires: fontconfig >= 2.15.0 Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26 Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.2.0 Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 9.5 Requires: coreutils >= 9.4
Requires: bash >= 5.2.26 Requires: bash >= 5.2.21
Requires: freetype >= 2.13 Requires: freetype >= 2.13
%description %description

View File

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

View File

@@ -5,34 +5,32 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" /> <PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.18" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
<PackageVersion Include="CliWrap" Version="3.10.2" /> <PackageVersion Include="CliWrap" Version="3.10.5" />
<PackageVersion Include="Downloader" Version="5.9.0" /> <PackageVersion Include="Downloader" Version="5.9.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" /> <PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.28" /> <PackageVersion Include="ReactiveUI" Version="24.1.0" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" /> <PackageVersion Include="ReactiveUI.WPF" Version="24.1.0" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" /> <PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
<PackageVersion Include="NLog" Version="6.1.3" /> <PackageVersion Include="NLog" Version="6.2.0" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" /> <PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="TUnit" Version="1.65.38" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" /> <PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="18.0.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" /> <PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup> </ItemGroup>

View File

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

View File

@@ -59,7 +59,7 @@ internal static class CoreConfigTestFactory
}, },
WebDavItem = new WebDavItem(), WebDavItem = new WebDavItem(),
CheckUpdateItem = new CheckUpdateItem(), CheckUpdateItem = new CheckUpdateItem(),
Fragment4RayItem = new Fragment4RayItem { Packets = "tlshello", Length = "100-200", Interval = "10-20" }, Fragment4RayItem = new Fragment4RayItem { Packets = "tlshello", Lengths = ["100-200"], Delays = ["10-20"] },
Inbound = Inbound =
[ [
new InItem new InItem
@@ -84,6 +84,7 @@ internal static class CoreConfigTestFactory
ParallelQuery = false, ParallelQuery = false,
Strategy4Freedom = Global.AsIs, Strategy4Freedom = Global.AsIs,
Strategy4Proxy = Global.AsIs, Strategy4Proxy = Global.AsIs,
Strategy4ProxyDial = Global.AsIs,
}, },
IndexId = string.Empty, IndexId = string.Empty,
SubIndexId = string.Empty, SubIndexId = string.Empty,
@@ -130,6 +131,42 @@ internal static class CoreConfigTestFactory
}; };
} }
public static ProfileItem CreateHttpNode(ECoreType coreType, string indexId = "node-http-1",
string remarks = "demo-http")
{
return new ProfileItem
{
IndexId = indexId,
ConfigType = EConfigType.HTTP,
CoreType = coreType,
Remarks = remarks,
Address = "proxy.example.com",
Port = 8080,
Password = "pass",
Username = "user",
Network = nameof(ETransport.raw),
StreamSecurity = string.Empty,
Subid = string.Empty,
};
}
public static ProfileItem CreateCustomOutboundNode(ECoreType coreType, string indexId = "node-custom-1",
string remarks = "demo-custom-outbound", string address = "custom_outbound.json")
{
return new ProfileItem
{
IndexId = indexId,
ConfigType = EConfigType.Outbound,
CoreType = coreType,
Remarks = remarks,
Address = address,
Port = 0,
Network = nameof(ETransport.raw),
StreamSecurity = string.Empty,
Subid = string.Empty,
};
}
public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks, public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
IEnumerable<string> childIndexIds) IEnumerable<string> childIndexIds)
{ {
@@ -214,4 +251,12 @@ internal static class CoreConfigTestFactory
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"]; config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
return config; return config;
} }
public static Config CreateConfigWithTun(ECoreType coreType, bool enableIPv6Address)
{
var config = CreateConfig(coreType);
config.TunModeItem.EnableTun = true;
config.TunModeItem.EnableIPv6Address = enableIPv6Address;
return config;
}
} }

View File

@@ -1,19 +1,9 @@
using AwesomeAssertions;
using ServiceLib.Common;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager;
using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig;
using Xunit;
namespace ServiceLib.Tests.CoreConfig.Singbox; namespace ServiceLib.Tests.CoreConfig.Singbox;
public class CoreConfigSingboxServiceTests public class CoreConfigSingboxServiceTests
{ {
[Fact] [Test]
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig() public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -22,17 +12,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull(); await result.Data.Should().NotBeNull();
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString()); var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
singboxConfig.Should().NotBeNull(); await singboxConfig.Should().NotBeNull();
singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks"); await singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed)); await singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
} }
[Fact] [Test]
public void GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound() public async Task GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -46,18 +36,100 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.inbounds.Should().Contain(i => await cfg.inbounds.Should().Contain(i =>
i.type == nameof(EInboundProtocol.mixed) i.type == nameof(EInboundProtocol.mixed)
&& i.listen == Global.Loopback && i.listen == Global.Loopback
&& i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks)); && i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks));
cfg.inbounds.Should().Contain(i => i.type == "tun"); await cfg.inbounds.Should().Contain(i => i.type == "tun");
} }
[Fact] [Test]
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface() public async Task GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
{
// The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast).
// They are deserialized into List<Rule4Sbox>, so a schema mismatch in the
// embedded template makes JsonUtils.Deserialize return null and silently
// drops every one of them.
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.TunModeItem.EnableTun = true;
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
{
IsTunEnabled = true,
};
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
await cfg.route.rules.Should().Contain(
r => r.action == "reject"
&& r.network != null && r.network.Contains("udp")
&& r.port != null && r.port.Contains(5353),
"the embedded tun rules must reject mDNS/NetBIOS noise");
await cfg.route.rules.Should().Contain(
r => r.action == "reject"
&& r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"),
"the embedded tun rules must reject multicast traffic");
}
[Test]
public async Task GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
{
// Regression test: traffic addressed to the TUN interface's own addresses must
// never reach an outbound. auto_route hijacks the default route, so `direct`
// writes such a packet straight back into the TUN, which routes it to the
// outbound again - an infinite loop that pins a CPU core. Observed in the wild
// with WebRTC ICE connectivity checks against the TUN's own fc00::/7 ULA
// address, sustaining ~8k packets/s out of the TUN interface.
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.TunModeItem.EnableTun = true;
config.TunModeItem.EnableIPv6Address = true;
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
{
IsTunEnabled = true,
};
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var tun = cfg.inbounds.First(i => i.type == "tun");
//tun.address.Should().NotBeNullOrEmpty();
await tun.address.Should().NotBeNull();
await tun.address.Should().NotBeEmpty();
foreach (var address in tun.address!)
{
var self = IPAddress.Parse(address.Split('/').First());
var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
var expected = $"{self}/{hostBits}";
await cfg.route.rules.Should().Contain(
r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(expected),
$"traffic to the TUN's own address '{address}' must be rejected, not routed");
}
// The match has to stay on the addresses themselves. sing-tun derives the TUN's DNS
// entry from the address right after the interface's own, and every prefix offered
// here leaves room for it, so a prefix match would drop system name lookups too.
var dropRule = cfg.route.rules.First(r =>
r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0);
//dropRule.ip_cidr!.Should().OnlyContain(c =>
// c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
await dropRule.ip_cidr.Should().All(c => c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
}
[Test]
public async Task GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreBasicItem.BindInterface = "eth0"; config.CoreBasicItem.BindInterface = "eth0";
@@ -71,16 +143,16 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag); var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.bind_interface.Should().Be("eth0"); await proxy.bind_interface.Should().BeEqualTo("eth0");
proxy.detour.Should().BeNullOrEmpty(); await proxy.detour.Should().BeNull().Or.BeEmpty();
} }
[Fact] [Test]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector() public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -97,17 +169,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
} }
[Fact] [Test]
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain() public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -124,18 +196,18 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks"); await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => await cfg.outbounds.Should().Contain(o =>
o.tag == Global.ProxyTag && o.tag == Global.ProxyTag &&
(o.detour ?? string.Empty).StartsWith("chain-proxy-1-", StringComparison.Ordinal)); (o.detour ?? string.Empty).StartsWith("chain-proxy-1-", StringComparison.Ordinal));
} }
[Fact] [Test]
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds() public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -157,18 +229,18 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
} }
[Fact] [Test]
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches() public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -190,25 +262,25 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)); var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal));
proxyCloneCount.Should().Be(2); await proxyCloneCount.Should().BeEqualTo(2);
var allCloneDetoursPointToGroupBranches = cfg.outbounds var allCloneDetoursPointToGroupBranches = cfg.outbounds
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)) .Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
.All(o => (o.detour ?? string.Empty).StartsWith("chain-proxy-1-group-", StringComparison.Ordinal)); .All(o => (o.detour ?? string.Empty).StartsWith("chain-proxy-1-group-", StringComparison.Ordinal));
allCloneDetoursPointToGroupBranches.Should().BeTrue(); await allCloneDetoursPointToGroupBranches.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules() public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -244,24 +316,24 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasDirectRule = cfg.route.rules.Any(r => var hasDirectRule = cfg.route.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("direct.example.com") && r.domain.Contains("direct.example.com")
&& r.outbound == Global.DirectTag); && r.outbound == Global.DirectTag);
hasDirectRule.Should().BeTrue(); await hasDirectRule.Should().BeTrue();
var hasBlockRule = cfg.route.rules.Any(r => var hasBlockRule = cfg.route.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("block.example.com") && r.domain.Contains("block.example.com")
&& r.action == "reject"); && r.action == "reject");
hasBlockRule.Should().BeTrue(); await hasBlockRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound() public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -293,21 +365,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}"; var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}";
cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
var hasRouteRule = cfg.route.rules.Any(r => var hasRouteRule = cfg.route.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("route.example.com") && r.domain.Contains("route.example.com")
&& (r.outbound ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal)); && (r.outbound ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
hasRouteRule.Should().BeTrue(); await hasRouteRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule() public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
{ {
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs( var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(
ECoreType.sing_box, ECoreType.sing_box,
@@ -338,7 +410,7 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasExpectedRule = cfg.dns.rules?.Any(r => var hasExpectedRule = cfg.dns.rules?.Any(r =>
@@ -347,11 +419,11 @@ public class CoreConfigSingboxServiceTests
&& r.rule_set?.Contains("geosite-cn") == true && r.rule_set?.Contains("geosite-cn") == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false; && r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeTrue(); await hasExpectedRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver() public async Task GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
{ {
var bootstrapDns = "8.8.8.8"; var bootstrapDns = "8.8.8.8";
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns); var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns);
@@ -362,17 +434,17 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
config.SimpleDNSItem.BootstrapDNS.Should().Be(bootstrapDns); await config.SimpleDNSItem.BootstrapDNS.Should().BeEqualTo(bootstrapDns);
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var bootstrapServer = cfg.dns.servers?.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag); var bootstrapServer = cfg.dns?.servers.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag);
bootstrapServer.Should().NotBeNull(); await bootstrapServer.Should().NotBeNull();
(bootstrapServer?.server ?? string.Empty).Should().Contain(bootstrapDns); await bootstrapServer!.server.Should().Contain(bootstrapDns);
} }
[Fact] [Test]
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns() public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.SimpleDNSItem.DirectDNS = "1.1.1.1"; config.SimpleDNSItem.DirectDNS = "1.1.1.1";
@@ -405,14 +477,14 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.dns.final.Should().Be(Global.SingboxDirectDNSTag); await cfg.dns.final.Should().BeEqualTo(Global.SingboxDirectDNSTag);
} }
[Fact] [Test]
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule() public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
{ {
var config = var config =
CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn"); CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn");
@@ -442,21 +514,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasExpectedRule = cfg.dns.rules?.Any(r => var hasExpectedRule = cfg.dns.rules?.Any(r =>
r.server == Global.SingboxDirectDNSTag r.server == Global.SingboxDirectDNSTag
&& r.ip_cidr?.Contains("192.168.0.0/16") == true && r.ip_cidr?.Contains("192.168.0.0/16") == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false; && r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeFalse(); await hasExpectedRule.Should().BeFalse();
} }
[Theory] [Test]
[InlineData("geosite:cn", "geosite-cn")] [Arguments("geosite:cn", "geosite-cn")]
[InlineData("geosite:geolocation-cn", "geosite-geolocation-cn")] [Arguments("geosite:geolocation-cn", "geosite-geolocation-cn")]
[InlineData("geosite:tld-cn", "geosite-tld-cn")] [Arguments("geosite:tld-cn", "geosite-tld-cn")]
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag, public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag,
string expectedRuleSetTag) string expectedRuleSetTag)
{ {
var config = var config =
@@ -484,7 +556,7 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hasExpectedRule = cfg.dns.rules?.Any(r => var hasExpectedRule = cfg.dns.rules?.Any(r =>
@@ -492,11 +564,11 @@ public class CoreConfigSingboxServiceTests
&& r.ip_cidr?.Contains("192.168.0.0/16") == true && r.ip_cidr?.Contains("192.168.0.0/16") == true
&& r.rule_set?.Contains(expectedRuleSetTag) == true && r.rule_set?.Contains(expectedRuleSetTag) == true
&& r.rule_set?.Contains("geoip-cn") == true) ?? false; && r.rule_set?.Contains("geoip-cn") == true) ?? false;
hasExpectedRule.Should().BeTrue(); await hasExpectedRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver() public async Task GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1"; config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
@@ -508,21 +580,21 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var hostsServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxHostsDNSTag); var hostsServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxHostsDNSTag);
hostsServer.Should().NotBeNull(); await hostsServer.Should().NotBeNull();
hostsServer!.predefined.Should().ContainKey("resolver.example"); await hostsServer!.predefined.Should().ContainKey("resolver.example");
hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1"); await hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1");
var directServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxDirectDNSTag); var directServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxDirectDNSTag);
directServer.Should().NotBeNull(); await directServer.Should().NotBeNull();
directServer!.domain_resolver.Should().Be(Global.SingboxHostsDNSTag); await directServer!.domain_resolver.Should().BeEqualTo(Global.SingboxHostsDNSTag);
} }
[Fact] [Test]
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver() public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -551,22 +623,22 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8"); await cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag); await cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global)); await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct)); await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
} }
[Fact] [Test]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl() public async Task GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{ {
var shareLink = var shareLink =
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test"; "hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _); var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull(); await node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box; node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
@@ -579,17 +651,88 @@ public class CoreConfigSingboxServiceTests
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}"); await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag); var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2"); await proxy.type.Should().BeEqualTo("hysteria2");
proxy.realm.Should().NotBeNull(); await proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://"); await proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io"); await proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public"); await proxy.realm.token.Should().BeEqualTo("public");
proxy.realm.realm_id.Should().Be("my-realm-id"); await proxy.realm.realm_id.Should().BeEqualTo("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478"); await proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull(); await proxy.server.Should().BeNull();
}
[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.
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.TunModeItem.EnableTun = true;
config.TunModeItem.Stack = "system";
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");
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(':');
await prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
$"'{address}' must leave room for the peer address the system stack derives");
}
}
[Test]
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
var customNode = CoreConfigTestFactory.CreateCustomOutboundNode(ECoreType.sing_box, "n-custom", "custom-singbox");
var customJsonContent = """
{
"type": "shadowsocks",
"server": "1.2.3.4",
"server_port": 8388,
"method": "aes-128-gcm",
"password": "custom_password"
}
""";
var context = CoreConfigTestFactory.CreateContext(config, customNode, ECoreType.sing_box);
context.CustomOutboundContent[customNode.IndexId] = customJsonContent;
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await result.Data.Should().NotBeNull();
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
await cfg.Should().NotBeNull();
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
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; namespace ServiceLib.Tests.CoreConfig.V2ray;
public class CoreConfigV2rayServiceTests public class CoreConfigV2rayServiceTests
{ {
[Fact] [Test]
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig() public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -19,17 +12,50 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
result.Data.Should().NotBeNull(); await result.Data.Should().NotBeNull();
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString()); var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
v2rayConfig.Should().NotBeNull(); await v2rayConfig.Should().NotBeNull();
v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess"); await v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess");
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed)); await v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
} }
[Fact] [Test]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer() public async Task GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateHttpNode(ECoreType.Xray);
node.SetProtocolExtra(node.GetProtocolExtra() with
{
HttpHeaders = "{\"User-Agent\":\"v2rayN\",\"Set-Cookie\":[\"a=1\",\"b=2\"]}",
});
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 outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http");
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().BeEquivalentTo(["a=1", "b=2"]);
await outbound.settings.servers.Should().BeNull();
await outbound.settings.vnext.Should().BeNull();
}
[Test]
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -46,17 +72,17 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
cfg.routing.balancers.Should().NotBeNull(); await cfg.routing.balancers.Should().NotBeNull();
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
} }
[Fact] [Test]
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain() public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -72,21 +98,21 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
var hasDialerChain = cfg.outbounds.Any(o => var hasDialerChain = cfg.outbounds.Any(o =>
o.tag == Global.ProxyTag o.tag == Global.ProxyTag
&& o.streamSettings is not null && o.streamSettings is not null
&& o.streamSettings.sockopt is not null && o.streamSettings.sockopt is not null
&& (o.streamSettings.sockopt.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-", && (o.streamSettings.sockopt.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-",
StringComparison.Ordinal)); StringComparison.Ordinal));
hasDialerChain.Should().BeTrue(); await hasDialerChain.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds() public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -107,18 +133,18 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
cfg.routing.balancers.Should().NotBeNull(); await cfg.routing.balancers.Should().NotBeNull();
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
} }
[Fact] [Test]
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches() public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -140,27 +166,27 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)); var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal));
proxyCloneCount.Should().Be(2); await proxyCloneCount.Should().BeEqualTo(2);
var allCloneDialersPointToGroupBranches = cfg.outbounds var allCloneDialersPointToGroupBranches = cfg.outbounds
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)) .Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
.All(o => (o.streamSettings?.sockopt?.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-group-", .All(o => (o.streamSettings?.sockopt?.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-group-",
StringComparison.Ordinal)); StringComparison.Ordinal));
allCloneDialersPointToGroupBranches.Should().BeTrue(); await allCloneDialersPointToGroupBranches.Should().BeTrue();
cfg.routing.balancers.Should().NotBeNull(); await cfg.routing.balancers.Should().NotBeNull();
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
} }
[Fact] [Test]
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules() public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -196,24 +222,24 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var hasDirectRule = cfg.routing.rules.Any(r => var hasDirectRule = cfg.routing.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("full:direct.example.com") && r.domain.Contains("full:direct.example.com")
&& r.outboundTag == Global.DirectTag); && r.outboundTag == Global.DirectTag);
hasDirectRule.Should().BeTrue(); await hasDirectRule.Should().BeTrue();
var hasBlockRule = cfg.routing.rules.Any(r => var hasBlockRule = cfg.routing.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("full:block.example.com") && r.domain.Contains("full:block.example.com")
&& r.outboundTag == Global.BlockTag); && r.outboundTag == Global.BlockTag);
hasBlockRule.Should().BeTrue(); await hasBlockRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound() public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -245,20 +271,20 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}"; var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}";
cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal)); await cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
var hasRouteRule = cfg.routing.rules.Any(r => var hasRouteRule = cfg.routing.rules.Any(r =>
r.domain != null r.domain != null
&& r.domain.Contains("full:route.example.com") && r.domain.Contains("full:route.example.com")
&& (r.outboundTag ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal)); && (r.outboundTag ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
hasRouteRule.Should().BeTrue(); await hasRouteRule.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer() public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer()
{ {
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -287,7 +313,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
@@ -302,11 +328,11 @@ public class CoreConfigV2rayServiceTests
&& s.domains?.Contains("geosite:cn") == true && s.domains?.Contains("geosite:cn") == true
&& s.expectedIPs?.Contains("192.168.0.0/16") == true && s.expectedIPs?.Contains("192.168.0.0/16") == true
&& s.expectedIPs?.Contains("geoip:cn") == true); && s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedServer.Should().BeTrue(); await hasExpectedServer.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains() public async Task GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains()
{ {
var bootstrapDns = "8.8.8.8"; var bootstrapDns = "8.8.8.8";
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.Xray, bootstrapDns); var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.Xray, bootstrapDns);
@@ -319,7 +345,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
@@ -333,11 +359,11 @@ public class CoreConfigV2rayServiceTests
s.address == bootstrapDns s.address == bootstrapDns
&& s.domains?.Contains("full:dns-direct.example") == true && s.domains?.Contains("full:dns-direct.example") == true
&& s.domains?.Contains("full:dns-remote.example") == true); && s.domains?.Contains("full:dns-remote.example") == true);
hasBootstrapServer.Should().BeTrue(); await hasBootstrapServer.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers() public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.SimpleDNSItem.DirectDNS = "1.1.1.1"; config.SimpleDNSItem.DirectDNS = "1.1.1.1";
@@ -370,7 +396,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
var dnsServers = dns.servers var dnsServers = dns.servers
@@ -382,14 +408,14 @@ public class CoreConfigV2rayServiceTests
var hasDirectFallback = dnsServers.Any(s => var hasDirectFallback = dnsServers.Any(s =>
(s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal) (s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal)
&& s.address == "1.1.1.1"); && s.address == "1.1.1.1");
hasDirectFallback.Should().BeTrue(); await hasDirectFallback.Should().BeTrue();
var hasRemoteFallback = dnsServers.Any(s => s.address == "9.9.9.9"); var hasRemoteFallback = dnsServers.Any(s => s.address == "9.9.9.9");
hasRemoteFallback.Should().BeFalse(); await hasRemoteFallback.Should().BeFalse();
} }
[Fact] [Test]
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs() public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs()
{ {
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -418,7 +444,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
var dnsServers = dns.servers var dnsServers = dns.servers
@@ -430,14 +456,14 @@ public class CoreConfigV2rayServiceTests
var hasExpectedIPs = dnsServers.Any(s => var hasExpectedIPs = dnsServers.Any(s =>
s.expectedIPs?.Contains("192.168.0.0/16") == true s.expectedIPs?.Contains("192.168.0.0/16") == true
|| s.expectedIPs?.Contains("geoip:cn") == true); || s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedIPs.Should().BeFalse(); await hasExpectedIPs.Should().BeFalse();
} }
[Theory] [Test]
[InlineData("geosite:cn")] [Arguments("geosite:cn")]
[InlineData("geosite:geolocation-cn")] [Arguments("geosite:geolocation-cn")]
[InlineData("geosite:tld-cn")] [Arguments("geosite:tld-cn")]
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag) public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag)
{ {
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -463,7 +489,7 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
var dnsServers = dns.servers var dnsServers = dns.servers
@@ -477,11 +503,11 @@ public class CoreConfigV2rayServiceTests
&& s.domains?.Contains(domainTag) == true && s.domains?.Contains(domainTag) == true
&& s.expectedIPs?.Contains("192.168.0.0/16") == true && s.expectedIPs?.Contains("192.168.0.0/16") == true
&& s.expectedIPs?.Contains("geoip:cn") == true); && s.expectedIPs?.Contains("geoip:cn") == true);
hasExpectedServer.Should().BeTrue(); await hasExpectedServer.Should().BeTrue();
} }
[Fact] [Test]
public void GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts() public async Task GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1"; config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
@@ -492,17 +518,17 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
dns.hosts.Should().NotBeNull(); await dns.hosts.Should().NotBeNull();
dns.hosts!.Should().ContainKey("resolver.example"); await dns.hosts!.Should().ContainKey("resolver.example");
JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1"); await JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1");
} }
[Fact] [Test]
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig() public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig()
{ {
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -523,22 +549,67 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!; var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8"); await JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8");
dns.hosts.Should().NotBeNull(); await dns.hosts.Should().NotBeNull();
dns.hosts!.Should().ContainKey("raw.example"); await dns.hosts!.Should().ContainKey("raw.example");
JsonUtils.Serialize(dns.hosts!["raw.example"]).Should().Contain("1.1.1.1"); await JsonUtils.Serialize(dns.hosts!["raw.example"]).Should().Contain("1.1.1.1");
var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom"); var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom");
directOutbound.Should().NotBeNull(); await directOutbound.Should().NotBeNull();
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4"); await directOutbound!.streamSettings.sockopt!.domainStrategy.Should().BeEqualTo("UseIPv4");
} }
[Fact] [Test]
public void GenerateClientConfigContent_TunRouteExcludeAddress() [Arguments(false)]
[Arguments(true)]
public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address)
{
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
await tunInbound.Should().NotBeNull();
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
await tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0");
// EnableIPv6Address governs the interface address only, never the routing table.
await tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
}
[Test]
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
{
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
config.TunModeItem.EnableIPv6Address = false;
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
await tunInbound.Should().NotBeNull();
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':'));
}
[Test]
public async Task GenerateClientConfigContent_TunRouteExcludeAddress()
{ {
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray); var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config); CoreConfigTestFactory.BindAppManagerConfig(config);
@@ -548,15 +619,54 @@ public class CoreConfigV2rayServiceTests
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue(); await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!; var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull(); await tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0"); await tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32"); await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31"); await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
}
[Test]
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
var customNode = CoreConfigTestFactory.CreateCustomOutboundNode(ECoreType.Xray, "n-custom", "custom-xray");
var customJsonContent = """
{
"protocol": "shadowsocks",
"settings": {
"servers": [
{
"address": "1.2.3.4",
"port": 8388,
"method": "aes-128-gcm",
"password": "custom_password"
}
]
}
}
""";
var context = CoreConfigTestFactory.CreateContext(config, customNode, ECoreType.Xray);
context.CustomOutboundContent[customNode.IndexId] = customJsonContent;
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
await result.Data.Should().NotBeNull();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
await cfg.Should().NotBeNull();
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
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; namespace ServiceLib.Tests.Fmt;
public class FmtHandlerTests public class FmtHandlerTests
{ {
[Fact] [Test]
public void GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields()
{ {
var source = CreateVmessProfile(); var source = CreateVmessProfile();
var resolved = ExportThenImport(source); var resolved = await ExportThenImport(source);
resolved.ConfigType.Should().Be(EConfigType.VMess); await resolved.ConfigType.Should().BeEqualTo(EConfigType.VMess);
resolved.Remarks.Should().Be(source.Remarks); await resolved.Remarks.Should().BeEqualTo(source.Remarks);
resolved.Address.Should().Be(source.Address); await resolved.Address.Should().BeEqualTo(source.Address);
resolved.Port.Should().Be(source.Port); await resolved.Port.Should().BeEqualTo(source.Port);
resolved.Password.Should().Be(source.Password); await resolved.Password.Should().BeEqualTo(source.Password);
resolved.GetProtocolExtra().AlterId.Should().Be(source.GetProtocolExtra().AlterId); await resolved.GetProtocolExtra().AlterId.Should().BeEqualTo(source.GetProtocolExtra().AlterId);
} }
[Fact] [Test]
public void GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields()
{ {
var source = CreateVlessProfile(); var source = CreateVlessProfile();
var resolved = ExportThenImport(source); var resolved = await ExportThenImport(source);
resolved.ConfigType.Should().Be(EConfigType.VLESS); await resolved.ConfigType.Should().BeEqualTo(EConfigType.VLESS);
resolved.Remarks.Should().Be(source.Remarks); await resolved.Remarks.Should().BeEqualTo(source.Remarks);
resolved.Address.Should().Be(source.Address); await resolved.Address.Should().BeEqualTo(source.Address);
resolved.Port.Should().Be(source.Port); await resolved.Port.Should().BeEqualTo(source.Port);
resolved.Password.Should().Be(source.Password); await resolved.Password.Should().BeEqualTo(source.Password);
resolved.GetProtocolExtra().VlessEncryption.Should().Be(Global.None); await resolved.GetProtocolExtra().VlessEncryption.Should().BeEqualTo(Global.None);
} }
[Fact] [Test]
public void GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields()
{ {
var source = CreateShadowsocksProfile(); var source = CreateShadowsocksProfile();
var resolved = ExportThenImport(source); var resolved = await ExportThenImport(source);
resolved.ConfigType.Should().Be(EConfigType.Shadowsocks); await resolved.ConfigType.Should().BeEqualTo(EConfigType.Shadowsocks);
resolved.Remarks.Should().Be(source.Remarks); await resolved.Remarks.Should().BeEqualTo(source.Remarks);
resolved.Address.Should().Be(source.Address); await resolved.Address.Should().BeEqualTo(source.Address);
resolved.Port.Should().Be(source.Port); await resolved.Port.Should().BeEqualTo(source.Port);
resolved.Password.Should().Be(source.Password); await resolved.Password.Should().BeEqualTo(source.Password);
resolved.GetProtocolExtra().SsMethod.Should().Be(source.GetProtocolExtra().SsMethod); await resolved.GetProtocolExtra().SsMethod.Should().BeEqualTo(source.GetProtocolExtra().SsMethod);
} }
[Fact] [Test]
public void GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields() public async Task GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields()
{ {
var source = CreateSocksProfile(); var source = CreateSocksProfile();
var resolved = ExportThenImport(source); var resolved = await ExportThenImport(source);
resolved.ConfigType.Should().Be(EConfigType.SOCKS); await resolved.ConfigType.Should().BeEqualTo(EConfigType.SOCKS);
resolved.Remarks.Should().Be(source.Remarks); await resolved.Remarks.Should().BeEqualTo(source.Remarks);
resolved.Address.Should().Be(source.Address); await resolved.Address.Should().BeEqualTo(source.Address);
resolved.Port.Should().Be(source.Port); await resolved.Port.Should().BeEqualTo(source.Port);
resolved.Username.Should().Be(source.Username); await resolved.Username.Should().BeEqualTo(source.Username);
resolved.Password.Should().Be(source.Password); await resolved.Password.Should().BeEqualTo(source.Password);
} }
[Fact] [Test]
public void ResolveConfig_UnsupportedProtocol_ShouldReturnNull() public async Task ResolveConfig_UnsupportedProtocol_ShouldReturnNull()
{ {
var resolved = FmtHandler.ResolveConfig("not-a-share-uri", out var msg); var resolved = FmtHandler.ResolveConfig("not-a-share-uri", out var msg);
resolved.Should().BeNull(); await resolved.Should().BeNull();
msg.Should().NotBeNullOrWhiteSpace(); await msg.Should().NotBeNull();
await msg.Should().NotBeEmpty();
} }
[Fact] [Test]
public void GetShareUri_UnsupportedConfigType_ShouldReturnNull() public async Task GetShareUri_UnsupportedConfigType_ShouldReturnNull()
{ {
var item = new ProfileItem { ConfigType = EConfigType.PolicyGroup, Remarks = "group", }; var item = new ProfileItem { ConfigType = EConfigType.PolicyGroup, Remarks = "group", };
var uri = FmtHandler.GetShareUri(item); var uri = FmtHandler.GetShareUri(item);
uri.Should().BeNull(); await uri.Should().BeNull();
} }
private static ProfileItem ExportThenImport(ProfileItem source) private static async Task<ProfileItem> ExportThenImport(ProfileItem source)
{ {
var uri = FmtHandler.GetShareUri(source); var uri = FmtHandler.GetShareUri(source);
uri.Should().NotBeNullOrWhiteSpace(); await uri.Should().NotBeNull();
uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should() await uri.Should().NotBeEmpty();
await uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should()
.BeTrue(); .BeTrue();
var resolved = FmtHandler.ResolveConfig(uri, out var msg); var resolved = FmtHandler.ResolveConfig(uri, out var msg);
resolved.Should().NotBeNull($"uri: {uri}, msg: {msg}"); await resolved.Should().NotBeNull().Because($"uri: {uri}, msg: {msg}");
return resolved!; return resolved!;
} }

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,6 @@ global using System.Diagnostics;
global using System.Net; global using System.Net;
global using System.Net.NetworkInformation; global using System.Net.NetworkInformation;
global using System.Net.Sockets; global using System.Net.Sockets;
global using System.Reactive;
global using System.Reactive.Disposables;
global using System.Reactive.Linq;
global using System.Reflection; global using System.Reflection;
global using System.Runtime.InteropServices; global using System.Runtime.InteropServices;
global using System.Security.Cryptography; global using System.Security.Cryptography;
@@ -15,10 +12,7 @@ global using System.Text.Json;
global using System.Text.Json.Nodes; global using System.Text.Json.Nodes;
global using System.Text.Json.Serialization; global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions; global using System.Text.RegularExpressions;
global using DynamicData;
global using DynamicData.Binding;
global using ReactiveUI; global using ReactiveUI;
global using ReactiveUI.Fody.Helpers;
global using ServiceLib.Base; global using ServiceLib.Base;
global using ServiceLib.Common; global using ServiceLib.Common;
global using ServiceLib.Enums; global using ServiceLib.Enums;

View File

@@ -0,0 +1,39 @@
namespace ServiceLib.Tests.Manager;
public class CoreManagerTests
{
[Test]
[Arguments(ECoreType.sing_box)]
[Arguments(ECoreType.mihomo)]
[Arguments(ECoreType.Xray)]
public async Task ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
{
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue();
}
[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.
await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse();
await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
}
[Test]
public async Task ShouldRunAsSudo_OnWindows_ShouldNotElevate()
{
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse();
}
[Test]
[Arguments(ECoreType.v2fly)]
[Arguments(ECoreType.hysteria)]
[Arguments(null)]
public async Task ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
{
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
}
}

View File

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

View File

@@ -0,0 +1,66 @@
namespace ServiceLib.Base;
public class BulkObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnCollectionChanged(e);
}
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnPropertyChanged(e);
}
}
public void AddRange(IEnumerable<T>? collection)
{
if (collection == null)
{
return;
}
_suppressNotification = true;
try
{
foreach (var item in collection)
{
Add(item);
}
}
finally
{
_suppressNotification = false;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public bool Replace(T oldItem, T newItem)
{
var index = Items.IndexOf(oldItem);
if (index < 0)
{
return false;
}
Items[index] = newItem;
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
newItem,
oldItem,
index));
return true;
}
}

View File

@@ -0,0 +1,6 @@
namespace ServiceLib.Base;
public interface ICloseable
{
public event EventHandler? RequestClose;
}

View File

@@ -0,0 +1,7 @@
namespace ServiceLib.Base;
public interface IWindowDialog
{
public Task<bool> ShowDialogAsync<TViewModel>(TViewModel vm)
where TViewModel : class;
}

View File

@@ -3,5 +3,4 @@ namespace ServiceLib.Base;
public class MyReactiveObject : ReactiveObject public class MyReactiveObject : ReactiveObject
{ {
protected static Config? _config; protected static Config? _config;
protected Func<EViewAction, object?, Task<bool>>? _updateView;
} }

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace ServiceLib.Common; namespace ServiceLib.Common;
@@ -92,7 +93,7 @@ public static class Extension
public static bool IsComplexType(this EConfigType configType) public static bool IsComplexType(this EConfigType configType)
{ {
return configType is EConfigType.Custom or EConfigType.PolicyGroup or EConfigType.ProxyChain; return configType is EConfigType.Custom or EConfigType.Outbound or EConfigType.PolicyGroup or EConfigType.ProxyChain;
} }
/// <summary> /// <summary>
@@ -134,4 +135,30 @@ public static class Extension
.Replace("\r", replacement) .Replace("\r", replacement)
.Replace("\n", replacement); .Replace("\n", replacement);
} }
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction,
TInput input,
TOutput defaultValue = default!,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
try
{
return await interaction.Handle(input);
}
catch (UnhandledInteractionException<TInput, TOutput> ex)
{
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
Logging.SaveLog(title, ex);
return defaultValue;
}
catch (Exception ex)
{
var title = $"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}";
Logging.SaveLog(title, ex);
return defaultValue;
}
}
} }

View File

@@ -1,4 +1,3 @@
using System.Collections.Specialized;
using System.Security.Principal; using System.Security.Principal;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
@@ -485,12 +484,17 @@ public class Utils
public static string? DomainStrategy4Sbox(string? strategy) public static string? DomainStrategy4Sbox(string? strategy)
{ {
if (strategy is null)
{
return null;
}
return strategy switch return strategy switch
{ {
not null when strategy.StartsWith("UseIPv4") => "prefer_ipv4", _ when strategy.StartsWith("UseIPv6") => "prefer_ipv6",
not null when strategy.StartsWith("UseIPv6") => "prefer_ipv6", _ when strategy.StartsWith("UseIP") => "prefer_ipv4",
not null when strategy.StartsWith("ForceIPv4") => "ipv4_only", _ when strategy.StartsWith("ForceIPv6") => "ipv6_only",
not null when strategy.StartsWith("ForceIPv6") => "ipv6_only", _ when strategy.StartsWith("ForceIP") => "ipv4_only",
_ => null _ => null
}; };
} }
@@ -748,11 +752,11 @@ public class Utils
return false; return false;
} }
public static int GetFreePort(int defaultPort = 0) public static int GetFreePort(int defaultPort)
{ {
try try
{ {
if (!(defaultPort == 0 || Utils.PortInUse(defaultPort))) if (!PortInUse(defaultPort))
{ {
return defaultPort; return defaultPort;
} }

View File

@@ -14,6 +14,7 @@ public enum EConfigType
HTTP = 10, HTTP = 10,
Anytls = 11, Anytls = 11,
Naive = 12, Naive = 12,
Outbound = 13,
PolicyGroup = 101, PolicyGroup = 101,
ProxyChain = 102, ProxyChain = 102,
} }

View File

@@ -1,36 +0,0 @@
namespace ServiceLib.Enums;
public enum EViewAction
{
CloseWindow,
ShowYesNo,
SaveFileDialog,
AddBatchRoutingRulesYesNo,
SetClipboardData,
AddServerViaClipboard,
ImportRulesFromClipboard,
ProfilesFocus,
ShareSub,
ShareServer,
ScanScreenTask,
ScanImageTask,
BrowseServer,
ImportRulesFromFile,
InitSettingFont,
PasswordInput,
SubEditWindow,
RoutingRuleSettingWindow,
RoutingRuleDetailsWindow,
AddServerWindow,
AddServer2Window,
AddGroupServerWindow,
DNSSettingWindow,
RoutingSettingWindow,
OptionSettingWindow,
FullConfigTemplateWindow,
GlobalHotkeySettingWindow,
SubSettingWindow,
DispatcherRefreshServersBiz,
DispatcherRefreshIcon,
DispatcherShowMsg,
}

View File

@@ -2,30 +2,16 @@ namespace ServiceLib.Events;
public static class AppEvents public static class AppEvents
{ {
public static readonly EventChannel<Unit> ReloadRequested = new(); public static readonly EventChannel<RxVoid> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool?> ShowHideWindowRequested = new();
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
public static readonly EventChannel<bool> HasUpdateNotified = new(); public static readonly EventChannel<bool> HasUpdateNotified = new();
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
public static readonly EventChannel<Unit> ProxiesReloadRequested = new();
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new(); public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
public static readonly EventChannel<string> SendSnackMsgRequested = new(); public static readonly EventChannel<string> SendSnackMsgRequested = new();
public static readonly EventChannel<string> SendMsgViewRequested = new(); public static readonly EventChannel<string> SendMsgViewRequested = new();
public static readonly EventChannel<Unit> AppExitRequested = new(); public static readonly EventChannel<RxVoid> AppExitRequested = new();
public static readonly EventChannel<bool> ShutdownRequested = new(); public static readonly EventChannel<bool> ShutdownRequested = new();
public static readonly EventChannel<Unit> AdjustMainLvColWidthRequested = new();
public static readonly EventChannel<string> SetDefaultServerRequested = new();
public static readonly EventChannel<Unit> RoutingsMenuRefreshRequested = new();
public static readonly EventChannel<Unit> TestServerRequested = new();
public static readonly EventChannel<Unit> InboundDisplayRequested = new();
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new(); public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
} }

View File

@@ -1,27 +1,38 @@
using System.Reactive.Subjects;
namespace ServiceLib.Events; namespace ServiceLib.Events;
public sealed class EventChannel<T> public sealed class EventChannel<T>
{ {
private readonly ISubject<T> _subject = Subject.Synchronize(new Subject<T>()); private readonly Signal<T> _signal = new();
private readonly Lock _gate = new();
private readonly IObservable<T> _observable;
public EventChannel()
{
_observable = _signal.Synchronize(_gate);
}
public IObservable<T> AsObservable() public IObservable<T> AsObservable()
{ {
return _subject.AsObservable(); return _observable;
} }
public void Publish(T value) public void Publish(T value)
{ {
_subject.OnNext(value); lock (_gate)
{
_signal.OnNext(value);
}
} }
public void Publish() public void Publish()
{ {
if (typeof(T) != typeof(Unit)) if (typeof(T) != typeof(RxVoid))
{ {
throw new InvalidOperationException("Publish() without value is only valid for EventChannel<Unit>."); throw new InvalidOperationException("Publish() without value is only valid for EventChannel<RxVoid>.");
}
lock (_gate)
{
_signal.OnNext((T)(object)RxVoid.Default);
} }
_subject.OnNext((T)(object)Unit.Default);
} }
} }

View File

@@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
</Weavers>

View File

@@ -2,8 +2,6 @@ namespace ServiceLib;
public class Global public class Global
{ {
#region const
public const string AppName = "v2rayN"; public const string AppName = "v2rayN";
public const string GithubUrl = "https://github.com"; public const string GithubUrl = "https://github.com";
public const string GithubApiUrl = "https://api.github.com/repos"; public const string GithubApiUrl = "https://api.github.com/repos";
@@ -106,9 +104,7 @@ public class Global
public const string SingboxFakeDNSTag = "fake_dns"; public const string SingboxFakeDNSTag = "fake_dns";
public const int Hysteria2DefaultHopInt = 30; public const int Hysteria2DefaultHopInt = 30;
public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset"; public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset";
public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$"; public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$";
public static readonly List<string> PolicyGroupDefaultFilterList = public static readonly List<string> PolicyGroupDefaultFilterList =
@@ -557,7 +553,6 @@ public class Global
"http", "http",
"tls", "tls",
"quic", "quic",
"fakedns",
]; ];
public static readonly List<int> TunMtus = public static readonly List<int> TunMtus =
@@ -737,6 +732,12 @@ public class Global
"reply", "reply",
]; ];
public static readonly List<string> FakeIPRanges =
[
"198.18.0.0/15",
"11.0.0.0/8",
];
public static readonly List<string> RootCertProviders = public static readonly List<string> RootCertProviders =
[ [
"system", "system",
@@ -744,5 +745,29 @@ public class Global
MozillaRootProvider, MozillaRootProvider,
]; ];
#endregion const public static readonly IReadOnlyList<string> TunIPv4Address =
[
"172.18.0.1/30",
"172.31.0.1/30",
"172.20.0.1/30",
"172.16.0.1/30",
"192.168.100.1/30",
"10.10.14.1/30",
"10.1.0.1/30",
"10.0.0.1/30",
];
// Prefixes must leave room for a peer address (max /126); the sing-box system
// stack derives a gateway from the first prefix and rejects single-address prefixes.
public static readonly IReadOnlyList<string> TunIPv6Address =
[
"fc00::172:18:0:1/126",
"fc00::172:31:0:1/126",
"fc00::172:20:0:1/126",
"fc00::172:16:0:1/126",
"fc00::192:168:100:1/126",
"fc00::10:10:14:1/126",
"fc00::10:1:0:1/126",
"fc00::10:0:0:1/126",
];
} }

View File

@@ -1,11 +1,15 @@
global using System.Collections.Concurrent; global using System.Collections.Concurrent;
global using System.Collections.ObjectModel;
global using System.Collections.Specialized;
global using System.ComponentModel;
global using System.Diagnostics; global using System.Diagnostics;
global using System.Net; global using System.Net;
global using System.Net.NetworkInformation; global using System.Net.NetworkInformation;
global using System.Net.Sockets; global using System.Net.Sockets;
global using System.Reactive; global using ReactiveUI.Primitives;
global using System.Reactive.Disposables; global using ReactiveUI.Primitives.Concurrency;
global using System.Reactive.Linq; global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.Primitives.Signals;
global using System.Reflection; global using System.Reflection;
global using System.Runtime.InteropServices; global using System.Runtime.InteropServices;
global using System.Runtime.Versioning; global using System.Runtime.Versioning;
@@ -16,10 +20,8 @@ global using System.Text.Json;
global using System.Text.Json.Nodes; global using System.Text.Json.Nodes;
global using System.Text.Json.Serialization; global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions; global using System.Text.RegularExpressions;
global using DynamicData;
global using DynamicData.Binding;
global using ReactiveUI; global using ReactiveUI;
global using ReactiveUI.Fody.Helpers; global using ReactiveUI.SourceGenerators;
global using ServiceLib.Base; global using ServiceLib.Base;
global using ServiceLib.Common; global using ServiceLib.Common;
global using ServiceLib.Enums; global using ServiceLib.Enums;
@@ -30,8 +32,8 @@ global using ServiceLib.Handler.Fmt;
global using ServiceLib.Handler.SysProxy; global using ServiceLib.Handler.SysProxy;
global using ServiceLib.Helper; global using ServiceLib.Helper;
global using ServiceLib.Manager; global using ServiceLib.Manager;
global using ServiceLib.Models.CoreConfigs;
global using ServiceLib.Models.Configs; global using ServiceLib.Models.Configs;
global using ServiceLib.Models.CoreConfigs;
global using ServiceLib.Models.Dto; global using ServiceLib.Models.Dto;
global using ServiceLib.Models.Entities; global using ServiceLib.Models.Entities;
global using ServiceLib.Resx; global using ServiceLib.Resx;
@@ -39,3 +41,5 @@ global using ServiceLib.Services;
global using ServiceLib.Services.CoreConfig; global using ServiceLib.Services.CoreConfig;
global using ServiceLib.Services.Statistics; global using ServiceLib.Services.Statistics;
global using SQLite; global using SQLite;

View File

@@ -49,6 +49,7 @@ public class CoreConfigContextBuilder
RoutingItem = await ConfigHandler.GetDefaultRouting(config), RoutingItem = await ConfigHandler.GetDefaultRouting(config),
IsWindows = Utils.IsWindows(), IsWindows = Utils.IsWindows(),
IsMacOS = Utils.IsMacOS(), IsMacOS = Utils.IsMacOS(),
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
}; };
var validatorResult = NodeValidatorResult.Empty(); var validatorResult = NodeValidatorResult.Empty();
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node); var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);
@@ -94,6 +95,7 @@ public class CoreConfigContextBuilder
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode; context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
} }
} }
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 }) if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{ {
var appConfig = JsonUtils.DeepCopy(config); var appConfig = JsonUtils.DeepCopy(config);
@@ -193,12 +195,16 @@ public class CoreConfigContextBuilder
if (preSocksItem != null) if (preSocksItem != null)
{ {
var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem); var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem);
var protectCoreTypeList = new HashSet<ECoreType>(nodeContext.ProtectCoreTypeList) { nodeContext.RunCoreType };
return preSocksResult with return preSocksResult with
{ {
Context = preSocksResult.Context with Context = preSocksResult.Context with
{ {
ProtectDomainList = ProtectDomainList =
[.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []], [.. nodeContext.ProtectDomainList, .. preSocksResult.Context.ProtectDomainList],
ProtectCoreTypeList = protectCoreTypeList,
}, },
}; };
} }
@@ -317,14 +323,14 @@ public class CoreConfigContextBuilder
{ {
return await RegisterGroupNodeAsync(context, node); return await RegisterGroupNodeAsync(context, node);
} }
return RegisterSingleNodeAsync(context, node); return await RegisterSingleNodeAsync(context, node);
} }
/// <summary> /// <summary>
/// Validates a single (non-group) node and, on success, adds it to the proxy map /// Validates a single (non-group) node and, on success, adds it to the proxy map
/// and records any domain addresses that should bypass the proxy. /// and records any domain addresses that should bypass the proxy.
/// </summary> /// </summary>
private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node) private static async Task<NodeValidatorResult> RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
{ {
if (node.ConfigType.IsGroupType()) if (node.ConfigType.IsGroupType())
{ {
@@ -332,6 +338,29 @@ public class CoreConfigContextBuilder
} }
var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType); var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType);
if (node.ConfigType == EConfigType.Outbound)
{
var addressFileName = node.Address;
if (!File.Exists(addressFileName))
{
addressFileName = Utils.GetConfigPath(addressFileName);
}
if (!File.Exists(addressFileName))
{
nodeValidatorResult.Errors.Add(string.Format(ResUI.MsgCustomOutboundFileNotFound, node.Remarks, addressFileName));
}
try
{
var fileContent = await File.ReadAllTextAsync(addressFileName);
context.CustomOutboundContent[node.IndexId] = fileContent;
}
catch
{
nodeValidatorResult.Errors.Add(string.Format(ResUI.MsgCustomOutboundFileNotFound, node.Remarks, addressFileName));
}
}
var msgs = new List<string>([.. nodeValidatorResult.Errors, .. nodeValidatorResult.Warnings]); var msgs = new List<string>([.. nodeValidatorResult.Errors, .. nodeValidatorResult.Warnings]);
if (msgs.Count > 0) if (msgs.Count > 0)
{ {
@@ -355,7 +384,7 @@ public class CoreConfigContextBuilder
{ {
var echQuerySni = node.Sni; var echQuerySni = node.Sni;
if (node.StreamSecurity == Global.StreamSecurity if (node.StreamSecurity == Global.StreamSecurity
&& node.EchConfigList?.Contains("://") == true) && node.EchConfigList.Contains("://"))
{ {
var idx = node.EchConfigList.IndexOf('+'); var idx = node.EchConfigList.IndexOf('+');
echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni; echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni;
@@ -433,7 +462,7 @@ public class CoreConfigContextBuilder
if (!childNode.ConfigType.IsGroupType()) if (!childNode.ConfigType.IsGroupType())
{ {
var childNodeResult = RegisterSingleNodeAsync(context, childNode); var childNodeResult = await RegisterSingleNodeAsync(context, childNode);
childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w => childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w =>
string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w))); string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w)));
childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e => childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e =>

View File

@@ -36,6 +36,15 @@ public class NodeValidator
return; return;
} }
if (item.ConfigType is EConfigType.Outbound)
{
if (item.CoreType != coreType)
{
v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, coreType.ToString(), item.ConfigType));
}
return;
}
if (item.ConfigType.IsGroupType()) if (item.ConfigType.IsGroupType())
{ {
// Group logic is handled in ValidateGroupNode // Group logic is handled in ValidateGroupNode
@@ -106,10 +115,10 @@ public class NodeValidator
if (item.GetNetwork() is nameof(ETransport.ws) if (item.GetNetwork() is nameof(ETransport.ws)
&& item.EchConfigList.IsNullOrEmpty() && item.EchConfigList.IsNullOrEmpty()
&& item.GetAlpn()?.FirstOrDefault() == "h3") && item.GetAlpn()?.FirstOrDefault() is "h3" or "h2")
{ {
v.Warning( v.Warning(
"WebSocket but ALPN is set to h3, the core may ignore the ALPN setting or cause unexpected issues."); $"WebSocket but ALPN is set to {item.Alpn}, the core may ignore the ALPN setting or cause unexpected issues.");
} }
// TLS & Security // TLS & Security
@@ -122,14 +131,6 @@ public class NodeValidator
isCertProvided = false; isCertProvided = false;
} }
// Check for deprecated allowInsecure property when TLS is enabled
if (item.GetAllowInsecure()
&& item.Cert.IsNullOrEmpty()
&& item.CertSha.IsNullOrEmpty())
{
v.Warning(ResUI.MsgAllowInsecureDeprecated);
}
if ((coreType == ECoreType.Xray if ((coreType == ECoreType.Xray
&& item.GetAllowInsecure() && item.GetAllowInsecure()
&& !isCertProvided && !isCertProvided
@@ -138,6 +139,10 @@ public class NodeValidator
&& item.GetAllowInsecure() && item.GetAllowInsecure()
&& !isCertProvided)) && !isCertProvided))
{ {
if (coreType == ECoreType.Xray)
{
v.Warning(ResUI.MsgAllowInsecureDeprecated);
}
v.Warning(ResUI.MsgInsecureConfiguration); v.Warning(ResUI.MsgInsecureConfiguration);
} }
} }

View File

@@ -92,7 +92,7 @@ public static class ConfigHandler
EnableTun = false, EnableTun = false,
Mtu = 9000, Mtu = 9000,
IcmpRouting = Global.TunIcmpRoutingPolicies.First(), IcmpRouting = Global.TunIcmpRoutingPolicies.First(),
EnableLegacyProtect = false, EnableLegacyProtect = true,
}; };
config.GuiItem ??= new(); config.GuiItem ??= new();
if (!Global.RootCertProviders.Contains(config.GuiItem.RootCertProvider)) if (!Global.RootCertProviders.Contains(config.GuiItem.RootCertProvider))
@@ -115,10 +115,13 @@ public static class ConfigHandler
config.ConstItem ??= new ConstItem(); config.ConstItem ??= new ConstItem();
config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
config.SimpleDNSItem.BlockAAAAQuery ??= false;
config.SimpleDNSItem.FakeIPRange ??= Global.FakeIPRanges.FirstOrDefault();
config.SimpleDNSItem.GlobalFakeIp ??= true; config.SimpleDNSItem.GlobalFakeIp ??= true;
config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault(); config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault();
config.SimpleDNSItem.ServeStale ??= false; config.SimpleDNSItem.ServeStale ??= false;
config.SimpleDNSItem.ParallelQuery ??= false; config.SimpleDNSItem.ParallelQuery ??= false;
config.SimpleDNSItem.EnableHappyEyeballs ??= false;
config.SpeedTestItem ??= new(); config.SpeedTestItem ??= new();
if (config.SpeedTestItem.SpeedTestTimeout < 10) if (config.SpeedTestItem.SpeedTestTimeout < 10)
@@ -168,10 +171,24 @@ public static class ConfigHandler
config.Fragment4RayItem ??= new() config.Fragment4RayItem ??= new()
{ {
Packets = "tlshello", Packets = "tlshello",
Length = "50-100",
Interval = "10-20",
MaxSplit = "0"
}; };
config.Fragment4RayItem.MaxSplit ??= "0";
config.HappyEyeballs4RayItem ??= new()
{
TryDelayMs = 250,
PrioritizeIPv6 = false,
Interleave = 1,
MaxConcurrentTry = 4,
};
if ((config.Fragment4RayItem.Lengths ?? []).Count == 0)
{
config.Fragment4RayItem.Lengths = [config.Fragment4RayItem.Length ?? "50-100"];
}
if ((config.Fragment4RayItem.Delays ?? []).Count == 0)
{
config.Fragment4RayItem.Delays = [config.Fragment4RayItem.Interval ?? "10-20"];
}
config.GlobalHotkeys ??= []; config.GlobalHotkeys ??= [];
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty()) if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
@@ -366,6 +383,13 @@ public static class ConfigHandler
{ {
} }
} }
else if (profileItem.ConfigType == EConfigType.Outbound)
{
profileItem.Address = Utils.GetConfigPath(profileItem.Address);
if (await AddCustomOutboundServer(config, profileItem, false) == 0)
{
}
}
else else
{ {
await AddServerCommon(config, profileItem, true); await AddServerCommon(config, profileItem, true);
@@ -563,6 +587,43 @@ public static class ConfigHandler
return 0; return 0;
} }
public static async Task<int> AddCustomOutboundServer(Config config, ProfileItem profileItem, bool blDelete, bool toFile = true)
{
var fileName = profileItem.Address;
if (!File.Exists(fileName))
{
return -1;
}
var ext = Path.GetExtension(fileName);
var newFileName = $"{Utils.GetGuid()}{ext}";
//newFileName = Path.Combine(Utile.GetTempPath(), newFileName);
try
{
File.Copy(fileName, Utils.GetConfigPath(newFileName));
if (blDelete)
{
File.Delete(fileName);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return -1;
}
profileItem.Address = newFileName;
profileItem.ConfigType = EConfigType.Outbound;
if (profileItem.Remarks.IsNullOrEmpty())
{
profileItem.Remarks = $"import custom outbound@{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}";
}
await AddServerCommon(config, profileItem, toFile);
return 0;
}
/// <summary> /// <summary>
/// Edit an existing custom server configuration /// Edit an existing custom server configuration
/// Updates the server's properties without changing the file /// Updates the server's properties without changing the file
@@ -584,6 +645,8 @@ public static class ConfigHandler
item.CoreType = profileItem.CoreType; item.CoreType = profileItem.CoreType;
item.DisplayLog = profileItem.DisplayLog; item.DisplayLog = profileItem.DisplayLog;
item.PreSocksPort = profileItem.PreSocksPort; item.PreSocksPort = profileItem.PreSocksPort;
item.ProtoExtra = profileItem.ProtoExtra;
} }
if (await SQLiteHelper.Instance.UpdateAsync(item) > 0) if (await SQLiteHelper.Instance.UpdateAsync(item) > 0)
@@ -1463,7 +1526,7 @@ public static class ConfigHandler
var matchedChildProfiles = childProfiles?.Where(p => var matchedChildProfiles = childProfiles?.Where(p =>
p != null && p != null &&
p.IsValid() && p.IsValid() &&
!p.ConfigType.IsComplexType() && (!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter)) (extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
) )
.ToList() ?? []; .ToList() ?? [];
@@ -1511,7 +1574,8 @@ public static class ConfigHandler
else if (node.ConfigType == EConfigType.Custom else if (node.ConfigType == EConfigType.Custom
&& node.PreSocksPort is > 0 and <= 65535) && node.PreSocksPort is > 0 and <= 65535)
{ {
var preCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; var customPreCoreType = AppManager.Instance.GetCoreType(null, EConfigType.Custom);
var preCoreType = (enableLegacyProtect && config.TunModeItem.EnableTun) ? ECoreType.sing_box : customPreCoreType;
itemSocks = new ProfileItem() itemSocks = new ProfileItem()
{ {
CoreType = preCoreType, CoreType = preCoreType,
@@ -1658,69 +1722,196 @@ public static class ConfigHandler
} }
var subItem = await AppManager.Instance.GetSubItem(subid); var subItem = await AppManager.Instance.GetSubItem(subid);
if (subItem?.CustomCoreType is null)
{
return await AddBatchServersDefaultCustom(config, strData, subid, isSub, subItem);
}
return await AddBatchServersSpecificCustom(config, strData, subid, isSub, subItem);
}
private static async Task<int> AddBatchServersDefaultCustom(
Config config,
string strData,
string subid,
bool isSub,
SubItem? subItem)
{
var subRemarks = subItem?.Remarks; var subRemarks = subItem?.Remarks;
var preSocksPort = subItem?.PreSocksPort; // Prioritize using complete custom parsing, followed by custom outbound parsing.
var lstProfiles = V2rayFmt.ResolveToCustom(strData, subRemarks);
List<ProfileItem>? lstProfiles = null; if (lstProfiles.Count == 0)
//Is sing-box array configuration
if (lstProfiles is null || lstProfiles.Count <= 0)
{ {
lstProfiles = SingboxFmt.ResolveFullArray(strData, subRemarks); lstProfiles = SingboxFmt.ResolveToCustom(strData, subRemarks);
} }
//Is v2ray array configuration if (lstProfiles.Count == 0)
if (lstProfiles is null || lstProfiles.Count <= 0)
{ {
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks); lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks);
} }
if (lstProfiles is { Count: > 0 }) if (lstProfiles.Count == 0)
{ {
var count = 0; lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
foreach (var it in lstProfiles)
{
it.Subid = subid;
it.IsSub = isSub;
it.PreSocksPort = preSocksPort;
if (await AddCustomServer(config, it, true) == 0)
{
count++;
}
}
if (count > 0)
{
return count;
}
} }
if (lstProfiles.Count == 0)
ProfileItem? profileItem = null;
//Is sing-box configuration
profileItem ??= SingboxFmt.ResolveFull(strData, subRemarks);
//Is v2ray configuration
profileItem ??= V2rayFmt.ResolveFull(strData, subRemarks);
//Is Html Page
if (profileItem is null && HtmlPageFmt.IsHtmlPage(strData))
{ {
return -1; return -1;
} }
//Is Clash configuration
profileItem ??= ClashFmt.ResolveFull(strData, subRemarks); var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
//Is hysteria configuration if (count > 0)
profileItem ??= Hysteria2Fmt.ResolveFull2(strData, subRemarks); {
if (profileItem is null || profileItem.Address.IsNullOrEmpty()) return count;
}
if (HtmlPageFmt.IsHtmlPage(strData))
{
return -1;
}
var profileItem = ClashFmt.ResolveFull(strData, subRemarks)
?? Hysteria2Fmt.ResolveFull2(strData, subRemarks);
if (profileItem == null)
{ {
return -1; return -1;
} }
profileItem.Subid = subid; profileItem.Subid = subid;
profileItem.IsSub = isSub; profileItem.IsSub = isSub;
profileItem.PreSocksPort = preSocksPort; profileItem.PreSocksPort = subItem?.PreSocksPort;
if (await AddCustomServer(config, profileItem, true) == 0)
return await AddCustomServer(config, profileItem, true) == 0 ? 1 : -1;
}
private static async Task<int> AddBatchServersSpecificCustom(
Config config,
string strData,
string subid,
bool isSub,
SubItem subItem)
{
var subRemarks = subItem.Remarks;
var customCoreType = subItem.CustomCoreType!.Value;
var lstProfiles = customCoreType switch
{ {
return 1; ECoreType.Xray => V2rayFmt.ResolveToCustom(strData, subRemarks),
} ECoreType.sing_box => SingboxFmt.ResolveToCustom(strData, subRemarks),
else _ => null,
};
if ((lstProfiles?.Count ?? 0) == 0)
{ {
return -1; return -1;
} }
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> AddBatchCustomServers(
Config config,
List<ProfileItem> lstProfiles,
string subid,
bool isSub)
{
var count = 0;
foreach (var it in lstProfiles)
{
it.Subid = subid;
it.IsSub = isSub;
if (it.ConfigType == EConfigType.Custom)
{
if (await AddCustomServer(config, it, true) == 0)
{
count++;
}
}
else
{
if (await AddCustomOutboundServer(config, it, true) == 0)
{
count++;
}
}
}
return count;
}
private static async Task<int> SaveCustomRawFileServer(
Config config,
string strData,
string subid,
bool isSub,
SubItem subItem,
ECoreType customCoreType)
{
var ext = DetectFileExtension(strData);
var fileName = Utils.GetTempPath($"{Utils.GetGuid(false)}{ext}");
await File.WriteAllTextAsync(fileName, strData);
var profileItem = new ProfileItem
{
CoreType = customCoreType,
ConfigType = EConfigType.Custom,
Address = fileName,
Remarks = subItem.Remarks ?? customCoreType.ToString(),
Subid = subid,
IsSub = isSub,
PreSocksPort = subItem.PreSocksPort,
};
return await AddCustomServer(config, profileItem, true) == 0 ? 1 : -1;
static string DetectFileExtension(string data)
{
var trimmed = data.AsSpan().TrimStart();
if (trimmed.IsEmpty)
{
return string.Empty;
}
if (trimmed[0] is '{' or '[')
{
return ".json";
}
if (trimmed.StartsWith("---"))
{
return ".yaml";
}
foreach (var line in trimmed.EnumerateLines())
{
var lineTrimmed = line.TrimStart();
if (lineTrimmed.IsEmpty || lineTrimmed.StartsWith("#"))
{
continue;
}
var colonIndex = lineTrimmed.IndexOf(':');
if (colonIndex > 0)
{
var keySpan = lineTrimmed[..colonIndex];
if (!keySpan.Contains(' ') && !keySpan.Contains('\t'))
{
if (colonIndex == lineTrimmed.Length - 1 || lineTrimmed[colonIndex + 1] is ' ' or '\t' or '\r' or '\n')
{
return ".yaml";
}
}
}
}
return string.Empty;
}
} }
/// <summary> /// <summary>
@@ -1820,6 +2011,7 @@ public static class ConfigHandler
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false), EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
EConfigType.Naive => await AddNaiveServer(config, profileItem, false), EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false), EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
EConfigType.Outbound => await AddCustomOutboundServer(config, profileItem, true, false),
_ => -1, _ => -1,
}; };
if (addStatus == 0) if (addStatus == 0)
@@ -2021,6 +2213,7 @@ public static class ConfigHandler
item.NextProfile = subItem.NextProfile; item.NextProfile = subItem.NextProfile;
item.PreSocksPort = subItem.PreSocksPort; item.PreSocksPort = subItem.PreSocksPort;
item.Memo = subItem.Memo; item.Memo = subItem.Memo;
item.CustomCoreType = subItem.CustomCoreType;
} }
if (item.Id.IsNullOrEmpty()) if (item.Id.IsNullOrEmpty())
@@ -2061,7 +2254,7 @@ public static class ConfigHandler
{ {
return -1; return -1;
} }
var customProfile = await SQLiteHelper.Instance.TableAsync<ProfileItem>().Where(t => t.Subid == subid && t.ConfigType == EConfigType.Custom).ToListAsync(); var customProfile = await SQLiteHelper.Instance.TableAsync<ProfileItem>().Where(t => t.Subid == subid && (t.ConfigType == EConfigType.Custom || t.ConfigType == EConfigType.Outbound)).ToListAsync();
if (isSub) if (isSub)
{ {
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where isSub = 1 and subid = '{subid}'"); await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where isSub = 1 and subid = '{subid}'");

View File

@@ -17,7 +17,7 @@ public static class CoreConfigHandler
{ {
result = node.CoreType switch result = node.CoreType switch
{ {
ECoreType.mihomo => await new CoreConfigClashService(config).GenerateClientCustomConfig(node, fileName), ECoreType.mihomo => await new CoreConfigClashService(config, context.IsTunEnabled).GenerateClientCustomConfig(node, fileName),
_ => await GenerateClientCustomConfig(node, fileName) _ => await GenerateClientCustomConfig(node, fileName)
}; };
} }

View File

@@ -25,6 +25,11 @@ public class AnytlsFmt : BaseFmt
var query = Utils.ParseQueryString(parsedUrl.Query); var query = Utils.ParseQueryString(parsedUrl.Query);
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
if (GetQueryValue(query, "insecure") == "1")
{
item.AllowInsecure = Global.StringTrue;
}
return item; return item;
} }
@@ -41,6 +46,10 @@ public class AnytlsFmt : BaseFmt
} }
var pw = item.Password; var pw = item.Password;
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
if (item.GetAllowInsecure())
{
dicQuery.Add("insecure", "1");
}
ToUriQuery(item, Global.None, ref dicQuery); ToUriQuery(item, Global.None, ref dicQuery);
return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark); return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark);

View File

@@ -1,11 +1,7 @@
using System.Collections.Specialized;
namespace ServiceLib.Handler.Fmt; namespace ServiceLib.Handler.Fmt;
public class BaseFmt public class BaseFmt
{ {
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure" };
private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty); private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty);
protected static string GetIpv6(string address) protected static string GetIpv6(string address)
@@ -67,7 +63,6 @@ public class BaseFmt
{ {
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
} }
ToUriQueryAllowInsecure(item, ref dicQuery);
} }
if (item.EchConfigList.IsNotEmpty()) if (item.EchConfigList.IsNotEmpty())
{ {
@@ -196,25 +191,6 @@ public class BaseFmt
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
} }
ToUriQueryAllowInsecure(item, ref dicQuery);
return 0;
}
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (item.GetAllowInsecure())
{
// Add two for compatibility
dicQuery.Add("insecure", "1");
dicQuery.Add("allowInsecure", "1");
}
else
{
dicQuery.Add("insecure", "0");
dicQuery.Add("allowInsecure", "0");
}
return 0; return 0;
} }
@@ -252,19 +228,6 @@ public class BaseFmt
item.Finalmask = string.Empty; item.Finalmask = string.Empty;
} }
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{
item.AllowInsecure = Global.StringTrue;
}
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
{
item.AllowInsecure = Global.StringFalse;
}
else
{
item.AllowInsecure = string.Empty;
}
var net = GetQueryValue(query, "type", nameof(ETransport.raw)); var net = GetQueryValue(query, "type", nameof(ETransport.raw));
if (net == Global.RawNetworkAlias) if (net == Global.RawNetworkAlias)
{ {

View File

@@ -1,5 +1,3 @@
using System.Collections.Specialized;
namespace ServiceLib.Handler.Fmt; namespace ServiceLib.Handler.Fmt;
public class Hysteria2Fmt : BaseFmt public class Hysteria2Fmt : BaseFmt
@@ -162,10 +160,27 @@ public class Hysteria2Fmt : BaseFmt
private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item) private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item)
{ {
if (GetQueryValue(query, "insecure") == "1")
{
item.AllowInsecure = Global.StringTrue;
}
if (item.CertSha.IsNullOrEmpty()) if (item.CertSha.IsNullOrEmpty())
{ {
item.CertSha = GetQueryDecoded(query, "pinSHA256"); var pinSHA256 = GetQueryDecoded(query, "pinSHA256");
item.CertSha = pinSHA256;
if (!pinSHA256.IsNullOrEmpty())
{
// NOTE:
// To accommodate Xray changes,
// some providers issue self-signed cert links with `insecure = false` and a certificate fingerprint,
// breaking interoperability between Xray, official Hysteria 2 client, and sing-box.
// Since this won't compromise the overall security model,
// `insecure = true` is automatically set when a fingerprint is detected,
// and the value is restored when generating configurations.
item.AllowInsecure = Global.StringTrue;
}
} }
item.EchConfigList = GetQueryDecoded(query, "ech");
item.SetProtocolExtra(item.GetProtocolExtra() with item.SetProtocolExtra(item.GetProtocolExtra() with
{ {
Ports = GetQueryDecoded(query, "mport"), Ports = GetQueryDecoded(query, "mport"),
@@ -198,12 +213,20 @@ public class Hysteria2Fmt : BaseFmt
private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery) private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery)
{ {
if (item.GetAllowInsecure())
{
dicQuery.Add("insecure", "1");
}
if (!item.CertSha.IsNullOrEmpty() if (!item.CertSha.IsNullOrEmpty()
&& !item.CertSha.Contains(',')) && !item.CertSha.Contains(','))
{ {
var sha = item.CertSha; var sha = item.CertSha;
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha)); dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
} }
if (!item.EchConfigList.IsNullOrEmpty())
{
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
}
var protocolExtraItem = item.GetProtocolExtra(); var protocolExtraItem = item.GetProtocolExtra();
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty(); var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty()) if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())

View File

@@ -1,6 +1,6 @@
namespace ServiceLib.Handler.Fmt; namespace ServiceLib.Handler.Fmt;
public class InnerFmt public class InnerFmt : BaseFmt
{ {
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false)); private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
@@ -50,19 +50,19 @@ public class InnerFmt
var protocolExtra = item.GetProtocolExtra(); var protocolExtra = item.GetProtocolExtra();
// Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks, // Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks,
// which means it will be replaced with the subid, otherwise set it to null // which means it will be replaced with the subid, otherwise set it to null
//if (!protocolExtra.SubChildItems.IsNullOrEmpty()) // if (!protocolExtra.SubChildItems.IsNullOrEmpty())
if (protocolExtra.SubChildItems == "self") if (protocolExtra.SubChildItems == "self")
{ {
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
SubChildItems = subid SubChildItems = subid,
}; };
} }
else else
{ {
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
SubChildItems = null SubChildItems = null,
}; };
} }
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds) if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
@@ -73,14 +73,14 @@ public class InnerFmt
.ToList(); .ToList();
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
ChildItems = Utils.List2String(newChildIndexIds) ChildItems = Utils.List2String(newChildIndexIds),
}; };
} }
else else
{ {
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
ChildItems = null ChildItems = null,
}; };
} }
item.SetProtocolExtra(protocolExtra); item.SetProtocolExtra(protocolExtra);
@@ -120,7 +120,7 @@ public class InnerFmt
{ {
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
SubChildItems = "self" SubChildItems = "self",
}; };
} }
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds) if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
@@ -131,7 +131,7 @@ public class InnerFmt
.ToList(); .ToList();
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
ChildItems = Utils.List2String(newChildIndexIds) ChildItems = Utils.List2String(newChildIndexIds),
}; };
} }
itemClone.SetProtocolExtra(protocolExtra); itemClone.SetProtocolExtra(protocolExtra);
@@ -175,6 +175,19 @@ public class InnerFmt
jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false); jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
jsonObj.Remove("TransportExtraObj"); jsonObj.Remove("TransportExtraObj");
} }
var customOutboundFilePath = string.Empty;
if (jsonObj.TryGetPropertyValue("CustomOutboundObj", out var customOutboundNode)
&& customOutboundNode is JsonObject customOutboundObj)
{
var customOutboundContent = JsonUtils.Serialize(customOutboundObj, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
customOutboundFilePath = WriteAllText(customOutboundContent);
jsonObj.Remove("CustomOutboundObj");
}
var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false)); var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false));
if (profileItem is null) if (profileItem is null)
{ {
@@ -193,6 +206,14 @@ public class InnerFmt
{ {
return null; return null;
} }
if (profileItem.ConfigType is EConfigType.Outbound)
{
if (customOutboundFilePath.IsNullOrEmpty())
{
return null;
}
profileItem.Address = customOutboundFilePath;
}
var protocolExtra = profileItem.GetProtocolExtra(); var protocolExtra = profileItem.GetProtocolExtra();
var multipleLoad = protocolExtra.MultipleLoad; var multipleLoad = protocolExtra.MultipleLoad;
if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad)) if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad))
@@ -209,6 +230,26 @@ public class InnerFmt
{ {
return null; return null;
} }
if (item.ConfigType is EConfigType.Outbound)
{
var customOutboundFilePath = item.Address;
if (!File.Exists(customOutboundFilePath))
{
customOutboundFilePath = Utils.GetConfigPath(customOutboundFilePath);
}
if (!File.Exists(customOutboundFilePath))
{
return null;
}
if (!customOutboundFilePath.IsNullOrEmpty()
&& File.Exists(customOutboundFilePath)
&& File.ReadAllText(customOutboundFilePath) is { Length: > 0 } customOutboundContent
&& JsonUtils.ParseJson(customOutboundContent) is JsonObject customOutboundObj)
{
jsonObj["CustomOutboundObj"] = customOutboundObj;
jsonObj.Remove("Address");
}
}
// unflatten // unflatten
// move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj // move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
// move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj // move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj
@@ -296,7 +337,7 @@ public class InnerFmt
JsonValue value when value.TryGetValue<string>(out var str) => string.IsNullOrEmpty(str), JsonValue value when value.TryGetValue<string>(out var str) => string.IsNullOrEmpty(str),
JsonObject obj => obj.Count == 0, JsonObject obj => obj.Count == 0,
JsonArray arr => arr.Count == 0, JsonArray arr => arr.Count == 0,
_ => false _ => false,
}; };
} }
} }

View File

@@ -2,19 +2,103 @@ namespace ServiceLib.Handler.Fmt;
public class SingboxFmt : BaseFmt public class SingboxFmt : BaseFmt
{ {
public static List<ProfileItem>? ResolveFullArray(string strData, string? subRemarks) public static List<ProfileItem> ResolveToCustom(string strData, string? subRemarks)
{ {
var configObjects = JsonUtils.Deserialize<object[]>(strData); var jsonNode = JsonUtils.ParseJson(strData);
if (configObjects is not { Length: > 0 }) return ResolveCommon(jsonNode, subRemarks, false);
}
public static List<ProfileItem> ResolveToCustomOutbound(string strData, string? subRemarks)
{
var jsonNode = JsonUtils.ParseJson(strData);
return ResolveCommon(jsonNode, subRemarks, true);
}
private static List<ProfileItem> ResolveCommon(JsonNode? jsonNode, string? subRemarks, bool isOutbound)
{
if (jsonNode is JsonArray jsonArray)
{
return
[
.. jsonArray.Select(item => ResolveCommon(item, subRemarks, isOutbound))
.Where(list => list is { Count: > 0 })
.SelectMany(list => list),
];
}
if (jsonNode is not JsonObject jsonObject)
{
return [];
}
// Process the individual JSON object
var profileList = new List<ProfileItem>();
if (!isOutbound)
{
var fullProfile = ResolveFull(jsonObject, subRemarks);
if (fullProfile is not null)
{
profileList.Add(fullProfile);
return profileList;
}
}
profileList.AddRange(ResolveFullToOutbound(jsonObject, subRemarks));
if (profileList.Count != 0)
{
return profileList;
}
var outboundProfile = ResolveOutbound(jsonObject, subRemarks);
if (outboundProfile is not null)
{
profileList.Add(outboundProfile);
}
return profileList;
}
private static ProfileItem? ResolveFull(JsonObject jsonObject, string? subRemarks)
{
if (jsonObject?["inbounds"] == null
|| jsonObject["outbounds"] == null)
{ {
return null; return null;
} }
List<ProfileItem> lstResult = []; if (jsonObject["outbounds"] is JsonArray outboundsArray)
foreach (var configObject in configObjects)
{ {
var objectString = JsonUtils.Serialize(configObject); if (!outboundsArray.Any(IsValidSingboxOutbound))
var profileIt = ResolveFull(objectString, subRemarks); {
return null;
}
}
else
{
return null;
}
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem
{
ConfigType = EConfigType.Custom,
CoreType = ECoreType.sing_box,
Address = fileName,
Remarks = subRemarks ?? "singbox_custom",
};
return profileItem;
}
private static List<ProfileItem> ResolveFullToOutbound(JsonObject jsonObject, string? subRemarks)
{
if (jsonObject?["outbounds"] is not JsonArray outboundsArray)
{
return [];
}
List<ProfileItem> lstResult = [];
foreach (var outbound in outboundsArray)
{
if (outbound is not JsonObject outboundObj)
{
continue;
}
var profileIt = ResolveOutbound(outboundObj, subRemarks);
if (profileIt != null) if (profileIt != null)
{ {
lstResult.Add(profileIt); lstResult.Add(profileIt);
@@ -23,25 +107,58 @@ public class SingboxFmt : BaseFmt
return lstResult; return lstResult;
} }
public static ProfileItem? ResolveFull(string strData, string? subRemarks) private static ProfileItem? ResolveOutbound(JsonObject jsonObject, string? subRemarks)
{ {
var config = JsonUtils.ParseJson(strData); if (!IsValidSingboxOutbound(jsonObject))
if (config?["inbounds"] == null
|| config["outbounds"] == null
|| config["route"] == null
|| config["dns"] == null)
{ {
return null; return null;
} }
var type = jsonObject["type"]?.ToString();
var fileName = WriteAllText(strData); if (type is null or "direct" or "block" or "dns" or "selector" or "urltest")
{
return null;
}
var tag = jsonObject["tag"]?.ToString();
var remarks = $"{type}_{tag}";
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem var profileItem = new ProfileItem
{ {
ConfigType = EConfigType.Outbound,
CoreType = ECoreType.sing_box, CoreType = ECoreType.sing_box,
Address = fileName, Address = fileName,
Remarks = subRemarks ?? "singbox_custom" Remarks = remarks,
}; };
return profileItem; return profileItem;
} }
private static bool IsValidSingboxOutbound(JsonNode? jsonNode)
{
if (jsonNode is not JsonObject jsonObject)
{
return false;
}
var matchedCounter = 0;
if (string.IsNullOrEmpty(jsonObject["type"]?.ToString()))
{
return false;
}
matchedCounter += 1;
if (!string.IsNullOrEmpty(jsonObject["tag"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["server"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["server_port"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["tls"]?.ToString()))
{
matchedCounter += 1;
}
return matchedCounter >= 2;
}
} }

View File

@@ -2,6 +2,8 @@ namespace ServiceLib.Handler.Fmt;
public class TrojanFmt : BaseFmt public class TrojanFmt : BaseFmt
{ {
private static readonly List<string> _insecureQueryKeys = new() { "allowInsecure", "insecure" };
public static ProfileItem? Resolve(string str, out string msg) public static ProfileItem? Resolve(string str, out string msg)
{ {
msg = ResUI.ConfigurationFormatIncorrect; msg = ResUI.ConfigurationFormatIncorrect;
@@ -23,6 +25,10 @@ public class TrojanFmt : BaseFmt
item.Password = Utils.UrlDecode(url.UserInfo); item.Password = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
if (_insecureQueryKeys.Any(q => GetQueryValue(query, q) == "1"))
{
item.AllowInsecure = Global.StringTrue;
}
item.SetProtocolExtra(item.GetProtocolExtra() with { Flow = GetQueryValue(query, "flow") }); item.SetProtocolExtra(item.GetProtocolExtra() with { Flow = GetQueryValue(query, "flow") });
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
@@ -41,6 +47,10 @@ public class TrojanFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
if (item.GetAllowInsecure())
{
_insecureQueryKeys.ForEach(q => dicQuery.Add(q, "1"));
}
if (!item.GetProtocolExtra().Flow.IsNullOrEmpty()) if (!item.GetProtocolExtra().Flow.IsNullOrEmpty())
{ {
dicQuery.Add("flow", item.GetProtocolExtra().Flow); dicQuery.Add("flow", item.GetProtocolExtra().Flow);

View File

@@ -30,6 +30,10 @@ public class TuicFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
if (GetQueryValue(query, "allow_insecure") == "1")
{
item.AllowInsecure = Global.StringTrue;
}
item.SetProtocolExtra(item.GetProtocolExtra() with item.SetProtocolExtra(item.GetProtocolExtra() with
{ {
CongestionControl = GetQueryValue(query, "congestion_control") CongestionControl = GetQueryValue(query, "congestion_control")
@@ -53,7 +57,10 @@ public class TuicFmt : BaseFmt
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
ToUriQueryLite(item, ref dicQuery); ToUriQueryLite(item, ref dicQuery);
if (item.GetAllowInsecure())
{
dicQuery.Add("allow_insecure", "1");
}
if (!item.GetProtocolExtra().CongestionControl.IsNullOrEmpty()) if (!item.GetProtocolExtra().CongestionControl.IsNullOrEmpty())
{ {
dicQuery.Add("congestion_control", item.GetProtocolExtra().CongestionControl); dicQuery.Add("congestion_control", item.GetProtocolExtra().CongestionControl);

View File

@@ -2,47 +2,166 @@ namespace ServiceLib.Handler.Fmt;
public class V2rayFmt : BaseFmt public class V2rayFmt : BaseFmt
{ {
public static List<ProfileItem>? ResolveFullArray(string strData, string? subRemarks) public static List<ProfileItem> ResolveToCustom(string strData, string? subRemarks)
{ {
var configObjects = JsonUtils.Deserialize<object[]>(strData); var jsonNode = JsonUtils.ParseJson(strData);
if (configObjects is not { Length: > 0 }) return ResolveCommon(jsonNode, subRemarks, false);
}
public static List<ProfileItem> ResolveToCustomOutbound(string strData, string? subRemarks)
{
var jsonNode = JsonUtils.ParseJson(strData);
return ResolveCommon(jsonNode, subRemarks, true);
}
private static List<ProfileItem> ResolveCommon(JsonNode? jsonNode, string? subRemarks, bool isOutbound)
{
if (jsonNode is JsonArray jsonArray)
{
return
[
.. jsonArray.Select(item => ResolveCommon(item, subRemarks, isOutbound))
.Where(list => list is { Count: > 0 })
.SelectMany(list => list),
];
}
if (jsonNode is not JsonObject jsonObject)
{
return [];
}
// Process the individual JSON object
var profileList = new List<ProfileItem>();
if (!isOutbound)
{
var fullProfile = ResolveFull(jsonObject, subRemarks);
if (fullProfile is not null)
{
profileList.Add(fullProfile);
return profileList;
}
}
profileList.AddRange(ResolveFullToOutbound(jsonObject, subRemarks));
if (profileList.Count != 0)
{
return profileList;
}
var outboundProfile = ResolveOutbound(jsonObject, subRemarks);
if (outboundProfile is not null)
{
profileList.Add(outboundProfile);
}
return profileList;
}
private static ProfileItem? ResolveFull(JsonObject jsonObject, string? subRemarks)
{
if (jsonObject?["inbounds"] == null
|| jsonObject["outbounds"] == null)
{ {
return null; return null;
} }
List<ProfileItem> lstResult = []; if (jsonObject["outbounds"] is JsonArray outboundsArray)
foreach (var configObject in configObjects)
{ {
var objectString = JsonUtils.Serialize(configObject); if (!outboundsArray.Any(IsValidV2rayOutbound))
var profileIt = ResolveFull(objectString, subRemarks); {
return null;
}
}
else
{
return null;
}
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem
{
ConfigType = EConfigType.Custom,
CoreType = ECoreType.Xray,
Address = fileName,
Remarks = jsonObject["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom",
};
return profileItem;
}
public static List<ProfileItem> ResolveFullToOutbound(JsonObject jsonObject, string? subRemarks)
{
if (jsonObject["outbounds"] is not JsonArray outboundsArray)
{
return [];
}
List<ProfileItem> lstResult = [];
foreach (var outbound in outboundsArray)
{
if (outbound is not JsonObject outboundObj)
{
continue;
}
var profileIt = ResolveOutbound(outboundObj, subRemarks);
if (profileIt != null) if (profileIt != null)
{ {
lstResult.Add(profileIt); lstResult.Add(profileIt);
} }
} }
return lstResult; return lstResult;
} }
public static ProfileItem? ResolveFull(string strData, string? subRemarks) public static ProfileItem? ResolveOutbound(JsonObject jsonObject, string? subRemarks)
{ {
var config = JsonUtils.ParseJson(strData); if (!IsValidV2rayOutbound(jsonObject))
if (config?["inbounds"] == null
|| config["outbounds"] == null
|| config["routing"] == null)
{ {
return null; return null;
} }
var protocol = jsonObject["protocol"]?.ToString();
var fileName = WriteAllText(strData); if (protocol is null or "freedom" or "blackhole" or "dns" or "loopback")
{
return null;
}
var tag = jsonObject["tag"]?.ToString();
var remarks = $"{protocol}_{tag}";
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem var profileItem = new ProfileItem
{ {
ConfigType = EConfigType.Outbound,
CoreType = ECoreType.Xray, CoreType = ECoreType.Xray,
Address = fileName, Address = fileName,
Remarks = config?["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom" Remarks = remarks,
}; };
return profileItem; return profileItem;
} }
private static bool IsValidV2rayOutbound(JsonNode? jsonNode)
{
if (jsonNode is not JsonObject jsonObject)
{
return false;
}
var matchedCounter = 0;
if (string.IsNullOrEmpty(jsonObject["protocol"]?.ToString()))
{
return false;
}
matchedCounter += 1;
if (!string.IsNullOrEmpty(jsonObject["settings"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["streamSettings"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["tag"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["mux"]?.ToString()))
{
matchedCounter += 1;
}
return matchedCounter >= 3;
}
} }

View File

@@ -16,7 +16,6 @@ public static class SysProxyHandler
try try
{ {
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
var exceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
if (port <= 0) if (port <= 0)
{ {
return false; return false;
@@ -24,17 +23,18 @@ public static class SysProxyHandler
switch (type) switch (type)
{ {
case ESysProxyType.ForcedChange when Utils.IsWindows(): case ESysProxyType.ForcedChange when Utils.IsWindows():
{ var (strProxy, strExceptions) = GetWindowsProxyString(config, port);
GetWindowsProxyString(config, port, out var strProxy, out var strExceptions); ProxySettingWindows.SetProxy(strProxy, strExceptions, 2);
ProxySettingWindows.SetProxy(strProxy, strExceptions, 2); break;
break;
}
case ESysProxyType.ForcedChange when Utils.IsLinux(): case ESysProxyType.ForcedChange when Utils.IsLinux():
var exceptions = SanitizeExceptions(config);
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions); await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
break; break;
case ESysProxyType.ForcedChange when Utils.IsMacOS(): case ESysProxyType.ForcedChange when Utils.IsMacOS():
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions); var exceptions2 = SanitizeExceptions(config);
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions2);
break; break;
case ESysProxyType.ForcedClear when Utils.IsWindows(): case ESysProxyType.ForcedClear when Utils.IsWindows():
@@ -66,15 +66,31 @@ public static class SysProxyHandler
return true; return true;
} }
private static void GetWindowsProxyString(Config config, int port, out string strProxy, out string strExceptions) private static string SanitizeExceptions(Config config)
{ {
strExceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", ""); var exceptions = config.SystemProxyItem.SystemProxyExceptions;
if (exceptions.IsNullOrEmpty())
{
return string.Empty;
}
var items = exceptions
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.Select(item => item.Replace(" ", string.Empty))
.Where(item => item.Length > 0);
return string.Join(',', items);
}
private static (string strProxy, string strExceptions) GetWindowsProxyString(Config config, int port)
{
var strExceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
if (config.SystemProxyItem.NotProxyLocalAddress) if (config.SystemProxyItem.NotProxyLocalAddress)
{ {
strExceptions = $"<local>;{strExceptions}"; strExceptions = $"<local>;{strExceptions}";
} }
strProxy = string.Empty; var strProxy = string.Empty;
if (config.SystemProxyItem.SystemProxyAdvancedProtocol.IsNullOrEmpty()) if (config.SystemProxyItem.SystemProxyAdvancedProtocol.IsNullOrEmpty())
{ {
strProxy = $"{Global.Loopback}:{port}"; strProxy = $"{Global.Loopback}:{port}";
@@ -86,6 +102,8 @@ public static class SysProxyHandler
.Replace("{http_port}", port.ToString()) .Replace("{http_port}", port.ToString())
.Replace("{socks_port}", port.ToString()); .Replace("{socks_port}", port.ToString());
} }
return (strProxy, strExceptions);
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]

View File

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

View File

@@ -10,6 +10,7 @@ public sealed class AppManager
private int? _statePort2; private int? _statePort2;
public static AppManager Instance => _instance.Value; public static AppManager Instance => _instance.Value;
public Config Config => _config; public Config Config => _config;
public IWindowDialog WindowDialog { get; set; } = null!;
public int StatePort public int StatePort
{ {
@@ -662,7 +663,7 @@ public sealed class AppManager
return Global.SsSecuritiesInSingbox; return Global.SsSecuritiesInSingbox;
} }
public ECoreType GetCoreType(ProfileItem profileItem, EConfigType eConfigType) public ECoreType GetCoreType(ProfileItem? profileItem, EConfigType eConfigType)
{ {
if (profileItem?.CoreType != null) if (profileItem?.CoreType != null)
{ {

View File

@@ -288,6 +288,7 @@ public class CertPemManager
collection.ImportFromPem(pemText); collection.ImportFromPem(pemText);
return collection; return collection;
}); });
private static readonly Lazy<X509Certificate2Collection> _mozillaRootCerts = new(() => private static readonly Lazy<X509Certificate2Collection> _mozillaRootCerts = new(() =>
{ {
var pemText = EmbedUtils.GetEmbedText(Global.MozillaRootCertFileName); var pemText = EmbedUtils.GetEmbedText(Global.MozillaRootCertFileName);
@@ -295,6 +296,7 @@ public class CertPemManager
collection.ImportFromPem(pemText); collection.ImportFromPem(pemText);
return collection; return collection;
}); });
private X509Certificate2Collection BuildTrustedCertificateCollection() private X509Certificate2Collection BuildTrustedCertificateCollection()
{ {
if (_config.GuiItem.RootCertProvider == Global.ChromeRootProvider) if (_config.GuiItem.RootCertProvider == Global.ChromeRootProvider)

View File

@@ -34,7 +34,18 @@ public class CoreAdminManager
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("#!/bin/bash"); sb.AppendLine("#!/bin/bash");
var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}"; var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";
sb.AppendLine($"exec sudo -S -- {cmdLine}");
// Passing environment variables to the sudo command, here it only xray or sing-box.
if (coreInfo.Environment.Count > 0)
{
var envArgs = string.Join(" ", coreInfo.Environment.Where(kv => kv.Value.IsNotEmpty()).Select(kv => $"{kv.Key}={kv.Value.AppendQuotes()}"));
sb.AppendLine($"exec sudo -S -- env {envArgs} {cmdLine}");
}
else
{
sb.AppendLine($"exec sudo -S -- {cmdLine}");
}
var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
var procService = new ProcessService( var procService = new ProcessService(
@@ -80,6 +91,8 @@ public class CoreAdminManager
.ExecuteBufferedAsync(); .ExecuteBufferedAsync();
await UpdateFunc(false, result.StandardOutput.ToString()); await UpdateFunc(false, result.StandardOutput.ToString());
await Task.Delay(1000); // Wait for a second to ensure the process is killed
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -8,8 +8,10 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new()); private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value; public static CoreManager Instance => _instance.Value;
private Config _config; private Config _config;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private WindowsJobService? _processJob; private WindowsJobService? _processJob;
private ProcessService? _processService; private ProcessService? _processService;
private ProcessService? _processPreService; private ProcessService? _processPreService;
private bool _linuxSudo = false; private bool _linuxSudo = false;
@@ -83,7 +85,7 @@ public class CoreManager
await CoreStop(); await CoreStop();
await Task.Delay(100); await Task.Delay(100);
if (Utils.IsWindows() && _config.TunModeItem.EnableTun) if (Utils.IsWindows() && (mainContext?.IsTunEnabled == true || preContext?.IsTunEnabled == true))
{ {
await Task.Delay(100); await Task.Delay(100);
await WindowsUtils.RemoveTunDevice(); await WindowsUtils.RemoveTunDevice();
@@ -181,7 +183,7 @@ public class CoreManager
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog; var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true); var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true, context.IsTunEnabled);
if (proc is null) if (proc is null)
{ {
return; return;
@@ -199,7 +201,7 @@ public class CoreManager
if (result.Success) if (result.Success)
{ {
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true); var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true, preContext.IsTunEnabled);
if (proc is null) if (proc is null)
{ {
return; return;
@@ -220,7 +222,7 @@ public class CoreManager
{ {
return; return;
} }
if (!preContext.AppConfig.TunModeItem.EnableTun) if (!preContext.IsTunEnabled)
{ {
return; return;
} }
@@ -287,7 +289,20 @@ public class CoreManager
#region Process #region Process
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo) /// <summary>
/// Decides whether a core launch must be elevated on non-Windows platforms.
/// The TUN state comes from the immutable <see cref="CoreConfigContext" /> snapshot that
/// generated the config, never from the live mutable config: the generated config and the
/// launch mode must always agree, even if TUN is toggled while a reload is in flight.
/// </summary>
public static bool ShouldRunAsSudo(bool isTunLaunch, ECoreType? coreType, bool isNonWindows)
{
return isTunLaunch
&& coreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray
&& isNonWindows;
}
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo, bool isTunLaunch = false)
{ {
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg); var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
@@ -299,9 +314,7 @@ public class CoreManager
try try
{ {
if (mayNeedSudo if (mayNeedSudo
&& _config.TunModeItem.EnableTun && ShouldRunAsSudo(isTunLaunch, coreInfo.CoreType, Utils.IsNonWindows()))
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray)
&& Utils.IsNonWindows())
{ {
_linuxSudo = true; _linuxSudo = true;
await CoreAdminManager.Instance.Init(_config, _updateFunc); await CoreAdminManager.Instance.Init(_config, _updateFunc);

View File

@@ -118,7 +118,7 @@ public class GroupProfileManager
return childProfiles?.Where(p => return childProfiles?.Where(p =>
p != null && p != null &&
p.IsValid() && p.IsValid() &&
!p.ConfigType.IsComplexType() && (!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter)) (extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
) )
.ToList() ?? []; .ToList() ?? [];

View File

@@ -1,5 +1,3 @@
//using System.Reactive.Linq;
namespace ServiceLib.Manager; namespace ServiceLib.Manager;
public class ProfileExManager public class ProfileExManager

View File

@@ -34,6 +34,7 @@ public class Config
public List<KeyEventItem> GlobalHotkeys { get; set; } public List<KeyEventItem> GlobalHotkeys { get; set; }
public List<CoreTypeItem> CoreTypeItem { get; set; } public List<CoreTypeItem> CoreTypeItem { get; set; }
public SimpleDNSItem SimpleDNSItem { get; set; } public SimpleDNSItem SimpleDNSItem { get; set; }
public HappyEyeballs4RayItem HappyEyeballs4RayItem { get; set; }
#endregion other entities #endregion other entities
} }

View File

@@ -103,6 +103,7 @@ public class UIItem
public bool MacOSShowInDock { get; set; } public bool MacOSShowInDock { get; set; }
public List<ColumnItem> MainColumnItem { get; set; } public List<ColumnItem> MainColumnItem { get; set; }
public List<WindowSizeItem> WindowSizeItem { get; set; } public List<WindowSizeItem> WindowSizeItem { get; set; }
public bool HideColumnIpInfo { get; set; }
} }
[Serializable] [Serializable]
@@ -146,8 +147,10 @@ public class TunModeItem
public int Mtu { get; set; } public int Mtu { get; set; }
public bool EnableIPv6Address { get; set; } public bool EnableIPv6Address { get; set; }
public string IcmpRouting { get; set; } public string IcmpRouting { get; set; }
public bool EnableLegacyProtect { get; set; } public bool EnableLegacyProtect { get; set; } = true;
public List<string>? RouteExcludeAddress { get; set; } public List<string>? RouteExcludeAddress { get; set; }
public string IPv4Address { get; set; }
public string IPv6Address { get; set; }
} }
[Serializable] [Serializable]
@@ -248,9 +251,15 @@ public class CheckUpdateItem
public class Fragment4RayItem public class Fragment4RayItem
{ {
public string? Packets { get; set; } public string? Packets { get; set; }
public string? Length { get; set; } public List<string>? Lengths { get; set; }
public string? Interval { get; set; } public List<string>? Delays { get; set; }
public string? MaxSplit { get; set; } public string? MaxSplit { get; set; }
// For migration from old version, remove those properties in the future
public string? Length { get; set; }
public string? Interval { get; set; }
// migration end
} }
[Serializable] [Serializable]
@@ -268,14 +277,27 @@ public class SimpleDNSItem
public bool? AddCommonHosts { get; set; } public bool? AddCommonHosts { get; set; }
public bool? FakeIP { get; set; } public bool? FakeIP { get; set; }
public bool? GlobalFakeIp { get; set; } public bool? GlobalFakeIp { get; set; }
public string? FakeIPRange { get; set; }
public bool? BlockBindingQuery { get; set; } public bool? BlockBindingQuery { get; set; }
public bool? BlockAAAAQuery { get; set; }
public string? DirectDNS { get; set; } public string? DirectDNS { get; set; }
public string? RemoteDNS { get; set; } public string? RemoteDNS { get; set; }
public string? BootstrapDNS { get; set; } public string? BootstrapDNS { get; set; }
public string? Strategy4Freedom { get; set; } public string? Strategy4Freedom { get; set; }
public string? Strategy4Proxy { get; set; } public string? Strategy4Proxy { get; set; }
public string? Strategy4ProxyDial { get; set; }
public bool? ServeStale { get; set; } public bool? ServeStale { get; set; }
public bool? ParallelQuery { get; set; } public bool? ParallelQuery { get; set; }
public string? Hosts { get; set; } public string? Hosts { get; set; }
public string? DirectExpectedIPs { get; set; } public string? DirectExpectedIPs { get; set; }
public bool? EnableHappyEyeballs { get; set; }
}
[Serializable]
public class HappyEyeballs4RayItem
{
public int? TryDelayMs { get; set; }
public bool? PrioritizeIPv6 { get; set; }
public int? Interleave { get; set; }
public int? MaxConcurrentTry { get; set; }
} }

View File

@@ -11,13 +11,20 @@ public record CoreConfigContext
public Config AppConfig { get; init; } = new(); public Config AppConfig { get; init; } = new();
public FullConfigTemplateItem? FullConfigTemplate { get; init; } = new(); public FullConfigTemplateItem? FullConfigTemplate { get; init; } = new();
public Dictionary<string, string> CustomOutboundContent { get; init; } = new();
// Test ServerTestItem Map // Test ServerTestItem Map
public Dictionary<string, string> ServerTestItemMap { get; init; } = new(); public Dictionary<string, string> ServerTestItemMap { get; init; } = new();
// TUN Compatibility // TUN Compatibility
public bool IsTunEnabled { get; init; } = false; public bool IsTunEnabled { get; init; } = false;
public HashSet<string> ProtectDomainList { get; init; } = []; public HashSet<string> ProtectDomainList { get; init; } = [];
// Typically, it is the core of the outbound chain
public HashSet<ECoreType> ProtectCoreTypeList { get; init; } = [];
public bool IsWindows { get; init; } public bool IsWindows { get; init; }
public bool IsMacOS { get; init; } public bool IsMacOS { get; init; }
// Generation Context
public Dictionary<object, string> CustomOutboundMap { get; init; } = new();
} }

View File

@@ -4,6 +4,7 @@ public class V2rayConfig
{ {
public Log4Ray log { get; set; } public Log4Ray log { get; set; }
public object dns { get; set; } public object dns { get; set; }
public FakeDns4Ray? fakedns { get; set; }
public List<Inbounds4Ray> inbounds { get; set; } public List<Inbounds4Ray> inbounds { get; set; }
public List<Outbounds4Ray> outbounds { get; set; } public List<Outbounds4Ray> outbounds { get; set; }
public Routing4Ray routing { get; set; } public Routing4Ray routing { get; set; }
@@ -43,6 +44,12 @@ public class Log4Ray
public string? loglevel { get; set; } public string? loglevel { get; set; }
} }
public class FakeDns4Ray
{
public string? ipPool { get; set; }
public long? poolSize { get; set; }
}
public class Inbounds4Ray public class Inbounds4Ray
{ {
public string tag { get; set; } public string tag { get; set; }
@@ -136,8 +143,6 @@ public class Outboundsettings4Ray
public Response4Ray? response { get; set; } public Response4Ray? response { get; set; }
public string? domainStrategy { get; set; }
public int? userLevel { get; set; } public int? userLevel { get; set; }
public string? secretKey { get; set; } public string? secretKey { get; set; }
@@ -146,6 +151,16 @@ public class Outboundsettings4Ray
public int? port { get; set; } public int? port { get; set; }
public string? user { get; set; }
public string? pass { get; set; }
public int? level { get; set; }
public string? email { get; set; }
public object? headers { get; set; }
public List<WireguardPeer4Ray>? peers { get; set; } public List<WireguardPeer4Ray>? peers { get; set; }
public bool? noKernelTun { get; set; } public bool? noKernelTun { get; set; }
@@ -226,6 +241,7 @@ public class Dns4Ray
public List<object> servers { get; set; } public List<object> servers { get; set; }
public bool? serveStale { get; set; } public bool? serveStale { get; set; }
public bool? enableParallelQuery { get; set; } public bool? enableParallelQuery { get; set; }
public string? queryStrategy { get; set; }
public string? tag { get; set; } public string? tag { get; set; }
} }
@@ -276,7 +292,6 @@ public class BalancersItem4Ray
public List<string>? selector { get; set; } public List<string>? selector { get; set; }
public BalancersStrategy4Ray? strategy { get; set; } public BalancersStrategy4Ray? strategy { get; set; }
public string? tag { get; set; } public string? tag { get; set; }
public string? fallbackTag { get; set; }
} }
public class BalancersStrategy4Ray public class BalancersStrategy4Ray
@@ -350,8 +365,6 @@ public class StreamSettings4Ray
public class TlsSettings4Ray public class TlsSettings4Ray
{ {
public bool? allowInsecure { get; set; }
public string? serverName { get; set; } public string? serverName { get; set; }
public List<string>? alpn { get; set; } public List<string>? alpn { get; set; }
@@ -507,6 +520,8 @@ public class MaskSettings4Ray
public string? length { get; set; } public string? length { get; set; }
public string? delay { get; set; } public string? delay { get; set; }
public List<string>? lengths { get; set; }
public List<string>? delays { get; set; }
public int? maxSplit { get; set; } public int? maxSplit { get; set; }
// noise // noise
@@ -538,15 +553,20 @@ public class AccountsItem4Ray
public class Sockopt4Ray public class Sockopt4Ray
{ {
public string? domainStrategy { get; set; }
public string? dialerProxy { get; set; } public string? dialerProxy { get; set; }
[JsonPropertyName("interface")] [JsonPropertyName("interface")]
public string? Interface { get; set; } public string? Interface { get; set; }
public HappyEyeballs4Ray? happyEyeballs { get; set; }
} }
public class FragmentItem4Ray public class HappyEyeballs4Ray
{ {
public string? packets { get; set; } public int? tryDelayMs { get; set; }
public string? length { get; set; } public bool? prioritizeIPv6 { get; set; }
public string? interval { get; set; } public int? interleave { get; set; }
public int? maxConcurrentTry { get; set; }
} }

View File

@@ -1,10 +1,10 @@
namespace ServiceLib.Models.Dto; namespace ServiceLib.Models.Dto;
public class CheckUpdateModel : ReactiveObject public partial class CheckUpdateModel : ReactiveObject
{ {
public bool? IsSelected { get; set; } public bool? IsSelected { get; set; }
public ECoreType? CoreType { get; set; } public ECoreType? CoreType { get; set; }
[Reactive] public string? Remarks { get; set; } [Reactive] public partial string? Remarks { get; set; }
public string? FileName { get; set; } public string? FileName { get; set; }
public bool? IsFinished { get; set; } public bool? IsFinished { get; set; }
public bool IsGeoFile { get; set; } public bool IsGeoFile { get; set; }

View File

@@ -1,7 +1,7 @@
namespace ServiceLib.Models.Dto; namespace ServiceLib.Models.Dto;
[Serializable] [Serializable]
public class ClashProxyModel : ReactiveObject public partial class ClashProxyModel : ReactiveObject
{ {
public string? Name { get; set; } public string? Name { get; set; }
@@ -9,9 +9,9 @@ public class ClashProxyModel : ReactiveObject
public string? Now { get; set; } public string? Now { get; set; }
[Reactive] public int Delay { get; set; } [Reactive] public partial int Delay { get; set; }
[Reactive] public string? DelayName { get; set; } [Reactive] public partial string? DelayName { get; set; }
public bool IsActive { get; set; } public bool IsActive { 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

@@ -1,7 +1,7 @@
namespace ServiceLib.Models.Dto; namespace ServiceLib.Models.Dto;
[Serializable] [Serializable]
public class ProfileItemModel : ReactiveObject public partial class ProfileItemModel : ReactiveObject
{ {
public bool IsActive { get; set; } public bool IsActive { get; set; }
public string IndexId { get; set; } public string IndexId { get; set; }
@@ -16,30 +16,30 @@ public class ProfileItemModel : ReactiveObject
public int Sort { get; set; } public int Sort { get; set; }
[Reactive] [Reactive]
public int Delay { get; set; } public partial int Delay { get; set; }
public decimal Speed { get; set; } public decimal Speed { get; set; }
[Reactive] [Reactive]
public string DelayVal { get; set; } public partial string DelayVal { get; set; }
[Reactive] [Reactive]
public string SpeedVal { get; set; } public partial string SpeedVal { get; set; }
[Reactive] [Reactive]
public string IpInfo { get; set; } public partial string IpInfo { get; set; }
[Reactive] [Reactive]
public string TodayUp { get; set; } public partial string TodayUp { get; set; }
[Reactive] [Reactive]
public string TodayDown { get; set; } public partial string TodayDown { get; set; }
[Reactive] [Reactive]
public string TotalUp { get; set; } public partial string TotalUp { get; set; }
[Reactive] [Reactive]
public string TotalDown { get; set; } public partial string TotalDown { get; set; }
public string GetSummary() public string GetSummary()
{ {

View File

@@ -66,7 +66,7 @@ public class ProfileItem
public bool IsValid() public bool IsValid()
{ {
if (IsComplex()) if (IsComplex() || ConfigType == EConfigType.Outbound)
{ {
return true; return true;
} }

View File

@@ -5,6 +5,9 @@ public record ProtocolExtraItem
public bool? Uot { get; init; } public bool? Uot { get; init; }
public string? CongestionControl { get; init; } public string? CongestionControl { get; init; }
// http outbound
public string? HttpHeaders { get; init; }
// vmess // vmess
public string? AlterId { get; init; } public string? AlterId { get; init; }
public string? VmessSecurity { get; init; } public string? VmessSecurity { get; init; }
@@ -48,4 +51,7 @@ public record ProtocolExtraItem
public string? SubChildItems { get; init; } public string? SubChildItems { get; init; }
public string? Filter { get; init; } public string? Filter { get; init; }
public EMultipleLoad? MultipleLoad { get; init; } public EMultipleLoad? MultipleLoad { get; init; }
// custom outbound
public bool? IsSingboxEndpoint { get; init; }
} }

View File

@@ -33,4 +33,6 @@ public class SubItem
public int? PreSocksPort { get; set; } public int? PreSocksPort { get; set; }
public string? Memo { get; set; } public string? Memo { get; set; }
public ECoreType? CustomCoreType { get; set; }
} }

View File

@@ -178,7 +178,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Failed to import custom configuration Configuration 的本地化字符串。 /// 查找类似 Failed to import configuration 的本地化字符串。
/// </summary> /// </summary>
public static string FailedImportedCustomServer { public static string FailedImportedCustomServer {
get { get {
@@ -268,7 +268,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Please browse to import Configuration configuration 的本地化字符串。 /// 查找类似 Please browse to import configuration 的本地化字符串。
/// </summary> /// </summary>
public static string FillServerAddressCustom { public static string FillServerAddressCustom {
get { get {
@@ -321,6 +321,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Please enter valid HTTP request headers JSON. 的本地化字符串。
/// </summary>
public static string InvalidHttpOutboundHeaders {
get {
return ResourceManager.GetString("InvalidHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Invalid Realm URL. 的本地化字符串。 /// 查找类似 Invalid Realm URL. 的本地化字符串。
/// </summary> /// </summary>
@@ -429,6 +438,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom config core 的本地化字符串。
/// </summary>
public static string LvCustomCoreType {
get {
return ResourceManager.GetString("LvCustomCoreType", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Custom icon 的本地化字符串。 /// 查找类似 Custom icon 的本地化字符串。
/// </summary> /// </summary>
@@ -726,6 +744,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Add a custom outbound 的本地化字符串。
/// </summary>
public static string menuAddCustomOutboundServer {
get {
return ResourceManager.GetString("menuAddCustomOutboundServer", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Add a custom configuration 的本地化字符串。 /// 查找类似 Add a custom configuration 的本地化字符串。
/// </summary> /// </summary>
@@ -1924,7 +1951,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration. 的本地化字符串。 /// 查找类似 The current node uses an unencrypted connection, meaning your communications could be directly monitored by network intermediaries controlled by authoritarian governments. For security reasons, nodes of this type cannot connect via Xray-core versions 26.2.6 or higher. If this is a self-built node, please enable TLS or other secure encryption, or pin the certificate using pinSHA256. If this is an airport/provider node, please contact your service provider for a technical upgrade. If the provider refuses to c [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary> /// </summary>
public static string MsgAllowInsecureDeprecated { public static string MsgAllowInsecureDeprecated {
get { get {
@@ -1968,6 +1995,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom outbound {0} file not found: {1} 的本地化字符串。
/// </summary>
public static string MsgCustomOutboundFileNotFound {
get {
return ResourceManager.GetString("MsgCustomOutboundFileNotFound", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。 /// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。
/// </summary> /// </summary>
@@ -2617,7 +2653,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Custom configuration Configuration imported successfully 的本地化字符串。 /// 查找类似 Custom configuration imported successfully 的本地化字符串。
/// </summary> /// </summary>
public static string SuccessfullyImportedCustomServer { public static string SuccessfullyImportedCustomServer {
get { get {
@@ -2733,6 +2769,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Block AAAA Queries 的本地化字符串。
/// </summary>
public static string TbBlockAAAAQueries {
get {
return ResourceManager.GetString("TbBlockAAAAQueries", resourceCulture);
}
}
/// <summary>
/// 查找类似 Block IPv6 queries when enabled 的本地化字符串。
/// </summary>
public static string TbBlockAAAAQueriesTips {
get {
return ResourceManager.GetString("TbBlockAAAAQueriesTips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Block SVCB and HTTPS Queries 的本地化字符串。 /// 查找类似 Block SVCB and HTTPS Queries 的本地化字符串。
/// </summary> /// </summary>
@@ -2743,7 +2797,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Block ECH and HTTP/3 availability checks when enabled 的本地化字符串。 /// 查找类似 Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray 的本地化字符串。
/// </summary> /// </summary>
public static string TbBlockSVCBHTTPSQueriesTips { public static string TbBlockSVCBHTTPSQueriesTips {
get { get {
@@ -2916,6 +2970,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Only single outbound/endpoint supported for xray/sing-box 的本地化字符串。
/// </summary>
public static string TbCustomOutboundTip {
get {
return ResourceManager.GetString("TbCustomOutboundTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Direct Target Resolution Strategy 的本地化字符串。 /// 查找类似 Direct Target Resolution Strategy 的本地化字符串。
/// </summary> /// </summary>
@@ -3051,6 +3114,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Enable Happy Eyeballs 的本地化字符串。
/// </summary>
public static string TbEnableHappyEyeballs {
get {
return ResourceManager.GetString("TbEnableHappyEyeballs", resourceCulture);
}
}
/// <summary>
/// 查找类似 Requires the UseIP Strategy. When enabled, it attempts IPv4 and IPv6 connections simultaneously and automatically selects the faster available path. 的本地化字符串。
/// </summary>
public static string TbEnableHappyEyeballsTip {
get {
return ResourceManager.GetString("TbEnableHappyEyeballsTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 DNS via Bridge 的本地化字符串。 /// 查找类似 DNS via Bridge 的本地化字符串。
/// </summary> /// </summary>
@@ -3079,7 +3160,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Applies globally by default, with built-in FakeIP filtering (sing-box only). 的本地化字符串。 /// 查找类似 Applies globally by default, and built-in FakeIP filtering is only built into sing-box. 的本地化字符串。
/// </summary> /// </summary>
public static string TbFakeIPTips { public static string TbFakeIPTips {
get { get {
@@ -3240,6 +3321,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 HTTP headers 的本地化字符串。
/// </summary>
public static string TbHttpOutboundHeaders {
get {
return ResourceManager.GetString("TbHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Realm URL 的本地化字符串。 /// 查找类似 Realm URL 的本地化字符串。
/// </summary> /// </summary>
@@ -3312,6 +3402,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Ipv4 Address 的本地化字符串。
/// </summary>
public static string TbIpv4Address {
get {
return ResourceManager.GetString("TbIpv4Address", resourceCulture);
}
}
/// <summary>
/// 查找类似 Ipv6 Address 的本地化字符串。
/// </summary>
public static string TbIpv6Address {
get {
return ResourceManager.GetString("TbIpv6Address", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Most Stable 的本地化字符串。 /// 查找类似 Most Stable 的本地化字符串。
/// </summary> /// </summary>
@@ -3510,6 +3618,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Proxy Dial Resolution Strategy 的本地化字符串。
/// </summary>
public static string TbProxyDialResolveStrategy {
get {
return ResourceManager.GetString("TbProxyDialResolveStrategy", resourceCulture);
}
}
/// <summary>
/// 查找类似 Not recommended; may cause routing loops. 的本地化字符串。
/// </summary>
public static string TbProxyDialResolveStrategyTip {
get {
return ResourceManager.GetString("TbProxyDialResolveStrategyTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Public Key 的本地化字符串。 /// 查找类似 Public Key 的本地化字符串。
/// </summary> /// </summary>
@@ -4923,6 +5049,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Xray Only 的本地化字符串。
/// </summary>
public static string TbXrayOnly {
get {
return ResourceManager.GetString("TbXrayOnly", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。 /// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
/// </summary> /// </summary>
@@ -4986,6 +5121,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom HTTP outbound request headers as a JSON object with string or string array values. 的本地化字符串。
/// </summary>
public static string TipHttpOutboundHeaders {
get {
return ResourceManager.GetString("TipHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 *Default value raw 的本地化字符串。 /// 查找类似 *Default value raw 的本地化字符串。
/// </summary> /// </summary>

View File

@@ -1456,7 +1456,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value> <value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value> <value>Please fill in the correct config template</value>
@@ -1495,7 +1495,7 @@
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value> <value>Please Add At Least One Configuration</value>

View File

@@ -1453,7 +1453,7 @@
<value>DNS personnalisé activé ; la configuration de cette page sera ignorée</value> <value>DNS personnalisé activé ; la configuration de cette page sera ignorée</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Une fois activé, bloque les requêtes ECH et de disponibilité HTTP/3</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Veuillez saisir un modèle de configuration valide</value> <value>Veuillez saisir un modèle de configuration valide</value>
@@ -1492,7 +1492,7 @@
<value>Choisir une config.</value> <value>Choisir une config.</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Actif globalement par défaut, avec filtre FakeIP intégré ; ne fonctionne que dans sing-box</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Veuillez ajouter au moins une configuration</value> <value>Veuillez ajouter au moins une configuration</value>

View File

@@ -1456,7 +1456,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value> <value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value> <value>Please fill in the correct config template</value>
@@ -1495,7 +1495,7 @@
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value> <value>Please Add At Least One Configuration</value>

View File

@@ -1456,7 +1456,7 @@
<value>DNS kustom diaktifkan, pengaturan halaman ini tidak berlaku</value> <value>DNS kustom diaktifkan, pengaturan halaman ini tidak berlaku</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Blokir pemeriksaan ketersediaan ECH dan HTTP/3 saat diaktifkan</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Isi template konfigurasi yang benar</value> <value>Isi template konfigurasi yang benar</value>
@@ -1495,7 +1495,7 @@
<value>Pilih profil</value> <value>Pilih profil</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Berlaku secara global secara default, dengan filter FakeIP bawaan (hanya sing-box).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Silakan tambahkan setidaknya satu konfigurasi.</value> <value>Silakan tambahkan setidaknya satu konfigurasi.</value>

View File

@@ -142,7 +142,7 @@
<value>Failed to get the default configuration</value> <value>Failed to get the default configuration</value>
</data> </data>
<data name="FailedImportedCustomServer" xml:space="preserve"> <data name="FailedImportedCustomServer" xml:space="preserve">
<value>Failed to import custom configuration Configuration</value> <value>Failed to import configuration</value>
</data> </data>
<data name="FailedReadConfiguration" xml:space="preserve"> <data name="FailedReadConfiguration" xml:space="preserve">
<value>Failed to read configuration file</value> <value>Failed to read configuration file</value>
@@ -283,7 +283,7 @@
<value>Configuration successful. {0}</value> <value>Configuration successful. {0}</value>
</data> </data>
<data name="SuccessfullyImportedCustomServer" xml:space="preserve"> <data name="SuccessfullyImportedCustomServer" xml:space="preserve">
<value>Custom configuration Configuration imported successfully</value> <value>Custom configuration imported successfully</value>
</data> </data>
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve"> <data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
<value>{0} Configurations have been imported from clipboard</value> <value>{0} Configurations have been imported from clipboard</value>
@@ -385,7 +385,7 @@
<value>All</value> <value>All</value>
</data> </data>
<data name="FillServerAddressCustom" xml:space="preserve"> <data name="FillServerAddressCustom" xml:space="preserve">
<value>Please browse to import Configuration configuration</value> <value>Please browse to import configuration</value>
</data> </data>
<data name="Speedtesting" xml:space="preserve"> <data name="Speedtesting" xml:space="preserve">
<value>Testing...</value> <value>Testing...</value>
@@ -1044,6 +1044,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>Congestion control</value> <value>Congestion control</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP headers</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>Custom HTTP outbound request headers as a JSON object with string or string array values.</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>Previous proxy remarks</value> <value>Previous proxy remarks</value>
</data> </data>
@@ -1456,7 +1462,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value> <value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value> <value>Please fill in the correct config template</value>
@@ -1495,7 +1501,7 @@
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value> <value>Please Add At Least One Configuration</value>
@@ -1774,7 +1780,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>New Update</value> <value>New Update</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve"> <data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration.</value> <value>The current node uses an unencrypted connection, meaning your communications could be directly monitored by network intermediaries controlled by authoritarian governments. For security reasons, nodes of this type cannot connect via Xray-core versions 26.2.6 or higher. If this is a self-built node, please enable TLS or other secure encryption, or pin the certificate using pinSHA256. If this is an airport/provider node, please contact your service provider for a technical upgrade. If the provider refuses to cooperate, it is recommended to switch to a service provider that prioritizes user security. For more information, please visit https://github.com/2dust/v2rayN/discussions/9460.</value>
</data> </data>
<data name="TbRouteExcludeAddress" xml:space="preserve"> <data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Route Exclude Address</value> <value>Route Exclude Address</value>
@@ -1806,6 +1812,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="InvalidHy2RealmUrl" xml:space="preserve"> <data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Invalid Realm URL.</value> <value>Invalid Realm URL.</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>Please enter valid HTTP request headers JSON.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve"> <data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value> <value>Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data> </data>
@@ -1821,4 +1830,43 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbRootCertificateProviderTip" xml:space="preserve"> <data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>Only applies to the v2rayN GUI's downloads and network requests. Does not affect the core's certificate validation.</value> <value>Only applies to the v2rayN GUI's downloads and network requests. Does not affect the core's certificate validation.</value>
</data> </data>
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
<value>Proxy Dial Resolution Strategy</value>
</data>
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
<value>Not recommended; may cause routing loops.</value>
</data>
<data name="TbEnableHappyEyeballs" xml:space="preserve">
<value>Enable Happy Eyeballs</value>
</data>
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
<value>Requires the UseIP Strategy. When enabled, it attempts IPv4 and IPv6 connections simultaneously and automatically selects the faster available path.</value>
</data>
<data name="TbIpv6Address" xml:space="preserve">
<value>Ipv6 Address</value>
</data>
<data name="TbIpv4Address" xml:space="preserve">
<value>Ipv4 Address</value>
</data>
<data name="menuAddCustomOutboundServer" xml:space="preserve">
<value>Add a custom outbound</value>
</data>
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
<value>Custom outbound {0} file not found: {1}</value>
</data>
<data name="TbCustomOutboundTip" xml:space="preserve">
<value>Only single outbound/endpoint supported for xray/sing-box</value>
</data>
<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> </root>

View File

@@ -268,7 +268,7 @@
<value>Сначала выберите сервер</value> <value>Сначала выберите сервер</value>
</data> </data>
<data name="RemoveDuplicateServerResult" xml:space="preserve"> <data name="RemoveDuplicateServerResult" xml:space="preserve">
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}.</value> <value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}</value>
</data> </data>
<data name="RemoveServer" xml:space="preserve"> <data name="RemoveServer" xml:space="preserve">
<value>Вы уверены, что хотите удалить сервер?</value> <value>Вы уверены, что хотите удалить сервер?</value>
@@ -1044,6 +1044,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>Управление перегрузками</value> <value>Управление перегрузками</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP-заголовки</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>Псевдоним предыдущего прокси</value> <value>Псевдоним предыдущего прокси</value>
</data> </data>
@@ -1114,28 +1120,28 @@
<value>Длина фрагмента</value> <value>Длина фрагмента</value>
</data> </data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve"> <data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию.</value> <value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию</value>
</data> </data>
<data name="TbSettingsFragmentInterval" xml:space="preserve"> <data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Интервал фрагментации</value> <value>Интервал фрагментации</value>
</data> </data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve"> <data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию.</value> <value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию</value>
</data> </data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve"> <data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Макс. разделение</value> <value>Макс. разделение</value>
</data> </data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve"> <data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию.</value> <value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию</value>
</data> </data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve"> <data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Резервная задержка</value> <value>Резервная задержка</value>
</data> </data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve"> <data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию.</value> <value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию</value>
</data> </data>
<data name="FillFragmentParameterError" xml:space="preserve"> <data name="FillFragmentParameterError" xml:space="preserve">
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100).</value> <value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100)</value>
</data> </data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Включить файл кэша для sing-box (файлы наборов правил)</value> <value>Включить файл кэша для sing-box (файлы наборов правил)</value>
@@ -1345,7 +1351,7 @@
<value>Пароль sudo системы</value> <value>Пароль sudo системы</value>
</data> </data>
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve"> <data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска.</value> <value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска</value>
</data> </data>
<data name="TransportHeaderType5" xml:space="preserve"> <data name="TransportHeaderType5" xml:space="preserve">
<value>XHTTP-режим</value> <value>XHTTP-режим</value>
@@ -1405,7 +1411,7 @@
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value> <value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
</data> </data>
<data name="SudoIncorrectPasswordTip" xml:space="preserve"> <data name="SudoIncorrectPasswordTip" xml:space="preserve">
<value>Неверный пароль, попробуйте ещё раз.</value> <value>Неверный пароль, попробуйте ещё раз</value>
</data> </data>
<data name="TbMldsa65Verify" xml:space="preserve"> <data name="TbMldsa65Verify" xml:space="preserve">
<value>Mldsa65Verify</value> <value>Mldsa65Verify</value>
@@ -1456,7 +1462,7 @@
<value>Включён пользовательский DNS — настройки на этой странице не применяются</value> <value>Включён пользовательский DNS — настройки на этой странице не применяются</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>При включении блокирует проверки доступности ECH и HTTP/3</value> <value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Пожалуйста, заполните корректный шаблон конфигурации</value> <value>Пожалуйста, заполните корректный шаблон конфигурации</value>
@@ -1486,7 +1492,7 @@
<value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value> <value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value>
</data> </data>
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value> <value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve"> <data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Начинается разбор и обработка содержимого подписки</value> <value>Начинается разбор и обработка содержимого подписки</value>
@@ -1495,7 +1501,7 @@
<value>Выбрать профиль</value> <value>Выбрать профиль</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>По умолчанию применяется глобально, со встроенной фильтрацией FakeIP (только sing-box).</value> <value>Применяется глобально по умолчанию; встроенная фильтрация FakeIP есть только в sing-box</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Добавьте хотя бы одну конфигурацию</value> <value>Добавьте хотя бы одну конфигурацию</value>
@@ -1582,7 +1588,7 @@
<value>Привязанный сертификат (заполните любое из полей) <value>Привязанный сертификат (заполните любое из полей)
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится. При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value> Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Получить сертификат</value> <value>Получить сертификат</value>
@@ -1614,6 +1620,9 @@
<data name="TbEchConfigList" xml:space="preserve"> <data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value> <value>EchConfigList</value>
</data> </data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Проверять сертификат узла по имени</value>
</data>
<data name="TbFullCertTips" xml:space="preserve"> <data name="TbFullCertTips" xml:space="preserve">
<value>Полный сертификат (цепочка) в формате PEM</value> <value>Полный сертификат (цепочка) в формате PEM</value>
</data> </data>
@@ -1633,10 +1642,10 @@
<value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value> <value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value>
</data> </data>
<data name="TbDirectResolveStrategyTips" xml:space="preserve"> <data name="TbDirectResolveStrategyTips" xml:space="preserve">
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль.</value> <value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль</value>
</data> </data>
<data name="TbRemoteResolveStrategyTips" xml:space="preserve"> <data name="TbRemoteResolveStrategyTips" xml:space="preserve">
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль.</value> <value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль</value>
</data> </data>
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Интервал смены портов (Port Hopping)</value> <value>Интервал смены портов (Port Hopping)</value>
@@ -1651,37 +1660,37 @@
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value> <value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
</data> </data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve"> <data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел.</value> <value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел</value>
</data> </data>
<data name="MsgGroupCycleDependency" xml:space="preserve"> <data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен.</value> <value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен</value>
</data> </data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve"> <data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value> <value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
</data> </data>
<data name="MsgGroupChildNodeError" xml:space="preserve"> <data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен.</value> <value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен</value>
</data> </data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve"> <data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value> <value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
</data> </data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve"> <data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен.</value> <value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен</value>
</data> </data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve"> <data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>У группы {0} нет допустимых дочерних узлов.</value> <value>У группы {0} нет допустимых дочерних узлов</value>
</data> </data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve"> <data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел.</value> <value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел</value>
</data> </data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve"> <data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел.</value> <value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел</value>
</data> </data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve"> <data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Предыдущий прокси подписки {0} не найден. Пропущено.</value> <value>Предыдущий прокси подписки {0} не найден. Пропущено</value>
</data> </data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve"> <data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Следующий прокси подписки {0} не найден. Пропущено.</value> <value>Следующий прокси подписки {0} не найден. Пропущено</value>
</data> </data>
<data name="menuGenGroupServer" xml:space="preserve"> <data name="menuGenGroupServer" xml:space="preserve">
<value>Сгенерировать группу политик</value> <value>Сгенерировать группу политик</value>
@@ -1747,7 +1756,7 @@
<value>Привязать интерфейс</value> <value>Привязать интерфейс</value>
</data> </data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve"> <data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value> <value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN</value>
</data> </data>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>Общий ключ (PSK)</value> <value>Общий ключ (PSK)</value>
@@ -1770,4 +1779,85 @@
<data name="menuNewUpdate" xml:space="preserve"> <data name="menuNewUpdate" xml:space="preserve">
<value>Доступно обновление</value> <value>Доступно обновление</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<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>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>Конфликт между {0} и {1}</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>Включить финальную фрагментацию (Final Fragment)</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS через Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<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>
</data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<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>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Размер пакета Gecko (мин/макс)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<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>
</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> </root>

View File

@@ -1041,6 +1041,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>拥塞控制算法</value> <value>拥塞控制算法</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP 请求头</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>自定义 HTTP 出站请求头,请输入值为字符串或字符串数组的 JSON 对象。</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>前置代理配置别名</value> <value>前置代理配置别名</value>
</data> </data>
@@ -1453,7 +1459,7 @@
<value>自定义 DNS 已启用,此页面配置将无效</value> <value>自定义 DNS 已启用,此页面配置将无效</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>开启后将阻止 ECH 和 HTTP/3 可用性查询</value> <value>开启后将阻止 ECH 和 HTTP/3 可用性查询。Xray 固定开启</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>请填写正确的配置模板</value> <value>请填写正确的配置模板</value>
@@ -1492,7 +1498,7 @@
<value>选择配置</value> <value>选择配置</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value> <value>默认全局生效,仅在 sing-box 中内置 FakeIP 过滤。</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>请至少添加一个配置</value> <value>请至少添加一个配置</value>
@@ -1768,7 +1774,11 @@
<value>有更新</value> <value>有更新</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve"> <data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 </value> <value>当前节点使用未加密连接,您的通信可能会被威权政府掌控的网络中间设施直接查看
为了安全,此类节点无法通过 26.2.6+ 版本的Xray核心连接
如果是自建节点请启用 TLS 等安全加密,或固定证书 pinSHA256
如果机场节点请联系服务商完成技术升级,如服务商拒绝配合,建议更换更重视用户安全的服务商
更多的信息,请访问 https://github.com/2dust/v2rayN/discussions/9460</value>
</data> </data>
<data name="TbVerifyPeerCertByName" xml:space="preserve"> <data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value> <value>Verify Peer Cert By Name</value>
@@ -1803,6 +1813,9 @@
<data name="InvalidHy2RealmUrl" xml:space="preserve"> <data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Realm URL 不正确。</value> <value>Realm URL 不正确。</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>请填写合法的HTTP请求头JSON。</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve"> <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> <value>格式realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data> </data>
@@ -1818,4 +1831,43 @@
<data name="TbRootCertificateProviderTip" xml:space="preserve"> <data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>仅用于 v2rayN 界面程序的下载及网络请求,不影响核心的证书验证。</value> <value>仅用于 v2rayN 界面程序的下载及网络请求,不影响核心的证书验证。</value>
</data> </data>
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
<value>连接代理解析策略</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>添加自定义出站</value>
</data>
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
<value>自定义出站 {0} 的文件未找到:{1}</value>
</data>
<data name="TbCustomOutboundTip" xml:space="preserve">
<value>仅支持 xray/sing-box 的单个 outbound/endpoin</value>
</data>
<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> </root>

View File

@@ -1041,6 +1041,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>擁塞控制算法</value> <value>擁塞控制算法</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP 請求頭</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>自訂 HTTP 出站請求頭,請輸入值為字串或字串陣列的 JSON 物件。</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>前置代理節點別名</value> <value>前置代理節點別名</value>
</data> </data>
@@ -1411,7 +1417,7 @@
<value>新增 [Anytls] 節點</value> <value>新增 [Anytls] 節點</value>
</data> </data>
<data name="TbRemoteDNS" xml:space="preserve"> <data name="TbRemoteDNS" xml:space="preserve">
<value>遠 DNS</value> <value>遠 DNS</value>
</data> </data>
<data name="TbDomesticDNS" xml:space="preserve"> <data name="TbDomesticDNS" xml:space="preserve">
<value>直連 DNS</value> <value>直連 DNS</value>
@@ -1423,16 +1429,16 @@
<value>代理目標解析策略</value> <value>代理目標解析策略</value>
</data> </data>
<data name="TbAddCommonDNSHosts" xml:space="preserve"> <data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>新增常用 DNS Hosts</value> <value>新增常用 DNS 主機對應</value>
</data> </data>
<data name="TbFakeIP" xml:space="preserve"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueries" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueries" xml:space="preserve">
<value>阻止 SVCB HTTPS 查詢</value> <value>封鎖 SVCB HTTPS 查詢</value>
</data> </data>
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts網域名稱1 ip1 ip2” 一行一個</value> <value>DNS 主機對應:(每行一組「網域名稱1 ip1 ip2</value>
</data> </data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>DNS 基礎設定</value> <value>DNS 基礎設定</value>
@@ -1441,49 +1447,49 @@
<value>DNS 進階設定</value> <value>DNS 進階設定</value>
</data> </data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve"> <data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>校驗相應地區域名 IP</value> <value>驗證區域網域名稱的 IP</value>
</data> </data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve"> <data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>配置後,會對相應地區域名(如 geosite:cn - geoip:cn的返回 IP 進行校驗,僅返回期望 IP</value> <value>設定後,系統會驗證區域網域名稱(例如 geosite:cn - geoip:cn所傳回的 IP並只傳回預期的 IP</value>
</data> </data>
<data name="TbCustomDNSEnable" xml:space="preserve"> <data name="TbCustomDNSEnable" xml:space="preserve">
<value>啟用自訂 DNS</value> <value>啟用自訂 DNS</value>
</data> </data>
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve"> <data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>自訂 DNS 已啟用,此頁面配置將無效</value> <value>已啟用自訂 DNS,此頁面的設定將不會生效</value>
</data> </data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve"> <data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>開啟後將阻止 ECH HTTP/3 可用性查詢</value> <value>啟用後將封鎖 ECH HTTP/3 可用性查詢。Xray 固定啟用</value>
</data> </data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve"> <data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>請填寫正確的配置範本</value> <value>請填寫正確的設定範本</value>
</data> </data>
<data name="menuFullConfigTemplate" xml:space="preserve"> <data name="menuFullConfigTemplate" xml:space="preserve">
<value>完整配置範本設定</value> <value>完整設定範本</value>
</data> </data>
<data name="TbFullConfigTemplateEnable" xml:space="preserve"> <data name="TbFullConfigTemplateEnable" xml:space="preserve">
<value>啟用完整配置範本</value> <value>啟用完整設定範本</value>
</data> </data>
<data name="TbRayFullConfigTemplate" xml:space="preserve"> <data name="TbRayFullConfigTemplate" xml:space="preserve">
<value>v2ray 完整配置範本</value> <value>v2ray 完整設定範本</value>
</data> </data>
<data name="TbRayFullConfigTemplateDesc" xml:space="preserve"> <data name="TbRayFullConfigTemplateDesc" xml:space="preserve">
<value>僅添加出站配置,routing.balancers routing.rules.outboundTag,點擊查看文檔</value> <value>僅新增出站設定、routing.balancers routing.rules.outboundTag。點選以查看說明文件</value>
</data> </data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve"> <data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>不添加非代理協定出站</value> <value>不新增非代理協定出站</value>
</data> </data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve"> <data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>設定上游代理 tag</value> <value>設定上游代理標籤</value>
</data> </data>
<data name="TbSBFullConfigTemplate" xml:space="preserve"> <data name="TbSBFullConfigTemplate" xml:space="preserve">
<value>sing-box 完整配置範本</value> <value>sing-box 完整設定範本</value>
</data> </data>
<data name="TbSBFullConfigTemplateDesc" xml:space="preserve"> <data name="TbSBFullConfigTemplateDesc" xml:space="preserve">
<value>僅添加出站端點配置,點擊查看文檔</value> <value>僅新增出站端點設定。點選以查看說明文件</value>
</data> </data>
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>此功能供高級用戶和有特殊需求的用戶使用。 啟用此功能後,將忽略 Core 基礎設定DNS 設定 路由設定。你需要保證系統代理的埠和流量統計等功能的配置正確,一切都由你來設定。</value> <value>此功能適合進階使用者與有特殊需求的使用者。啟用後,Core 基礎設定DNS 設定路由設定將被忽略。請確認系統代理連接埠、流量統計及其他相關設定皆正確;所有項目都必須由您自行設定。</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve"> <data name="MsgStartParsingSubscription" xml:space="preserve">
<value>開始解析和處理訂閱內容</value> <value>開始解析和處理訂閱內容</value>
@@ -1492,10 +1498,10 @@
<value>選擇節點</value> <value>選擇節點</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>默認全域生效,內置 FakeIP 過濾,僅在 sing-box 中生效</value> <value>預設會套用至全域;內建 FakeIP 過濾僅支援 sing-box</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>請至少添加一個節點</value> <value>請至少新增一個設定檔</value>
</data> </data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve"> <data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>策略組</value> <value>策略組</value>
@@ -1621,7 +1627,7 @@
<value>提供過期快取Serve Stale</value> <value>提供過期快取Serve Stale</value>
</data> </data>
<data name="TbParallelQuery" xml:space="preserve"> <data name="TbParallelQuery" xml:space="preserve">
<value>行查詢</value> <value>行查詢</value>
</data> </data>
<data name="TbDomesticDNSTips" xml:space="preserve"> <data name="TbDomesticDNSTips" xml:space="preserve">
<value>預設僅在路由期間進行解析時調用</value> <value>預設僅在路由期間進行解析時調用</value>
@@ -1768,7 +1774,11 @@
<value>有更新</value> <value>有更新</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve"> <data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。</value> <value>目前節點使用未加密連接,您的通訊可能會被威權政府掌控的網路中間設施直接查看
為了安全,此類節點無法透過 26.2.6+ 版本的Xray核心連接
如果是自建節點請啟用 TLS 等安全加密,或固定憑證 pinSHA256
若機場節點請聯絡服務商完成技術升級,如服務商拒絕配合,建議更換更重視用戶安全的服務商
更多的信息,請訪問 https://github.com/2dust/v2rayN/discussions/9460</value>
</data> </data>
<data name="TbVerifyPeerCertByName" xml:space="preserve"> <data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value> <value>Verify Peer Cert By Name</value>
@@ -1794,7 +1804,61 @@
<data name="TbEnabletDnsViaProxy" xml:space="preserve"> <data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS 透過 Bridge</value> <value>DNS 透過 Bridge</value>
</data> </data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<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>
</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>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko 封包大小 (min/max)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve"> <data name="TbLegacyProtectTip" xml:space="preserve">
<value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value> <value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>請填寫合法的HTTP請求頭JSON。</value>
</data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>根憑證提供者</value>
</data>
<data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>僅適用於 v2rayN GUI 的下載與網路請求,不影響核心的憑證驗證</value>
</data>
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
<value>連線代理解析策略</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>新增自訂出站</value>
</data>
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
<value>自訂出站 {0} 的檔案未找到:{1}</value>
</data>
<data name="TbCustomOutboundTip" xml:space="preserve">
<value>僅支援 xray/sing-box 的單一 outbound/endpoint</value>
</data>
<data name="LvCustomCoreType" xml:space="preserve">
<value>自訂設定核心</value>
</data>
</root> </root>

View File

@@ -1,27 +1,28 @@
{ {
"tag": "tun", "tag": "tun",
"protocol": "tun", "protocol": "tun",
"settings": { "settings": {
"name": "xray_tun", "name": "xray_tun",
"MTU": 9000, "MTU": 9000,
"gateway": [ "gateway": [
"172.18.0.1/30", "172.18.0.1/30",
"fdfe:dcba:9876::1/126" "fdfe:dcba:9876::1/126"
], ],
"dns": [ "dns": [
"172.18.0.1" "1.1.1.1",
], "8.8.8.8"
"autoSystemRoutingTable": [ ],
"0.0.0.0/0", "autoSystemRoutingTable": [
"::/0" "0.0.0.0/0",
], "::/0"
"autoOutboundsInterface": "auto" ],
}, "autoOutboundsInterface": "auto"
"sniffing": { },
"enabled": true, "sniffing": {
"destOverride": [ "enabled": true,
"http", "destOverride": [
"tls" "http",
] "tls"
} ]
}
} }

View File

@@ -66,6 +66,11 @@ kill_children "$PID"
echo "Terminating main process: $PID" echo "Terminating main process: $PID"
kill -9 "$PID" 2>/dev/null || true kill -9 "$PID" 2>/dev/null || true
# Wait a little for process/port resources to be fully released
FINAL_WAIT_SECONDS=1
echo "Waiting ${FINAL_WAIT_SECONDS}s for resources to settle..."
sleep "$FINAL_WAIT_SECONDS"
echo "============================================" echo "============================================"
echo "Process $PID and all its children have been terminated" echo "Process $PID and all its children have been terminated"
echo "============================================" echo "============================================"

View File

@@ -63,6 +63,11 @@ kill_descendants "$PID"
echo "Terminating main process: $PID" echo "Terminating main process: $PID"
kill -9 "$PID" 2>/dev/null || true kill -9 "$PID" 2>/dev/null || true
# Wait a little for process/port resources to be fully released
FINAL_WAIT_SECONDS=1
echo "Waiting ${FINAL_WAIT_SECONDS}s for resources to settle..."
sleep "$FINAL_WAIT_SECONDS"
echo "============================================" echo "============================================"
echo "Process $PID and all its descendants have been terminated" echo "Process $PID and all its descendants have been terminated"
echo "============================================" echo "============================================"

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
[ [
{ {
"network": "udp", "network": [
"udp"
],
"port": [ "port": [
135, 135,
137, 137,

View File

@@ -10,7 +10,10 @@
<PackageReference Include="ReactiveUI"> <PackageReference Include="ReactiveUI">
<TreatAsUsed>true</TreatAsUsed> <TreatAsUsed>true</TreatAsUsed>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI.Fody" /> <PackageReference Include="ReactiveUI.SourceGenerators">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="sqlite-net-e" /> <PackageReference Include="sqlite-net-e" />
<PackageReference Include="Repobot.SQLite.Unofficial" /> <PackageReference Include="Repobot.SQLite.Unofficial" />
<PackageReference Include="NLog" /> <PackageReference Include="NLog" />

View File

@@ -1,9 +1,14 @@
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace ServiceLib.Services.CoreConfig; namespace ServiceLib.Services.CoreConfig;
/// <summary> /// <summary>
/// Core configuration file processing class /// Core configuration file processing class.
/// The TUN state is taken as a snapshot so the generated config always agrees
/// with the launch elevation decision (see CoreManager.ShouldRunAsSudo).
/// </summary> /// </summary>
public class CoreConfigClashService(Config config) public class CoreConfigClashService(Config config, bool isTunEnabled)
{ {
private static readonly string _tag = "CoreConfigClashService"; private static readonly string _tag = "CoreConfigClashService";
@@ -102,7 +107,7 @@ public class CoreConfigClashService(Config config)
} }
//enable tun mode //enable tun mode
if (config.TunModeItem.EnableTun) if (isTunEnabled)
{ {
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml); var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
if (tun.IsNotEmpty()) if (tun.IsNotEmpty())
@@ -125,7 +130,27 @@ public class CoreConfigClashService(Config config)
Logging.SaveLog($"{_tag}-Mixin", ex); Logging.SaveLog($"{_tag}-Mixin", ex);
} }
// Mihomo parses plain values such as 815458e4 as floats, so quote REALITY short IDs.
var originalRealityShortIds = new List<(Dictionary<object, object> RealityOptions, string ShortId)>();
if (fileContent.GetValueOrDefault("proxies") is List<object> proxies)
{
foreach (var proxy in proxies.OfType<Dictionary<object, object>>())
{
if (proxy.GetValueOrDefault("reality-opts") is Dictionary<object, object> realityOptions
&& realityOptions.GetValueOrDefault("short-id") is string shortId
&& !shortId.StartsWith(tagYamlStr2, StringComparison.Ordinal))
{
originalRealityShortIds.Add((realityOptions, shortId));
realityOptions["short-id"] = new YamlScalarNode(shortId) { Style = ScalarStyle.DoubleQuoted };
}
}
}
var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3); var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3);
foreach (var (realityOptions, shortId) in originalRealityShortIds)
{
realityOptions["short-id"] = shortId;
}
await File.WriteAllTextAsync(fileName, txtFileNew); await File.WriteAllTextAsync(fileName, txtFileNew);
//check again //check again
if (!File.Exists(fileName)) if (!File.Exists(fileName))
@@ -171,7 +196,7 @@ public class CoreConfigClashService(Config config)
} }
foreach (var item in mixinContent) foreach (var item in mixinContent)
{ {
if (!config.TunModeItem.EnableTun && item.Key == "tun") if (!isTunEnabled && item.Key == "tun")
{ {
continue; continue;
} }

View File

@@ -57,13 +57,10 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ConvertGeo2Ruleset(); ConvertGeo2Ruleset();
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = ApplyFullConfigTemplate(); ret.Data = ApplyFinalConfigModifiers();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@@ -107,7 +104,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
foreach (var it in selecteds) foreach (var it in selecteds)
{ {
if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType())) if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType() || it.ConfigType is EConfigType.Outbound))
{ {
continue; continue;
} }
@@ -174,7 +171,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ApplyOutboundBindInterface(); ApplyOutboundBindInterface();
ApplyOutboundSendThrough(); ApplyOutboundSendThrough();
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig); ret.Data = ApplyCustomOutboundReplace();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@@ -236,7 +233,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig); ret.Data = ApplyCustomOutboundReplace();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -2,54 +2,138 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private string ApplyFullConfigTemplate() private string ApplyFinalConfigModifiers()
{
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
var coreConfigContent = ApplyCustomOutboundReplace();
return ApplyFullConfigTemplate(coreConfigContent);
}
private string ApplyCustomOutboundReplace()
{
var coreConfigContent = JsonUtils.Serialize(_coreConfig);
if (context.CustomOutboundMap.Count == 0)
{
return coreConfigContent;
}
var coreConfigNode = JsonNode.Parse(coreConfigContent) as JsonObject;
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
ReplaceCustomOutbounds(_coreConfig.outbounds, coreConfigOutboundsNode);
coreConfigNode!["outbounds"] = coreConfigOutboundsNode;
var coreConfigEndpointsNode = coreConfigNode?["endpoints"] as JsonArray ?? [];
ReplaceCustomOutbounds(_coreConfig.endpoints, coreConfigEndpointsNode);
if (coreConfigEndpointsNode.Count > 0)
{
coreConfigNode!["endpoints"] = coreConfigEndpointsNode;
}
else
{
coreConfigNode?.Remove("endpoints");
}
return JsonUtils.Serialize(coreConfigNode);
void ReplaceCustomOutbounds(IReadOnlyList<BaseServer4Sbox>? source, JsonArray jsonArrayOutbounds)
{
foreach (var outbound in source ?? [])
{
if (!context.CustomOutboundMap.TryGetValue(outbound, out var customOutboundIndex))
{
continue;
}
var outboundTag = outbound.tag;
var outboundDetour = outbound.detour ?? string.Empty;
var outboundBindInterface = outbound.bind_interface ?? string.Empty;
var customOutboundContent = context.CustomOutboundContent[customOutboundIndex];
var containTagPlaceholder = customOutboundContent.Contains("{{tag}}");
var containDetourPlaceholder = customOutboundContent.Contains("{{detour}}");
var containBindInterfacePlaceholder = customOutboundContent.Contains("{{interface}}");
customOutboundContent = customOutboundContent.Replace("{{tag}}", outboundTag);
customOutboundContent = customOutboundContent.Replace("{{detour}}", outboundDetour);
customOutboundContent = customOutboundContent.Replace("{{interface}}", outboundBindInterface);
var customOutboundObj = JsonUtils.ParseJson(customOutboundContent) as JsonObject;
if (!containTagPlaceholder)
{
customOutboundObj?["tag"] = outboundTag;
}
if (!containDetourPlaceholder && !outboundDetour.IsNullOrEmpty())
{
customOutboundObj?["detour"] = outboundDetour;
}
else if (outboundDetour.IsNullOrEmpty())
{
customOutboundObj?.Remove("detour");
}
if (!containBindInterfacePlaceholder && !outboundBindInterface.IsNullOrEmpty())
{
customOutboundObj?["bind_interface"] = outboundBindInterface;
}
var index = jsonArrayOutbounds
.Select((node, idx) => new { node, idx })
.FirstOrDefault(x => x.node?["tag"]?.ToString() == outboundTag)?.idx ?? -1;
if (index != -1)
{
jsonArrayOutbounds[index] = customOutboundObj;
}
}
}
}
private string ApplyFullConfigTemplate(string coreConfigContent)
{ {
var fullConfigTemplate = context.FullConfigTemplate; var fullConfigTemplate = context.FullConfigTemplate;
if (fullConfigTemplate is not { Enabled: true }) if (fullConfigTemplate is not { Enabled: true })
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config; var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
if (fullConfigTemplateItem.IsNullOrEmpty()) if (fullConfigTemplateItem.IsNullOrEmpty())
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem); var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
if (fullConfigTemplateNode == null) if (fullConfigTemplateNode == null)
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
// Process outbounds // Process outbounds
var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? []; var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
foreach (var outbound in _coreConfig.outbounds) var coreConfigNode = JsonNode.Parse(coreConfigContent);
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
foreach (var outbound in coreConfigOutboundsNode)
{ {
if (outbound.type.ToLower() is "direct" or "block") if (outbound["type"]?.ToString()?.ToLower() is "direct" or "block")
{ {
if (fullConfigTemplate.AddProxyOnly == true) if (fullConfigTemplate.AddProxyOnly == true)
{ {
continue; continue;
} }
} }
else if (outbound.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound.server ?? string.Empty)) if (outbound["detour"] is null && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound["server"]?.ToString() ?? string.Empty))
{ {
outbound.detour = fullConfigTemplate.ProxyDetour; outbound["detour"] = fullConfigTemplate.ProxyDetour;
} }
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound)); customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
} }
fullConfigTemplateNode["outbounds"] = customOutboundsNode; fullConfigTemplateNode["outbounds"] = customOutboundsNode;
// Process endpoints // Process endpoints
if (_coreConfig.endpoints is { Count: > 0 }) var coreConfigEndpointsNode = coreConfigNode?["endpoints"] as JsonArray ?? [];
if (coreConfigEndpointsNode is { Count: > 0 })
{ {
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? []; var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
foreach (var endpoint in _coreConfig.endpoints) foreach (var endpoint in coreConfigEndpointsNode)
{ {
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) if (endpoint["detour"] is null && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
{ {
endpoint.detour = fullConfigTemplate.ProxyDetour; endpoint["detour"] = fullConfigTemplate.ProxyDetour;
} }
customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint)); customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint));
} }

View File

@@ -137,12 +137,12 @@ public partial class CoreConfigSingboxService
// fake ip // fake ip
if (simpleDnsItem.FakeIP == true) if (simpleDnsItem.FakeIP == true)
{ {
var fakeipRange = simpleDnsItem.FakeIPRange.IsNullOrEmpty() ? Global.FakeIPRanges.First() : simpleDnsItem.FakeIPRange;
var fakeip = new Server4Sbox var fakeip = new Server4Sbox
{ {
tag = Global.SingboxFakeDNSTag, tag = Global.SingboxFakeDNSTag,
type = "fakeip", type = "fakeip",
inet4_range = "198.18.0.0/15", inet4_range = fakeipRange,
inet6_range = "fc00::/18",
}; };
_coreConfig.dns.servers.Add(fakeip); _coreConfig.dns.servers.Add(fakeip);
} }
@@ -171,7 +171,7 @@ public partial class CoreConfigSingboxService
_coreConfig.dns.rules.Add(new() _coreConfig.dns.rules.Add(new()
{ {
server = Global.SingboxDirectDNSTag, server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom), strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4ProxyDial),
domain = context.ProtectDomainList.ToList(), domain = context.ProtectDomainList.ToList(),
}); });
} }
@@ -266,7 +266,7 @@ public partial class CoreConfigSingboxService
}); });
} }
if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp == true) if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp != false)
{ {
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName)); var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
fakeipFilterRule.invert = true; fakeipFilterRule.invert = true;
@@ -282,13 +282,23 @@ public partial class CoreConfigSingboxService
{ {
query_type = [1, 28], // A and AAAA query_type = [1, 28], // A and AAAA
}, },
fakeipFilterRule fakeipFilterRule,
] ],
}; };
_coreConfig.dns.rules.Add(rule4Fake); _coreConfig.dns.rules.Add(rule4Fake);
} }
if (simpleDnsItem.BlockAAAAQuery == true)
{
_coreConfig.dns.rules.Add(new()
{
query_type = [28],
action = "predefined",
rcode = "NOERROR",
});
}
var routing = context.RoutingItem; var routing = context.RoutingItem;
if (routing == null) if (routing == null)
{ {

View File

@@ -21,26 +21,27 @@ public partial class CoreConfigSingboxService
}; };
_coreConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
var inboundConf = _config.Inbound.First();
inbound.listen_port = listenPort; inbound.listen_port = listenPort;
if (_config.Inbound.First().SecondLocalPortEnabled) if (inboundConf.SecondLocalPortEnabled)
{ {
var inbound2 = BuildInbound(inbound, EInboundProtocol.socks2, true); var inbound2 = BuildInbound(inbound, EInboundProtocol.socks2, true);
_coreConfig.inbounds.Add(inbound2); _coreConfig.inbounds.Add(inbound2);
} }
if (_config.Inbound.First().AllowLANConn) if (inboundConf.AllowLANConn)
{ {
if (_config.Inbound.First().NewPort4LAN) if (inboundConf.NewPort4LAN)
{ {
var inbound3 = BuildInbound(inbound, EInboundProtocol.socks3, true); var inbound3 = BuildInbound(inbound, EInboundProtocol.socks3, true);
inbound3.listen = listen; inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3); _coreConfig.inbounds.Add(inbound3);
//auth //auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) if (inboundConf.User.IsNotEmpty() && inboundConf.Pass.IsNotEmpty())
{ {
inbound3.users = new() { new() { username = _config.Inbound.First().User, password = _config.Inbound.First().Pass } }; inbound3.users = new() { new() { username = inboundConf.User, password = inboundConf.Pass } };
} }
} }
else else
@@ -67,9 +68,13 @@ public partial class CoreConfigSingboxService
tunInbound.auto_route = _config.TunModeItem.AutoRoute; tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute; tunInbound.strict_route = _config.TunModeItem.StrictRoute;
tunInbound.stack = _config.TunModeItem.Stack; tunInbound.stack = _config.TunModeItem.Stack;
if (_config.TunModeItem.EnableIPv6Address == false)
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.address = [address];
if (_config.TunModeItem.EnableIPv6Address == true)
{ {
tunInbound.address = ["172.18.0.1/30"]; var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
tunInbound.address.Add(address6);
} }
tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress; tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress;

View File

@@ -11,7 +11,7 @@ public partial class CoreConfigSingboxService
private List<BaseServer4Sbox> BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag, bool withSelector = true) private List<BaseServer4Sbox> BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag, bool withSelector = true)
{ {
var proxyOutboundList = new List<BaseServer4Sbox>(); var proxyOutboundList = new List<BaseServer4Sbox>();
if (!_node.ConfigType.IsComplexType()) if (!_node.ConfigType.IsGroupType())
{ {
var outbound = BuildProxyOutbound(baseTagName); var outbound = BuildProxyOutbound(baseTagName);
proxyOutboundList.Add(outbound); proxyOutboundList.Add(outbound);
@@ -35,6 +35,10 @@ public partial class CoreConfigSingboxService
{ {
var outbound = BuildProxyServer(); var outbound = BuildProxyServer();
outbound.tag = baseTagName; outbound.tag = baseTagName;
if (_node.ConfigType == EConfigType.Outbound)
{
context.CustomOutboundMap[outbound] = _node.IndexId;
}
return outbound; return outbound;
} }
@@ -59,6 +63,20 @@ public partial class CoreConfigSingboxService
try try
{ {
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (_node.ConfigType == EConfigType.Outbound)
{
if (_node.GetProtocolExtra().IsSingboxEndpoint == true)
{
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
return endpoint;
}
else
{
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
return outbound;
}
}
if (_node.ConfigType == EConfigType.WireGuard) if (_node.ConfigType == EConfigType.WireGuard)
{ {
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound); var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
@@ -417,11 +435,15 @@ public partial class CoreConfigSingboxService
var tls = new Tls4Sbox() var tls = new Tls4Sbox()
{ {
enabled = true, enabled = true,
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
server_name = serverName, server_name = serverName,
insecure = _node.GetAllowInsecure(), insecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(), alpn = _node.GetAlpn(),
}; };
if (_config.CoreBasicItem.EnableFragment == true)
{
tls.fragment = true;
tls.record_fragment = true;
}
if (_node.Fingerprint.IsNotEmpty()) if (_node.Fingerprint.IsNotEmpty())
{ {
tls.utls = new Utls4Sbox() tls.utls = new Utls4Sbox()
@@ -589,7 +611,7 @@ public partial class CoreConfigSingboxService
{ {
type = "selector", type = "selector",
tag = baseTagName, tag = baseTagName,
outbounds = JsonUtils.DeepCopy(proxyTags), outbounds = [.. proxyTags],
interrupt_exist_connections = false, interrupt_exist_connections = false,
}; };
outSelector.outbounds.Insert(0, outUrltest.tag); outSelector.outbounds.Insert(0, outUrltest.tag);
@@ -717,9 +739,9 @@ public partial class CoreConfigSingboxService
return resultOutbounds; return resultOutbounds;
} }
private static List<BaseServer4Sbox> CloneOutbounds(List<BaseServer4Sbox> source) private List<BaseServer4Sbox> CloneOutbounds(List<BaseServer4Sbox> source)
{ {
if (source is null || source.Count == 0) if (source is not { Count: > 0 })
{ {
return []; return [];
} }
@@ -736,9 +758,14 @@ public partial class CoreConfigSingboxService
{ {
clone = JsonUtils.DeepCopy(endpoint); clone = JsonUtils.DeepCopy(endpoint);
} }
if (clone is not null) if (clone is null)
{ {
result.Add(clone); continue;
}
result.Add(clone);
if (context.CustomOutboundMap.ContainsKey(item))
{
context.CustomOutboundMap[clone] = context.CustomOutboundMap[item];
} }
} }
return result; return result;

View File

@@ -10,18 +10,27 @@ public partial class CoreConfigSingboxService
var simpleDnsItem = context.SimpleDnsItem; var simpleDnsItem = context.SimpleDnsItem;
var defaultDomainResolverTag = Global.SingboxDirectDNSTag; var defaultDomainResolverTag = Global.SingboxDirectDNSTag;
var directDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom); var dialDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4ProxyDial);
var rawDNSItem = context.RawDnsItem; var rawDNSItem = context.RawDnsItem;
if (rawDNSItem is { Enabled: true }) if (rawDNSItem is { Enabled: true })
{ {
defaultDomainResolverTag = Global.SingboxLocalDNSTag; defaultDomainResolverTag = Global.SingboxLocalDNSTag;
directDnsStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? null : rawDNSItem.DomainStrategy4Freedom; dialDnsStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? null : rawDNSItem.DomainStrategy4Freedom;
}
else if (!simpleDnsItem.Strategy4Freedom.IsNullOrEmpty())
{
var directOutbound = _coreConfig.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag);
directOutbound?.domain_resolver = new()
{
server = defaultDomainResolverTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
};
} }
_coreConfig.route.default_domain_resolver = new() _coreConfig.route.default_domain_resolver = new()
{ {
server = defaultDomainResolverTag, server = defaultDomainResolverTag,
strategy = directDnsStrategy strategy = dialDnsStrategy
}; };
if (context.IsTunEnabled) if (context.IsTunEnabled)
@@ -34,19 +43,44 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.AddRange(tunRules); _coreConfig.route.rules.AddRange(tunRules);
} }
var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe(); // Traffic addressed to the TUN interface's own addresses must never reach an
_coreConfig.route.rules.Add(new() // outbound. auto_route hijacks the default route, so `direct` writes such a
// 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)
{ {
port = [53], _coreConfig.route.rules.Add(new()
action = "hijack-dns", {
process_name = lstDnsExe ip_cidr = [.. tunAddresses.Select(ToSingleAddressPrefix)],
}); action = "reject",
method = "drop",
});
}
_coreConfig.route.rules.Add(new() var lstDirectExe = BuildRoutingDirectExe();
if (lstDirectExe.Count > 0)
{ {
outbound = Global.DirectTag, _coreConfig.route.rules.Add(new()
process_name = lstDirectExe {
}); port = [53],
action = "hijack-dns",
process_path = lstDirectExe,
});
_coreConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
process_path = lstDirectExe,
});
}
// ICMP Routing // ICMP Routing
var icmpRouting = _config.TunModeItem.IcmpRouting ?? ""; var icmpRouting = _config.TunModeItem.IcmpRouting ?? "";
@@ -256,34 +290,46 @@ public partial class CoreConfigSingboxService
} }
} }
private static (List<string> lstDnsExe, List<string> lstDirectExe) BuildRoutingDirectExe() 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 dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo(); var allCoreInfo = CoreInfoManager.Instance.GetCoreInfo();
foreach (var coreConfig in coreInfoResult) foreach (var coreConfig in allCoreInfo)
{ {
if (!context.ProtectCoreTypeList.Contains(coreConfig.CoreType))
{
continue;
}
if (coreConfig.CoreType == ECoreType.v2rayN) if (coreConfig.CoreType == ECoreType.v2rayN)
{ {
continue; continue;
} }
if (coreConfig.CoreExes == null)
{
continue;
}
foreach (var baseExeName in coreConfig.CoreExes) foreach (var baseExeName in coreConfig.CoreExes)
{ {
if (coreConfig.CoreType != ECoreType.sing_box) //directExeSet.Add(Utils.GetExeName(baseExeName));
var exePath = CoreInfoManager.Instance.GetCoreExecFile(coreConfig, out _);
if (!exePath.IsNullOrEmpty())
{ {
dnsExeSet.Add(Utils.GetExeName(baseExeName)); directExeSet.Add(exePath);
} }
directExeSet.Add(Utils.GetExeName(baseExeName));
} }
} }
var lstDnsExe = new List<string>(dnsExeSet); return directExeSet.ToList();
var lstDirectExe = new List<string>(directExeSet);
return (lstDnsExe, lstDirectExe);
} }
private void GenRoutingUserRule(RulesItem? item) private void GenRoutingUserRule(RulesItem? item)
@@ -542,7 +588,8 @@ public partial class CoreConfigSingboxService
if (node == null if (node == null
|| (!Global.SingboxSupportConfigType.Contains(node.ConfigType) || (!Global.SingboxSupportConfigType.Contains(node.ConfigType)
&& !node.ConfigType.IsGroupType())) && !node.ConfigType.IsGroupType()
&& node.ConfigType is not EConfigType.Outbound))
{ {
return Global.ProxyTag; return Global.ProxyTag;
} }

View File

@@ -64,8 +64,6 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{ {
ApplyFinalFragment(); ApplyFinalFragment();
} }
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
var finalRule = BuildFinalRule(); var finalRule = BuildFinalRule();
if (!string.IsNullOrEmpty(finalRule?.balancerTag)) if (!string.IsNullOrEmpty(finalRule?.balancerTag))
@@ -75,7 +73,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = ApplyFullConfigTemplate(); ret.Data = ApplyFinalConfigModifiers();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@@ -119,7 +117,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
foreach (var it in selecteds) foreach (var it in selecteds)
{ {
if (!(Global.XraySupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType())) if (!(Global.XraySupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType() || it.ConfigType is EConfigType.Outbound))
{ {
continue; continue;
} }
@@ -216,7 +214,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
ApplyOutboundSendThrough(); ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary()); //ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig); ret.Data = ApplyCustomOutboundReplace();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@@ -293,7 +291,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig); ret.Data = ApplyCustomOutboundReplace();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -96,18 +96,19 @@ public partial class CoreConfigV2rayService
var balancer = new BalancersItem4Ray var balancer = new BalancersItem4Ray
{ {
selector = [selector], selector = [selector],
strategy = strategyType == "leastLoad" ? new() strategy = new()
{ {
type = strategyType, type = strategyType,
settings = new() settings = strategyType == "leastLoad"
{ ? new()
expected = 1, {
tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null, expected = 1,
maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null, tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null,
}, maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null,
} : null, }
: null,
},
tag = balancerTag, tag = balancerTag,
fallbackTag = _coreConfig.outbounds?.FirstOrDefault(o => o.tag.StartsWith(selector))?.tag,
}; };
_coreConfig.routing.balancers ??= []; _coreConfig.routing.balancers ??= [];
_coreConfig.routing.balancers.Add(balancer); _coreConfig.routing.balancers.Add(balancer);

View File

@@ -2,24 +2,105 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private string ApplyFullConfigTemplate() private string ApplyFinalConfigModifiers()
{
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
var coreConfigContent = ApplyCustomOutboundReplace();
return ApplyFullConfigTemplate(coreConfigContent);
}
private string ApplyCustomOutboundReplace()
{
var coreConfigContent = JsonUtils.Serialize(_coreConfig);
if (context.CustomOutboundMap.Count == 0)
{
return coreConfigContent;
}
var coreConfigNode = JsonNode.Parse(coreConfigContent) as JsonObject;
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
foreach (var outbound in _coreConfig.outbounds ?? [])
{
if (!context.CustomOutboundMap.TryGetValue(outbound, out var customOutboundIndex))
{
continue;
}
var outboundTag = outbound.tag;
var outboundDetour = outbound.streamSettings?.sockopt?.dialerProxy ?? string.Empty;
var outboundBindInterface = outbound.streamSettings?.sockopt?.Interface ?? string.Empty;
var customOutboundContent = context.CustomOutboundContent[customOutboundIndex];
var containTagPlaceholder = customOutboundContent.Contains("{{tag}}");
var containDetourPlaceholder = customOutboundContent.Contains("{{detour}}");
var containBindInterfacePlaceholder = customOutboundContent.Contains("{{interface}}");
customOutboundContent = customOutboundContent.Replace("{{tag}}", outboundTag);
customOutboundContent = customOutboundContent.Replace("{{detour}}", outboundDetour);
customOutboundContent = customOutboundContent.Replace("{{interface}}", outboundBindInterface);
var customOutboundObj = JsonUtils.ParseJson(customOutboundContent) as JsonObject;
if (!containTagPlaceholder)
{
customOutboundObj?["tag"] = outboundTag;
}
if (!containDetourPlaceholder && !outboundDetour.IsNullOrEmpty())
{
customOutboundObj!["streamSettings"] ??= new JsonObject();
customOutboundObj["streamSettings"]["sockopt"] ??= new JsonObject();
customOutboundObj["streamSettings"]["sockopt"]["dialerProxy"] = outboundDetour;
if (customOutboundObj["streamSettings"]?["xhttpSettings"]?["extra"]?["downloadSettings"] is JsonObject downloadSettings)
{
downloadSettings["sockopt"] ??= new JsonObject();
downloadSettings["sockopt"]["dialerProxy"] = outboundDetour;
}
}
else if (outboundDetour.IsNullOrEmpty())
{
(customOutboundObj?["streamSettings"]?["sockopt"] as JsonObject)?.Remove("dialerProxy");
}
if (!containBindInterfacePlaceholder && !outboundBindInterface.IsNullOrEmpty())
{
customOutboundObj!["streamSettings"] ??= new JsonObject();
customOutboundObj["streamSettings"]["sockopt"] ??= new JsonObject();
customOutboundObj["streamSettings"]["sockopt"]["interface"] = outboundBindInterface;
if (customOutboundObj["streamSettings"]?["xhttpSettings"]?["extra"]?["downloadSettings"] is JsonObject downloadSettings)
{
downloadSettings["sockopt"] ??= new JsonObject();
downloadSettings["sockopt"]["interface"] = outboundBindInterface;
}
}
var index = coreConfigOutboundsNode
.Select((node, idx) => new { node, idx })
.FirstOrDefault(x => x.node?["tag"]?.ToString() == outboundTag)?.idx ?? -1;
if (index != -1)
{
coreConfigOutboundsNode[index] = customOutboundObj;
}
}
return JsonUtils.Serialize(coreConfigNode);
}
private string ApplyFullConfigTemplate(string coreConfigContent)
{ {
var fullConfigTemplate = context.FullConfigTemplate; var fullConfigTemplate = context.FullConfigTemplate;
if (fullConfigTemplate is not { Enabled: true }) if (fullConfigTemplate is not { Enabled: true })
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config; var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
if (fullConfigTemplateItem.IsNullOrEmpty()) if (fullConfigTemplateItem.IsNullOrEmpty())
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem); var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
if (fullConfigTemplateNode == null) if (fullConfigTemplateNode == null)
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
// Handle balancer and rules modifications (for multiple load scenarios) // Handle balancer and rules modifications (for multiple load scenarios)
@@ -74,8 +155,8 @@ public partial class CoreConfigV2rayService
else else
{ {
var subjectSelector = _coreConfig.observatory.subjectSelector; var subjectSelector = _coreConfig.observatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []); subjectSelector?.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList())); fullConfigTemplateNode["observatory"]?["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector?.Distinct().ToList()));
} }
} }
@@ -88,16 +169,18 @@ public partial class CoreConfigV2rayService
else else
{ {
var subjectSelector = _coreConfig.burstObservatory.subjectSelector; var subjectSelector = _coreConfig.burstObservatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []); subjectSelector?.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList())); fullConfigTemplateNode["burstObservatory"]?["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector?.Distinct().ToList()));
} }
} }
var customOutboundsNode = new JsonArray(); var customOutboundsNode = new JsonArray();
foreach (var outbound in _coreConfig.outbounds) var coreConfigNode = JsonNode.Parse(coreConfigContent);
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
foreach (var outbound in coreConfigOutboundsNode)
{ {
if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom") if (outbound?["protocol"]?.ToString()?.ToLower() is "blackhole" or "dns" or "freedom")
{ {
if (fullConfigTemplate.AddProxyOnly == true) if (fullConfigTemplate.AddProxyOnly == true)
{ {
@@ -105,14 +188,22 @@ public partial class CoreConfigV2rayService
} }
} }
else if (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty() else if (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()
&& (outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() ?? true)) && (outbound["streamSettings"]?["sockopt"]?["dialerProxy"].ToString().IsNullOrEmpty() ?? true))
{ {
var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address var outboundAddress = outbound["settings"]?["servers"]?.AsArray()?.FirstOrDefault()?["address"]?.ToString()
?? outbound.settings?.vnext?.FirstOrDefault()?.address ?? outbound["settings"]?["vnext"]?.AsArray()?.FirstOrDefault()?["address"]?.ToString()
?? string.Empty; ?? string.Empty;
if (!Utils.IsPrivateNetwork(outboundAddress)) if (!Utils.IsPrivateNetwork(outboundAddress))
{ {
FillDialerProxy(outbound, fullConfigTemplate.ProxyDetour); //FillDialerProxy(outbound, fullConfigTemplate.ProxyDetour);
outbound["streamSettings"] ??= new JsonObject();
outbound["streamSettings"]["sockopt"] ??= new JsonObject();
outbound["streamSettings"]["sockopt"]["dialerProxy"] = fullConfigTemplate.ProxyDetour;
if (outbound["streamSettings"]?["xhttpSettings"]?["extra"]?["downloadSettings"] is JsonObject downloadSettings)
{
downloadSettings["sockopt"] ??= new JsonObject();
downloadSettings["sockopt"]["dialerProxy"] = fullConfigTemplate.ProxyDetour;
}
} }
} }
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound)); customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));

View File

@@ -28,7 +28,7 @@ public partial class CoreConfigV2rayService
_coreConfig.routing.rules.Add(new RulesItem4Ray _coreConfig.routing.rules.Add(new RulesItem4Ray
{ {
type = "field", type = "field",
inboundTag = new List<string> { Global.DnsTag }, inboundTag = [Global.DnsTag],
outboundTag = Global.ProxyTag, outboundTag = Global.ProxyTag,
}); });
return; return;
@@ -43,11 +43,7 @@ public partial class CoreConfigV2rayService
var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
if (outbound != null) if (outbound != null)
{ {
outbound.settings = new() FillSockoptDomainStrategy(outbound, strategy4Freedom);
{
domainStrategy = strategy4Freedom,
userLevel = 0
};
} }
} }
@@ -64,12 +60,33 @@ public partial class CoreConfigV2rayService
.ForEach(outbound => outbound.targetStrategy = strategy4Proxy); .ForEach(outbound => outbound.targetStrategy = strategy4Proxy);
} }
var strategy4DialProxy = simpleDnsItem?.Strategy4ProxyDial ?? Global.AsIs;
//Outbound DialProxy domainStrategy
if (strategy4DialProxy.IsNotEmpty() && strategy4DialProxy != Global.AsIs)
{
var xraySupportConfigTypeNames = Global.XraySupportConfigType
.Select(x => x == EConfigType.Hysteria2 ? "hysteria" : Global.ProtocolTypes[x])
.ToHashSet();
_coreConfig.outbounds
.Where(t => xraySupportConfigTypeNames.Contains(t.protocol))
.ToList()
.ForEach(outbound =>
{
FillSockoptDomainStrategy(outbound, strategy4DialProxy);
});
}
FillDnsServers(dnsItem); FillDnsServers(dnsItem);
FillDnsHosts(dnsItem); FillDnsHosts(dnsItem);
dnsItem.serveStale = simpleDnsItem?.ServeStale is true ? true : null; dnsItem.serveStale = simpleDnsItem?.ServeStale is true ? true : null;
dnsItem.enableParallelQuery = simpleDnsItem?.ParallelQuery is true ? true : null; dnsItem.enableParallelQuery = simpleDnsItem?.ParallelQuery is true ? true : null;
if (simpleDnsItem.BlockAAAAQuery == true)
{
dnsItem.queryStrategy = "UseIPv4";
}
// DNS routing // DNS routing
var directDnsTags = dnsItem.servers var directDnsTags = dnsItem.servers
.Select(server => .Select(server =>
@@ -108,6 +125,33 @@ public partial class CoreConfigV2rayService
} }
} }
private void GenFakeDns()
{
var fakeipRange = _config.SimpleDNSItem.FakeIPRange.IsNullOrEmpty() ? Global.FakeIPRanges.First() : _config.SimpleDNSItem.FakeIPRange;
var poolSize = 65535L;
try
{
var fakeipNetwork = IPNetwork2.Parse(fakeipRange);
var totalIPs = fakeipNetwork.Total;
// see https://github.com/XTLS/Xray-core/blob/6e3322d219140a025285ded1114fe17a5edb74d8/app/dns/fakedns/fake.go#L88
// if math.Log2(float64(lruSize)) >= float64(rooms) { return errors.New("LRU size is bigger than subnet size").AtError() }
totalIPs -= 1;
if (totalIPs > 0)
{
poolSize = (totalIPs >= long.MaxValue) ? long.MaxValue : (long)totalIPs;
}
}
catch
{
// Ignore
}
_coreConfig.fakedns = new()
{
ipPool = fakeipRange,
poolSize = poolSize,
};
}
private void FillDnsServers(Dns4Ray dnsItem) private void FillDnsServers(Dns4Ray dnsItem)
{ {
var simpleDNSItem = context.SimpleDnsItem; var simpleDNSItem = context.SimpleDnsItem;
@@ -234,6 +278,23 @@ public partial class CoreConfigV2rayService
var directDnsTagIndex = 1; var directDnsTagIndex = 1;
if (simpleDNSItem.FakeIP == true)
{
var fakeIPMatchDomain = new HashSet<string>(proxyDomainList);
fakeIPMatchDomain.UnionWith(proxyGeositeList);
if (simpleDNSItem.GlobalFakeIp != false)
{
fakeIPMatchDomain.UnionWith(directDomainList);
fakeIPMatchDomain.UnionWith(directGeositeList);
fakeIPMatchDomain.UnionWith(expectedDomainList);
}
if (fakeIPMatchDomain.Count > 0)
{
GenFakeDns();
AddDnsServers(["fakedns"], fakeIPMatchDomain.ToList());
}
}
AddDnsServers(remoteDNSAddress, proxyDomainList); AddDnsServers(remoteDNSAddress, proxyDomainList);
AddDnsServers(directDNSAddress, directDomainList, true); AddDnsServers(directDNSAddress, directDomainList, true);
AddDnsServers(remoteDNSAddress, proxyGeositeList); AddDnsServers(remoteDNSAddress, proxyGeositeList);
@@ -384,11 +445,9 @@ public partial class CoreConfigV2rayService
var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
if (outbound != null) if (outbound != null)
{ {
outbound.settings = new() outbound.streamSettings ??= new();
{ outbound.streamSettings.sockopt ??= new();
domainStrategy = domainStrategy4Freedom, outbound.streamSettings.sockopt.domainStrategy = domainStrategy4Freedom;
userLevel = 0,
};
} }
} }
@@ -471,4 +530,28 @@ public partial class CoreConfigV2rayService
}; };
servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer)); servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer));
} }
private void FillSockoptDomainStrategy(Outbounds4Ray outbound, string? domainStrategy, bool skipHappyEyeballs = false)
{
if (domainStrategy.IsNullOrEmpty())
{
return;
}
outbound.streamSettings ??= new();
outbound.streamSettings.sockopt ??= new();
var sockopt = outbound.streamSettings.sockopt;
sockopt.domainStrategy = domainStrategy;
if (skipHappyEyeballs || _config.SimpleDNSItem.EnableHappyEyeballs != true)
{
return;
}
var happyEyeballsItem = _config.HappyEyeballs4RayItem;
sockopt.happyEyeballs ??= new();
var happyEyeballs = sockopt.happyEyeballs;
happyEyeballs.tryDelayMs = happyEyeballsItem.TryDelayMs;
happyEyeballs.prioritizeIPv6 = happyEyeballsItem.PrioritizeIPv6;
happyEyeballs.interleave = happyEyeballsItem.Interleave;
happyEyeballs.maxConcurrentTry = happyEyeballsItem.MaxConcurrentTry;
}
} }

View File

@@ -9,37 +9,38 @@ public partial class CoreConfigV2rayService
var listen = "0.0.0.0"; var listen = "0.0.0.0";
var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
_coreConfig.inbounds = []; _coreConfig.inbounds = [];
var inbound = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks, true); var inboundConf = _config.Inbound.First();
var inbound = BuildInbound(inboundConf, EInboundProtocol.socks, true);
var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort; var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort;
if (!context.IsTunEnabled || !isUsingLocalMixedPort) if (!context.IsTunEnabled || !isUsingLocalMixedPort)
{ {
_coreConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
if (_config.Inbound.First().SecondLocalPortEnabled) if (inboundConf.SecondLocalPortEnabled)
{ {
var inbound2 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks2, true); var inbound2 = BuildInbound(inboundConf, EInboundProtocol.socks2, true);
_coreConfig.inbounds.Add(inbound2); _coreConfig.inbounds.Add(inbound2);
} }
if (_config.Inbound.First().AllowLANConn) if (inboundConf.AllowLANConn)
{ {
if (_config.Inbound.First().NewPort4LAN) if (inboundConf.NewPort4LAN)
{ {
var inbound3 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks3, true); var inbound3 = BuildInbound(inboundConf, EInboundProtocol.socks3, true);
inbound3.listen = listen; inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3); _coreConfig.inbounds.Add(inbound3);
// auth // auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) if (inboundConf.User.IsNotEmpty() && inboundConf.Pass.IsNotEmpty())
{ {
inbound3.settings.auth = "password"; inbound3.settings.auth = "password";
inbound3.settings.accounts = inbound3.settings.accounts =
[ [
new() new()
{ {
user = _config.Inbound.First().User, user = inboundConf.User,
pass = _config.Inbound.First().Pass, pass = inboundConf.Pass,
}, },
]; ];
@@ -63,17 +64,27 @@ public partial class CoreConfigV2rayService
new Inbounds4Ray(); new Inbounds4Ray();
tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun"; tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun";
tunInbound.settings.MTU = _config.TunModeItem.Mtu; tunInbound.settings.MTU = _config.TunModeItem.Mtu;
if (!_config.TunModeItem.EnableIPv6Address)
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.settings.gateway = [address];
// 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)
{ {
tunInbound.settings.gateway = ["172.18.0.1/30"]; var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"]; tunInbound.settings.gateway.Add(address6);
} }
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (!bindInterface.IsNullOrEmpty()) if (!bindInterface.IsNullOrEmpty())
{ {
tunInbound.settings.autoOutboundsInterface = bindInterface; tunInbound.settings.autoOutboundsInterface = bindInterface;
} }
tunInbound.sniffing = inbound.sniffing; tunInbound.sniffing = inbound.sniffing;
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
tunInbound.sniffing.routeOnly = true;
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 }) if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{ {
@@ -110,15 +121,8 @@ public partial class CoreConfigV2rayService
includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList(); includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList();
includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList(); includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList();
if (_config.TunModeItem.EnableIPv6Address) tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
{ .Concat(includeListV6.Select(x => x.ToString())).ToList();
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();
}
} }
_coreConfig.inbounds.Add(tunInbound); _coreConfig.inbounds.Add(tunInbound);
@@ -151,6 +155,16 @@ public partial class CoreConfigV2rayService
inbound.sniffing.destOverride = inItem.DestOverride; inbound.sniffing.destOverride = inItem.DestOverride;
inbound.sniffing.routeOnly = inItem.RouteOnly; inbound.sniffing.routeOnly = inItem.RouteOnly;
if (_config.SimpleDNSItem.FakeIP == true)
{
// Ensure destOverride contains "fakedns" if FakeIP is enabled
inbound.sniffing.destOverride ??= [];
if (!inbound.sniffing.destOverride.Contains("fakedns"))
{
inbound.sniffing.destOverride.Add("fakedns");
}
}
return inbound; return inbound;
} }
} }

Some files were not shown because too many files have changed in this diff Show More