Compare commits

...

142 Commits

Author SHA1 Message Date
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
2dust
3e9f78f831 up 7.23.3 2026-07-09 19:53:43 +08:00
JieXu
8c4bd52389 Update package-rhel.sh (#9710)
* 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-osx.sh
2026-07-09 16:25:39 +08:00
DHR60
fa61fedf11 Add Root Cert Store (#9703) 2026-07-09 16:25:02 +08:00
DHR60
ad12a6c456 Update ca cert (#9700) 2026-07-08 09:48:46 +08:00
2dust
66b48c231f Synchronize GlobalHotKeys library and update HotKeyManager 2026-07-06 16:07:42 +08:00
JieXu
52766d66bf Update build-linux.yml (#9684)
* Update build-linux.yml

* Update package-debian-loong.sh

* Update package-rhel-loong.sh

* Update package-rhel-loong.sh

* Update package-debian-loong.sh
2026-07-06 13:48:05 +08:00
2dust
e1a5c360ac Optimize timeout logic, try fix
https://github.com/2dust/v2rayN/issues/9681
2026-07-06 11:49:54 +08:00
2dust
0c570fcb38 Add Legacy Protect Tip 2026-07-06 10:18:31 +08:00
2dust
a236fab27c up 7.23.2 2026-07-04 16:48:24 +08:00
2dust
f8e1186095 Bug fix and Optimize profile items query with batching and ordering
Prevent SQLite variables limit issue by implementing chunked/batched queries.
2026-07-02 10:35:17 +08:00
Жора Змейкин
ca186e2636 fix: clarify bind interface availability (#9660) 2026-07-02 09:21:59 +08:00
JieXu
1e8ba9ad9b Update README.md (#9654)
* Update README.md

* Update build-linux.yml

* Update build.yml

* Update Directory.Packages.props
2026-07-02 09:20:46 +08:00
DHR60
31e32f174e Fix (#9648) 2026-07-02 09:20:14 +08:00
dr mike
bf376a8fac fix(hysteria2): emit https server_url for sing-box realm outbound (#9642)
sing-box requires realm.server_url as a full URL (https://host:port).
Bare host:port caused "missing host in realm server_url" on import.
Also forward stun_servers and clear ResolveRealm success message.

Fixes #9635
2026-06-28 11:05:41 +08:00
DHR60
5a9a9b7ab1 Wait For Proxy Port (#9640)
Co-authored-by: MrTeeett <mrteeett@gmail.com>
Co-authored-by: drownrat <drownrat@users.noreply.github.com>
2026-06-28 09:44:34 +08:00
dependabot[bot]
e7a9aa66dc Bump actions/setup-dotnet from 5.3.0 to 5.4.0 (#9636)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.3.0 to 5.4.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.3.0...v5.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-27 10:40:34 +08:00
2dust
709b9a0958 up 7.23.1 2026-06-26 17:37:39 +08:00
2dust
191fe48257 Update Directory.Packages.props 2026-06-26 17:37:15 +08:00
2dust
48d58d482a Remove NetBridge 2026-06-26 17:30:28 +08:00
2dust
7100b3f0a5 Try fix
https://github.com/2dust/v2rayN/issues/9593
2026-06-26 10:18:02 +08:00
DHR60
8d94997ba0 xray tun for linux and macos (#9632) 2026-06-26 09:04:44 +08:00
DHR60
c97dc1d894 Fix ui (#9626) 2026-06-25 19:28:21 +08:00
MrArrowww
b198fd8be7 Fix TCPing tests not stopping when ESC is pressed (#9623) 2026-06-25 19:28:04 +08:00
JieXu
e0c1ff24be Update README.md (#9616)
* Update README.md

* Update logo image in README.md

* Update README.md

* Update README.md

* Update README.md

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-23 14:07:43 +08:00
DHR60
3cebf492d1 Fix (#9614) 2026-06-23 13:46:36 +08:00
DHR60
ef46f4e7e6 Hysteria Realm & Gecko (#9516)
* Hysteria Realm

* Core config

* Fix

* Fix

* Add Gecko support
2026-06-22 20:52:47 +08:00
Enqvy
81c118c9b2 customizable packet fragmentation settings (#9597)
* first changes

* fix: use ItemsSource for fragment packets ComboBox so value saves correctly

* feat: add customizable packet fragmentation for Xray-core

- Add fragment settings in Core tab: Packets (dropdown), Length (range), Interval (range), MaxSplit (0-10000)
- Support both 'from-to' (e.g., 50-100) and single value (e.g., 50) formats for Length, Interval, and MaxSplit
- Validation with shortened error message: 'Invalid range format. Use from-to (e.g., 50-100).'
- UI in PopupBox (materialDesign) visible only when EnableFragment is checked
- Only Xray-core supported (Mihomo/sing-box fragment code removed)
- Localization updated for 8 languages

* fix: address 2dust review feedback

- Move FragmentPacketsOptions to Global class
- Remove default values from Fragment4RayItem model (set in ConfigHandler)
- Fix Avalonia bindings: move ItemsSource and SelectedItem to code-behind
- Update ViewModel to use Global.FragmentPacketsOptions
- Remove defaults from model properties

* Adjust ui

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-22 16:00:13 +08:00
JieXu
f1b88ccf2a Add GPG key file (#9608)
* Add files via upload

* Update build-all.yml

* Update build-linux.yml

* Update build-osx.yml

* Update build-windows-desktop.yml

* Update build-windows-x86.yml

* Update build-windows.yml

* Update package-zip.yml

* Update build-linux.yml

* Update build-osx.yml

* Update build-windows-x86.yml

* Update package-zip.yml

* Update upload-sign.yml

* Update pub-key.yml

* Update README.md

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-22 09:48:06 +08:00
DHR60
ce380b861c Node check (#9603) 2026-06-20 10:09:27 +08:00
2dust
46e9e4fead up 7.23.0 2026-06-19 18:10:24 +08:00
2dust
00903d3531 Verify if sendThrough is a local address.
https://github.com/2dust/v2rayN/issues/9562
2026-06-19 18:09:18 +08:00
dependabot[bot]
616eea39f9 Bump actions/checkout from 6 to 7 (#9594)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  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-06-19 17:31:22 +08:00
2dust
ff456a9389 Add DNS via NetBridge 2026-06-19 17:13:51 +08:00
2dust
0d225cd29d Create NetBridge 2026-06-19 11:48:27 +08:00
2dust
67426cff12 Add NetBridge process hijacking feature for windows (#9588)
https://github.com/2dust/NetBridge
https://github.com/InterceptSuite/ProxyBridge
2026-06-19 11:21:15 +08:00
JieXu
a8c27027b4 bump dependencies && Purge SDKs before build (#9576)
* Update package-rhel-loong.sh

* Update package-debian-loong.sh

* Update build.yml

* Update build-windows-x86.yml

* Enhance SDK purge step with echo statements

Added echo statements to display SDKs before and after purging.

* Update build-windows-x86.yml

* Update build.yml

* Update build-windows-x86.yml

* Update build.yml

* Update build-windows-x86.yml
2026-06-18 17:55:45 +08:00
DHR60
1a681695a2 Revert "Fetch cert allow insecure (#8998)" (#9585)
This reverts commit 25d7f393b6.
2026-06-18 10:34:18 +08:00
DHR60
2126ea5a87 Fix tun (#9582)
* Fix tun

* Format
2026-06-18 10:33:58 +08:00
barkahss
be005abf9e Add Indonesian (id) UI localization (#9573)
* Add Indonesian (id) UI localization

Register ResUI.id.resx with 550 translated strings and expose the language in the app settings list alongside existing locales.

* Translate menuRouting to Perutean in Indonesian locale

---------

Co-authored-by: Barkasj <katrako1234@gmail.com>
2026-06-18 10:31:15 +08:00
dependabot[bot]
da7dfde706 Bump actions/setup-dotnet from 5.2.0 to 5.3.0 (#9564)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.2.0...v5.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:30:28 +08:00
2dust
7491e86d80 Update 01_bug_report.yml 2026-06-17 14:42:29 +08:00
2dust
af037ed087 Added ipinfo copy functionality to the desktop version
https://github.com/2dust/v2rayN/issues/9529
2026-06-15 14:34:45 +08:00
JieXu
c9298db607 Update package-rhel.sh (#9545)
* Update build-linux.yml

* Create package-rhel-loong.sh

* Update package-debian-riscv.sh

* Update package-rhel-riscv.sh

* Update package-rhel-riscv.sh

* Update package-rhel.sh

* Update build-linux.yml

* Update build-linux.yml

* Create build-windows-x86.yml

* Update build-all.yml
2026-06-14 12:21:43 +08:00
2dust
da81101cfd up 7.22.7 2026-06-12 19:15:08 +08:00
DHR60
3d4f202cd7 Metrics listen directly (#9533) 2026-06-12 09:47:59 +08:00
2dust
35fbb137e8 Add vcn and pcs properties to VmessQRCode DTO 2026-06-12 09:47:03 +08:00
sparklelcm333
1869a95700 fix: Desktop(Avalonia) 移除子对话框最小化按钮,仅禁 CanMinimize (#9526) 2026-06-10 20:45:23 +08:00
JieXu
34359885ab Update AppBuilderExtension.cs (#9514)
* Update AppBuilderExtension.cs

* Update MainWindow.axaml.cs

* Update MacAppUtils.cs

* Update App.axaml.cs

* Add regions for MacOS activation and app events

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-09 17:00:51 +08:00
znah
93832656dd fix: disable .net9 CET (#9507) 2026-06-09 14:06:13 +08:00
2dust
b1a400b393 binConfigs only deletes test files periodically 2026-06-08 14:22:11 +08:00
2dust
879c736924 Update Utils.cs
5fed566a32
2026-06-07 15:49:31 +08:00
sparklelcm333
8147b39723 fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移 (#9498)
* fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移

WindowStartupLocation="CenterScreen"(来自 XAML)能正确包含 chrome 居中窗口,
但只能使用生效时的 Width/Height。旧代码在 OnLoaded 中设置保存的尺寸为时已晚
(CenterScreen 已应用),随后又用不含 chrome 的手工计算覆盖 Position,
导致窗口每次启动偏右下。

修复方式:在 Initialized 事件中设置保存的 Width/Height。
Avalonia 中 Initialized 在 Show() 过程中触发,早于 WindowStartupLocation 生效,
因此 CenterScreen 能用正确的保存尺寸居中。

同时移除 OnLoaded 中重复的 Width/Height 设置(Initialized 中已设置)。

* Update WindowBase.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-07 14:59:50 +08:00
2dust
5fed566a32 Add ReplaceLineBreaks extension and Fix bug
https://github.com/2dust/v2rayN/issues/9497
2026-06-07 14:49:39 +08:00
2dust
f0ee792778 up 7.22.6 2026-06-07 11:23:36 +08:00
JieXu
cd4aa5531c Fix (#9495)
* Update App.axaml.cs

* Update App.axaml.cs
2026-06-07 09:04:06 +08:00
DHR60
e6d08c2db1 Code clean (#9482) 2026-06-06 11:20:06 +08:00
JieXu
4dc9c60b85 Update AppBuilderExtension.cs && Fix Bug (#9483)
* Update AppBuilderExtension.cs

* Update OptionSettingWindow.axaml.cs

* Update OptionSettingWindow.axaml.cs

* Update Global.cs

* Rename ResUI.fa-Ir.resx to ResUI.fa.resx

* Update ServiceLib.csproj
2026-06-06 11:09:53 +08:00
tt2563
eb9c974436 Update Traditional Chinese translation (#9484)
* Update Traditional Chinese translation

Update Traditional Chinese translation

* Update ResUI.zh-Hant.resx

Update Traditional Chinese translation
2026-06-06 09:37:40 +08:00
DHR60
27b8f889fe Tail Fragmentation (#9476) 2026-06-05 19:45:49 +08:00
DHR60
0d5169e4cd Remove global AllowInsecure and MuxEnabled (#9473)
* Remove global AllowInsecure

* Remove global MuxEnabled

* Fix core config

* Centralize AllowInsecure handling

* Warn insecure configuration

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-05 15:31:23 +08:00
DHR60
abcdd9497c Support new kcp config (#9477) 2026-06-05 15:08:57 +08:00
DHR60
da65faa49f Set xray default tun dns (#9474) 2026-06-05 09:58:00 +08:00
DHR60
b6cfce069a Xray Route Exclude Address (#9469)
* Add IPNetwork2 package

* Xray Route Exclude Address

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-05 09:54:57 +08:00
JieXu
39ec9239d2 Revert "Fix NUGET_CERT_REVOCATION_MODE=offline && Update Directory.Packages.props (#9478)
* Revert "Fix NUGET_CERT_REVOCATION_MODE=offline (#9470)"

This reverts commit a2a5940d91.

* Update Directory.Packages.props

* Update build-linux.yml

* Update Directory.Packages.props
2026-06-05 09:41:21 +08:00
JieXu
a2a5940d91 Fix NUGET_CERT_REVOCATION_MODE=offline (#9470)
* Update build.yml

* Update package-rhel.sh

* Update package-rhel-riscv.sh

* Update package-debian.sh

* Update package-debian-riscv.sh

* Update package-debian-loong.sh

* Update build-linux.yml

* Update Directory.Packages.props
2026-06-04 20:00:32 +08:00
2dust
ce8caa89a4 Add Route Exclude Address setting and UI for sing-box tun
https://github.com/2dust/v2rayN/issues/9456
2026-06-04 15:55:09 +08:00
DHR60
af3d579890 Fix (#9462)
* Remove outdated CA certificates

* Support verifyPeerCertByName

* Fix
2026-06-03 15:07:53 +08:00
2dust
b2bbc865af Add VerifyPeerCertByName support
https://github.com/2dust/v2rayN/issues/9427
2026-06-03 09:56:34 +08:00
Miheichev Aleksandr Sergeevich
2eb325643a chore(deps): update NuGet dependencies (#9455)
- Avalonia.Desktop 11.3.16 -> 11.3.17
- Avalonia.Diagnostics 11.3.16 -> 11.3.17
- Downloader 5.5.0 -> 5.7.0
- Microsoft.NET.Test.Sdk 18.5.1 -> 18.6.0
- Tmds.DBus.Protocol 0.21.3 -> 0.94.1
- YamlDotNet 17.1.0 -> 18.0.0
2026-06-03 09:03:14 +08:00
Miheichev Aleksandr Sergeevich
8609e2b8d1 fix(ui): omit country flag emoji on Windows to avoid DE(DE) display (#9454) 2026-06-03 09:03:11 +08:00
2dust
fcf6c1e3ae up 7.22.5 2026-06-02 19:14:15 +08:00
2dust
ab907e371b Clarify deprecation message for allowInsecure 2026-06-02 19:13:52 +08:00
2dust
ef4dda0996 Warn on deprecated allowInsecure usage
https://github.com/2dust/v2rayN/issues/9435
2026-06-02 16:42:04 +08:00
JieXu
f2e0bd4eba Bump skiasharp version && Add loong64 Update && Translate FR (#9433)
* Update Directory.Packages.props

* Update CoreInfoManager.cs

* Update CoreInfo.cs

* Update UpdateService.cs

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx
2026-06-01 13:46:57 +08:00
Miheichev Aleksandr Sergeevich
bca07f801d i18n(ru): refine Russian localization against the zh-Hans source (#9431)
* i18n(ru): refine Russian localization against the zh-Hans source

Translate newly added UI strings and align existing Russian entries
with the authoritative Simplified Chinese (zh-Hans) source, since the
project is maintained by a Chinese-speaking author.

New strings (check-for-update flow):
- MsgCheckUpdateHasNewVersion, menuCheckOnly, menuNewUpdate,
  MsgNotSupport, LvTestIpInfo

Correctness fixes (changed meaning):
- TbSettingsHysteriaBandwidth: restore upload/download order (Up/Dw)
- TbSettingsMixedConcurrencyCount: "multi-threaded" (was "multi-stage")
- TransportRequestHostTip4: "QUIC encryption method" (was "security")
- TransportPathTip3: "QUIC encryption key"
- TbDomesticDNS: "DNS for direct connections"

Terminology and clarity:
- Unify the alias field to one consistent term across all rule/profile
  labels, distinct from the memo field
- allowInsecure -> "skip certificate verification" (+ cert-pinning ref)
- Restore details dropped from EN but present in zh-Hans: Port Hopping
  range, pre-SOCKS split routing, optional / "may fail" hints

Consistency and cleanup:
- Orthography, transport names (httpupgrade, XHTTPObject),
  move-to-top/bottom wording, trailing-space cleanup

Scope: ServiceLib/Resx/ResUI.ru.resx only. Technical abbreviations are
kept in English (LAN, UUID, MTU, TLS, ALPN, SNI, ...). Stale Chinese
strings vs. current behavior were judged case by case.

* i18n(ru): shorten update-check button labels to fit fixed-width buttons

The check-update window has two fixed-width (100px) buttons. The Russian
labels were faithful but too long, so both were truncated and looked almost
identical, hiding the difference between the two actions:
- the "Only Check" button only checks whether a newer version is available
- the "Check Update" button checks, downloads, installs and restarts

Shorten both so they fit the button and clearly distinguish check vs update.

Note: menuCheckUpdate is also reused as the default row placeholder in the
update list, which now shows the shorter label as well.
2026-06-01 13:46:42 +08:00
JieXu
54a3388c99 Update App.axaml.cs (#9421)
* Update App.axaml.cs

* Update App.axaml.cs

* Create MacOSAppHelper.cs

* Update App.axaml.cs

Removed unused macOS activation policy methods and constants.

* Update and rename MacOSAppHelper.cs to MacAppUtils.cs

* Update App.axaml.cs
2026-05-31 17:57:59 +08:00
direnquanbuKO
5140954b37 Correct multiple DNS Hosts ips (#9365)
* Correct dns.cloudflare.com ips

* Add ipv6 for engage.cloudflareclient.com

* Add new ips for dns.sb
2026-05-31 10:32:14 +08:00
JieXu
c7efe39e1c Fix (#9418)
* Update package-osx.sh

* Update package-osx.sh
2026-05-30 20:50:20 +08:00
2dust
a74ca3f9d0 up 7.22.4 2026-05-29 11:08:39 +08:00
2dust
ed61f5c420 Rename ViewModel properties to PascalCase 2026-05-29 11:05:43 +08:00
2dust
bf98c4007f Use configurable page size and delay in speedtests
https://github.com/2dust/v2rayN/pull/9392
2026-05-29 10:37:00 +08:00
dependabot[bot]
78dcf51c3c Bump actions/setup-dotnet from 5.2.0 to 5.3.0 (#9407)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.2.0...v5.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 09:56:43 +08:00
2dust
1e59344074 Add balancer fallbackTag
https://github.com/2dust/v2rayN/issues/9401
2026-05-29 09:55:59 +08:00
JieXu
1d2442d58a Fix the problem of font select (#9405)
* Update OptionSettingWindow.axaml.cs

* Update OptionSettingWindow.axaml.cs
2026-05-28 15:31:06 +08:00
2dust
3448782925 Remove unused C# style setting from .editorconfig 2026-05-28 14:31:27 +08:00
2dust
e6f4a57913 Limit update checks to selected core types
https://github.com/2dust/v2rayN/issues/9381
2026-05-28 14:31:01 +08:00
Roffild
4ae5c021fd Add in Global.cs speed and ping test URLs (#9374)
* Add in Global.cs speed and ping test URLs

* 100mb

* Update Global.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-05-27 16:54:26 +08:00
xihan123
0a357fd1a7 Change the SelectedValue bound to Text #9388 (#9389) 2026-05-26 20:20:15 +08:00
2dust
5ba5a805ce Make MTU combobox editable
https://github.com/2dust/v2rayN/issues/9370
2026-05-24 14:54:06 +08:00
YFdyh000
807f0aba06 Make Admin AutoRun faster (#9366)
* Set Admin AutoRun task priority to Normal rather than Below Normal

* Remove 30s delay for Admin AutoRun
2026-05-23 13:58:00 +08:00
2dust
f4a2086dfb Refine reload logic after RefreshServers
https://github.com/2dust/v2rayN/issues/9344
2026-05-22 10:07:36 +08:00
236 changed files with 16441 additions and 4538 deletions

View File

@@ -100,7 +100,6 @@ csharp_style_prefer_tuple_swap = true:warning
csharp_style_prefer_utf8_string_literals = true:warning
csharp_style_throw_expression = true:warning
csharp_style_unused_value_assignment_preference = discard_variable:warning
csharp_style_unused_value_expression_statement_preference = discard_variable:warning
csharp_using_directive_placement = outside_namespace:warning
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:warning
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning

View File

@@ -18,8 +18,8 @@ body:
- type: input
id: "os-version"
attributes:
label: "操作系统版本"
description: "操作系统版本"
label: "操作系统 / v2rayN / Core 版本"
description: "操作系统 / v2rayN / Core 版本"
validations:
required: true

113
.github/scripts/UpdateCert.cs vendored Normal file
View File

@@ -0,0 +1,113 @@
/*
==============================================================================
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.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Collections.Generic;
using System.Threading.Tasks;
var client = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
var outputDir = args.Length > 0
? args[0]
: Path.Combine(Environment.CurrentDirectory, "v2rayN", "ServiceLib", "Sample");
Directory.CreateDirectory(outputDir);
Console.WriteLine("=== Mozilla + Chrome Root Store PEM Converter ===\n");
Console.WriteLine($"Output directory: {outputDir}\n");
// Process Mozilla
await ProcessMozillaAsync(outputDir);
// Process Chrome
await ProcessChromeAsync(outputDir);
Console.WriteLine("\nAll done!");
async Task ProcessMozillaAsync(string outputDir)
{
const string certdataUrl = "https://ccadb.my.salesforce-sites.com/mozilla/IncludedRootsPEMTxt?TrustBitsInclude=Websites";
var outputFile = Path.Combine(outputDir, "mozilla_roots_pem");
Console.WriteLine("Downloading Mozilla certdata.txt...");
var content = await client.GetStringAsync(certdataUrl);
var pemMatches = Regex.Matches(content, @"(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)");
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));
Console.WriteLine($"Mozilla roots saved to: {outputFile} ({pems.Count} certificates)");
}
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";
var chromeOutput = Path.Combine(outputDir, "chrome_roots_pem");
Console.WriteLine("Downloading Chrome root_store.certs...");
var base64Content = await client.GetStringAsync(chromeCertsUrl);
var decoded = Convert.FromBase64String(base64Content.Replace("\n", ""));
var text = Encoding.UTF8.GetString(decoded);
var pemMatches = Regex.Matches(text, @"(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)");
var pems = new List<string>();
foreach (Match m in pemMatches)
{
pems.Add(m.Groups[1].Value.Trim());
}
await File.WriteAllTextAsync(chromeOutput, string.Join("\n\n", pems));
Console.WriteLine($"Chrome roots saved to: {chromeOutput} ({pems.Count} certificates)");
}

View File

@@ -9,8 +9,17 @@ on:
permissions:
actions: write
contents: write
jobs:
public-key:
if: inputs.release_tag != ''
uses: ./.github/workflows/pub-key.yml
with:
release_tag: ${{ inputs.release_tag }}
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
update:
runs-on: ubuntu-latest
steps:
@@ -70,3 +79,17 @@ jobs:
\"release_tag\": \"${{ inputs.release_tag }}\"
}
}"
- name: Trigger build windows x86
if: inputs.release_tag != ''
run: |
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-windows-x86.yml/dispatches \
-d "{
\"ref\": \"master\",
\"inputs\": {
\"release_tag\": \"${{ inputs.release_tag }}\"
}
}"

View File

@@ -12,6 +12,7 @@ on:
permissions:
contents: write
actions: read
jobs:
build:
@@ -26,17 +27,14 @@ jobs:
with:
target: linux
release_tag: ${{ inputs.release_tag }}
secrets: inherit
deb:
name: build and release deb x64 & arm64
if: |
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04
container: debian:13
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
steps:
- name: Prepare tools (Debian)
shell: bash
@@ -44,124 +42,73 @@ jobs:
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \
ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \
libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
- name: Checkout repo (for scripts)
uses: actions/checkout@v6
with:
submodules: 'recursive'
fetch-depth: '0'
uses: actions/checkout@v7
with: { submodules: 'recursive', fetch-depth: '0' }
- name: Ensure script permissions
run: chmod 755 package-debian.sh
- name: Package DEB (Debian-family)
run: ./package-debian.sh "${RELEASE_TAG}" --arch all
- name: Collect DEBs into workspace
run: |
mkdir -p "$GITHUB_WORKSPACE/dist/deb"
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb" || true
- name: Upload DEBs to release
uses: svenstaro/upload-release-action@v2
with:
file: dist/deb/**/*.deb
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true
- name: Upload DEB artifact
uses: actions/upload-artifact@v7
with: { name: release-deb, path: "dist/deb/**/*.deb", if-no-files-found: error }
rpm:
name: build and release rpm x64 & arm64
if: |
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04
container: registry.access.redhat.com/ubi10/ubi
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
steps:
- name: Prepare tools (Red Hat)
shell: bash
run: |
set -euo pipefail
. /etc/os-release
EL_MAJOR="${VERSION_ID%%.*}"
echo "EL_MAJOR=${EL_MAJOR}"
dnf -y makecache || true
command -v curl >/dev/null || dnf -y install curl ca-certificates
ARCH="$(uname -m)"
case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac
install_epel_from_dir() {
local base="$1" rpm
echo "Try: $base"
rpm="$(
{
curl -fsSL "$base/Packages/" 2>/dev/null
curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'
} |
sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' |
sort -V | tail -n1
)" || true
if [[ -n "$rpm" ]]; then
dnf -y install "$base/Packages/$rpm"
return 0
fi
return 1
rpm="$({ curl -fsSL "$base/Packages/" 2>/dev/null; curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'; } |
sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' | sort -V | tail -n1)" || true
[[ -n "$rpm" ]] || return 1
dnf -y install "$base/Packages/$rpm"
}
FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm"
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os"
ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os"
echo "Try Fedora: $FEDORA"
curl -fsSLI "$FEDORA" >/dev/null && dnf -y install "$FEDORA" ||
install_epel_from_dir "$ROCKY" ||
install_epel_from_dir "$ALMA" ||
{ echo "EPEL bootstrap failed (Fedora/Rocky/Alma)"; exit 1; }
if curl -fsSLI "$FEDORA" >/dev/null; then
dnf -y install "$FEDORA"
else
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os"
if install_epel_from_dir "$ROCKY"; then
:
else
ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os"
if install_epel_from_dir "$ALMA"; then
:
else
echo "EPEL bootstrap failed (Fedora/Rocky/Alma)"
exit 1
fi
fi
fi
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core \
rsync findutils tar gzip unzip which
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which
dnf repolist | grep -i epel || true
- name: Checkout repo (for scripts)
uses: actions/checkout@v6
with:
submodules: 'recursive'
fetch-depth: '0'
uses: actions/checkout@v7
with: { submodules: 'recursive', fetch-depth: '0' }
- name: Ensure script permissions
run: chmod 755 package-rhel.sh
- name: Package RPM (RHEL-family)
run: ./package-rhel.sh "${RELEASE_TAG}" --arch all
- name: Collect RPMs into workspace
run: |
mkdir -p "$GITHUB_WORKSPACE/dist/rpm"
@@ -170,39 +117,26 @@ jobs:
find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
- name: Upload RPMs to release
uses: svenstaro/upload-release-action@v2
with:
file: dist/rpm/**/*.rpm
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true
- name: Upload RPM artifact
uses: actions/upload-artifact@v7
with: { name: release-rpm, path: "dist/rpm/**/*.rpm", if-no-files-found: error }
rpm-riscv64:
name: build and release rpm riscv64
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-24.04-riscv
container: rockylinux/rockylinux:10
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
steps:
- name: Prepare tools (Red Hat)
shell: bash
run: |
set -euo pipefail
dnf -y makecache
dnf -y install \
sudo git rpm-build rpmdevtools dnf-plugins-core \
rsync findutils tar gzip unzip which jq
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which jq
- name: Checkout repo (for scripts)
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf ./*
@@ -212,56 +146,27 @@ jobs:
git fetch --depth=1 origin "${GITHUB_SHA}"
git checkout FETCH_HEAD
git submodule update --init --recursive
- name: Ensure script permissions
run: chmod 755 package-rhel-riscv.sh
- name: Package RPM (RHEL-family)
run: ./package-rhel-riscv.sh "${RELEASE_TAG}"
- name: Collect RPMs into workspace
run: |
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64"
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" \
-exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true
- name: Upload RPMs to release
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
shopt -s globstar nullglob
files=(dist/rpm-riscv64/**/*.rpm)
(( ${#files[@]} )) || { echo "No RPMs found."; exit 1; }
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
upload_url="$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$api" | jq -r '.upload_url // empty' | sed 's/{?name,label}//')"
[[ "$upload_url" ]] || { echo "Release upload URL not found: ${RELEASE_TAG}"; exit 1; }
for f in "${files[@]}"; do
echo "Uploading ${f##*/}"
curl -fsSL -X POST \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/x-rpm" \
--data-binary @"$f" \
"${upload_url}?name=${f##*/}"
done
- name: Upload RPM artifact
uses: actions/upload-artifact@v7
with: { name: release-rpm-riscv64, path: "dist/rpm-riscv64/**/*.rpm", if-no-files-found: error }
deb-riscv64:
name: build and release deb riscv64
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-24.04-riscv
container: debian:13
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
steps:
- name: Prepare tools (Debian)
shell: bash
@@ -269,14 +174,10 @@ jobs:
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \
ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \
gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1
- name: Checkout repo (for scripts)
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf ./*
@@ -286,53 +187,25 @@ jobs:
git fetch --depth=1 origin "${GITHUB_SHA}"
git checkout FETCH_HEAD
git submodule update --init --recursive
- name: Ensure script permissions
run: chmod 755 package-debian-riscv.sh
- name: Package DEB (Debian-family)
run: ./package-debian-riscv.sh "${RELEASE_TAG}"
- name: Collect DEBs into workspace
run: |
mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64"
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true
- name: Upload DEBs to release
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
shopt -s globstar nullglob
files=(dist/deb-riscv64/**/*.deb)
(( ${#files[@]} )) || { echo "No DEBs found."; exit 1; }
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
upload_url="$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$api" | jq -r '.upload_url // empty' | sed 's/{?name,label}//')"
[[ "$upload_url" ]] || { echo "Release upload URL not found: ${RELEASE_TAG}"; exit 1; }
for f in "${files[@]}"; do
echo "Uploading ${f##*/}"
curl -fsSL -X POST \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/vnd.debian.binary-package" \
--data-binary @"$f" \
"${upload_url}?name=${f##*/}"
done
- name: Upload DEB artifact
uses: actions/upload-artifact@v7
with: { name: release-deb-riscv64, path: "dist/deb-riscv64/**/*.deb", if-no-files-found: error }
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/'))
runs-on: ubuntu-24.04
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04
env:
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
@@ -341,75 +214,73 @@ jobs:
QCOW2_IMAGE: debian13-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.2
QEMU_VERSION: 10.2.4
steps:
- name: Prepare host tools
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y rsync qemu-utils expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0
- name: Checkout repo
uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
virtiofsd --version || virtiofsd --help | head
- name: Host clone repo
shell: bash
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf repo
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
- name: Download QEMU prebuild
run: |
set -euo pipefail
wget -O qemu-linux-x64.tar.gz \
"https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64.tar.gz"
tar -xzf qemu-linux-x64.tar.gz
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"
tar -xzf qemu-linux-x64-v3.tar.gz
mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64/ "$HOME/qemu-install/"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware
shell: bash
run: |
set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE"
- name: Build loong64 DEB in VM through serial console
shell: bash
timeout-minutes: 180
env:
RELEASE_TAG: ${{ env.RELEASE_TAG }}
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock"
rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
for i in {1..100}; do
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
sleep 0.1
done
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
expect <<'EOF'
log_user 1
set timeout -1
set release_tag $env(RELEASE_TAG)
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set workspace $env(GITHUB_WORKSPACE)
set repo $env(GITHUB_REPOSITORY)
set sha $env(GITHUB_SHA)
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS)
proc wait_prompt {} {
expect {
-re "__CI_PROMPT__# " {}
@@ -417,86 +288,258 @@ jobs:
eof { exit 1 }
}
}
proc run_cmd {cmd} {
send -- "$cmd\r"
wait_prompt
}
spawn $qemu_bin \
-L $qemu_rom_dir \
-machine virt \
-accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \
-m 9216 \
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
-numa node,memdev=mem0 \
-smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \
-device virtio-net-pci,netdev=net0 \
-virtfs local,path=$workspace,mount_tag=workspace,security_model=none,id=workspace \
-device virtio-net-pci-non-transitional,netdev=net0 \
-chardev socket,id=charfs0,path=$virtiofs_socket \
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
-display none \
-serial stdio \
-monitor none
expect -re "login:|debian login:"
send -- "root\r"
expect -re "Password:|密码:|密码:"
send -- "password\r"
expect {
-re "# " {}
timeout { exit 1 }
eof { exit 1 }
}
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt
run_cmd "mkdir -p /workspace"
run_cmd "mount -t 9p -o trans=virtio,version=9p2000.L workspace /workspace || mount -t 9p -o trans=virtio workspace /workspace"
run_cmd "mount -t virtiofs workspace /workspace"
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
run_cmd "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
run_cmd "apt-get update || apt-get update || apt-get update"
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1"
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 fuse3"
run_cmd "rm -rf /root/v2rayN-src"
run_cmd "git clone --recursive https://github.com/$repo.git /root/v2rayN-src"
run_cmd "cd /root/v2rayN-src && git fetch --depth=1 origin $sha && git checkout FETCH_HEAD && git submodule update --init --recursive"
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh"
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r"
expect {
-re "__BUILD_DONE__0" {
send -- "poweroff\r"
}
default {
exit 1
}
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
default { exit 1 }
}
EOF
- name: Collect DEBs
shell: bash
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
- name: Upload DEB artifact
uses: actions/upload-artifact@v7
with: { name: release-deb-loong64, path: "dist/deb-loong64/**/*.deb", if-no-files-found: error }
- name: Upload DEBs to release
uses: svenstaro/upload-release-action@v2
with:
file: dist/deb-loong64/**/*.deb
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true
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/'))
runs-on: ubuntu-26.04
env:
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
EFI_CODE_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-vars.fd
QCOW2_IMAGE: fedora43-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.4
steps:
- name: Prepare host tools
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
virtiofsd --version || virtiofsd --help | head
- name: Host clone repo
shell: bash
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf repo
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
- name: Download QEMU prebuild
run: |
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"
tar -xzf qemu-linux-x64-v3.tar.gz
mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware
shell: bash
run: |
set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE"
- name: Build loong64 RPM in VM through serial console
shell: bash
timeout-minutes: 180
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock"
rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
for i in {1..100}; do
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
sleep 0.1
done
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
expect <<'EOF'
log_user 1
set timeout -1
set release_tag $env(RELEASE_TAG)
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS)
proc wait_prompt {} {
expect {
-re "__CI_PROMPT__# " {}
timeout { exit 1 }
eof { exit 1 }
}
}
proc run_cmd {cmd} {
send -- "$cmd\r"
wait_prompt
}
spawn $qemu_bin \
-L $qemu_rom_dir \
-machine virt \
-accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \
-m 9216 \
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
-numa node,memdev=mem0 \
-smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \
-device virtio-net-pci-non-transitional,netdev=net0 \
-chardev socket,id=charfs0,path=$virtiofs_socket \
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
-display none \
-serial stdio \
-monitor none
expect -re "login:|fedora login:"
send -- "root\r"
expect -re "Password:|密码:|密码:"
send -- "password\r"
expect {
-re "# " {}
timeout { exit 1 }
eof { exit 1 }
}
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt
run_cmd "mkdir -p /workspace"
run_cmd "mount -t virtiofs workspace /workspace"
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
run_cmd "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
run_cmd "dnf -y makecache || dnf -y makecache || dnf -y makecache"
run_cmd "dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which curl jq file ca-certificates fuse3 --nogpgcheck"
run_cmd "rm -rf /root/v2rayN-src"
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
run_cmd "cd /root/v2rayN-src && chmod 755 package-rhel-loong.sh"
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-rpm-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-rhel-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/rpm-loong64\nfind /root/rpmbuild/RPMS -name '*.rpm' -exec cp -av {} /workspace/dist/rpm-loong64/ \\; 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-rpm-build.sh\r"
expect {
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
default { exit 1 }
}
EOF
- name: Collect RPMs
shell: bash
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
find "$GITHUB_WORKSPACE/dist/rpm-loong64" -name "*.loongarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-loong64/v2rayN-linux-rhel-loong64.rpm" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm-loong64"
- name: Upload RPM artifact
uses: actions/upload-artifact@v7
with: { name: release-rpm-loong64, path: "dist/rpm-loong64/**/*.rpm", if-no-files-found: error }
release-deb:
name: sign and upload deb x64 arm64
needs: deb
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb, file_pattern: "*.deb" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-rpm:
name: sign and upload rpm x64 arm64
needs: rpm
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm, file_pattern: "*.rpm" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-rpm-riscv64:
name: sign and upload rpm riscv64
needs: rpm-riscv64
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm-riscv64, file_pattern: "*.rpm" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-deb-riscv64:
name: sign and upload deb riscv64
needs: deb-riscv64
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb-riscv64, file_pattern: "*.deb" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-deb-loong64:
name: sign and upload deb loong64
needs: deb-loong64
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb-loong64, file_pattern: "*.deb" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-rpm-loong64:
name: sign and upload rpm loong64
needs: rpm-loong64
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm-loong64, file_pattern: "*.rpm" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }

View File

@@ -12,6 +12,7 @@ on:
permissions:
contents: write
actions: read
jobs:
build:
@@ -26,6 +27,7 @@ jobs:
with:
target: macos
release_tag: ${{ inputs.release_tag }}
secrets: inherit
dmg:
name: package and release macOS dmg
@@ -45,7 +47,7 @@ jobs:
}}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Restore build artifacts
uses: actions/download-artifact@v8
@@ -62,13 +64,24 @@ jobs:
- name: Package dmg
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
- name: Sleep for race condition between matrix jobs
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')"
- name: Upload dmg to release
uses: svenstaro/upload-release-action@v2
- name: Upload dmg artifact
uses: actions/upload-artifact@v7
with:
file: ${{ github.workspace }}/v2rayN*.dmg
tag: ${{ inputs.release_tag }}
file_glob: true
prerelease: true
name: release-dmg-${{ matrix.arch }}
path: v2rayN*.dmg
if-no-files-found: error
release-dmg:
name: sign and upload macOS dmg
if: inputs.release_tag != ''
needs: dmg
strategy:
matrix:
arch: [ x64, arm64 ]
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: release-dmg-${{ matrix.arch }}
file_pattern: '*.dmg'
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}

View File

@@ -12,6 +12,7 @@ on:
permissions:
contents: write
actions: read
jobs:
build:
@@ -26,3 +27,4 @@ jobs:
with:
target: windows-desktop
release_tag: ${{ inputs.release_tag }}
secrets: inherit

247
.github/workflows/build-windows-x86.yml vendored Normal file
View File

@@ -0,0 +1,247 @@
name: release Windows x86
on:
workflow_dispatch:
inputs:
release_tag:
required: false
type: string
permissions:
contents: write
actions: read
jobs:
package-x86:
name: build and release windows x86 WPF & Avalonia
if: inputs.release_tag != ''
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- flavor: wpf
project: ./v2rayN/v2rayN/v2rayN.csproj
zip_name: v2rayN-windows-86.zip
- flavor: desktop
project: ./v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj
zip_name: v2rayN-windows-86-desktop.zip
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Purge SDKs
shell: pwsh
run: |
echo "Before:"
dotnet --list-sdks
dotnet --list-sdks | % {
$_ -match '^\S+\s+\[(.+)\]$' > $null
$d = Join-Path $Matches[1] $_.Split()[0]
Write-Host "Removing $d"
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
}
echo "After:"
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET 10.0.1xx
uses: actions/setup-dotnet@v6.0.0
with:
dotnet-version: 10.0.1xx
- name: Publish ${{ matrix.flavor }} win-x86
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
dotnet restore "${{ matrix.project }}" -r win-x86
dotnet publish "${{ matrix.project }}" `
-c Release `
-r win-x86 `
-p:SelfContained=true `
-p:EnableWindowsTargeting=true `
-o "publish/${{ matrix.flavor }}"
- name: Publish AmazTool win-x86
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
dotnet publish "./v2rayN/AmazTool/AmazTool.csproj" `
-c Release `
-r win-x86 `
-p:SelfContained=true `
-p:PublishTrimmed=true `
-p:EnableWindowsTargeting=true `
-o "publish/${{ matrix.flavor }}"
- name: Detect bundled core versions
id: detect-core
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force -Path "tmp/probe" | Out-Null
Invoke-WebRequest -Uri "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-windows-64/bin/xray/xray.exe" -OutFile "tmp/probe/xray.exe"
$xrayOutput = (& "tmp/probe/xray.exe" version) -join "`n"
Write-Host $xrayOutput
$xrayMatch = [regex]::Match($xrayOutput, 'Xray\s+([0-9.]+)')
if ($xrayMatch.Success) {
$xrayVer = $xrayMatch.Groups[1].Value
} else {
throw "Failed to detect Xray version"
}
Invoke-WebRequest -Uri "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-windows-64/bin/sing_box/sing-box.exe" -OutFile "tmp/probe/sing-box.exe"
$singOutput = (& "tmp/probe/sing-box.exe" version) -join "`n"
Write-Host $singOutput
$singMatch = [regex]::Match($singOutput, 'sing-box version\s+([0-9.]+)')
if ($singMatch.Success) {
$singVer = $singMatch.Groups[1].Value
} else {
throw "Failed to detect sing-box version"
}
Write-Host "Bundled Xray version: $xrayVer"
Write-Host "Bundled sing-box version: $singVer"
"xray_version=$xrayVer" >> $env:GITHUB_OUTPUT
"sing_version=$singVer" >> $env:GITHUB_OUTPUT
- name: Download x86 cores and rule assets
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$ErrorActionPreference = "Stop"
$headers = @{
Authorization = "Bearer $env:GH_TOKEN"
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}
$root = "publish/${{ matrix.flavor }}"
$bin = Join-Path $root "bin"
$xrayDir = Join-Path $bin "xray"
$singDir = Join-Path $bin "sing_box"
$srssDir = Join-Path $bin "srss"
New-Item -ItemType Directory -Force -Path $xrayDir, $singDir, $srssDir | Out-Null
$xrayVer = "${{ steps.detect-core.outputs.xray_version }}"
$singVer = "${{ steps.detect-core.outputs.sing_version }}"
$xrayTag = "v$xrayVer"
$singTag = "v$singVer"
$xrayUrl = "https://github.com/autorepobot/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
Write-Host "Bundled Xray version: $xrayVer"
Write-Host "Bundled sing-box version: $singVer"
Write-Host "Xray: $xrayUrl"
Write-Host "sing-box: $singUrl"
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
Invoke-WebRequest -Uri $xrayUrl -OutFile "tmp/xray.zip"
Expand-Archive -Force "tmp/xray.zip" "tmp/xray"
Get-ChildItem "tmp/xray" -Recurse -Include "*.exe","*.dll" | ForEach-Object {
Copy-Item $_.FullName (Join-Path $xrayDir $_.Name) -Force
}
Invoke-WebRequest -Uri $singUrl -OutFile "tmp/sing-box.zip"
Expand-Archive -Force "tmp/sing-box.zip" "tmp/sing-box"
$singExe = Get-ChildItem "tmp/sing-box" -Recurse -Filter "sing-box.exe" | Select-Object -First 1
if (-not $singExe) {
throw "sing-box.exe not found"
}
Copy-Item $singExe.FullName "$singDir/sing-box.exe" -Force
$cronet = Get-ChildItem "tmp/sing-box" -Recurse -Filter "libcronet*.dll" | Select-Object -First 1
if ($cronet) {
Copy-Item $cronet.FullName "$singDir/libcronet.dll" -Force
}
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "$bin/geosite.dat"
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "$bin/geoip.dat"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat" -OutFile "$bin/geoip-only-cn-private.dat"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb" -OutFile "$bin/Country.mmdb"
Invoke-WebRequest -Uri "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" -OutFile "$bin/geoip.metadb"
$geoipRules = @(
"geoip-private.srs",
"geoip-cn.srs",
"geoip-facebook.srs",
"geoip-fastly.srs",
"geoip-google.srs",
"geoip-netflix.srs",
"geoip-telegram.srs",
"geoip-twitter.srs"
)
foreach ($f in $geoipRules) {
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geoip/$f" -OutFile "$srssDir/$f"
}
$geositeRules = @(
"geosite-cn.srs",
"geosite-gfw.srs",
"geosite-google.srs",
"geosite-greatfire.srs",
"geosite-geolocation-cn.srs",
"geosite-category-ads-all.srs",
"geosite-private.srs"
)
foreach ($f in $geositeRules) {
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geosite/$f" -OutFile "$srssDir/$f"
}
- name: Package zip
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Compress-Archive `
-Path "publish/${{ matrix.flavor }}/*" `
-DestinationPath "${{ matrix.zip_name }}" `
-Force
- name: Upload zip artifact
uses: actions/upload-artifact@v7
with:
name: release-windows-x86-${{ matrix.flavor }}
path: ${{ matrix.zip_name }}
if-no-files-found: error
release-x86:
name: sign and upload windows x86
if: inputs.release_tag != ''
needs: package-x86
strategy:
fail-fast: false
matrix:
flavor: [ wpf, desktop ]
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: release-windows-x86-${{ matrix.flavor }}
file_pattern: '*.zip'
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}

View File

@@ -12,6 +12,7 @@ on:
permissions:
contents: write
actions: read
jobs:
build:
@@ -27,3 +28,4 @@ jobs:
with:
target: windows
release_tag: ${{ inputs.release_tag }}
secrets: inherit

View File

@@ -22,6 +22,7 @@ jobs:
case(
inputs.target == 'macos', 'macos-latest',
inputs.target == 'linux', 'ubuntu-24.04',
inputs.target == 'windows', 'windows-latest',
'ubuntu-latest'
)
}}
@@ -46,26 +47,49 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: 'recursive'
fetch-depth: '0'
- name: Purge SDKs
shell: pwsh
run: |
echo "Before:"
dotnet --list-sdks
dotnet --list-sdks | % {
$_ -match '^\S+\s+\[(.+)\]$' > $null
$d = Join-Path $Matches[1] $_.Split()[0]
Write-Host "Removing $d"
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
}
echo "After:"
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET
uses: actions/setup-dotnet@v5.2.0
uses: actions/setup-dotnet@v6.0.0
with:
dotnet-version: '10.0.1xx'
- name: Build v2rayN
shell: bash
working-directory: ./v2rayN
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
- name: Build AmazTool
shell: bash
working-directory: ./v2rayN
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
uses: actions/upload-artifact@v7.0.1
with:
name: ${{ matrix.arch }}
path: ${{ matrix.arch }}
path: ${{ matrix.arch }}

View File

@@ -9,9 +9,13 @@ on:
target: # windows linux macos windows-desktop
required: true
type: string
secrets:
GPG_PRIVATE_KEY:
required: true
permissions:
contents: write
actions: read
jobs:
package:
@@ -37,7 +41,7 @@ jobs:
}}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Restore build artifacts
uses: actions/download-artifact@v8
@@ -55,13 +59,23 @@ jobs:
if: inputs.target == 'windows-desktop'
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
- name: Sleep for race condition between matrix jobs
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')"
- name: Upload zip archive to release
uses: svenstaro/upload-release-action@v2
- name: Upload zip artifact
uses: actions/upload-artifact@v7
with:
file: ${{ github.workspace }}/v2rayN*.zip
tag: ${{ inputs.release_tag }}
file_glob: true
prerelease: true
name: release-${{ inputs.target }}-${{ matrix.arch }}
path: v2rayN*.zip
if-no-files-found: error
release:
name: sign and upload zip x64 arm64
needs: package
strategy:
matrix:
arch: [ x64, arm64 ]
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: release-${{ inputs.target }}-${{ matrix.arch }}
file_pattern: '*.zip'
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}

48
.github/workflows/pub-key.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: publish GPG public key
on:
workflow_call:
inputs:
release_tag:
required: true
type: string
secrets:
GPG_PRIVATE_KEY:
required: true
workflow_dispatch:
inputs:
release_tag:
required: true
type: string
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-26.04
steps:
- name: Import GPG private key
shell: bash
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
set -euo pipefail
install -m 700 -d ~/.gnupg
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
- name: Export GPG public key
shell: bash
run: |
set -euo pipefail
fingerprint="$(gpg --batch --list-secret-keys --with-colons | awk -F: '$1 == "fpr" { print $10; exit }')"
[[ -n "$fingerprint" ]] || { echo "No GPG secret key found."; exit 1; }
gpg --batch --armor --export "$fingerprint" > v2rayN-public-key.asc
- name: Upload GPG public key to release
uses: svenstaro/upload-release-action@v2
with:
file: v2rayN-public-key.asc
tag: ${{ inputs.release_tag }}
prerelease: true
overwrite: true

View File

@@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: 'recursive'
fetch-depth: '0'
- name: Setup .NET
uses: actions/setup-dotnet@v5.2.0
uses: actions/setup-dotnet@v6.0.0
with:
dotnet-version: '8.0.x'

76
.github/workflows/upload-sign.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: sign and upload release files
on:
workflow_call:
inputs:
release_tag:
required: true
type: string
artifact_name:
required: true
type: string
file_pattern:
required: false
type: string
default: '**/*'
content_type:
required: false
type: string
default: application/octet-stream
secrets:
GPG_PRIVATE_KEY:
required: true
permissions:
contents: write
actions: read
jobs:
upload:
runs-on: ubuntu-26.04
steps:
- name: Download release artifact
uses: actions/download-artifact@v8
with:
name: ${{ inputs.artifact_name }}
path: dist
- name: Import GPG private key
shell: bash
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
set -euo pipefail
install -m 700 -d ~/.gnupg
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
- name: Sign release files
shell: bash
run: |
set -euo pipefail
cd dist
shopt -s globstar nullglob
files=( ${{ inputs.file_pattern }} )
(( ${#files[@]} )) || { echo "No files matched: ${{ inputs.file_pattern }}"; exit 1; }
for f in "${files[@]}"; do
[[ -f "$f" ]] || continue
gpg --batch --yes --armor --detach-sign --output "$f.sig" "$f"
done
- name: Upload release files
uses: svenstaro/upload-release-action@v2
with:
file: dist/${{ inputs.file_pattern }}
tag: ${{ inputs.release_tag }}
file_glob: true
prerelease: true
overwrite: true
- name: Upload release signatures
uses: svenstaro/upload-release-action@v2
with:
file: dist/${{ inputs.file_pattern }}.sig
tag: ${{ inputs.release_tag }}
file_glob: true
prerelease: true
overwrite: true

View File

@@ -23,8 +23,13 @@ jobs:
$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
$arm64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-arm64\.zip' | Select -ExpandProperty browser_download_url
$assets = $targetRelease | Select -ExpandProperty assets -First 1
$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

3
.gitignore vendored
View File

@@ -84,6 +84,9 @@ StyleCopReport.xml
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
bin/
obj/
build/
*.sbr
*.tlb
*.tli

View File

@@ -1,18 +1,78 @@
# v2rayN
A GUI client for Windows, Linux and macOS, support [Xray](https://github.com/XTLS/Xray-core)
and [sing-box](https://github.com/SagerNet/sing-box)
and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores)
### A GUI client for Windows, Linux and macOS. Support [Xray](https://github.com/XTLS/Xray-core) and [sing-box](https://github.com/SagerNet/sing-box) and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayN)](https://github.com/2dust/v2rayN/commits/master)
[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayn/badge)](https://www.codefactor.io/repository/github/2dust/v2rayn)
[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayN/latest/total?logo=github)](https://github.com/2dust/v2rayN/releases)
[![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/v2rayn)
[![Release](https://img.shields.io/github/v/release/2dust/v2rayN?logo=github&label=Release)](https://github.com/2dust/v2rayN/releases)
[![Downloads](https://img.shields.io/github/downloads/2dust/v2rayN/latest/total?logo=github&label=Downloads)](https://github.com/2dust/v2rayN/releases)
[![Telegram](https://img.shields.io/badge/Telegram-Chat-26A5E4?logo=telegram)](https://t.me/v2rayn)
[![Windows](https://img.shields.io/badge/Windows-supported-0078D6?logo=windows)](https://github.com/2dust/v2rayN)
[![Linux](https://img.shields.io/badge/Linux-supported-FCC624?logo=linux&logoColor=000)](https://github.com/2dust/v2rayN)
[![macOS](https://img.shields.io/badge/macOS-supported-000000?logo=apple)](https://github.com/2dust/v2rayN)
[![GPG Signed](https://img.shields.io/badge/GPG-signed-4B32C3?logo=gnuprivacyguard)](https://github.com/2dust/v2rayN)
## How to use
Read the [Wiki](https://github.com/2dust/v2rayN/wiki) for details.
---
## Telegram Channel
## Download / 下载
[github_2dust](https://t.me/github_2dust)
Download the latest release here:
在这里下载最新版本:
[https://github.com/2dust/v2rayN/releases](https://github.com/2dust/v2rayN/releases)
> [!TIP]
> v2rayN is the desktop version. For the mobile version, please visit the v2rayNG \
> v2rayN 是电脑版,手机版请访问 v2rayNG
>
> https://github.com/2dust/v2rayNG
---
## Documentation / 使用文档
Read the Wiki for usage guides and configuration details.
请阅读 Wiki 获取使用说明和配置教程。
[https://github.com/2dust/v2rayN/wiki](https://github.com/2dust/v2rayN/wiki)
---
## Supported Platforms / 支持平台
| Platform / 平台 | x64 | x86 | arm64 | riscv64 | loong64 |
| --- | --- | --- | --- | --- | --- |
| Windows | ✅ | ✅ | ✅ | - | - |
| Linux | ✅ | - | ✅ | ✅ | ✅ |
| macOS | ✅ | - | ✅ | - | - |
---
## GPG Verification / GPG 签名校验
Release files are signed with GPG to verify authenticity and integrity, helping prevent mirror, ISP, or CDN hijacking.
发布文件已使用 GPG 签名,可用于校验文件真实性与完整性,预防镜像站、运营商或 CDN 劫持。
### Fingerprint / 公钥指纹
```text
7694 5E9F 3E9A 168F 8070 F195 805D 661C
134D FAF6 8903 C199 463C 31E5 AE90 3AE0
```
---
## Community / 社区
Telegram Group / Telegram 群组:
[https://t.me/v2rayN](https://t.me/v2rayN)
Telegram Channel / Telegram 频道:
[https://t.me/github_2dust](https://t.me/github_2dust)

View File

@@ -8,13 +8,13 @@ BUILD_FROM=""
XRAY_VER="${XRAY_VER:-}"
SING_VER="${SING_VER:-}"
MIN_KERNEL="5.10"
MIN_KERNEL="6.12"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.108"
DOTNET_LOONGARCH_TAG="v10.0.108-loongarch64"
DOTNET_LOONGARCH_VERSION="10.0.110"
DOTNET_LOONGARCH_TAG="v10.0.110-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
@@ -116,20 +116,7 @@ install_dependencies() {
export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
mkdir -p "$HOME/.nuget/NuGet"
cat > "$HOME/.nuget/NuGet/NuGet.Config" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="loongnix" value="https://lnuget.loongnix.cn/v3/index.json" allowInsecureConnections="true" />
</packageSources>
</configuration>
EOF
dotnet --info >/dev/null 2>&1 && install_ok=1
fi
@@ -610,8 +597,8 @@ package_binary() {
write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
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"
cat > "$workdir/debian/control" <<EOF
Source: v2rayn

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild"
DOTNET_RISCV_VERSION="10.0.108"
DOTNET_RISCV_VERSION="10.0.110"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -595,8 +595,8 @@ package_binary() {
write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
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"
cat > "$workdir/debian/control" <<EOF
Source: v2rayn

View File

@@ -609,7 +609,7 @@ package_binary() {
write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
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"
cat > "$workdir/debian/control" <<EOF
@@ -753,4 +753,4 @@ main() {
print_summary
}
main "$@"
main "$@"

View File

@@ -22,7 +22,17 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>zh-Hans</string>
<string>zh-Hant</string>
<string>en</string>
<string>fa</string>
<string>fr</string>
<string>ru</string>
<string>hu</string>
</array>
<key>CFBundleDisplayName</key>
<string>v2rayN</string>
<key>CFBundleExecutable</key>
@@ -44,7 +54,7 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
<key>NSHighResolutionCapable</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>12.7</string>
<string>13.7</string>
</dict>
</plist>
EOF

696
package-rhel-loong.sh Normal file
View File

@@ -0,0 +1,696 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION_ARG=""
WITH_CORE="both"
FORCE_NETCORE=0
BUILD_FROM=""
XRAY_VER="${XRAY_VER:-}"
SING_VER="${SING_VER:-}"
MIN_KERNEL="6.12"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_LOONGARCH_VERSION="10.0.110"
DOTNET_LOONGARCH_TAG="v10.0.110-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
OS_ID=""
OS_NAME=""
OS_VERSION_ID=""
HOST_ARCH=""
SCRIPT_DIR=""
PROJECT=""
VERSION=""
declare -a BUILT_RPMS=()
die() {
echo "$*" >&2
exit 1
}
parse_args() {
local first_arg="${1:-}"
if [[ -n "$first_arg" && "$first_arg" != --* ]]; then
VERSION_ARG="$first_arg"
shift || true
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--with-core) WITH_CORE="${2:-both}"; shift 2 ;;
--xray-ver) XRAY_VER="${2:-}"; shift 2 ;;
--singbox-ver) SING_VER="${2:-}"; shift 2 ;;
--netcore) FORCE_NETCORE=1; shift ;;
--buildfrom) BUILD_FROM="${2:-}"; shift 2 ;;
*)
[[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1"
shift
;;
esac
done
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
die "You cannot specify both an explicit version and --buildfrom at the same time.
Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
fi
}
detect_environment() {
local current_kernel=""
local lowest=""
. /etc/os-release
OS_ID="${ID:-}"
OS_NAME="${NAME:-$OS_ID}"
OS_VERSION_ID="${VERSION_ID:-}"
HOST_ARCH="$(uname -m)"
case "$OS_ID" in
rhel|rocky|almalinux|fedora|centos)
echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}"
;;
*)
die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}).
This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS."
;;
esac
case "$HOST_ARCH" in
loongarch64) ;;
*) die "Only supports loongarch64" ;;
esac
current_kernel="$(uname -r)"
lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)"
[[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL"
echo "[OK] Kernel $current_kernel verified."
}
install_dependencies() {
local install_ok=0
local tmp_dotnet=""
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y --nogpgcheck install \
rpm-build rpmdevtools curl unzip tar jq rsync git python3 \
glibc-devel kernel-headers libatomic file ca-certificates libicu \
&& install_ok=1
mkdir -p "$HOME/.dotnet"
tmp_dotnet="$(mktemp -d)"
curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_LOONGARCH_FILE"
tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_LOONGARCH_FILE"
rm -rf "$tmp_dotnet"
export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
dotnet --info >/dev/null 2>&1 || install_ok=0
fi
if [[ "$install_ok" -ne 1 ]]; then
echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:"
echo "dotnet-loongarch SDK, curl, unzip, tar, rsync, git, python3, rpm, rpmdevtools, rpm-build (on Red Hat branch)"
exit 1
fi
}
prepare_workspace() {
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
PROJECT="$PROJECT_HINT"
[[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
[[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found"
}
choose_channel() {
local ch="latest"
local sel=""
if [[ -n "${BUILD_FROM:-}" ]]; then
case "$BUILD_FROM" in
1) echo "latest"; return 0 ;;
2) echo "prerelease"; return 0 ;;
3) echo "keep"; return 0 ;;
*) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;;
esac
fi
if [[ -t 0 ]]; then
echo "[?] Choose v2rayN release channel:" >&2
echo " 1) Latest (stable) [default]" >&2
echo " 2) Pre-release (preview)" >&2
echo " 3) Keep current (do nothing)" >&2
printf "Enter 1, 2 or 3 [default 1]: " >&2
if read -r sel </dev/tty; then
case "${sel:-}" in
2) ch="prerelease" ;;
3) ch="keep" ;;
esac
fi
fi
echo "$ch"
}
get_latest_tag_latest() {
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
| jq -re '.tag_name' \
| sed 's/^v//'
}
get_latest_tag_prerelease() {
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20" \
| jq -re 'first(.[] | select(.prerelease == true) | .tag_name)' \
| sed 's/^v//'
}
sync_submodules() {
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
}
git_try_checkout() {
local want="$1"
local ref=""
if git rev-parse --git-dir >/dev/null 2>&1; then
git fetch --tags --force --prune --depth=1 || true
git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want"
if [[ -n "$ref" ]]; then
echo "[OK] Found ref '${ref}', checking out..."
git checkout -f "$ref"
sync_submodules
return 0
fi
fi
return 1
}
apply_channel_or_keep() {
local ch="$1"
local tag=""
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
VERSION="$(git describe --tags --abbrev=0 2>/dev/null || echo '0.0.0+git')"
VERSION="${VERSION#v}"
return 0
fi
echo "[*] Resolving ${ch} tag from GitHub releases..."
case "$ch" in
latest) tag="$(get_latest_tag_latest || true)" ;;
prerelease) tag="$(get_latest_tag_prerelease || true)" ;;
*) die "Failed to resolve latest tag for channel '${ch}'." ;;
esac
[[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'."
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || die "Failed to checkout '${tag}'."
VERSION="${tag#v}"
}
resolve_version() {
if git rev-parse --git-dir >/dev/null 2>&1; then
if [[ -n "${VERSION_ARG:-}" ]]; then
local clean_ver="${VERSION_ARG#v}"
if git_try_checkout "$clean_ver"; then
VERSION="$clean_ver"
else
echo "[WARN] Tag '${VERSION_ARG}' not found."
apply_channel_or_keep "$(choose_channel)"
fi
else
apply_channel_or_keep "$(choose_channel)"
fi
else
echo "Current directory is not a git repo; proceeding on current tree."
VERSION="${VERSION_ARG:-0.0.0}"
fi
VERSION="${VERSION#v}"
echo "[*] GUI version resolved as: ${VERSION}"
}
xray_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-loongarch64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-loong64.zip" ;;
*) return 1 ;;
esac
}
singbox_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-loongarch64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-loong64.tar.gz" ;;
*) return 1 ;;
esac
}
bundle_url_for_rid() {
local rid="$1"
case "$rid" in
linux-loongarch64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-loong64.zip" ;;
*) return 1 ;;
esac
}
download_xray() {
local outdir="$1"
local rid="$2"
local ver="${XRAY_VER:-}"
local url=""
local tmp=""
mkdir -p "$outdir"
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' \
| sed -E 's/.*"v([^"]+)".*/\1/' \
| head -n1)" || true
fi
[[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; }
url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; }
echo "[+] Download xray: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; }
unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; }
install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; }
rm -rf "$tmp"
}
download_singbox() {
local outdir="$1"
local rid="$2"
local ver="${SING_VER:-}"
local url=""
local tmp=""
local bin=""
local cronet=""
mkdir -p "$outdir"
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' \
| sed -E 's/.*"v([^"]+)".*/\1/' \
| head -n1)" || true
fi
[[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; }
url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; }
echo "[+] Download sing-box: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; }
tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; }
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)"
[[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; rm -rf "$tmp"; return 1; }
install -m 755 "$bin" "$outdir/sing-box" || { rm -rf "$tmp"; return 1; }
cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)"
[[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true
rm -rf "$tmp"
}
unify_geo_layout() {
local outroot="$1"
local n
local names=(
geosite.dat
geoip.dat
geoip-only-cn-private.dat
Country.mmdb
geoip.metadb
)
mkdir -p "$outroot/bin"
for n in "${names[@]}"; do
if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
done
}
download_geo_assets() {
local outroot="$1"
local bin_dir="$outroot/bin"
local srss_dir="$bin_dir/srss"
local f=""
mkdir -p "$bin_dir" "$srss_dir"
echo "[+] Download Xray Geo to ${bin_dir}"
curl -fsSL -o "$bin_dir/geosite.dat" "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat"
curl -fsSL -o "$bin_dir/geoip.dat" "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat"
curl -fsSL -o "$bin_dir/geoip-only-cn-private.dat" "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat"
curl -fsSL -o "$bin_dir/Country.mmdb" "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
echo "[+] Download sing-box rule DB & rule-sets"
curl -fsSL -o "$bin_dir/geoip.metadb" "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb"
for f in geoip-private.srs geoip-cn.srs geoip-facebook.srs geoip-fastly.srs geoip-google.srs geoip-netflix.srs geoip-telegram.srs geoip-twitter.srs; do
curl -fsSL -o "$srss_dir/$f" "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geoip/$f"
done
for f in geosite-cn.srs geosite-gfw.srs geosite-google.srs geosite-greatfire.srs geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do
curl -fsSL -o "$srss_dir/$f" "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geosite/$f"
done
unify_geo_layout "$outroot"
}
populate_assets_zip_mode() {
local outroot="$1"
local rid="$2"
local url=""
local tmp=""
local nested_dir=""
url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; }
echo "[+] Try v2rayN bundle archive: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; }
unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; return 1; }
if [[ -d "$tmp/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$tmp/bin/" "$outroot/bin/"
else
rsync -a "$tmp/" "$outroot/"
fi
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
if [[ -n "$nested_dir" && -d "$nested_dir/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$nested_dir/bin/" "$outroot/bin/"
rm -rf "$nested_dir"
fi
unify_geo_layout "$outroot"
rm -rf "$tmp"
echo "[+] Bundle extracted to $outroot"
}
populate_assets_netcore_mode() {
local outroot="$1"
local rid="$2"
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)"
}
stage_runtime_assets() {
local outroot="$1"
local rid="$2"
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
if [[ "$FORCE_NETCORE" -eq 0 ]]; then
if populate_assets_zip_mode "$outroot" "$rid"; then
echo "[*] Using v2rayN bundle archive."
else
echo "[*] Bundle failed, fallback to separate core + rules."
populate_assets_netcore_mode "$outroot" "$rid"
fi
else
echo "[*] --netcore specified: use separate core + rules."
populate_assets_netcore_mode "$outroot" "$rid"
fi
}
describe_target() {
local short="$1"
case "$short" in
loongarch64) printf '%s\n%s\n%s\n' "linux-loongarch64" "loongarch64" "loongarch64" ;;
*) echo "Unknown arch '$short' (use loongarch64)" >&2; return 1 ;;
esac
}
publish_binary() {
local rid="$1"
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true
dotnet restore "$PROJECT"
dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true
}
write_spec_file() {
local specfile="$1"
cat > "$specfile" <<'SPEC'
%global debug_package %{nil}
%undefine _debuginfo_subpackages
%undefine _debugsource_packages
%global __requires_exclude ^liblttng-ust\.so\..*$
Name: v2rayN
Version: __VERSION__
Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues
ExclusiveArch: loongarch64
Source0: __PKGROOT__.tar.gz
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.39
Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 9.4
Requires: bash >= 5.2.21
Requires: freetype >= 2.13
%description
v2rayN Linux for Red Hat Enterprise Linux
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS
For more information, Please visit our website
https://github.com/2dust/v2rayN
%prep
%setup -q -n __PKGROOT__
%build
%install
install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/
find %{buildroot}/opt/v2rayN -type d -exec chmod 0755 {} +
find %{buildroot}/opt/v2rayN -type f -exec chmod 0644 {} +
[ -f %{buildroot}/opt/v2rayN/v2rayN ] && chmod 0755 %{buildroot}/opt/v2rayN/v2rayN || :
install -dm0755 %{buildroot}%{_bindir}
install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash
set -euo pipefail
DIR="/opt/v2rayN"
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi
for dll in v2rayN.Desktop.dll v2rayN.dll; do
if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi
done
echo "v2rayN launcher: no executable found in $DIR" >&2
ls -l "$DIR" >&2 || true
exit 1
EOF
install -dm0755 %{buildroot}%{_datadir}/applications
install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=v2rayN
Comment=v2rayN for Red Hat Enterprise Linux
Exec=v2rayn
Icon=v2rayn
Terminal=false
Categories=Network;
EOF
install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
%post
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
%postun
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
%files
%{_bindir}/v2rayn
/opt/v2rayN
%{_datadir}/applications/v2rayn.desktop
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
SPEC
sed -i "s/__VERSION__/${VERSION}/g" "$specfile"
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$specfile"
}
package_binary() {
local short="$1"
local rid="$2"
local rpm_target="$3"
local archdir="$4"
local pubdir=""
local workdir=""
local specfile=""
local sourcedir=""
local specdir=""
local project_dir=""
local icon_candidate=""
local f=""
pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish"
[[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; }
workdir="$(mktemp -d)"
trap '[[ -n "${workdir:-}" ]] && rm -rf "$workdir"' RETURN
mkdir -p "$workdir/$PKGROOT"
cp -a "$pubdir/." "$workdir/$PKGROOT/"
project_dir="$(cd "$(dirname "$PROJECT")" && pwd)"
icon_candidate="$project_dir/v2rayN.png"
[[ -f "$icon_candidate" ]] || { echo "Required icon not found: $icon_candidate"; return 1; }
cp "$icon_candidate" "$workdir/$PKGROOT/v2rayn.png"
stage_runtime_assets "$workdir/$PKGROOT" "$rid"
rpmdev-setuptree
sourcedir="${RPM_TOPDIR}/SOURCES"
specdir="${RPM_TOPDIR}/SPECS"
specfile="${specdir}/v2rayN.spec"
mkdir -p "$sourcedir" "$specdir"
tar -C "$workdir" -czf "$sourcedir/$PKGROOT.tar.gz" "$PKGROOT"
write_spec_file "$specfile"
rpmbuild -ba "$specfile" --target "$rpm_target"
echo "Build done for $short. RPM at:"
for f in "${RPM_TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do
[[ -e "$f" ]] || continue
echo " $f"
BUILT_RPMS+=("$f")
done
}
select_targets() {
printf '%s\n' loongarch64
}
build_one_target() {
local short="$1"
local meta=()
local rid=""
local rpm_target=""
local archdir=""
mapfile -t meta < <(describe_target "$short") || return 1
rid="${meta[0]}"
rpm_target="${meta[1]}"
archdir="${meta[2]}"
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
publish_binary "$rid"
package_binary "$short" "$rid" "$rpm_target" "$archdir"
}
print_summary() {
local rp=""
echo ""
echo "================ Build Summary ================"
if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then
for rp in "${BUILT_RPMS[@]}"; do
echo "$rp"
done
else
echo "No RPMs detected in summary (check build logs above)."
fi
echo "=============================================="
}
main() {
local targets=()
local arch=""
parse_args "$@"
detect_environment
install_dependencies
prepare_workspace
resolve_version
mapfile -t targets < <(select_targets)
for arch in "${targets[@]}"; do
build_one_target "$arch"
done
print_summary
}
main "$@"

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_RISCV_VERSION="10.0.108"
DOTNET_RISCV_VERSION="10.0.110"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -501,7 +501,7 @@ write_spec_file() {
Name: v2rayN
Version: __VERSION__
Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (riscv64)
Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues
@@ -509,13 +509,13 @@ ExclusiveArch: riscv64
Source0: __PKGROOT__.tar.gz
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.34
Requires: fontconfig >= 2.13.1
Requires: glibc >= 2.39
Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32
Requires: bash >= 5.1
Requires: freetype >= 2.10
Requires: coreutils >= 9.4
Requires: bash >= 5.2.21
Requires: freetype >= 2.13
%description
v2rayN Linux for Red Hat Enterprise Linux

View File

@@ -490,7 +490,7 @@ write_spec_file() {
Name: v2rayN
Version: __VERSION__
Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64)
Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues
@@ -498,13 +498,13 @@ ExclusiveArch: aarch64 x86_64
Source0: __PKGROOT__.tar.gz
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.34
Requires: fontconfig >= 2.13.1
Requires: glibc >= 2.39
Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32
Requires: bash >= 5.1
Requires: freetype >= 2.10
Requires: coreutils >= 9.4
Requires: bash >= 5.2.21
Requires: freetype >= 2.13
%description
v2rayN Linux for Red Hat Enterprise Linux
@@ -698,4 +698,4 @@ main() {
print_summary
}
main "$@"
main "$@"

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.22.3</Version>
<Version>7.24.3</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -1,39 +1,39 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.16" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.16" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
<PackageVersion Include="CliWrap" Version="3.10.1" />
<PackageVersion Include="Downloader" Version="5.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.27" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.27" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.1.7" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4-preview.1.1" />
</ItemGroup>
</Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.0.3" />
<PackageVersion Include="CliWrap" Version="3.10.2" />
<PackageVersion Include="Downloader" Version="5.9.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.28" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0" />
<PackageVersion Include="NLog" Version="6.1.4" />
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3.10" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup>
</Project>

View File

@@ -17,7 +17,7 @@ internal static class CoreConfigTestFactory
{
return new Config
{
CoreBasicItem = new CoreBasicItem { Loglevel = "warning", MuxEnabled = false },
CoreBasicItem = new CoreBasicItem { Loglevel = "warning" },
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
KcpItem = new KcpItem(),
GrpcItem = new GrpcItem(),
@@ -59,7 +59,7 @@ internal static class CoreConfigTestFactory
},
WebDavItem = new WebDavItem(),
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 =
[
new InItem
@@ -84,6 +84,7 @@ internal static class CoreConfigTestFactory
ParallelQuery = false,
Strategy4Freedom = Global.AsIs,
Strategy4Proxy = Global.AsIs,
Strategy4ProxyDial = Global.AsIs,
},
IndexId = string.Empty,
SubIndexId = string.Empty,
@@ -130,6 +131,25 @@ 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 CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
IEnumerable<string> childIndexIds)
{
@@ -187,7 +207,7 @@ internal static class CoreConfigTestFactory
SimpleDnsItem = config.SimpleDNSItem,
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
FullConfigTemplate = null,
IsTunEnabled = false,
IsTunEnabled = config.TunModeItem.EnableTun,
ProtectDomainList = [],
};
}
@@ -206,4 +226,12 @@ internal static class CoreConfigTestFactory
config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
return config;
}
public static Config CreateConfigWithTunRouteExcludeAddress(ECoreType coreType)
{
var config = CreateConfig(coreType);
config.TunModeItem.EnableTun = true;
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
return config;
}
}

View File

@@ -1,8 +1,10 @@
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;
@@ -554,7 +556,74 @@ public class CoreConfigSingboxServiceTests
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Global.ToString());
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Direct.ToString());
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
}
[Fact]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{
var shareLink =
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreTypeItem =
[
new CoreTypeItem { ConfigType = EConfigType.Hysteria2, CoreType = ECoreType.sing_box }
];
CoreConfigTestFactory.BindAppManagerConfig(config);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2");
proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public");
proxy.realm.realm_id.Should().Be("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull();
}
[Fact]
public void 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();
result.Success.Should().BeTrue($"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();
foreach (var address in tun.address!)
{
var prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]);
var isIpv6 = address.Contains(':');
prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
$"'{address}' must leave room for the peer address the system stack derives");
}
}
}

View File

@@ -28,6 +28,39 @@ public class CoreConfigV2rayServiceTests
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
}
[Fact]
public void 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();
result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http");
outbound.settings.address?.ToString().Should().Be("proxy.example.com");
outbound.settings.port.Should().Be(8080);
outbound.settings.user.Should().Be("user");
outbound.settings.pass.Should().Be("pass");
outbound.settings.level.Should().Be(1);
outbound.settings.headers.Should().NotBeNull();
var headers = JsonUtils.ParseJson(outbound.settings.headers.ToString());
headers["User-Agent"]!.GetValue<string>().Should().Be("v2rayN");
headers["Set-Cookie"]!.AsArray()
.Select(item => item!.GetValue<string>())
.Should().Equal("a=1", "b=2");
outbound.settings.servers.Should().BeNull();
outbound.settings.vnext.Should().BeNull();
}
[Fact]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{
@@ -534,6 +567,29 @@ public class CoreConfigV2rayServiceTests
var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom");
directOutbound.Should().NotBeNull();
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4");
directOutbound!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4");
}
[Fact]
public void GenerateClientConfigContent_TunRouteExcludeAddress()
{
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
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();
result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
}
}

View File

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

View File

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

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
{
protected static Config? _config;
protected Func<EViewAction, object?, Task<bool>>? _updateView;
}

View File

@@ -87,6 +87,6 @@ public static class CountryExtension
return null;
}
return CountryEmojiMap.TryGetValue(countryCode, out var emoji) ? emoji : null;
return CountryEmojiMap.GetValueOrDefault(countryCode);
}
}

View File

@@ -47,7 +47,7 @@ public static class Extension
public static string TrimEx(this string? value)
{
return value == null ? string.Empty : value.Trim();
return value?.Trim() ?? string.Empty;
}
public static string RemovePrefix(this string value, char prefix)
@@ -118,4 +118,20 @@ public static class Extension
destination.Add(item);
}
}
/// <summary>
/// Replace all cross-platform newline characters with the specified string
/// </summary>
public static string ReplaceLineBreaks(this string input, string replacement)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
// You must replace \r\n first, and then replace the single characters \r and \n.
return input.Replace("\r\n", replacement)
.Replace("\r", replacement)
.Replace("\n", replacement);
}
}

View File

@@ -202,7 +202,7 @@ public static class FileUtils
}
}
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine)
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine, string? contains = null)
{
try
{
@@ -214,6 +214,10 @@ public static class FileUtils
{
continue;
}
if (contains.IsNotEmpty() && !file.Name.Contains(contains))
{
continue;
}
file.Delete();
}
}

View File

@@ -51,7 +51,7 @@ public class Utils
try
{
str = str.Replace(Environment.NewLine, string.Empty);
str = str.ReplaceLineBreaks(string.Empty);
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
}
catch (Exception ex)
@@ -114,9 +114,7 @@ public class Utils
}
plainText = plainText.Trim()
.Replace(Environment.NewLine, "")
.Replace("\n", "")
.Replace("\r", "")
.ReplaceLineBreaks("")
.Replace('_', '/')
.Replace('-', '+')
.Replace(" ", "");
@@ -321,7 +319,32 @@ public class Utils
return text;
}
return text.Replace("", ",").Replace(Environment.NewLine, ",");
return text.Replace("", ",")
.Replace(" ", "")
.ReplaceLineBreaks(",");
}
public static string ParseProcess(string text)
{
if (text.IsNullOrEmpty())
{
return string.Empty;
}
if (text.StartsWith('"'))
{
text = text[1..];
}
if (text.EndsWith('"'))
{
text = text[..^1];
}
return List2String(text.Replace("", ",")
.Replace("\\", "/")
.ReplaceLineBreaks(",")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.TrimEx())
.Where(x => x.IsNotEmpty())
.ToList());
}
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
@@ -462,12 +485,17 @@ public class Utils
public static string? DomainStrategy4Sbox(string? strategy)
{
if (strategy is null)
{
return null;
}
return strategy switch
{
not null when strategy.StartsWith("UseIPv4") => "prefer_ipv4",
not null when strategy.StartsWith("UseIPv6") => "prefer_ipv6",
not null when strategy.StartsWith("ForceIPv4") => "ipv4_only",
not null when strategy.StartsWith("ForceIPv6") => "ipv6_only",
_ when strategy.StartsWith("UseIPv6") => "prefer_ipv6",
_ when strategy.StartsWith("UseIP") => "prefer_ipv4",
_ when strategy.StartsWith("ForceIPv6") => "ipv6_only",
_ when strategy.StartsWith("ForceIP") => "ipv4_only",
_ => null
};
}
@@ -576,6 +604,58 @@ public class Utils
}
}
public static bool TryParseRange(string? input, int min, int max, out int from, out int to)
{
from = to = 0;
if (string.IsNullOrWhiteSpace(input))
{
return true;
}
var parts = input.Split('-');
if (parts.Length == 1)
{
if (!int.TryParse(parts[0], out from))
{
return false;
}
to = from;
return from >= min && to <= max;
}
if (parts.Length != 2
|| !int.TryParse(parts[0], out from)
|| !int.TryParse(parts[1], out to))
{
return false;
}
return from >= min && to <= max && from <= to;
}
public static bool TryParseMaxSplit(string? input, int min, int max, out int from, out int to)
{
from = to = 0;
if (string.IsNullOrWhiteSpace(input))
{
return true;
}
var parts = input.Split('-');
if (parts.Length == 1)
{
if (!int.TryParse(parts[0], out from))
{
return false;
}
to = from;
return from >= min && to <= max;
}
if (parts.Length != 2
|| !int.TryParse(parts[0], out from)
|| !int.TryParse(parts[1], out to))
{
return false;
}
return from >= min && to <= max && from <= to;
}
public static bool IsPrivateNetwork(string ip)
{
if (IPAddress.TryParse(ip, out var address))
@@ -716,6 +796,24 @@ public class Utils
return (endpoints, connections);
}
public static bool IsLocalIP(string ipAddress)
{
if (!IPAddress.TryParse(ipAddress, out var targetAddress))
{
return false;
}
return NetworkInterface.GetAllNetworkInterfaces()
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
.Any(ua => ua.Address.Equals(targetAddress));
}
public static bool ContainsInterfaceName(string inInterfaceName)
{
return NetworkInterface.GetAllNetworkInterfaces()
.Any(ni => ni.Name.Equals(inInterfaceName, StringComparison.OrdinalIgnoreCase));
}
#endregion Speed Test
#region Miscellaneous

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,16 +2,9 @@ namespace ServiceLib.Events;
public static class AppEvents
{
public static readonly EventChannel<Unit> ReloadRequested = 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<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<string> SendSnackMsgRequested = new();
@@ -20,12 +13,5 @@ public static class AppEvents
public static readonly EventChannel<Unit> AppExitRequested = 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();
}

View File

@@ -2,8 +2,6 @@ namespace ServiceLib;
public class Global
{
#region const
public const string AppName = "v2rayN";
public const string GithubUrl = "https://github.com";
public const string GithubApiUrl = "https://api.github.com/repos";
@@ -42,6 +40,11 @@ public class Global
public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh";
public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
public const string ChromeRootCertFileName = NamespaceSample + "chrome_roots_pem";
public const string MozillaRootCertFileName = NamespaceSample + "mozilla_roots_pem";
public const string ChromeRootProvider = "chrome";
public const string MozillaRootProvider = "mozilla";
public const string DefaultSecurity = "auto";
public const string DefaultNetwork = "raw";
@@ -55,7 +58,7 @@ public class Global
public const string DnsOutboundTag = "dns";
public const string DnsTag = "dns-module";
public const string DirectDnsTag = "direct-dns";
public const string BalancerTagSuffix = "-round";
public const string BalancerTagSuffix = "-balancer";
public const string StreamSecurity = "tls";
public const string StreamSecurityReality = "reality";
public const string Loopback = "127.0.0.1";
@@ -90,6 +93,9 @@ public class Global
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
public const int SpeedTestPageSize = 1000;
public const string LinuxBash = "/bin/bash";
public const string StringTrue = "true";
public const string StringFalse = "false";
public const int SqliteMaxBatchSize = 10000;
public const string SingboxDirectDNSTag = "direct_dns";
public const string SingboxRemoteDNSTag = "remote_dns";
@@ -98,9 +104,7 @@ public class Global
public const string SingboxFakeDNSTag = "fake_dns";
public const int Hysteria2DefaultHopInt = 30;
public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset";
public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$";
public static readonly List<string> PolicyGroupDefaultFilterList =
@@ -149,6 +153,9 @@ public class Global
public static readonly List<string> SpeedTestUrls =
[
@"https://cachefly.cachefly.net/50mb.test",
@"https://cachefly.cachefly.net/100mb.test",
@"https://cachefly.cachefly.net/1mb.test",
@"https://cachefly.cachefly.net/10mb.test",
@"https://speed.cloudflare.com/__down?bytes=10000000",
@"https://speed.cloudflare.com/__down?bytes=50000000",
@"https://speed.cloudflare.com/__down?bytes=99999999",
@@ -157,6 +164,8 @@ public class Global
public static readonly List<string> SpeedPingTestUrls =
[
@"https://www.google.com/generate_204",
@"https://www.youtube.com/generate_204",
@"https://www.googlevideo.com/generate_204",
@"https://www.gstatic.com/generate_204",
@"https://www.apple.com/library/test/success.html",
@"http://www.msftconnecttest.com/connecttest.txt"
@@ -203,6 +212,10 @@ public class Global
public const string Hysteria2ProtocolShare = "hy2://";
public const string Hysteria2RealmProtocolShare = "hysteria2+realm://";
public const string Hysteria2HttpRealmProtocolShare = "hysteria2+realm+http://";
public const string NaiveHttpsProtocolShare = "naive+https://";
public const string NaiveQuicProtocolShare = "naive+quic://";
@@ -325,12 +338,12 @@ public class Global
public static readonly Dictionary<string, string> KcpHeaderMaskMap = new()
{
{ "srtp", "header-srtp" },
{ "utp", "header-utp" },
{ "wechat-video", "header-wechat" },
{ "dtls", "header-dtls" },
{ "wireguard", "header-wireguard" },
{ "dns", "header-dns" }
{ "srtp", "srtp" },
{ "utp", "utp" },
{ "wechat-video", "wechat" },
{ "dtls", "dtls" },
{ "wireguard", "wireguard" },
{ "dns", "dns" }
};
public static readonly List<string> CoreTypes =
@@ -399,6 +412,16 @@ public class Global
""
];
public static readonly List<string> FragmentPacketsOptions =
[
"tlshello",
"1-1",
"1-2",
"1-3",
"1-4",
"1-5"
];
public static readonly List<string> UserAgent =
[
"chrome",
@@ -416,13 +439,6 @@ public class Global
"stream-one"
];
public static readonly List<string> AllowInsecure =
[
"true",
"false",
""
];
public static readonly List<string> DomainStrategy =
[
"AsIs",
@@ -473,10 +489,11 @@ public class Global
"zh-Hans",
"zh-Hant",
"en",
"fa-Ir",
"fa",
"fr",
"ru",
"hu"
"hu",
"id"
];
public static readonly List<string> Alpns =
@@ -536,7 +553,6 @@ public class Global
"http",
"tls",
"quic",
"fakedns",
];
public static readonly List<int> TunMtus =
@@ -666,6 +682,14 @@ public class Global
"mcbe:bedrock.talonmc.net",
];
public static readonly List<string> DefaultRealmStunList =
[
"turn.cloudflare.com:3478",
"stun.nextcloud.com:3478",
"stun.sip.us:3478",
"global.stun.twilio.com:3478",
];
public static readonly List<string> OutboundTags =
[
ProxyTag,
@@ -680,15 +704,15 @@ public class Global
{ "one.one.one.one", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
{ "1dot1dot1dot1.cloudflare-dns.com", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
{ "cloudflare-dns.com", ["104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9"] },
{ "dns.cloudflare.com", ["104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5"] },
{ "dns.cloudflare.com", ["162.159.61.8", "172.64.41.8", "2a06:98c1:52::8", "2803:f800:53::8"] },
{ "dot.pub", ["1.12.12.12", "120.53.53.53"] },
{ "doh.pub", ["1.12.12.12", "120.53.53.53"] },
{ "dns.quad9.net", ["9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"] },
{ "dns.yandex.net", ["77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff"] },
{ "dns.sb", ["185.222.222.222", "2a09::"] },
{ "dns.sb", ["45.11.45.11", "185.222.222.222", "2a09::", "2a11::"] },
{ "dns.umbrella.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
{ "dns.sse.cisco.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
{ "engage.cloudflareclient.com", ["162.159.192.1"] }
{ "engage.cloudflareclient.com", ["162.159.192.1", "2606:4700:d0::a29f:c001"] }
};
public static readonly List<string> ExpectedIPs =
@@ -708,5 +732,42 @@ public class Global
"reply",
];
#endregion const
public static readonly List<string> FakeIPRanges =
[
"198.18.0.0/15",
"11.0.0.0/8",
];
public static readonly List<string> RootCertProviders =
[
"system",
ChromeRootProvider,
MozillaRootProvider,
];
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

@@ -30,8 +30,8 @@ global using ServiceLib.Handler.Fmt;
global using ServiceLib.Handler.SysProxy;
global using ServiceLib.Helper;
global using ServiceLib.Manager;
global using ServiceLib.Models.CoreConfigs;
global using ServiceLib.Models.Configs;
global using ServiceLib.Models.CoreConfigs;
global using ServiceLib.Models.Dto;
global using ServiceLib.Models.Entities;
global using ServiceLib.Resx;

View File

@@ -111,7 +111,8 @@ public static class AutoStartupHandler
task.Settings.RunOnlyIfIdle = false;
task.Settings.IdleSettings.StopOnIdleEnd = false;
task.Settings.ExecutionTimeLimit = TimeSpan.Zero;
task.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger { UserId = logonUser, Delay = TimeSpan.FromSeconds(30) });
task.Settings.Priority = ProcessPriorityClass.Normal;
task.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger { UserId = logonUser });
task.Principal.RunLevel = Microsoft.Win32.TaskScheduler.TaskRunLevel.Highest;
task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName)));

View File

@@ -6,8 +6,8 @@ public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeVali
}
/// <summary>
/// Holds the results of a full context build, including the main context and an optional
/// pre-socks context (e.g. for TUN protection or pre-socks chaining).
/// Holds the results of a full context build, including the main context and an optional
/// pre-socks context (e.g. for TUN protection or pre-socks chaining).
/// </summary>
public record CoreConfigContextBuilderAllResult(
CoreConfigContextBuilderResult MainResult,
@@ -17,8 +17,8 @@ public record CoreConfigContextBuilderAllResult(
public bool Success => MainResult.Success && (PreSocksResult?.Success ?? true);
/// <summary>
/// Merges all errors and warnings from the main result and the optional pre-socks result
/// into a single <see cref="NodeValidatorResult"/> for unified notification.
/// Merges all errors and warnings from the main result and the optional pre-socks result
/// into a single <see cref="NodeValidatorResult" /> for unified notification.
/// </summary>
public NodeValidatorResult CombinedValidatorResult => new(
[.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []],
@@ -28,14 +28,14 @@ public record CoreConfigContextBuilderAllResult(
public class CoreConfigContextBuilder
{
/// <summary>
/// Builds a <see cref="CoreConfigContext"/> for the given node, resolves its proxy map,
/// and processes outbound nodes referenced by routing rules.
/// Builds a <see cref="CoreConfigContext" /> for the given node, resolves its proxy map,
/// and processes outbound nodes referenced by routing rules.
/// </summary>
public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node)
{
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
var context = new CoreConfigContext()
var context = new CoreConfigContext
{
Node = node,
RunCoreType = runCoreType,
@@ -61,7 +61,8 @@ public class CoreConfigContextBuilder
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? [];
foreach (var ruleItem in rules.Where(ruleItem => ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
foreach (var ruleItem in rules.Where(ruleItem =>
ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
{
if (ruleItem.OutboundTag.IsNullOrEmpty())
{
@@ -72,7 +73,8 @@ public class CoreConfigContextBuilder
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
if (ruleOutboundNode == null)
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound, ruleItem.Remarks, ruleItem.OutboundTag));
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound,
ruleItem.Remarks, ruleItem.OutboundTag));
ruleItem.OutboundTag = Global.ProxyTag;
continue;
}
@@ -83,7 +85,8 @@ public class CoreConfigContextBuilder
if (!ruleNodeValidatorResult.Success)
{
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e =>
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag, e)));
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag,
e)));
ruleItem.OutboundTag = Global.ProxyTag;
continue;
}
@@ -91,14 +94,50 @@ public class CoreConfigContextBuilder
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
}
}
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{
var appConfig = JsonUtils.DeepCopy(config);
var routeExcludeAddressList = new List<string>();
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
{
try
{
IPNetwork2.Parse(addr);
routeExcludeAddressList.Add(addr);
}
catch
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr));
}
}
appConfig.TunModeItem.RouteExcludeAddress = routeExcludeAddressList;
context = context with { AppConfig = appConfig };
}
if (!context.AppConfig.CoreBasicItem.SendThrough.IsNullOrEmpty()
&& !Utils.IsLocalIP(context.AppConfig.CoreBasicItem.SendThrough))
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsSendThrough));
var appConfig = JsonUtils.DeepCopy(config);
appConfig.CoreBasicItem.SendThrough = string.Empty;
context = context with { AppConfig = appConfig };
}
if (!context.AppConfig.CoreBasicItem.BindInterface.IsNullOrEmpty()
&& (!Utils.ContainsInterfaceName(context.AppConfig.CoreBasicItem.BindInterface)
|| !(context.IsTunEnabled || context.IsWindows)))
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsBindInterface));
var appConfig = JsonUtils.DeepCopy(config);
appConfig.CoreBasicItem.BindInterface = string.Empty;
context = context with { AppConfig = appConfig };
}
return new CoreConfigContextBuilderResult(context, validatorResult);
}
/// <summary>
/// Builds the main <see cref="CoreConfigContext"/> for <paramref name="node"/> and, when
/// the main build succeeds, also builds the optional pre-socks context required for TUN
/// protection or pre-socks proxy chaining.
/// Builds the main <see cref="CoreConfigContext" /> for <paramref name="node" /> and, when
/// the main build succeeds, also builds the optional pre-socks context required for TUN
/// protection or pre-socks proxy chaining.
/// </summary>
public static async Task<CoreConfigContextBuilderAllResult> BuildAll(Config config, ProfileItem node)
{
@@ -118,16 +157,31 @@ public class CoreConfigContextBuilder
{
Context = mainResult.Context with
{
IsTunEnabled = false, // main core doesn't handle tun directly when pre-socks is used
IsTunEnabled = false,
// main core doesn't handle tun directly when pre-socks is used
ProtectDomainList = [.. mainResult.Context.ProtectDomainList, .. preResult.Context.ProtectDomainList],
}
},
};
if (mainResult.Context.IsTunEnabled
&& mainResult.Context.AppConfig.TunModeItem.StrictRoute)
{
var appConfig = JsonUtils.DeepCopy(mainResult.Context.AppConfig);
appConfig.CoreBasicItem.BindInterface = string.Empty;
appConfig.CoreBasicItem.SendThrough = string.Empty;
resolvedMainResult = resolvedMainResult with
{
Context = resolvedMainResult.Context with
{
AppConfig = appConfig,
},
};
}
return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult);
}
/// <summary>
/// Determines whether a pre-socks context is required for <paramref name="nodeContext"/>
/// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed.
/// Determines whether a pre-socks context is required for <paramref name="nodeContext" />
/// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed.
/// </summary>
private static async Task<CoreConfigContextBuilderResult?> BuildPreSocksIfNeeded(CoreConfigContext nodeContext)
{
@@ -139,12 +193,17 @@ public class CoreConfigContextBuilder
if (preSocksItem != null)
{
var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem);
var protectCoreTypeList = new HashSet<ECoreType>(nodeContext.ProtectCoreTypeList) { nodeContext.RunCoreType };
return preSocksResult with
{
Context = preSocksResult.Context with
{
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
}
ProtectDomainList =
[.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
ProtectCoreTypeList = protectCoreTypeList,
},
};
}
@@ -152,8 +211,8 @@ public class CoreConfigContextBuilder
}
/// <summary>
/// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain.
/// Returns the effective (possibly replaced) node and the validation result.
/// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain.
/// Returns the effective (possibly replaced) node and the validation result.
/// </summary>
public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context,
ProfileItem node,
@@ -188,12 +247,13 @@ public class CoreConfigContextBuilder
}
/// <summary>
/// If the node's subscription defines prev/next profiles, creates a virtual
/// <see cref="EConfigType.ProxyChain"/> node that wraps them together.
/// Returns <c>null</c> as the chain item when no chain is needed.
/// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
/// If the node's subscription defines prev/next profiles, creates a virtual
/// <see cref="EConfigType.ProxyChain" /> node that wraps them together.
/// Returns <c>null</c> as the chain item when no chain is needed.
/// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
/// </summary>
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)> BuildSubscriptionChainNodeAsync(ProfileItem node)
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)>
BuildSubscriptionChainNodeAsync(ProfileItem node)
{
var result = NodeValidatorResult.Empty();
@@ -234,7 +294,7 @@ public class CoreConfigContextBuilder
}
// Build new proxy chain node
var chainNode = new ProfileItem()
var chainNode = new ProfileItem
{
IndexId = $"inner-{Utils.GetGuid(false)}",
ConfigType = EConfigType.ProxyChain,
@@ -252,8 +312,8 @@ public class CoreConfigContextBuilder
}
/// <summary>
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync"/> or
/// <see cref="RegisterSingleNodeAsync"/> based on the node's config type.
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync" /> or
/// <see cref="RegisterSingleNodeAsync" /> based on the node's config type.
/// </summary>
private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node)
{
@@ -261,15 +321,12 @@ public class CoreConfigContextBuilder
{
return await RegisterGroupNodeAsync(context, node);
}
else
{
return RegisterSingleNodeAsync(context, node);
}
return RegisterSingleNodeAsync(context, node);
}
/// <summary>
/// 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.
/// 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.
/// </summary>
private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
{
@@ -333,8 +390,8 @@ public class CoreConfigContextBuilder
}
/// <summary>
/// Entry point for registering a group node. Initialises the visited/ancestor sets
/// and delegates to <see cref="TraverseGroupNodeAsync"/>.
/// Entry point for registering a group node. Initialises the visited/ancestor sets
/// and delegates to <see cref="TraverseGroupNodeAsync" />.
/// </summary>
private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context,
ProfileItem node)
@@ -350,9 +407,9 @@ public class CoreConfigContextBuilder
}
/// <summary>
/// Recursively walks the children of a group node, registering valid leaf nodes
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup"/> and
/// deduplicates shared nodes via <paramref name="globalVisitedGroup"/>.
/// Recursively walks the children of a group node, registering valid leaf nodes
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup" /> and
/// deduplicates shared nodes via <paramref name="globalVisitedGroup" />.
/// </summary>
private static async Task<NodeValidatorResult> TraverseGroupNodeAsync(
CoreConfigContext context,
@@ -416,13 +473,10 @@ public class CoreConfigContextBuilder
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
return childNodeValidatorResult;
}
else
{
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
childNodeValidatorResult.Errors.Clear();
}
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
childNodeValidatorResult.Errors.Clear();
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList), });
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList) });
context.AllProxiesMap[node.IndexId] = node;
return childNodeValidatorResult;
}

View File

@@ -29,35 +29,6 @@ public class NodeValidator
return v.ToResult();
}
private class ValidationContext
{
public List<string> Errors { get; } = [];
public List<string> Warnings { get; } = [];
public void Error(string message)
{
Errors.Add(message);
}
public void Warning(string message)
{
Warnings.Add(message);
}
public void Assert(bool condition, string errorMsg)
{
if (!condition)
{
Error(errorMsg);
}
}
public NodeValidatorResult ToResult()
{
return new NodeValidatorResult(Errors, Warnings);
}
}
private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v)
{
if (item.ConfigType is EConfigType.Custom)
@@ -72,8 +43,8 @@ public class NodeValidator
}
// Basic Property Validation
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address"));
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port"));
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbAddress));
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, ResUI.TbPort));
// Network & Core Logic
var net = item.GetNetwork();
@@ -104,41 +75,76 @@ public class NodeValidator
{
case EConfigType.VMess:
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
string.Format(ResUI.MsgInvalidProperty, "Password"));
string.Format(ResUI.MsgInvalidProperty, ResUI.TbId));
break;
case EConfigType.VLESS:
v.Assert(
!item.Password.IsNullOrEmpty()
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30),
string.Format(ResUI.MsgInvalidProperty, "Password")
string.Format(ResUI.MsgInvalidProperty, ResUI.TbId5)
);
v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
string.Format(ResUI.MsgInvalidProperty, "Flow"));
string.Format(ResUI.MsgInvalidProperty, ResUI.TbFlow5));
break;
case EConfigType.Shadowsocks:
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password"));
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbId3));
v.Assert(
!string.IsNullOrEmpty(protocolExtra.SsMethod) &&
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
string.Format(ResUI.MsgInvalidProperty, "SsMethod"));
string.Format(ResUI.MsgInvalidProperty, ResUI.TbSecurity3));
break;
}
if (coreType is ECoreType.Xray
&& (protocolExtra.Flow ?? string.Empty).StartsWith("xtls", StringComparison.OrdinalIgnoreCase)
&& item.MuxEnabled == true)
{
v.Warning(string.Format(ResUI.MsgOptionsConflict, "XTLS", "Mux.Cool"));
}
if (item.GetNetwork() is nameof(ETransport.ws)
&& item.EchConfigList.IsNullOrEmpty()
&& item.GetAlpn()?.FirstOrDefault() == "h3")
{
v.Warning(
"WebSocket but ALPN is set to h3, the core may ignore the ALPN setting or cause unexpected issues.");
}
// TLS & Security
if (item.StreamSecurity == Global.StreamSecurity)
{
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 &&
!item.CertSha.IsNullOrEmpty())
var isCertProvided = !item.Cert.IsNullOrEmpty();
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0)
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate"));
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFullCertTips));
isCertProvided = false;
}
// Check for deprecated allowInsecure property when TLS is enabled
if (item.GetAllowInsecure()
&& item.Cert.IsNullOrEmpty()
&& item.CertSha.IsNullOrEmpty())
{
v.Warning(ResUI.MsgAllowInsecureDeprecated);
}
if ((coreType == ECoreType.Xray
&& item.GetAllowInsecure()
&& !isCertProvided
&& item.CertSha.IsNullOrEmpty())
|| (coreType == ECoreType.sing_box
&& item.GetAllowInsecure()
&& !isCertProvided))
{
v.Warning(ResUI.MsgInsecureConfiguration);
}
}
if (item.StreamSecurity == Global.StreamSecurityReality)
{
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey"));
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbPublicKey));
}
var transport = item.GetTransportExtra();
@@ -146,7 +152,7 @@ public class NodeValidator
{
if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject)
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra"));
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TransportExtra));
}
}
@@ -154,7 +160,7 @@ public class NodeValidator
{
if (JsonUtils.ParseJson(item.Finalmask) is not JsonObject)
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "Finalmask"));
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFinalmask));
}
}
}
@@ -183,4 +189,33 @@ public class NodeValidator
return null;
}
private class ValidationContext
{
public List<string> Errors { get; } = [];
public List<string> Warnings { get; } = [];
public void Error(string message)
{
Errors.Add(message);
}
public void Warning(string message)
{
Warnings.Add(message);
}
public void Assert(bool condition, string errorMsg)
{
if (!condition)
{
Error(errorMsg);
}
}
public NodeValidatorResult ToResult()
{
return new NodeValidatorResult(Errors, Warnings);
}
}
}

View File

@@ -39,15 +39,14 @@ public static class ConfigHandler
{
LogEnabled = false,
Loglevel = "warning",
MuxEnabled = false,
};
if (config.Inbound == null)
{
config.Inbound = new List<InItem>();
config.Inbound = [];
InItem inItem = new()
{
Protocol = EInboundProtocol.socks.ToString(),
Protocol = nameof(EInboundProtocol.socks),
LocalPort = 10808,
UdpEnabled = true,
SniffingEnabled = true,
@@ -60,7 +59,7 @@ public static class ConfigHandler
{
if (config.Inbound.Count > 0)
{
config.Inbound.First().Protocol = EInboundProtocol.socks.ToString();
config.Inbound.First().Protocol = nameof(EInboundProtocol.socks);
}
}
@@ -93,14 +92,18 @@ public static class ConfigHandler
EnableTun = false,
Mtu = 9000,
IcmpRouting = Global.TunIcmpRoutingPolicies.First(),
EnableLegacyProtect = false,
EnableLegacyProtect = true,
};
config.GuiItem ??= new();
if (!Global.RootCertProviders.Contains(config.GuiItem.RootCertProvider))
{
config.GuiItem.RootCertProvider = Global.RootCertProviders.First();
}
config.MsgUIItem ??= new();
config.UiItem ??= new();
config.UiItem.MainColumnItem ??= new();
config.UiItem.WindowSizeItem ??= new();
config.UiItem.MainColumnItem ??= [];
config.UiItem.WindowSizeItem ??= [];
if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
{
@@ -112,10 +115,12 @@ public static class ConfigHandler
config.ConstItem ??= new ConstItem();
config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
config.SimpleDNSItem.FakeIPRange ??= Global.FakeIPRanges.FirstOrDefault();
config.SimpleDNSItem.GlobalFakeIp ??= true;
config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault();
config.SimpleDNSItem.ServeStale ??= false;
config.SimpleDNSItem.ParallelQuery ??= false;
config.SimpleDNSItem.EnableHappyEyeballs ??= false;
config.SpeedTestItem ??= new();
if (config.SpeedTestItem.SpeedTestTimeout < 10)
@@ -158,17 +163,32 @@ public static class ConfigHandler
DownMbps = 100
};
config.ClashUIItem ??= new();
config.ClashUIItem.ConnectionsColumnItem ??= new();
config.ClashUIItem.ConnectionsColumnItem ??= [];
config.SystemProxyItem ??= new();
config.WebDavItem ??= new();
config.CheckUpdateItem ??= new();
config.Fragment4RayItem ??= new()
{
Packets = "tlshello",
Length = "50-100",
Interval = "10-20"
};
config.GlobalHotkeys ??= new();
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 ??= [];
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
{
@@ -255,6 +275,7 @@ public static class ConfigHandler
item.Cert = profileItem.Cert;
item.CertSha = profileItem.CertSha;
item.EchConfigList = profileItem.EchConfigList;
item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName;
item.Finalmask = profileItem.Finalmask;
item.ProtoExtra = profileItem.ProtoExtra;
item.TransportExtra = profileItem.TransportExtra;
@@ -720,12 +741,51 @@ public static class ConfigHandler
{
return -1;
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
var protocolExtra = profileItem.GetProtocolExtra();
profileItem.SetProtocolExtra(protocolExtra with
{
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(),
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(),
SalamanderPass = protocolExtra.SalamanderPass?.TrimEx(),
HopInterval = protocolExtra.HopInterval?.TrimEx(),
});
if (!protocolExtra.Hy2RealmUrl.IsNullOrEmpty())
{
var realmResult = HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm);
if (!realmResult || realm is null)
{
return -1;
}
if (realm.StunList.Count == 0)
{
realm = realm with
{
StunList = Global.DefaultRealmStunList,
};
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
Hy2RealmUrl = realm.ToUri(),
});
}
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
if (isGecko)
{
var minPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
var maxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
if (minPacketSize <= 0
|| minPacketSize > maxPacketSize
|| maxPacketSize > 2048)
{
minPacketSize = 512;
maxPacketSize = 1200;
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
GeckoMinPacketSize = minPacketSize.ToString(),
GeckoMaxPacketSize = maxPacketSize.ToString(),
});
}
await AddServerCommon(config, profileItem, toFile);
return 0;
@@ -874,7 +934,7 @@ public static class ConfigHandler
profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty;
profileItem.AllowInsecure = "false";
profileItem.AllowInsecure = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
@@ -1056,8 +1116,8 @@ public static class ConfigHandler
return new Tuple<int, int>(0, 0);
}
List<ProfileItem> lstKeep = new();
List<ProfileItem> lstRemove = new();
List<ProfileItem> lstKeep = [];
List<ProfileItem> lstRemove = [];
if (!config.GuiItem.KeepOlderDedupl)
{
lstProfile.Reverse();
@@ -1106,10 +1166,6 @@ public static class ConfigHandler
}
else
{
if (profileItem.AllowInsecure.IsNullOrEmpty())
{
profileItem.AllowInsecure = config.CoreBasicItem.DefAllowInsecure.ToString().ToLower();
}
if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality)
{
profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint;
@@ -1454,8 +1510,7 @@ public static class ConfigHandler
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
{
ProfileItem? itemSocks = null;
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect
|| Utils.IsNonWindows();
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect;
if (node.ConfigType != EConfigType.Custom
&& coreType != ECoreType.sing_box
&& config.TunModeItem.EnableTun
@@ -1472,7 +1527,8 @@ public static class ConfigHandler
else if (node.ConfigType == EConfigType.Custom
&& 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()
{
CoreType = preCoreType,
@@ -1537,7 +1593,7 @@ public static class ConfigHandler
}
var countServers = 0;
List<ProfileItem> lstAdd = new();
List<ProfileItem> lstAdd = [];
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
if (isSub)
{
@@ -1633,7 +1689,7 @@ public static class ConfigHandler
{
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
}
if (lstProfiles != null && lstProfiles.Count > 0)
if (lstProfiles is { Count: > 0 })
{
var count = 0;
foreach (var it in lstProfiles)

View File

@@ -38,7 +38,7 @@ public static class ConnectionHandler
for (var i = 0; i < 2; i++)
{
responseTime = await GetRealPingTime(webProxy, 10);
responseTime = await GetRealPingTime(webProxy);
if (responseTime > 0)
{
break;
@@ -66,7 +66,7 @@ public static class ConnectionHandler
/// <summary>
/// Measures response time by sending HTTP requests through proxy.
/// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout)
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9)
{
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1;
@@ -77,17 +77,18 @@ public static class ConnectionHandler
using var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
UseProxy = webProxy != null
UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(3)
});
List<int> oneTime = new();
List<int> oneTime = [];
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100);
await Task.Delay(100, cts.Token);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}

View File

@@ -73,6 +73,10 @@ public class BaseFmt
{
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
}
if (item.VerifyPeerCertByName.IsNotEmpty())
{
dicQuery.Add("vcn", Utils.UrlEncode(item.VerifyPeerCertByName));
}
if (item.CertSha.IsNotEmpty())
{
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
@@ -199,7 +203,7 @@ public class BaseFmt
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (item.AllowInsecure.Equals(Global.AllowInsecure.First()))
if (item.GetAllowInsecure())
{
// Add two for compatibility
dicQuery.Add("insecure", "1");
@@ -227,6 +231,7 @@ public class BaseFmt
item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.EchConfigList = GetQueryDecoded(query, "ech");
item.VerifyPeerCertByName = GetQueryDecoded(query, "vcn");
item.CertSha = GetQueryDecoded(query, "pcs");
var finalmaskDecoded = GetQueryDecoded(query, "fm");
@@ -249,11 +254,11 @@ public class BaseFmt
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{
item.AllowInsecure = Global.AllowInsecure.First();
item.AllowInsecure = Global.StringTrue;
}
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
{
item.AllowInsecure = Global.AllowInsecure.Skip(1).First();
item.AllowInsecure = Global.StringFalse;
}
else
{

View File

@@ -72,6 +72,11 @@ public class FmtHandler
{
return Hysteria2Fmt.Resolve(str, out msg);
}
else if (str.StartsWith(Global.Hysteria2RealmProtocolShare)
|| str.StartsWith(Global.Hysteria2HttpRealmProtocolShare))
{
return Hysteria2Fmt.ResolveRealm(str, out msg);
}
else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC]))
{
return TuicFmt.Resolve(str, out msg);

View File

@@ -1,3 +1,5 @@
using System.Collections.Specialized;
namespace ServiceLib.Handler.Fmt;
public class Hysteria2Fmt : BaseFmt
@@ -23,15 +25,7 @@ public class Hysteria2Fmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
ResolveUriQuery(query, ref item);
if (item.CertSha.IsNullOrEmpty())
{
item.CertSha = GetQueryDecoded(query, "pinSHA256");
}
item.SetProtocolExtra(item.GetProtocolExtra() with
{
Ports = GetQueryDecoded(query, "mport"),
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
});
ResolveHy2UriQuery(query, ref item);
return item;
}
@@ -42,8 +36,11 @@ public class Hysteria2Fmt : BaseFmt
{
return null;
}
var url = string.Empty;
var protocolExtraItem = item.GetProtocolExtra();
if (!protocolExtraItem.Hy2RealmUrl.TrimEx().IsNullOrEmpty())
{
return ToUriForRealm(item);
}
var remark = string.Empty;
if (item.Remarks.IsNotEmpty())
@@ -52,27 +49,7 @@ public class Hysteria2Fmt : BaseFmt
}
var dicQuery = new Dictionary<string, string>();
ToUriQueryLite(item, ref dicQuery);
var protocolExtraItem = item.GetProtocolExtra();
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
{
dicQuery.Add("obfs", "salamander");
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
}
if (!protocolExtraItem.Ports.IsNullOrEmpty())
{
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
}
if (!item.CertSha.IsNullOrEmpty())
{
var sha = item.CertSha;
var idx = sha.IndexOf(',');
if (idx > 0)
{
sha = sha[..idx];
}
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
}
ToHy2UriQuery(item, ref dicQuery);
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
}
@@ -94,4 +71,155 @@ public class Hysteria2Fmt : BaseFmt
return null;
}
public static ProfileItem? ResolveRealm(string str, out string msg)
{
msg = ResUI.ConfigurationFormatIncorrect;
ProfileItem item = new()
{
ConfigType = EConfigType.Hysteria2
};
var realmStr = str["hysteria2+".Length..];
var result = HyRealm.TryParse(realmStr, out var realm);
if (!result || realm == null)
{
return null;
}
var url = Utils.TryUri(str);
if (url == null)
{
return null;
}
item.Address = realm.RendezvousHost;
item.Port = realm.RendezvousPort;
item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
var query = Utils.ParseQueryString(url.Query);
ResolveUriQuery(query, ref item);
var authPassword = GetQueryDecoded(query, "auth");
if (authPassword.IsNullOrEmpty())
{
return null;
}
item.Password = authPassword;
ResolveHy2UriQuery(query, ref item);
item.SetProtocolExtra(item.GetProtocolExtra() with
{
Hy2RealmUrl = realm.ToUri(),
});
msg = string.Empty;
return item;
}
public static string? ToUriForRealm(ProfileItem? item)
{
if (item == null)
{
return null;
}
var protocolExtraItem = item.GetProtocolExtra();
var result = HyRealm.TryParse(protocolExtraItem.Hy2RealmUrl, out var realm);
if (!result || realm == null)
{
return null;
}
var remark = string.Empty;
if (item.Remarks.IsNotEmpty())
{
remark = "#" + Utils.UrlEncode(item.Remarks);
}
var dicQuery = new Dictionary<string, string>();
ToUriQueryLite(item, ref dicQuery);
ToHy2UriQuery(item, ref dicQuery);
dicQuery.Add("auth", Utils.UrlEncode(item.Password));
var queryBuilder = new StringBuilder();
queryBuilder.Append('?');
foreach (var kv in dicQuery)
{
queryBuilder.Append(kv.Key);
queryBuilder.Append('=');
queryBuilder.Append(kv.Value);
queryBuilder.Append('&');
}
foreach (var stun in realm.StunList)
{
queryBuilder.Append("stun=");
queryBuilder.Append(Utils.UrlEncode(stun));
queryBuilder.Append('&');
}
var query = queryBuilder.ToString().TrimEnd('&');
var url = $"{Utils.UrlEncode(realm.Token)}@{GetIpv6(realm.RendezvousHost)}:{realm.RendezvousPort}";
var scheme = realm.IsHttp ? Global.Hysteria2HttpRealmProtocolShare : Global.Hysteria2RealmProtocolShare;
return $"{scheme}{url}/{realm.RealmName}{query}{remark}";
}
private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item)
{
if (item.CertSha.IsNullOrEmpty())
{
item.CertSha = GetQueryDecoded(query, "pinSHA256");
}
item.SetProtocolExtra(item.GetProtocolExtra() with
{
Ports = GetQueryDecoded(query, "mport"),
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
GeckoMinPacketSize = GetQueryDecoded(query, "minPacketSize"),
GeckoMaxPacketSize = GetQueryDecoded(query, "maxPacketSize"),
});
if (GetQueryDecoded(query, "obfs") == "gecko")
{
// Ensure the "PacketSize" parameters are present for gecko obfs.
var protocolExtraItem = item.GetProtocolExtra();
if (protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty())
{
protocolExtraItem = protocolExtraItem with
{
GeckoMinPacketSize = "512",
};
}
if (protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty())
{
protocolExtraItem = protocolExtraItem with
{
GeckoMaxPacketSize = "1200",
};
}
item.SetProtocolExtra(protocolExtraItem);
}
}
private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (!item.CertSha.IsNullOrEmpty()
&& !item.CertSha.Contains(','))
{
var sha = item.CertSha;
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
}
var protocolExtraItem = item.GetProtocolExtra();
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
{
dicQuery.Add("obfs", isGecko ? "gecko" : "salamander");
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
if (isGecko)
{
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
dicQuery.Add("minPacketSize", protocolExtraItem.GeckoMinPacketSize);
dicQuery.Add("maxPacketSize", protocolExtraItem.GeckoMaxPacketSize);
}
}
if (!protocolExtraItem.Ports.IsNullOrEmpty())
{
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
}
}
}

View File

@@ -185,7 +185,7 @@ public class InnerFmt
return null;
}
// Check Enum.IsDefined
if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType))
if (!Enum.IsDefined(profileItem.ConfigType))
{
return null;
}

View File

@@ -71,7 +71,7 @@ public class SocksFmt : BaseFmt
return null;
}
var arr21 = arr1.First().Split(':');
var indexPort = arr1.Last().LastIndexOf(":");
var indexPort = arr1.Last().LastIndexOf(':');
if (arr21.Length != 2 || indexPort < 0)
{
return null;

View File

@@ -66,7 +66,9 @@ public class VmessFmt : BaseFmt
sni = item.Sni,
alpn = item.Alpn,
fp = item.Fingerprint,
insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0"
insecure = item.GetAllowInsecure() ? "1" : "0",
vcn = item.VerifyPeerCertByName,
pcs = item.CertSha
};
var url = JsonUtils.Serialize(vmessQRCode);
@@ -140,7 +142,9 @@ public class VmessFmt : BaseFmt
item.Sni = Utils.ToString(vmessQRCode.sni);
item.Alpn = Utils.ToString(vmessQRCode.alpn);
item.Fingerprint = Utils.ToString(vmessQRCode.fp);
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty;
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.StringTrue : string.Empty;
item.VerifyPeerCertByName = Utils.ToString(vmessQRCode.vcn);
item.CertSha = Utils.ToString(vmessQRCode.pcs);
return item;
}

View File

@@ -133,12 +133,12 @@ public static class SubscriptionHandler
if (!url.Contains("target="))
{
url += string.Format("&target={0}", item.ConvertTarget);
url += $"&target={item.ConvertTarget}";
}
if (!url.Contains("config="))
{
url += string.Format("&config={0}", Global.SubConvertConfig.FirstOrDefault());
url += $"&config={Global.SubConvertConfig.FirstOrDefault()}";
}
}

View File

@@ -16,7 +16,6 @@ public static class SysProxyHandler
try
{
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
var exceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
if (port <= 0)
{
return false;
@@ -24,17 +23,18 @@ public static class SysProxyHandler
switch (type)
{
case ESysProxyType.ForcedChange when Utils.IsWindows():
{
GetWindowsProxyString(config, port, out var strProxy, out var strExceptions);
ProxySettingWindows.SetProxy(strProxy, strExceptions, 2);
break;
}
var (strProxy, strExceptions) = GetWindowsProxyString(config, port);
ProxySettingWindows.SetProxy(strProxy, strExceptions, 2);
break;
case ESysProxyType.ForcedChange when Utils.IsLinux():
var exceptions = SanitizeExceptions(config);
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
break;
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;
case ESysProxyType.ForcedClear when Utils.IsWindows():
@@ -66,15 +66,31 @@ public static class SysProxyHandler
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)
{
strExceptions = $"<local>;{strExceptions}";
}
strProxy = string.Empty;
var strProxy = string.Empty;
if (config.SystemProxyItem.SystemProxyAdvancedProtocol.IsNullOrEmpty())
{
strProxy = $"{Global.Loopback}:{port}";
@@ -86,6 +102,8 @@ public static class SysProxyHandler
.Replace("{http_port}", port.ToString())
.Replace("{socks_port}", port.ToString());
}
return (strProxy, strExceptions);
}
[SupportedOSPlatform("windows")]

View File

@@ -1,3 +1,4 @@
using System.Security.Authentication;
using Downloader;
namespace ServiceLib.Helper;
@@ -14,6 +15,8 @@ public class DownloaderHelper
return null;
}
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
Uri uri = new(url);
//Authorization Header
var headers = new WebHeaderCollection();
@@ -22,17 +25,19 @@ public class DownloaderHelper
headers.Add(HttpRequestHeader.Authorization, "Basic " + Utils.Base64Encode(uri.UserInfo));
}
var requestConfiguration = new RequestConfiguration()
{
Headers = headers,
UserAgent = userAgent,
ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration()
{
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration =
{
Headers = headers,
UserAgent = userAgent,
ConnectTimeout = timeout * 1000,
Proxy = webProxy
}
RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
};
await using var downloader = new Downloader.DownloadService(downloadOpt);
@@ -45,7 +50,9 @@ public class DownloaderHelper
};
using var cts = new CancellationTokenSource();
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token);
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
using StreamReader reader = new(stream);
downloadOpt = null;
@@ -60,15 +67,18 @@ public class DownloaderHelper
throw new ArgumentNullException(nameof(url));
}
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration()
{
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration =
{
ConnectTimeout= timeout * 1000,
Proxy = webProxy
}
RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
};
var lastUpdateTime = DateTime.Now;
@@ -116,7 +126,7 @@ public class DownloaderHelper
};
//progress.Report("......");
using var cts = new CancellationTokenSource();
cts.CancelAfter(timeout * 1000);
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
downloadOpt = null;
@@ -137,15 +147,18 @@ public class DownloaderHelper
File.Delete(fileName);
}
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration()
{
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration =
{
ConnectTimeout= timeout * 1000,
Proxy = webProxy
}
RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
};
var progressPercentage = 0;
@@ -182,4 +195,75 @@ public class DownloaderHelper
downloadOpt = null;
}
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/SocketClient.cs#L45
// There is a risk of MITM attacks
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/Extensions/ExceptionHelper.cs#L111
private static SocketsHttpHandler GetSocketsHttpHandler(RequestConfiguration config)
{
SocketsHttpHandler handler = new()
{
AllowAutoRedirect = config.AllowAutoRedirect,
MaxAutomaticRedirections = config.MaximumAutomaticRedirections,
AutomaticDecompression = config.AutomaticDecompression,
PreAuthenticate = config.PreAuthenticate,
UseCookies = config.CookieContainer != null,
UseProxy = config.Proxy != null,
MaxConnectionsPerServer = 1000,
PooledConnectionIdleTimeout = config.KeepAliveTimeout,
PooledConnectionLifetime = Timeout.InfiniteTimeSpan,
EnableMultipleHttp2Connections = true,
ConnectTimeout = TimeSpan.FromMilliseconds(config.ConnectTimeout)
};
// Set up the SslClientAuthenticationOptions for custom certificate validation
if (config.ClientCertificates?.Count > 0)
{
handler.SslOptions.ClientCertificates = config.ClientCertificates;
}
handler.SslOptions.EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12;
//handler.SslOptions.RemoteCertificateValidationCallback = ExceptionHelper.CertificateValidationCallBack;
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null)
{
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
handler.SslOptions.RemoteCertificateValidationCallback = null;
}
// Configure keep-alive
if (config.KeepAlive)
{
handler.KeepAlivePingTimeout = config.KeepAliveTimeout;
handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
}
// Configure credentials
if (config.Credentials != null)
{
handler.Credentials = config.Credentials;
handler.PreAuthenticate = config.PreAuthenticate;
}
// Configure cookies
if (handler.UseCookies && config.CookieContainer != null)
{
handler.CookieContainer = config.CookieContainer;
}
// Configure proxy
if (handler.UseProxy && config.Proxy != null)
{
handler.Proxy = config.Proxy;
}
// Add expect header
if (!string.IsNullOrWhiteSpace(config.Expect))
{
handler.Expect100ContinueTimeout = TimeSpan.FromSeconds(1);
}
return handler;
}
}

View File

@@ -10,6 +10,7 @@ public sealed class AppManager
private int? _statePort2;
public static AppManager Instance => _instance.Value;
public Config Config => _config;
public IWindowDialog WindowDialog { get; set; } = null!;
public int StatePort
{
@@ -244,9 +245,26 @@ public sealed class AppManager
{
return [];
}
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId))
.ToListAsync();
if (ids.Count <= Global.SqliteMaxBatchSize)
{
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId))
.ToListAsync();
}
var items = new List<ProfileItem>();
for (var size = 0; size < ids.Count; size += Global.SqliteMaxBatchSize)
{
var chunk = ids.Skip(size).Take(Global.SqliteMaxBatchSize).ToList();
var chunkItems = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => chunk.Contains(it.IndexId))
.ToListAsync();
items.AddRange(chunkItems);
}
return items;
}
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
@@ -257,18 +275,11 @@ public sealed class AppManager
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
{
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (idList.Count == 0)
{
return [];
}
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => idList.Contains(it.IndexId))
.ToListAsync();
var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
var items = await GetProfileItemsByIndexIds(ids);
var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(id => itemMap.GetValueOrDefault(id))
return ids.Select(itemMap.GetValueOrDefault)
.Where(item => item != null)
.ToList();
}
@@ -652,7 +663,7 @@ public sealed class AppManager
return Global.SsSecuritiesInSingbox;
}
public ECoreType GetCoreType(ProfileItem profileItem, EConfigType eConfigType)
public ECoreType GetCoreType(ProfileItem? profileItem, EConfigType eConfigType)
{
if (profileItem?.CoreType != null)
{

View File

@@ -4,206 +4,31 @@ using System.Security.Cryptography.X509Certificates;
namespace ServiceLib.Manager;
/// <summary>
/// Manager for certificate operations with CA pinning to prevent MITM attacks
/// Manager for certificate operations with CA pinning to prevent MITM attacks
/// </summary>
public class CertPemManager
{
private static readonly string _tag = "CertPemManager";
private static readonly Lazy<CertPemManager> _instance = new(() => new());
private Config _config;
public async Task Init(Config config)
{
if (_config != null)
{
return;
}
_config = config;
await Task.CompletedTask;
}
public static CertPemManager Instance => _instance.Value;
/// <summary>
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
/// Get certificate in PEM format from a server with CA pinning validation
/// </summary>
private static readonly HashSet<string> TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase)
{
"EBD41040E4BB3EC742C9E381D31EF2A41A48B6685C96E7CEF3C1DF6CD4331C99", // GlobalSign Root CA
"6DC47172E01CBCB0BF62580D895FE2B8AC9AD4F873801E0C10B9C837D21EB177", // Entrust.net Premium 2048 Secure Server CA
"73C176434F1BC6D5ADF45B0E76E727287C8DE57616C1E6E6141A2B2CBC7D8E4C", // Entrust Root Certification Authority
"D8E0FEBC1DB2E38D00940F37D27D41344D993E734B99D5656D9778D4D8143624", // Certum Root CA
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
"CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
"F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA
"4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
"EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
"2530CC8E98321502BAD96F9B1FBA1B099E2D299E0F4548BB914F363BC0D4531F", // Izenpe.com
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
"568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2
"0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial
"0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking
"70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium
"BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC
"5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
"55926084EC963A64B96E2ABE01CE0BA86A64FBFEBCC7AAB5AFC155B37FD76066", // Actalis Authentication Root CA
"9A114025197C5BB95D94E63D55CD43790847B646B23CDF11ADA4A00EFF15FB48", // Buypass Class 2 Root CA
"EDF7EBBCA27A2A384D387B7D4010C666E2EDB4843E4C29B4AE1D5B9332E6B24D", // Buypass Class 3 Root CA
"FD73DAD31C644FF1B43BEF0CCDDA96710B9CD9875ECA7E31707AF3E96D522BBD", // T-TeleSec GlobalRoot Class 3
"49E7A442ACF0EA6287050054B52564B650E4F49E42E348D6AA38E039E957B1C1", // D-TRUST Root Class 3 CA 2 2009
"EEC5496B988CE98625B934092EEC2908BED0B0F316C2D4730C84EAF1F3D34881", // D-TRUST Root Class 3 CA 2 EV 2009
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
"DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
"8FE4FB0AF93A4D0D67DB0BEBB23E37C71BF325DCBCDD240EA04DAF58B47E1840", // QuoVadis Root CA 2 G3
"88EF81DE202EB018452E43F864725CEA5FBD1FC2D9D205730709C5D8B8690F46", // QuoVadis Root CA 3 G3
"7D05EBB682339F8C9451EE094EEBFEFA7953A114EDB2F44949452FAB7D2FC185", // DigiCert Assured ID Root G2
"7E37CB8B4C47090CAB36551BA6F45DB840680FBA166A952DB100717F43053FC2", // DigiCert Assured ID Root G3
"CB3CCBB76031E5E0138F8DD39A23F9DE47FFC35E43C1144CEA27D46A5AB1CB5F", // DigiCert Global Root G2
"31AD6648F8104138C738F39EA4320133393E3A18CC02296EF97C2AC9EF6731D0", // DigiCert Global Root G3
"552F7BDCF1A7AF9E6CE672017F4F12ABF77240C78E761AC203D1D9D20AC89988", // DigiCert Trusted Root G4
"52F0E1C4E58EC629291B60317F074671B85D7EA80D5B07273463534B32B40234", // COMODO RSA Certification Authority
"E793C9B02FD8AA13E21C31228ACCB08119643B749C898964B1746D46C3D4CBD2", // USERTrust RSA Certification Authority
"4FF460D54B9C86DABFBCFC5712E0400D2BED3FBC4D4FBDAA86E06ADCD2A9AD7A", // USERTrust ECC Certification Authority
"179FBC148A3DD00FD24EA13458CC43BFA7F59C8182D783A513F6EBEC100C8924", // GlobalSign ECC Root CA - R5
"3C4FB0B95AB8B30032F432B86F535FE172C185D0FD39865837CF36187FA6F428", // Staat der Nederlanden Root CA - G3
"5D56499BE4D2E08BCFCAD08A3E38723D50503BDE706948E42F55603019E528AE", // IdenTrust Commercial Root CA 1
"30D0895A9A448A262091635522D1F52010B5867ACAE12C78EF958FD4F4389F2F", // IdenTrust Public Sector Root CA 1
"43DF5774B03E7FEF5FE40D931A7BEDF1BB2E6B42738C4E6D3841103D3AA7F339", // Entrust Root Certification Authority - G2
"02ED0EB28C14DA45165C566791700D6451D7FB56F0B2AB1D3B8EB070E56EDFF5", // Entrust Root Certification Authority - EC1
"5CC3D78E4E1D5E45547A04E6873E64F90CF9536D1CCC2EF800F355C4C5FD70FD", // CFCA EV ROOT
"6B9C08E86EB0F767CFAD65CD98B62149E5494A67F5845E7BD1ED019F27B86BD6", // OISTE WISeKey Global Root GB CA
"A1339D33281A0B56E557D3D32B1CE7F9367EB094BD5FA72A7E5004C8DED7CAFE", // SZAFIR ROOT CA2
"B676F2EDDAE8775CD36CB0F63CD1D4603961F49E6265BA013A2F0307B6D0B804", // Certum Trusted Network CA 2
"A040929A02CE53B4ACF4F2FFC6981CE4496F755E6D45FE0B2A692BCD52523F36", // Hellenic Academic and Research Institutions RootCA 2015
"44B545AA8A25E65A73CA15DC27FC36D24C1CB9953A066539B11582DC487B4833", // Hellenic Academic and Research Institutions ECC RootCA 2015
"96BCEC06264976F37460779ACF28C5A7CFE8A3C0AAE11A8FFCEE05C0BDDF08C6", // ISRG Root X1
"EBC5570C29018C4D67B1AA127BAF12F703B4611EBC17B7DAB5573894179B93FA", // AC RAIZ FNMT-RCM
"8ECDE6884F3D87B1125BA31AC3FCB13D7016DE7F57CC904FE1CB97C6AE98196E", // Amazon Root CA 1
"1BA5B2AA8C65401A82960118F80BEC4F62304D83CEC4713A19C39C011EA46DB4", // Amazon Root CA 2
"18CE6CFE7BF14E60B2E347B8DFE868CB31D02EBB3ADA271569F50343B46DB3A4", // Amazon Root CA 3
"E35D28419ED02025CFA69038CD623962458DA5C695FBDEA3C22B0BFB25897092", // Amazon Root CA 4
"A1A86D04121EB87F027C66F53303C28E5739F943FC84B38AD6AF009035DD9457", // D-TRUST Root CA 3 2013
"46EDC3689046D53A453FB3104AB80DCAEC658B2660EA1629DD7E867990648716", // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1
"BFFF8FD04433487D6A8AA60C1A29767A9FC2BBB05E420F713A13B992891D3893", // GDCA TrustAUTH R5 ROOT
"85666A562EE0BE5CE925C1D8890A6F76A87EC16D4D7D5F29EA7419CF20123B69", // SSL.com Root Certification Authority RSA
"3417BB06CC6007DA1B961C920B8AB4CE3FAD820E4AA30B9ACBC4A74EBDCEBC65", // SSL.com Root Certification Authority ECC
"2E7BF16CC22485A7BBE2AA8696750761B0AE39BE3B2FE9D0CC6D4EF73491425C", // SSL.com EV Root Certification Authority RSA R2
"22A2C1F7BDED704CC1E701B5F408C310880FE956B5DE2A4A44F99C873A25A7C8", // SSL.com EV Root Certification Authority ECC
"2CABEAFE37D06CA22ABA7391C0033D25982952C453647349763A3AB5AD6CCF69", // GlobalSign Root CA - R6
"8560F91C3624DABA9570B5FEA0DBE36FF11A8323BE9486854FB3F34A5571198D", // OISTE WISeKey Global Root GC CA
"9BEA11C976FE014764C1BE56A6F914B5A560317ABD9988393382E5161AA0493C", // UCA Global G2 Root
"D43AF9B35473755C9684FC06D7D8CB70EE5C28E773FB294EB41EE71722924D24", // UCA Extended Validation Root
"D48D3D23EEDB50A459E55197601C27774B9D7B18C94D5A059511A10250B93168", // Certigna Root CA
"40F6AF0346A99AA1CD1D555A4E9CCE62C7F9634603EE406615833DC8C8D00367", // emSign Root CA - G1
"86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B", // emSign ECC Root CA - G3
"125609AA301DA0A249B97A8239CB6A34216F44DCAC9F3954B14292F2E8C8608F", // emSign Root CA - C1
"BC4D809B15189D78DB3E1D8CF4F9726A795DA1643CA5F1358E1DDB0EDC0D7EB3", // emSign ECC Root CA - C3
"5A2FC03F0C83B090BBFA40604B0988446C7636183DF9846E17101A447FB8EFD6", // Hongkong Post Root CA 3
"DB3517D1F6732A2D5AB97C533EC70779EE3270A62FB4AC4238372460E6F01E88", // Entrust Root Certification Authority - G4
"358DF39D764AF9E1B766E9C972DF352EE15CFAC227AF6AD1D70E8E4A6EDCBA02", // Microsoft ECC Root Certificate Authority 2017
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
"657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2
"97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority
"945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority
"55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority
"88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
"9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
"2E44102AB58CB85419451C8E19D9ACF3662CAFBC614B6A53960A30F7D0E2EB41", // TunTrust Root CA
"D95D0E8EDA79525BF9BEB11B14D2100D3294985F0C62D9FABD9CD999ECCB7B1D", // HARICA TLS RSA Root CA 2021
"3F99CC474ACFCE4DFED58794665E478D1547739F2E780F1BB4CA9B133097D401", // HARICA TLS ECC Root CA 2021
"1BE7ABE30686B16348AFD1C61B6866A0EA7F4821E67D5E8AF937CF8011BC750D", // HARICA Client RSA Root CA 2021
"8DD4B5373CB0DE36769C12339280D82746B3AA6CD426E797A31BABE4279CF00B", // HARICA Client ECC Root CA 2021
"57DE0583EFD2B26E0361DA99DA9DF4648DEF7EE8441C3B728AFA9BCDE0F9B26A", // Autoridad de Certificacion Firmaprofesional CIF A62634068
"30FBBA2C32238E2A98547AF97931E550428B9B3F1C8EEB6633DCFA86C5B27DD3", // vTrus ECC Root CA
"8A71DE6559336F426C26E53880D00D88A18DA4C6A91F0DCB6194E206C5C96387", // vTrus Root CA
"69729B8E15A86EFC177A57AFB7171DFC64ADD28C2FCA8CF1507E34453CCB1470", // ISRG Root X2
"F015CE3CC239BFEF064BE9F1D2C417E1A0264A0A94BE1F0C8D121864EB6949CC", // HiPKI Root CA - G1
"B085D70B964F191A73E4AF0D54AE7A0E07AAFDAF9B71DD0862138AB7325A24A2", // GlobalSign ECC Root CA - R4
"D947432ABDE7B7FA90FC2E6B59101B1280E0E1C7E4E40FA3C6887FFF57A7F4CF", // GTS Root R1
"8D25CD97229DBF70356BDA4EB3CC734031E24CF00FAFCFD32DC76EB5841C7EA8", // GTS Root R2
"34D8A73EE208D9BCDB0D956520934B4E40E69482596E8B6F73C8426B010A6F48", // GTS Root R3
"349DFA4058C5E263123B398AE795573C4E1313C83FE68F93556CD5E8031B3C7D", // GTS Root R4
"242B69742FCB1E5B2ABF98898B94572187544E5B4D9911786573621F6A74B82C", // Telia Root CA v2
"E59AAA816009C22BFF5B25BAD37DF306F049797C1F81D85AB089E657BD8F0044", // D-TRUST BR Root CA 1 2020
"08170D1AA36453901A2F959245E347DB0C8D37ABAABC56B81AA100DC958970DB", // D-TRUST EV Root CA 1 2020
"018E13F0772532CF809BD1B17281867283FC48C6E13BE9C69812854A490C1B05", // DigiCert TLS ECC P384 Root G5
"371A00DC0533B3721A7EEB40E8419E70799D2B0A0F2C1D80693165F7CEC4AD75", // DigiCert TLS RSA4096 Root G5
"E8E8176536A60CC2C4E10187C3BEFCA20EF263497018F566D5BEA0F94D0C111B", // DigiCert SMIME ECC P384 Root G5
"90370D3EFA88BF58C30105BA25104A358460A7FA52DFC2011DF233A0F417912A", // DigiCert SMIME RSA4096 Root G5
"77B82CD8644C4305F7ACC5CB156B45675004033D51C60C6202A8E0C33467D3A0", // Certainly Root R1
"B4585F22E4AC756A4E8612A1361C5D9D031A93FD84FEBB778FA3068B0FC42DC2", // Certainly Root E1
"82BD5D851ACF7F6E1BA7BFCBC53030D0E7BC3C21DF772D858CAB41D199BDF595", // DIGITALSIGN GLOBAL ROOT RSA CA
"261D7114AE5F8FF2D8C7209A9DE4289E6AFC9D717023D85450909199F1857CFE", // DIGITALSIGN GLOBAL ROOT ECDSA CA
"E74FBDA55BD564C473A36B441AA799C8A68E077440E8288B9FA1E50E4BBACA11", // Security Communication ECC RootCA1
"F3896F88FE7C0A882766A7FA6AD2749FB57A7F3E98FB769C1FA7B09C2C44D5AE", // BJCA Global Root CA1
"574DF6931E278039667B720AFDC1600FC27EB66DD3092979FB73856487212882", // BJCA Global Root CA2
"48E1CF9E43B688A51044160F46D773B8277FE45BEAAD0E4DF90D1974382FEA99", // LAWtrust Root CA2 (4096)
"22D9599234D60F1D4BC7C7E96F43FA555B07301FD475175089DAFB8C25E477B3", // Sectigo Public Email Protection Root E46
"D5917A7791EB7CF20A2E57EB98284A67B28A57E89182DA53D546678C9FDE2B4F", // Sectigo Public Email Protection Root R46
"C90F26F0FB1B4018B22227519B5CA2B53E2CA5B3BE5CF18EFE1BEF47380C5383", // Sectigo Public Server Authentication Root E46
"7BB647A62AEEAC88BF257AA522D01FFEA395E0AB45C73F93F65654EC38F25A06", // Sectigo Public Server Authentication Root R46
"8FAF7D2E2CB4709BB8E0B33666BF75A5DD45B5DE480F8EA8D4BFE6BEBC17F2ED", // SSL.com TLS RSA Root CA 2022
"C32FFD9F46F936D16C3673990959434B9AD60AAFBB9E7CF33654F144CC1BA143", // SSL.com TLS ECC Root CA 2022
"AD7DD58D03AEDB22A30B5084394920CE12230C2D8017AD9B81AB04079BDD026B", // SSL.com Client ECC Root CA 2022
"1D4CA4A2AB21D0093659804FC0EB2175A617279B56A2475245C9517AFEB59153", // SSL.com Client RSA Root CA 2022
"E38655F4B0190C84D3B3893D840A687E190A256D98052F159E6D4A39F589A6EB", // Atos TrustedRoot Root CA ECC G2 2020
"78833A783BB2986C254B9370D3C20E5EBA8FA7840CBF63FE17297A0B0119685E", // Atos TrustedRoot Root CA RSA G2 2020
"B2FAE53E14CCD7AB9212064701AE279C1D8988FACB775FA8A008914E663988A8", // Atos TrustedRoot Root CA ECC TLS 2021
"81A9088EA59FB364C548A6F85559099B6F0405EFBF18E5324EC9F457BA00112F", // Atos TrustedRoot Root CA RSA TLS 2021
"E0D3226AEB1163C2E48FF9BE3B50B4C6431BE7BB1EACC5C36B5D5EC509039A08", // TrustAsia Global Root CA G3
"BE4B56CB5056C0136A526DF444508DAA36A0B54F42E4AC38F72AF470E479654C", // TrustAsia Global Root CA G4
"D92C171F5CF890BA428019292927FE22F3207FD2B54449CB6F675AF4922146E2", // D-Trust SBR Root CA 1 2022
"DBA84DD7EF622D485463A90137EA4D574DF8550928F6AFA03B4D8B1141E636CC", // D-Trust SBR Root CA 2 2022
"3AE6DF7E0D637A65A8C81612EC6F9A142F85A16834C10280D88E707028518755", // Telekom Security SMIME ECC Root 2021
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
"BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
"4B009C1034494F9AB56BBA3BA1D62731FC4D20D8955ADCEC10A925607261E338", // SecureSign Root CA14
"E778F0F095FE843729CD1A0082179E5314A9C291442805E1FB1D8FB6B8886C3A", // SecureSign Root CA15
"0552E6F83FDF65E8FA9670E666DF28A4E21340B510CBE52566F97C4FB94B2BD1", // D-TRUST BR Root CA 2 2023
"436472C1009A325C54F1A5BBB5468A7BAEECCBE05DE5F099CB70D3FE41E13C16", // TrustAsia SMIME ECC Root CA
"C7796BEB62C101BB143D262A7C96A0C6168183223EF50D699632D86E03B8CC9B", // TrustAsia SMIME RSA Root CA
"C0076B9EF0531FB1A656D67C4EBE97CD5DBAA41EF44598ACC2489878C92D8711", // TrustAsia TLS ECC Root CA
"06C08D7DAFD876971EB1124FE67F847EC0C7A158D3EA53CBE940E2EA9791F4C3", // TrustAsia TLS RSA Root CA
"8E8221B2E7D4007836A1672F0DCC299C33BC07D316F132FA1A206D587150F1CE", // D-TRUST EV Root CA 2 2023
"9A12C392BFE57891A0C545309D4D9FD567E480CB613D6342278B195C79A7931F", // SwissSign RSA SMIME Root CA 2022 - 1
"193144F431E0FDDB740717D4DE926A571133884B4360D30E272913CBE660CE41", // SwissSign RSA TLS Root CA 2022 - 1
"D9A32485A8CCA85539CEF12FFFFF711378A17851D73DA2732AB4302D763BD62B", // OISTE Client Root ECC G1
"D02A0F994A868C66395F2E7A880DF509BD0C29C96DE16015A0FD501EDA4F96A9", // OISTE Client Root RSA G1
"EEC997C0C30F216F7E3B8B307D2BAE42412D753FC8219DAFD1520B2572850F49", // OISTE Server Root ECC G1
"9AE36232A5189FFDDB353DFD26520C015395D22777DAC59DB57B98C089A651E6", // OISTE Server Root RSA G1
"B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023
};
/// <summary>
/// Get certificate in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4)
{
try
{
@@ -216,13 +41,13 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = callback
RemoteCertificateValidationCallback = callback,
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
@@ -249,9 +74,10 @@ public class CertPemManager
}
/// <summary>
/// Get certificate chain in PEM format from a server with CA pinning validation
/// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4)
{
var pemList = new List<string>();
try
@@ -265,13 +91,13 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = callback
RemoteCertificateValidationCallback = callback,
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
@@ -301,42 +127,35 @@ public class CertPemManager
}
/// <summary>
/// Validate server certificate with CA pinning
/// Validate server certificate with CA pinning
/// </summary>
private bool ValidateServerCertificate(
object _,
X509Certificate? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors,
bool allowInsecure)
List<string> verifyPeerCertByName)
{
if (certificate == null)
{
return false;
}
// In insecure mode, accept any certificate so self-signed certs can be fetched.
if (allowInsecure)
{
return true;
}
// Check certificate name mismatch
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
{
return false;
}
// Build certificate chain
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
var certChain = chain ?? new X509Chain();
certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
certChain.ChainPolicy.VerificationTime = DateTime.Now;
if (chain == null)
{
certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
certChain.ChainPolicy.VerificationTime = DateTime.UtcNow;
certChain.Build(cert2);
if (!certChain.Build(cert2))
{
return false;
}
}
// Find root CA
if (certChain.ChainElements.Count == 0)
@@ -344,10 +163,38 @@ public class CertPemManager
return false;
}
var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate;
var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256);
var rootCert = certChain.ChainElements[^1].Certificate;
return TrustedCaThumbprints.Contains(rootThumbprint);
var trustedCerts = BuildTrustedCertificateCollection();
if (!trustedCerts.Contains(rootCert))
{
return false;
}
if (!sslPolicyErrors.HasFlag(
SslPolicyErrors.RemoteCertificateNameMismatch))
{
return true;
}
if (verifyPeerCertByName.Count == 0)
{
return false;
}
foreach (var ext in cert2.Extensions)
{
if (ext is not X509SubjectAlternativeNameExtension san)
{
continue;
}
return san.EnumerateDnsNames().Any(dnsName =>
verifyPeerCertByName.Contains(dnsName, StringComparer.OrdinalIgnoreCase));
}
return false;
}
public static string ExportCertToPem(X509Certificate2 cert)
@@ -358,8 +205,8 @@ public class CertPemManager
}
/// <summary>
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// </summary>
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
/// <returns>List of individual PEM certificate strings with normalized format</returns>
@@ -411,18 +258,13 @@ public class CertPemManager
}
/// <summary>
/// Concatenate a list of PEM certificates into a single string
/// Concatenate a list of PEM certificates into a single string
/// </summary>
/// <param name="pemList">List of individual PEM certificate strings</param>
/// <returns>Concatenated PEM certificates string</returns>
public static string ConcatenatePemChain(IEnumerable<string> pemList)
public static string ConcatenatePemChain(IEnumerable<string>? pemList)
{
if (pemList == null)
{
return string.Empty;
}
return string.Concat(pemList);
return pemList == null ? string.Empty : string.Concat(pemList);
}
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
@@ -431,15 +273,57 @@ public class CertPemManager
{
var cert = X509Certificate2.CreateFromPem(pemCert);
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
if (includeColon)
{
return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c)));
}
return thumbprint;
return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint;
}
catch
{
return string.Empty;
}
}
private static readonly Lazy<X509Certificate2Collection> _chromeRootCerts = new(() =>
{
var pemText = EmbedUtils.GetEmbedText(Global.ChromeRootCertFileName);
var collection = new X509Certificate2Collection();
collection.ImportFromPem(pemText);
return collection;
});
private static readonly Lazy<X509Certificate2Collection> _mozillaRootCerts = new(() =>
{
var pemText = EmbedUtils.GetEmbedText(Global.MozillaRootCertFileName);
var collection = new X509Certificate2Collection();
collection.ImportFromPem(pemText);
return collection;
});
private X509Certificate2Collection BuildTrustedCertificateCollection()
{
if (_config.GuiItem.RootCertProvider == Global.ChromeRootProvider)
{
return _chromeRootCerts.Value;
}
return _mozillaRootCerts.Value;
}
private bool IsSystemRootCertProvider()
{
return _config.GuiItem.RootCertProvider != Global.ChromeRootProvider && _config.GuiItem.RootCertProvider != Global.MozillaRootProvider;
}
public X509ChainPolicy? BuildCertificateChainPolicy()
{
if (IsSystemRootCertProvider())
{
return null;
}
var trustedCerts = BuildTrustedCertificateCollection();
var chainPolicy = new X509ChainPolicy
{
TrustMode = X509ChainTrustMode.CustomRootTrust,
RevocationMode = X509RevocationMode.NoCheck,
};
chainPolicy.CustomTrustStore.AddRange(trustedCerts);
return chainPolicy;
}
}

View File

@@ -45,19 +45,14 @@ public sealed class ClashApiManager
{
await GetClashProxiesAsync();
}
lstProxy = new List<ClashProxyModel>();
foreach (var kv in _proxies ?? [])
{
if (Global.notAllowTestType.Contains(kv.Value.type?.ToLower()))
{
continue;
}
lstProxy.Add(new ClashProxyModel()
{
Name = kv.Value.name,
Type = kv.Value.type?.ToLower(),
});
}
lstProxy = [];
lstProxy.AddRange(from kv in _proxies ?? []
where !Global.notAllowTestType.Contains(kv.Value.type?.ToLower())
select new ClashProxyModel()
{
Name = kv.Value.name,
Type = kv.Value.type?.ToLower(),
});
}
if (lstProxy is not { Count: > 0 })

View File

@@ -34,7 +34,18 @@ public class CoreAdminManager
StringBuilder sb = new();
sb.AppendLine("#!/bin/bash");
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 procService = new ProcessService(

View File

@@ -113,6 +113,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip",
DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip",
DownloadUrlLinuxRiscV64 = urlN + "/download/{0}/v2rayN-linux-riscv64.zip",
DownloadUrlLinuxLoong64 = urlN + "/download/{0}/v2rayN-linux-loong64.zip",
DownloadUrlOSX64 = urlN + "/download/{0}/v2rayN-macos-64.zip",
DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.zip",
},
@@ -157,6 +158,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip",
DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip",
DownloadUrlLinuxRiscV64 = urlXray + "/download/{0}/Xray-linux-riscv64.zip",
DownloadUrlLinuxLoong64 = urlXray + "/download/{0}/Xray-linux-loong64.zip",
DownloadUrlOSX64 = urlXray + "/download/{0}/Xray-macos-64.zip",
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
Match = "Xray",
@@ -180,6 +182,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
DownloadUrlLinuxRiscV64 = urlMihomo + "/download/{0}/mihomo-linux-riscv64-{0}.gz",
DownloadUrlLinuxLoong64 = urlMihomo + "/download/{0}/mihomo-linux-loong64-abi2-{0}.gz",
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
Match = "Mihomo",
@@ -223,6 +226,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz",
DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz",
DownloadUrlLinuxRiscV64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-riscv64.tar.gz",
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
Match = "sing-box",
@@ -314,6 +318,7 @@ public sealed class CoreInfoManager
names.Add("mihomo-linux-amd64");
names.Add("mihomo-linux-arm64");
names.Add("mihomo-linux-riscv64");
names.Add("mihomo-linux-loong64-abi2");
}
else if (Utils.IsMacOS())
{

View File

@@ -8,8 +8,10 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value;
private Config _config;
[SupportedOSPlatform("windows")]
private WindowsJobService? _processJob;
private ProcessService? _processService;
private ProcessService? _processPreService;
private bool _linuxSudo = false;
@@ -90,6 +92,7 @@ public class CoreManager
}
await CoreStart(mainContext);
await WaitForProxyPort(preContext);
await CoreStartPreService(preContext);
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
@@ -180,7 +183,7 @@ public class CoreManager
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
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)
{
return;
@@ -198,7 +201,7 @@ public class CoreManager
if (result.Success)
{
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)
{
return;
@@ -213,11 +216,93 @@ public class CoreManager
await _updateFunc?.Invoke(notify, msg);
}
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
{
if (preContext is null)
{
return;
}
if (!preContext.AppConfig.TunModeItem.EnableTun)
{
return;
}
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
var rootToken = rootCts.Token;
var port = preContext.Node.Port;
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
var buf = new byte[2];
while (!rootToken.IsCancellationRequested)
{
using var tcp = new TcpClient();
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
var linkedToken = linkedCts.Token;
try
{
await tcp.ConnectAsync(Global.Loopback, port, linkedToken);
var stream = tcp.GetStream();
await stream.WriteAsync(greeting, linkedToken);
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
if (read == 2 && buf[0] == 0x05)
{
return;
}
}
catch (OperationCanceledException)
{
if (!rootToken.IsCancellationRequested)
{
continue;
}
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
return;
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
// Connection refused, proxy not ready yet, wait 50ms before retrying
try
{
await Task.Delay(50, rootToken);
}
catch (OperationCanceledException)
{
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
return;
}
}
catch
{
// Ignore other exceptions and continue
}
}
}
#endregion Private
#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);
if (fileName.IsNullOrEmpty())
@@ -229,9 +314,7 @@ public class CoreManager
try
{
if (mayNeedSudo
&& _config.TunModeItem.EnableTun
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo)
&& Utils.IsNonWindows())
&& ShouldRunAsSudo(isTunLaunch, coreInfo.CoreType, Utils.IsNonWindows()))
{
_linuxSudo = true;
await CoreAdminManager.Instance.Init(_config, _updateFunc);

View File

@@ -9,7 +9,7 @@ public class GroupProfileManager
public static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo)
{
return await HasCycle(indexId, extraInfo, new HashSet<string>(), new HashSet<string>());
return await HasCycle(indexId, extraInfo, [], []);
}
private static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet<string> visited, HashSet<string> stack)
@@ -55,7 +55,7 @@ public class GroupProfileManager
var childItems = await AppManager.Instance.GetProfileItemsByIndexIds(childIds);
foreach (var childItem in childItems)
{
if (await HasCycle(childItem.IndexId, childItem?.GetProtocolExtra(), visited, stack))
if (await HasCycle(childItem.IndexId, childItem.GetProtocolExtra(), visited, stack))
{
return true;
}
@@ -71,7 +71,7 @@ public class GroupProfileManager
public static async Task<(List<ProfileItem> Items, ProtocolExtraItem? Extra)> GetChildProfileItems(ProfileItem profileItem)
{
var protocolExtra = profileItem?.GetProtocolExtra();
var protocolExtra = profileItem.GetProtocolExtra();
return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra);
}

View File

@@ -111,7 +111,7 @@ public class ProfileExManager
public async Task ClearAll()
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
_lstProfileEx = new();
_lstProfileEx = [];
}
public async Task SaveTo()
@@ -182,6 +182,6 @@ public class ProfileExManager
{
return 0;
}
return _lstProfileEx.Max(t => t == null ? 0 : t.Sort);
return _lstProfileEx.Max(t => t?.Sort ?? 0);
}
}

View File

@@ -45,7 +45,7 @@ public class StatisticsManager
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem ");
_serverStatItem = null;
_lstServerStat = new();
_lstServerStat = [];
}
public async Task SaveTo()

View File

@@ -56,7 +56,7 @@ public class TaskManager
{
//Logging.SaveLog("Execute delete expired files");
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1));
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1), "Test");
FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));

View File

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

View File

@@ -7,10 +7,6 @@ public class CoreBasicItem
public string Loglevel { get; set; }
public bool MuxEnabled { get; set; }
public bool DefAllowInsecure { get; set; }
public string DefFingerprint { get; set; }
public string DefUserAgent { get; set; }
@@ -21,6 +17,8 @@ public class CoreBasicItem
public bool EnableFragment { get; set; }
public bool EnableFinalFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true;
}
@@ -76,6 +74,7 @@ public class GUIItem
public int TrayMenuServersLimit { get; set; } = 20;
public bool EnableHWA { get; set; } = false;
public bool EnableLog { get; set; } = true;
public string? RootCertProvider { get; set; }
}
[Serializable]
@@ -104,6 +103,7 @@ public class UIItem
public bool MacOSShowInDock { get; set; }
public List<ColumnItem> MainColumnItem { get; set; }
public List<WindowSizeItem> WindowSizeItem { get; set; }
public bool HideColumnIpInfo { get; set; }
}
[Serializable]
@@ -147,7 +147,10 @@ public class TunModeItem
public int Mtu { get; set; }
public bool EnableIPv6Address { 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 string IPv4Address { get; set; }
public string IPv6Address { get; set; }
}
[Serializable]
@@ -159,6 +162,8 @@ public class SpeedTestItem
public int MixedConcurrencyCount { get; set; }
public string IPAPIUrl { get; set; }
public string UdpTestTarget { get; set; }
public int? SpeedTestPageSize { get; set; }
public int? SpeedTestDelayInterval { get; set; }
}
[Serializable]
@@ -246,8 +251,15 @@ public class CheckUpdateItem
public class Fragment4RayItem
{
public string? Packets { get; set; }
public List<string>? Lengths { get; set; }
public List<string>? Delays { 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]
@@ -265,14 +277,26 @@ public class SimpleDNSItem
public bool? AddCommonHosts { get; set; }
public bool? FakeIP { get; set; }
public bool? GlobalFakeIp { get; set; }
public string? FakeIPRange { get; set; }
public bool? BlockBindingQuery { get; set; }
public string? DirectDNS { get; set; }
public string? RemoteDNS { get; set; }
public string? BootstrapDNS { get; set; }
public string? Strategy4Freedom { get; set; }
public string? Strategy4Proxy { get; set; }
public string? Strategy4ProxyDial { get; set; }
public bool? ServeStale { get; set; }
public bool? ParallelQuery { get; set; }
public string? Hosts { 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

@@ -17,6 +17,8 @@ public record CoreConfigContext
// TUN Compatibility
public bool IsTunEnabled { get; init; } = false;
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 IsMacOS { get; init; }

View File

@@ -13,6 +13,7 @@ public class CoreInfo
public string? DownloadUrlLinux64 { get; set; }
public string? DownloadUrlLinuxArm64 { get; set; }
public string? DownloadUrlLinuxRiscV64 { get; set; }
public string? DownloadUrlLinuxLoong64 { get; set; }
public string? DownloadUrlOSX64 { get; set; }
public string? DownloadUrlOSXArm64 { get; set; }
public string? Match { get; set; }

View File

@@ -93,6 +93,7 @@ public class Rule4Sbox
public List<string>? wifi_bssid { get; set; }
public bool? rule_set_ip_cidr_match_source { get; set; }
public bool? rule_set_ip_cidr_accept_empty { get; set; }
public bool? tls_record_fragment { get; set; }
}
[Serializable]
@@ -110,6 +111,7 @@ public class Inbound4Sbox
public bool? endpoint_independent_nat { get; set; }
public string? stack { get; set; }
public List<User4Sbox> users { get; set; }
public List<string>? route_exclude_address { get; set; }
}
public class User4Sbox
@@ -246,6 +248,16 @@ public class HyObfs4Sbox
{
public string? type { get; set; }
public string? password { get; set; }
public int? min_packet_size { get; set; }
public int? max_packet_size { get; set; }
}
public class HyRealm4Sbox
{
public string? server_url { get; set; }
public string? token { get; set; }
public string? realm_id { get; set; }
public List<string>? stun_servers { get; set; }
}
public class Server4Sbox : BaseServer4Sbox
@@ -332,6 +344,7 @@ public abstract class DialFields4Sbox
public Multiplex4Sbox? multiplex { get; set; }
public Transport4Sbox? transport { get; set; }
public HyObfs4Sbox? obfs { get; set; }
public HyRealm4Sbox? realm { get; set; }
}
public abstract class BaseServer4Sbox : DialFields4Sbox

View File

@@ -4,6 +4,7 @@ public class V2rayConfig
{
public Log4Ray log { get; set; }
public object dns { get; set; }
public FakeDns4Ray? fakedns { get; set; }
public List<Inbounds4Ray> inbounds { get; set; }
public List<Outbounds4Ray> outbounds { get; set; }
public Routing4Ray routing { get; set; }
@@ -20,7 +21,7 @@ public class Stats4Ray
public class Metrics4Ray
{
public string tag { get; set; }
public string listen { get; set; }
}
public class Policy4Ray
@@ -43,6 +44,12 @@ public class Log4Ray
public string? loglevel { get; set; }
}
public class FakeDns4Ray
{
public string? ipPool { get; set; }
public long? poolSize { get; set; }
}
public class Inbounds4Ray
{
public string tag { get; set; }
@@ -86,7 +93,7 @@ public class Inboundsettings4Ray
public string? autoOutboundsInterface { get; set; }
// public List<string>? dns { get; set; }
public List<string>? dns { get; set; }
}
public class UsersItem4Ray
@@ -136,8 +143,6 @@ public class Outboundsettings4Ray
public Response4Ray? response { get; set; }
public string domainStrategy { get; set; }
public int? userLevel { get; set; }
public string? secretKey { get; set; }
@@ -146,6 +151,16 @@ public class Outboundsettings4Ray
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 bool? noKernelTun { get; set; }
@@ -288,16 +303,7 @@ public class BalancersStrategySettings4Ray
{
public int? expected { get; set; }
public string? maxRTT { get; set; }
public float? tolerance { get; set; }
public List<string>? baselines { get; set; }
public List<BalancersStrategySettingsCosts4Ray>? costs { get; set; }
}
public class BalancersStrategySettingsCosts4Ray
{
public bool? regexp { get; set; }
public string? match { get; set; }
public float? value { get; set; }
public double? tolerance { get; set; }
}
public class Observatory4Ray
@@ -372,6 +378,7 @@ public class TlsSettings4Ray
public string? spiderX { get; set; }
public string? mldsa65Verify { get; set; }
public List<CertificateSettings4Ray>? certificates { get; set; }
public string? verifyPeerCertByName { get; set; }
public string? pinnedPeerCertSha256 { get; set; }
public bool? disableSystemRoot { get; set; }
public string? echConfigList { get; set; }
@@ -487,6 +494,7 @@ public class UdpHop4Ray
public class Finalmask4Ray
{
public List<Mask4Ray>? tcp { get; set; }
public List<Mask4Ray>? udp { get; set; }
public QuicParams4Ray? quicParams { get; set; }
}
@@ -499,14 +507,23 @@ public class Mask4Ray
public class MaskSettings4Ray
{
public string? header { get; set; }
public string? value { get; set; }
public string? password { get; set; }
public string? domain { get; set; }
public string? url { get; set; }
public List<string>? stunServers { get; set; }
public string? packetSize { get; set; }
// fragment
public string? packets { get; set; }
public string? length { get; set; }
public string? delay { get; set; }
public List<string>? lengths { get; set; }
public List<string>? delays { get; set; }
public int? maxSplit { get; set; }
// noise
public int? reset { get; set; }
@@ -537,15 +554,20 @@ public class AccountsItem4Ray
public class Sockopt4Ray
{
public string? domainStrategy { get; set; }
public string? dialerProxy { get; set; }
[JsonPropertyName("interface")]
public string? Interface { get; set; }
public HappyEyeballs4Ray? happyEyeballs { get; set; }
}
public class FragmentItem4Ray
public class HappyEyeballs4Ray
{
public string? packets { get; set; }
public string? length { get; set; }
public string? interval { get; set; }
public int? tryDelayMs { get; set; }
public bool? prioritizeIPv6 { get; set; }
public int? interleave { get; set; }
public int? maxConcurrentTry { get; set; }
}

View File

@@ -0,0 +1,115 @@
namespace ServiceLib.Models.Dto;
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
// realm+http://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
// example:
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
public record HyRealm(
bool IsHttp,
string Token,
string RendezvousHost,
int RendezvousPort,
string RealmName,
List<string> StunList
)
{
public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost;
/// <summary>
/// sing-box realm.server_url requires a scheme (https:// or http://).
/// </summary>
public string ToServerUrl()
{
var scheme = IsHttp ? "http" : "https";
return $"{scheme}://{RendezvousHostPort}";
}
public static bool TryParse(string? str, out HyRealm? realm)
{
realm = null;
if (str == null)
{
return false;
}
try
{
var isHttp = str.StartsWith("realm+http://");
var prefix = isHttp ? "realm+http://" : "realm://";
if (!str.StartsWith(prefix))
{
return false;
}
var uri = new Uri(str);
var token = Utils.UrlDecode(uri.UserInfo);
var rendezvousHost = uri.Host;
var rendezvousPort = uri.IsDefaultPort ? (isHttp ? 80 : 443) : uri.Port;
var realmName = uri.AbsolutePath.TrimStart('/');
var stunList = new List<string>();
var query = uri.Query;
if (!query.IsNullOrEmpty())
{
if (query.StartsWith('?'))
{
query = query.Substring(1);
}
var pairs = query.Split('&', StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs)
{
var idx = pair.IndexOf('=');
if (idx <= 0)
{
continue;
}
var key = pair.Substring(0, idx);
if (!key.Equals("stun", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var val = pair.Substring(idx + 1);
stunList.Add(Utils.UrlDecode(val));
}
}
realm = new HyRealm(isHttp, token, rendezvousHost, rendezvousPort, realmName, stunList);
return true;
}
catch
{
return false;
}
}
public string ToUri()
{
var prefix = IsHttp ? "realm+http://" : "realm://";
var uriBuilder = new UriBuilder
{
Scheme = IsHttp ? "http" : "realm",
Host = RendezvousHost,
Port = RendezvousPort,
Path = "/" + RealmName,
UserName = Utils.UrlEncode(Token),
};
if (StunList is { Count: > 0 })
{
// var query = string.Join('&', StunList.Select(s => "stun=" + Utils.UrlEncode(s)));
// NOTE: maybe we don't need to encode the stun host:port, since it should be a valid URI component already, and encoding will make it unreadable
var query = string.Join('&', StunList.Select(s => "stun=" + s));
uriBuilder.Query = query;
}
return prefix + uriBuilder.Uri.GetComponents(
UriComponents.UserInfo | UriComponents.HostAndPort | UriComponents.PathAndQuery, UriFormat.UriEscaped);
}
public string ToUriForFinalmask()
{
var prefix = IsHttp ? "realm+http://" : "realm://";
var uriBuilder = new StringBuilder();
uriBuilder.Append(prefix);
uriBuilder.Append(Utils.UrlEncode(Token));
uriBuilder.Append('@');
uriBuilder.Append(RendezvousHostPort);
uriBuilder.Append('/');
uriBuilder.Append(Utils.UrlEncode(RealmName));
return uriBuilder.ToString();
}
}

View File

@@ -22,7 +22,7 @@ public readonly record struct IpInfoResult(string Country, string? Ip)
{
public override string ToString()
{
var emoji = Country.CountryToEmoji();
var emoji = Utils.IsWindows() ? null : Country.CountryToEmoji();
return $"{emoji}({Country}) {Ip}";
}
}

View File

@@ -1,26 +1,16 @@
namespace ServiceLib.Models.Dto;
public class RetResult
public class RetResult(bool success, string? msg, object? data)
{
public bool Success { get; set; }
public string? Msg { get; set; }
public object? Data { get; set; }
public RetResult(bool success = false)
public RetResult(bool success = false) : this(success, null, null)
{
Success = success;
}
public RetResult(bool success, string? msg)
public RetResult(bool success, string? msg) : this(success, msg, null)
{
Success = success;
Msg = msg;
}
public RetResult(bool success, string? msg, object? data)
{
Success = success;
Msg = msg;
Data = data;
}
public bool Success { get; set; } = success;
public string? Msg { get; set; } = msg;
public object? Data { get; set; } = data;
}

View File

@@ -40,4 +40,8 @@ public class VmessQRCode
public string fp { get; set; } = string.Empty;
public string insecure { get; set; } = string.Empty;
public string vcn { get; set; } = string.Empty;
public string pcs { get; set; } = string.Empty;
}

View File

@@ -146,6 +146,11 @@ public class ProfileItem
TransportExtra = JsonUtils.Serialize(transportExtra, false);
}
public bool GetAllowInsecure()
{
return AllowInsecure == Global.StringTrue;
}
#endregion function
[PrimaryKey]
@@ -191,6 +196,7 @@ public class ProfileItem
public string Cert { get; set; }
public string CertSha { get; set; }
public string EchConfigList { get; set; }
public string VerifyPeerCertByName { get; set; }
public string Finalmask { get; set; }
public string ProtoExtra { get; set; }

View File

@@ -5,6 +5,9 @@ public record ProtocolExtraItem
public bool? Uot { get; init; }
public string? CongestionControl { get; init; }
// http outbound
public string? HttpHeaders { get; init; }
// vmess
public string? AlterId { get; init; }
public string? VmessSecurity { get; init; }
@@ -31,6 +34,12 @@ public record ProtocolExtraItem
public int? DownMbps { get; init; }
public string? Ports { get; init; }
public string? HopInterval { get; init; }
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
// example:
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
public string? Hy2RealmUrl { get; init; }
public string? GeckoMinPacketSize { get; init; }
public string? GeckoMaxPacketSize { get; init; }
// naiveproxy
public int? InsecureConcurrency { get; init; }

View File

@@ -222,15 +222,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。
/// </summary>
public static string FillCorrectSendThroughIPv4 {
get {
return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture);
}
}
/// <summary>
/// 查找类似 Please enter the correct port format. 的本地化字符串。
/// </summary>
@@ -240,6 +231,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Invalid range format. Use &apos;from-to&apos; (e.g., 50-100). 的本地化字符串。
/// </summary>
public static string FillFragmentParameterError {
get {
return ResourceManager.GetString("FillFragmentParameterError", resourceCulture);
}
}
/// <summary>
/// 查找类似 Please enter the local listening port. 的本地化字符串。
/// </summary>
@@ -321,6 +321,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Please enter valid HTTP request headers JSON. 的本地化字符串。
/// </summary>
public static string InvalidHttpOutboundHeaders {
get {
return ResourceManager.GetString("InvalidHttpOutboundHeaders", resourceCulture);
}
}
/// <summary>
/// 查找类似 Invalid Realm URL. 的本地化字符串。
/// </summary>
public static string InvalidHy2RealmUrl {
get {
return ResourceManager.GetString("InvalidHy2RealmUrl", resourceCulture);
}
}
/// <summary>
/// 查找类似 Invalid address (URL) 的本地化字符串。
/// </summary>
@@ -1914,6 +1932,15 @@ namespace ServiceLib.Resx {
}
}
/// <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. 的本地化字符串。
/// </summary>
public static string MsgAllowInsecureDeprecated {
get {
return ResourceManager.GetString("MsgAllowInsecureDeprecated", resourceCulture);
}
}
/// <summary>
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
/// </summary>
@@ -2058,6 +2085,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks. 的本地化字符串。
/// </summary>
public static string MsgInsecureConfiguration {
get {
return ResourceManager.GetString("MsgInsecureConfiguration", resourceCulture);
}
}
/// <summary>
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
/// </summary>
@@ -2103,6 +2139,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Conflict between {0} and {1} 的本地化字符串。
/// </summary>
public static string MsgOptionsConflict {
get {
return ResourceManager.GetString("MsgOptionsConflict", resourceCulture);
}
}
/// <summary>
/// 查找类似 Resolved {0} successfully 的本地化字符串。
/// </summary>
@@ -2220,6 +2265,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Invalid address in TUN route exclude list: {0} 的本地化字符串。
/// </summary>
public static string MsgTunRouteExcludeInvalidAddress {
get {
return ResourceManager.GetString("MsgTunRouteExcludeInvalidAddress", resourceCulture);
}
}
/// <summary>
/// 查找类似 Unpacking... 的本地化字符串。
/// </summary>
@@ -2643,24 +2697,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Allow insecure cert fetch (self-signed) 的本地化字符串。
/// </summary>
public static string TbAllowInsecureCertFetch {
get {
return ResourceManager.GetString("TbAllowInsecureCertFetch", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only for fetching self-signed certificates. This may expose you to MITM risks. 的本地化字符串。
/// </summary>
public static string TbAllowInsecureCertFetchTips {
get {
return ResourceManager.GetString("TbAllowInsecureCertFetchTips", resourceCulture);
}
}
/// <summary>
/// 查找类似 ALPN 的本地化字符串。
/// </summary>
@@ -3006,6 +3042,51 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Enable Final Fragment 的本地化字符串。
/// </summary>
public static string TbEnableFinalFragment {
get {
return ResourceManager.GetString("TbEnableFinalFragment", resourceCulture);
}
}
/// <summary>
/// 查找类似 Split the tail of packets into smaller fragments. This may affect throughput and latency. 的本地化字符串。
/// </summary>
public static string TbEnableFinalFragmentTip {
get {
return ResourceManager.GetString("TbEnableFinalFragmentTip", resourceCulture);
}
}
/// <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>
/// 查找类似 DNS via Bridge 的本地化字符串。
/// </summary>
public static string TbEnabletDnsViaProxy {
get {
return ResourceManager.GetString("TbEnabletDnsViaProxy", resourceCulture);
}
}
/// <summary>
/// 查找类似 Enable Tun 的本地化字符串。
/// </summary>
@@ -3025,7 +3106,7 @@ namespace ServiceLib.Resx {
}
/// <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>
public static string TbFakeIPTips {
get {
@@ -3114,6 +3195,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
/// </summary>
public static string TbGeckoPacketSize {
get {
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
}
}
/// <summary>
/// 查找类似 Global Hotkey Settings 的本地化字符串。
/// </summary>
@@ -3177,6 +3267,33 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 HTTP headers 的本地化字符串。
/// </summary>
public static string TbHttpOutboundHeaders {
get {
return ResourceManager.GetString("TbHttpOutboundHeaders", resourceCulture);
}
}
/// <summary>
/// 查找类似 Realm URL 的本地化字符串。
/// </summary>
public static string TbHy2RealmUrl {
get {
return ResourceManager.GetString("TbHy2RealmUrl", resourceCulture);
}
}
/// <summary>
/// 查找类似 Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port] 的本地化字符串。
/// </summary>
public static string TbHy2RealmUrlTip {
get {
return ResourceManager.GetString("TbHy2RealmUrlTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 ICMP routing policy 的本地化字符串。
/// </summary>
@@ -3231,6 +3348,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>
/// 查找类似 Most Stable 的本地化字符串。
/// </summary>
@@ -3258,6 +3393,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 If enabled, use sing-box TUN; otherwise, use xray TUN. 的本地化字符串。
/// </summary>
public static string TbLegacyProtectTip {
get {
return ResourceManager.GetString("TbLegacyProtectTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Address (IPv4, IPv6) 的本地化字符串。
/// </summary>
@@ -3420,6 +3564,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>
/// 查找类似 Public Key 的本地化字符串。
/// </summary>
@@ -3519,6 +3681,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Root Certificate Provider 的本地化字符串。
/// </summary>
public static string TbRootCertificateProvider {
get {
return ResourceManager.GetString("TbRootCertificateProvider", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only applies to the v2rayN GUI&apos;s downloads and network requests. Does not affect the core&apos;s certificate validation. 的本地化字符串。
/// </summary>
public static string TbRootCertificateProviderTip {
get {
return ResourceManager.GetString("TbRootCertificateProviderTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Round Robin 的本地化字符串。
/// </summary>
@@ -3528,6 +3708,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Route Exclude Address 的本地化字符串。
/// </summary>
public static string TbRouteExcludeAddress {
get {
return ResourceManager.GetString("TbRouteExcludeAddress", resourceCulture);
}
}
/// <summary>
/// 查找类似 Use commas (,) to separate. 的本地化字符串。
/// </summary>
public static string TbRouteExcludeAddressTip {
get {
return ResourceManager.GetString("TbRouteExcludeAddressTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// </summary>
@@ -3763,7 +3961,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode 的本地化字符串。
/// 查找类似 For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled. 的本地化字符串。
/// </summary>
public static string TbSettingsBindInterfaceTip {
get {
@@ -4050,6 +4248,96 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Fallback Delay 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentFallbackDelay {
get {
return ResourceManager.GetString("TbSettingsFragmentFallbackDelay", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentFallbackDelayTip {
get {
return ResourceManager.GetString("TbSettingsFragmentFallbackDelayTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Interval 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentInterval {
get {
return ResourceManager.GetString("TbSettingsFragmentInterval", resourceCulture);
}
}
/// <summary>
/// 查找类似 Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be &lt;= second. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentIntervalTip {
get {
return ResourceManager.GetString("TbSettingsFragmentIntervalTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Length 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentLength {
get {
return ResourceManager.GetString("TbSettingsFragmentLength", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment size range in bytes (e.g., 50-100). First value must be &lt;= second. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentLengthTip {
get {
return ResourceManager.GetString("TbSettingsFragmentLengthTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Max Split 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentMaxSplit {
get {
return ResourceManager.GetString("TbSettingsFragmentMaxSplit", resourceCulture);
}
}
/// <summary>
/// 查找类似 Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentMaxSplitTip {
get {
return ResourceManager.GetString("TbSettingsFragmentMaxSplitTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Packets 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentPackets {
get {
return ResourceManager.GetString("TbSettingsFragmentPackets", resourceCulture);
}
}
/// <summary>
/// 查找类似 Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets) 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentPacketsTip {
get {
return ResourceManager.GetString("TbSettingsFragmentPacketsTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Geo files source (optional) 的本地化字符串。
/// </summary>
@@ -4698,6 +4986,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Verify Peer Cert By Name 的本地化字符串。
/// </summary>
public static string TbVerifyPeerCertByName {
get {
return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture);
}
}
/// <summary>
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
/// </summary>
@@ -4761,6 +5058,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>
/// 查找类似 *Default value raw 的本地化字符串。
/// </summary>

View File

@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>فعال کردن فرگمنت</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>پکت‌های فرگمنت</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>پکت‌های قابل فرگمنت: tlshello (TLS ClientHello) یا 1-1 تا 1-5 (N اول پکت TCP)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>طول فرگمنت</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>محدوده اندازه فرگمنت بایت (مثلا 50-100). اول ≤ دوم. خالی = پیش‌فرض.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>فاصله فرگمنت</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>تأخیر بین فرگمنت‌ها میلی‌ثانیه (مثلا 10-20). بازه 1-100. اول ≤ دوم. خالی = پیش‌فرض.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>حداکثر تقسیم</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>مکسیمم تعداد تقسیم (0 = نامحدود، 0-10000). فقط Xray-core. خالی = پیش‌فرض.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>تأخیر فول‌بک</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>تأخیر فول‌بک فرگمنت TLS. فقط sing-box. خالی = پیش‌فرض.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>فرمت محدوده نامعتبر. از 'from-to' استفاده کنید (مثلا 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value>
</data>
@@ -1462,7 +1495,7 @@
<value>Select Profile</value>
</data>
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
@@ -1698,12 +1731,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value>
</data>
@@ -1716,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value>
</data>

View File

@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Activer le fragmentation (Fragment)</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Paquets fragmentés</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Paquets à fragmenter : tlshello (TLS ClientHello) ou 1-1 à 1-5 (premiers N paquets TCP)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Longueur fragment</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Plage de taille fragment (ex: 50-100). Première valeur ≤ seconde. Vide = défaut.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Intervalle fragment</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Délai entre fragments en ms (ex: 10-20). Plage 1-100. Première valeur ≤ seconde. Vide = défaut.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Max Split</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Nb max de splits (0 = illimité, 0-10000). Xray-core seulement. Vide = défaut.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Délai fallback</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Délai fallback pour fragment TLS. sing-box seulement. Vide = défaut.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Format de plage invalide. Utilisez 'from-to' (ex: 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Activer le fichier de cache de sing-box (fichiers règles)</value>
</data>
@@ -1459,7 +1492,7 @@
<value>Choisir une config.</value>
</data>
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Veuillez ajouter au moins une configuration</value>
@@ -1675,7 +1708,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Ajouter [NaïveProxy]</value>
</data>
<data name="TbInsecureConcurrency" xml:space="preserve">
<value>Insecure Concurrency</value>
<value>Concurrence non sûre</value>
</data>
<data name="TbUsername" xml:space="preserve">
<value>Nom dutilisateur</value>
@@ -1695,21 +1728,12 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Adresse sortante locale (SendThrough)</value>
<value>Adresse de sortie locale (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Veuillez saisir ladresse IPv4 correcte de SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Lier l'interface</value>
</data>
@@ -1717,30 +1741,30 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value>
<value>Tester la latence UDP des profils</value>
</data>
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>UDP Test Url</value>
<value>URL de test UDP</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value>
</data>
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Export v2rayN Internal Share Link to Clipboard</value>
<value>Exporter le lien interne v2rayN vers le presse-papiers</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
<value>{0} a une nouvelle version disponible: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
<value>Uniquement vérifier</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
<value>Non supporté</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
<value>Détails IP</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
<value>Nouvelle MAJ</value>
</data>
</root>

View File

@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Fragment engedélyezése</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Fragmentált csomagok</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Fragmentálandó csomagok: tlshello (TLS ClientHello) vagy 1-1 1-5 (az első N TCP csomag)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Fragment hossz</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Fragment méret tartománya bájtokban (pl. 50-100). Az első érték ≤ második. Üres = alapértelmezett.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Fragment intervallum</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Késleltetés a fragmented csomagok között ms-ben (pl. 10-20). Tartomány 1-100. Az első érték ≤ második. Üres = alapértelmezett.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Max darabolás</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Maximális darabolások száma (0 = korlátlan, 0-10000). Csak Xray-core. Üres = alapértelmezett.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Fallback késleltetés</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>TLS fragment fallback késleltetés. Csak sing-box. Üres = alapértelmezett.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Érvénytelen tartomány formátum. Használja 'from-to' (pl. 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value>
</data>
@@ -1462,7 +1495,7 @@
<value>Select Profile</value>
</data>
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
@@ -1698,12 +1731,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value>
</data>
@@ -1716,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value>
</data>

File diff suppressed because it is too large Load Diff

View File

@@ -1044,6 +1044,12 @@
<data name="TbHeaderType8" xml:space="preserve">
<value>Congestion control</value>
</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">
<value>Previous proxy remarks</value>
</data>
@@ -1104,6 +1110,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Enable fragment</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Fragment Packets</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Fragment Length</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Fragment size range in bytes (e.g., 50-100). First value must be &lt;= second. Empty = default.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Fragment Interval</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be &lt;= second. Empty = default.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Max Split</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Fallback Delay</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Invalid range format. Use 'from-to' (e.g., 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Enable cache file for sing-box (ruleset files)</value>
</data>
@@ -1462,7 +1501,7 @@
<value>Select Profile</value>
</data>
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
@@ -1581,6 +1620,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
@@ -1698,12 +1740,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value>
</data>
@@ -1716,14 +1752,11 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value>
</data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value>
<value>For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled.</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value>
@@ -1746,4 +1779,73 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
<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>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Route Exclude Address</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>Use commas (,) to separate.</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Invalid address in TUN route exclude list: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>Conflict between {0} and {1}</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>Enable Final Fragment</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS via Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Invalid Realm URL.</value>
</data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>Please enter valid HTTP request headers JSON.</value>
</data>
<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>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko Packet Size (min/max)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>If enabled, use sing-box TUN; otherwise, use xray TUN.</value>
</data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>Root Certificate Provider</value>
</data>
<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>
</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>
</root>

View File

@@ -121,7 +121,7 @@
<value>Ссылка успешно скопирована в буфер обмена</value>
</data>
<data name="CheckServerSettings" xml:space="preserve">
<value>Сначала проверьте настройки сервера</value>
<value>Недопустимая конфигурация, проверьте или выберите заново</value>
</data>
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
<value>Недопустимый формат конфигурации</value>
@@ -166,7 +166,7 @@
<value>Некорректная конфигурация. Проверьте</value>
</data>
<data name="InitialConfiguration" xml:space="preserve">
<value>Исходная конфигурация</value>
<value>Инициализация конфигурации</value>
</data>
<data name="IsLatestCore" xml:space="preserve">
<value>{0} {1} является последней версией</value>
@@ -256,7 +256,7 @@
<value>Операция не удалась, проверьте и повторите попытку</value>
</data>
<data name="PleaseFillRemarks" xml:space="preserve">
<value>Введите примечания</value>
<value>Введите псевдоним</value>
</data>
<data name="PleaseSelectEncryption" xml:space="preserve">
<value>Выберите метод шифрования</value>
@@ -307,7 +307,7 @@
<value>{0}: одно из обязательных полей</value>
</data>
<data name="LvRemarks" xml:space="preserve">
<value>Примечания</value>
<value>Псевдоним</value>
</data>
<data name="LvUrl" xml:space="preserve">
<value>URL (необязательно)</value>
@@ -334,13 +334,13 @@
<value>Введите корректный пользовательский DNS</value>
</data>
<data name="TransportPathTip1" xml:space="preserve">
<value>*Путь ws/http upgrade/xhttp</value>
<value>*Путь ws/httpupgrade/xhttp</value>
</data>
<data name="TransportPathTip2" xml:space="preserve">
<value>*HTTP2-путь</value>
</data>
<data name="TransportPathTip3" xml:space="preserve">
<value>*QUIC-ключ / KCP-seed</value>
<value>*Ключ шифрования QUIC</value>
</data>
<data name="TransportPathTip4" xml:space="preserve">
<value>Имя сервиса gRPC</value>
@@ -349,13 +349,13 @@
<value>*http-хосты, разделённые запятыми (,)</value>
</data>
<data name="TransportRequestHostTip2" xml:space="preserve">
<value>*Хост ws/http upgrade/xhttp</value>
<value>*Хост ws/httpupgrade/xhttp</value>
</data>
<data name="TransportRequestHostTip3" xml:space="preserve">
<value>*HTTP2-хосты, разделённые запятыми (,)</value>
</data>
<data name="TransportRequestHostTip4" xml:space="preserve">
<value>Безопасность *QUIC</value>
<value>*Метод шифрования QUIC</value>
</data>
<data name="TransportHeaderType1" xml:space="preserve">
<value>Тип raw-камуфляжа</value>
@@ -400,7 +400,7 @@
<value>Фильтр, нажмите Enter для выполнения</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Проверить обновления</value>
<value>Обновить</value>
</data>
<data name="menuClose" xml:space="preserve">
<value>Закрыть</value>
@@ -415,13 +415,13 @@
<value>Помощь</value>
</data>
<data name="menuOptionSetting" xml:space="preserve">
<value>Настройки</value>
<value>Параметры</value>
</data>
<data name="menuPromotion" xml:space="preserve">
<value>Продвижение</value>
</data>
<data name="menuReload" xml:space="preserve">
<value>Перезагрузить</value>
<value>Перезапустить службу</value>
</data>
<data name="menuRoutingSetting" xml:space="preserve">
<value>Настройки маршрутизации</value>
@@ -445,10 +445,10 @@
<value>Настройки групп подписки</value>
</data>
<data name="menuSubUpdate" xml:space="preserve">
<value>Обновить подписку без прокси</value>
<value>Обновить все подписки без прокси</value>
</data>
<data name="menuSubUpdateViaProxy" xml:space="preserve">
<value>Обновить подписку с прокси</value>
<value>Обновить все подписки через прокси</value>
</data>
<data name="menuSystemproxy" xml:space="preserve">
<value>Системный прокси</value>
@@ -562,7 +562,7 @@
<value>Сортировка</value>
</data>
<data name="LvUserAgent" xml:space="preserve">
<value>User-Agent</value>
<value>User-Agent (необязательно)</value>
</data>
<data name="TbCancel" xml:space="preserve">
<value>Отмена</value>
@@ -577,7 +577,7 @@
<value>Адрес</value>
</data>
<data name="TbAllowInsecure" xml:space="preserve">
<value>Разрешить небезопасные</value>
<value>Пропустить проверку сертификата (allowInsecure)</value>
</data>
<data name="TbAlpn" xml:space="preserve">
<value>ALPN</value>
@@ -616,10 +616,10 @@
<value>TLS</value>
</data>
<data name="TipNetwork" xml:space="preserve">
<value>*По-умолчанию raw</value>
<value>*По умолчанию raw; при неверном выборе подключение будет невозможно</value>
</data>
<data name="TbCoreType" xml:space="preserve">
<value>Ядро</value>
<value>Тип ядра</value>
</data>
<data name="TbFlow5" xml:space="preserve">
<value>Управление потоком (Flow)</value>
@@ -640,7 +640,7 @@
<value>Шифрование</value>
</data>
<data name="TbSecurity4" xml:space="preserve">
<value>Пользователь (необязательно)</value>
<value>Имя пользователя (необязательно)</value>
</data>
<data name="TbSecurity5" xml:space="preserve">
<value>Шифрование</value>
@@ -649,7 +649,7 @@
<value>Порт SOCKS</value>
</data>
<data name="TipPreSocksPort" xml:space="preserve">
<value>* После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости</value>
<value>*Порт SOCKS пользовательской конфигурации можно не указывать. Если он задан, Xray/sing-box (TUN) дополнительно запустит предварительную службу SOCKS для разделения трафика, отображения скорости и других функций</value>
</data>
<data name="TbBrowse" xml:space="preserve">
<value>Обзор</value>
@@ -682,7 +682,7 @@
<value>Настройки типа ядра</value>
</data>
<data name="TbSettingsDefAllowInsecure" xml:space="preserve">
<value>Разрешить небезопасные</value>
<value>По умолчанию пропускать проверку сертификата (allowInsecure)</value>
</data>
<data name="TbSettingsDomainStrategy4Freedom" xml:space="preserve">
<value>«Freedom»: стратегия обработки доменов исходящего трафика</value>
@@ -703,7 +703,7 @@
<value>Показывать скорость в реальном времени (требуется перезапуск)</value>
</data>
<data name="TbSettingsKeepOlderDedupl" xml:space="preserve">
<value>Сохранить старые при удалении дублей</value>
<value>При удалении дубликатов сохранять элементы с меньшим порядковым номером</value>
</data>
<data name="TbSettingsLogEnabled" xml:space="preserve">
<value>Записывать логи</value>
@@ -730,13 +730,13 @@
<value>Смешанный порт</value>
</data>
<data name="TbSettingsStartBoot" xml:space="preserve">
<value>Запускать при старте системы</value>
<value>Запускать при старте системы (может не сработать)</value>
</data>
<data name="TbSettingsStatistics" xml:space="preserve">
<value>Включить статистику трафика (требуется перезапуск)</value>
</data>
<data name="TbSettingsSubConvert" xml:space="preserve">
<value>URL конвертации подписок</value>
<value>URL конвертации подписок (необязательно)</value>
</data>
<data name="TbSettingsSystemproxy" xml:space="preserve">
<value>Настройки системного прокси</value>
@@ -787,13 +787,13 @@
<value>Запущено от имени администратора</value>
</data>
<data name="menuMoveBottom" xml:space="preserve">
<value>Спуститься вниз</value>
<value>Переместить в самый низ</value>
</data>
<data name="menuMoveDown" xml:space="preserve">
<value>Вниз</value>
</data>
<data name="menuMoveTop" xml:space="preserve">
<value>Подняться наверх</value>
<value>Переместить в самый верх</value>
</data>
<data name="menuMoveUp" xml:space="preserve">
<value>Вверх</value>
@@ -805,7 +805,7 @@
<value>Веб-сайт {0}</value>
</data>
<data name="menuRoutingAdvancedAdd" xml:space="preserve">
<value>Добавить</value>
<value>Добавить набор правил</value>
</data>
<data name="menuRoutingAdvancedImportRules" xml:space="preserve">
<value>Импортировать правила</value>
@@ -841,7 +841,7 @@
<value>Добавить правило</value>
</data>
<data name="menuRuleExportSelected" xml:space="preserve">
<value>Экспорт выделенных правил</value>
<value>Экспортировать выбранные правила в буфер обмена</value>
</data>
<data name="menuRuleList" xml:space="preserve">
<value>Список правил</value>
@@ -859,7 +859,7 @@
<value>Документация RuleObject</value>
</data>
<data name="TbDnsObjectDoc" xml:space="preserve">
<value>Поддерживаются DNS-объекты, нажмите для просмотра документации</value>
<value>Заполните DNS-объект (формат JSON), нажмите для просмотра документации</value>
</data>
<data name="SubUrlTips" xml:space="preserve">
<value>Для группы оставьте это поле пустым</value>
@@ -871,13 +871,13 @@
<value>Системные прокси изменены</value>
</data>
<data name="TbSettingsRouteOnly" xml:space="preserve">
<value>Только маршрут</value>
<value>Только для маршрутизации (routeOnly)</value>
</data>
<data name="TbSettingsNotProxyLocalAddress" xml:space="preserve">
<value>Не использовать прокси для локальных (интранет) адресов</value>
</data>
<data name="menuMixedTestServer" xml:space="preserve">
<value>Тест задержки и скорости всех серверов (Ctrl+E)</value>
<value>Многопоточный тест задержки и скорости (Ctrl+E)</value>
</data>
<data name="LvTestDelay" xml:space="preserve">
<value>Задержка (мс)</value>
@@ -886,10 +886,10 @@
<value>Скорость (МБ/с)</value>
</data>
<data name="FailedToRunCore" xml:space="preserve">
<value>Не удалось запустить ядро, посмотрите логи</value>
<value>Не удалось запустить ядро, см. сообщение</value>
</data>
<data name="LvFilter" xml:space="preserve">
<value>Фильтр по примечаниям (регулярные выражения)</value>
<value>Фильтр по псевдониму (регулярные выражения)</value>
</data>
<data name="TbDisplayLog" xml:space="preserve">
<value>Отображать журнал</value>
@@ -937,7 +937,7 @@
<value>Шрифт (требуется перезапуск)</value>
</data>
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек</value>
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и перезапустите приложение</value>
</data>
<data name="TbSettingsSocksPortTip" xml:space="preserve">
<value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value>
@@ -1009,7 +1009,7 @@
<value>Пользовательский DNS для sing-box</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>Заполните структуру DNS, нажмите, чтобы открыть документ</value>
<value>Заполните структуру DNS (формат JSON), нажмите для просмотра документации</value>
</data>
<data name="TbSettingDnsImportDefConfig" xml:space="preserve">
<value>Нажмите, чтобы импортировать конфигурацию DNS по умолчанию</value>
@@ -1033,7 +1033,7 @@
<value>Добавить сервер [Hysteria2]</value>
</data>
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
<value>Максимальная пропускная способность Hysteria (загрузка/отдача)</value>
<value>Максимальная пропускная способность Hysteria (отдача/загрузка)</value>
</data>
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
<value>Использовать системный файл hosts</value>
@@ -1044,14 +1044,20 @@
<data name="TbHeaderType8" xml:space="preserve">
<value>Управление перегрузками</value>
</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">
<value>Примечания к предыдущему прокси</value>
<value>Псевдоним предыдущего прокси</value>
</data>
<data name="LvNextProfile" xml:space="preserve">
<value>Примечания к следующему прокси</value>
<value>Псевдоним следующего прокси</value>
</data>
<data name="LvPrevProfileTip" xml:space="preserve">
<value>Убедитесь, что примечание существует и является уникальным</value>
<value>Убедитесь, что псевдоним существует и является уникальным</value>
</data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>Автоматическая маршрутизация</value>
@@ -1104,6 +1110,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Включить фрагментацию (Fragment)</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Пакеты для фрагментации</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Пакеты для фрагментации: tlshello (TLS ClientHello) или 1-1 — 1-5 (первые N TCP-пакетов)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Длина фрагмента</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Интервал фрагментации</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Макс. разделение</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Резервная задержка</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
</data>
@@ -1177,7 +1216,7 @@
<value>Глобальный режим</value>
</data>
<data name="menuModeNothing" xml:space="preserve">
<value>Не менять</value>
<value>Как в исходной конфигурации</value>
</data>
<data name="menuModeRule" xml:space="preserve">
<value>Правила</value>
@@ -1210,7 +1249,7 @@
<value>Экспорт ссылок в формате Base64 в буфер обмена</value>
</data>
<data name="menuExport2ClientConfigClipboard" xml:space="preserve">
<value>Экспортировать полную конфигурацию в буфер обмена</value>
<value>Экспортировать выбранную полную конфигурацию в буфер обмена</value>
</data>
<data name="menuShowOrHideMainWindow" xml:space="preserve">
<value>Показать или скрыть главное окно</value>
@@ -1300,7 +1339,7 @@
<value>Не используйте небезопасный адрес подписки по протоколу HTTP</value>
</data>
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
<value>Установите шрифт в систему, выберите или введите имя шрифта, перезапустите настройки</value>
<value>Установите шрифт в систему, выберите или введите имя шрифта и перезапустите приложение</value>
</data>
<data name="menuExitTips" xml:space="preserve">
<value>Вы уверены, что хотите выйти?</value>
@@ -1318,13 +1357,13 @@
<value>XHTTP-режим</value>
</data>
<data name="TransportExtraTip" xml:space="preserve">
<value>Сырой JSON, формат: { XHTTP Object }</value>
<value>Сырой JSON, формат: { XHTTPObject }</value>
</data>
<data name="TbSettingsHide2TrayWhenClose" xml:space="preserve">
<value>Сворачивать в трей при закрытии окна</value>
</data>
<data name="TbSettingsMixedConcurrencyCount" xml:space="preserve">
<value>Количество одновременно выполняемых тестов при многоэтапном тестировании</value>
<value>Количество параллельных задач при многопоточном тестировании</value>
</data>
<data name="TbSettingsExceptionTip2" xml:space="preserve">
<value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value>
@@ -1336,7 +1375,7 @@
<value>Включить второй смешанный порт</value>
</data>
<data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
<value>tun: входящий TUN, socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>Тема</value>
@@ -1357,7 +1396,7 @@
<value>Удалено {0} недействительных</value>
</data>
<data name="TbPorts7" xml:space="preserve">
<value>Диапазон портов сервера</value>
<value>Диапазон портов для переключения (Port Hopping)</value>
</data>
<data name="TbPorts7Tips" xml:space="preserve">
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
@@ -1369,7 +1408,7 @@
<value>URL для тестирования текущего соединения</value>
</data>
<data name="TbRuleOutboundTagTip" xml:space="preserve">
<value>Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально</value>
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
</data>
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
<value>Неверный пароль, попробуйте ещё раз.</value>
@@ -1384,7 +1423,7 @@
<value>Удалённый DNS</value>
</data>
<data name="TbDomesticDNS" xml:space="preserve">
<value>Внутренний DNS</value>
<value>DNS для прямых подключений</value>
</data>
<data name="TbDirectResolveStrategy" xml:space="preserve">
<value>Стратегия разрешения прямых соединений</value>
@@ -1462,7 +1501,7 @@
<value>Выбрать профиль</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>По умолчанию применяется глобально, со встроенной фильтрацией FakeIP (только sing-box).</value>
<value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Добавьте хотя бы одну конфигурацию</value>
@@ -1489,13 +1528,13 @@
<value>Тип группы политик</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Добавить группу политик </value>
<value>Добавить группу политик</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Добавить цепочку прокси</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Добавить дочернюю конфигурацию </value>
<value>Добавить дочернюю конфигурацию</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Удалить дочернюю конфигурацию </value>
@@ -1534,7 +1573,7 @@
<value>Bootstrap DNS</value>
</data>
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Разрешает домены DNS-серверов, требуется IP-адрес</value>
<value>Для разрешения доменных имён DNS-серверов необходимо указать IP-адрес</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>Тест реальной задержки</value>
@@ -1547,7 +1586,7 @@
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Привязанный сертификат (заполните любое из полей)
При указании сертификат будет привязан, а «Разрешить небезопасные» отключится.
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
</data>
@@ -1581,6 +1620,9 @@
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Проверять сертификат узла по имени</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Полный сертификат (цепочка) в формате PEM</value>
</data>
@@ -1698,12 +1740,6 @@
<data name="TransportExtra" xml:space="preserve">
<value>Дополнительные параметры XHTTP (Extra)</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Разрешить небезопасную загрузку сертификата (самоподписанного)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Только для загрузки самоподписанных сертификатов. Это может подвергнуть вас риску атаки «человек посередине» (MITM).</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>Тест UDP-задержки конфигураций</value>
</data>
@@ -1716,14 +1752,11 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Привязать интерфейс</value>
</data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value>
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>Общий ключ (PSK)</value>
@@ -1732,18 +1765,69 @@
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
<value>Доступна новая версия {0}: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
<value>Проверить</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
<value>Не поддерживается</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
<value>Информация об IP</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
<value>Доступно обновление</value>
</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>
</root>

View File

@@ -1030,7 +1030,7 @@
<value>添加 [Hysteria2]</value>
</data>
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
<value>Hysteria 最大带宽 (Up/Dw)</value>
<value>Hysteria 最大带宽 (上传/下载)</value>
</data>
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
<value>使用系统 hosts</value>
@@ -1041,6 +1041,12 @@
<data name="TbHeaderType8" xml:space="preserve">
<value>拥塞控制算法</value>
</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">
<value>前置代理配置别名</value>
</data>
@@ -1101,6 +1107,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>启用分片 (Fragment)</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>分片包类型</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>要分片的包类型: tlshello (TLS ClientHello) 或 1-1 到 1-5 (前 N 个 TCP 包)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>分片长度</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>分片大小范围(字节), 如 50-100. 第一个值必须 &lt;= 第二个值. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>分片间隔</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>分片间延迟(毫秒), 如 10-20. 范围 1-100. 第一个值必须 &lt;= 第二个值. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>最大分片数</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>每个包最大分片数 (0-10000, 0=无限制). 仅 Xray. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>回退延迟</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>ACK 检测不可用时的回退延迟 (如 500ms). 仅 sing-box. 留空使用默认值.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>无效的范围格式。请使用 'from-to' (如 50-100)。</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>启用 sing-box (规则集文件) 的缓存文件</value>
</data>
@@ -1459,7 +1498,7 @@
<value>选择配置</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value>
<value>默认全局生效,仅在 sing-box 中内置 FakeIP 过滤。</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>请至少添加一个配置</value>
@@ -1695,12 +1734,6 @@
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>允许不安全获取证书(自签名)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>仅用于抓取自签证书,存在中间人风险。</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>测试 UDP 延迟 (多选)</value>
</data>
@@ -1713,9 +1746,6 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>用于多网口环境,请填写本机 IPv4 地址</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>请填写正确的 SendThrough IPv4 地址。</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>绑定网口</value>
</data>
@@ -1743,4 +1773,76 @@
<data name="menuNewUpdate" xml:space="preserve">
<value>有更新</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 </value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</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>启用末端分片</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 已启用,但未提供证书。这可能会导致中间人攻击。</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>请填写合法的HTTP请求头JSON。</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>连接代理解析策略</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>
</root>

View File

@@ -1041,6 +1041,12 @@
<data name="TbHeaderType8" xml:space="preserve">
<value>擁塞控制算法</value>
</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">
<value>前置代理節點別名</value>
</data>
@@ -1101,6 +1107,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>啟用分片Fragment</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>分片封包類型</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>要分片的封包類型tlshello (TLS ClientHello) 或 1-1 到 1-5 (前 N 個 TCP 封包)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>分片長度</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>分片大小範圍(位元組),如 50-100。第一個值必須 &lt;= 第二個值。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>分片間隔</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>分片間延遲(毫秒),如 10-20。範圍 1-100。第一個值必須 &lt;= 第二個值。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>最大分片數</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>每個封包最大分片數 (0-10000, 0=無限制)。僅 Xray。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>回退延遲</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>ACK 偵測不可用時的回退延遲 (如 500ms)。僅 sing-box。留空使用預設值。</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>無效的範圍格式。請使用 'from-to' (如 50-100)。</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>啟用 sing-box規則集檔案的快取檔案</value>
</data>
@@ -1333,7 +1372,7 @@
<value>開啟第二個本機監聽埠</value>
</data>
<data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks埠,socks2第二個本埠,socks3區域網路埠</value>
<value>TunTUN 入站Socks機連接埠,Socks2第二個本機連接埠,Socks3區域網路連接埠</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>主題</value>
@@ -1459,7 +1498,7 @@
<value>選擇節點</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>默認全生效,內置 FakeIP 過濾,僅在 sing-box 中生效</value>
<value>默認全生效,僅在 sing-box 中內置 FakeIP 過濾。</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>請至少添加一個節點</value>
@@ -1695,12 +1734,6 @@
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>允許不安全獲取證書(自簽名)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>僅用於抓取自簽證書,存在中間人風險。</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>測試 UDP 延遲(多選)</value>
</data>
@@ -1713,9 +1746,6 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>適用於多網路介面環境,請填寫本機 IPv4 位址</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>請填寫正確的 SendThrough IPv4 位址。</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>綁定網路介面</value>
</data>
@@ -1743,4 +1773,76 @@
<data name="menuNewUpdate" xml:space="preserve">
<value>有更新</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。</value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</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>啟用末端分片</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="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">
<value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value>
</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>
</root>

View File

@@ -17,14 +17,6 @@
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"inboundTag": [
"api"
],
"outboundTag": "api",
"type": "field"
}
]
"rules": []
}
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Downloader" />
<PackageReference Include="IPNetwork2" />
<PackageReference Include="ReactiveUI">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
@@ -22,6 +23,7 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Sample\chrome_roots_pem" />
<EmbeddedResource Include="Sample\clash_mixin_yaml" />
<EmbeddedResource Include="Sample\clash_tun_yaml" />
<EmbeddedResource Include="Sample\custom_routing_black" />
@@ -31,6 +33,7 @@
<EmbeddedResource Include="Sample\dns_v2ray_normal" />
<EmbeddedResource Include="Sample\kill_as_sudo_linux_sh" />
<EmbeddedResource Include="Sample\kill_as_sudo_osx_sh" />
<EmbeddedResource Include="Sample\mozilla_roots_pem" />
<EmbeddedResource Include="Sample\pac" />
<EmbeddedResource Include="Sample\proxy_set_linux_sh" />
<EmbeddedResource Include="Sample\proxy_set_osx_sh" />
@@ -56,7 +59,7 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<EmbeddedResource Update="Resx\ResUI.fa-Ir.resx">
<EmbeddedResource Update="Resx\ResUI.fa.resx">
<SubType>Designer</SubType>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
@@ -66,6 +69,9 @@
<EmbeddedResource Update="Resx\ResUI.hu.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.id.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.resx">
<SubType>Designer</SubType>
<LastGenOutput>ResUI.Designer.cs</LastGenOutput>

View File

@@ -3,16 +3,10 @@ namespace ServiceLib.Services.CoreConfig;
/// <summary>
/// Core configuration file processing class
/// </summary>
public class CoreConfigClashService
public class CoreConfigClashService(Config config)
{
private Config _config;
private static readonly string _tag = "CoreConfigClashService";
public CoreConfigClashService(Config config)
{
_config = config;
}
public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName)
{
var ret = new RetResult();
@@ -56,7 +50,7 @@ public class CoreConfigClashService
var tagYamlStr1 = "!<str>";
var tagYamlStr2 = "__strn__";
var tagYamlStr3 = "!!str";
var txtFile = File.ReadAllText(addressFileName);
var txtFile = await File.ReadAllTextAsync(addressFileName);
txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2);
//YAML anchors
@@ -75,13 +69,13 @@ public class CoreConfigClashService
//mixed-port
fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
//log-level
fileContent["log-level"] = GetLogLevel(_config.CoreBasicItem.Loglevel);
fileContent["log-level"] = GetLogLevel(config.CoreBasicItem.Loglevel);
//external-controller
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
fileContent.Remove("secret");
//allow-lan
if (_config.Inbound.First().AllowLANConn)
if (config.Inbound.First().AllowLANConn)
{
fileContent["allow-lan"] = "true";
fileContent["bind-address"] = "*";
@@ -92,23 +86,23 @@ public class CoreConfigClashService
}
//ipv6
fileContent["ipv6"] = _config.ClashUIItem.EnableIPv6;
fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
//mode
if (!fileContent.ContainsKey("mode"))
{
fileContent["mode"] = ERuleMode.Rule.ToString().ToLower();
fileContent["mode"] = nameof(ERuleMode.Rule).ToLower();
}
else
{
if (_config.ClashUIItem.RuleMode != ERuleMode.Unchanged)
if (config.ClashUIItem.RuleMode != ERuleMode.Unchanged)
{
fileContent["mode"] = _config.ClashUIItem.RuleMode.ToString().ToLower();
fileContent["mode"] = config.ClashUIItem.RuleMode.ToString().ToLower();
}
}
//enable tun mode
if (_config.TunModeItem.EnableTun)
if (config.TunModeItem.EnableTun)
{
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
if (tun.IsNotEmpty())
@@ -156,7 +150,7 @@ public class CoreConfigClashService
private async Task MixinContent(Dictionary<string, object> fileContent, ProfileItem node)
{
if (!_config.ClashUIItem.EnableMixinContent)
if (!config.ClashUIItem.EnableMixinContent)
{
return;
}
@@ -177,7 +171,7 @@ public class CoreConfigClashService
}
foreach (var item in mixinContent)
{
if (!_config.TunModeItem.EnableTun && item.Key == "tun")
if (!config.TunModeItem.EnableTun && item.Key == "tun")
{
continue;
}
@@ -220,9 +214,8 @@ public class CoreConfigClashService
return;
}
if (!blRemoved && !fileContent.ContainsKey(key))
if (!blRemoved && fileContent.TryAdd(key, value))
{
fileContent.Add(key, value);
return;
}
var lstOri = (List<object>)fileContent[key];
@@ -244,7 +237,7 @@ public class CoreConfigClashService
}
else
{
lstValue.ForEach(item => lstOri.Add(item));
lstValue.ForEach(lstOri.Add);
}
}

View File

@@ -153,7 +153,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
{
listen = Global.Loopback,
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
type = nameof(EInboundProtocol.mixed),
};
inbound.tag = inbound.type + inbound.listen_port.ToString();
_coreConfig.inbounds.Add(inbound);
@@ -229,7 +229,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
tag = $"{EInboundProtocol.mixed}{port}",
listen = Global.Loopback,
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
type = nameof(EInboundProtocol.mixed),
});
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();

View File

@@ -23,7 +23,7 @@ public partial class CoreConfigSingboxService
}
// Process outbounds
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : [];
var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
foreach (var outbound in _coreConfig.outbounds)
{
if (outbound.type.ToLower() is "direct" or "block")
@@ -42,9 +42,9 @@ public partial class CoreConfigSingboxService
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
// Process endpoints
if (_coreConfig.endpoints != null && _coreConfig.endpoints.Count > 0)
if (_coreConfig.endpoints is { Count: > 0 })
{
var customEndpointsNode = fullConfigTemplateNode["endpoints"] is JsonArray endpoints ? endpoints : [];
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
foreach (var endpoint in _coreConfig.endpoints)
{
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
@@ -66,10 +66,6 @@ public partial class CoreConfigSingboxService
{
return;
}
if (!(context.IsTunEnabled || context.IsWindows))
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? [])
{
outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
@@ -83,6 +79,7 @@ public partial class CoreConfigSingboxService
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? [])
{
outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;

View File

@@ -26,7 +26,7 @@ public partial class CoreConfigSingboxService
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
if (rules?.LastOrDefault() is { OutboundTag: Global.DirectTag } lastRule)
{
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
@@ -82,7 +82,7 @@ public partial class CoreConfigSingboxService
if (simpleDnsItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts != null && systemHosts.Count > 0)
if (systemHosts is { Count: > 0 })
{
foreach (var host in systemHosts)
{
@@ -137,12 +137,12 @@ public partial class CoreConfigSingboxService
// fake ip
if (simpleDnsItem.FakeIP == true)
{
var fakeipRange = simpleDnsItem.FakeIPRange.IsNullOrEmpty() ? Global.FakeIPRanges.First() : simpleDnsItem.FakeIPRange;
var fakeip = new Server4Sbox
{
tag = Global.SingboxFakeDNSTag,
type = "fakeip",
inet4_range = "198.18.0.0/15",
inet6_range = "fc00::/18",
inet4_range = fakeipRange,
};
_coreConfig.dns.servers.Add(fakeip);
}
@@ -171,7 +171,7 @@ public partial class CoreConfigSingboxService
_coreConfig.dns.rules.Add(new()
{
server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4ProxyDial),
domain = context.ProtectDomainList.ToList(),
});
}
@@ -182,13 +182,13 @@ public partial class CoreConfigSingboxService
{
server = Global.SingboxRemoteDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy),
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
},
new Rule4Sbox
{
server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
}
});
@@ -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));
fakeipFilterRule.invert = true;
@@ -454,12 +454,12 @@ public partial class CoreConfigSingboxService
dns4Sbox.rules.Insert(0, new()
{
server = tag,
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
});
dns4Sbox.rules.Insert(0, new()
{
server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote",
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
});
var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;

View File

@@ -15,8 +15,8 @@ public partial class CoreConfigSingboxService
{
var inbound = new Inbound4Sbox()
{
type = EInboundProtocol.mixed.ToString(),
tag = EInboundProtocol.socks.ToString(),
type = nameof(EInboundProtocol.mixed),
tag = nameof(EInboundProtocol.socks),
listen = Global.Loopback,
};
_coreConfig.inbounds.Add(inbound);
@@ -67,10 +67,15 @@ public partial class CoreConfigSingboxService
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute;
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;
_coreConfig.inbounds.Add(tunInbound);
}
@@ -86,7 +91,7 @@ public partial class CoreConfigSingboxService
var inbound = JsonUtils.DeepCopy(inItem);
inbound.tag = protocol.ToString();
inbound.listen_port = inItem.listen_port + (int)protocol;
inbound.type = EInboundProtocol.mixed.ToString();
inbound.type = nameof(EInboundProtocol.mixed);
return inbound;
}
}

View File

@@ -190,15 +190,16 @@ public partial class CoreConfigSingboxService
outbound.packet_encoding = "xudp";
if (!protocolExtra.Flow.IsNullOrEmpty())
if (protocolExtra.Flow is "xtls-rprx-vision" or "xtls-rprx-vision-udp443")
{
outbound.flow = "xtls-rprx-vision";
}
else if (!protocolExtra.Flow.IsNullOrEmpty())
{
outbound.flow = protocolExtra.Flow;
}
else
{
FillOutboundMux(outbound);
}
FillOutboundMux(outbound);
FillOutboundTransport(outbound);
break;
}
@@ -216,11 +217,17 @@ public partial class CoreConfigSingboxService
if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
{
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
outbound.obfs = new()
{
type = "salamander",
type = isGecko ? "gecko" : "salamander",
password = protocolExtra.SalamanderPass.TrimEx(),
};
if (isGecko)
{
outbound.obfs.min_packet_size = protocolExtra.GeckoMinPacketSize.ToInt();
outbound.obfs.max_packet_size = protocolExtra.GeckoMaxPacketSize.ToInt();
}
}
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
? su
@@ -263,6 +270,22 @@ public partial class CoreConfigSingboxService
}
}
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
&& realm is not null)
{
var realm4Sbox = new HyRealm4Sbox()
{
server_url = realm.ToServerUrl(),
token = realm.Token,
realm_id = realm.RealmName,
stun_servers = realm.StunList?.Count > 0 ? realm.StunList : null,
};
outbound.realm = realm4Sbox;
outbound.server = null;
outbound.server_port = null;
outbound.server_ports = null;
}
break;
}
case EConfigType.TUIC:
@@ -342,7 +365,7 @@ public partial class CoreConfigSingboxService
{
try
{
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
var muxEnabled = _node.MuxEnabled ?? false;
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
{
var mux = new Multiplex4Sbox()
@@ -394,11 +417,15 @@ public partial class CoreConfigSingboxService
var tls = new Tls4Sbox()
{
enabled = true,
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
server_name = serverName,
insecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure),
insecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(),
};
if (_config.CoreBasicItem.EnableFragment == true)
{
tls.fragment = true;
tls.record_fragment = true;
}
if (_node.Fingerprint.IsNotEmpty())
{
tls.utls = new Utls4Sbox()
@@ -729,8 +756,8 @@ public partial class CoreConfigSingboxService
{
return;
}
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList();
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList();
var outbounds = servers.OfType<Outbound4Sbox>().ToList();
var endpoints = servers.OfType<Endpoints4Sbox>().ToList();
singboxConfig.endpoints ??= [];
if (prepend)
{

View File

@@ -10,18 +10,27 @@ public partial class CoreConfigSingboxService
var simpleDnsItem = context.SimpleDnsItem;
var defaultDomainResolverTag = Global.SingboxDirectDNSTag;
var directDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom);
var dialDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4ProxyDial);
var rawDNSItem = context.RawDnsItem;
if (rawDNSItem is { Enabled: true })
{
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()
{
server = defaultDomainResolverTag,
strategy = directDnsStrategy
strategy = dialDnsStrategy
};
if (context.IsTunEnabled)
@@ -34,19 +43,22 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.AddRange(tunRules);
}
var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe();
_coreConfig.route.rules.Add(new()
var lstDirectExe = BuildRoutingDirectExe();
if (lstDirectExe.Count > 0)
{
port = [53],
action = "hijack-dns",
process_name = lstDnsExe
});
_coreConfig.route.rules.Add(new()
{
port = [53],
action = "hijack-dns",
process_path = lstDirectExe,
});
_coreConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
process_name = lstDirectExe
});
_coreConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
process_path = lstDirectExe,
});
}
// ICMP Routing
var icmpRouting = _config.TunModeItem.IcmpRouting ?? "";
@@ -96,6 +108,15 @@ public partial class CoreConfigSingboxService
new() { protocol = ["dns"] },
],
});
if (_config.CoreBasicItem.EnableFinalFragment)
{
_coreConfig.route.rules.Add(new()
{
protocol = ["tls"],
action = "route-options",
tls_record_fragment = true,
});
}
}
else
{
@@ -104,6 +125,14 @@ public partial class CoreConfigSingboxService
port = [53],
action = "hijack-dns",
});
if (_config.CoreBasicItem.EnableFinalFragment)
{
_coreConfig.route.rules.Add(new()
{
action = "route-options",
tls_record_fragment = true,
});
}
}
var hostsDomains = new List<string>();
@@ -176,12 +205,12 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
});
_coreConfig.route.rules.Add(new()
{
outbound = Global.ProxyTag,
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
});
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty();
@@ -239,34 +268,38 @@ public partial class CoreConfigSingboxService
}
}
private static (List<string> lstDnsExe, List<string> lstDirectExe) BuildRoutingDirectExe()
private List<string> BuildRoutingDirectExe()
{
var dnsExeSet = 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)
{
continue;
}
if (coreConfig.CoreExes == null)
{
continue;
}
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);
var lstDirectExe = new List<string>(directExeSet);
return (lstDnsExe, lstDirectExe);
return directExeSet.ToList();
}
private void GenRoutingUserRule(RulesItem? item)

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