Compare commits

...

83 Commits

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

* Update Directory.Packages.props

* Update Directory.Packages.props
2026-05-20 09:01:22 +08:00
148 changed files with 6970 additions and 1742 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,130 +187,100 @@ 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.4/debian13-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/edk2-loongarch64-vars.fd
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
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.3
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.3
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.3.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

@@ -46,13 +46,29 @@ 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.3.0
with:
dotnet-version: '10.0.1xx'
@@ -68,4 +84,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.3.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,71 @@
# 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)
---
## 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.2</Version>
<Version>7.23.1</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.15" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" />
<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.4" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4-preview.1.1" />
</ItemGroup>
</Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.17" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.17" />
<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.2" />
<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

@@ -554,7 +554,7 @@ 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));
}
}

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,61 @@
using AwesomeAssertions;
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");
}
}

View File

@@ -0,0 +1,92 @@
namespace ServiceLib.Common;
/// <summary>
/// Extension methods for country code utilities
/// </summary>
public static class CountryExtension
{
/// <summary>
/// Country code to emoji flag mapping for common countries
/// </summary>
private static readonly Dictionary<string, string> CountryEmojiMap = new(StringComparer.OrdinalIgnoreCase)
{
// Asia
{ "CN", "🇨🇳" }, // China
{ "HK", "🇭🇰" }, // Hong Kong
{ "TW", "🇹🇼" }, // Taiwan
{ "JP", "🇯🇵" }, // Japan
{ "SG", "🇸🇬" }, // Singapore
{ "KR", "🇰🇷" }, // South Korea
{ "TH", "🇹🇭" }, // Thailand
{ "VN", "🇻🇳" }, // Vietnam
{ "ID", "🇮🇩" }, // Indonesia
{ "PH", "🇵🇭" }, // Philippines
{ "MY", "🇲🇾" }, // Malaysia
{ "IN", "🇮🇳" }, // India
{ "PK", "🇵🇰" }, // Pakistan
{ "BD", "🇧🇩" }, // Bangladesh
{ "LK", "🇱🇰" }, // Sri Lanka
{ "KH", "🇰🇭" }, // Cambodia
{ "LA", "🇱🇦" }, // Laos
{ "MM", "🇲🇲" }, // Myanmar
// Americas
{ "US", "🇺🇸" }, // United States
{ "CA", "🇨🇦" }, // Canada
{ "MX", "🇲🇽" }, // Mexico
{ "BR", "🇧🇷" }, // Brazil
{ "AR", "🇦🇷" }, // Argentina
{ "CL", "🇨🇱" }, // Chile
{ "CO", "🇨🇴" }, // Colombia
// Europe
{ "GB", "🇬🇧" }, // United Kingdom
{ "DE", "🇩🇪" }, // Germany
{ "FR", "🇫🇷" }, // France
{ "IT", "🇮🇹" }, // Italy
{ "ES", "🇪🇸" }, // Spain
{ "RU", "🇷🇺" }, // Russia
{ "NL", "🇳🇱" }, // Netherlands
{ "CH", "🇨🇭" }, // Switzerland
{ "SE", "🇸🇪" }, // Sweden
{ "NO", "🇳🇴" }, // Norway
{ "DK", "🇩🇰" }, // Denmark
{ "FI", "🇫🇮" }, // Finland
{ "PL", "🇵🇱" }, // Poland
{ "CZ", "🇨🇿" }, // Czech Republic
{ "AT", "🇦🇹" }, // Austria
{ "GR", "🇬🇷" }, // Greece
{ "PT", "🇵🇹" }, // Portugal
{ "TR", "🇹🇷" }, // Turkey
{ "UA", "🇺🇦" }, // Ukraine
{ "RO", "🇷🇴" }, // Romania
// Middle East & Central Asia
{ "AE", "🇦🇪" }, // United Arab Emirates
{ "SA", "🇸🇦" }, // Saudi Arabia
{ "IL", "🇮🇱" }, // Israel
{ "KZ", "🇰🇿" }, // Kazakhstan
// Oceania
{ "AU", "🇦🇺" }, // Australia
{ "NZ", "🇳🇿" }, // New Zealand
// Africa
{ "ZA", "🇿🇦" }, // South Africa
{ "EG", "🇪🇬" }, // Egypt
};
/// <summary>
/// Converts country code to flag emoji using predefined mapping
/// Example: "US" -> "🇺🇸", "CN" -> "🇨🇳"
/// </summary>
public static string? CountryToEmoji(this string? countryCode)
{
if (countryCode.IsNullOrEmpty())
{
return null;
}
return CountryEmojiMap.GetValueOrDefault(countryCode);
}
}

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

