Compare commits

...

83 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
5fed566a32
2026-06-07 15:49:31 +08:00
sparklelcm333
8147b39723 fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移 (#9498)
* fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移

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

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

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

* Update WindowBase.cs

---------

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

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

* Update OptionSettingWindow.axaml.cs

* Update OptionSettingWindow.axaml.cs

* Update Global.cs

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

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

Update Traditional Chinese translation

* Update ResUI.zh-Hant.resx

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

* Remove global MuxEnabled

* Fix core config

* Centralize AllowInsecure handling

* Warn insecure configuration

---------

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

* Xray Route Exclude Address

---------

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

This reverts commit a2a5940d91.

* Update Directory.Packages.props

* Update build-linux.yml

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

* Update package-rhel.sh

* Update package-rhel-riscv.sh

* Update package-debian.sh

* Update package-debian-riscv.sh

* Update package-debian-loong.sh

* Update build-linux.yml

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

* Support verifyPeerCertByName

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

* Update CoreInfoManager.cs

* Update CoreInfo.cs

* Update UpdateService.cs

* Update ResUI.fr.resx

* Update ResUI.fr.resx

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

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

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

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

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

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

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

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

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

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

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

* Update App.axaml.cs

* Create MacOSAppHelper.cs

* Update App.axaml.cs

Removed unused macOS activation policy methods and constants.

* Update and rename MacOSAppHelper.cs to MacAppUtils.cs

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

* Add ipv6 for engage.cloudflareclient.com

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

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

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

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

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

* 100mb

* Update Global.cs

---------

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,247 @@
name: release Windows x86
on:
workflow_dispatch:
inputs:
release_tag:
required: false
type: string
permissions:
contents: write
actions: read
jobs:
package-x86:
name: build and release windows x86 WPF & Avalonia
if: inputs.release_tag != ''
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- flavor: wpf
project: ./v2rayN/v2rayN/v2rayN.csproj
zip_name: v2rayN-windows-86.zip
- flavor: desktop
project: ./v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj
zip_name: v2rayN-windows-86-desktop.zip
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Purge SDKs
shell: pwsh
run: |
echo "Before:"
dotnet --list-sdks
dotnet --list-sdks | % {
$_ -match '^\S+\s+\[(.+)\]$' > $null
$d = Join-Path $Matches[1] $_.Split()[0]
Write-Host "Removing $d"
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
}
echo "After:"
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET 10.0.1xx
uses: actions/setup-dotnet@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 }}

View File

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

View File

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

View File

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

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

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

View File

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

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

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

3
.gitignore vendored
View File

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

View File

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

View File

@@ -13,8 +13,8 @@ PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.108"
DOTNET_LOONGARCH_TAG="v10.0.108-loongarch64"
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}"

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild"
DOTNET_RISCV_VERSION="10.0.108"
DOTNET_RISCV_VERSION="10.0.109"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"

View File

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

709
package-rhel-loong.sh Normal file
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 "$@"

View File

@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_RISCV_VERSION="10.0.108"
DOTNET_RISCV_VERSION="10.0.109"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -501,7 +501,7 @@ write_spec_file() {
Name: v2rayN
Version: __VERSION__
Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (riscv64)
Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues

View File

@@ -490,7 +490,7 @@ write_spec_file() {
Name: v2rayN
Version: __VERSION__
Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64)
Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues
@@ -698,4 +698,4 @@ main() {
print_summary
}
main "$@"
main "$@"

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ internal static class CoreConfigTestFactory
{
return new Config
{
CoreBasicItem = new CoreBasicItem { Loglevel = "warning", MuxEnabled = false },
CoreBasicItem = new CoreBasicItem { Loglevel = "warning" },
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
KcpItem = new KcpItem(),
GrpcItem = new GrpcItem(),
@@ -187,7 +187,7 @@ internal static class CoreConfigTestFactory
SimpleDnsItem = config.SimpleDNSItem,
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
FullConfigTemplate = null,
IsTunEnabled = false,
IsTunEnabled = config.TunModeItem.EnableTun,
ProtectDomainList = [],
};
}
@@ -206,4 +206,12 @@ internal static class CoreConfigTestFactory
config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
return config;
}
public static Config CreateConfigWithTunRouteExcludeAddress(ECoreType coreType)
{
var config = CreateConfig(coreType);
config.TunModeItem.EnableTun = true;
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
return config;
}
}

View File

@@ -1,8 +1,10 @@
using AwesomeAssertions;
using ServiceLib.Common;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager;
using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig;
using Xunit;
@@ -554,7 +556,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 == Global.SingboxLocalDNSTag);
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Global.ToString());
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Direct.ToString());
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
}
[Fact]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{
var shareLink =
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreTypeItem =
[
new CoreTypeItem { ConfigType = EConfigType.Hysteria2, CoreType = ECoreType.sing_box }
];
CoreConfigTestFactory.BindAppManagerConfig(config);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2");
proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public");
proxy.realm.realm_id.Should().Be("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull();
}
}

View File

@@ -536,4 +536,27 @@ public class CoreConfigV2rayServiceTests
directOutbound.Should().NotBeNull();
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");
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ public class Utils
try
{
str = str.Replace(Environment.NewLine, string.Empty);
str = str.ReplaceLineBreaks(string.Empty);
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
}
catch (Exception ex)
@@ -114,9 +114,7 @@ public class Utils
}
plainText = plainText.Trim()
.Replace(Environment.NewLine, "")
.Replace("\n", "")
.Replace("\r", "")
.ReplaceLineBreaks("")
.Replace('_', '/')
.Replace('-', '+')
.Replace(" ", "");
@@ -321,7 +319,32 @@ public class Utils
return text;
}
return text.Replace("", ",").Replace(Environment.NewLine, ",");
return text.Replace("", ",")
.Replace(" ", "")
.ReplaceLineBreaks(",");
}
public static string ParseProcess(string text)
{
if (text.IsNullOrEmpty())
{
return string.Empty;
}
if (text.StartsWith('"'))
{
text = text[1..];
}
if (text.EndsWith('"'))
{
text = text[..^1];
}
return List2String(text.Replace("", ",")
.Replace("\\", "/")
.ReplaceLineBreaks(",")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.TrimEx())
.Where(x => x.IsNotEmpty())
.ToList());
}
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
@@ -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)
{
if (IPAddress.TryParse(ip, out var address))
@@ -716,6 +791,24 @@ public class Utils
return (endpoints, connections);
}
public static bool IsLocalIP(string ipAddress)
{
if (!IPAddress.TryParse(ipAddress, out var targetAddress))
{
return false;
}
return NetworkInterface.GetAllNetworkInterfaces()
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
.Any(ua => ua.Address.Equals(targetAddress));
}
public static bool ContainsInterfaceName(string inInterfaceName)
{
return NetworkInterface.GetAllNetworkInterfaces()
.Any(ni => ni.Name.Equals(inInterfaceName, StringComparison.OrdinalIgnoreCase));
}
#endregion Speed Test
#region Miscellaneous

View File

