Compare commits

...

91 Commits

Author SHA1 Message Date
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
https://github.com/2dust/v2rayN/commit/5fed566a324569da59252d8ee8179e22ff21a035
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
2dust ccb0ffb3b6 up 7.22.3 2026-05-21 17:25:50 +08:00
2dust 1fa1246c0b Bug fix
https://github.com/2dust/v2rayN/issues/9351
2026-05-21 14:15:07 +08:00
2dust 14cc37d07a Add 'New Update' notification flow
Introduce a small update-notification feature: add AppEvents.HasUpdateNotified event and have TaskManager publish it when update messages exist. Add localized resource key (menuNewUpdate) and expose it in the ResUI designer; update resource files for several languages. In the UI, add a New Update button in MainWindow.xaml, wire its click to the existing check-update handler, bind its visibility to a new BlNewUpdate property on MainWindowViewModel, and subscribe the viewmodel to the new event. Also reset the notification flag after showing the Check Update dialog.
2026-05-21 11:25:49 +08:00
2dust c7519f8ea7 Case-insensitive name checks and IpInfo visibility 2026-05-21 10:33:17 +08:00
2dust 0c796a157b Add IP info & flag emoji to test,add ip info column for main window
Fetch and display tested server IP and country (with emoji) in speed tests. Adds CountryExtension for country->emoji mapping and a new IpInfoResult type; ConnectionHandler now retrieves/parses IP API results and GetRealPingTime signature adjusted. Models and entities (ProfileItemModel, ProfileExItem, SpeedTestResult) gain IpInfo fields; ProfileExManager can store test IP info. UI/UX updated: new IpInfo column in ProfilesView (desktop and Avalonia), ResUI resource strings for "IP Info", and EServerColName ordering supports IpInfo. SpeedtestService now captures IP info and forwards it to the view model via the update function.
2026-05-20 19:16:58 +08:00
2dust a9824fe6ec Improve ConnectionHandler DownloadService 2026-05-20 15:42:47 +08:00
2dust f18758d4bf Support socks4/5 URIs and improve userinfo parsing 2026-05-20 14:28:01 +08:00
JieXu c1a009a409 bump dependencies (#9341)
* Update build-linux.yml

* Update Directory.Packages.props

* Update Directory.Packages.props
2026-05-20 09:01:22 +08:00
148 changed files with 7158 additions and 1768 deletions
-1
View File
@@ -100,7 +100,6 @@ csharp_style_prefer_tuple_swap = true:warning
csharp_style_prefer_utf8_string_literals = true:warning csharp_style_prefer_utf8_string_literals = true:warning
csharp_style_throw_expression = true:warning csharp_style_throw_expression = true:warning
csharp_style_unused_value_assignment_preference = discard_variable: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_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_colon_in_constructor_initializer_experimental = true:warning
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning
+2 -2
View File
@@ -18,8 +18,8 @@ body:
- type: input - type: input
id: "os-version" id: "os-version"
attributes: attributes:
label: "操作系统和版本" label: "操作系统 / v2rayN / Core 版本"
description: "操作系统和版本" description: "操作系统 / v2rayN / Core 版本"
validations: validations:
required: true required: true
+23
View File
@@ -9,8 +9,17 @@ on:
permissions: permissions:
actions: write actions: write
contents: write
jobs: 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: update:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -70,3 +79,17 @@ jobs:
\"release_tag\": \"${{ inputs.release_tag }}\" \"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 }}\"
}
}"
+289 -246
View File
@@ -12,6 +12,7 @@ on:
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
build: build:
@@ -26,17 +27,14 @@ jobs:
with: with:
target: linux target: linux
release_tag: ${{ inputs.release_tag }} release_tag: ${{ inputs.release_tag }}
secrets: inherit
deb: deb:
name: build and release deb x64 & arm64 name: build and release deb x64 & arm64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || runs-on: ubuntu-26.04
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
container: debian:13 container: debian:13
env: env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps: steps:
- name: Prepare tools (Debian) - name: Prepare tools (Debian)
shell: bash shell: bash
@@ -44,124 +42,73 @@ jobs:
set -euo pipefail set -euo pipefail
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update apt-get update
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \ 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
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) - name: Checkout repo (for scripts)
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with: { submodules: 'recursive', fetch-depth: '0' }
submodules: 'recursive'
fetch-depth: '0'
- name: Ensure script permissions - name: Ensure script permissions
run: chmod 755 package-debian.sh run: chmod 755 package-debian.sh
- name: Package DEB (Debian-family) - name: Package DEB (Debian-family)
run: ./package-debian.sh "${RELEASE_TAG}" --arch all run: ./package-debian.sh "${RELEASE_TAG}" --arch all
- name: Collect DEBs into workspace - name: Collect DEBs into workspace
run: | run: |
mkdir -p "$GITHUB_WORKSPACE/dist/deb" mkdir -p "$GITHUB_WORKSPACE/dist/deb"
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" \ find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
-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_*_arm64.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
echo "==== Dist tree ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb" || true ls -R "$GITHUB_WORKSPACE/dist/deb" || true
- name: Upload DEB artifact
- name: Upload DEBs to release uses: actions/upload-artifact@v7
uses: svenstaro/upload-release-action@v2 with: { name: release-deb, path: "dist/deb/**/*.deb", if-no-files-found: error }
with:
file: dist/deb/**/*.deb
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true
rpm: rpm:
name: build and release rpm x64 & arm64 name: build and release rpm x64 & arm64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || runs-on: ubuntu-26.04
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
container: registry.access.redhat.com/ubi10/ubi container: registry.access.redhat.com/ubi10/ubi
env: env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps: steps:
- name: Prepare tools (Red Hat) - name: Prepare tools (Red Hat)
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
. /etc/os-release . /etc/os-release
EL_MAJOR="${VERSION_ID%%.*}" EL_MAJOR="${VERSION_ID%%.*}"
echo "EL_MAJOR=${EL_MAJOR}" echo "EL_MAJOR=${EL_MAJOR}"
dnf -y makecache || true dnf -y makecache || true
command -v curl >/dev/null || dnf -y install curl ca-certificates command -v curl >/dev/null || dnf -y install curl ca-certificates
ARCH="$(uname -m)" ARCH="$(uname -m)"
case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac
install_epel_from_dir() { install_epel_from_dir() {
local base="$1" rpm local base="$1" rpm
echo "Try: $base" echo "Try: $base"
rpm="$({ curl -fsSL "$base/Packages/" 2>/dev/null; curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'; } |
rpm="$( sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' | sort -V | tail -n1)" || true
{ [[ -n "$rpm" ]] || return 1
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" dnf -y install "$base/Packages/$rpm"
return 0
fi
return 1
} }
FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm" FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm"
echo "Try Fedora: $FEDORA"
if curl -fsSLI "$FEDORA" >/dev/null; then
dnf -y install "$FEDORA"
else
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os" 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" 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 \ echo "Try Fedora: $FEDORA"
rsync findutils tar gzip unzip which 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; }
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which
dnf repolist | grep -i epel || true dnf repolist | grep -i epel || true
- name: Checkout repo (for scripts) - name: Checkout repo (for scripts)
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with: { submodules: 'recursive', fetch-depth: '0' }
submodules: 'recursive'
fetch-depth: '0'
- name: Ensure script permissions - name: Ensure script permissions
run: chmod 755 package-rhel.sh run: chmod 755 package-rhel.sh
- name: Package RPM (RHEL-family) - name: Package RPM (RHEL-family)
run: ./package-rhel.sh "${RELEASE_TAG}" --arch all run: ./package-rhel.sh "${RELEASE_TAG}" --arch all
- name: Collect RPMs into workspace - name: Collect RPMs into workspace
run: | run: |
mkdir -p "$GITHUB_WORKSPACE/dist/rpm" 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 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 ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
- name: Upload RPM artifact
- name: Upload RPMs to release uses: actions/upload-artifact@v7
uses: svenstaro/upload-release-action@v2 with: { name: release-rpm, path: "dist/rpm/**/*.rpm", if-no-files-found: error }
with:
file: dist/rpm/**/*.rpm
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true
rpm-riscv64: rpm-riscv64:
name: build and release rpm riscv64 name: build and release rpm riscv64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04-riscv runs-on: ubuntu-24.04-riscv
container: rockylinux/rockylinux:10 container: rockylinux/rockylinux:10
env: env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps: steps:
- name: Prepare tools (Red Hat) - name: Prepare tools (Red Hat)
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
dnf -y makecache dnf -y makecache
dnf -y install \ dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which jq
sudo git rpm-build rpmdevtools dnf-plugins-core \
rsync findutils tar gzip unzip which jq
- name: Checkout repo (for scripts) - name: Checkout repo (for scripts)
shell: bash shell: bash
env: env: { GITHUB_TOKEN: "${{ github.token }}" }
GITHUB_TOKEN: ${{ github.token }}
run: | run: |
set -euo pipefail set -euo pipefail
rm -rf ./* rm -rf ./*
@@ -212,56 +146,27 @@ jobs:
git fetch --depth=1 origin "${GITHUB_SHA}" git fetch --depth=1 origin "${GITHUB_SHA}"
git checkout FETCH_HEAD git checkout FETCH_HEAD
git submodule update --init --recursive git submodule update --init --recursive
- name: Ensure script permissions - name: Ensure script permissions
run: chmod 755 package-rhel-riscv.sh run: chmod 755 package-rhel-riscv.sh
- name: Package RPM (RHEL-family) - name: Package RPM (RHEL-family)
run: ./package-rhel-riscv.sh "${RELEASE_TAG}" run: ./package-rhel-riscv.sh "${RELEASE_TAG}"
- name: Collect RPMs into workspace - name: Collect RPMs into workspace
run: | run: |
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64" mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64"
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" \ find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
-exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
echo "==== Dist tree ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true
- name: Upload RPM artifact
- name: Upload RPMs to release uses: actions/upload-artifact@v7
shell: bash with: { name: release-rpm-riscv64, path: "dist/rpm-riscv64/**/*.rpm", if-no-files-found: error }
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
deb-riscv64: deb-riscv64:
name: build and release deb riscv64 name: build and release deb riscv64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04-riscv runs-on: ubuntu-24.04-riscv
container: debian:13 container: debian:13
env: env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps: steps:
- name: Prepare tools (Debian) - name: Prepare tools (Debian)
shell: bash shell: bash
@@ -269,14 +174,10 @@ jobs:
set -euo pipefail set -euo pipefail
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update apt-get update
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \ 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
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) - name: Checkout repo (for scripts)
shell: bash shell: bash
env: env: { GITHUB_TOKEN: "${{ github.token }}" }
GITHUB_TOKEN: ${{ github.token }}
run: | run: |
set -euo pipefail set -euo pipefail
rm -rf ./* rm -rf ./*
@@ -286,130 +187,100 @@ jobs:
git fetch --depth=1 origin "${GITHUB_SHA}" git fetch --depth=1 origin "${GITHUB_SHA}"
git checkout FETCH_HEAD git checkout FETCH_HEAD
git submodule update --init --recursive git submodule update --init --recursive
- name: Ensure script permissions - name: Ensure script permissions
run: chmod 755 package-debian-riscv.sh run: chmod 755 package-debian-riscv.sh
- name: Package DEB (Debian-family) - name: Package DEB (Debian-family)
run: ./package-debian-riscv.sh "${RELEASE_TAG}" run: ./package-debian-riscv.sh "${RELEASE_TAG}"
- name: Collect DEBs into workspace - name: Collect DEBs into workspace
run: | run: |
mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64" mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64"
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" \ find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
echo "==== Dist tree ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true
- name: Upload DEB artifact
- name: Upload DEBs to release uses: actions/upload-artifact@v7
shell: bash with: { name: release-deb-riscv64, path: "dist/deb-riscv64/**/*.deb", if-no-files-found: error }
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
deb-loong64: deb-loong64:
name: build and release deb loong64 name: build and release deb loong64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || runs-on: ubuntu-26.04
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
env: env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/debian13-loong64.qcow2 QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/edk2-loongarch64-code.fd EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/edk2-loongarch64-vars.fd EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
QCOW2_IMAGE: debian13-loong64.qcow2 QCOW2_IMAGE: debian13-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.2 QEMU_VERSION: 10.2.4
steps: steps:
- name: Prepare host tools - name: Prepare host tools
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
sudo apt-get update 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 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
- name: Checkout repo virtiofsd --version || virtiofsd --help | head
uses: actions/checkout@v6 - name: Host clone repo
with: shell: bash
submodules: recursive env: { GITHUB_TOKEN: "${{ github.token }}" }
fetch-depth: 0 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 - name: Download QEMU prebuild
run: | run: |
set -euo pipefail set -euo pipefail
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz"
wget -O qemu-linux-x64.tar.gz \ tar -xzf qemu-linux-x64-v3.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
mkdir -p "$HOME/qemu-install" mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/"
rsync -a qemu-linux-x64/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version "$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware - name: Download loong64 qcow2 and EFI firmware
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL" wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL" wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL" wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE" qemu-img info "$QCOW2_IMAGE"
- name: Build loong64 DEB in VM through serial console - name: Build loong64 DEB in VM through serial console
shell: bash shell: bash
timeout-minutes: 180 timeout-minutes: 180
env: env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
RELEASE_TAG: ${{ env.RELEASE_TAG }}
run: | run: |
set -euo pipefail set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64" 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" -o source="$GITHUB_WORKSPACE" -o 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' expect <<'EOF'
log_user 1 log_user 1
set timeout -1 set timeout -1
set release_tag $env(RELEASE_TAG) set release_tag $env(RELEASE_TAG)
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64" set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu" set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
set workspace $env(GITHUB_WORKSPACE)
set repo $env(GITHUB_REPOSITORY)
set sha $env(GITHUB_SHA)
set qcow2 $env(QCOW2_IMAGE) set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE) set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS) set efi_vars $env(EFI_VARS)
proc wait_prompt {} { proc wait_prompt {} {
expect { expect {
-re "__CI_PROMPT__# " {} -re "__CI_PROMPT__# " {}
@@ -417,86 +288,258 @@ jobs:
eof { exit 1 } eof { exit 1 }
} }
} }
proc run_cmd {cmd} { proc run_cmd {cmd} {
send -- "$cmd\r" send -- "$cmd\r"
wait_prompt wait_prompt
} }
spawn $qemu_bin \ spawn $qemu_bin \
-L $qemu_rom_dir \ -L $qemu_rom_dir \
-machine virt \ -machine virt \
-accel tcg,thread=multi,tb-size=2048 \ -accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \ -cpu la464 \
-m 9216 \ -m 9216 \
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
-numa node,memdev=mem0 \
-smp 4 \ -smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \ -drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \ -drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \ -device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \ -drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \ -netdev user,id=net0 \
-device virtio-net-pci,netdev=net0 \ -device virtio-net-pci-non-transitional,netdev=net0 \
-virtfs local,path=$workspace,mount_tag=workspace,security_model=none,id=workspace \ -chardev socket,id=charfs0,path=$virtiofs_socket \
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
-display none \ -display none \
-serial stdio \ -serial stdio \
-monitor none -monitor none
expect -re "login:|debian login:" expect -re "login:|debian login:"
send -- "root\r" send -- "root\r"
expect -re "Password:|密码:|密码:" expect -re "Password:|密码:|密码:"
send -- "password\r" send -- "password\r"
expect { expect {
-re "# " {} -re "# " {}
timeout { exit 1 } timeout { exit 1 }
eof { exit 1 } eof { exit 1 }
} }
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r" send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt wait_prompt
run_cmd "mkdir -p /workspace" 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 "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 "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf" 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 "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 "rm -rf /root/v2rayN-src"
run_cmd "git clone --recursive https://github.com/$repo.git /root/v2rayN-src" run_cmd "rsync -a --info=progress2 /workspace/repo/ /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 "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" 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" 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 { expect {
-re "__BUILD_DONE__0" { -re "__BUILD_DONE__0" { send -- "poweroff\r" }
send -- "poweroff\r" default { exit 1 }
}
default {
exit 1
}
} }
EOF EOF
- name: Collect DEBs - name: Collect DEBs
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64" 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 ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64" 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 rpm-loong64:
uses: svenstaro/upload-release-action@v2 name: build and release rpm loong64
with: if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
file: dist/deb-loong64/**/*.deb runs-on: ubuntu-26.04
tag: ${{ env.RELEASE_TAG }} env:
file_glob: true RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
prerelease: true 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" -o source="$GITHUB_WORKSPACE" -o 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 }}" }
+23 -10
View File
@@ -12,6 +12,7 @@ on:
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
build: build:
@@ -26,6 +27,7 @@ jobs:
with: with:
target: macos target: macos
release_tag: ${{ inputs.release_tag }} release_tag: ${{ inputs.release_tag }}
secrets: inherit
dmg: dmg:
name: package and release macOS dmg name: package and release macOS dmg
@@ -45,7 +47,7 @@ jobs:
}} }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Restore build artifacts - name: Restore build artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v8
@@ -62,13 +64,24 @@ jobs:
- name: Package dmg - name: Package dmg
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }} run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
- name: Sleep for race condition between matrix jobs - name: Upload dmg artifact
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')" uses: actions/upload-artifact@v7
- name: Upload dmg to release
uses: svenstaro/upload-release-action@v2
with: with:
file: ${{ github.workspace }}/v2rayN*.dmg name: release-dmg-${{ matrix.arch }}
tag: ${{ inputs.release_tag }} path: v2rayN*.dmg
file_glob: true if-no-files-found: error
prerelease: true
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 }}
@@ -12,6 +12,7 @@ on:
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
build: build:
@@ -26,3 +27,4 @@ jobs:
with: with:
target: windows-desktop target: windows-desktop
release_tag: ${{ inputs.release_tag }} release_tag: ${{ inputs.release_tag }}
secrets: inherit
+247
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@v5.4.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 }}
+2
View File
@@ -12,6 +12,7 @@ on:
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
build: build:
@@ -27,3 +28,4 @@ jobs:
with: with:
target: windows target: windows
release_tag: ${{ inputs.release_tag }} release_tag: ${{ inputs.release_tag }}
secrets: inherit
+21 -2
View File
@@ -22,6 +22,7 @@ jobs:
case( case(
inputs.target == 'macos', 'macos-latest', inputs.target == 'macos', 'macos-latest',
inputs.target == 'linux', 'ubuntu-24.04', inputs.target == 'linux', 'ubuntu-24.04',
inputs.target == 'windows', 'windows-latest',
'ubuntu-latest' 'ubuntu-latest'
) )
}} }}
@@ -46,21 +47,39 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: 'recursive' submodules: 'recursive'
fetch-depth: '0' 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 - name: Setup .NET
uses: actions/setup-dotnet@v5.2.0 uses: actions/setup-dotnet@v5.4.0
with: with:
dotnet-version: '10.0.1xx' dotnet-version: '10.0.1xx'
- name: Build v2rayN - name: Build v2rayN
shell: bash
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
- name: Build AmazTool - name: Build AmazTool
shell: bash
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
+24 -10
View File
@@ -9,9 +9,13 @@ on:
target: # windows linux macos windows-desktop target: # windows linux macos windows-desktop
required: true required: true
type: string type: string
secrets:
GPG_PRIVATE_KEY:
required: true
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
package: package:
@@ -37,7 +41,7 @@ jobs:
}} }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Restore build artifacts - name: Restore build artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v8
@@ -55,13 +59,23 @@ jobs:
if: inputs.target == 'windows-desktop' if: inputs.target == 'windows-desktop'
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip" run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
- name: Sleep for race condition between matrix jobs - name: Upload zip artifact
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')" uses: actions/upload-artifact@v7
- name: Upload zip archive to release
uses: svenstaro/upload-release-action@v2
with: with:
file: ${{ github.workspace }}/v2rayN*.zip name: release-${{ inputs.target }}-${{ matrix.arch }}
tag: ${{ inputs.release_tag }} path: v2rayN*.zip
file_glob: true if-no-files-found: error
prerelease: true
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
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
+2 -2
View File
@@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: 'recursive' submodules: 'recursive'
fetch-depth: '0' fetch-depth: '0'
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v5.2.0 uses: actions/setup-dotnet@v5.4.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
+76
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
+3
View File
@@ -84,6 +84,9 @@ StyleCopReport.xml
*.rsp *.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults # but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp !Directory.Build.rsp
bin/
obj/
build/
*.sbr *.sbr
*.tlb *.tlb
*.tli *.tli
+70 -10
View File
@@ -1,18 +1,78 @@
# v2rayN # v2rayN
A GUI client for Windows, Linux and macOS, support [Xray](https://github.com/XTLS/Xray-core) ### 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)
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) [![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) [![Release](https://img.shields.io/github/v/release/2dust/v2rayN?logo=github&label=Release)](https://github.com/2dust/v2rayN/releases)
[![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/v2rayn) [![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)
## How to use [![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)
Read the [Wiki](https://github.com/2dust/v2rayN/wiki) for details.
## Telegram Channel ---
[github_2dust](https://t.me/github_2dust) ## Download / 下载
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)
+2 -2
View File
@@ -13,8 +13,8 @@ PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0" DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.108" DOTNET_LOONGARCH_VERSION="10.0.109"
DOTNET_LOONGARCH_TAG="v10.0.108-loongarch64" DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
+1 -1
View File
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_RISCV_VERSION="10.0.108" DOTNET_RISCV_VERSION="10.0.109"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
+11 -1
View File
@@ -22,7 +22,17 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <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> <key>CFBundleDisplayName</key>
<string>v2rayN</string> <string>v2rayN</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
+709
View File
@@ -0,0 +1,709 @@
#!/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="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_LOONGARCH_VERSION="10.0.109"
DOTNET_LOONGARCH_TAG="v10.0.109-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"
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=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.34
Requires: fontconfig >= 2.13.1
Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32
Requires: bash >= 5.1
Requires: freetype >= 2.10
%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 "$@"
+2 -2
View File
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild" RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_RISCV_VERSION="10.0.108" DOTNET_RISCV_VERSION="10.0.109"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -501,7 +501,7 @@ write_spec_file() {
Name: v2rayN Name: v2rayN
Version: __VERSION__ Version: __VERSION__
Release: 1%{?dist} Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (riscv64) Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues BugURL: https://github.com/2dust/v2rayN/issues
+1 -1
View File
@@ -490,7 +490,7 @@ write_spec_file() {
Name: v2rayN Name: v2rayN
Version: __VERSION__ Version: __VERSION__
Release: 1%{?dist} 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 License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues BugURL: https://github.com/2dust/v2rayN/issues
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.22.2</Version> <Version>7.23.2</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
+12 -12
View File
@@ -7,33 +7,33 @@
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.15" /> <PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" /> <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.18" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" /> <PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="CliWrap" Version="3.10.1" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" />
<PackageVersion Include="Downloader" Version="5.5.0" /> <PackageVersion Include="CliWrap" Version="3.10.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" /> <PackageVersion Include="Downloader" Version="5.9.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" /> <PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.27" /> <PackageVersion Include="ReactiveUI" Version="23.2.28" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.27" /> <PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" /> <PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.3" /> <PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" /> <PackageVersion Include="sqlite-net-e" Version="1.11.0" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.1.4" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <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="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" /> <PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="17.1.0" /> <PackageVersion Include="YamlDotNet" Version="18.0.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" /> <PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4-preview.1.1" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -17,7 +17,7 @@ internal static class CoreConfigTestFactory
{ {
return new Config return new Config
{ {
CoreBasicItem = new CoreBasicItem { Loglevel = "warning", MuxEnabled = false }, CoreBasicItem = new CoreBasicItem { Loglevel = "warning" },
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" }, TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
KcpItem = new KcpItem(), KcpItem = new KcpItem(),
GrpcItem = new GrpcItem(), GrpcItem = new GrpcItem(),
@@ -187,7 +187,7 @@ internal static class CoreConfigTestFactory
SimpleDnsItem = config.SimpleDNSItem, SimpleDnsItem = config.SimpleDNSItem,
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node }, AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
FullConfigTemplate = null, FullConfigTemplate = null,
IsTunEnabled = false, IsTunEnabled = config.TunModeItem.EnableTun,
ProtectDomainList = [], ProtectDomainList = [],
}; };
} }
@@ -206,4 +206,12 @@ internal static class CoreConfigTestFactory
config.SimpleDNSItem.BootstrapDNS = bootstrapDns; config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
return config; 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;
}
} }
@@ -1,8 +1,10 @@
using AwesomeAssertions; using AwesomeAssertions;
using ServiceLib.Common; using ServiceLib.Common;
using ServiceLib.Enums; using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager; using ServiceLib.Manager;
using ServiceLib.Models; using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig; using ServiceLib.Services.CoreConfig;
using Xunit; using Xunit;
@@ -554,7 +556,40 @@ 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 == "remote" && s.type == "udp" && s.server == "8.8.8.8");
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag); 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 == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Direct.ToString()); 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();
} }
} }
@@ -536,4 +536,27 @@ public class CoreConfigV2rayServiceTests
directOutbound.Should().NotBeNull(); directOutbound.Should().NotBeNull();
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4"); directOutbound!.settings.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");
}
} }
@@ -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");
}
}
@@ -0,0 +1,92 @@
namespace ServiceLib.Common;
/// <summary>
/// Extension methods for country code utilities
/// </summary>
public static class CountryExtension
{
/// <summary>
/// Country code to emoji flag mapping for common countries
/// </summary>
private static readonly Dictionary<string, string> CountryEmojiMap = new(StringComparer.OrdinalIgnoreCase)
{
// Asia
{ "CN", "🇨🇳" }, // China
{ "HK", "🇭🇰" }, // Hong Kong
{ "TW", "🇹🇼" }, // Taiwan
{ "JP", "🇯🇵" }, // Japan
{ "SG", "🇸🇬" }, // Singapore
{ "KR", "🇰🇷" }, // South Korea
{ "TH", "🇹🇭" }, // Thailand
{ "VN", "🇻🇳" }, // Vietnam
{ "ID", "🇮🇩" }, // Indonesia
{ "PH", "🇵🇭" }, // Philippines
{ "MY", "🇲🇾" }, // Malaysia
{ "IN", "🇮🇳" }, // India
{ "PK", "🇵🇰" }, // Pakistan
{ "BD", "🇧🇩" }, // Bangladesh
{ "LK", "🇱🇰" }, // Sri Lanka
{ "KH", "🇰🇭" }, // Cambodia
{ "LA", "🇱🇦" }, // Laos
{ "MM", "🇲🇲" }, // Myanmar
// Americas
{ "US", "🇺🇸" }, // United States
{ "CA", "🇨🇦" }, // Canada
{ "MX", "🇲🇽" }, // Mexico
{ "BR", "🇧🇷" }, // Brazil
{ "AR", "🇦🇷" }, // Argentina
{ "CL", "🇨🇱" }, // Chile
{ "CO", "🇨🇴" }, // Colombia
// Europe
{ "GB", "🇬🇧" }, // United Kingdom
{ "DE", "🇩🇪" }, // Germany
{ "FR", "🇫🇷" }, // France
{ "IT", "🇮🇹" }, // Italy
{ "ES", "🇪🇸" }, // Spain
{ "RU", "🇷🇺" }, // Russia
{ "NL", "🇳🇱" }, // Netherlands
{ "CH", "🇨🇭" }, // Switzerland
{ "SE", "🇸🇪" }, // Sweden
{ "NO", "🇳🇴" }, // Norway
{ "DK", "🇩🇰" }, // Denmark
{ "FI", "🇫🇮" }, // Finland
{ "PL", "🇵🇱" }, // Poland
{ "CZ", "🇨🇿" }, // Czech Republic
{ "AT", "🇦🇹" }, // Austria
{ "GR", "🇬🇷" }, // Greece
{ "PT", "🇵🇹" }, // Portugal
{ "TR", "🇹🇷" }, // Turkey
{ "UA", "🇺🇦" }, // Ukraine
{ "RO", "🇷🇴" }, // Romania
// Middle East & Central Asia
{ "AE", "🇦🇪" }, // United Arab Emirates
{ "SA", "🇸🇦" }, // Saudi Arabia
{ "IL", "🇮🇱" }, // Israel
{ "KZ", "🇰🇿" }, // Kazakhstan
// Oceania
{ "AU", "🇦🇺" }, // Australia
{ "NZ", "🇳🇿" }, // New Zealand
// Africa
{ "ZA", "🇿🇦" }, // South Africa
{ "EG", "🇪🇬" }, // Egypt
};
/// <summary>
/// Converts country code to flag emoji using predefined mapping
/// Example: "US" -> "🇺🇸", "CN" -> "🇨🇳"
/// </summary>
public static string? CountryToEmoji(this string? countryCode)
{
if (countryCode.IsNullOrEmpty())
{
return null;
}
return CountryEmojiMap.GetValueOrDefault(countryCode);
}
}
+17 -1
View File
@@ -47,7 +47,7 @@ public static class Extension
public static string TrimEx(this string? value) 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) public static string RemovePrefix(this string value, char prefix)
@@ -118,4 +118,20 @@ public static class Extension
destination.Add(item); 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);
}
} }
+5 -1
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 try
{ {
@@ -214,6 +214,10 @@ public static class FileUtils
{ {
continue; continue;
} }
if (contains.IsNotEmpty() && !file.Name.Contains(contains))
{
continue;
}
file.Delete(); file.Delete();
} }
} }
+98 -5
View File
@@ -51,7 +51,7 @@ public class Utils
try try
{ {
str = str.Replace(Environment.NewLine, string.Empty); str = str.ReplaceLineBreaks(string.Empty);
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries)); return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
} }
catch (Exception ex) catch (Exception ex)
@@ -114,9 +114,7 @@ public class Utils
} }
plainText = plainText.Trim() plainText = plainText.Trim()
.Replace(Environment.NewLine, "") .ReplaceLineBreaks("")
.Replace("\n", "")
.Replace("\r", "")
.Replace('_', '/') .Replace('_', '/')
.Replace('-', '+') .Replace('-', '+')
.Replace(" ", ""); .Replace(" ", "");
@@ -321,7 +319,32 @@ public class Utils
return text; 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 public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
@@ -576,6 +599,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) public static bool IsPrivateNetwork(string ip)
{ {
if (IPAddress.TryParse(ip, out var address)) if (IPAddress.TryParse(ip, out var address))
@@ -716,6 +791,24 @@ public class Utils
return (endpoints, connections); 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 #endregion Speed Test
#region Miscellaneous #region Miscellaneous
@@ -12,6 +12,7 @@ public enum EServerColName
SubRemarks, SubRemarks,
DelayVal, DelayVal,
SpeedVal, SpeedVal,
IpInfo,
TodayDown, TodayDown,
TodayUp, TodayUp,
+1
View File
@@ -7,6 +7,7 @@ public static class AppEvents
public static readonly EventChannel<Unit> AddServerViaScanRequested = new(); public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new(); public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = 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> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new(); public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
+47 -19
View File
@@ -55,7 +55,7 @@ public class Global
public const string DnsOutboundTag = "dns"; public const string DnsOutboundTag = "dns";
public const string DnsTag = "dns-module"; public const string DnsTag = "dns-module";
public const string DirectDnsTag = "direct-dns"; public const string DirectDnsTag = "direct-dns";
public const string BalancerTagSuffix = "-round"; public const string BalancerTagSuffix = "-balancer";
public const string StreamSecurity = "tls"; public const string StreamSecurity = "tls";
public const string StreamSecurityReality = "reality"; public const string StreamSecurityReality = "reality";
public const string Loopback = "127.0.0.1"; public const string Loopback = "127.0.0.1";
@@ -90,6 +90,9 @@ public class Global
public const string XrayLocalCert = "XRAY_LOCATION_CERT"; public const string XrayLocalCert = "XRAY_LOCATION_CERT";
public const int SpeedTestPageSize = 1000; public const int SpeedTestPageSize = 1000;
public const string LinuxBash = "/bin/bash"; 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 SingboxDirectDNSTag = "direct_dns";
public const string SingboxRemoteDNSTag = "remote_dns"; public const string SingboxRemoteDNSTag = "remote_dns";
@@ -149,6 +152,9 @@ public class Global
public static readonly List<string> SpeedTestUrls = public static readonly List<string> SpeedTestUrls =
[ [
@"https://cachefly.cachefly.net/50mb.test", @"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=10000000",
@"https://speed.cloudflare.com/__down?bytes=50000000", @"https://speed.cloudflare.com/__down?bytes=50000000",
@"https://speed.cloudflare.com/__down?bytes=99999999", @"https://speed.cloudflare.com/__down?bytes=99999999",
@@ -157,6 +163,8 @@ public class Global
public static readonly List<string> SpeedPingTestUrls = public static readonly List<string> SpeedPingTestUrls =
[ [
@"https://www.google.com/generate_204", @"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.gstatic.com/generate_204",
@"https://www.apple.com/library/test/success.html", @"https://www.apple.com/library/test/success.html",
@"http://www.msftconnecttest.com/connecttest.txt" @"http://www.msftconnecttest.com/connecttest.txt"
@@ -203,10 +211,18 @@ public class Global
public const string Hysteria2ProtocolShare = "hy2://"; 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 NaiveHttpsProtocolShare = "naive+https://";
public const string NaiveQuicProtocolShare = "naive+quic://"; public const string NaiveQuicProtocolShare = "naive+quic://";
public const string SOCKS5Protocol = "socks5://";
public const string SOCKS4Protocol = "socks4://";
public static readonly Dictionary<EConfigType, string> ProtocolShares = new() public static readonly Dictionary<EConfigType, string> ProtocolShares = new()
{ {
{ EConfigType.VMess, "vmess://" }, { EConfigType.VMess, "vmess://" },
@@ -321,12 +337,12 @@ public class Global
public static readonly Dictionary<string, string> KcpHeaderMaskMap = new() public static readonly Dictionary<string, string> KcpHeaderMaskMap = new()
{ {
{ "srtp", "header-srtp" }, { "srtp", "srtp" },
{ "utp", "header-utp" }, { "utp", "utp" },
{ "wechat-video", "header-wechat" }, { "wechat-video", "wechat" },
{ "dtls", "header-dtls" }, { "dtls", "dtls" },
{ "wireguard", "header-wireguard" }, { "wireguard", "wireguard" },
{ "dns", "header-dns" } { "dns", "dns" }
}; };
public static readonly List<string> CoreTypes = public static readonly List<string> CoreTypes =
@@ -395,6 +411,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 = public static readonly List<string> UserAgent =
[ [
"chrome", "chrome",
@@ -412,13 +438,6 @@ public class Global
"stream-one" "stream-one"
]; ];
public static readonly List<string> AllowInsecure =
[
"true",
"false",
""
];
public static readonly List<string> DomainStrategy = public static readonly List<string> DomainStrategy =
[ [
"AsIs", "AsIs",
@@ -469,10 +488,11 @@ public class Global
"zh-Hans", "zh-Hans",
"zh-Hant", "zh-Hant",
"en", "en",
"fa-Ir", "fa",
"fr", "fr",
"ru", "ru",
"hu" "hu",
"id"
]; ];
public static readonly List<string> Alpns = public static readonly List<string> Alpns =
@@ -662,6 +682,14 @@ public class Global
"mcbe:bedrock.talonmc.net", "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 = public static readonly List<string> OutboundTags =
[ [
ProxyTag, ProxyTag,
@@ -676,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"] }, { "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"] }, { "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"] }, { "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"] }, { "dot.pub", ["1.12.12.12", "120.53.53.53"] },
{ "doh.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.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.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.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"] }, { "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 = public static readonly List<string> ExpectedIPs =
@@ -111,7 +111,8 @@ public static class AutoStartupHandler
task.Settings.RunOnlyIfIdle = false; task.Settings.RunOnlyIfIdle = false;
task.Settings.IdleSettings.StopOnIdleEnd = false; task.Settings.IdleSettings.StopOnIdleEnd = false;
task.Settings.ExecutionTimeLimit = TimeSpan.Zero; 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.Principal.RunLevel = Microsoft.Win32.TaskScheduler.TaskRunLevel.Highest;
task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName))); task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName)));
@@ -35,7 +35,7 @@ public class CoreConfigContextBuilder
{ {
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray; var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
var context = new CoreConfigContext() var context = new CoreConfigContext
{ {
Node = node, Node = node,
RunCoreType = runCoreType, RunCoreType = runCoreType,
@@ -61,7 +61,8 @@ public class CoreConfigContextBuilder
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true)) if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
{ {
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? []; 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()) if (ruleItem.OutboundTag.IsNullOrEmpty())
{ {
@@ -72,7 +73,8 @@ public class CoreConfigContextBuilder
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag); var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
if (ruleOutboundNode == null) 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; ruleItem.OutboundTag = Global.ProxyTag;
continue; continue;
} }
@@ -83,7 +85,8 @@ public class CoreConfigContextBuilder
if (!ruleNodeValidatorResult.Success) if (!ruleNodeValidatorResult.Success)
{ {
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e => 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; ruleItem.OutboundTag = Global.ProxyTag;
continue; continue;
} }
@@ -91,6 +94,42 @@ public class CoreConfigContextBuilder
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode; context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
} }
} }
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{
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); return new CoreConfigContextBuilderResult(context, validatorResult);
} }
@@ -118,10 +157,25 @@ public class CoreConfigContextBuilder
{ {
Context = mainResult.Context with 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], 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); return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult);
} }
@@ -143,8 +197,9 @@ public class CoreConfigContextBuilder
{ {
Context = preSocksResult.Context with Context = preSocksResult.Context with
{ {
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []], ProtectDomainList =
} [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
},
}; };
} }
@@ -193,7 +248,8 @@ public class CoreConfigContextBuilder
/// Returns <c>null</c> as the chain item when no chain is needed. /// 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. /// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
/// </summary> /// </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(); var result = NodeValidatorResult.Empty();
@@ -234,7 +290,7 @@ public class CoreConfigContextBuilder
} }
// Build new proxy chain node // Build new proxy chain node
var chainNode = new ProfileItem() var chainNode = new ProfileItem
{ {
IndexId = $"inner-{Utils.GetGuid(false)}", IndexId = $"inner-{Utils.GetGuid(false)}",
ConfigType = EConfigType.ProxyChain, ConfigType = EConfigType.ProxyChain,
@@ -261,11 +317,8 @@ public class CoreConfigContextBuilder
{ {
return await RegisterGroupNodeAsync(context, node); return await RegisterGroupNodeAsync(context, node);
} }
else
{
return RegisterSingleNodeAsync(context, node); return RegisterSingleNodeAsync(context, node);
} }
}
/// <summary> /// <summary>
/// Validates a single (non-group) node and, on success, adds it to the proxy map /// Validates a single (non-group) node and, on success, adds it to the proxy map
@@ -416,13 +469,10 @@ public class CoreConfigContextBuilder
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks)); childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
return childNodeValidatorResult; return childNodeValidatorResult;
} }
else
{
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors); childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
childNodeValidatorResult.Errors.Clear(); 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; context.AllProxiesMap[node.IndexId] = node;
return childNodeValidatorResult; return childNodeValidatorResult;
} }
@@ -29,35 +29,6 @@ public class NodeValidator
return v.ToResult(); 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) private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v)
{ {
if (item.ConfigType is EConfigType.Custom) if (item.ConfigType is EConfigType.Custom)
@@ -72,8 +43,8 @@ public class NodeValidator
} }
// Basic Property Validation // Basic Property Validation
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address")); v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbAddress));
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port")); v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, ResUI.TbPort));
// Network & Core Logic // Network & Core Logic
var net = item.GetNetwork(); var net = item.GetNetwork();
@@ -104,41 +75,76 @@ public class NodeValidator
{ {
case EConfigType.VMess: case EConfigType.VMess:
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password), v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
string.Format(ResUI.MsgInvalidProperty, "Password")); string.Format(ResUI.MsgInvalidProperty, ResUI.TbId));
break; break;
case EConfigType.VLESS: case EConfigType.VLESS:
v.Assert( v.Assert(
!item.Password.IsNullOrEmpty() !item.Password.IsNullOrEmpty()
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30), && (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), v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
string.Format(ResUI.MsgInvalidProperty, "Flow")); string.Format(ResUI.MsgInvalidProperty, ResUI.TbFlow5));
break; break;
case EConfigType.Shadowsocks: 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( v.Assert(
!string.IsNullOrEmpty(protocolExtra.SsMethod) && !string.IsNullOrEmpty(protocolExtra.SsMethod) &&
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod), Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
string.Format(ResUI.MsgInvalidProperty, "SsMethod")); string.Format(ResUI.MsgInvalidProperty, ResUI.TbSecurity3));
break; 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 // TLS & Security
if (item.StreamSecurity == Global.StreamSecurity) if (item.StreamSecurity == Global.StreamSecurity)
{ {
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 && var isCertProvided = !item.Cert.IsNullOrEmpty();
!item.CertSha.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) 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(); var transport = item.GetTransportExtra();
@@ -146,7 +152,7 @@ public class NodeValidator
{ {
if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject) 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) 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; 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);
}
}
} }
+60 -22
View File
@@ -39,15 +39,14 @@ public static class ConfigHandler
{ {
LogEnabled = false, LogEnabled = false,
Loglevel = "warning", Loglevel = "warning",
MuxEnabled = false,
}; };
if (config.Inbound == null) if (config.Inbound == null)
{ {
config.Inbound = new List<InItem>(); config.Inbound = [];
InItem inItem = new() InItem inItem = new()
{ {
Protocol = EInboundProtocol.socks.ToString(), Protocol = nameof(EInboundProtocol.socks),
LocalPort = 10808, LocalPort = 10808,
UdpEnabled = true, UdpEnabled = true,
SniffingEnabled = true, SniffingEnabled = true,
@@ -60,7 +59,7 @@ public static class ConfigHandler
{ {
if (config.Inbound.Count > 0) if (config.Inbound.Count > 0)
{ {
config.Inbound.First().Protocol = EInboundProtocol.socks.ToString(); config.Inbound.First().Protocol = nameof(EInboundProtocol.socks);
} }
} }
@@ -99,8 +98,8 @@ public static class ConfigHandler
config.MsgUIItem ??= new(); config.MsgUIItem ??= new();
config.UiItem ??= new(); config.UiItem ??= new();
config.UiItem.MainColumnItem ??= new(); config.UiItem.MainColumnItem ??= [];
config.UiItem.WindowSizeItem ??= new(); config.UiItem.WindowSizeItem ??= [];
if (config.UiItem.CurrentLanguage.IsNullOrEmpty()) if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
{ {
@@ -158,7 +157,7 @@ public static class ConfigHandler
DownMbps = 100 DownMbps = 100
}; };
config.ClashUIItem ??= new(); config.ClashUIItem ??= new();
config.ClashUIItem.ConnectionsColumnItem ??= new(); config.ClashUIItem.ConnectionsColumnItem ??= [];
config.SystemProxyItem ??= new(); config.SystemProxyItem ??= new();
config.WebDavItem ??= new(); config.WebDavItem ??= new();
config.CheckUpdateItem ??= new(); config.CheckUpdateItem ??= new();
@@ -166,9 +165,10 @@ public static class ConfigHandler
{ {
Packets = "tlshello", Packets = "tlshello",
Length = "50-100", Length = "50-100",
Interval = "10-20" Interval = "10-20",
MaxSplit = "0"
}; };
config.GlobalHotkeys ??= new(); config.GlobalHotkeys ??= [];
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty()) if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
{ {
@@ -255,6 +255,7 @@ public static class ConfigHandler
item.Cert = profileItem.Cert; item.Cert = profileItem.Cert;
item.CertSha = profileItem.CertSha; item.CertSha = profileItem.CertSha;
item.EchConfigList = profileItem.EchConfigList; item.EchConfigList = profileItem.EchConfigList;
item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName;
item.Finalmask = profileItem.Finalmask; item.Finalmask = profileItem.Finalmask;
item.ProtoExtra = profileItem.ProtoExtra; item.ProtoExtra = profileItem.ProtoExtra;
item.TransportExtra = profileItem.TransportExtra; item.TransportExtra = profileItem.TransportExtra;
@@ -720,11 +721,50 @@ public static class ConfigHandler
{ {
return -1; return -1;
} }
var protocolExtra = profileItem.GetProtocolExtra();
profileItem.SetProtocolExtra(protocolExtra with
{
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 profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{ {
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(), Hy2RealmUrl = realm.ToUri(),
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(),
}); });
}
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); await AddServerCommon(config, profileItem, toFile);
@@ -874,7 +914,7 @@ public static class ConfigHandler
profileItem.Fingerprint = string.Empty; profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty; profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty; profileItem.Network = string.Empty;
profileItem.AllowInsecure = "false"; profileItem.AllowInsecure = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty()) if (profileItem.StreamSecurity.IsNullOrEmpty())
{ {
profileItem.StreamSecurity = Global.StreamSecurity; profileItem.StreamSecurity = Global.StreamSecurity;
@@ -923,6 +963,7 @@ public static class ConfigHandler
Delay = t33?.Delay ?? 0, Delay = t33?.Delay ?? 0,
Speed = t33?.Speed ?? 0, Speed = t33?.Speed ?? 0,
Sort = t33?.Sort ?? 0, Sort = t33?.Sort ?? 0,
IpInfo = t33?.IpInfo ?? string.Empty,
TodayDown = (t22?.TodayDown ?? 0).ToString("D16"), TodayDown = (t22?.TodayDown ?? 0).ToString("D16"),
TodayUp = (t22?.TodayUp ?? 0).ToString("D16"), TodayUp = (t22?.TodayUp ?? 0).ToString("D16"),
TotalDown = (t22?.TotalDown ?? 0).ToString("D16"), TotalDown = (t22?.TotalDown ?? 0).ToString("D16"),
@@ -943,6 +984,7 @@ public static class ConfigHandler
EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(), EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(),
EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(), EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(),
EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).ToList(), EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).ToList(),
EServerColName.IpInfo => lstProfile.OrderBy(t => t.IpInfo).ToList(),
EServerColName.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(), EServerColName.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(),
EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(), EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(),
EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(), EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(),
@@ -963,6 +1005,7 @@ public static class ConfigHandler
EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(), EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(),
EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(), EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(),
EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).ToList(), EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).ToList(),
EServerColName.IpInfo => lstProfile.OrderByDescending(t => t.IpInfo).ToList(),
EServerColName.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(), EServerColName.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(),
EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(), EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(),
EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(), EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(),
@@ -1053,8 +1096,8 @@ public static class ConfigHandler
return new Tuple<int, int>(0, 0); return new Tuple<int, int>(0, 0);
} }
List<ProfileItem> lstKeep = new(); List<ProfileItem> lstKeep = [];
List<ProfileItem> lstRemove = new(); List<ProfileItem> lstRemove = [];
if (!config.GuiItem.KeepOlderDedupl) if (!config.GuiItem.KeepOlderDedupl)
{ {
lstProfile.Reverse(); lstProfile.Reverse();
@@ -1103,10 +1146,6 @@ public static class ConfigHandler
} }
else else
{ {
if (profileItem.AllowInsecure.IsNullOrEmpty())
{
profileItem.AllowInsecure = config.CoreBasicItem.DefAllowInsecure.ToString().ToLower();
}
if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality) if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality)
{ {
profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint; profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint;
@@ -1451,8 +1490,7 @@ public static class ConfigHandler
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType) public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
{ {
ProfileItem? itemSocks = null; ProfileItem? itemSocks = null;
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect;
|| Utils.IsNonWindows();
if (node.ConfigType != EConfigType.Custom if (node.ConfigType != EConfigType.Custom
&& coreType != ECoreType.sing_box && coreType != ECoreType.sing_box
&& config.TunModeItem.EnableTun && config.TunModeItem.EnableTun
@@ -1534,7 +1572,7 @@ public static class ConfigHandler
} }
var countServers = 0; var countServers = 0;
List<ProfileItem> lstAdd = new(); List<ProfileItem> lstAdd = [];
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty()); var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
if (isSub) if (isSub)
{ {
@@ -1630,7 +1668,7 @@ public static class ConfigHandler
{ {
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks); lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
} }
if (lstProfiles != null && lstProfiles.Count > 0) if (lstProfiles is { Count: > 0 })
{ {
var count = 0; var count = 0;
foreach (var it in lstProfiles) foreach (var it in lstProfiles)
+68 -30
View File
@@ -4,53 +4,41 @@ public static class ConnectionHandler
{ {
private static readonly string _tag = "ConnectionHandler"; private static readonly string _tag = "ConnectionHandler";
/// <summary>
/// Runs ping and IP checks and returns a formatted result string.
/// </summary>
public static async Task<string> RunAvailabilityCheck() public static async Task<string> RunAvailabilityCheck()
{ {
var time = await GetRealPingTimeInfo(); var time = await GetRealPingTimeInfo();
var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None; var ip = time > 0 ? await GetIPInfo() : Global.None;
return string.Format(ResUI.TestMeOutput, time, ip); return string.Format(ResUI.TestMeOutput, time, ip);
} }
/// <summary>
/// Gets IP information using the default local proxy.
/// </summary>
private static async Task<string?> GetIPInfo() private static async Task<string?> GetIPInfo()
{ {
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl; var webProxy = await GetWebProxy();
if (url.IsNullOrEmpty())
{
return null;
}
var downloadHandle = new DownloadService(); var ipInfo = await GetIPInfo(webProxy);
var result = await downloadHandle.TryDownloadString(url, true, ""); return ipInfo?.ToString() ?? Global.None;
if (result == null)
{
return null;
}
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(result);
if (ipInfo == null)
{
return null;
}
var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query;
var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code;
return $"({country ?? "unknown"}) {ip}";
} }
/// <summary>
/// Measures real ping time using configured test URL.
/// </summary>
private static async Task<int> GetRealPingTimeInfo() private static async Task<int> GetRealPingTimeInfo()
{ {
var responseTime = -1; var responseTime = -1;
try try
{ {
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); var webProxy = await GetWebProxy();
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}");
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
responseTime = await GetRealPingTime(url, webProxy, 10); responseTime = await GetRealPingTime(webProxy, 10);
if (responseTime > 0) if (responseTime > 0)
{ {
break; break;
@@ -66,8 +54,21 @@ public static class ConnectionHandler
return responseTime; return responseTime;
} }
public static async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout) /// <summary>
/// Creates local SOCKS proxy instance.
/// </summary>
private static async Task<WebProxy?> GetWebProxy()
{ {
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
return new WebProxy($"socks5://{Global.Loopback}:{port}");
}
/// <summary>
/// Measures response time by sending HTTP requests through proxy.
/// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout)
{
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1; var responseTime = -1;
try try
{ {
@@ -79,14 +80,14 @@ public static class ConnectionHandler
UseProxy = webProxy != null UseProxy = webProxy != null
}); });
List<int> oneTime = new(); List<int> oneTime = [];
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false); await client.GetAsync(url, cts.Token).ConfigureAwait(false);
timer.Stop(); timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds); 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(); responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
} }
@@ -95,4 +96,41 @@ public static class ConnectionHandler
} }
return responseTime; return responseTime;
} }
/// <summary>
/// Gets IP and country information through specified proxy.
/// </summary>
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy)
{
try
{
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
if (url.IsNullOrEmpty())
{
return null;
}
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, webProxy, "");
if (result == null)
{
return null;
}
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(result);
if (ipInfo == null)
{
return null;
}
var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query;
var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code ?? "unknown";
return new IpInfoResult(country, ip);
}
catch
{
return null;
}
}
} }
+8 -3
View File
@@ -73,6 +73,10 @@ public class BaseFmt
{ {
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList)); dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
} }
if (item.VerifyPeerCertByName.IsNotEmpty())
{
dicQuery.Add("vcn", Utils.UrlEncode(item.VerifyPeerCertByName));
}
if (item.CertSha.IsNotEmpty()) if (item.CertSha.IsNotEmpty())
{ {
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha)); 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) 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 // Add two for compatibility
dicQuery.Add("insecure", "1"); dicQuery.Add("insecure", "1");
@@ -227,6 +231,7 @@ public class BaseFmt
item.SpiderX = GetQueryDecoded(query, "spx"); item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.EchConfigList = GetQueryDecoded(query, "ech"); item.EchConfigList = GetQueryDecoded(query, "ech");
item.VerifyPeerCertByName = GetQueryDecoded(query, "vcn");
item.CertSha = GetQueryDecoded(query, "pcs"); item.CertSha = GetQueryDecoded(query, "pcs");
var finalmaskDecoded = GetQueryDecoded(query, "fm"); var finalmaskDecoded = GetQueryDecoded(query, "fm");
@@ -249,11 +254,11 @@ public class BaseFmt
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1")) 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")) else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
{ {
item.AllowInsecure = Global.AllowInsecure.Skip(1).First(); item.AllowInsecure = Global.StringFalse;
} }
else else
{ {
+10 -2
View File
@@ -53,7 +53,9 @@ public class FmtHandler
{ {
return ShadowsocksFmt.Resolve(str, out msg); return ShadowsocksFmt.Resolve(str, out msg);
} }
else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS])) else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS])
|| str.StartsWith(Global.SOCKS5Protocol)
|| str.StartsWith(Global.SOCKS4Protocol))
{ {
return SocksFmt.Resolve(str, out msg); return SocksFmt.Resolve(str, out msg);
} }
@@ -65,10 +67,16 @@ public class FmtHandler
{ {
return VLESSFmt.Resolve(str, out msg); return VLESSFmt.Resolve(str, out msg);
} }
else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2]) || str.StartsWith(Global.Hysteria2ProtocolShare)) else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2])
|| str.StartsWith(Global.Hysteria2ProtocolShare))
{ {
return Hysteria2Fmt.Resolve(str, out msg); 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])) else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC]))
{ {
return TuicFmt.Resolve(str, out msg); return TuicFmt.Resolve(str, out msg);
+160 -32
View File
@@ -1,3 +1,5 @@
using System.Collections.Specialized;
namespace ServiceLib.Handler.Fmt; namespace ServiceLib.Handler.Fmt;
public class Hysteria2Fmt : BaseFmt public class Hysteria2Fmt : BaseFmt
@@ -23,15 +25,7 @@ public class Hysteria2Fmt : BaseFmt
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
if (item.CertSha.IsNullOrEmpty()) ResolveHy2UriQuery(query, ref item);
{
item.CertSha = GetQueryDecoded(query, "pinSHA256");
}
item.SetProtocolExtra(item.GetProtocolExtra() with
{
Ports = GetQueryDecoded(query, "mport"),
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
});
return item; return item;
} }
@@ -42,8 +36,11 @@ public class Hysteria2Fmt : BaseFmt
{ {
return null; return null;
} }
var protocolExtraItem = item.GetProtocolExtra();
var url = string.Empty; if (!protocolExtraItem.Hy2RealmUrl.TrimEx().IsNullOrEmpty())
{
return ToUriForRealm(item);
}
var remark = string.Empty; var remark = string.Empty;
if (item.Remarks.IsNotEmpty()) if (item.Remarks.IsNotEmpty())
@@ -52,27 +49,7 @@ public class Hysteria2Fmt : BaseFmt
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
ToUriQueryLite(item, ref dicQuery); ToUriQueryLite(item, ref dicQuery);
var protocolExtraItem = item.GetProtocolExtra(); ToHy2UriQuery(item, ref dicQuery);
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));
}
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark); return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
} }
@@ -94,4 +71,155 @@ public class Hysteria2Fmt : BaseFmt
return null; 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(':', '-')));
}
}
} }
+1 -1
View File
@@ -185,7 +185,7 @@ public class InnerFmt
return null; return null;
} }
// Check Enum.IsDefined // Check Enum.IsDefined
if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType)) if (!Enum.IsDefined(profileItem.ConfigType))
{ {
return null; return null;
} }
+9 -4
View File
@@ -71,7 +71,7 @@ public class SocksFmt : BaseFmt
return null; return null;
} }
var arr21 = arr1.First().Split(':'); var arr21 = arr1.First().Split(':');
var indexPort = arr1.Last().LastIndexOf(":"); var indexPort = arr1.Last().LastIndexOf(':');
if (arr21.Length != 2 || indexPort < 0) if (arr21.Length != 2 || indexPort < 0)
{ {
return null; return null;
@@ -99,12 +99,17 @@ public class SocksFmt : BaseFmt
}; };
// parse base64 UserInfo // parse base64 UserInfo
var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo); var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo);
var userInfo = Utils.Base64Decode(rawUserInfo); if (rawUserInfo.IsNotEmpty())
var userInfoParts = userInfo.Split([':'], 2); {
var userInfoParts = rawUserInfo.Contains(':')
? rawUserInfo.Split(":", 2)
: Utils.Base64Decode(rawUserInfo).Split(":", 2);
if (userInfoParts.Length == 2) if (userInfoParts.Length == 2)
{ {
item.Username = userInfoParts.First(); item.Username = userInfoParts.First();
item.Password = userInfoParts[1]; item.Password = userInfoParts.Last();
}
} }
return item; return item;
+6 -2
View File
@@ -66,7 +66,9 @@ public class VmessFmt : BaseFmt
sni = item.Sni, sni = item.Sni,
alpn = item.Alpn, alpn = item.Alpn,
fp = item.Fingerprint, 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); var url = JsonUtils.Serialize(vmessQRCode);
@@ -140,7 +142,9 @@ public class VmessFmt : BaseFmt
item.Sni = Utils.ToString(vmessQRCode.sni); item.Sni = Utils.ToString(vmessQRCode.sni);
item.Alpn = Utils.ToString(vmessQRCode.alpn); item.Alpn = Utils.ToString(vmessQRCode.alpn);
item.Fingerprint = Utils.ToString(vmessQRCode.fp); 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; return item;
} }
@@ -133,12 +133,12 @@ public static class SubscriptionHandler
if (!url.Contains("target=")) if (!url.Contains("target="))
{ {
url += string.Format("&target={0}", item.ConvertTarget); url += $"&target={item.ConvertTarget}";
} }
if (!url.Contains("config=")) if (!url.Contains("config="))
{ {
url += string.Format("&config={0}", Global.SubConvertConfig.FirstOrDefault()); url += $"&config={Global.SubConvertConfig.FirstOrDefault()}";
} }
} }
+20 -10
View File
@@ -244,11 +244,28 @@ public sealed class AppManager
{ {
return []; return [];
} }
if (ids.Count <= Global.SqliteMaxBatchSize)
{
return await SQLiteHelper.Instance.TableAsync<ProfileItem>() return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId)) .Where(it => ids.Contains(it.IndexId))
.ToListAsync(); .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) public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
{ {
var items = await GetProfileItemsByIndexIds(indexIds); var items = await GetProfileItemsByIndexIds(indexIds);
@@ -257,18 +274,11 @@ public sealed class AppManager
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds) public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
{ {
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList(); var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (idList.Count == 0) var items = await GetProfileItemsByIndexIds(ids);
{
return [];
}
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => idList.Contains(it.IndexId))
.ToListAsync();
var itemMap = items.ToDictionary(it => it.IndexId); var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(id => itemMap.GetValueOrDefault(id)) return ids.Select(itemMap.GetValueOrDefault)
.Where(item => item != null) .Where(item => item != null)
.ToList(); .ToList();
} }
+54 -54
View File
@@ -10,7 +10,6 @@ public class CertPemManager
{ {
private static readonly string _tag = "CertPemManager"; private static readonly string _tag = "CertPemManager";
private static readonly Lazy<CertPemManager> _instance = new(() => new()); private static readonly Lazy<CertPemManager> _instance = new(() => new());
public static CertPemManager Instance => _instance.Value;
/// <summary> /// <summary>
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks /// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
@@ -24,21 +23,17 @@ public class CertPemManager
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2 "85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3 "18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
"CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA "C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA "1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA "3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA "4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA "7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2 "62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
"F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA
"4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority "0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority "1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA "41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna "E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority "C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
"EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány "6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009 "3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3 "CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
@@ -46,10 +41,6 @@ public class CertPemManager
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2 "45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2 "2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
"568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services 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 "5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority "BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2 "513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
@@ -62,7 +53,6 @@ public class CertPemManager
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2 "E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1 "9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA "59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
"DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2 "91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011 "F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3 "8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
@@ -116,16 +106,12 @@ public class CertPemManager
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017 "C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017 "BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
"657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2 "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 "88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS "554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45 "319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45 "5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46 "4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46 "CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
"9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA "FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA "6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA "FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
@@ -179,7 +165,6 @@ public class CertPemManager
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020 "578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023 "78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023 "EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
"BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA "3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2 "3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12 "3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
@@ -200,10 +185,13 @@ public class CertPemManager
"B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023 "B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023
}; };
public static CertPemManager Instance => _instance.Value;
/// <summary> /// <summary>
/// Get certificate in PEM format from a server with CA pinning validation /// Get certificate in PEM format from a server with CA pinning validation
/// </summary> /// </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 try
{ {
@@ -216,13 +204,13 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => 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); await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions var sslOptions = new SslClientAuthenticationOptions
{ {
TargetHost = serverName, TargetHost = serverName,
RemoteCertificateValidationCallback = callback RemoteCertificateValidationCallback = callback,
}; };
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
@@ -251,7 +239,8 @@ public class CertPemManager
/// <summary> /// <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> /// </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>(); var pemList = new List<string>();
try try
@@ -265,13 +254,13 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => 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); await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions var sslOptions = new SslClientAuthenticationOptions
{ {
TargetHost = serverName, TargetHost = serverName,
RemoteCertificateValidationCallback = callback RemoteCertificateValidationCallback = callback,
}; };
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
@@ -303,40 +292,33 @@ public class CertPemManager
/// <summary> /// <summary>
/// Validate server certificate with CA pinning /// Validate server certificate with CA pinning
/// </summary> /// </summary>
private bool ValidateServerCertificate( private static bool ValidateServerCertificate(
object _, object _,
X509Certificate? certificate, X509Certificate? certificate,
X509Chain? chain, X509Chain? chain,
SslPolicyErrors sslPolicyErrors, SslPolicyErrors sslPolicyErrors,
bool allowInsecure) List<string> verifyPeerCertByName)
{ {
if (certificate == null) if (certificate == null)
{ {
return false; 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 // Build certificate chain
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate); var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
var certChain = chain ?? new X509Chain(); var certChain = chain ?? new X509Chain();
certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online; if (chain == null)
certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; {
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
certChain.ChainPolicy.VerificationTime = DateTime.Now; certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
certChain.ChainPolicy.VerificationTime = DateTime.UtcNow;
certChain.Build(cert2); if (!certChain.Build(cert2))
{
return false;
}
}
// Find root CA // Find root CA
if (certChain.ChainElements.Count == 0) if (certChain.ChainElements.Count == 0)
@@ -344,10 +326,37 @@ public class CertPemManager
return false; return false;
} }
var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate; var rootCert = certChain.ChainElements[^1].Certificate;
var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256); var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256);
return TrustedCaThumbprints.Contains(rootThumbprint); if (!TrustedCaThumbprints.Contains(rootThumbprint))
{
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) public static string ExportCertToPem(X509Certificate2 cert)
@@ -415,14 +424,9 @@ public class CertPemManager
/// </summary> /// </summary>
/// <param name="pemList">List of individual PEM certificate strings</param> /// <param name="pemList">List of individual PEM certificate strings</param>
/// <returns>Concatenated PEM certificates string</returns> /// <returns>Concatenated PEM certificates string</returns>
public static string ConcatenatePemChain(IEnumerable<string> pemList) public static string ConcatenatePemChain(IEnumerable<string>? pemList)
{ {
if (pemList == null) return pemList == null ? string.Empty : string.Concat(pemList);
{
return string.Empty;
}
return string.Concat(pemList);
} }
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false) public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
@@ -431,11 +435,7 @@ public class CertPemManager
{ {
var cert = X509Certificate2.CreateFromPem(pemCert); var cert = X509Certificate2.CreateFromPem(pemCert);
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256); var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
if (includeColon) return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint;
{
return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c)));
}
return thumbprint;
} }
catch catch
{ {
+4 -9
View File
@@ -45,20 +45,15 @@ public sealed class ClashApiManager
{ {
await GetClashProxiesAsync(); await GetClashProxiesAsync();
} }
lstProxy = new List<ClashProxyModel>(); lstProxy = [];
foreach (var kv in _proxies ?? []) lstProxy.AddRange(from kv in _proxies ?? []
{ where !Global.notAllowTestType.Contains(kv.Value.type?.ToLower())
if (Global.notAllowTestType.Contains(kv.Value.type?.ToLower())) select new ClashProxyModel()
{
continue;
}
lstProxy.Add(new ClashProxyModel()
{ {
Name = kv.Value.name, Name = kv.Value.name,
Type = kv.Value.type?.ToLower(), Type = kv.Value.type?.ToLower(),
}); });
} }
}
if (lstProxy is not { Count: > 0 }) if (lstProxy is not { Count: > 0 })
{ {
@@ -113,6 +113,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip", DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip",
DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip", DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip",
DownloadUrlLinuxRiscV64 = urlN + "/download/{0}/v2rayN-linux-riscv64.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", DownloadUrlOSX64 = urlN + "/download/{0}/v2rayN-macos-64.zip",
DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.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", DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip",
DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip", DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip",
DownloadUrlLinuxRiscV64 = urlXray + "/download/{0}/Xray-linux-riscv64.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", DownloadUrlOSX64 = urlXray + "/download/{0}/Xray-macos-64.zip",
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip", DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
Match = "Xray", Match = "Xray",
@@ -180,6 +182,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz", DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz", DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
DownloadUrlLinuxRiscV64 = urlMihomo + "/download/{0}/mihomo-linux-riscv64-{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", DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz", DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
Match = "Mihomo", Match = "Mihomo",
@@ -223,6 +226,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz", DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz",
DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz", DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz",
DownloadUrlLinuxRiscV64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-riscv64.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", DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz", DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
Match = "sing-box", Match = "sing-box",
@@ -314,6 +318,7 @@ public sealed class CoreInfoManager
names.Add("mihomo-linux-amd64"); names.Add("mihomo-linux-amd64");
names.Add("mihomo-linux-arm64"); names.Add("mihomo-linux-arm64");
names.Add("mihomo-linux-riscv64"); names.Add("mihomo-linux-riscv64");
names.Add("mihomo-linux-loong64-abi2");
} }
else if (Utils.IsMacOS()) else if (Utils.IsMacOS())
{ {
+71 -1
View File
@@ -90,6 +90,7 @@ public class CoreManager
} }
await CoreStart(mainContext); await CoreStart(mainContext);
await WaitForProxyPort(preContext);
await CoreStartPreService(preContext); await CoreStartPreService(preContext);
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType; AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
@@ -213,6 +214,75 @@ public class CoreManager
await _updateFunc?.Invoke(notify, msg); 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 #endregion Private
#region Process #region Process
@@ -230,7 +300,7 @@ public class CoreManager
{ {
if (mayNeedSudo if (mayNeedSudo
&& _config.TunModeItem.EnableTun && _config.TunModeItem.EnableTun
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo) && (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray)
&& Utils.IsNonWindows()) && Utils.IsNonWindows())
{ {
_linuxSudo = true; _linuxSudo = true;
@@ -9,7 +9,7 @@ public class GroupProfileManager
public static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo) 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) 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); var childItems = await AppManager.Instance.GetProfileItemsByIndexIds(childIds);
foreach (var childItem in childItems) 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; return true;
} }
@@ -71,7 +71,7 @@ public class GroupProfileManager
public static async Task<(List<ProfileItem> Items, ProtocolExtraItem? Extra)> GetChildProfileItems(ProfileItem profileItem) 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); return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra);
} }
+10 -2
View File
@@ -111,7 +111,7 @@ public class ProfileExManager
public async Task ClearAll() public async Task ClearAll()
{ {
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem "); await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
_lstProfileEx = new(); _lstProfileEx = [];
} }
public async Task SaveTo() public async Task SaveTo()
@@ -150,6 +150,14 @@ public class ProfileExManager
IndexIdEnqueue(indexId); IndexIdEnqueue(indexId);
} }
public void SetTestIpInfo(string indexId, string ipInfo)
{
var profileEx = GetProfileExItem(indexId);
profileEx.IpInfo = ipInfo;
IndexIdEnqueue(indexId);
}
public void SetSort(string indexId, int sort) public void SetSort(string indexId, int sort)
{ {
var profileEx = GetProfileExItem(indexId); var profileEx = GetProfileExItem(indexId);
@@ -174,6 +182,6 @@ public class ProfileExManager
{ {
return 0; return 0;
} }
return _lstProfileEx.Max(t => t == null ? 0 : t.Sort); return _lstProfileEx.Max(t => t?.Sort ?? 0);
} }
} }
@@ -45,7 +45,7 @@ public class StatisticsManager
{ {
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem "); await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem ");
_serverStatItem = null; _serverStatItem = null;
_lstServerStat = new(); _lstServerStat = [];
} }
public async Task SaveTo() public async Task SaveTo()
+6 -1
View File
@@ -56,7 +56,7 @@ public class TaskManager
{ {
//Logging.SaveLog("Execute delete expired files"); //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.GetLogPath(), DateTime.Now.AddMonths(-1));
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1)); FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
@@ -142,5 +142,10 @@ public class TaskManager
await _updateFunc?.Invoke(false, msg); await _updateFunc?.Invoke(false, msg);
} }
NoticeManager.Instance.Enqueue(string.Join("\n", msgs)); NoticeManager.Instance.Enqueue(string.Join("\n", msgs));
if (msgs.Count > 0)
{
AppEvents.HasUpdateNotified.Publish(true);
}
} }
} }
@@ -7,10 +7,6 @@ public class CoreBasicItem
public string Loglevel { get; set; } public string Loglevel { get; set; }
public bool MuxEnabled { get; set; }
public bool DefAllowInsecure { get; set; }
public string DefFingerprint { get; set; } public string DefFingerprint { get; set; }
public string DefUserAgent { get; set; } public string DefUserAgent { get; set; }
@@ -21,6 +17,8 @@ public class CoreBasicItem
public bool EnableFragment { get; set; } public bool EnableFragment { get; set; }
public bool EnableFinalFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true; public bool EnableCacheFile4Sbox { get; set; } = true;
} }
@@ -148,6 +146,7 @@ public class TunModeItem
public bool EnableIPv6Address { get; set; } public bool EnableIPv6Address { get; set; }
public string IcmpRouting { get; set; } public string IcmpRouting { get; set; }
public bool EnableLegacyProtect { get; set; } public bool EnableLegacyProtect { get; set; }
public List<string>? RouteExcludeAddress { get; set; }
} }
[Serializable] [Serializable]
@@ -159,6 +158,8 @@ public class SpeedTestItem
public int MixedConcurrencyCount { get; set; } public int MixedConcurrencyCount { get; set; }
public string IPAPIUrl { get; set; } public string IPAPIUrl { get; set; }
public string UdpTestTarget { get; set; } public string UdpTestTarget { get; set; }
public int? SpeedTestPageSize { get; set; }
public int? SpeedTestDelayInterval { get; set; }
} }
[Serializable] [Serializable]
@@ -248,6 +249,7 @@ public class Fragment4RayItem
public string? Packets { get; set; } public string? Packets { get; set; }
public string? Length { get; set; } public string? Length { get; set; }
public string? Interval { get; set; } public string? Interval { get; set; }
public string? MaxSplit { get; set; }
} }
[Serializable] [Serializable]
@@ -13,6 +13,7 @@ public class CoreInfo
public string? DownloadUrlLinux64 { get; set; } public string? DownloadUrlLinux64 { get; set; }
public string? DownloadUrlLinuxArm64 { get; set; } public string? DownloadUrlLinuxArm64 { get; set; }
public string? DownloadUrlLinuxRiscV64 { get; set; } public string? DownloadUrlLinuxRiscV64 { get; set; }
public string? DownloadUrlLinuxLoong64 { get; set; }
public string? DownloadUrlOSX64 { get; set; } public string? DownloadUrlOSX64 { get; set; }
public string? DownloadUrlOSXArm64 { get; set; } public string? DownloadUrlOSXArm64 { get; set; }
public string? Match { get; set; } public string? Match { get; set; }
@@ -93,6 +93,7 @@ public class Rule4Sbox
public List<string>? wifi_bssid { get; set; } public List<string>? wifi_bssid { get; set; }
public bool? rule_set_ip_cidr_match_source { get; set; } public bool? rule_set_ip_cidr_match_source { get; set; }
public bool? rule_set_ip_cidr_accept_empty { get; set; } public bool? rule_set_ip_cidr_accept_empty { get; set; }
public bool? tls_record_fragment { get; set; }
} }
[Serializable] [Serializable]
@@ -110,6 +111,7 @@ public class Inbound4Sbox
public bool? endpoint_independent_nat { get; set; } public bool? endpoint_independent_nat { get; set; }
public string? stack { get; set; } public string? stack { get; set; }
public List<User4Sbox> users { get; set; } public List<User4Sbox> users { get; set; }
public List<string>? route_exclude_address { get; set; }
} }
public class User4Sbox public class User4Sbox
@@ -246,6 +248,16 @@ public class HyObfs4Sbox
{ {
public string? type { get; set; } public string? type { get; set; }
public string? password { 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 public class Server4Sbox : BaseServer4Sbox
@@ -332,6 +344,7 @@ public abstract class DialFields4Sbox
public Multiplex4Sbox? multiplex { get; set; } public Multiplex4Sbox? multiplex { get; set; }
public Transport4Sbox? transport { get; set; } public Transport4Sbox? transport { get; set; }
public HyObfs4Sbox? obfs { get; set; } public HyObfs4Sbox? obfs { get; set; }
public HyRealm4Sbox? realm { get; set; }
} }
public abstract class BaseServer4Sbox : DialFields4Sbox public abstract class BaseServer4Sbox : DialFields4Sbox
@@ -20,7 +20,7 @@ public class Stats4Ray
public class Metrics4Ray public class Metrics4Ray
{ {
public string tag { get; set; } public string listen { get; set; }
} }
public class Policy4Ray public class Policy4Ray
@@ -86,7 +86,7 @@ public class Inboundsettings4Ray
public string? autoOutboundsInterface { get; set; } public string? autoOutboundsInterface { get; set; }
// public List<string>? dns { get; set; } public List<string>? dns { get; set; }
} }
public class UsersItem4Ray public class UsersItem4Ray
@@ -136,7 +136,7 @@ public class Outboundsettings4Ray
public Response4Ray? response { get; set; } public Response4Ray? response { get; set; }
public string domainStrategy { get; set; } public string? domainStrategy { get; set; }
public int? userLevel { get; set; } public int? userLevel { get; set; }
@@ -276,6 +276,7 @@ public class BalancersItem4Ray
public List<string>? selector { get; set; } public List<string>? selector { get; set; }
public BalancersStrategy4Ray? strategy { get; set; } public BalancersStrategy4Ray? strategy { get; set; }
public string? tag { get; set; } public string? tag { get; set; }
public string? fallbackTag { get; set; }
} }
public class BalancersStrategy4Ray public class BalancersStrategy4Ray
@@ -288,16 +289,7 @@ public class BalancersStrategySettings4Ray
{ {
public int? expected { get; set; } public int? expected { get; set; }
public string? maxRTT { get; set; } public string? maxRTT { get; set; }
public float? tolerance { get; set; } public double? 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 class Observatory4Ray public class Observatory4Ray
@@ -372,6 +364,7 @@ public class TlsSettings4Ray
public string? spiderX { get; set; } public string? spiderX { get; set; }
public string? mldsa65Verify { get; set; } public string? mldsa65Verify { get; set; }
public List<CertificateSettings4Ray>? certificates { get; set; } public List<CertificateSettings4Ray>? certificates { get; set; }
public string? verifyPeerCertByName { get; set; }
public string? pinnedPeerCertSha256 { get; set; } public string? pinnedPeerCertSha256 { get; set; }
public bool? disableSystemRoot { get; set; } public bool? disableSystemRoot { get; set; }
public string? echConfigList { get; set; } public string? echConfigList { get; set; }
@@ -487,6 +480,7 @@ public class UdpHop4Ray
public class Finalmask4Ray public class Finalmask4Ray
{ {
public List<Mask4Ray>? tcp { get; set; }
public List<Mask4Ray>? udp { get; set; } public List<Mask4Ray>? udp { get; set; }
public QuicParams4Ray? quicParams { get; set; } public QuicParams4Ray? quicParams { get; set; }
} }
@@ -499,14 +493,21 @@ public class Mask4Ray
public class MaskSettings4Ray public class MaskSettings4Ray
{ {
public string? header { get; set; }
public string? value { get; set; }
public string? password { 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 // fragment
public string? packets { get; set; } public string? packets { get; set; }
public string? length { get; set; } public string? length { get; set; }
public string? delay { get; set; } public string? delay { get; set; }
public int? maxSplit { get; set; }
// noise // noise
public int? reset { get; set; } public int? reset { get; set; }
+115
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();
}
}
@@ -17,3 +17,12 @@ public class LocationInfo
{ {
public string? country_code { get; set; } public string? country_code { get; set; }
} }
public readonly record struct IpInfoResult(string Country, string? Ip)
{
public override string ToString()
{
var emoji = Utils.IsWindows() ? null : Country.CountryToEmoji();
return $"{emoji}({Country}) {Ip}";
}
}
@@ -26,6 +26,9 @@ public class ProfileItemModel : ReactiveObject
[Reactive] [Reactive]
public string SpeedVal { get; set; } public string SpeedVal { get; set; }
[Reactive]
public string IpInfo { get; set; }
[Reactive] [Reactive]
public string TodayUp { get; set; } public string TodayUp { get; set; }
+6 -16
View File
@@ -1,26 +1,16 @@
namespace ServiceLib.Models.Dto; namespace ServiceLib.Models.Dto;
public class RetResult public class RetResult(bool success, string? msg, object? data)
{ {
public bool Success { get; set; } public RetResult(bool success = false) : this(success, null, null)
public string? Msg { get; set; }
public object? Data { get; set; }
public RetResult(bool success = false)
{ {
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) public bool Success { get; set; } = success;
{ public string? Msg { get; set; } = msg;
Success = success; public object? Data { get; set; } = data;
Msg = msg;
Data = data;
}
} }
@@ -8,4 +8,6 @@ public class SpeedTestResult
public string? Delay { get; set; } public string? Delay { get; set; }
public string? Speed { get; set; } public string? Speed { get; set; }
public string? IpInfo { get; set; }
} }
@@ -40,4 +40,8 @@ public class VmessQRCode
public string fp { get; set; } = string.Empty; public string fp { get; set; } = string.Empty;
public string insecure { 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;
} }
@@ -10,4 +10,5 @@ public class ProfileExItem
public decimal Speed { get; set; } public decimal Speed { get; set; }
public int Sort { get; set; } public int Sort { get; set; }
public string? Message { get; set; } public string? Message { get; set; }
public string? IpInfo { get; set; }
} }
@@ -146,6 +146,11 @@ public class ProfileItem
TransportExtra = JsonUtils.Serialize(transportExtra, false); TransportExtra = JsonUtils.Serialize(transportExtra, false);
} }
public bool GetAllowInsecure()
{
return AllowInsecure == Global.StringTrue;
}
#endregion function #endregion function
[PrimaryKey] [PrimaryKey]
@@ -191,6 +196,7 @@ public class ProfileItem
public string Cert { get; set; } public string Cert { get; set; }
public string CertSha { get; set; } public string CertSha { get; set; }
public string EchConfigList { get; set; } public string EchConfigList { get; set; }
public string VerifyPeerCertByName { get; set; }
public string Finalmask { get; set; } public string Finalmask { get; set; }
public string ProtoExtra { get; set; } public string ProtoExtra { get; set; }
@@ -31,6 +31,12 @@ public record ProtocolExtraItem
public int? DownMbps { get; init; } public int? DownMbps { get; init; }
public string? Ports { get; init; } public string? Ports { get; init; }
public string? HopInterval { 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 // naiveproxy
public int? InsecureConcurrency { get; init; } public int? InsecureConcurrency { get; init; }
+244 -28
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> /// <summary>
/// 查找类似 Please enter the correct port format. 的本地化字符串。 /// 查找类似 Please enter the correct port format. 的本地化字符串。
/// </summary> /// </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> /// <summary>
/// 查找类似 Please enter the local listening port. 的本地化字符串。 /// 查找类似 Please enter the local listening port. 的本地化字符串。
/// </summary> /// </summary>
@@ -321,6 +321,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Invalid Realm URL. 的本地化字符串。
/// </summary>
public static string InvalidHy2RealmUrl {
get {
return ResourceManager.GetString("InvalidHy2RealmUrl", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Invalid address (URL) 的本地化字符串。 /// 查找类似 Invalid address (URL) 的本地化字符串。
/// </summary> /// </summary>
@@ -564,6 +573,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 IP Info 的本地化字符串。
/// </summary>
public static string LvTestIpInfo {
get {
return ResourceManager.GetString("LvTestIpInfo", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Speed (MB/s) 的本地化字符串。 /// 查找类似 Speed (MB/s) 的本地化字符串。
/// </summary> /// </summary>
@@ -1311,6 +1329,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 New Update 的本地化字符串。
/// </summary>
public static string menuNewUpdate {
get {
return ResourceManager.GetString("menuNewUpdate", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Open the storage location 的本地化字符串。 /// 查找类似 Open the storage location 的本地化字符串。
/// </summary> /// </summary>
@@ -1896,6 +1923,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> /// <summary>
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。 /// 查找类似 {0} has a new version available: {1} 的本地化字符串。
/// </summary> /// </summary>
@@ -2040,6 +2076,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> /// <summary>
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。 /// 查找类似 The {0} property is invalid, please check 的本地化字符串。
/// </summary> /// </summary>
@@ -2085,6 +2130,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Conflict between {0} and {1} 的本地化字符串。
/// </summary>
public static string MsgOptionsConflict {
get {
return ResourceManager.GetString("MsgOptionsConflict", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Resolved {0} successfully 的本地化字符串。 /// 查找类似 Resolved {0} successfully 的本地化字符串。
/// </summary> /// </summary>
@@ -2202,6 +2256,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> /// <summary>
/// 查找类似 Unpacking... 的本地化字符串。 /// 查找类似 Unpacking... 的本地化字符串。
/// </summary> /// </summary>
@@ -2625,24 +2688,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> /// <summary>
/// 查找类似 ALPN 的本地化字符串。 /// 查找类似 ALPN 的本地化字符串。
/// </summary> /// </summary>
@@ -2988,6 +3033,33 @@ 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>
/// 查找类似 DNS via Bridge 的本地化字符串。
/// </summary>
public static string TbEnabletDnsViaProxy {
get {
return ResourceManager.GetString("TbEnabletDnsViaProxy", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Enable Tun 的本地化字符串。 /// 查找类似 Enable Tun 的本地化字符串。
/// </summary> /// </summary>
@@ -3096,6 +3168,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
/// </summary>
public static string TbGeckoPacketSize {
get {
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Global Hotkey Settings 的本地化字符串。 /// 查找类似 Global Hotkey Settings 的本地化字符串。
/// </summary> /// </summary>
@@ -3159,6 +3240,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <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> /// <summary>
/// 查找类似 ICMP routing policy 的本地化字符串。 /// 查找类似 ICMP routing policy 的本地化字符串。
/// </summary> /// </summary>
@@ -3510,6 +3609,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> /// <summary>
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// </summary> /// </summary>
@@ -3745,7 +3862,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <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> /// </summary>
public static string TbSettingsBindInterfaceTip { public static string TbSettingsBindInterfaceTip {
get { get {
@@ -4032,6 +4149,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> /// <summary>
/// 查找类似 Geo files source (optional) 的本地化字符串。 /// 查找类似 Geo files source (optional) 的本地化字符串。
/// </summary> /// </summary>
@@ -4680,6 +4887,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Verify Peer Cert By Name 的本地化字符串。
/// </summary>
public static string TbVerifyPeerCertByName {
get {
return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。 /// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
/// </summary> /// </summary>
@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>فعال کردن فرگمنت</value> <value>فعال کردن فرگمنت</value>
</data> </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"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value> <value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value>
</data> </data>
@@ -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"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </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"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value> <value>Test Configurations UDP Delay</value>
</data> </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"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value> <value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data> </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"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value> <value>Bind Interface</value>
</data> </data>
@@ -1740,4 +1764,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve"> <data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value> <value>Not Support</value>
</data> </data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root> </root>
+47 -17
View File
@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Activer le fragmentation (Fragment)</value> <value>Activer le fragmentation (Fragment)</value>
</data> </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"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Activer le fichier de cache de sing-box (fichiers règles)</value> <value>Activer le fichier de cache de sing-box (fichiers règles)</value>
</data> </data>
@@ -1675,7 +1708,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Ajouter [NaïveProxy]</value> <value>Ajouter [NaïveProxy]</value>
</data> </data>
<data name="TbInsecureConcurrency" xml:space="preserve"> <data name="TbInsecureConcurrency" xml:space="preserve">
<value>Insecure Concurrency</value> <value>Concurrence non sûre</value>
</data> </data>
<data name="TbUsername" xml:space="preserve"> <data name="TbUsername" xml:space="preserve">
<value>Nom d’utilisateur</value> <value>Nom d’utilisateur</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"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </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"> <data name="TbSettingsSendThrough" xml:space="preserve">
<value>Adresse sortante locale (SendThrough)</value> <value>Adresse de sortie locale (SendThrough)</value>
</data> </data>
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value> <value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Veuillez saisir l’adresse IPv4 correcte de SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Lier l'interface</value> <value>Lier l'interface</value>
</data> </data>
@@ -1717,24 +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> <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>
<data name="menuUdpTestServer" xml:space="preserve"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value> <value>Tester la latence UDP des profils</value>
</data> </data>
<data name="TbSettingsUdpTestUrl" xml:space="preserve"> <data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>UDP Test Url</value> <value>URL de test UDP</value>
</data> </data>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value> <value>PreSharedKey</value>
</data> </data>
<data name="menuExport2InnerUri" xml:space="preserve"> <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>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve"> <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>
<data name="menuCheckOnly" xml:space="preserve"> <data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value> <value>Uniquement vérifier</value>
</data> </data>
<data name="MsgNotSupport" xml:space="preserve"> <data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value> <value>Non supporté</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>Détails IP</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>Nouvelle MAJ</value>
</data> </data>
</root> </root>
+39 -9
View File
@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Fragment engedélyezése</value> <value>Fragment engedélyezése</value>
</data> </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"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value> <value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value>
</data> </data>
@@ -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"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </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"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value> <value>Test Configurations UDP Delay</value>
</data> </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"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value> <value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data> </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"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value> <value>Bind Interface</value>
</data> </data>
@@ -1740,4 +1764,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve"> <data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value> <value>Not Support</value>
</data> </data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root> </root>
File diff suppressed because it is too large Load Diff
+82 -10
View File
@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Enable fragment</value> <value>Enable fragment</value>
</data> </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"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Enable cache file for sing-box (ruleset files)</value> <value>Enable cache file for sing-box (ruleset files)</value>
</data> </data>
@@ -1581,6 +1614,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbEchConfigList" xml:space="preserve"> <data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value> <value>EchConfigList</value>
</data> </data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value>
</data>
<data name="TbFullCertTips" xml:space="preserve"> <data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value> <value>Full certificate (chain), PEM format</value>
</data> </data>
@@ -1698,12 +1734,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </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"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value> <value>Test Configurations UDP Delay</value>
</data> </data>
@@ -1716,14 +1746,11 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value> <value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data> </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"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value> <value>Bind Interface</value>
</data> </data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve"> <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>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value> <value>PreSharedKey</value>
@@ -1740,4 +1767,49 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve"> <data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value> <value>Not Support</value>
</data> </data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<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="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>
</root> </root>
+89 -59
View File
@@ -121,7 +121,7 @@
<value>Ссылка успешно скопирована в буфер обмена</value> <value>Ссылка успешно скопирована в буфер обмена</value>
</data> </data>
<data name="CheckServerSettings" xml:space="preserve"> <data name="CheckServerSettings" xml:space="preserve">
<value>Сначала проверьте настройки сервера</value> <value>Недопустимая конфигурация, проверьте или выберите заново</value>
</data> </data>
<data name="ConfigurationFormatIncorrect" xml:space="preserve"> <data name="ConfigurationFormatIncorrect" xml:space="preserve">
<value>Недопустимый формат конфигурации</value> <value>Недопустимый формат конфигурации</value>
@@ -166,7 +166,7 @@
<value>Некорректная конфигурация. Проверьте</value> <value>Некорректная конфигурация. Проверьте</value>
</data> </data>
<data name="InitialConfiguration" xml:space="preserve"> <data name="InitialConfiguration" xml:space="preserve">
<value>Исходная конфигурация</value> <value>Инициализация конфигурации</value>
</data> </data>
<data name="IsLatestCore" xml:space="preserve"> <data name="IsLatestCore" xml:space="preserve">
<value>{0} {1} является последней версией</value> <value>{0} {1} является последней версией</value>
@@ -256,7 +256,7 @@
<value>Операция не удалась, проверьте и повторите попытку</value> <value>Операция не удалась, проверьте и повторите попытку</value>
</data> </data>
<data name="PleaseFillRemarks" xml:space="preserve"> <data name="PleaseFillRemarks" xml:space="preserve">
<value>Введите примечания</value> <value>Введите псевдоним</value>
</data> </data>
<data name="PleaseSelectEncryption" xml:space="preserve"> <data name="PleaseSelectEncryption" xml:space="preserve">
<value>Выберите метод шифрования</value> <value>Выберите метод шифрования</value>
@@ -307,7 +307,7 @@
<value>{0}: одно из обязательных полей</value> <value>{0}: одно из обязательных полей</value>
</data> </data>
<data name="LvRemarks" xml:space="preserve"> <data name="LvRemarks" xml:space="preserve">
<value>Примечания</value> <value>Псевдоним</value>
</data> </data>
<data name="LvUrl" xml:space="preserve"> <data name="LvUrl" xml:space="preserve">
<value>URL (необязательно)</value> <value>URL (необязательно)</value>
@@ -340,7 +340,7 @@
<value>*HTTP2-путь</value> <value>*HTTP2-путь</value>
</data> </data>
<data name="TransportPathTip3" xml:space="preserve"> <data name="TransportPathTip3" xml:space="preserve">
<value>*QUIC-ключ / KCP-seed</value> <value>*Ключ шифрования QUIC</value>
</data> </data>
<data name="TransportPathTip4" xml:space="preserve"> <data name="TransportPathTip4" xml:space="preserve">
<value>Имя сервиса gRPC</value> <value>Имя сервиса gRPC</value>
@@ -355,7 +355,7 @@
<value>*HTTP2-хосты, разделённые запятыми (,)</value> <value>*HTTP2-хосты, разделённые запятыми (,)</value>
</data> </data>
<data name="TransportRequestHostTip4" xml:space="preserve"> <data name="TransportRequestHostTip4" xml:space="preserve">
<value>Безопасность *QUIC</value> <value>*Метод шифрования QUIC</value>
</data> </data>
<data name="TransportHeaderType1" xml:space="preserve"> <data name="TransportHeaderType1" xml:space="preserve">
<value>Тип raw-камуфляжа</value> <value>Тип raw-камуфляжа</value>
@@ -400,7 +400,7 @@
<value>Фильтр, нажмите Enter для выполнения</value> <value>Фильтр, нажмите Enter для выполнения</value>
</data> </data>
<data name="menuCheckUpdate" xml:space="preserve"> <data name="menuCheckUpdate" xml:space="preserve">
<value>Проверить обновления</value> <value>Обновить</value>
</data> </data>
<data name="menuClose" xml:space="preserve"> <data name="menuClose" xml:space="preserve">
<value>Закрыть</value> <value>Закрыть</value>
@@ -415,13 +415,13 @@
<value>Помощь</value> <value>Помощь</value>
</data> </data>
<data name="menuOptionSetting" xml:space="preserve"> <data name="menuOptionSetting" xml:space="preserve">
<value>Настройки</value> <value>Параметры</value>
</data> </data>
<data name="menuPromotion" xml:space="preserve"> <data name="menuPromotion" xml:space="preserve">
<value>Продвижение</value> <value>Продвижение</value>
</data> </data>
<data name="menuReload" xml:space="preserve"> <data name="menuReload" xml:space="preserve">
<value>Перезагрузить</value> <value>Перезапустить службу</value>
</data> </data>
<data name="menuRoutingSetting" xml:space="preserve"> <data name="menuRoutingSetting" xml:space="preserve">
<value>Настройки маршрутизации</value> <value>Настройки маршрутизации</value>
@@ -445,10 +445,10 @@
<value>Настройки групп подписки</value> <value>Настройки групп подписки</value>
</data> </data>
<data name="menuSubUpdate" xml:space="preserve"> <data name="menuSubUpdate" xml:space="preserve">
<value>Обновить подписку без прокси</value> <value>Обновить все подписки без прокси</value>
</data> </data>
<data name="menuSubUpdateViaProxy" xml:space="preserve"> <data name="menuSubUpdateViaProxy" xml:space="preserve">
<value>Обновить подписку с прокси</value> <value>Обновить все подписки через прокси</value>
</data> </data>
<data name="menuSystemproxy" xml:space="preserve"> <data name="menuSystemproxy" xml:space="preserve">
<value>Системный прокси</value> <value>Системный прокси</value>
@@ -562,7 +562,7 @@
<value>Сортировка</value> <value>Сортировка</value>
</data> </data>
<data name="LvUserAgent" xml:space="preserve"> <data name="LvUserAgent" xml:space="preserve">
<value>User-Agent</value> <value>User-Agent (необязательно)</value>
</data> </data>
<data name="TbCancel" xml:space="preserve"> <data name="TbCancel" xml:space="preserve">
<value>Отмена</value> <value>Отмена</value>
@@ -577,7 +577,7 @@
<value>Адрес</value> <value>Адрес</value>
</data> </data>
<data name="TbAllowInsecure" xml:space="preserve"> <data name="TbAllowInsecure" xml:space="preserve">
<value>Разрешить небезопасные</value> <value>Пропустить проверку сертификата (allowInsecure)</value>
</data> </data>
<data name="TbAlpn" xml:space="preserve"> <data name="TbAlpn" xml:space="preserve">
<value>ALPN</value> <value>ALPN</value>
@@ -616,10 +616,10 @@
<value>TLS</value> <value>TLS</value>
</data> </data>
<data name="TipNetwork" xml:space="preserve"> <data name="TipNetwork" xml:space="preserve">
<value>*По-умолчанию raw</value> <value>*По умолчанию raw; при неверном выборе подключение будет невозможно</value>
</data> </data>
<data name="TbCoreType" xml:space="preserve"> <data name="TbCoreType" xml:space="preserve">
<value>Ядро</value> <value>Тип ядра</value>
</data> </data>
<data name="TbFlow5" xml:space="preserve"> <data name="TbFlow5" xml:space="preserve">
<value>Управление потоком (Flow)</value> <value>Управление потоком (Flow)</value>
@@ -640,7 +640,7 @@
<value>Шифрование</value> <value>Шифрование</value>
</data> </data>
<data name="TbSecurity4" xml:space="preserve"> <data name="TbSecurity4" xml:space="preserve">
<value>Пользователь (необязательно)</value> <value>Имя пользователя (необязательно)</value>
</data> </data>
<data name="TbSecurity5" xml:space="preserve"> <data name="TbSecurity5" xml:space="preserve">
<value>Шифрование</value> <value>Шифрование</value>
@@ -649,7 +649,7 @@
<value>Порт SOCKS</value> <value>Порт SOCKS</value>
</data> </data>
<data name="TipPreSocksPort" xml:space="preserve"> <data name="TipPreSocksPort" xml:space="preserve">
<value>* После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости</value> <value>*Порт SOCKS пользовательской конфигурации можно не указывать. Если он задан, Xray/sing-box (TUN) дополнительно запустит предварительную службу SOCKS для разделения трафика, отображения скорости и других функций</value>
</data> </data>
<data name="TbBrowse" xml:space="preserve"> <data name="TbBrowse" xml:space="preserve">
<value>Обзор</value> <value>Обзор</value>
@@ -682,7 +682,7 @@
<value>Настройки типа ядра</value> <value>Настройки типа ядра</value>
</data> </data>
<data name="TbSettingsDefAllowInsecure" xml:space="preserve"> <data name="TbSettingsDefAllowInsecure" xml:space="preserve">
<value>Разрешить небезопасные</value> <value>По умолчанию пропускать проверку сертификата (allowInsecure)</value>
</data> </data>
<data name="TbSettingsDomainStrategy4Freedom" xml:space="preserve"> <data name="TbSettingsDomainStrategy4Freedom" xml:space="preserve">
<value>«Freedom»: стратегия обработки доменов исходящего трафика</value> <value>«Freedom»: стратегия обработки доменов исходящего трафика</value>
@@ -703,7 +703,7 @@
<value>Показывать скорость в реальном времени (требуется перезапуск)</value> <value>Показывать скорость в реальном времени (требуется перезапуск)</value>
</data> </data>
<data name="TbSettingsKeepOlderDedupl" xml:space="preserve"> <data name="TbSettingsKeepOlderDedupl" xml:space="preserve">
<value>Сохранить старые при удалении дублей</value> <value>При удалении дубликатов сохранять элементы с меньшим порядковым номером</value>
</data> </data>
<data name="TbSettingsLogEnabled" xml:space="preserve"> <data name="TbSettingsLogEnabled" xml:space="preserve">
<value>Записывать логи</value> <value>Записывать логи</value>
@@ -730,13 +730,13 @@
<value>Смешанный порт</value> <value>Смешанный порт</value>
</data> </data>
<data name="TbSettingsStartBoot" xml:space="preserve"> <data name="TbSettingsStartBoot" xml:space="preserve">
<value>Запускать при старте системы</value> <value>Запускать при старте системы (может не сработать)</value>
</data> </data>
<data name="TbSettingsStatistics" xml:space="preserve"> <data name="TbSettingsStatistics" xml:space="preserve">
<value>Включить статистику трафика (требуется перезапуск)</value> <value>Включить статистику трафика (требуется перезапуск)</value>
</data> </data>
<data name="TbSettingsSubConvert" xml:space="preserve"> <data name="TbSettingsSubConvert" xml:space="preserve">
<value>URL конвертации подписок</value> <value>URL конвертации подписок (необязательно)</value>
</data> </data>
<data name="TbSettingsSystemproxy" xml:space="preserve"> <data name="TbSettingsSystemproxy" xml:space="preserve">
<value>Настройки системного прокси</value> <value>Настройки системного прокси</value>
@@ -787,13 +787,13 @@
<value>Запущено от имени администратора</value> <value>Запущено от имени администратора</value>
</data> </data>
<data name="menuMoveBottom" xml:space="preserve"> <data name="menuMoveBottom" xml:space="preserve">
<value>Спуститься вниз</value> <value>Переместить в самый низ</value>
</data> </data>
<data name="menuMoveDown" xml:space="preserve"> <data name="menuMoveDown" xml:space="preserve">
<value>Вниз</value> <value>Вниз</value>
</data> </data>
<data name="menuMoveTop" xml:space="preserve"> <data name="menuMoveTop" xml:space="preserve">
<value>Подняться наверх</value> <value>Переместить в самый верх</value>
</data> </data>
<data name="menuMoveUp" xml:space="preserve"> <data name="menuMoveUp" xml:space="preserve">
<value>Вверх</value> <value>Вверх</value>
@@ -805,7 +805,7 @@
<value>Веб-сайт {0}</value> <value>Веб-сайт {0}</value>
</data> </data>
<data name="menuRoutingAdvancedAdd" xml:space="preserve"> <data name="menuRoutingAdvancedAdd" xml:space="preserve">
<value>Добавить</value> <value>Добавить набор правил</value>
</data> </data>
<data name="menuRoutingAdvancedImportRules" xml:space="preserve"> <data name="menuRoutingAdvancedImportRules" xml:space="preserve">
<value>Импортировать правила</value> <value>Импортировать правила</value>
@@ -841,7 +841,7 @@
<value>Добавить правило</value> <value>Добавить правило</value>
</data> </data>
<data name="menuRuleExportSelected" xml:space="preserve"> <data name="menuRuleExportSelected" xml:space="preserve">
<value>Экспорт выделенных правил</value> <value>Экспортировать выбранные правила в буфер обмена</value>
</data> </data>
<data name="menuRuleList" xml:space="preserve"> <data name="menuRuleList" xml:space="preserve">
<value>Список правил</value> <value>Список правил</value>
@@ -859,7 +859,7 @@
<value>Документация RuleObject</value> <value>Документация RuleObject</value>
</data> </data>
<data name="TbDnsObjectDoc" xml:space="preserve"> <data name="TbDnsObjectDoc" xml:space="preserve">
<value>Поддерживаются DNS-объекты, нажмите для просмотра документации</value> <value>Заполните DNS-объект (формат JSON), нажмите для просмотра документации</value>
</data> </data>
<data name="SubUrlTips" xml:space="preserve"> <data name="SubUrlTips" xml:space="preserve">
<value>Для группы оставьте это поле пустым</value> <value>Для группы оставьте это поле пустым</value>
@@ -871,13 +871,13 @@
<value>Системные прокси изменены</value> <value>Системные прокси изменены</value>
</data> </data>
<data name="TbSettingsRouteOnly" xml:space="preserve"> <data name="TbSettingsRouteOnly" xml:space="preserve">
<value>Только маршрут</value> <value>Только для маршрутизации (routeOnly)</value>
</data> </data>
<data name="TbSettingsNotProxyLocalAddress" xml:space="preserve"> <data name="TbSettingsNotProxyLocalAddress" xml:space="preserve">
<value>Не использовать прокси для локальных (интранет) адресов</value> <value>Не использовать прокси для локальных (интранет) адресов</value>
</data> </data>
<data name="menuMixedTestServer" xml:space="preserve"> <data name="menuMixedTestServer" xml:space="preserve">
<value>Тест задержки и скорости всех серверов (Ctrl+E)</value> <value>Многопоточный тест задержки и скорости (Ctrl+E)</value>
</data> </data>
<data name="LvTestDelay" xml:space="preserve"> <data name="LvTestDelay" xml:space="preserve">
<value>Задержка (мс)</value> <value>Задержка (мс)</value>
@@ -886,10 +886,10 @@
<value>Скорость (МБ/с)</value> <value>Скорость (МБ/с)</value>
</data> </data>
<data name="FailedToRunCore" xml:space="preserve"> <data name="FailedToRunCore" xml:space="preserve">
<value>Не удалось запустить ядро, посмотрите логи</value> <value>Не удалось запустить ядро, см. сообщение</value>
</data> </data>
<data name="LvFilter" xml:space="preserve"> <data name="LvFilter" xml:space="preserve">
<value>Фильтр по примечаниям (регулярные выражения)</value> <value>Фильтр по псевдониму (регулярные выражения)</value>
</data> </data>
<data name="TbDisplayLog" xml:space="preserve"> <data name="TbDisplayLog" xml:space="preserve">
<value>Отображать журнал</value> <value>Отображать журнал</value>
@@ -937,7 +937,7 @@
<value>Шрифт (требуется перезапуск)</value> <value>Шрифт (требуется перезапуск)</value>
</data> </data>
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve"> <data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек</value> <value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и перезапустите приложение</value>
</data> </data>
<data name="TbSettingsSocksPortTip" xml:space="preserve"> <data name="TbSettingsSocksPortTip" xml:space="preserve">
<value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value> <value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value>
@@ -1009,7 +1009,7 @@
<value>Пользовательский DNS для sing-box</value> <value>Пользовательский DNS для sing-box</value>
</data> </data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve"> <data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>Заполните структуру DNS, нажмите, чтобы открыть документ</value> <value>Заполните структуру DNS (формат JSON), нажмите для просмотра документации</value>
</data> </data>
<data name="TbSettingDnsImportDefConfig" xml:space="preserve"> <data name="TbSettingDnsImportDefConfig" xml:space="preserve">
<value>Нажмите, чтобы импортировать конфигурацию DNS по умолчанию</value> <value>Нажмите, чтобы импортировать конфигурацию DNS по умолчанию</value>
@@ -1033,7 +1033,7 @@
<value>Добавить сервер [Hysteria2]</value> <value>Добавить сервер [Hysteria2]</value>
</data> </data>
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve"> <data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
<value>Максимальная пропускная способность Hysteria (загрузка/отдача)</value> <value>Максимальная пропускная способность Hysteria (отдача/загрузка)</value>
</data> </data>
<data name="TbSettingsUseSystemHosts" xml:space="preserve"> <data name="TbSettingsUseSystemHosts" xml:space="preserve">
<value>Использовать системный файл hosts</value> <value>Использовать системный файл hosts</value>
@@ -1045,13 +1045,13 @@
<value>Управление перегрузками</value> <value>Управление перегрузками</value>
</data> </data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>Примечания к предыдущему прокси</value> <value>Псевдоним предыдущего прокси</value>
</data> </data>
<data name="LvNextProfile" xml:space="preserve"> <data name="LvNextProfile" xml:space="preserve">
<value>Примечания к следующему прокси</value> <value>Псевдоним следующего прокси</value>
</data> </data>
<data name="LvPrevProfileTip" xml:space="preserve"> <data name="LvPrevProfileTip" xml:space="preserve">
<value>Убедитесь, что примечание существует и является уникальным</value> <value>Убедитесь, что псевдоним существует и является уникальным</value>
</data> </data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve"> <data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>Автоматическая маршрутизация</value> <value>Автоматическая маршрутизация</value>
@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Включить фрагментацию (Fragment)</value> <value>Включить фрагментацию (Fragment)</value>
</data> </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"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Включить файл кэша для sing-box (файлы наборов правил)</value> <value>Включить файл кэша для sing-box (файлы наборов правил)</value>
</data> </data>
@@ -1177,7 +1210,7 @@
<value>Глобальный режим</value> <value>Глобальный режим</value>
</data> </data>
<data name="menuModeNothing" xml:space="preserve"> <data name="menuModeNothing" xml:space="preserve">
<value>Не менять</value> <value>Как в исходной конфигурации</value>
</data> </data>
<data name="menuModeRule" xml:space="preserve"> <data name="menuModeRule" xml:space="preserve">
<value>Правила</value> <value>Правила</value>
@@ -1210,7 +1243,7 @@
<value>Экспорт ссылок в формате Base64 в буфер обмена</value> <value>Экспорт ссылок в формате Base64 в буфер обмена</value>
</data> </data>
<data name="menuExport2ClientConfigClipboard" xml:space="preserve"> <data name="menuExport2ClientConfigClipboard" xml:space="preserve">
<value>Экспортировать полную конфигурацию в буфер обмена</value> <value>Экспортировать выбранную полную конфигурацию в буфер обмена</value>
</data> </data>
<data name="menuShowOrHideMainWindow" xml:space="preserve"> <data name="menuShowOrHideMainWindow" xml:space="preserve">
<value>Показать или скрыть главное окно</value> <value>Показать или скрыть главное окно</value>
@@ -1300,7 +1333,7 @@
<value>Не используйте небезопасный адрес подписки по протоколу HTTP</value> <value>Не используйте небезопасный адрес подписки по протоколу HTTP</value>
</data> </data>
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve"> <data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
<value>Установите шрифт в систему, выберите или введите имя шрифта, перезапустите настройки</value> <value>Установите шрифт в систему, выберите или введите имя шрифта и перезапустите приложение</value>
</data> </data>
<data name="menuExitTips" xml:space="preserve"> <data name="menuExitTips" xml:space="preserve">
<value>Вы уверены, что хотите выйти?</value> <value>Вы уверены, что хотите выйти?</value>
@@ -1324,7 +1357,7 @@
<value>Сворачивать в трей при закрытии окна</value> <value>Сворачивать в трей при закрытии окна</value>
</data> </data>
<data name="TbSettingsMixedConcurrencyCount" xml:space="preserve"> <data name="TbSettingsMixedConcurrencyCount" xml:space="preserve">
<value>Количество одновременно выполняемых тестов при многоэтапном тестировании</value> <value>Количество параллельных задач при многопоточном тестировании</value>
</data> </data>
<data name="TbSettingsExceptionTip2" xml:space="preserve"> <data name="TbSettingsExceptionTip2" xml:space="preserve">
<value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value> <value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value>
@@ -1336,7 +1369,7 @@
<value>Включить второй смешанный порт</value> <value>Включить второй смешанный порт</value>
</data> </data>
<data name="TbRoutingInboundTagTips" xml:space="preserve"> <data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value> <value>tun: входящий TUN, socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
</data> </data>
<data name="TbSettingsTheme" xml:space="preserve"> <data name="TbSettingsTheme" xml:space="preserve">
<value>Тема</value> <value>Тема</value>
@@ -1357,7 +1390,7 @@
<value>Удалено {0} недействительных</value> <value>Удалено {0} недействительных</value>
</data> </data>
<data name="TbPorts7" xml:space="preserve"> <data name="TbPorts7" xml:space="preserve">
<value>Диапазон портов сервера</value> <value>Диапазон портов для переключения (Port Hopping)</value>
</data> </data>
<data name="TbPorts7Tips" xml:space="preserve"> <data name="TbPorts7Tips" xml:space="preserve">
<value>Заменит указанный порт, перечисляйте через запятую (,)</value> <value>Заменит указанный порт, перечисляйте через запятую (,)</value>
@@ -1369,7 +1402,7 @@
<value>URL для тестирования текущего соединения</value> <value>URL для тестирования текущего соединения</value>
</data> </data>
<data name="TbRuleOutboundTagTip" xml:space="preserve"> <data name="TbRuleOutboundTagTip" xml:space="preserve">
<value>Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально</value> <value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
</data> </data>
<data name="SudoIncorrectPasswordTip" xml:space="preserve"> <data name="SudoIncorrectPasswordTip" xml:space="preserve">
<value>Неверный пароль, попробуйте ещё раз.</value> <value>Неверный пароль, попробуйте ещё раз.</value>
@@ -1384,7 +1417,7 @@
<value>Удалённый DNS</value> <value>Удалённый DNS</value>
</data> </data>
<data name="TbDomesticDNS" xml:space="preserve"> <data name="TbDomesticDNS" xml:space="preserve">
<value>Внутренний DNS</value> <value>DNS для прямых подключений</value>
</data> </data>
<data name="TbDirectResolveStrategy" xml:space="preserve"> <data name="TbDirectResolveStrategy" xml:space="preserve">
<value>Стратегия разрешения прямых соединений</value> <value>Стратегия разрешения прямых соединений</value>
@@ -1534,7 +1567,7 @@
<value>Bootstrap DNS</value> <value>Bootstrap DNS</value>
</data> </data>
<data name="TbBootstrapDNSTips" xml:space="preserve"> <data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Разрешает домены DNS-серверов, требуется IP-адрес</value> <value>Для разрешения доменных имён DNS-серверов необходимо указать IP-адрес</value>
</data> </data>
<data name="menuFastRealPing" xml:space="preserve"> <data name="menuFastRealPing" xml:space="preserve">
<value>Тест реальной задержки</value> <value>Тест реальной задержки</value>
@@ -1547,7 +1580,7 @@
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Привязанный сертификат (заполните любое из полей) <value>Привязанный сертификат (заполните любое из полей)
При указании сертификат будет привязан, а «Разрешить небезопасные» отключится. При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value> Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
</data> </data>
@@ -1698,12 +1731,6 @@
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>Дополнительные параметры XHTTP (Extra)</value> <value>Дополнительные параметры XHTTP (Extra)</value>
</data> </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"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Тест UDP-задержки конфигураций</value> <value>Тест UDP-задержки конфигураций</value>
</data> </data>
@@ -1716,14 +1743,11 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value> <value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Привязать интерфейс</value> <value>Привязать интерфейс</value>
</data> </data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve"> <data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value> <value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value>
</data> </data>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>Общий ключ (PSK)</value> <value>Общий ключ (PSK)</value>
@@ -1732,12 +1756,18 @@
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value> <value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
</data> </data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve"> <data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value> <value>Доступна новая версия {0}: {1}</value>
</data> </data>
<data name="menuCheckOnly" xml:space="preserve"> <data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value> <value>Проверить</value>
</data> </data>
<data name="MsgNotSupport" xml:space="preserve"> <data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value> <value>Не поддерживается</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>Информация об IP</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>Доступно обновление</value>
</data> </data>
</root> </root>
+82 -10
View File
@@ -1030,7 +1030,7 @@
<value>添加 [Hysteria2]</value> <value>添加 [Hysteria2]</value>
</data> </data>
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve"> <data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
<value>Hysteria 最大带宽 (Up/Dw)</value> <value>Hysteria 最大带宽 (上传/下载)</value>
</data> </data>
<data name="TbSettingsUseSystemHosts" xml:space="preserve"> <data name="TbSettingsUseSystemHosts" xml:space="preserve">
<value>使用系统 hosts</value> <value>使用系统 hosts</value>
@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>启用分片 (Fragment)</value> <value>启用分片 (Fragment)</value>
</data> </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"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>启用 sing-box (规则集文件) 的缓存文件</value> <value>启用 sing-box (规则集文件) 的缓存文件</value>
</data> </data>
@@ -1695,12 +1728,6 @@
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </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"> <data name="menuUdpTestServer" xml:space="preserve">
<value>测试 UDP 延迟 (多选)</value> <value>测试 UDP 延迟 (多选)</value>
</data> </data>
@@ -1713,9 +1740,6 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>用于多网口环境,请填写本机 IPv4 地址</value> <value>用于多网口环境,请填写本机 IPv4 地址</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>请填写正确的 SendThrough IPv4 地址。</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>绑定网口</value> <value>绑定网口</value>
</data> </data>
@@ -1737,4 +1761,52 @@
<data name="MsgNotSupport" xml:space="preserve"> <data name="MsgNotSupport" xml:space="preserve">
<value>不支持</value> <value>不支持</value>
</data> </data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP 信息</value>
</data>
<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="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>
</root> </root>
+67 -10
View File
@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>啟用分片(Fragment)</value> <value>啟用分片(Fragment)</value>
</data> </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"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>啟用 sing-box(規則集檔案)的快取檔案</value> <value>啟用 sing-box(規則集檔案)的快取檔案</value>
</data> </data>
@@ -1333,7 +1366,7 @@
<value>開啟第二個本機監聽埠</value> <value>開啟第二個本機監聽埠</value>
</data> </data>
<data name="TbRoutingInboundTagTips" xml:space="preserve"> <data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks:本地埠,socks2:第二個本地埠,socks3:區域網路埠</value> <value>Tun:TUN 入站,Socks:本機連接埠,Socks2:第二個本機連接埠,Socks3:區域網路連接埠</value>
</data> </data>
<data name="TbSettingsTheme" xml:space="preserve"> <data name="TbSettingsTheme" xml:space="preserve">
<value>主題</value> <value>主題</value>
@@ -1695,12 +1728,6 @@
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </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"> <data name="menuUdpTestServer" xml:space="preserve">
<value>測試 UDP 延遲(多選)</value> <value>測試 UDP 延遲(多選)</value>
</data> </data>
@@ -1713,9 +1740,6 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>適用於多網路介面環境,請填寫本機 IPv4 位址</value> <value>適用於多網路介面環境,請填寫本機 IPv4 位址</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>請填寫正確的 SendThrough IPv4 位址。</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>綁定網路介面</value> <value>綁定網路介面</value>
</data> </data>
@@ -1737,4 +1761,37 @@
<data name="MsgNotSupport" xml:space="preserve"> <data name="MsgNotSupport" xml:space="preserve">
<value>不支援</value> <value>不支援</value>
</data> </data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP 資訊</value>
</data>
<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>
</root> </root>
+1 -9
View File
@@ -17,14 +17,6 @@
], ],
"routing": { "routing": {
"domainStrategy": "IPIfNonMatch", "domainStrategy": "IPIfNonMatch",
"rules": [ "rules": []
{
"inboundTag": [
"api"
],
"outboundTag": "api",
"type": "field"
}
]
} }
} }
@@ -8,6 +8,9 @@
"172.18.0.1/30", "172.18.0.1/30",
"fdfe:dcba:9876::1/126" "fdfe:dcba:9876::1/126"
], ],
"dns": [
"172.18.0.1"
],
"autoSystemRoutingTable": [ "autoSystemRoutingTable": [
"0.0.0.0/0", "0.0.0.0/0",
"::/0" "::/0"
+5 -1
View File
@@ -6,6 +6,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Downloader" /> <PackageReference Include="Downloader" />
<PackageReference Include="IPNetwork2" />
<PackageReference Include="ReactiveUI"> <PackageReference Include="ReactiveUI">
<TreatAsUsed>true</TreatAsUsed> <TreatAsUsed>true</TreatAsUsed>
</PackageReference> </PackageReference>
@@ -56,7 +57,7 @@
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
</Compile> </Compile>
<EmbeddedResource Update="Resx\ResUI.fa-Ir.resx"> <EmbeddedResource Update="Resx\ResUI.fa.resx">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>PublicResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource> </EmbeddedResource>
@@ -66,6 +67,9 @@
<EmbeddedResource Update="Resx\ResUI.hu.resx"> <EmbeddedResource Update="Resx\ResUI.hu.resx">
<Generator>PublicResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.id.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.resx"> <EmbeddedResource Update="Resx\ResUI.resx">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<LastGenOutput>ResUI.Designer.cs</LastGenOutput> <LastGenOutput>ResUI.Designer.cs</LastGenOutput>
@@ -3,16 +3,10 @@ namespace ServiceLib.Services.CoreConfig;
/// <summary> /// <summary>
/// Core configuration file processing class /// Core configuration file processing class
/// </summary> /// </summary>
public class CoreConfigClashService public class CoreConfigClashService(Config config)
{ {
private Config _config;
private static readonly string _tag = "CoreConfigClashService"; private static readonly string _tag = "CoreConfigClashService";
public CoreConfigClashService(Config config)
{
_config = config;
}
public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName) public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName)
{ {
var ret = new RetResult(); var ret = new RetResult();
@@ -56,7 +50,7 @@ public class CoreConfigClashService
var tagYamlStr1 = "!<str>"; var tagYamlStr1 = "!<str>";
var tagYamlStr2 = "__strn__"; var tagYamlStr2 = "__strn__";
var tagYamlStr3 = "!!str"; var tagYamlStr3 = "!!str";
var txtFile = File.ReadAllText(addressFileName); var txtFile = await File.ReadAllTextAsync(addressFileName);
txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2); txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2);
//YAML anchors //YAML anchors
@@ -75,13 +69,13 @@ public class CoreConfigClashService
//mixed-port //mixed-port
fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
//log-level //log-level
fileContent["log-level"] = GetLogLevel(_config.CoreBasicItem.Loglevel); fileContent["log-level"] = GetLogLevel(config.CoreBasicItem.Loglevel);
//external-controller //external-controller
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}"; fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
fileContent.Remove("secret"); fileContent.Remove("secret");
//allow-lan //allow-lan
if (_config.Inbound.First().AllowLANConn) if (config.Inbound.First().AllowLANConn)
{ {
fileContent["allow-lan"] = "true"; fileContent["allow-lan"] = "true";
fileContent["bind-address"] = "*"; fileContent["bind-address"] = "*";
@@ -92,23 +86,23 @@ public class CoreConfigClashService
} }
//ipv6 //ipv6
fileContent["ipv6"] = _config.ClashUIItem.EnableIPv6; fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
//mode //mode
if (!fileContent.ContainsKey("mode")) if (!fileContent.ContainsKey("mode"))
{ {
fileContent["mode"] = ERuleMode.Rule.ToString().ToLower(); fileContent["mode"] = nameof(ERuleMode.Rule).ToLower();
} }
else 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 //enable tun mode
if (_config.TunModeItem.EnableTun) if (config.TunModeItem.EnableTun)
{ {
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml); var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
if (tun.IsNotEmpty()) if (tun.IsNotEmpty())
@@ -156,7 +150,7 @@ public class CoreConfigClashService
private async Task MixinContent(Dictionary<string, object> fileContent, ProfileItem node) private async Task MixinContent(Dictionary<string, object> fileContent, ProfileItem node)
{ {
if (!_config.ClashUIItem.EnableMixinContent) if (!config.ClashUIItem.EnableMixinContent)
{ {
return; return;
} }
@@ -177,7 +171,7 @@ public class CoreConfigClashService
} }
foreach (var item in mixinContent) foreach (var item in mixinContent)
{ {
if (!_config.TunModeItem.EnableTun && item.Key == "tun") if (!config.TunModeItem.EnableTun && item.Key == "tun")
{ {
continue; continue;
} }
@@ -220,9 +214,8 @@ public class CoreConfigClashService
return; return;
} }
if (!blRemoved && !fileContent.ContainsKey(key)) if (!blRemoved && fileContent.TryAdd(key, value))
{ {
fileContent.Add(key, value);
return; return;
} }
var lstOri = (List<object>)fileContent[key]; var lstOri = (List<object>)fileContent[key];
@@ -244,7 +237,7 @@ public class CoreConfigClashService
} }
else else
{ {
lstValue.ForEach(item => lstOri.Add(item)); lstValue.ForEach(lstOri.Add);
} }
} }
@@ -153,7 +153,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
{ {
listen = Global.Loopback, listen = Global.Loopback,
listen_port = port, listen_port = port,
type = EInboundProtocol.mixed.ToString(), type = nameof(EInboundProtocol.mixed),
}; };
inbound.tag = inbound.type + inbound.listen_port.ToString(); inbound.tag = inbound.type + inbound.listen_port.ToString();
_coreConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
@@ -229,7 +229,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
tag = $"{EInboundProtocol.mixed}{port}", tag = $"{EInboundProtocol.mixed}{port}",
listen = Global.Loopback, listen = Global.Loopback,
listen_port = port, listen_port = port,
type = EInboundProtocol.mixed.ToString(), type = nameof(EInboundProtocol.mixed),
}); });
ApplyOutboundBindInterface(); ApplyOutboundBindInterface();
ApplyOutboundSendThrough(); ApplyOutboundSendThrough();
@@ -23,7 +23,7 @@ public partial class CoreConfigSingboxService
} }
// Process outbounds // Process outbounds
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : []; var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
foreach (var outbound in _coreConfig.outbounds) foreach (var outbound in _coreConfig.outbounds)
{ {
if (outbound.type.ToLower() is "direct" or "block") if (outbound.type.ToLower() is "direct" or "block")
@@ -42,9 +42,9 @@ public partial class CoreConfigSingboxService
fullConfigTemplateNode["outbounds"] = customOutboundsNode; fullConfigTemplateNode["outbounds"] = customOutboundsNode;
// Process endpoints // 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) foreach (var endpoint in _coreConfig.endpoints)
{ {
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
@@ -66,10 +66,6 @@ public partial class CoreConfigSingboxService
{ {
return; return;
} }
if (!(context.IsTunEnabled || context.IsWindows))
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? []) foreach (var outbound in _coreConfig.outbounds ?? [])
{ {
outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null; outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
@@ -83,6 +79,7 @@ public partial class CoreConfigSingboxService
{ {
return; return;
} }
foreach (var outbound in _coreConfig.outbounds ?? []) foreach (var outbound in _coreConfig.outbounds ?? [])
{ {
outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null; outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;
@@ -26,7 +26,7 @@ public partial class CoreConfigSingboxService
{ {
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? []; 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 noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0; var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
@@ -82,7 +82,7 @@ public partial class CoreConfigSingboxService
if (simpleDnsItem.UseSystemHosts == true) if (simpleDnsItem.UseSystemHosts == true)
{ {
var systemHosts = Utils.GetSystemHosts(); var systemHosts = Utils.GetSystemHosts();
if (systemHosts != null && systemHosts.Count > 0) if (systemHosts is { Count: > 0 })
{ {
foreach (var host in systemHosts) foreach (var host in systemHosts)
{ {
@@ -182,13 +182,13 @@ public partial class CoreConfigSingboxService
{ {
server = Global.SingboxRemoteDNSTag, server = Global.SingboxRemoteDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy), strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy),
clash_mode = ERuleMode.Global.ToString() clash_mode = nameof(ERuleMode.Global)
}, },
new Rule4Sbox new Rule4Sbox
{ {
server = Global.SingboxDirectDNSTag, server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom), strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
clash_mode = ERuleMode.Direct.ToString() clash_mode = nameof(ERuleMode.Direct)
} }
}); });
@@ -454,12 +454,12 @@ public partial class CoreConfigSingboxService
dns4Sbox.rules.Insert(0, new() dns4Sbox.rules.Insert(0, new()
{ {
server = tag, server = tag,
clash_mode = ERuleMode.Direct.ToString() clash_mode = nameof(ERuleMode.Direct)
}); });
dns4Sbox.rules.Insert(0, new() dns4Sbox.rules.Insert(0, new()
{ {
server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote", 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; var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;
@@ -15,8 +15,8 @@ public partial class CoreConfigSingboxService
{ {
var inbound = new Inbound4Sbox() var inbound = new Inbound4Sbox()
{ {
type = EInboundProtocol.mixed.ToString(), type = nameof(EInboundProtocol.mixed),
tag = EInboundProtocol.socks.ToString(), tag = nameof(EInboundProtocol.socks),
listen = Global.Loopback, listen = Global.Loopback,
}; };
_coreConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
@@ -71,6 +71,7 @@ public partial class CoreConfigSingboxService
{ {
tunInbound.address = ["172.18.0.1/30"]; tunInbound.address = ["172.18.0.1/30"];
} }
tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress;
_coreConfig.inbounds.Add(tunInbound); _coreConfig.inbounds.Add(tunInbound);
} }
@@ -86,7 +87,7 @@ public partial class CoreConfigSingboxService
var inbound = JsonUtils.DeepCopy(inItem); var inbound = JsonUtils.DeepCopy(inItem);
inbound.tag = protocol.ToString(); inbound.tag = protocol.ToString();
inbound.listen_port = inItem.listen_port + (int)protocol; inbound.listen_port = inItem.listen_port + (int)protocol;
inbound.type = EInboundProtocol.mixed.ToString(); inbound.type = nameof(EInboundProtocol.mixed);
return inbound; return inbound;
} }
} }
@@ -190,15 +190,16 @@ public partial class CoreConfigSingboxService
outbound.packet_encoding = "xudp"; 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; outbound.flow = protocolExtra.Flow;
} }
else
{
FillOutboundMux(outbound);
}
FillOutboundMux(outbound);
FillOutboundTransport(outbound); FillOutboundTransport(outbound);
break; break;
} }
@@ -216,11 +217,17 @@ public partial class CoreConfigSingboxService
if (!protocolExtra.SalamanderPass.IsNullOrEmpty()) if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
{ {
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
outbound.obfs = new() outbound.obfs = new()
{ {
type = "salamander", type = isGecko ? "gecko" : "salamander",
password = protocolExtra.SalamanderPass.TrimEx(), 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 int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
? su ? 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; break;
} }
case EConfigType.TUIC: case EConfigType.TUIC:
@@ -342,7 +365,7 @@ public partial class CoreConfigSingboxService
{ {
try try
{ {
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; var muxEnabled = _node.MuxEnabled ?? false;
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty()) if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
{ {
var mux = new Multiplex4Sbox() var mux = new Multiplex4Sbox()
@@ -396,7 +419,7 @@ public partial class CoreConfigSingboxService
enabled = true, enabled = true,
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null, record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
server_name = serverName, server_name = serverName,
insecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure), insecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(), alpn = _node.GetAlpn(),
}; };
if (_node.Fingerprint.IsNotEmpty()) if (_node.Fingerprint.IsNotEmpty())
@@ -729,8 +752,8 @@ public partial class CoreConfigSingboxService
{ {
return; return;
} }
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList(); var outbounds = servers.OfType<Outbound4Sbox>().ToList();
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList(); var endpoints = servers.OfType<Endpoints4Sbox>().ToList();
singboxConfig.endpoints ??= []; singboxConfig.endpoints ??= [];
if (prepend) if (prepend)
{ {
@@ -96,6 +96,15 @@ public partial class CoreConfigSingboxService
new() { protocol = ["dns"] }, new() { protocol = ["dns"] },
], ],
}); });
if (_config.CoreBasicItem.EnableFinalFragment)
{
_coreConfig.route.rules.Add(new()
{
protocol = ["tls"],
action = "route-options",
tls_record_fragment = true,
});
}
} }
else else
{ {
@@ -104,6 +113,14 @@ public partial class CoreConfigSingboxService
port = [53], port = [53],
action = "hijack-dns", action = "hijack-dns",
}); });
if (_config.CoreBasicItem.EnableFinalFragment)
{
_coreConfig.route.rules.Add(new()
{
action = "route-options",
tls_record_fragment = true,
});
}
} }
var hostsDomains = new List<string>(); var hostsDomains = new List<string>();
@@ -176,12 +193,12 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
outbound = Global.DirectTag, outbound = Global.DirectTag,
clash_mode = ERuleMode.Direct.ToString() clash_mode = nameof(ERuleMode.Direct)
}); });
_coreConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
outbound = Global.ProxyTag, outbound = Global.ProxyTag,
clash_mode = ERuleMode.Global.ToString() clash_mode = nameof(ERuleMode.Global)
}); });
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty(); var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty();
@@ -18,15 +18,15 @@ public partial class CoreConfigSingboxService
//convert route geosite & geoip to ruleset //convert route geosite & geoip to ruleset
foreach (var rule in _coreConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? []) foreach (var rule in _coreConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
{ {
rule.rule_set ??= new List<string>(); rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList()); rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList() ?? []);
rule.geosite = null; rule.geosite = null;
AddRuleSets(ruleSets, rule.rule_set); AddRuleSets(ruleSets, rule.rule_set);
} }
foreach (var rule in _coreConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? []) foreach (var rule in _coreConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
{ {
rule.rule_set ??= new List<string>(); rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList()); rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList() ?? []);
rule.geoip = null; rule.geoip = null;
AddRuleSets(ruleSets, rule.rule_set); AddRuleSets(ruleSets, rule.rule_set);
} }
@@ -34,14 +34,14 @@ public partial class CoreConfigSingboxService
//convert dns geosite & geoip to ruleset //convert dns geosite & geoip to ruleset
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? []) foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
{ {
rule.rule_set ??= new List<string>(); rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList()); rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList() ?? []);
rule.geosite = null; rule.geosite = null;
} }
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? []) foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
{ {
rule.rule_set ??= new List<string>(); rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList()); rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList() ?? []);
rule.geoip = null; rule.geoip = null;
} }
foreach (var dnsRule in _coreConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? []) foreach (var dnsRule in _coreConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? [])
@@ -60,6 +60,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{ {
ApplyOutboundFragment(); ApplyOutboundFragment();
} }
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface(); ApplyOutboundBindInterface();
ApplyOutboundSendThrough(); ApplyOutboundSendThrough();
@@ -161,7 +165,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{ {
listen = Global.Loopback, listen = Global.Loopback,
port = port, port = port,
protocol = EInboundProtocol.mixed.ToString(), protocol = nameof(EInboundProtocol.mixed),
settings = new Inboundsettings4Ray() settings = new Inboundsettings4Ray()
{ {
udp = true, udp = true,
@@ -204,6 +208,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{ {
ApplyOutboundFragment(); ApplyOutboundFragment();
} }
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface(); ApplyOutboundBindInterface();
ApplyOutboundSendThrough(); ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary()); //ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
@@ -262,7 +270,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
tag = $"{EInboundProtocol.socks}{port}", tag = $"{EInboundProtocol.socks}{port}",
listen = Global.Loopback, listen = Global.Loopback,
port = port, port = port,
protocol = EInboundProtocol.mixed.ToString(), protocol = nameof(EInboundProtocol.mixed),
settings = new Inboundsettings4Ray() settings = new Inboundsettings4Ray()
{ {
udp = true, udp = true,
@@ -276,6 +284,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{ {
ApplyOutboundFragment(); ApplyOutboundFragment();
} }
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface(); ApplyOutboundBindInterface();
ApplyOutboundSendThrough(); ApplyOutboundSendThrough();
@@ -56,7 +56,7 @@ public partial class CoreConfigV2rayService
} }
else else
{ {
_coreConfig.burstObservatory.subjectSelector ??= new(); _coreConfig.burstObservatory.subjectSelector ??= [];
_coreConfig.burstObservatory.subjectSelector.Add(baseTagName); _coreConfig.burstObservatory.subjectSelector.Add(baseTagName);
} }
} }
@@ -75,7 +75,7 @@ public partial class CoreConfigV2rayService
} }
else else
{ {
_coreConfig.observatory.subjectSelector ??= new(); _coreConfig.observatory.subjectSelector ??= [];
_coreConfig.observatory.subjectSelector.Add(baseTagName); _coreConfig.observatory.subjectSelector.Add(baseTagName);
} }
} }
@@ -89,23 +89,27 @@ public partial class CoreConfigV2rayService
EMultipleLoad.RoundRobin => "roundRobin", EMultipleLoad.RoundRobin => "roundRobin",
EMultipleLoad.LeastPing => "leastPing", EMultipleLoad.LeastPing => "leastPing",
EMultipleLoad.LeastLoad => "leastLoad", EMultipleLoad.LeastLoad => "leastLoad",
EMultipleLoad.Fallback => "leastLoad",
_ => "roundRobin", _ => "roundRobin",
}; };
var balancerTag = $"{selector}{Global.BalancerTagSuffix}"; var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
var balancer = new BalancersItem4Ray var balancer = new BalancersItem4Ray
{ {
selector = [selector], selector = [selector],
strategy = new() strategy = strategyType == "leastLoad" ? new()
{ {
type = strategyType, type = strategyType,
settings = new() settings = new()
{ {
expected = 1, expected = 1,
tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null,
maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null,
}, },
}, } : null,
tag = balancerTag, tag = balancerTag,
fallbackTag = _coreConfig.outbounds?.FirstOrDefault(o => o.tag.StartsWith(selector))?.tag,
}; };
_coreConfig.routing.balancers ??= new(); _coreConfig.routing.balancers ??= [];
_coreConfig.routing.balancers.Add(balancer); _coreConfig.routing.balancers.Add(balancer);
} }
} }
@@ -46,10 +46,7 @@ public partial class CoreConfigV2rayService
} }
// Ensure routing node exists // Ensure routing node exists
if (fullConfigTemplateNode["routing"] == null) fullConfigTemplateNode["routing"] ??= new JsonObject();
{
fullConfigTemplateNode["routing"] = new JsonObject();
}
// Handle balancers - append instead of override // Handle balancers - append instead of override
if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode) if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode)
@@ -141,10 +138,6 @@ public partial class CoreConfigV2rayService
{ {
return; return;
} }
if (!(context.IsTunEnabled || context.IsWindows))
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? []) foreach (var outbound in _coreConfig.outbounds ?? [])
{ {
if (!ShouldBindNet(outbound)) if (!ShouldBindNet(outbound))
@@ -179,6 +172,7 @@ public partial class CoreConfigV2rayService
{ {
return; return;
} }
foreach (var outbound in _coreConfig.outbounds ?? []) foreach (var outbound in _coreConfig.outbounds ?? [])
{ {
outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null; outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null;
@@ -34,7 +34,7 @@ public partial class CoreConfigV2rayService
return; return;
} }
var simpleDnsItem = context.SimpleDnsItem; var simpleDnsItem = context.SimpleDnsItem;
var dnsItem = _coreConfig.dns is Dns4Ray dns4Ray ? dns4Ray : new Dns4Ray(); var dnsItem = _coreConfig.dns as Dns4Ray ?? new Dns4Ray();
var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs; var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs;
//Outbound Freedom domainStrategy //Outbound Freedom domainStrategy
@@ -246,7 +246,7 @@ public partial class CoreConfigV2rayService
var useDirectDns = false; var useDirectDns = false;
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 noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0; var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
@@ -384,9 +384,11 @@ public partial class CoreConfigV2rayService
var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
if (outbound != null) if (outbound != null)
{ {
outbound.settings = new(); outbound.settings = new()
outbound.settings.domainStrategy = domainStrategy4Freedom; {
outbound.settings.userLevel = 0; domainStrategy = domainStrategy4Freedom,
userLevel = 0,
};
} }
} }
@@ -34,10 +34,15 @@ public partial class CoreConfigV2rayService
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
{ {
inbound3.settings.auth = "password"; inbound3.settings.auth = "password";
inbound3.settings.accounts = new List<AccountsItem4Ray> inbound3.settings.accounts =
[
new()
{ {
new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } user = _config.Inbound.First().User,
}; pass = _config.Inbound.First().Pass,
},
];
} }
} }
else else
@@ -53,12 +58,15 @@ public partial class CoreConfigV2rayService
{ {
_config.TunModeItem.Mtu = Global.TunMtus.First(); _config.TunModeItem.Mtu = Global.TunMtus.First();
} }
var tunInbound = JsonUtils.Deserialize<Inbounds4Ray>(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? new Inbounds4Ray { }; var tunInbound =
JsonUtils.Deserialize<Inbounds4Ray>(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ??
new Inbounds4Ray();
tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun"; tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun";
tunInbound.settings.MTU = _config.TunModeItem.Mtu; tunInbound.settings.MTU = _config.TunModeItem.Mtu;
if (_config.TunModeItem.EnableIPv6Address == false) if (!_config.TunModeItem.EnableIPv6Address)
{ {
tunInbound.settings.gateway = ["172.18.0.1/30"]; tunInbound.settings.gateway = ["172.18.0.1/30"];
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
} }
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (!bindInterface.IsNullOrEmpty()) if (!bindInterface.IsNullOrEmpty())
@@ -66,6 +74,53 @@ public partial class CoreConfigV2rayService
tunInbound.settings.autoOutboundsInterface = bindInterface; tunInbound.settings.autoOutboundsInterface = bindInterface;
} }
tunInbound.sniffing = inbound.sniffing; tunInbound.sniffing = inbound.sniffing;
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{
var wholeInternet = IPNetwork2.Parse("0.0.0.0/0");
var wholeInternetV6 = IPNetwork2.Parse("::/0");
var excludeList = _config.TunModeItem.RouteExcludeAddress.Select(IPNetwork2.Parse)
.Where(x => x != null).ToList();
var includeList = new List<IPNetwork2> { wholeInternet };
var includeListV6 = new List<IPNetwork2> { wholeInternetV6 };
foreach (var exclude in excludeList)
{
var temp = new List<IPNetwork2>();
if (exclude.AddressFamily == AddressFamily.InterNetwork)
{
foreach (var net in includeList)
{
temp.AddRange(net.Subtract(exclude));
}
includeList = temp;
}
else if (exclude.AddressFamily == AddressFamily.InterNetworkV6)
{
foreach (var net in includeListV6)
{
temp.AddRange(net.Subtract(exclude));
}
includeListV6 = temp;
}
}
includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList();
includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList();
if (_config.TunModeItem.EnableIPv6Address)
{
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
.Concat(includeListV6.Select(x => x.ToString())).ToList();
}
else
{
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList();
}
}
_coreConfig.inbounds.Add(tunInbound); _coreConfig.inbounds.Add(tunInbound);
} }
} }
@@ -90,7 +145,7 @@ public partial class CoreConfigV2rayService
} }
inbound.tag = protocol.ToString(); inbound.tag = protocol.ToString();
inbound.port = inItem.LocalPort + (int)protocol; inbound.port = inItem.LocalPort + (int)protocol;
inbound.protocol = EInboundProtocol.mixed.ToString(); inbound.protocol = nameof(EInboundProtocol.mixed);
inbound.settings.udp = inItem.UdpEnabled; inbound.settings.udp = inItem.UdpEnabled;
inbound.sniffing.enabled = inItem.SniffingEnabled; inbound.sniffing.enabled = inItem.SniffingEnabled;
inbound.sniffing.destOverride = inItem.DestOverride; inbound.sniffing.destOverride = inItem.DestOverride;
@@ -62,7 +62,7 @@ public partial class CoreConfigV2rayService
try try
{ {
var protocolExtra = _node.GetProtocolExtra(); var protocolExtra = _node.GetProtocolExtra();
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; var muxEnabled = _node.MuxEnabled ?? false;
switch (_node.ConfigType) switch (_node.ConfigType)
{ {
case EConfigType.VMess: case EConfigType.VMess:
@@ -243,9 +243,9 @@ public partial class CoreConfigV2rayService
version = 2, version = 2,
address = _node.Address, address = _node.Address,
port = _node.Port, port = _node.Port,
vnext = null,
servers = null,
}; };
outbound.settings.vnext = null;
outbound.settings.servers = null;
break; break;
} }
case EConfigType.WireGuard: case EConfigType.WireGuard:
@@ -380,10 +380,11 @@ public partial class CoreConfigV2rayService
TlsSettings4Ray tlsSettings = new() TlsSettings4Ray tlsSettings = new()
{ {
allowInsecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure), allowInsecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(), alpn = _node.GetAlpn(),
fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint, fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint,
echConfigList = _node.EchConfigList.NullIfEmpty(), echConfigList = _node.EchConfigList.NullIfEmpty(),
verifyPeerCertByName = _node.VerifyPeerCertByName.NullIfEmpty(),
}; };
if (sni.IsNotEmpty()) if (sni.IsNotEmpty())
{ {
@@ -449,14 +450,13 @@ public partial class CoreConfigV2rayService
KcpSettings4Ray kcpSettings = new() KcpSettings4Ray kcpSettings = new()
{ {
mtu = kcpMtu, mtu = kcpMtu,
tti = _config.KcpItem.Tti tti = _config.KcpItem.Tti,
uplinkCapacity = _config.KcpItem.UplinkCapacity,
downlinkCapacity = _config.KcpItem.DownlinkCapacity,
cwndMultiplier = _config.KcpItem.CwndMultiplier,
maxSendingWindow = _config.KcpItem.MaxSendingWindow,
}; };
kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity;
kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity;
kcpSettings.cwndMultiplier = _config.KcpItem.CwndMultiplier;
kcpSettings.maxSendingWindow = _config.KcpItem.MaxSendingWindow;
var kcpFinalmask = new Finalmask4Ray(); var kcpFinalmask = new Finalmask4Ray();
if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header)) if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header))
{ {
@@ -464,8 +464,8 @@ public partial class CoreConfigV2rayService
[ [
new Mask4Ray new Mask4Ray
{ {
type = header, type = "mkcp-legacy",
settings = null settings = new MaskSettings4Ray { header = header },
} }
]; ];
} }
@@ -474,15 +474,15 @@ public partial class CoreConfigV2rayService
{ {
kcpFinalmask.udp.Add(new Mask4Ray kcpFinalmask.udp.Add(new Mask4Ray
{ {
type = "mkcp-original" type = "mkcp-legacy",
}); });
} }
else else
{ {
kcpFinalmask.udp.Add(new Mask4Ray kcpFinalmask.udp.Add(new Mask4Ray
{ {
type = "mkcp-aes128gcm", type = "mkcp-legacy",
settings = new MaskSettings4Ray { password = kcpSeed } settings = new MaskSettings4Ray { value = kcpSeed },
}); });
} }
streamSettings.kcpSettings = kcpSettings; streamSettings.kcpSettings = kcpSettings;
@@ -530,7 +530,7 @@ public partial class CoreConfigV2rayService
break; break;
//xhttp //xhttp
case nameof(ETransport.xhttp): case nameof(ETransport.xhttp):
streamSettings.network = ETransport.xhttp.ToString(); streamSettings.network = nameof(ETransport.xhttp);
XhttpSettings4Ray xhttpSettings = new(); XhttpSettings4Ray xhttpSettings = new();
if (path.IsNotEmpty()) if (path.IsNotEmpty())
@@ -607,16 +607,32 @@ public partial class CoreConfigV2rayService
quicParams.congestion = "bbr"; quicParams.congestion = "bbr";
} }
hy2Finalmask.quicParams = quicParams; hy2Finalmask.quicParams = quicParams;
hy2Finalmask.udp ??= [];
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
&& realm is not null)
{
hy2Finalmask.udp.Add(new Mask4Ray
{
type = "realm",
settings = new MaskSettings4Ray { url = realm.ToUriForFinalmask(), stunServers = realm.StunList },
});
}
if (!protocolExtra.SalamanderPass.IsNullOrEmpty()) if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
{ {
hy2Finalmask.udp = var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
[ var salamanderSettings = new MaskSettings4Ray
new Mask4Ray {
password = protocolExtra.SalamanderPass.TrimEx(),
};
if (isGecko)
{
salamanderSettings.packetSize = $"{protocolExtra.GeckoMinPacketSize}-{protocolExtra.GeckoMaxPacketSize}";
}
hy2Finalmask.udp.Add(new Mask4Ray
{ {
type = "salamander", type = "salamander",
settings = new MaskSettings4Ray { password = protocolExtra.SalamanderPass.TrimEx(), } settings = salamanderSettings,
} });
];
} }
streamSettings.hysteriaSettings = new() streamSettings.hysteriaSettings = new()
{ {
@@ -825,29 +841,7 @@ public partial class CoreConfigV2rayService
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)) && (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
.ToList(); .ToList();
var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello"; var (fragmentMask, noiseMask) = BuildFragmentsMasks();
var configLength = _config.Fragment4RayItem?.Length ?? "50-100";
var configDelay = _config.Fragment4RayItem?.Interval ?? "10-20";
var fragmentMask = new Mask4Ray
{
type = "fragment",
settings = new MaskSettings4Ray
{
packets = configPackets,
length = configLength,
delay = configDelay,
}
};
var noiseMask = new Mask4Ray
{
type = "noise",
settings = new MaskSettings4Ray
{
length = "10-20",
delay = "10-16",
}
};
foreach (var outbound in actOutboundWithTlsList) foreach (var outbound in actOutboundWithTlsList)
{ {
@@ -881,4 +875,76 @@ public partial class CoreConfigV2rayService
outbound.streamSettings.finalmask = finalMaskJsonObj; outbound.streamSettings.finalmask = finalMaskJsonObj;
} }
} }
private void ApplyFinalFragment()
{
var (fragmentMask, noiseMask) = BuildFragmentsMasks();
var actOutboundList = _coreConfig.outbounds.Where(n => n.tag.StartsWith(Global.ProxyTag)).ToList();
var fragmentFreedom = new Outbounds4Ray()
{
tag = $"{Global.ProxyTag}-fragment-freedom",
protocol = "freedom",
streamSettings = new StreamSettings4Ray
{
finalmask = new Finalmask4Ray
{
tcp = [fragmentMask],
udp = [noiseMask],
}
}
};
foreach (var outbound in actOutboundList)
{
var index = _coreConfig.outbounds.IndexOf(outbound);
var originalTag = outbound.tag;
var afterTag = $"fragment-{originalTag}";
var cloneFragmentFreedom = JsonUtils.DeepCopy(fragmentFreedom);
cloneFragmentFreedom.tag = originalTag;
cloneFragmentFreedom.streamSettings.sockopt ??= new();
cloneFragmentFreedom.streamSettings.sockopt.dialerProxy = afterTag;
outbound.tag = afterTag;
_coreConfig.outbounds.Insert(index, cloneFragmentFreedom);
}
}
private (Mask4Ray tcpFragment, Mask4Ray udpNoise) BuildFragmentsMasks()
{
var configPackets = _config.Fragment4RayItem?.Packets.NullIfEmpty() ?? "tlshello";
var configLength = _config.Fragment4RayItem?.Length.NullIfEmpty() ?? "50-100";
var configDelay = _config.Fragment4RayItem?.Interval.NullIfEmpty() ?? "10-20";
var configMaxSplit = _config.Fragment4RayItem?.MaxSplit.NullIfEmpty() ?? "0";
var maxSplit = 0;
var parts = configMaxSplit.Split('-');
if (parts.Length > 0 && int.TryParse(parts[0], out var ms))
{
maxSplit = ms;
}
var fragmentMask = new Mask4Ray
{
type = "fragment",
settings = new MaskSettings4Ray
{
packets = configPackets,
length = configLength,
delay = configDelay,
maxSplit = maxSplit,
}
};
var noiseMask = new Mask4Ray
{
type = "noise",
settings = new MaskSettings4Ray
{
length = "10-20",
delay = "10-16",
}
};
return (fragmentMask, noiseMask);
}
} }
@@ -6,45 +6,19 @@ public partial class CoreConfigV2rayService
{ {
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{ {
var tag = EInboundProtocol.api.ToString(); Metrics4Ray metricsObj = new();
Metrics4Ray apiObj = new();
Policy4Ray policyObj = new(); Policy4Ray policyObj = new();
SystemPolicy4Ray policySystemSetting = new(); SystemPolicy4Ray policySystemSetting = new();
_coreConfig.stats = new Stats4Ray(); _coreConfig.stats = new Stats4Ray();
apiObj.tag = tag; metricsObj.listen = $"{Global.Loopback}:{AppManager.Instance.StatePort}";
_coreConfig.metrics = apiObj; _coreConfig.metrics = metricsObj;
policySystemSetting.statsOutboundDownlink = true; policySystemSetting.statsOutboundDownlink = true;
policySystemSetting.statsOutboundUplink = true; policySystemSetting.statsOutboundUplink = true;
policyObj.system = policySystemSetting; policyObj.system = policySystemSetting;
_coreConfig.policy = policyObj; _coreConfig.policy = policyObj;
if (!_coreConfig.inbounds.Exists(item => item.tag == tag))
{
Inbounds4Ray apiInbound = new();
Inboundsettings4Ray apiInboundSettings = new();
apiInbound.tag = tag;
apiInbound.listen = Global.Loopback;
apiInbound.port = AppManager.Instance.StatePort;
apiInbound.protocol = Global.InboundAPIProtocol;
apiInboundSettings.address = Global.Loopback;
apiInbound.settings = apiInboundSettings;
_coreConfig.inbounds.Add(apiInbound);
}
if (!_coreConfig.routing.rules.Exists(item => item.outboundTag == tag))
{
RulesItem4Ray apiRoutingRule = new()
{
inboundTag = new List<string> { tag },
outboundTag = tag,
type = "field"
};
_coreConfig.routing.rules.Add(apiRoutingRule);
}
} }
} }
} }
+34 -12
View File
@@ -13,7 +13,10 @@ public class DownloadService
private static readonly string _tag = "DownloadService"; private static readonly string _tag = "DownloadService";
public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc) /// <summary>
/// Downloads data with the specified proxy and reports progress messages.
/// </summary>
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
{ {
try try
{ {
@@ -36,6 +39,9 @@ public class DownloadService
return 0; return 0;
} }
/// <summary>
/// Downloads a file and reports progress through events.
/// </summary>
public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout) public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout)
{ {
try try
@@ -64,6 +70,9 @@ public class DownloadService
} }
} }
/// <summary>
/// Gets redirect target URL without following redirects automatically.
/// </summary>
public async Task<string?> UrlRedirectAsync(string url, bool blProxy) public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
{ {
var webRequestHandler = new SocketsHttpHandler var webRequestHandler = new SocketsHttpHandler
@@ -86,11 +95,23 @@ public class DownloadService
} }
} }
/// <summary>
/// Tries to download string content using proxy switch setting.
/// </summary>
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent) public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
{
var webProxy = await GetWebProxy(blProxy);
return await TryDownloadString(url, webProxy, userAgent);
}
/// <summary>
/// Tries to download string content with a specified proxy.
/// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
{ {
try try
{ {
var result1 = await DownloadStringAsync(url, blProxy, userAgent, 15); var result1 = await DownloadStringAsync(url, webProxy, userAgent, 15);
if (result1.IsNotEmpty()) if (result1.IsNotEmpty())
{ {
return result1; return result1;
@@ -108,7 +129,7 @@ public class DownloadService
try try
{ {
var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15); var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, 15);
if (result2.IsNotEmpty()) if (result2.IsNotEmpty())
{ {
return result2; return result2;
@@ -128,14 +149,12 @@ public class DownloadService
} }
/// <summary> /// <summary>
/// DownloadString /// Downloads string content via HttpClient.
/// </summary> /// </summary>
/// <param name="url"></param> private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout)
{ {
try try
{ {
var webProxy = await GetWebProxy(blProxy);
var client = new HttpClient(new SocketsHttpHandler() var client = new HttpClient(new SocketsHttpHandler()
{ {
Proxy = webProxy, Proxy = webProxy,
@@ -172,15 +191,12 @@ public class DownloadService
} }
/// <summary> /// <summary>
/// DownloadString /// Downloads string content via DownloaderHelper.
/// </summary> /// </summary>
/// <param name="url"></param> private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringViaDownloader(string url, bool blProxy, string userAgent, int timeout)
{ {
try try
{ {
var webProxy = await GetWebProxy(blProxy);
if (userAgent.IsNullOrEmpty()) if (userAgent.IsNullOrEmpty())
{ {
userAgent = Utils.GetVersion(false); userAgent = Utils.GetVersion(false);
@@ -200,6 +216,9 @@ public class DownloadService
return null; return null;
} }
/// <summary>
/// Creates local SOCKS proxy when proxy switch is enabled.
/// </summary>
private async Task<WebProxy?> GetWebProxy(bool blProxy) private async Task<WebProxy?> GetWebProxy(bool blProxy)
{ {
if (!blProxy) if (!blProxy)
@@ -215,6 +234,9 @@ public class DownloadService
return new WebProxy($"socks5://{Global.Loopback}:{port}"); return new WebProxy($"socks5://{Global.Loopback}:{port}");
} }
/// <summary>
/// Checks whether the specified TCP endpoint is reachable.
/// </summary>
private async Task<bool> SocketCheck(string ip, int port) private async Task<bool> SocketCheck(string ip, int port)
{ {
try try
+62 -16
View File
@@ -7,7 +7,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private static readonly string _tag = "SpeedtestService"; private static readonly string _tag = "SpeedtestService";
private readonly Config? _config = config; private readonly Config? _config = config;
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc; private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private static readonly ConcurrentBag<string> _lstExitLoop = new(); private static readonly ConcurrentBag<string> _lstExitLoop = [];
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds) public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
{ {
@@ -31,7 +33,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private static bool ShouldStopTest(string exitLoopKey) private static bool ShouldStopTest(string exitLoopKey)
{ {
return !_lstExitLoop.Any(p => p == exitLoopKey); return _lstExitLoop.All(p => p != exitLoopKey);
} }
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds) private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds)
@@ -44,7 +46,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
switch (actionType) switch (actionType)
{ {
case ESpeedActionType.Tcping: case ESpeedActionType.Tcping:
await RunTcpingAsync(lstSelected); await RunTcpingAsync(lstSelected, exitLoopKey);
break; break;
case ESpeedActionType.Realping: case ESpeedActionType.Realping:
@@ -133,11 +135,28 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return lstSelected; return lstSelected;
} }
private async Task RunTcpingAsync(List<ServerTestItem> selecteds) private async Task RunTcpingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
{ {
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
var lstBatch = GetTestBatchItem(selecteds, pageSize);
foreach (var lst in lstBatch)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
List<Task> tasks = []; List<Task> tasks = [];
foreach (var it in selecteds)
foreach (var it in lst)
{ {
if (ShouldStopTest(exitLoopKey))
{
return;
}
tasks.Add(Task.Run(async () => tasks.Add(Task.Run(async () =>
{ {
try try
@@ -153,18 +172,27 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
})); }));
} }
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
if (ShouldStopTest(exitLoopKey))
{
return;
}
await Task.Delay(_delayInterval);
}
} }
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0) private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
{ {
if (pageSize <= 0) if (pageSize <= 0)
{ {
pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize; pageSize = Math.Min(lstSelected.Count, _speedTestPageSize);
} }
var lstTest = GetTestBatchItem(lstSelected, pageSize); var lstTest = GetTestBatchItem(lstSelected, pageSize);
List<ServerTestItem> lstFailed = new(); List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest) foreach (var lst in lstTest)
{ {
var ret = await RunRealPingAsync(lst, exitLoopKey); var ret = await RunRealPingAsync(lst, exitLoopKey);
@@ -172,7 +200,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{ {
lstFailed.AddRange(lst); lstFailed.AddRange(lst);
} }
await Task.Delay(100); await Task.Delay(_delayInterval);
} }
//Retest the failed part //Retest the failed part
@@ -210,7 +238,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
await Task.Delay(1000); await Task.Delay(1000);
List<Task> tasks = new(); List<Task> tasks = [];
foreach (var it in selecteds) foreach (var it in selecteds)
{ {
if (!it.AllowTest) if (!it.AllowTest)
@@ -249,11 +277,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{ {
if (pageSize <= 0) if (pageSize <= 0)
{ {
pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize; pageSize = Math.Min(lstSelected.Count, _speedTestPageSize);
} }
var lstTest = GetTestBatchItem(lstSelected, pageSize); var lstTest = GetTestBatchItem(lstSelected, pageSize);
List<ServerTestItem> lstFailed = new(); List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest) foreach (var lst in lstTest)
{ {
var ret = await RunUdpTestAsync(lst, exitLoopKey); var ret = await RunUdpTestAsync(lst, exitLoopKey);
@@ -261,7 +289,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{ {
lstFailed.AddRange(lst); lstFailed.AddRange(lst);
} }
await Task.Delay(100); await Task.Delay(_delayInterval);
} }
//Retest the failed part //Retest the failed part
@@ -291,7 +319,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
await Task.Delay(1000); await Task.Delay(1000);
List<Task> tasks = new(); List<Task> tasks = [];
foreach (var it in selecteds) foreach (var it in selecteds)
{ {
if (!it.AllowTest) if (!it.AllowTest)
@@ -329,7 +357,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{ {
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount); using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
var downloadHandle = new DownloadService(); var downloadHandle = new DownloadService();
List<Task> tasks = new(); List<Task> tasks = [];
foreach (var it in selecteds) foreach (var it in selecteds)
{ {
if (ShouldStopTest(exitLoopKey)) if (ShouldStopTest(exitLoopKey))
@@ -392,10 +420,23 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<int> DoRealPing(ServerTestItem it) private async Task<int> DoRealPing(ServerTestItem it)
{ {
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, 10);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
if (responseTime > 0)
{
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
var ipStr = ipInfo?.ToString() ?? Global.None;
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
await UpdateIpInfoFunc(it.IndexId, ipStr);
}
else
{
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
}
return responseTime; return responseTime;
} }
@@ -467,7 +508,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize) private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
{ {
List<List<ServerTestItem>> lstTest = new(); List<List<ServerTestItem>> lstTest = [];
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList(); var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList(); var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
@@ -491,4 +532,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestMessage(indexId, speed); ProfileExManager.Instance.SetTestMessage(indexId, speed);
} }
} }
private async Task UpdateIpInfoFunc(string indexId, string ip)
{
await _updateFunc?.Invoke(new() { IndexId = indexId, IpInfo = ip });
}
} }
@@ -117,6 +117,11 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
var msgs = new List<string>(); var msgs = new List<string>();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes()) foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
{ {
if (!(_config.CheckUpdateItem.SelectedCoreTypes?.Contains(type.ToString()) ?? true))
{
continue;
}
var result = await CheckHasUpdateOnly(type, preRelease); var result = await CheckHasUpdateOnly(type, preRelease);
if (result.Success && result.Version != null) if (result.Success && result.Version != null)
{ {
@@ -337,6 +342,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
{ {
Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64, Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64,
Architecture.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64, Architecture.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64,
Architecture.LoongArch64 => coreInfo?.DownloadUrlLinuxLoong64,
Architecture.X64 => coreInfo?.DownloadUrlLinux64, Architecture.X64 => coreInfo?.DownloadUrlLinux64,
_ => null, _ => null,
}; };
@@ -31,8 +31,7 @@ public sealed class WindowsJobService : IDisposable
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr,
(uint)length)) (uint)length))
{ {
throw new Exception(string.Format("Unable to set information. Error: {0}", throw new Exception($"Unable to set information. Error: {Marshal.GetLastWin32Error()}");
Marshal.GetLastWin32Error()));
} }
} }
finally finally
@@ -216,7 +216,7 @@ public class AddGroupServerViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer); NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer);
return; return;
} }
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : Enum.Parse<ECoreType>(CoreType);
if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) || if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) ||
SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup)) SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup))
{ {

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