@@ -12,6 +12,7 @@ public enum EServerColName
SubRemarks,
DelayVal,
SpeedVal,
IpInfo,
TodayDown,
TodayUp,

View File

@@ -7,6 +7,7 @@ public static class AppEvents
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
public static readonly EventChannel<bool> HasUpdateNotified = new();
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();

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,8 @@ 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 string SingboxDirectDNSTag = "direct_dns";
public const string SingboxRemoteDNSTag = "remote_dns";
@@ -149,6 +151,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 +162,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,10 +210,18 @@ 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://";
public const string SOCKS5Protocol = "socks5://";
public const string SOCKS4Protocol = "socks4://";
public static readonly Dictionary<EConfigType, string> ProtocolShares = new()
{
{ EConfigType.VMess, "vmess://" },
@@ -321,12 +336,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 =
@@ -395,6 +410,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",
@@ -412,13 +437,6 @@ public class Global
"stream-one"
];
public static readonly List<string> AllowInsecure =
[
"true",
"false",
""
];
public static readonly List<string> DomainStrategy =
[
"AsIs",
@@ -469,10 +487,11 @@ public class Global
"zh-Hans",
"zh-Hant",
"en",
"fa-Ir",
"fa",
"fr",
"ru",
"hu"
"hu",
"id"
];
public static readonly List<string> Alpns =
@@ -662,6 +681,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,
@@ -676,15 +703,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;
@@ -923,6 +963,7 @@ public static class ConfigHandler
Delay = t33?.Delay ?? 0,
Speed = t33?.Speed ?? 0,
Sort = t33?.Sort ?? 0,
IpInfo = t33?.IpInfo ?? string.Empty,
TodayDown = (t22?.TodayDown ?? 0).ToString("D16"),
TodayUp = (t22?.TodayUp ?? 0).ToString("D16"),
TotalDown = (t22?.TotalDown ?? 0).ToString("D16"),
@@ -943,6 +984,7 @@ public static class ConfigHandler
EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(),
EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(),
EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).ToList(),
EServerColName.IpInfo => lstProfile.OrderBy(t => t.IpInfo).ToList(),
EServerColName.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(),
EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(),
EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(),
@@ -963,6 +1005,7 @@ public static class ConfigHandler
EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(),
EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(),
EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).ToList(),
EServerColName.IpInfo => lstProfile.OrderByDescending(t => t.IpInfo).ToList(),
EServerColName.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(),
EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(),
EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(),
@@ -1053,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();
@@ -1103,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;
@@ -1451,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
@@ -1534,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)
{
@@ -1630,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

@@ -4,53 +4,41 @@ public static class ConnectionHandler
{
private static readonly string _tag = "ConnectionHandler";
/// <summary>
/// Runs ping and IP checks and returns a formatted result string.
/// </summary>
public static async Task<string> RunAvailabilityCheck()
{
var time = await GetRealPingTimeInfo();
var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None;
var ip = time > 0 ? await GetIPInfo() : Global.None;
return string.Format(ResUI.TestMeOutput, time, ip);
}
/// <summary>
/// Gets IP information using the default local proxy.
/// </summary>
private static async Task<string?> GetIPInfo()
{
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
if (url.IsNullOrEmpty())
{
return null;
}
var webProxy = await GetWebProxy();
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, true, "");
if (result == null)
{
return null;
}
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(result);
if (ipInfo == null)
{
return null;
}
var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query;
var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code;
return $"({country ?? "unknown"}) {ip}";
var ipInfo = await GetIPInfo(webProxy);
return ipInfo?.ToString() ?? Global.None;
}
/// <summary>
/// Measures real ping time using configured test URL.
/// </summary>
private static async Task<int> GetRealPingTimeInfo()
{
var responseTime = -1;
try
{
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}");
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var webProxy = await GetWebProxy();
for (var i = 0; i < 2; i++)
{
responseTime = await GetRealPingTime(url, webProxy, 10);
responseTime = await GetRealPingTime(webProxy, 10);
if (responseTime > 0)
{
break;
@@ -66,8 +54,21 @@ public static class ConnectionHandler
return responseTime;
}
public static async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
/// <summary>
/// Creates local SOCKS proxy instance.
/// </summary>
private static async Task<WebProxy?> GetWebProxy()
{
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
return new WebProxy($"socks5://{Global.Loopback}:{port}");
}
/// <summary>
/// Measures response time by sending HTTP requests through proxy.
/// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout)
{
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1;
try
{
@@ -79,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();
}
@@ -95,4 +96,41 @@ public static class ConnectionHandler
}
return responseTime;
}
/// <summary>
/// Gets IP and country information through specified proxy.
/// </summary>
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy)
{
try
{
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
if (url.IsNullOrEmpty())
{
return null;
}
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, webProxy, "");
if (result == null)
{
return null;
}
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(result);
if (ipInfo == null)
{
return null;
}
var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query;
var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code ?? "unknown";
return new IpInfoResult(country, ip);
}
catch
{
return null;
}
}
}

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