@@ -55,7 +55,7 @@ public class Global
public const string DnsOutboundTag = "dns";
public const string DnsTag = "dns-module";
public const string DirectDnsTag = "direct-dns";
public const string BalancerTagSuffix = "-round";
public const string BalancerTagSuffix = "-balancer";
public const string StreamSecurity = "tls";
public const string StreamSecurityReality = "reality";
public const string Loopback = "127.0.0.1";
@@ -90,6 +90,9 @@ public class Global
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
public const int SpeedTestPageSize = 1000;
public const string LinuxBash = "/bin/bash";
public const string StringTrue = "true";
public const string StringFalse = "false";
public const int SqliteMaxBatchSize = 10000;
public const string SingboxDirectDNSTag = "direct_dns";
public const string SingboxRemoteDNSTag = "remote_dns";
@@ -149,6 +152,9 @@ public class Global
public static readonly List<string> SpeedTestUrls =
[
@"https://cachefly.cachefly.net/50mb.test",
@"https://cachefly.cachefly.net/100mb.test",
@"https://cachefly.cachefly.net/1mb.test",
@"https://cachefly.cachefly.net/10mb.test",
@"https://speed.cloudflare.com/__down?bytes=10000000",
@"https://speed.cloudflare.com/__down?bytes=50000000",
@"https://speed.cloudflare.com/__down?bytes=99999999",
@@ -157,6 +163,8 @@ public class Global
public static readonly List<string> SpeedPingTestUrls =
[
@"https://www.google.com/generate_204",
@"https://www.youtube.com/generate_204",
@"https://www.googlevideo.com/generate_204",
@"https://www.gstatic.com/generate_204",
@"https://www.apple.com/library/test/success.html",
@"http://www.msftconnecttest.com/connecttest.txt"
@@ -203,6 +211,10 @@ public class Global
public const string Hysteria2ProtocolShare = "hy2://";
public const string Hysteria2RealmProtocolShare = "hysteria2+realm://";
public const string Hysteria2HttpRealmProtocolShare = "hysteria2+realm+http://";
public const string NaiveHttpsProtocolShare = "naive+https://";
public const string NaiveQuicProtocolShare = "naive+quic://";
@@ -325,12 +337,12 @@ public class Global
public static readonly Dictionary<string, string> KcpHeaderMaskMap = new()
{
{ "srtp", "header-srtp" },
{ "utp", "header-utp" },
{ "wechat-video", "header-wechat" },
{ "dtls", "header-dtls" },
{ "wireguard", "header-wireguard" },
{ "dns", "header-dns" }
{ "srtp", "srtp" },
{ "utp", "utp" },
{ "wechat-video", "wechat" },
{ "dtls", "dtls" },
{ "wireguard", "wireguard" },
{ "dns", "dns" }
};
public static readonly List<string> CoreTypes =
@@ -399,6 +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 =
[
"chrome",
@@ -416,13 +438,6 @@ public class Global
"stream-one"
];
public static readonly List<string> AllowInsecure =
[
"true",
"false",
""
];
public static readonly List<string> DomainStrategy =
[
"AsIs",
@@ -473,10 +488,11 @@ public class Global
"zh-Hans",
"zh-Hant",
"en",
"fa-Ir",
"fa",
"fr",
"ru",
"hu"
"hu",
"id"
];
public static readonly List<string> Alpns =
@@ -666,6 +682,14 @@ public class Global
"mcbe:bedrock.talonmc.net",
];
public static readonly List<string> DefaultRealmStunList =
[
"turn.cloudflare.com:3478",
"stun.nextcloud.com:3478",
"stun.sip.us:3478",
"global.stun.twilio.com:3478",
];
public static readonly List<string> OutboundTags =
[
ProxyTag,
@@ -680,15 +704,15 @@ public class Global
{ "one.one.one.one", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
{ "1dot1dot1dot1.cloudflare-dns.com", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
{ "cloudflare-dns.com", ["104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9"] },
{ "dns.cloudflare.com", ["104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5"] },
{ "dns.cloudflare.com", ["162.159.61.8", "172.64.41.8", "2a06:98c1:52::8", "2803:f800:53::8"] },
{ "dot.pub", ["1.12.12.12", "120.53.53.53"] },
{ "doh.pub", ["1.12.12.12", "120.53.53.53"] },
{ "dns.quad9.net", ["9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"] },
{ "dns.yandex.net", ["77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff"] },
{ "dns.sb", ["185.222.222.222", "2a09::"] },
{ "dns.sb", ["45.11.45.11", "185.222.222.222", "2a09::", "2a11::"] },
{ "dns.umbrella.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
{ "dns.sse.cisco.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
{ "engage.cloudflareclient.com", ["162.159.192.1"] }
{ "engage.cloudflareclient.com", ["162.159.192.1", "2606:4700:d0::a29f:c001"] }
};
public static readonly List<string> ExpectedIPs =

View File

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

View File

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

View File

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

View File

@@ -39,15 +39,14 @@ public static class ConfigHandler
{
LogEnabled = false,
Loglevel = "warning",
MuxEnabled = false,
};
if (config.Inbound == null)
{
config.Inbound = new List<InItem>();
config.Inbound = [];
InItem inItem = new()
{
Protocol = EInboundProtocol.socks.ToString(),
Protocol = nameof(EInboundProtocol.socks),
LocalPort = 10808,
UdpEnabled = true,
SniffingEnabled = true,
@@ -60,7 +59,7 @@ public static class ConfigHandler
{
if (config.Inbound.Count > 0)
{
config.Inbound.First().Protocol = EInboundProtocol.socks.ToString();
config.Inbound.First().Protocol = nameof(EInboundProtocol.socks);
}
}
@@ -99,8 +98,8 @@ public static class ConfigHandler
config.MsgUIItem ??= new();
config.UiItem ??= new();
config.UiItem.MainColumnItem ??= new();
config.UiItem.WindowSizeItem ??= new();
config.UiItem.MainColumnItem ??= [];
config.UiItem.WindowSizeItem ??= [];
if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
{
@@ -158,17 +157,18 @@ public static class ConfigHandler
DownMbps = 100
};
config.ClashUIItem ??= new();
config.ClashUIItem.ConnectionsColumnItem ??= new();
config.ClashUIItem.ConnectionsColumnItem ??= [];
config.SystemProxyItem ??= new();
config.WebDavItem ??= new();
config.CheckUpdateItem ??= new();
config.CheckUpdateItem ??= new();
config.Fragment4RayItem ??= new()
{
Packets = "tlshello",
Length = "50-100",
Interval = "10-20"
Interval = "10-20",
MaxSplit = "0"
};
config.GlobalHotkeys ??= new();
config.GlobalHotkeys ??= [];
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
{
@@ -255,6 +255,7 @@ public static class ConfigHandler
item.Cert = profileItem.Cert;
item.CertSha = profileItem.CertSha;
item.EchConfigList = profileItem.EchConfigList;
item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName;
item.Finalmask = profileItem.Finalmask;
item.ProtoExtra = profileItem.ProtoExtra;
item.TransportExtra = profileItem.TransportExtra;
@@ -720,12 +721,51 @@ public static class ConfigHandler
{
return -1;
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
var protocolExtra = profileItem.GetProtocolExtra();
profileItem.SetProtocolExtra(protocolExtra with
{
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(),
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(),
SalamanderPass = protocolExtra.SalamanderPass?.TrimEx(),
HopInterval = protocolExtra.HopInterval?.TrimEx(),
});
if (!protocolExtra.Hy2RealmUrl.IsNullOrEmpty())
{
var realmResult = HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm);
if (!realmResult || realm is null)
{
return -1;
}
if (realm.StunList.Count == 0)
{
realm = realm with
{
StunList = Global.DefaultRealmStunList,
};
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
Hy2RealmUrl = realm.ToUri(),
});
}
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
if (isGecko)
{
var minPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
var maxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
if (minPacketSize <= 0
|| minPacketSize > maxPacketSize
|| maxPacketSize > 2048)
{
minPacketSize = 512;
maxPacketSize = 1200;
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
GeckoMinPacketSize = minPacketSize.ToString(),
GeckoMaxPacketSize = maxPacketSize.ToString(),
});
}
await AddServerCommon(config, profileItem, toFile);
return 0;
@@ -874,7 +914,7 @@ public static class ConfigHandler
profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty;
profileItem.AllowInsecure = "false";
profileItem.AllowInsecure = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
@@ -1056,8 +1096,8 @@ public static class ConfigHandler
return new Tuple<int, int>(0, 0);
}
List<ProfileItem> lstKeep = new();
List<ProfileItem> lstRemove = new();
List<ProfileItem> lstKeep = [];
List<ProfileItem> lstRemove = [];
if (!config.GuiItem.KeepOlderDedupl)
{
lstProfile.Reverse();
@@ -1106,10 +1146,6 @@ public static class ConfigHandler
}
else
{
if (profileItem.AllowInsecure.IsNullOrEmpty())
{
profileItem.AllowInsecure = config.CoreBasicItem.DefAllowInsecure.ToString().ToLower();
}
if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality)
{
profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint;
@@ -1454,8 +1490,7 @@ public static class ConfigHandler
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
{
ProfileItem? itemSocks = null;
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect
|| Utils.IsNonWindows();
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect;
if (node.ConfigType != EConfigType.Custom
&& coreType != ECoreType.sing_box
&& config.TunModeItem.EnableTun
@@ -1537,7 +1572,7 @@ public static class ConfigHandler
}
var countServers = 0;
List<ProfileItem> lstAdd = new();
List<ProfileItem> lstAdd = [];
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
if (isSub)
{
@@ -1633,7 +1668,7 @@ public static class ConfigHandler
{
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
}
if (lstProfiles != null && lstProfiles.Count > 0)
if (lstProfiles is { Count: > 0 })
{
var count = 0;
foreach (var it in lstProfiles)

View File

@@ -80,14 +80,14 @@ public static class ConnectionHandler
UseProxy = webProxy != null
});
List<int> oneTime = new();
List<int> oneTime = [];
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100);
await Task.Delay(100, cts.Token);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -244,9 +244,26 @@ public sealed class AppManager
{
return [];
}
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId))
.ToListAsync();
if (ids.Count <= Global.SqliteMaxBatchSize)
{
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId))
.ToListAsync();
}
var items = new List<ProfileItem>();
for (var size = 0; size < ids.Count; size += Global.SqliteMaxBatchSize)
{
var chunk = ids.Skip(size).Take(Global.SqliteMaxBatchSize).ToList();
var chunkItems = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => chunk.Contains(it.IndexId))
.ToListAsync();
items.AddRange(chunkItems);
}
return items;
}
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
@@ -257,18 +274,11 @@ public sealed class AppManager
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
{
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (idList.Count == 0)
{
return [];
}
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => idList.Contains(it.IndexId))
.ToListAsync();
var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
var items = await GetProfileItemsByIndexIds(ids);
var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(id => itemMap.GetValueOrDefault(id))
return ids.Select(itemMap.GetValueOrDefault)
.Where(item => item != null)
.ToList();
}

View File

@@ -4,16 +4,15 @@ using System.Security.Cryptography.X509Certificates;
namespace ServiceLib.Manager;
/// <summary>
/// Manager for certificate operations with CA pinning to prevent MITM attacks
/// Manager for certificate operations with CA pinning to prevent MITM attacks
/// </summary>
public class CertPemManager
{
private static readonly string _tag = "CertPemManager";
private static readonly Lazy<CertPemManager> _instance = new(() => new());
public static CertPemManager Instance => _instance.Value;
/// <summary>
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
/// </summary>
private static readonly HashSet<string> TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase)
{
@@ -24,21 +23,17 @@ public class CertPemManager
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
"CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
"F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA
"4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
"EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
@@ -46,10 +41,6 @@ public class CertPemManager
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
"568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2
"0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial
"0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking
"70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium
"BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC
"5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
@@ -62,7 +53,6 @@ public class CertPemManager
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
"DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
@@ -116,16 +106,12 @@ public class CertPemManager
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
"657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2
"97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority
"945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority
"55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority
"88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
"9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
@@ -179,7 +165,6 @@ public class CertPemManager
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
"BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
@@ -200,10 +185,13 @@ public class CertPemManager
"B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023
};
public static CertPemManager Instance => _instance.Value;
/// <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>
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4)
{
try
{
@@ -216,13 +204,13 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = callback
RemoteCertificateValidationCallback = callback,
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
@@ -249,9 +237,10 @@ public class CertPemManager
}
/// <summary>
/// Get certificate chain in PEM format from a server with CA pinning validation
/// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4)
{
var pemList = new List<string>();
try
@@ -265,13 +254,13 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = callback
RemoteCertificateValidationCallback = callback,
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
@@ -301,42 +290,35 @@ public class CertPemManager
}
/// <summary>
/// Validate server certificate with CA pinning
/// Validate server certificate with CA pinning
/// </summary>
private bool ValidateServerCertificate(
private static bool ValidateServerCertificate(
object _,
X509Certificate? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors,
bool allowInsecure)
List<string> verifyPeerCertByName)
{
if (certificate == null)
{
return false;
}
// In insecure mode, accept any certificate so self-signed certs can be fetched.
if (allowInsecure)
{
return true;
}
// Check certificate name mismatch
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
{
return false;
}
// Build certificate chain
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
var certChain = chain ?? new X509Chain();
certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
certChain.ChainPolicy.VerificationTime = DateTime.Now;
if (chain == null)
{
certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
certChain.ChainPolicy.VerificationTime = DateTime.UtcNow;
certChain.Build(cert2);
if (!certChain.Build(cert2))
{
return false;
}
}
// Find root CA
if (certChain.ChainElements.Count == 0)
@@ -344,10 +326,37 @@ public class CertPemManager
return false;
}
var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate;
var rootCert = certChain.ChainElements[^1].Certificate;
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)
@@ -358,8 +367,8 @@ public class CertPemManager
}
/// <summary>
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// </summary>
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
/// <returns>List of individual PEM certificate strings with normalized format</returns>
@@ -411,18 +420,13 @@ public class CertPemManager
}
/// <summary>
/// Concatenate a list of PEM certificates into a single string
/// Concatenate a list of PEM certificates into a single string
/// </summary>
/// <param name="pemList">List of individual PEM certificate strings</param>
/// <returns>Concatenated PEM certificates string</returns>
public static string ConcatenatePemChain(IEnumerable<string> pemList)
public static string ConcatenatePemChain(IEnumerable<string>? pemList)
{
if (pemList == null)
{
return string.Empty;
}
return string.Concat(pemList);
return pemList == null ? string.Empty : string.Concat(pemList);
}
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
@@ -431,11 +435,7 @@ public class CertPemManager
{
var cert = X509Certificate2.CreateFromPem(pemCert);
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
if (includeColon)
{
return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c)));
}
return thumbprint;
return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint;
}
catch
{

View File

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

View File

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

View File

@@ -90,6 +90,7 @@ public class CoreManager
}
await CoreStart(mainContext);
await WaitForProxyPort(preContext);
await CoreStartPreService(preContext);
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
@@ -213,6 +214,75 @@ public class CoreManager
await _updateFunc?.Invoke(notify, msg);
}
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
{
if (preContext is null)
{
return;
}
if (!preContext.AppConfig.TunModeItem.EnableTun)
{
return;
}
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
var rootToken = rootCts.Token;
var port = preContext.Node.Port;
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
var buf = new byte[2];
while (!rootToken.IsCancellationRequested)
{
using var tcp = new TcpClient();
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
var linkedToken = linkedCts.Token;
try
{
await tcp.ConnectAsync(Global.Loopback, port, linkedToken);
var stream = tcp.GetStream();
await stream.WriteAsync(greeting, linkedToken);
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
if (read == 2 && buf[0] == 0x05)
{
return;
}
}
catch (OperationCanceledException)
{
if (!rootToken.IsCancellationRequested)
{
continue;
}
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
return;
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
// Connection refused, proxy not ready yet, wait 50ms before retrying
try
{
await Task.Delay(50, rootToken);
}
catch (OperationCanceledException)
{
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
return;
}
}
catch
{
// Ignore other exceptions and continue
}
}
}
#endregion Private
#region Process
@@ -230,7 +300,7 @@ public class CoreManager
{
if (mayNeedSudo
&& _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())
{
_linuxSudo = true;

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,10 +7,6 @@ public class CoreBasicItem
public string Loglevel { get; set; }
public bool MuxEnabled { get; set; }
public bool DefAllowInsecure { get; set; }
public string DefFingerprint { get; set; }
public string DefUserAgent { get; set; }
@@ -21,6 +17,8 @@ public class CoreBasicItem
public bool EnableFragment { get; set; }
public bool EnableFinalFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true;
}
@@ -148,6 +146,7 @@ public class TunModeItem
public bool EnableIPv6Address { get; set; }
public string IcmpRouting { get; set; }
public bool EnableLegacyProtect { get; set; }
public List<string>? RouteExcludeAddress { get; set; }
}
[Serializable]
@@ -159,6 +158,8 @@ public class SpeedTestItem
public int MixedConcurrencyCount { get; set; }
public string IPAPIUrl { get; set; }
public string UdpTestTarget { get; set; }
public int? SpeedTestPageSize { get; set; }
public int? SpeedTestDelayInterval { get; set; }
}
[Serializable]
@@ -248,6 +249,7 @@ public class Fragment4RayItem
public string? Packets { get; set; }
public string? Length { get; set; }
public string? Interval { get; set; }
public string? MaxSplit { get; set; }
}
[Serializable]

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ public class Stats4Ray
public class Metrics4Ray
{
public string tag { get; set; }
public string listen { get; set; }
}
public class Policy4Ray
@@ -86,7 +86,7 @@ public class Inboundsettings4Ray
public string? autoOutboundsInterface { get; set; }
// public List<string>? dns { get; set; }
public List<string>? dns { get; set; }
}
public class UsersItem4Ray
@@ -136,7 +136,7 @@ public class Outboundsettings4Ray
public Response4Ray? response { get; set; }
public string domainStrategy { get; set; }
public string? domainStrategy { get; set; }
public int? userLevel { get; set; }
@@ -276,6 +276,7 @@ public class BalancersItem4Ray
public List<string>? selector { get; set; }
public BalancersStrategy4Ray? strategy { get; set; }
public string? tag { get; set; }
public string? fallbackTag { get; set; }
}
public class BalancersStrategy4Ray
@@ -288,16 +289,7 @@ public class BalancersStrategySettings4Ray
{
public int? expected { get; set; }
public string? maxRTT { get; set; }
public float? tolerance { get; set; }
public List<string>? baselines { get; set; }
public List<BalancersStrategySettingsCosts4Ray>? costs { get; set; }
}
public class BalancersStrategySettingsCosts4Ray
{
public bool? regexp { get; set; }
public string? match { get; set; }
public float? value { get; set; }
public double? tolerance { get; set; }
}
public class Observatory4Ray
@@ -372,6 +364,7 @@ public class TlsSettings4Ray
public string? spiderX { get; set; }
public string? mldsa65Verify { get; set; }
public List<CertificateSettings4Ray>? certificates { get; set; }
public string? verifyPeerCertByName { get; set; }
public string? pinnedPeerCertSha256 { get; set; }
public bool? disableSystemRoot { get; set; }
public string? echConfigList { get; set; }
@@ -487,6 +480,7 @@ public class UdpHop4Ray
public class Finalmask4Ray
{
public List<Mask4Ray>? tcp { get; set; }
public List<Mask4Ray>? udp { get; set; }
public QuicParams4Ray? quicParams { get; set; }
}
@@ -499,14 +493,21 @@ public class Mask4Ray
public class MaskSettings4Ray
{
public string? header { get; set; }
public string? value { get; set; }
public string? password { get; set; }
public string? domain { get; set; }
public string? url { get; set; }
public List<string>? stunServers { get; set; }
public string? packetSize { get; set; }
// fragment
public string? packets { get; set; }
public string? length { get; set; }
public string? delay { get; set; }
public int? maxSplit { get; set; }
// noise
public int? reset { get; set; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -222,15 +222,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。
/// </summary>
public static string FillCorrectSendThroughIPv4 {
get {
return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture);
}
}
/// <summary>
/// 查找类似 Please enter the correct port format. 的本地化字符串。
/// </summary>
@@ -240,6 +231,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Invalid range format. Use &apos;from-to&apos; (e.g., 50-100). 的本地化字符串。
/// </summary>
public static string FillFragmentParameterError {
get {
return ResourceManager.GetString("FillFragmentParameterError", resourceCulture);
}
}
/// <summary>
/// 查找类似 Please enter the local listening port. 的本地化字符串。
/// </summary>
@@ -321,6 +321,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Invalid Realm URL. 的本地化字符串。
/// </summary>
public static string InvalidHy2RealmUrl {
get {
return ResourceManager.GetString("InvalidHy2RealmUrl", resourceCulture);
}
}
/// <summary>
/// 查找类似 Invalid address (URL) 的本地化字符串。
/// </summary>
@@ -1914,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>
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
/// </summary>
@@ -2058,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>
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
/// </summary>
@@ -2103,6 +2130,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Conflict between {0} and {1} 的本地化字符串。
/// </summary>
public static string MsgOptionsConflict {
get {
return ResourceManager.GetString("MsgOptionsConflict", resourceCulture);
}
}
/// <summary>
/// 查找类似 Resolved {0} successfully 的本地化字符串。
/// </summary>
@@ -2220,6 +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>
/// 查找类似 Unpacking... 的本地化字符串。
/// </summary>
@@ -2643,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>
/// 查找类似 ALPN 的本地化字符串。
/// </summary>
@@ -3006,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>
/// 查找类似 Enable Tun 的本地化字符串。
/// </summary>
@@ -3114,6 +3168,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
/// </summary>
public static string TbGeckoPacketSize {
get {
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
}
}
/// <summary>
/// 查找类似 Global Hotkey Settings 的本地化字符串。
/// </summary>
@@ -3177,6 +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>
/// 查找类似 ICMP routing policy 的本地化字符串。
/// </summary>
@@ -3528,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>
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// </summary>
@@ -3763,7 +3862,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode 的本地化字符串。
/// 查找类似 For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled. 的本地化字符串。
/// </summary>
public static string TbSettingsBindInterfaceTip {
get {
@@ -4050,6 +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>
/// 查找类似 Geo files source (optional) 的本地化字符串。
/// </summary>
@@ -4698,6 +4887,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Verify Peer Cert By Name 的本地化字符串。
/// </summary>
public static string TbVerifyPeerCertByName {
get {
return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture);
}
}
/// <summary>
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
/// </summary>

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Enable fragment</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Fragment Packets</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Fragment Length</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Fragment size range in bytes (e.g., 50-100). First value must be &lt;= second. Empty = default.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Fragment Interval</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be &lt;= second. Empty = default.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Max Split</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Fallback Delay</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Invalid range format. Use 'from-to' (e.g., 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Enable cache file for sing-box (ruleset files)</value>
</data>
@@ -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">
<value>EchConfigList</value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
@@ -1698,12 +1734,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value>
</data>
@@ -1716,14 +1746,11 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value>
</data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value>
<value>For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled.</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value>
@@ -1746,4 +1773,43 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration.</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Route Exclude Address</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>Use commas (,) to separate.</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Invalid address in TUN route exclude list: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>Conflict between {0} and {1}</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>Enable Final Fragment</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS via Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Invalid Realm URL.</value>
</data>
<data name="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>

View File

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

View File

@@ -1030,7 +1030,7 @@
<value>添加 [Hysteria2]</value>
</data>
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
<value>Hysteria 最大带宽 (Up/Dw)</value>
<value>Hysteria 最大带宽 (上传/下载)</value>
</data>
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
<value>使用系统 hosts</value>
@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>启用分片 (Fragment)</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>分片包类型</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>要分片的包类型: tlshello (TLS ClientHello) 或 1-1 到 1-5 (前 N 个 TCP 包)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>分片长度</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>分片大小范围(字节), 如 50-100. 第一个值必须 &lt;= 第二个值. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>分片间隔</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>分片间延迟(毫秒), 如 10-20. 范围 1-100. 第一个值必须 &lt;= 第二个值. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>最大分片数</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>每个包最大分片数 (0-10000, 0=无限制). 仅 Xray. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>回退延迟</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>ACK 检测不可用时的回退延迟 (如 500ms). 仅 sing-box. 留空使用默认值.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>无效的范围格式。请使用 'from-to' (如 50-100)。</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>启用 sing-box (规则集文件) 的缓存文件</value>
</data>
@@ -1695,12 +1728,6 @@
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>允许不安全获取证书(自签名)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>仅用于抓取自签证书,存在中间人风险。</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>测试 UDP 延迟 (多选)</value>
</data>
@@ -1713,9 +1740,6 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>用于多网口环境,请填写本机 IPv4 地址</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>请填写正确的 SendThrough IPv4 地址。</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>绑定网口</value>
</data>
@@ -1743,4 +1767,46 @@
<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>

View File

@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve">
<value>啟用分片Fragment</value>
</data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>分片封包類型</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>要分片的封包類型tlshello (TLS ClientHello) 或 1-1 到 1-5 (前 N 個 TCP 封包)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>分片長度</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>分片大小範圍(位元組),如 50-100。第一個值必須 &lt;= 第二個值。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>分片間隔</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>分片間延遲(毫秒),如 10-20。範圍 1-100。第一個值必須 &lt;= 第二個值。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>最大分片數</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>每個封包最大分片數 (0-10000, 0=無限制)。僅 Xray。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>回退延遲</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>ACK 偵測不可用時的回退延遲 (如 500ms)。僅 sing-box。留空使用預設值。</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>無效的範圍格式。請使用 'from-to' (如 50-100)。</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>啟用 sing-box規則集檔案的快取檔案</value>
</data>
@@ -1333,7 +1366,7 @@
<value>開啟第二個本機監聽埠</value>
</data>
<data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks埠,socks2第二個本埠,socks3區域網路埠</value>
<value>TunTUN 入站Socks機連接埠,Socks2第二個本機連接埠,Socks3區域網路連接埠</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>主題</value>
@@ -1695,12 +1728,6 @@
<data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value>
</data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>允許不安全獲取證書(自簽名)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>僅用於抓取自簽證書,存在中間人風險。</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>測試 UDP 延遲(多選)</value>
</data>
@@ -1713,9 +1740,6 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>適用於多網路介面環境,請填寫本機 IPv4 位址</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>請填寫正確的 SendThrough IPv4 位址。</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>綁定網路介面</value>
</data>
@@ -1743,4 +1767,31 @@
<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>

View File

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

View File

@@ -8,6 +8,9 @@
"172.18.0.1/30",
"fdfe:dcba:9876::1/126"
],
"dns": [
"172.18.0.1"
],
"autoSystemRoutingTable": [
"0.0.0.0/0",
"::/0"

View File

@@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Downloader" />
<PackageReference Include="IPNetwork2" />
<PackageReference Include="ReactiveUI">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
@@ -56,7 +57,7 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<EmbeddedResource Update="Resx\ResUI.fa-Ir.resx">
<EmbeddedResource Update="Resx\ResUI.fa.resx">
<SubType>Designer</SubType>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
@@ -66,6 +67,9 @@
<EmbeddedResource Update="Resx\ResUI.hu.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.id.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.resx">
<SubType>Designer</SubType>
<LastGenOutput>ResUI.Designer.cs</LastGenOutput>

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ public partial class CoreConfigSingboxService
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
if (rules?.LastOrDefault() is { OutboundTag: Global.DirectTag } lastRule)
{
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
@@ -82,7 +82,7 @@ public partial class CoreConfigSingboxService
if (simpleDnsItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts != null && systemHosts.Count > 0)
if (systemHosts is { Count: > 0 })
{
foreach (var host in systemHosts)
{
@@ -182,13 +182,13 @@ public partial class CoreConfigSingboxService
{
server = Global.SingboxRemoteDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy),
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
},
new Rule4Sbox
{
server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
}
});
@@ -454,12 +454,12 @@ public partial class CoreConfigSingboxService
dns4Sbox.rules.Insert(0, new()
{
server = tag,
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
});
dns4Sbox.rules.Insert(0, new()
{
server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote",
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
});
var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;

View File

@@ -15,8 +15,8 @@ public partial class CoreConfigSingboxService
{
var inbound = new Inbound4Sbox()
{
type = EInboundProtocol.mixed.ToString(),
tag = EInboundProtocol.socks.ToString(),
type = nameof(EInboundProtocol.mixed),
tag = nameof(EInboundProtocol.socks),
listen = Global.Loopback,
};
_coreConfig.inbounds.Add(inbound);
@@ -71,6 +71,7 @@ public partial class CoreConfigSingboxService
{
tunInbound.address = ["172.18.0.1/30"];
}
tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress;
_coreConfig.inbounds.Add(tunInbound);
}
@@ -86,7 +87,7 @@ public partial class CoreConfigSingboxService
var inbound = JsonUtils.DeepCopy(inItem);
inbound.tag = protocol.ToString();
inbound.listen_port = inItem.listen_port + (int)protocol;
inbound.type = EInboundProtocol.mixed.ToString();
inbound.type = nameof(EInboundProtocol.mixed);
return inbound;
}
}

View File

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

View File

@@ -96,6 +96,15 @@ public partial class CoreConfigSingboxService
new() { protocol = ["dns"] },
],
});
if (_config.CoreBasicItem.EnableFinalFragment)
{
_coreConfig.route.rules.Add(new()
{
protocol = ["tls"],
action = "route-options",
tls_record_fragment = true,
});
}
}
else
{
@@ -104,6 +113,14 @@ public partial class CoreConfigSingboxService
port = [53],
action = "hijack-dns",
});
if (_config.CoreBasicItem.EnableFinalFragment)
{
_coreConfig.route.rules.Add(new()
{
action = "route-options",
tls_record_fragment = true,
});
}
}
var hostsDomains = new List<string>();
@@ -176,12 +193,12 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
});
_coreConfig.route.rules.Add(new()
{
outbound = Global.ProxyTag,
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
});
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty();