@@ -53,7 +53,9 @@ public class FmtHandler
{
return ShadowsocksFmt.Resolve(str, out msg);
}
else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS]))
else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS])
|| str.StartsWith(Global.SOCKS5Protocol)
|| str.StartsWith(Global.SOCKS4Protocol))
{
return SocksFmt.Resolve(str, out msg);
}
@@ -65,10 +67,16 @@ public class FmtHandler
{
return VLESSFmt.Resolve(str, out msg);
}
else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2]) || str.StartsWith(Global.Hysteria2ProtocolShare))
else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2])
|| str.StartsWith(Global.Hysteria2ProtocolShare))
{
return Hysteria2Fmt.Resolve(str, out msg);
}
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,154 @@ 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(),
});
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;
@@ -99,12 +99,17 @@ public class SocksFmt : BaseFmt
};
// parse base64 UserInfo
var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo);
var userInfo = Utils.Base64Decode(rawUserInfo);
var userInfoParts = userInfo.Split([':'], 2);
if (userInfoParts.Length == 2)
if (rawUserInfo.IsNotEmpty())
{
item.Username = userInfoParts.First();
item.Password = userInfoParts[1];
var userInfoParts = rawUserInfo.Contains(':')
? rawUserInfo.Split(":", 2)
: Utils.Base64Decode(rawUserInfo).Split(":", 2);
if (userInfoParts.Length == 2)
{
item.Username = userInfoParts.First();
item.Password = userInfoParts.Last();
}
}
return item;

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

@@ -268,7 +268,7 @@ public sealed class AppManager
.ToListAsync();
var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(id => itemMap.GetValueOrDefault(id))
return idList.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

@@ -230,7 +230,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()
@@ -150,6 +150,14 @@ public class ProfileExManager
IndexIdEnqueue(indexId);
}
public void SetTestIpInfo(string indexId, string ipInfo)
{
var profileEx = GetProfileExItem(indexId);
profileEx.IpInfo = ipInfo;
IndexIdEnqueue(indexId);
}
public void SetSort(string indexId, int sort)
{
var profileEx = GetProfileExItem(indexId);
@@ -174,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));
@@ -142,5 +142,10 @@ public class TaskManager
await _updateFunc?.Invoke(false, msg);
}
NoticeManager.Instance.Enqueue(string.Join("\n", msgs));
if (msgs.Count > 0)
{
AppEvents.HasUpdateNotified.Publish(true);
}
}
}

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,15 @@ 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 class Server4Sbox : BaseServer4Sbox
@@ -332,6 +343,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
@@ -372,6 +373,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 +489,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 +502,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,106 @@
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;
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

@@ -17,3 +17,12 @@ public class LocationInfo
{
public string? country_code { get; set; }
}
public readonly record struct IpInfoResult(string Country, string? Ip)
{
public override string ToString()
{
var emoji = Utils.IsWindows() ? null : Country.CountryToEmoji();
return $"{emoji}({Country}) {Ip}";
}
}

View File