View File

@@ -18,15 +18,15 @@ public partial class CoreConfigSingboxService
//convert route geosite & geoip to ruleset
foreach (var rule in _coreConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList() ?? []);
rule.geosite = null;
AddRuleSets(ruleSets, rule.rule_set);
}
foreach (var rule in _coreConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList() ?? []);
rule.geoip = null;
AddRuleSets(ruleSets, rule.rule_set);
}
@@ -34,14 +34,14 @@ public partial class CoreConfigSingboxService
//convert dns geosite & geoip to ruleset
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList() ?? []);
rule.geosite = null;
}
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList() ?? []);
rule.geoip = null;
}
foreach (var dnsRule in _coreConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? [])

View File

@@ -60,6 +60,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
@@ -161,7 +165,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
listen = Global.Loopback,
port = port,
protocol = EInboundProtocol.mixed.ToString(),
protocol = nameof(EInboundProtocol.mixed),
settings = new Inboundsettings4Ray()
{
udp = true,
@@ -204,6 +208,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
@@ -262,7 +270,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
tag = $"{EInboundProtocol.socks}{port}",
listen = Global.Loopback,
port = port,
protocol = EInboundProtocol.mixed.ToString(),
protocol = nameof(EInboundProtocol.mixed),
settings = new Inboundsettings4Ray()
{
udp = true,
@@ -276,6 +284,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();

View File

@@ -56,7 +56,7 @@ public partial class CoreConfigV2rayService
}
else
{
_coreConfig.burstObservatory.subjectSelector ??= new();
_coreConfig.burstObservatory.subjectSelector ??= [];
_coreConfig.burstObservatory.subjectSelector.Add(baseTagName);
}
}
@@ -75,7 +75,7 @@ public partial class CoreConfigV2rayService
}
else
{
_coreConfig.observatory.subjectSelector ??= new();
_coreConfig.observatory.subjectSelector ??= [];
_coreConfig.observatory.subjectSelector.Add(baseTagName);
}
}
@@ -89,23 +89,27 @@ public partial class CoreConfigV2rayService
EMultipleLoad.RoundRobin => "roundRobin",
EMultipleLoad.LeastPing => "leastPing",
EMultipleLoad.LeastLoad => "leastLoad",
EMultipleLoad.Fallback => "leastLoad",
_ => "roundRobin",
};
var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
var balancer = new BalancersItem4Ray
{
selector = [selector],
strategy = new()
strategy = strategyType == "leastLoad" ? new()
{
type = strategyType,
settings = new()
{
expected = 1,
tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null,
maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null,
},
},
} : null,
tag = balancerTag,
fallbackTag = _coreConfig.outbounds?.FirstOrDefault(o => o.tag.StartsWith(selector))?.tag,
};
_coreConfig.routing.balancers ??= new();
_coreConfig.routing.balancers ??= [];
_coreConfig.routing.balancers.Add(balancer);
}
}

View File

@@ -46,10 +46,7 @@ public partial class CoreConfigV2rayService
}
// Ensure routing node exists
if (fullConfigTemplateNode["routing"] == null)
{
fullConfigTemplateNode["routing"] = new JsonObject();
}
fullConfigTemplateNode["routing"] ??= new JsonObject();
// Handle balancers - append instead of override
if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode)
@@ -141,10 +138,6 @@ public partial class CoreConfigV2rayService
{
return;
}
if (!(context.IsTunEnabled || context.IsWindows))
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? [])
{
if (!ShouldBindNet(outbound))
@@ -179,6 +172,7 @@ public partial class CoreConfigV2rayService
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? [])
{
outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null;

View File

@@ -34,7 +34,7 @@ public partial class CoreConfigV2rayService
return;
}
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;
//Outbound Freedom domainStrategy
@@ -246,7 +246,7 @@ public partial class CoreConfigV2rayService
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 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 });
if (outbound != null)
{
outbound.settings = new();
outbound.settings.domainStrategy = domainStrategy4Freedom;
outbound.settings.userLevel = 0;
outbound.settings = new()
{
domainStrategy = domainStrategy4Freedom,
userLevel = 0,
};
}
}

View File

@@ -30,14 +30,19 @@ public partial class CoreConfigV2rayService
inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3);
//auth
// auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
{
inbound3.settings.auth = "password";
inbound3.settings.accounts = new List<AccountsItem4Ray>
{
new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass }
};
inbound3.settings.accounts =
[
new()
{
user = _config.Inbound.First().User,
pass = _config.Inbound.First().Pass,
},
];
}
}
else
@@ -53,12 +58,15 @@ public partial class CoreConfigV2rayService
{
_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.MTU = _config.TunModeItem.Mtu;
if (_config.TunModeItem.EnableIPv6Address == false)
if (!_config.TunModeItem.EnableIPv6Address)
{
tunInbound.settings.gateway = ["172.18.0.1/30"];
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
}
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (!bindInterface.IsNullOrEmpty())
@@ -66,6 +74,53 @@ public partial class CoreConfigV2rayService
tunInbound.settings.autoOutboundsInterface = bindInterface;
}
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);
}
}
@@ -90,7 +145,7 @@ public partial class CoreConfigV2rayService
}
inbound.tag = protocol.ToString();
inbound.port = inItem.LocalPort + (int)protocol;
inbound.protocol = EInboundProtocol.mixed.ToString();
inbound.protocol = nameof(EInboundProtocol.mixed);
inbound.settings.udp = inItem.UdpEnabled;
inbound.sniffing.enabled = inItem.SniffingEnabled;
inbound.sniffing.destOverride = inItem.DestOverride;