@@ -26,6 +26,9 @@ public class ProfileItemModel : ReactiveObject
[Reactive]
public string SpeedVal { get; set; }
[Reactive]
public string IpInfo { get; set; }
[Reactive]
public string TodayUp { get; set; }

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

@@ -8,4 +8,6 @@ public class SpeedTestResult
public string? Delay { get; set; }
public string? Speed { get; set; }
public string? IpInfo { get; set; }
}

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

@@ -10,4 +10,5 @@ public class ProfileExItem
public decimal Speed { get; set; }
public int Sort { get; set; }
public string? Message { get; set; }
public string? IpInfo { get; set; }
}

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>
@@ -564,6 +573,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 IP Info 的本地化字符串。
/// </summary>
public static string LvTestIpInfo {
get {
return ResourceManager.GetString("LvTestIpInfo", resourceCulture);
}
}
/// <summary>
/// 查找类似 Speed (MB/s) 的本地化字符串。
/// </summary>
@@ -1311,6 +1329,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 New Update 的本地化字符串。
/// </summary>
public static string menuNewUpdate {
get {
return ResourceManager.GetString("menuNewUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 Open the storage location 的本地化字符串。
/// </summary>
@@ -1896,6 +1923,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration. 的本地化字符串。
/// </summary>
public static string MsgAllowInsecureDeprecated {
get {
return ResourceManager.GetString("MsgAllowInsecureDeprecated", resourceCulture);
}
}
/// <summary>
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
/// </summary>
@@ -2040,6 +2076,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks. 的本地化字符串。
/// </summary>
public static string MsgInsecureConfiguration {
get {
return ResourceManager.GetString("MsgInsecureConfiguration", resourceCulture);
}
}
/// <summary>
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
/// </summary>
@@ -2085,6 +2130,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Conflict between {0} and {1} 的本地化字符串。
/// </summary>
public static string MsgOptionsConflict {
get {
return ResourceManager.GetString("MsgOptionsConflict", resourceCulture);
}
}
/// <summary>
/// 查找类似 Resolved {0} successfully 的本地化字符串。
/// </summary>
@@ -2202,6 +2256,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Invalid address in TUN route exclude list: {0} 的本地化字符串。
/// </summary>
public static string MsgTunRouteExcludeInvalidAddress {
get {
return ResourceManager.GetString("MsgTunRouteExcludeInvalidAddress", resourceCulture);
}
}
/// <summary>
/// 查找类似 Unpacking... 的本地化字符串。
/// </summary>
@@ -2625,24 +2688,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Allow insecure cert fetch (self-signed) 的本地化字符串。
/// </summary>
public static string TbAllowInsecureCertFetch {
get {
return ResourceManager.GetString("TbAllowInsecureCertFetch", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only for fetching self-signed certificates. This may expose you to MITM risks. 的本地化字符串。
/// </summary>
public static string TbAllowInsecureCertFetchTips {
get {
return ResourceManager.GetString("TbAllowInsecureCertFetchTips", resourceCulture);
}
}
/// <summary>
/// 查找类似 ALPN 的本地化字符串。
/// </summary>
@@ -2988,6 +3033,33 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Enable Final Fragment 的本地化字符串。
/// </summary>
public static string TbEnableFinalFragment {
get {
return ResourceManager.GetString("TbEnableFinalFragment", resourceCulture);
}
}
/// <summary>
/// 查找类似 Split the tail of packets into smaller fragments. This may affect throughput and latency. 的本地化字符串。
/// </summary>
public static string TbEnableFinalFragmentTip {
get {
return ResourceManager.GetString("TbEnableFinalFragmentTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 DNS via Bridge 的本地化字符串。
/// </summary>
public static string TbEnabletDnsViaProxy {
get {
return ResourceManager.GetString("TbEnabletDnsViaProxy", resourceCulture);
}
}
/// <summary>
/// 查找类似 Enable Tun 的本地化字符串。
/// </summary>
@@ -3096,6 +3168,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
/// </summary>
public static string TbGeckoPacketSize {
get {
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
}
}
/// <summary>
/// 查找类似 Global Hotkey Settings 的本地化字符串。
/// </summary>
@@ -3159,6 +3240,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Realm URL 的本地化字符串。
/// </summary>
public static string TbHy2RealmUrl {
get {
return ResourceManager.GetString("TbHy2RealmUrl", resourceCulture);
}
}
/// <summary>
/// 查找类似 Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port] 的本地化字符串。
/// </summary>
public static string TbHy2RealmUrlTip {
get {
return ResourceManager.GetString("TbHy2RealmUrlTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 ICMP routing policy 的本地化字符串。
/// </summary>
@@ -3510,6 +3609,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Route Exclude Address 的本地化字符串。
/// </summary>
public static string TbRouteExcludeAddress {
get {
return ResourceManager.GetString("TbRouteExcludeAddress", resourceCulture);
}
}
/// <summary>
/// 查找类似 Use commas (,) to separate. 的本地化字符串。
/// </summary>
public static string TbRouteExcludeAddressTip {
get {
return ResourceManager.GetString("TbRouteExcludeAddressTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// </summary>
@@ -4032,6 +4149,96 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Fallback Delay 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentFallbackDelay {
get {
return ResourceManager.GetString("TbSettingsFragmentFallbackDelay", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentFallbackDelayTip {
get {
return ResourceManager.GetString("TbSettingsFragmentFallbackDelayTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Interval 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentInterval {
get {
return ResourceManager.GetString("TbSettingsFragmentInterval", resourceCulture);
}
}
/// <summary>
/// 查找类似 Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be &lt;= second. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentIntervalTip {
get {
return ResourceManager.GetString("TbSettingsFragmentIntervalTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Length 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentLength {
get {
return ResourceManager.GetString("TbSettingsFragmentLength", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment size range in bytes (e.g., 50-100). First value must be &lt;= second. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentLengthTip {
get {
return ResourceManager.GetString("TbSettingsFragmentLengthTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Max Split 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentMaxSplit {
get {
return ResourceManager.GetString("TbSettingsFragmentMaxSplit", resourceCulture);
}
}
/// <summary>
/// 查找类似 Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentMaxSplitTip {
get {
return ResourceManager.GetString("TbSettingsFragmentMaxSplitTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Packets 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentPackets {
get {
return ResourceManager.GetString("TbSettingsFragmentPackets", resourceCulture);
}
}
/// <summary>
/// 查找类似 Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets) 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentPacketsTip {
get {
return ResourceManager.GetString("TbSettingsFragmentPacketsTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Geo files source (optional) 的本地化字符串。
/// </summary>
@@ -4680,6 +4887,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Verify Peer Cert By Name 的本地化字符串。
/// </summary>
public static string TbVerifyPeerCertByName {
get {
return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture);
}
}
/// <summary>
/// 查找类似 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>
@@ -1740,4 +1764,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

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,24 +1741,30 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN</value>
</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>Détails IP</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<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>
@@ -1740,4 +1764,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

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,9 +1746,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>
@@ -1740,4 +1767,49 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration.</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Route Exclude Address</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>Use commas (,) to separate.</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Invalid address in TUN route exclude list: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>Conflict between {0} and {1}</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>Enable Final Fragment</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS via Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Invalid Realm URL.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko Packet Size (min/max)</value>
</data>
</root>

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,9 +1743,6 @@
<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>
@@ -1732,12 +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</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<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>
@@ -1737,4 +1761,52 @@
<data name="MsgNotSupport" xml:space="preserve">
<value>不支持</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP 信息</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>有更新</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 </value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>路由排除地址</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>使用逗号 (,) 分隔</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>TUN 路由排除列表包含无效地址:{0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>{0} 与 {1} 冲突</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>启用末端分片</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS 通过 Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>检测到不安全配置AllowInsecure 已启用,但未提供证书。这可能会导致中间人攻击。</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Realm URL 不正确。</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>格式realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko 包大小 (最小/最大)</value>
</data>
</root>

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>
@@ -1737,4 +1761,37 @@
<data name="MsgNotSupport" xml:space="preserve">
<value>不支援</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP 資訊</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>有更新</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。</value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>路由排除位址</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>使用逗號 (,) 分隔</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>TUN 路由排除清單中的位址無效: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>{0} 與 {1} 發生衝突</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>啟用末端分片</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>將封包末端拆分為更小的片段進行傳送,可能會影響吞吐量與延遲</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS 透過 Bridge</value>
</data>
</root>

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,21 @@ public partial class CoreConfigSingboxService
}
}
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
&& realm is not null)
{
var realm4Sbox = new HyRealm4Sbox()
{
server_url = realm.RendezvousHostPort,
token = realm.Token,
realm_id = realm.RealmName,
};
outbound.realm = realm4Sbox;
outbound.server = null;
outbound.server_port = null;
outbound.server_ports = null;
}
break;
}
case EConfigType.TUIC:
@@ -342,7 +364,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 +418,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 +751,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);
}
}
@@ -104,8 +104,9 @@ public partial class CoreConfigV2rayService
},
},
tag = balancerTag,
fallbackTag = multipleLoad == EMultipleLoad.Fallback ? Global.DirectTag : null,
};
_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

@@ -3,7 +3,7 @@ using System.Net.Http.Headers;
namespace ServiceLib.Services;
/// <summary>
///Download
/// Download
/// </summary>
public class DownloadService
{
@@ -13,7 +13,10 @@ public class DownloadService
private static readonly string _tag = "DownloadService";
public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
/// <summary>
/// Downloads data with the specified proxy and reports progress messages.
/// </summary>
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
{
try
{
@@ -36,6 +39,9 @@ public class DownloadService
return 0;
}
/// <summary>
/// Downloads a file and reports progress through events.
/// </summary>
public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout)
{
try
@@ -64,6 +70,9 @@ public class DownloadService
}
}
/// <summary>
/// Gets redirect target URL without following redirects automatically.
/// </summary>
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
{
var webRequestHandler = new SocketsHttpHandler
@@ -86,11 +95,23 @@ public class DownloadService
}
}
/// <summary>
/// Tries to download string content using proxy switch setting.
/// </summary>
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
{
var webProxy = await GetWebProxy(blProxy);
return await TryDownloadString(url, webProxy, userAgent);
}
/// <summary>
/// Tries to download string content with a specified proxy.
/// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
{
try
{
var result1 = await DownloadStringAsync(url, blProxy, userAgent, 15);
var result1 = await DownloadStringAsync(url, webProxy, userAgent, 15);
if (result1.IsNotEmpty())
{
return result1;
@@ -108,7 +129,7 @@ public class DownloadService
try
{
var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15);
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, 15);
if (result2.IsNotEmpty())
{
return result2;
@@ -128,14 +149,12 @@ public class DownloadService
}
/// <summary>
/// DownloadString
/// Downloads string content via HttpClient.
/// </summary>
/// <param name="url"></param>
private async Task<string?> DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
{
try
{
var webProxy = await GetWebProxy(blProxy);
var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
@@ -172,15 +191,12 @@ public class DownloadService
}
/// <summary>
/// DownloadString
/// Downloads string content via DownloaderHelper.
/// </summary>
/// <param name="url"></param>
private async Task<string?> DownloadStringViaDownloader(string url, bool blProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
{
try
{
var webProxy = await GetWebProxy(blProxy);
if (userAgent.IsNullOrEmpty())
{
userAgent = Utils.GetVersion(false);
@@ -200,6 +216,9 @@ public class DownloadService
return null;
}
/// <summary>
/// Creates local SOCKS proxy when proxy switch is enabled.
/// </summary>
private async Task<WebProxy?> GetWebProxy(bool blProxy)
{
if (!blProxy)
@@ -215,6 +234,9 @@ public class DownloadService
return new WebProxy($"socks5://{Global.Loopback}:{port}");
}
/// <summary>
/// Checks whether the specified TCP endpoint is reachable.
/// </summary>
private async Task<bool> SocketCheck(string ip, int port)
{
try

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))
@@ -392,10 +420,23 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<int> DoRealPing(ServerTestItem it)
{
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, 10);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
if (responseTime > 0)
{
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
var ipStr = ipInfo?.ToString() ?? Global.None;
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
await UpdateIpInfoFunc(it.IndexId, ipStr);
}
else
{
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
}
return responseTime;
}
@@ -467,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();
@@ -491,4 +532,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestMessage(indexId, speed);
}
}
private async Task UpdateIpInfoFunc(string indexId, string ip)
{
await _updateFunc?.Invoke(new() { IndexId = indexId, IpInfo = ip });
}
}

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))
{

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