View File

@@ -62,7 +62,7 @@ public partial class CoreConfigV2rayService
try
{
var protocolExtra = _node.GetProtocolExtra();
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
var muxEnabled = _node.MuxEnabled ?? false;
switch (_node.ConfigType)
{
case EConfigType.VMess:
@@ -243,9 +243,9 @@ public partial class CoreConfigV2rayService
version = 2,
address = _node.Address,
port = _node.Port,
vnext = null,
servers = null,
};
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
case EConfigType.WireGuard:
@@ -380,10 +380,11 @@ public partial class CoreConfigV2rayService
TlsSettings4Ray tlsSettings = new()
{
allowInsecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure),
allowInsecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(),
fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint,
echConfigList = _node.EchConfigList.NullIfEmpty(),
verifyPeerCertByName = _node.VerifyPeerCertByName.NullIfEmpty(),
};
if (sni.IsNotEmpty())
{
@@ -449,14 +450,13 @@ public partial class CoreConfigV2rayService
KcpSettings4Ray kcpSettings = new()
{
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();
if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header))
{
@@ -464,8 +464,8 @@ public partial class CoreConfigV2rayService
[
new Mask4Ray
{
type = header,
settings = null
type = "mkcp-legacy",
settings = new MaskSettings4Ray { header = header },
}
];
}
@@ -474,15 +474,15 @@ public partial class CoreConfigV2rayService
{
kcpFinalmask.udp.Add(new Mask4Ray
{
type = "mkcp-original"
type = "mkcp-legacy",
});
}
else
{
kcpFinalmask.udp.Add(new Mask4Ray
{
type = "mkcp-aes128gcm",
settings = new MaskSettings4Ray { password = kcpSeed }
type = "mkcp-legacy",
settings = new MaskSettings4Ray { value = kcpSeed },
});
}
streamSettings.kcpSettings = kcpSettings;
@@ -530,7 +530,7 @@ public partial class CoreConfigV2rayService
break;
//xhttp
case nameof(ETransport.xhttp):
streamSettings.network = ETransport.xhttp.ToString();
streamSettings.network = nameof(ETransport.xhttp);
XhttpSettings4Ray xhttpSettings = new();
if (path.IsNotEmpty())
@@ -607,16 +607,32 @@ public partial class CoreConfigV2rayService
quicParams.congestion = "bbr";
}
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())
{
hy2Finalmask.udp =
[
new Mask4Ray
{
type = "salamander",
settings = new MaskSettings4Ray { password = protocolExtra.SalamanderPass.TrimEx(), }
}
];
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
var salamanderSettings = new MaskSettings4Ray
{
password = protocolExtra.SalamanderPass.TrimEx(),
};
if (isGecko)
{
salamanderSettings.packetSize = $"{protocolExtra.GeckoMinPacketSize}-{protocolExtra.GeckoMaxPacketSize}";
}
hy2Finalmask.udp.Add(new Mask4Ray
{
type = "salamander",
settings = salamanderSettings,
});
}
streamSettings.hysteriaSettings = new()
{
@@ -825,29 +841,7 @@ public partial class CoreConfigV2rayService
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
.ToList();
var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello";
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",
}
};
var (fragmentMask, noiseMask) = BuildFragmentsMasks();
foreach (var outbound in actOutboundWithTlsList)
{
@@ -881,4 +875,76 @@ public partial class CoreConfigV2rayService
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);
}
}

View File

@@ -6,45 +6,19 @@ public partial class CoreConfigV2rayService
{
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{
var tag = EInboundProtocol.api.ToString();
Metrics4Ray apiObj = new();
Metrics4Ray metricsObj = new();
Policy4Ray policyObj = new();
SystemPolicy4Ray policySystemSetting = new();
_coreConfig.stats = new Stats4Ray();
apiObj.tag = tag;
_coreConfig.metrics = apiObj;
metricsObj.listen = $"{Global.Loopback}:{AppManager.Instance.StatePort}";
_coreConfig.metrics = metricsObj;
policySystemSetting.statsOutboundDownlink = true;
policySystemSetting.statsOutboundUplink = true;
policyObj.system = policySystemSetting;
_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);
}
}
}
}

View File

@@ -7,7 +7,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private static readonly string _tag = "SpeedtestService";
private readonly Config? _config = config;
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)
{
@@ -31,7 +33,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
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)
@@ -44,7 +46,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
switch (actionType)
{
case ESpeedActionType.Tcping:
await RunTcpingAsync(lstSelected);
await RunTcpingAsync(lstSelected, exitLoopKey);
break;
case ESpeedActionType.Realping:
@@ -133,38 +135,64 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return lstSelected;
}
private async Task RunTcpingAsync(List<ServerTestItem> selecteds)
private async Task RunTcpingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
{
List<Task> tasks = [];
foreach (var it in selecteds)
{
tasks.Add(Task.Run(async () =>
{
try
{
var responseTime = await GetTcpingTime(it.Address, it.Port);
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
var lstBatch = GetTestBatchItem(selecteds, pageSize);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
foreach (var lst in lstBatch)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
List<Task> tasks = [];
foreach (var it in lst)
{
if (ShouldStopTest(exitLoopKey))
{
Logging.SaveLog(_tag, ex);
return;
}
}));
tasks.Add(Task.Run(async () =>
{
try
{
var responseTime = await GetTcpingTime(it.Address, it.Port);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}));
}
await Task.WhenAll(tasks);
if (ShouldStopTest(exitLoopKey))
{
return;
}
await Task.Delay(_delayInterval);
}
await Task.WhenAll(tasks);
}
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int 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);
List<ServerTestItem> lstFailed = new();
List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest)
{
var ret = await RunRealPingAsync(lst, exitLoopKey);
@@ -172,7 +200,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
lstFailed.AddRange(lst);
}
await Task.Delay(100);
await Task.Delay(_delayInterval);
}
//Retest the failed part
@@ -210,7 +238,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
}
await Task.Delay(1000);
List<Task> tasks = new();
List<Task> tasks = [];
foreach (var it in selecteds)
{
if (!it.AllowTest)
@@ -249,11 +277,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
if (pageSize <= 0)
{
pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize;
pageSize = Math.Min(lstSelected.Count, _speedTestPageSize);
}
var lstTest = GetTestBatchItem(lstSelected, pageSize);
List<ServerTestItem> lstFailed = new();
List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest)
{
var ret = await RunUdpTestAsync(lst, exitLoopKey);
@@ -261,7 +289,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
lstFailed.AddRange(lst);
}
await Task.Delay(100);
await Task.Delay(_delayInterval);
}
//Retest the failed part
@@ -291,7 +319,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
}
await Task.Delay(1000);
List<Task> tasks = new();
List<Task> tasks = [];
foreach (var it in selecteds)
{
if (!it.AllowTest)
@@ -329,7 +357,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
var downloadHandle = new DownloadService();
List<Task> tasks = new();
List<Task> tasks = [];
foreach (var it in selecteds)
{
if (ShouldStopTest(exitLoopKey))
@@ -480,7 +508,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
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 lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();

View File

@@ -117,6 +117,11 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
var msgs = new List<string>();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
{
if (!(_config.CheckUpdateItem.SelectedCoreTypes?.Contains(type.ToString()) ?? true))
{
continue;
}
var result = await CheckHasUpdateOnly(type, preRelease);
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.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64,
Architecture.LoongArch64 => coreInfo?.DownloadUrlLinuxLoong64,
Architecture.X64 => coreInfo?.DownloadUrlLinux64,
_ => null,
};

View File

@@ -31,8 +31,7 @@ public sealed class WindowsJobService : IDisposable
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr,
(uint)length))
{
throw new Exception(string.Format("Unable to set information. Error: {0}",
Marshal.GetLastWin32Error()));
throw new Exception($"Unable to set information. Error: {Marshal.GetLastWin32Error()}");
}
}
finally

View File

@@ -216,7 +216,7 @@ public class AddGroupServerViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer);
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) ||
SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup))
{

View File

@@ -50,7 +50,7 @@ public class AddServer2ViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillServerAddressCustom);
return;
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0)
{

View File

@@ -8,6 +8,12 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public string? CoreType { get; set; }
[Reactive]
public bool AllowInsecure { get; set; }
[Reactive]
public bool MuxEnabled { get; set; }
[Reactive]
public string Cert { get; set; }
@@ -17,9 +23,6 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public string CertSha { get; set; }
[Reactive]
public bool AllowInsecureCertFetch { get; set; }
[Reactive]
public string SalamanderPass { get; set; }
@@ -77,6 +80,15 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public bool NaiveQuic { get; set; }
[Reactive]
public string Hy2RealmUrl { get; set; }
[Reactive]
public int GeckoMinPacketSize { get; set; }
[Reactive]
public int GeckoMaxPacketSize { get; set; }
[Reactive]
public string RawHeaderType { get; set; }
@@ -273,8 +285,10 @@ public class AddServerViewModel : MyReactiveObject
SelectedSource = JsonUtils.DeepCopy(profileItem);
}
CoreType = SelectedSource?.CoreType?.ToString();
Cert = SelectedSource?.Cert?.ToString() ?? string.Empty;
CertSha = SelectedSource?.CertSha?.ToString() ?? string.Empty;
AllowInsecure = SelectedSource?.GetAllowInsecure() == true;
MuxEnabled = SelectedSource?.MuxEnabled == true;
Cert = SelectedSource?.Cert ?? string.Empty;
CertSha = SelectedSource?.CertSha ?? string.Empty;
var protocolExtra = SelectedSource?.GetProtocolExtra() ?? new();
var transport = SelectedSource?.GetTransportExtra() ?? new();
@@ -297,6 +311,9 @@ public class AddServerViewModel : MyReactiveObject
CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
NaiveQuic = protocolExtra.NaiveQuic ?? false;
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
RawHeaderType = transport.RawHeaderType ?? Global.None;
Host = transport.Host ?? string.Empty;
@@ -352,7 +369,19 @@ public class AddServerViewModel : MyReactiveObject
return;
}
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
HyRealm? realm = null;
if (!Hy2RealmUrl.IsNullOrEmpty())
{
var realmResult = HyRealm.TryParse(Hy2RealmUrl, out realm);
if (!realmResult)
{
NoticeManager.Instance.Enqueue(ResUI.InvalidHy2RealmUrl);
return;
}
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse;
SelectedSource.MuxEnabled = MuxEnabled;
SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert;
SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha;
if (!Global.Networks.Contains(SelectedSource.Network))
@@ -396,6 +425,9 @@ public class AddServerViewModel : MyReactiveObject
CongestionControl = CongestionControl.NullIfEmpty(),
InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null,
NaiveQuic = NaiveQuic ? true : null,
Hy2RealmUrl = realm?.ToUri().NullIfEmpty(),
GeckoMinPacketSize = GeckoMinPacketSize > 0 ? GeckoMinPacketSize.ToString() : null,
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
});
SelectedSource.SetTransportExtra(transport);
@@ -464,7 +496,8 @@ public class AddServerViewModel : MyReactiveObject
domain += $":{SelectedSource.Port}";
}
(Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName, allowInsecure: AllowInsecureCertFetch);
(Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName,
verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName));
UpdateCertTip(certError);
}
@@ -489,7 +522,8 @@ public class AddServerViewModel : MyReactiveObject
domain += $":{SelectedSource.Port}";
}
var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName, allowInsecure: AllowInsecureCertFetch);
var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName,
verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName));
Cert = CertPemManager.ConcatenatePemChain(certs);
UpdateCertTip(certError);
}

View File

@@ -36,21 +36,21 @@ public class CheckUpdateViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.EnableCheckPreReleaseUpdate,
y => y == true)
.Subscribe(c => _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate);
.Subscribe(c => _ = OnCheckPreReleaseUpdateChanged());
RefreshCheckUpdateItems();
}
private void RefreshCheckUpdateItems()
{
var models = CoreInfoManager.Instance.GetCheckUpdateCoreTypes()
.Select(t => GetCheckUpdateModel(t))
.ToList();
models.Add(GetGeoFileCheckUpdateModel());
CheckUpdateModels.Clear();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
{
CheckUpdateModels.Add(GetCheckUpdateModel(type));
}
CheckUpdateModels.Add(GetGeoFileCheckUpdateModel());
CheckUpdateModels.AddRange(models);
}
private CheckUpdateModel GetCheckUpdateModel(ECoreType coreType)
@@ -87,12 +87,23 @@ public class CheckUpdateViewModel : MyReactiveObject
};
}
private async Task OnCheckPreReleaseUpdateChanged()
{
if (_config.CheckUpdateItem.CheckPreReleaseUpdate == EnableCheckPreReleaseUpdate)
{
return;
}
_config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate;
await SaveSelectedCoreTypes();
}
private async Task SaveSelectedCoreTypes()
{
_config.CheckUpdateItem.SelectedCoreTypes = CheckUpdateModels
.Where(t => t.IsSelected == true)
.Select(t => t.CoreTypeForStorage)
.ToList();
_config.CheckUpdateItem.SelectedCoreTypes =
CheckUpdateModels.Where(t => t.IsSelected == true)
.Select(t => t.CoreTypeForStorage)
.ToList();
await ConfigHandler.SaveConfig(_config);
}

View File

@@ -69,7 +69,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
var dtNow = DateTime.Now;
var lstModel = new List<ClashConnectionModel>();
foreach (var item in connections ?? new())
foreach (var item in connections ?? [])
{
var host = $"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}";
if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter))
@@ -85,7 +85,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
Host = host,
Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds,
Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"),
Chain = $"{item.rule} , {string.Join("->", item.chains ?? new())}"
Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}"
};
lstModel.Add(model);

View File

@@ -67,7 +67,7 @@ public class ClashProxiesViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.SelectedGroup,
y => y != null && y.Name.IsNotEmpty())
.Subscribe(c => RefreshProxyDetails(c));
.Subscribe(RefreshProxyDetails);
this.WhenAnyValue(
x => x.RuleModeSelected,
@@ -77,7 +77,7 @@ public class ClashProxiesViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.SortingSelected,
y => y >= 0)
.Subscribe(c => DoSortingSelected(c));
.Subscribe(DoSortingSelected);
this.WhenAnyValue(
x => x.AutoRefresh,
@@ -187,21 +187,21 @@ public class ClashProxiesViewModel : MyReactiveObject
var selectedName = SelectedGroup?.Name;
ProxyGroups.Clear();
var lstProxyGroups = new List<ClashProxyModel>();
var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups();
if (proxyGroups != null && proxyGroups.Count > 0)
if (proxyGroups is { Count: > 0 })
{
foreach (var it in proxyGroups)
{
if (it.name.IsNullOrEmpty() || !_proxies.ContainsKey(it.name))
if (it.name.IsNullOrEmpty() || !_proxies.TryGetValue(it.name, out var item))
{
continue;
}
var item = _proxies[it.name];
if (!Global.allowSelectType.Contains(item.type.ToLower()))
{
continue;
}
ProxyGroups.Add(new ClashProxyModel()
lstProxyGroups.Add(new ClashProxyModel()
{
Now = item.now,
Name = item.name,
@@ -217,12 +217,12 @@ public class ClashProxiesViewModel : MyReactiveObject
{
continue;
}
var item = ProxyGroups.FirstOrDefault(t => t.Name == kv.Key);
var item = lstProxyGroups.FirstOrDefault(t => t.Name == kv.Key);
if (item != null && item.Name.IsNotEmpty())
{
continue;
}
ProxyGroups.Add(new ClashProxyModel()
lstProxyGroups.Add(new ClashProxyModel()
{
Now = kv.Value.now,
Name = kv.Key,
@@ -230,7 +230,9 @@ public class ClashProxiesViewModel : MyReactiveObject
});
}
if (ProxyGroups != null && ProxyGroups.Count > 0)
ProxyGroups.AddRange(lstProxyGroups);
if (ProxyGroups is { Count: > 0 })
{
if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName))
{

View File

@@ -56,7 +56,7 @@ public class DNSSettingViewModel : MyReactiveObject
});
this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible)
.Select(x => !(x.Item1 && x.Item2))
.Select(x => x is not { Item1: true, Item2: true })
.ToPropertyEx(this, x => x.IsSimpleDNSEnabled);
_ = Init();

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