Compare commits

...

66 Commits

Author SHA1 Message Date
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
2dust
0f3fc8e053 up 7.22.2 2026-05-18 17:36:05 +08:00
2dust
f7206f3405 Improve CheckUpdateModel 2026-05-18 17:32:07 +08:00
2dust
460a674ebc Add 'Check Only' update action
Introduce a new "Check Only" feature: add CheckOnlyCmd to CheckUpdateViewModel with CheckOnlyTask that queries updates (via UpdateService.CheckHasUpdateOnly) for selected cores and reports results without performing updates. Wire up a new btnCheckOnly in both Desktop and Avalonia views and bind the command. Add localized menuCheckOnly entries to multiple .resx files and update ResUI.Designer. Also shorten the pre-release label to "Check for pre-release" in resource files.
2026-05-17 19:15:57 +08:00
DHR60
e2428f2500 Add tun inbound rule (#9327) 2026-05-17 18:54:12 +08:00
DHR60
bc3cbb4277 Fix (#9325)
* Fix

* Rename tun tag
2026-05-17 18:52:42 +08:00
2dust
ac9d0a0193 Add periodic update checks and core support 2026-05-17 17:09:34 +08:00
2dust
2291214b6f up 7.22.1 2026-05-16 19:00:46 +08:00
Miheichev Aleksandr Sergeevich
5b47d8ba05 i18n(ru): translate untranslated PreSharedKey label and Export menu (#9309)
Two strings in ResUI.ru.resx were left with their English (or identifier) values and surfaced as untranslated text on a Russian UI culture. This commit translates both, keeping the existing translation style and the existing technical vocabulary established earlier in this file.

Changes:

1. TbPreSharedKey: 'PreSharedKey' -> 'Общий ключ (PSK)'

   The label is the WireGuard pre-shared-key field. The sibling WireGuard fields are already translated in the same file (TbPublicKey -> 'Открытый ключ', TbPrivateKey -> 'Приватный ключ'), so leaving this one as the raw identifier was inconsistent. 'Общий ключ' matches the wording used in Russian-localized network UIs for this concept (NetworkManager, MikroTik, OpenVPN GUIs); the '(PSK)' suffix preserves the technical abbreviation so users familiar with the WireGuard documentation immediately recognize the field.

2. menuExport2InnerUri: 'Export v2rayN Internal Share Link to Clipboard' -> 'Экспорт внутренней ссылки v2rayN в буфер обмена'

   This context-menu item was added recently and the Russian resource kept the English string verbatim. The translation follows the convention of the sibling export-to-clipboard items (menuExport2ShareUrlBase64 uses 'Экспорт ... в буфер обмена'), and 'внутренней ссылки v2rayN' preserves the 'internal' qualifier because this share-link format is specific to v2rayN's own importer and not interchangeable with the standard VMess/VLESS/Trojan/etc. URI schemes.

Verified:

- Diff scope: only ResUI.ru.resx, only the two <value> elements; the .resx XML schema headers and all other keys are untouched.

- Full audit of ResUI.ru.resx vs ResUI.resx: every other key is present and translated; these were the only two strings still surfacing in English on a Russian UI culture.

- 'dotnet build v2rayN/v2rayN.sln -c Release' passes with 0 errors and 0 warnings.
2026-05-15 15:09:17 +08:00
JieXu
b193c39ad7 Remove patch for riscv (#9310)
* Delete .github/workflows/update-riscv-depand.yml

* Update package-rhel-riscv.sh

* Update package-debian-riscv.sh

* Create package-debian-loong.sh

* Update build-linux.yml

* Update build-linux.yml

* Update package-debian-loong.sh

* Update build-linux.yml

* Update Directory.Packages.props
2026-05-15 15:09:04 +08:00
2dust
e68842bb78 Add SkiaSharp Linux native assets package
Add SkiaSharp.NativeAssets.Linux (v3.119.1) to Directory.Packages.props and add a PackageReference in v2rayN.Desktop.csproj so the Linux native SkiaSharp binaries are included for the desktop build/runtime.
2026-05-14 20:34:42 +08:00
JieXu
6c06c8a00a Update (#9303)
* Update DOTNET_RISCV_VERSION to 10.0.108

* Update DOTNET_RISCV_VERSION to 10.0.108
2026-05-14 19:27:31 +08:00
Miheichev Aleksandr Sergeevich
58d2641559 chore: remove NoWarn and fix .NET 10 build warnings across platforms (#9301)
* deps: bump ZXing.Net.Bindings.SkiaSharp from 0.16.14 to 0.16.22

Patch update to the latest stable release on the 0.16.x line. No breaking changes, no public API changes - purely internal fixes.

Verified by a full Release build of v2rayN.sln on .NET 10; no new warnings or errors are introduced.

* chore: remove NoWarn and fix .NET 10 build warnings

Removes the repository-level NoWarn suppression from Directory.Build.props and addresses the warnings that surface on top of the .NET 10 migration in #9179, keeping Debug, Release, and cross-platform publishes warning-free without suppressing warnings globally.

Changes:

- Removes <NoWarn>CA1031;CS1591;NU1507;CA1416;IDE0058;IDE0053;IDE0200</NoWarn> from Directory.Build.props.

- Annotates Windows-only APIs with [SupportedOSPlatform] and [SupportedOSPlatformGuard] so CA1416 accepts that the Windows surface is gated behind Utils.IsWindows() / Utils.IsNonWindows().

- Splits Utils.SetUnixFileMode into a cross-platform wrapper and a private [UnsupportedOSPlatform("windows")] implementation so File.SetUnixFileMode never reaches the analyzer on Windows builds.

- Adds a parameterless constructor to MessageBoxDialog so Avalonia's runtime XAML loader (AVLN3001) can instantiate the dialog.

- Moves the WPF high-DPI configuration from app.manifest to <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode> in v2rayN.csproj, fixing WFO0003.

- Adds global using System.Runtime.Versioning; to ServiceLib and v2rayN.Desktop so the platform attributes are usable project-wide.

* test: make cycle dependency tests locale-independent

Accept the localized Russian cycle dependency diagnostic in CoreConfigContextBuilderTests so the assertions pass when tests run under a Russian UI culture.

* fix: tighten Unix platform handling

Adds Linux and macOS platform guards so the analyzer can narrow calls through Utils.IsLinux() and Utils.IsMacOS().

Marks the Linux/macOS autostart and system proxy helpers with explicit platform attributes.

Updates Utils.GetSystemHosts() to read /etc/hosts on Linux and macOS while keeping the existing Windows hosts and hosts.ics merge behavior.
2026-05-14 19:25:07 +08:00
2dust
780777068d up 7.22.0 2026-05-13 09:34:18 +08:00
2dust
8446b4df8b Bump package versions in Directory.Packages.props 2026-05-13 09:32:05 +08:00
dependabot[bot]
3778d2058e Bump actions/checkout from 5 to 6 (#9288)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [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/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  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-05-13 09:08:58 +08:00
2dust
5c85598cfa Update GlobalHotKeys 2026-05-13 09:07:38 +08:00
2dust
51b384e119 Bug fix
https://github.com/2dust/v2rayN/issues/9277
2026-05-12 09:10:22 +08:00
JieXu
d1c9c0b536 Update .NET 10 (#9239)
* Update dotnet to 10

* Update .NET version in build workflow

* Update build-osx.yml

* Update package-zip.yml

* Update package-rhel.sh

* Update print statement from 'Hello' to 'Goodbye'

* Update package-rhel-riscv.sh

* Create package-debian-riscv.sh

* Update build-linux.yml

* Create update-riscv-depand.yml

* Update update-riscv-depand.yml

* Update ServiceLib.csproj

* Update Directory.Packages.props

* Update UpdateService.cs

* Update UpdateService.cs

* Update Directory.Packages.props

* Update ServiceLib.csproj

* Replace SourceGear.sqlite3 with Repobot.SQLite.Unofficial

* Replace SourceGear.sqlite3 with Repobot.SQLite.Unofficial

* Update Repobot.SQLite.Unofficial version to 3.53.1.1

* Update package-zip.yml

* Update build-osx.yml

Adjust sleep duration to reduce race condition likelihood.

* Update Directory.Packages.props

* Update Directory.Packages.props

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: DHR60 <dehongren60@gmail.com>
Co-authored-by: xujie86 <167618598+xujie86@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 17:56:35 +08:00
DHR60
cc57f952db Update dotnet to 10 (#9179) 2026-05-11 16:35:26 +08:00
2dust
8090799ccc Refactor models into sub-namespaces
Move many model classes into new sub-namespaces (ServiceLib.Models.CoreConfigs, ServiceLib.Models.Configs, ServiceLib.Models.Dto, ServiceLib.Models.Entities). Update GlobalUsings in ServiceLib, v2rayN.Desktop, v2rayN and add a tests GlobalUsings file to reference the new namespaces. Adjust static using directives in ClashApiManager and ClashProxiesViewModel to use ServiceLib.Models.Dto. This is a reorganization/rename of files and namespaces with no functional changes.
2026-05-11 11:00:19 +08:00
DHR60
700f98193a Fix (#9274) 2026-05-11 09:57:16 +08:00
2dust
eee43288a4 up 7.21.3 2026-05-10 17:58:35 +08:00
2dust
61ff871dd2 Update GlobalHotKeys 2026-05-10 17:54:50 +08:00
DHR60
f8bc706cda Fix (#9271)
* Fix

* Fix res and uri

* Fix core config
2026-05-10 14:21:51 +08:00
2dust
e7973840ce up 7.21.2 2026-05-08 20:11:01 +08:00
VinnyTheFemboy
212071681d Fix bind interface handling in desktop and sing-box (#9258)
* Fix desktop bind interface setting

* Fix sing-box bind interface config

* Update CoreConfigSingboxServiceTests.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-05-08 19:34:25 +08:00
tt2563
0f9bfeb275 Update Traditional Chinese translation (#9253)
* Update traditional Chinese translations in ResUI

 Update Traditional Chinese translation

* Update binding interface tip for clarity
2026-05-08 19:13:05 +08:00
VinnyTheFemboy
f5059f1165 Fix sing-box TUN custom config inbound (#9259) 2026-05-08 19:12:09 +08:00
DHR60
2dc967bc04 Fix (#9251) 2026-05-07 10:11:41 +08:00
DHR60
75ea81dd69 Inner uri (#9245)
* Inner uri import and export

* Add tests

* Fix

* Compress export length
2026-05-06 20:33:35 +08:00
DHR60
d13f7a4db6 Fix json compact (#9246) 2026-05-06 15:36:26 +08:00
DHR60
f073b14fcc Add PR test (#9244)
* Add pr test

* Fix
2026-05-06 15:35:53 +08:00
DHR60
1a44af33d0 Wireguard (#9241)
* Adjust mtu

* Add Wireguard PresharedKey

* WireGuard config import

* AI opt

* Opt
2026-05-06 15:35:10 +08:00
DHR60
8450f2e420 Add bind interface (#9222) 2026-05-04 14:38:32 +08:00
DHR60
37ef25cbfe Fix (#9235)
* Perf routing

* GeoPrefix move to Global

* Fix negative rules

* Fix
2026-05-03 16:09:47 +08:00
DHR60
0fac18ba95 Fix (#9230) 2026-05-02 19:01:51 +08:00
DHR60
3ccd59d1dc Fix (#9224) 2026-05-02 19:01:31 +08:00
152 changed files with 5736 additions and 1790 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

@@ -49,7 +49,7 @@ jobs:
libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
- name: Checkout repo (for scripts)
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
with:
submodules: 'recursive'
fetch-depth: '0'
@@ -151,7 +151,7 @@ jobs:
dnf repolist | grep -i epel || true
- name: Checkout repo (for scripts)
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
with:
submodules: 'recursive'
fetch-depth: '0'
@@ -251,3 +251,252 @@ jobs:
--data-binary @"$f" \
"${upload_url}?name=${f##*/}"
done
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/'))
runs-on: ubuntu-24.04-riscv
container: debian:13
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps:
- name: Prepare tools (Debian)
shell: bash
run: |
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
- name: Checkout repo (for scripts)
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
rm -rf ./*
git init .
git config --global --add safe.directory "$PWD"
git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
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
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
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
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
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
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
- 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
mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64/ "$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 }}
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
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 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 \
-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 \
-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 "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 "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 "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
}
}
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
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
- 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

View File

@@ -45,7 +45,7 @@ jobs:
}}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
- name: Restore build artifacts
uses: actions/download-artifact@v8
@@ -63,7 +63,7 @@ jobs:
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
- name: Sleep for race condition between matrix jobs
run: sleep $(awk 'BEGIN { srand(); printf "%.3f", rand()*2 }')
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

View File

@@ -46,15 +46,15 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
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'
dotnet-version: '10.0.1xx'
- name: Build v2rayN
working-directory: ./v2rayN
@@ -68,4 +68,4 @@ jobs:
uses: actions/upload-artifact@v7.0.1
with:
name: ${{ matrix.arch }}
path: ${{ matrix.arch }}
path: ${{ matrix.arch }}

View File

@@ -37,7 +37,7 @@ jobs:
}}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
- name: Restore build artifacts
uses: actions/download-artifact@v8
@@ -56,8 +56,8 @@ jobs:
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
- name: Sleep for race condition between matrix jobs
run: sleep $(awk 'BEGIN { srand(); printf "%.3f", rand()*2 }')
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
with:

29
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Code Test
on:
pull_request:
branches:
- master
paths:
- 'v2rayN/ServiceLib/Services/CoreConfig/**'
- 'v2rayN/ServiceLib/Handler/Fmt/**'
- '.github/workflows/test.yml'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: 'recursive'
fetch-depth: '0'
- name: Setup .NET
uses: actions/setup-dotnet@v5.3.0
with:
dotnet-version: '8.0.x'
- name: Test Code
working-directory: ./v2rayN
run: dotnet test ./ServiceLib.Tests

742
package-debian-loong.sh Normal file
View File

@@ -0,0 +1,742 @@
#!/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"
OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.108"
DOTNET_LOONGARCH_TAG="v10.0.108-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_DEBS=()
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
debian)
echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}"
;;
*)
die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}).
This script only supports: Debian."
;;
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=""
mkdir -p "$OUTPUT_DIR"
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get -y install \
curl unzip tar jq rsync ca-certificates git dpkg-dev fakeroot file \
desktop-file-utils xdg-utils wget gcc make pkg-config \
libicu-dev libssl-dev libfontconfig1 libfreetype6 zlib1g
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=1
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, gcc, make, dpkg-deb, fakeroot, libicu-dev, libssl-dev"
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' "linux-loongarch64" "loong64" ;;
*) 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_launcher_file() {
local stage="$1"
install -m 755 /dev/stdin "$stage/usr/bin/v2rayn" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DIR="/opt/v2rayN"
cd "$DIR"
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
}
write_desktop_file() {
local stage="$1"
install -m 644 /dev/stdin "$stage/usr/share/applications/v2rayn.desktop" <<'EOF'
[Desktop Entry]
Type=Application
Name=v2rayN
Comment=v2rayN for Debian GNU Linux
Exec=v2rayn
Icon=v2rayn
Terminal=false
Categories=Network;
EOF
}
write_maintainer_scripts() {
local debian_dir="$1"
install -m 755 /dev/stdin "$debian_dir/postinst" <<'EOF'
#!/bin/sh
set -e
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
exit 0
EOF
install -m 755 /dev/stdin "$debian_dir/postrm" <<'EOF'
#!/bin/sh
set -e
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
exit 0
EOF
}
package_binary() {
local short="$1"
local rid="$2"
local deb_arch="$3"
local pubdir=""
local workdir=""
local stage=""
local debian_dir=""
local project_dir=""
local icon_candidate=""
local shlibs_depends=""
local extra_depends=""
local final_depends=""
local multiarch=""
local sys_libdir=""
local sys_usrlibdir=""
local deb_out=""
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
stage="$workdir/${PKGROOT}_${VERSION}_${deb_arch}"
debian_dir="$stage/DEBIAN"
mkdir -p "$stage/opt/v2rayN" "$stage/usr/bin" "$stage/usr/share/applications" "$stage/usr/share/icons/hicolor/256x256/apps" "$debian_dir"
cp -a "$pubdir/." "$stage/opt/v2rayN/"
project_dir="$(cd "$(dirname "$PROJECT")" && pwd)"
icon_candidate="$project_dir/v2rayN.png"
[[ -f "$icon_candidate" ]] && cp "$icon_candidate" "$stage/usr/share/icons/hicolor/256x256/apps/v2rayn.png" || true
stage_runtime_assets "$stage/opt/v2rayN" "$rid"
write_launcher_file "$stage"
write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
mkdir -p "$workdir/debian"
cat > "$workdir/debian/control" <<EOF
Source: v2rayn
Section: net
Priority: optional
Maintainer: 2dust <noreply@github.com>
Standards-Version: 4.7.0
Package: v2rayn
Architecture: ${deb_arch}
Description: v2rayN
EOF
multiarch="$(dpkg-architecture -a"$deb_arch" -qDEB_HOST_MULTIARCH)"
sys_libdir="/lib/$multiarch"
sys_usrlibdir="/usr/lib/$multiarch"
: > "$debian_dir/substvars"
mapfile -t ELF_FILES < <(
find "$stage/opt/v2rayN" -type f \( -name "*.so*" -o -perm -111 \) ! -name 'libcoreclrtraceptprovider.so'
)
if [[ "${#ELF_FILES[@]}" -gt 0 ]]; then
(
cd "$workdir"
dpkg-shlibdeps \
-l"$stage/opt/v2rayN" \
-l"$sys_libdir" \
-l"$sys_usrlibdir" \
-T"$debian_dir/substvars" \
"${ELF_FILES[@]}"
) >/dev/null 2>&1 || true
fi
shlibs_depends="$(sed -n 's/^shlibs:Depends=//p' "$debian_dir/substvars" | head -n1 || true)"
if [[ -n "$shlibs_depends" ]]; then
shlibs_depends="$(echo "$shlibs_depends" \
| sed -E 's/ *\([^)]*\)//g' \
| sed -E 's/ *, */, /g' \
| sed -E 's/^, *//; s/, *$//')"
final_depends="${shlibs_depends}, ${extra_depends}"
else
final_depends="${extra_depends}"
fi
cat > "$debian_dir/control" <<EOF
Package: v2rayn
Version: ${VERSION}
Architecture: ${deb_arch}
Maintainer: 2dust <noreply@github.com>
Homepage: https://github.com/2dust/v2rayN
Section: net
Priority: optional
Depends: ${final_depends}
Description: v2rayN (Avalonia) GUI client for Linux
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 /
Shadowsocks / tuic / WireGuard.
EOF
find "$stage/opt/v2rayN" -type d -exec chmod 0755 {} +
find "$stage/opt/v2rayN" -type f -exec chmod 0644 {} +
[[ -f "$stage/opt/v2rayN/v2rayN" ]] && chmod 0755 "$stage/opt/v2rayN/v2rayN" || true
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
dpkg-deb --root-owner-group --build "$stage" "$deb_out"
echo "Build done for $short. DEB at:"
echo " $deb_out"
BUILT_DEBS+=("$deb_out")
}
select_targets() {
printf '%s\n' loongarch64
}
build_one_target() {
local short="$1"
local meta=()
local rid=""
local deb_arch=""
mapfile -t meta < <(describe_target "$short") || return 1
rid="${meta[0]}"
deb_arch="${meta[1]}"
echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)"
publish_binary "$rid"
package_binary "$short" "$rid" "$deb_arch"
}
print_summary() {
local pkg=""
echo ""
echo "================ Build Summary ================="
if [[ "${#BUILT_DEBS[@]}" -gt 0 ]]; then
echo "Output directory: $OUTPUT_DIR"
for pkg in "${BUILT_DEBS[@]}"; do
echo "$pkg"
done
else
echo "No DEBs 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 "$@"

727
package-debian-riscv.sh Normal file
View File

@@ -0,0 +1,727 @@
#!/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"
OUTPUT_DIR="${HOME}/debbuild"
DOTNET_RISCV_VERSION="10.0.108"
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}"
OS_ID=""
OS_NAME=""
OS_VERSION_ID=""
HOST_ARCH=""
SCRIPT_DIR=""
PROJECT=""
VERSION=""
declare -a BUILT_DEBS=()
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
debian)
echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}"
;;
*)
die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}).
This script only supports: Debian."
;;
esac
case "$HOST_ARCH" in
riscv64) ;;
*) die "Only supports riscv64" ;;
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=""
mkdir -p "$OUTPUT_DIR"
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get -y install \
curl unzip tar jq rsync ca-certificates git dpkg-dev fakeroot file \
desktop-file-utils xdg-utils wget gcc make pkg-config \
libicu-dev libssl-dev libfontconfig1 libfreetype6 zlib1g
mkdir -p "$HOME/.dotnet"
tmp_dotnet="$(mktemp -d)"
curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_RISCV_FILE"
tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_RISCV_FILE"
rm -rf "$tmp_dotnet"
export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
dotnet --info >/dev/null 2>&1 && install_ok=1
fi
if [[ "$install_ok" -ne 1 ]]; then
echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:"
echo "dotnet-riscv SDK, curl, unzip, tar, rsync, git, gcc, make, dpkg-deb, fakeroot, libicu-dev, libssl-dev"
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-riscv64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-riscv64.zip" ;;
*) return 1 ;;
esac
}
singbox_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-riscv64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-riscv64.tar.gz" ;;
*) return 1 ;;
esac
}
bundle_url_for_rid() {
local rid="$1"
case "$rid" in
linux-riscv64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-riscv64.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
riscv64) printf '%s\n%s\n' "linux-riscv64" "riscv64" ;;
*) echo "Unknown arch '$short' (use riscv64)" >&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_launcher_file() {
local stage="$1"
install -m 755 /dev/stdin "$stage/usr/bin/v2rayn" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DIR="/opt/v2rayN"
cd "$DIR"
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
}
write_desktop_file() {
local stage="$1"
install -m 644 /dev/stdin "$stage/usr/share/applications/v2rayn.desktop" <<'EOF'
[Desktop Entry]
Type=Application
Name=v2rayN
Comment=v2rayN for Debian GNU Linux
Exec=v2rayn
Icon=v2rayn
Terminal=false
Categories=Network;
EOF
}
write_maintainer_scripts() {
local debian_dir="$1"
install -m 755 /dev/stdin "$debian_dir/postinst" <<'EOF'
#!/bin/sh
set -e
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
exit 0
EOF
install -m 755 /dev/stdin "$debian_dir/postrm" <<'EOF'
#!/bin/sh
set -e
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
exit 0
EOF
}
package_binary() {
local short="$1"
local rid="$2"
local deb_arch="$3"
local pubdir=""
local workdir=""
local stage=""
local debian_dir=""
local project_dir=""
local icon_candidate=""
local shlibs_depends=""
local extra_depends=""
local final_depends=""
local multiarch=""
local sys_libdir=""
local sys_usrlibdir=""
local deb_out=""
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
stage="$workdir/${PKGROOT}_${VERSION}_${deb_arch}"
debian_dir="$stage/DEBIAN"
mkdir -p "$stage/opt/v2rayN" "$stage/usr/bin" "$stage/usr/share/applications" "$stage/usr/share/icons/hicolor/256x256/apps" "$debian_dir"
cp -a "$pubdir/." "$stage/opt/v2rayN/"
project_dir="$(cd "$(dirname "$PROJECT")" && pwd)"
icon_candidate="$project_dir/v2rayN.png"
[[ -f "$icon_candidate" ]] && cp "$icon_candidate" "$stage/usr/share/icons/hicolor/256x256/apps/v2rayn.png" || true
stage_runtime_assets "$stage/opt/v2rayN" "$rid"
write_launcher_file "$stage"
write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
mkdir -p "$workdir/debian"
cat > "$workdir/debian/control" <<EOF
Source: v2rayn
Section: net
Priority: optional
Maintainer: 2dust <noreply@github.com>
Standards-Version: 4.7.0
Package: v2rayn
Architecture: ${deb_arch}
Description: v2rayN
EOF
multiarch="$(dpkg-architecture -a"$deb_arch" -qDEB_HOST_MULTIARCH)"
sys_libdir="/lib/$multiarch"
sys_usrlibdir="/usr/lib/$multiarch"
: > "$debian_dir/substvars"
mapfile -t ELF_FILES < <(
find "$stage/opt/v2rayN" -type f \( -name "*.so*" -o -perm -111 \) ! -name 'libcoreclrtraceptprovider.so'
)
if [[ "${#ELF_FILES[@]}" -gt 0 ]]; then
(
cd "$workdir"
dpkg-shlibdeps \
-l"$stage/opt/v2rayN" \
-l"$sys_libdir" \
-l"$sys_usrlibdir" \
-T"$debian_dir/substvars" \
"${ELF_FILES[@]}"
) >/dev/null 2>&1 || true
fi
shlibs_depends="$(sed -n 's/^shlibs:Depends=//p' "$debian_dir/substvars" | head -n1 || true)"
if [[ -n "$shlibs_depends" ]]; then
shlibs_depends="$(echo "$shlibs_depends" \
| sed -E 's/ *\([^)]*\)//g' \
| sed -E 's/ *, */, /g' \
| sed -E 's/^, *//; s/, *$//')"
final_depends="${shlibs_depends}, ${extra_depends}"
else
final_depends="${extra_depends}"
fi
cat > "$debian_dir/control" <<EOF
Package: v2rayn
Version: ${VERSION}
Architecture: ${deb_arch}
Maintainer: 2dust <noreply@github.com>
Homepage: https://github.com/2dust/v2rayN
Section: net
Priority: optional
Depends: ${final_depends}
Description: v2rayN (Avalonia) GUI client for Linux
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 /
Shadowsocks / tuic / WireGuard.
EOF
find "$stage/opt/v2rayN" -type d -exec chmod 0755 {} +
find "$stage/opt/v2rayN" -type f -exec chmod 0644 {} +
[[ -f "$stage/opt/v2rayN/v2rayN" ]] && chmod 0755 "$stage/opt/v2rayN/v2rayN" || true
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
dpkg-deb --root-owner-group --build "$stage" "$deb_out"
echo "Build done for $short. DEB at:"
echo " $deb_out"
BUILT_DEBS+=("$deb_out")
}
select_targets() {
printf '%s\n' riscv64
}
build_one_target() {
local short="$1"
local meta=()
local rid=""
local deb_arch=""
mapfile -t meta < <(describe_target "$short") || return 1
rid="${meta[0]}"
deb_arch="${meta[1]}"
echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)"
publish_binary "$rid"
package_binary "$short" "$rid" "$deb_arch"
}
print_summary() {
local pkg=""
echo ""
echo "================ Build Summary ================="
if [[ "${#BUILT_DEBS[@]}" -gt 0 ]]; then
echo "Output directory: $OUTPUT_DIR"
for pkg in "${BUILT_DEBS[@]}"; do
echo "$pkg"
done
else
echo "No DEBs 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 "$@"

File diff suppressed because it is too large Load Diff

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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail
# Require Red Hat base branch
. /etc/os-release
VERSION_ARG=""
WITH_CORE="both"
FORCE_NETCORE=0
ARCH_OVERRIDE=""
BUILD_FROM=""
XRAY_VER="${XRAY_VER:-}"
SING_VER="${SING_VER:-}"
case "${ID:-}" in
rhel|rocky|almalinux|fedora|centos)
echo "Detected supported system: ${NAME:-$ID} ${VERSION_ID:-}"
;;
*)
echo "Unsupported system: ${NAME:-unknown} (${ID:-unknown})."
echo "This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS."
exit 1
;;
esac
MIN_KERNEL="6.12"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild"
# Kernel version
MIN_KERNEL="6.11"
CURRENT_KERNEL="$(uname -r)"
OS_ID=""
OS_NAME=""
OS_VERSION_ID=""
HOST_ARCH=""
SCRIPT_DIR=""
PROJECT=""
VERSION=""
BUILT_ALL=0
lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$CURRENT_KERNEL" | sort -V | head -n1)"
declare -a BUILT_RPMS=()
if [[ "$lowest" != "$MIN_KERNEL" ]]; then
echo "Kernel $CURRENT_KERNEL is below $MIN_KERNEL"
exit 1
fi
echo "[OK] Kernel $CURRENT_KERNEL verified."
# Config & Parse arguments
VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty
WITH_CORE="both" # Default: bundle both xray+sing-box
FORCE_NETCORE=0 # --netcore => skip archive bundle, use separate downloads
ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target)
BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively
# If the first argument starts with --, do not treat it as a version number
if [[ "${VERSION_ARG:-}" == --* ]]; then
VERSION_ARG=""
fi
# Take the first non --* argument as version, discard it
if [[ -n "${VERSION_ARG:-}" ]]; then shift || true; fi
# Parse remaining optional arguments
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;;
--arch) ARCH_OVERRIDE="${2:-}"; shift 2;;
--buildfrom) BUILD_FROM="${2:-}"; shift 2;;
*)
if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi
shift;;
esac
done
# Conflict: version number AND --buildfrom cannot be used together
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
echo "You cannot specify both an explicit version and --buildfrom at the same time."
echo " Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
die() {
echo "$*" >&2
exit 1
fi
}
# Check and install dependencies
host_arch="$(uname -m)"
[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; }
parse_args() {
local first_arg="${1:-}"
install_ok=0
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install rpm-build rpmdevtools curl unzip tar jq rsync dotnet-sdk-8.0 \
&& install_ok=1
fi
if [[ "$install_ok" -ne 1 ]]; then
echo "Could not auto-install dependencies for '$ID'. Make sure these are available:"
echo "dotnet-sdk 8.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on Red Hat branch)"
fi
# Root directory
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Git submodules (best effort)
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
# Locate project
PROJECT="v2rayN.Desktop/v2rayN.Desktop.csproj"
if [[ ! -f "$PROJECT" ]]; then
PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
fi
[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; }
choose_channel() {
# If --buildfrom provided, map it directly and skip interaction.
if [[ -n "${BUILD_FROM:-}" ]]; then
case "$BUILD_FROM" in
1) echo "latest"; return 0;;
2) echo "prerelease"; return 0;;
3) echo "keep"; return 0;;
*) echo "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." >&2; exit 1;;
esac
if [[ -n "$first_arg" && "$first_arg" != --* ]]; then
VERSION_ARG="$first_arg"
shift || true
fi
# Print menu to stderr and read from /dev/tty so stdout only carries the token.
local ch="latest" sel=""
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 ;;
--arch) ARCH_OVERRIDE="${2:-}"; shift 2 ;;
--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
x86_64|aarch64) ;;
*) die "Only supports aarch64 / x86_64" ;;
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
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install rpm-build rpmdevtools curl unzip tar jq rsync dotnet-sdk-10.0 \
&& install_ok=1
fi
if [[ "$install_ok" -ne 1 ]]; then
echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:"
echo "dotnet-sdk 10.x, curl, unzip, tar, rsync, 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
@@ -141,29 +164,35 @@ get_latest_tag_prerelease() {
| sed 's/^v//'
}
sync_submodules() {
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
}
git_try_checkout() {
# Try a series of refs and checkout when found.
local want="$1" ref=""
local want="$1"
local ref=""
if git rev-parse --git-dir >/dev/null 2>&1; then
git fetch --tags --force --prune --depth=1 || true
if git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then
ref="${want}"
fi
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}"
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
git checkout -f "$ref"
sync_submodules
return 0
fi
fi
return 1
}
apply_channel_or_keep() {
local ch="$1" tag
local ch="$1"
local tag=""
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
@@ -173,102 +202,154 @@ apply_channel_or_keep() {
fi
echo "[*] Resolving ${ch} tag from GitHub releases..."
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "Failed to resolve latest tag for channel '${ch}'."; exit 1; }
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" || { echo "Failed to checkout '${tag}'."; exit 1; }
git_try_checkout "$tag" || die "Failed to checkout '${tag}'."
VERSION="${tag#v}"
}
if git rev-parse --git-dir >/dev/null 2>&1; then
if [[ -n "${VERSION_ARG:-}" ]]; then
clean_ver="${VERSION_ARG#v}"
if git_try_checkout "$clean_ver"; then
VERSION="$clean_ver"
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
echo "[WARN] Tag '${VERSION_ARG}' not found."
ch="$(choose_channel)"
apply_channel_or_keep "$ch"
apply_channel_or_keep "$(choose_channel)"
fi
else
ch="$(choose_channel)"
apply_channel_or_keep "$ch"
echo "Current directory is not a git repo; proceeding on current tree."
VERSION="${VERSION_ARG:-0.0.0}"
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}"
VERSION="${VERSION#v}"
echo "[*] GUI version resolved as: ${VERSION}"
}
xray_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-x64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip" ;;
linux-arm64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip" ;;
*) return 1 ;;
esac
}
singbox_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-x64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz" ;;
linux-arm64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz" ;;
*) return 1 ;;
esac
}
bundle_url_for_rid() {
local rid="$1"
case "$rid" in
linux-x64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip" ;;
linux-arm64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64.zip" ;;
*) return 1 ;;
esac
}
# Helpers for core
download_xray() {
# Download Xray core
local outdir="$1" rid="$2" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
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
| 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; }
if [[ "$rid" == "linux-arm64" ]]; then
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip"
else
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip"
fi
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/$zipname"
unzip -q "$tmp/$zipname" -d "$tmp"
install -m 755 "$tmp/xray" "$outdir/xray"
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() {
# Download sing-box
local outdir="$1" rid="$2" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin cronet
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; }
if [[ "$rid" == "linux-arm64" ]]; then
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz"
else
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz"
fi
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/$tarname"
tar -C "$tmp" -xzf "$tmp/$tarname"
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"
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"
[[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true
rm -rf "$tmp"
}
# Move geo files to outroot/bin
unify_geo_layout() {
local outroot="$1"
mkdir -p "$outroot/bin"
local names=( \
"geosite.dat" \
"geoip.dat" \
"geoip-only-cn-private.dat" \
"Country.mmdb" \
"geoip.metadb" \
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"
@@ -276,58 +357,48 @@ unify_geo_layout() {
done
}
# Download geo/rule assets
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"
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" || true
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/rule-set-geoip/$f" || true
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/rule-set-geosite/$f" || true
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 to bin
unify_geo_layout "$outroot"
}
# Prefer the prebuilt v2rayN core bundle; then unify geo layout
download_v2rayn_bundle() {
local outroot="$1" rid="$2"
populate_assets_zip_mode() {
local outroot="$1"
local rid="$2"
local url=""
if [[ "$rid" == "linux-arm64" ]]; then
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64.zip"
else
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip"
fi
local tmp=""
local nested_dir=""
url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; }
echo "[+] Try v2rayN bundle archive: $url"
local tmp zipname
tmp="$(mktemp -d)"; zipname="$tmp/v2rayn.zip"
curl -fL "$url" -o "$zipname" || { echo "[!] Bundle download failed"; return 1; }
unzip -q "$zipname" -d "$tmp" || { echo "[!] Bundle unzip failed"; return 1; }
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"
@@ -339,7 +410,6 @@ download_v2rayn_bundle() {
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
local nested_dir
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"
@@ -347,109 +417,74 @@ download_v2rayn_bundle() {
rm -rf "$nested_dir"
fi
# Unify to bin/
unify_geo_layout "$outroot"
rm -rf "$tmp"
echo "[+] Bundle extracted to $outroot"
}
# ===== Build results collection for --arch all ========================================
BUILT_RPMS=() # Will collect absolute paths of built RPMs
BUILT_ALL=0 # Flag to know if we should print the final summary
populate_assets_netcore_mode() {
local outroot="$1"
local rid="$2"
# ===== Build (single-arch) function ====================================================
build_for_arch() {
# $1: target short arch: x64 | arm64
local short="$1"
local rid rpm_target archdir
case "$short" in
x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;;
arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;;
*) echo "Unknown arch '$short' (use x64|arm64)"; return 1;;
esac
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)"
fi
# .NET publish (self-contained) for this RID
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net8.0" || true
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)"
fi
dotnet restore "$PROJECT"
dotnet publish "$PROJECT" \
-c Release -r "$rid" \
-p:PublishSingleFile=false \
-p:SelfContained=true
download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)"
}
# Per-arch variables (scoped)
local RID_DIR="$rid"
local PUBDIR
PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish"
[[ -d "$PUBDIR" ]] || { echo "Publish directory not found: $PUBDIR"; return 1; }
stage_runtime_assets() {
local outroot="$1"
local rid="$2"
# Per-arch working area
local PKGROOT="v2rayN-publish"
local WORKDIR
WORKDIR="$(mktemp -d)"
trap '[[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"' RETURN
# rpmbuild topdir selection
local TOPDIR SPECDIR SOURCEDIR
rpmdev-setuptree
TOPDIR="${HOME}/rpmbuild"
SPECDIR="${TOPDIR}/SPECS"
SOURCEDIR="${TOPDIR}/SOURCES"
# Stage publish content
mkdir -p "$WORKDIR/$PKGROOT"
cp -a "$PUBDIR/." "$WORKDIR/$PKGROOT/"
# Required icon
local ICON_CANDIDATE
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"
# Prepare bin structure
mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box"
# Bundle / cores per-arch
fetch_separate_cores_and_rules() {
local outroot="$1"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" "$RID_DIR" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$outroot/bin/sing_box" "$RID_DIR" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)"
}
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
if [[ "$FORCE_NETCORE" -eq 0 ]]; then
if download_v2rayn_bundle "$WORKDIR/$PKGROOT" "$RID_DIR"; then
if populate_assets_zip_mode "$outroot" "$rid"; then
echo "[*] Using v2rayN bundle archive."
else
echo "[*] Bundle failed, fallback to separate core + rules."
fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT"
populate_assets_netcore_mode "$outroot" "$rid"
fi
else
echo "[*] --netcore specified: use separate core + rules."
fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT"
populate_assets_netcore_mode "$outroot" "$rid"
fi
}
# Tarball
mkdir -p "$SOURCEDIR"
tar -C "$WORKDIR" -czf "$SOURCEDIR/$PKGROOT.tar.gz" "$PKGROOT"
describe_target() {
local short="$1"
# SPEC
local SPECFILE="$SPECDIR/v2rayN.spec"
mkdir -p "$SPECDIR"
cat > "$SPECFILE" <<'SPEC'
case "$short" in
x64) printf '%s\n%s\n%s\n' "linux-x64" "x86_64" "x86_64" ;;
arm64) printf '%s\n%s\n%s\n' "linux-arm64" "aarch64" "aarch64" ;;
*) echo "Unknown arch '$short' (use x64|arm64)" >&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
# Ignore outdated LTTng dependencies incorrectly reported by the .NET runtime (to avoid installation failures)
%global __requires_exclude ^liblttng-ust\.so\..*$
Name: v2rayN
@@ -462,7 +497,6 @@ BugURL: https://github.com/2dust/v2rayN/issues
ExclusiveArch: aarch64 x86_64
Source0: __PKGROOT__.tar.gz
# Runtime dependencies (Avalonia / X11 / Fonts / GL)
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.34
Requires: fontconfig >= 2.13.1
@@ -483,28 +517,23 @@ https://github.com/2dust/v2rayN
%setup -q -n __PKGROOT__
%build
# no build
%install
install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/
# Normalize permissions
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 || :
# Launcher (prefer native ELF first, then DLL fallback)
install -dm0755 %{buildroot}%{_bindir}
install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash
set -euo pipefail
DIR="/opt/v2rayN"
# Prefer native apphost
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi
# DLL fallback
for dll in v2rayN.Desktop.dll v2rayN.dll; do
if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi
done
@@ -514,7 +543,6 @@ ls -l "$DIR" >&2 || true
exit 1
EOF
# Desktop file
install -dm0755 %{buildroot}%{_datadir}/applications
install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
[Desktop Entry]
@@ -527,7 +555,6 @@ Terminal=false
Categories=Network;
EOF
# Icon
install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
@@ -546,45 +573,129 @@ install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
SPEC
# Replace placeholders
sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE"
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE"
sed -i "s/__VERSION__/${VERSION}/g" "$specfile"
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$specfile"
}
# Build RPM for this arch
rpmbuild -ba "$SPECFILE" --target "$rpm_target"
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:"
local f
for f in "${TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do
for f in "${RPM_TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do
[[ -e "$f" ]] || continue
echo " $f"
BUILT_RPMS+=("$f")
done
}
# ===== Arch selection and build orchestration =========================================
case "${ARCH_OVERRIDE:-}" in
all) targets=(x64 arm64); BUILT_ALL=1 ;;
x64|amd64) targets=(x64) ;;
arm64|aarch64) targets=(arm64) ;;
"") targets=($([[ "$host_arch" == "aarch64" ]] && echo arm64 || echo x64)) ;;
*) echo "Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all."; exit 1 ;;
esac
select_targets() {
case "${ARCH_OVERRIDE:-}" in
all) printf '%s\n' x64 arm64 ;;
x64|amd64) printf '%s\n' x64 ;;
arm64|aarch64) printf '%s\n' arm64 ;;
"")
case "$HOST_ARCH" in
x86_64) printf '%s\n' x64 ;;
aarch64) printf '%s\n' arm64 ;;
*) return 1 ;;
esac
;;
*)
echo "Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all." >&2
return 1
;;
esac
}
for arch in "${targets[@]}"; do
build_for_arch "$arch"
done
build_one_target() {
local short="$1"
local meta=()
local rid=""
local rpm_target=""
local archdir=""
# Print Both arches information
if [[ "$BUILT_ALL" -eq 1 ]]; then
echo ""
echo "================ Build Summary (both architectures) ================"
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)."
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() {
if [[ "$BUILT_ALL" -eq 1 ]]; then
local rp=""
echo ""
echo "================ Build Summary (both architectures) ================"
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 "===================================================================="
fi
echo "===================================================================="
fi
}
main() {
local targets=()
local arch=""
parse_args "$@"
detect_environment
install_dependencies
prepare_workspace
resolve_version
mapfile -t targets < <(select_targets)
[[ "${ARCH_OVERRIDE:-}" == "all" ]] && BUILT_ALL=1 || BUILT_ALL=0
for arch in "${targets[@]}"; do
build_one_target "$arch"
done
print_summary
}
main "$@"

View File

@@ -1,14 +1,13 @@
<Project>
<PropertyGroup>
<Version>7.21.1</Version>
<Version>7.22.5</Version>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<NoWarn>CA1031;CS1591;NU1507;CA1416;IDE0058;IDE0053;IDE0200</NoWarn>
<Nullable>annotations</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Authors>2dust</Authors>

View File

@@ -1,36 +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.13" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.13" />
<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.2.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.1" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.7.3" />
<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.2" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<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="16.3.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
</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.16" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.16" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
<PackageVersion Include="CliWrap" Version="3.10.1" />
<PackageVersion Include="Downloader" Version="5.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.27" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.27" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.1.7" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup>
</Project>

View File

@@ -99,7 +99,8 @@ public class CoreConfigContextBuilderTests
{
return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase)
|| message.Contains("循环依赖", StringComparison.Ordinal)
|| message.Contains("循環依賴", StringComparison.Ordinal);
|| message.Contains("循環依賴", StringComparison.Ordinal)
|| message.Contains("циклическую зависимость", StringComparison.OrdinalIgnoreCase);
}
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)

View File

@@ -1,6 +1,7 @@
using AwesomeAssertions;
using ServiceLib.Common;
using ServiceLib.Enums;
using ServiceLib.Manager;
using ServiceLib.Models;
using ServiceLib.Services.CoreConfig;
using Xunit;
@@ -28,6 +29,54 @@ public class CoreConfigSingboxServiceTests
singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
}
[Fact]
public void GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box);
node.Address = Global.Loopback;
node.Port = 1080;
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
{
IsTunEnabled = true,
};
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
cfg.inbounds.Should().Contain(i =>
i.type == nameof(EInboundProtocol.mixed)
&& i.listen == Global.Loopback
&& i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks));
cfg.inbounds.Should().Contain(i => i.type == "tun");
}
[Fact]
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreBasicItem.BindInterface = "eth0";
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
{
IsTunEnabled = true,
};
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.bind_interface.Should().Be("eth0");
proxy.detour.Should().BeNullOrEmpty();
}
[Fact]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
{

View File

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

View File

@@ -0,0 +1,47 @@
using AwesomeAssertions;
using ServiceLib.Handler.Fmt;
using Xunit;
namespace ServiceLib.Tests.Fmt;
public class WireguardFmtTests
{
[Fact]
public void ResolveConfig_ShouldParsePeersAndIgnoreInlineComments()
{
const string config =
"""
[Interface]
PrivateKey = interface-private-key
Address = 10.0.0.2/32, fd00::2/128 ; inline comment
MTU = 1420
[Peer]
PublicKey = peer-public-key
PresharedKey = peer-preshared-key
Reserved = 1, 2, 3 # inline comment
Endpoint = [2001:db8::1]:51820 # inline comment
[Peer]
PublicKey = peer-public-key-2
Endpoint = example.com:12345
""";
var resolved = WireguardFmt.ResolveConfig(config);
resolved.Should().NotBeNull();
resolved.Should().HaveCount(2);
var first = resolved![0];
first.Address.Should().Be("2001:db8::1");
first.Port.Should().Be(51820);
first.Password.Should().Be("interface-private-key");
first.GetProtocolExtra().WgReserved.Should().Be("1, 2, 3");
first.GetProtocolExtra().WgInterfaceAddress.Should().Be("10.0.0.2/32, fd00::2/128");
first.GetProtocolExtra().WgMtu.Should().Be(1420);
var second = resolved[1];
second.Address.Should().Be("example.com");
second.Port.Should().Be(12345);
}
}

View File

@@ -0,0 +1,40 @@
global using System.Collections.Concurrent;
global using System.Diagnostics;
global using System.Net;
global using System.Net.NetworkInformation;
global using System.Net.Sockets;
global using System.Reactive;
global using System.Reactive.Disposables;
global using System.Reactive.Linq;
global using System.Reflection;
global using System.Runtime.InteropServices;
global using System.Security.Cryptography;
global using System.Text;
global using System.Text.Encodings.Web;
global using System.Text.Json;
global using System.Text.Json.Nodes;
global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions;
global using DynamicData;
global using DynamicData.Binding;
global using ReactiveUI;
global using ReactiveUI.Fody.Helpers;
global using ServiceLib.Base;
global using ServiceLib.Common;
global using ServiceLib.Enums;
global using ServiceLib.Events;
global using ServiceLib.Handler;
global using ServiceLib.Handler.Builder;
global using ServiceLib.Handler.Fmt;
global using ServiceLib.Handler.SysProxy;
global using ServiceLib.Helper;
global using ServiceLib.Manager;
global using ServiceLib.Models.CoreConfigs;
global using ServiceLib.Models.Configs;
global using ServiceLib.Models.Dto;
global using ServiceLib.Models.Entities;
global using ServiceLib.Resx;
global using ServiceLib.Services;
global using ServiceLib.Services.CoreConfig;
global using ServiceLib.Services.Statistics;
global using SQLite;

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.TryGetValue(countryCode, out var emoji) ? emoji : null;
}
}

View File

@@ -17,6 +17,13 @@ public class JsonUtils
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonSerializerOptions _defaultSerializeNoIndentedOptions = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonSerializerOptions _nullValueSerializeOptions = new()
{
WriteIndented = true,
@@ -24,6 +31,13 @@ public class JsonUtils
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonSerializerOptions _nullValueSerializeNoIndentedOptions = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonDocumentOptions _defaultDocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip
@@ -104,7 +118,13 @@ public class JsonUtils
{
return result;
}
var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions;
var options = (nullValue, indented) switch
{
(true, true) => _nullValueSerializeOptions,
(true, false) => _nullValueSerializeNoIndentedOptions,
(false, true) => _defaultSerializeOptions,
_ => _defaultSerializeNoIndentedOptions
};
result = JsonSerializer.Serialize(obj, options);
}
catch (Exception ex)

View File

@@ -864,15 +864,25 @@ public class Utils
public static Dictionary<string, string> GetSystemHosts()
{
var hosts = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts");
var hostsIcs = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts.ics");
foreach (var (key, value) in hostsIcs)
if (IsWindows())
{
hosts[key] = value;
var hosts = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts");
var hostsIcs = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts.ics");
foreach (var (key, value) in hostsIcs)
{
hosts[key] = value;
}
return hosts;
}
return hosts;
if (IsLinux() || IsMacOS())
{
return GetSystemHosts("/etc/hosts");
}
return new Dictionary<string, string>();
}
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg)
@@ -1114,12 +1124,16 @@ public class Utils
#region Platform
[SupportedOSPlatformGuard("windows")]
public static bool IsWindows() => OperatingSystem.IsWindows();
[SupportedOSPlatformGuard("linux")]
public static bool IsLinux() => OperatingSystem.IsLinux();
[SupportedOSPlatformGuard("macos")]
public static bool IsMacOS() => OperatingSystem.IsMacOS();
[UnsupportedOSPlatformGuard("windows")]
public static bool IsNonWindows() => !OperatingSystem.IsWindows();
public static string GetExeName(string name)
@@ -1214,6 +1228,16 @@ public class Utils
}
public static bool SetUnixFileMode(string? fileName)
{
if (IsWindows())
{
return false;
}
return SetUnixFileModeInternal(fileName);
}
[UnsupportedOSPlatform("windows")]
private static bool SetUnixFileModeInternal(string? fileName)
{
try
{

View File

@@ -2,6 +2,7 @@ using Microsoft.Win32;
namespace ServiceLib.Common;
[SupportedOSPlatform("windows")]
internal static class WindowsUtils
{
private static readonly string _tag = "WindowsUtils";

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";
@@ -64,9 +64,12 @@ public class Global
public const string HttpsProtocol = "https://";
public const string SocksProtocol = "socks://";
public const string Socks5Protocol = "socks5://";
public const string InnerUriProtocol = "v2rayn://";
public const string AsIs = "AsIs";
public const string IPIfNonMatch = "IPIfNonMatch";
public const string IPOnDemand = "IPOnDemand";
public const string GeoSitePrefix = "geosite:";
public const string GeoIPPrefix = "geoip:";
public const string UserEMail = "t@t.tt";
public const string AutoRunRegPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
@@ -146,6 +149,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",
@@ -154,6 +160,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"
@@ -204,6 +212,10 @@ public class Global
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://" },
@@ -502,6 +514,7 @@ public class Global
public static readonly List<string> InboundTags =
[
"tun",
"socks",
"socks2",
"socks3"
@@ -672,15 +685,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

@@ -8,6 +8,7 @@ global using System.Reactive.Disposables;
global using System.Reactive.Linq;
global using System.Reflection;
global using System.Runtime.InteropServices;
global using System.Runtime.Versioning;
global using System.Security.Cryptography;
global using System.Text;
global using System.Text.Encodings.Web;
@@ -29,7 +30,10 @@ global using ServiceLib.Handler.Fmt;
global using ServiceLib.Handler.SysProxy;
global using ServiceLib.Helper;
global using ServiceLib.Manager;
global using ServiceLib.Models;
global using ServiceLib.Models.CoreConfigs;
global using ServiceLib.Models.Configs;
global using ServiceLib.Models.Dto;
global using ServiceLib.Models.Entities;
global using ServiceLib.Resx;
global using ServiceLib.Services;
global using ServiceLib.Services.CoreConfig;

View File

@@ -41,6 +41,7 @@ public static class AutoStartupHandler
#region Windows
[SupportedOSPlatform("windows")]
private static async Task ClearTaskWindows()
{
var autoRunName = GetAutoRunNameWindows();
@@ -53,6 +54,7 @@ public static class AutoStartupHandler
await Task.CompletedTask;
}
[SupportedOSPlatform("windows")]
private static async Task SetTaskWindows()
{
try
@@ -82,6 +84,7 @@ public static class AutoStartupHandler
/// <param name="fileName"></param>
/// <param name="description"></param>
/// <exception cref="ArgumentNullException"></exception>
[SupportedOSPlatform("windows")]
public static void AutoStartTaskService(string taskName, string fileName, string description)
{
if (taskName.IsNullOrEmpty())
@@ -108,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)));
@@ -124,6 +128,7 @@ public static class AutoStartupHandler
#region Linux
[SupportedOSPlatform("linux")]
private static async Task ClearTaskLinux()
{
try
@@ -137,6 +142,7 @@ public static class AutoStartupHandler
await Task.CompletedTask;
}
[SupportedOSPlatform("linux")]
private static async Task SetTaskLinux()
{
try
@@ -157,6 +163,7 @@ public static class AutoStartupHandler
}
}
[SupportedOSPlatform("linux")]
private static string GetHomePathLinux()
{
var homePath = Path.Combine(Utils.GetHomePath(), ".config", "autostart", $"{Global.AppName}.desktop");
@@ -168,6 +175,7 @@ public static class AutoStartupHandler
#region macOS
[SupportedOSPlatform("macos")]
private static async Task ClearTaskOSX()
{
try
@@ -187,6 +195,7 @@ public static class AutoStartupHandler
}
}
[SupportedOSPlatform("macos")]
private static async Task SetTaskOSX()
{
try
@@ -204,6 +213,7 @@ public static class AutoStartupHandler
}
}
[SupportedOSPlatform("macos")]
private static string GetLaunchAgentPathMacOS()
{
var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
@@ -212,6 +222,7 @@ public static class AutoStartupHandler
return launchAgentPath;
}
[SupportedOSPlatform("macos")]
private static string GenerateLaunchAgentPlist()
{
var exePath = Utils.GetExePath();

View File

@@ -47,6 +47,8 @@ public class CoreConfigContextBuilder
ProtectDomainList = [],
RawDnsItem = await AppManager.Instance.GetDNSItem(coreType),
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
IsWindows = Utils.IsWindows(),
IsMacOS = Utils.IsMacOS(),
};
var validatorResult = NodeValidatorResult.Empty();
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);

View File

@@ -134,6 +134,12 @@ public class NodeValidator
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate"));
}
// Check for deprecated allowInsecure property when TLS is enabled
if (item.AllowInsecure == "true")
{
v.Warning(ResUI.MsgAllowInsecureDeprecated);
}
}
if (item.StreamSecurity == Global.StreamSecurityReality)

View File

@@ -41,7 +41,6 @@ public static class ConfigHandler
Loglevel = "warning",
MuxEnabled = false,
};
config.CoreBasicItem.SendThrough = config.CoreBasicItem.SendThrough?.TrimEx();
if (config.Inbound == null)
{
@@ -705,10 +704,12 @@ public static class ConfigHandler
public static async Task<int> AddHysteria2Server(Config config, ProfileItem profileItem, bool toFile = true)
{
profileItem.ConfigType = EConfigType.Hysteria2;
//profileItem.CoreType = ECoreType.sing_box;
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
//profileItem.Alpn = "h3";
profileItem.Network = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty())
@@ -748,6 +749,7 @@ public static class ConfigHandler
profileItem.Username = profileItem.Username.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
profileItem.Network = string.Empty;
profileItem.Fingerprint = string.Empty;
var congestionControl = profileItem.GetProtocolExtra().CongestionControl;
if (!Global.TuicCongestionControls.Contains(congestionControl))
@@ -788,12 +790,30 @@ public static class ConfigHandler
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
var wgReserved = profileItem.GetProtocolExtra().WgReserved?.TrimEx();
if (!wgReserved.IsNullOrEmpty()
&& !wgReserved.Contains(','))
{
// Base64 format, convert to standard format
try
{
var bytes = Convert.FromBase64String(wgReserved);
var reserved = new byte[3];
Array.Copy(bytes, reserved, Math.Min(bytes.Length, 3));
wgReserved = string.Join(", ", reserved);
}
catch
{
// If conversion fails, keep the original value
}
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
WgPublicKey = profileItem.GetProtocolExtra().WgPublicKey?.TrimEx(),
WgPresharedKey = profileItem.GetProtocolExtra().WgPresharedKey?.TrimEx(),
WgInterfaceAddress = profileItem.GetProtocolExtra().WgInterfaceAddress?.TrimEx(),
WgReserved = profileItem.GetProtocolExtra().WgReserved?.TrimEx(),
WgReserved = wgReserved,
WgMtu = profileItem.GetProtocolExtra().WgMtu is null or <= 0 ? Global.TunMtus.First() : profileItem.GetProtocolExtra().WgMtu,
});
@@ -851,8 +871,10 @@ public static class ConfigHandler
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Username = profileItem.Username.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty;
profileItem.AllowInsecure = "false";
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
@@ -901,6 +923,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"),
@@ -921,6 +944,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(),
@@ -941,6 +965,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(),
@@ -1506,10 +1531,8 @@ public static class ConfigHandler
}
var subFilter = string.Empty;
//remove sub items
if (isSub && subid.IsNotEmpty())
{
await RemoveServersViaSubid(config, subid, isSub);
subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? "";
}
@@ -1612,10 +1635,6 @@ public static class ConfigHandler
}
if (lstProfiles != null && lstProfiles.Count > 0)
{
if (isSub && subid.IsNotEmpty())
{
await RemoveServersViaSubid(config, subid, isSub);
}
var count = 0;
foreach (var it in lstProfiles)
{
@@ -1635,40 +1654,23 @@ public static class ConfigHandler
ProfileItem? profileItem = null;
//Is sing-box configuration
if (profileItem is null)
{
profileItem = SingboxFmt.ResolveFull(strData, subRemarks);
}
profileItem ??= SingboxFmt.ResolveFull(strData, subRemarks);
//Is v2ray configuration
if (profileItem is null)
{
profileItem = V2rayFmt.ResolveFull(strData, subRemarks);
}
profileItem ??= V2rayFmt.ResolveFull(strData, subRemarks);
//Is Html Page
if (profileItem is null && HtmlPageFmt.IsHtmlPage(strData))
{
return -1;
}
//Is Clash configuration
if (profileItem is null)
{
profileItem = ClashFmt.ResolveFull(strData, subRemarks);
}
profileItem ??= ClashFmt.ResolveFull(strData, subRemarks);
//Is hysteria configuration
if (profileItem is null)
{
profileItem = Hysteria2Fmt.ResolveFull2(strData, subRemarks);
}
profileItem ??= Hysteria2Fmt.ResolveFull2(strData, subRemarks);
if (profileItem is null || profileItem.Address.IsNullOrEmpty())
{
return -1;
}
if (isSub && subid.IsNotEmpty())
{
await RemoveServersViaSubid(config, subid, isSub);
}
profileItem.Subid = subid;
profileItem.IsSub = isSub;
profileItem.PreSocksPort = preSocksPort;
@@ -1698,11 +1700,6 @@ public static class ConfigHandler
return -1;
}
if (isSub && subid.IsNotEmpty())
{
await RemoveServersViaSubid(config, subid, isSub);
}
var lstSsServer = ShadowsocksFmt.ResolveSip008(strData);
if (lstSsServer?.Count > 0)
{
@@ -1723,6 +1720,86 @@ public static class ConfigHandler
return -1;
}
private static async Task<int> AddBatchServers4Wireguard(Config config, string strData, string subid, bool isSub)
{
if (strData.IsNullOrEmpty())
{
return -1;
}
if (!(strData.Contains("[Interface]", StringComparison.OrdinalIgnoreCase)
&& strData.Contains("[Peer]", StringComparison.OrdinalIgnoreCase)))
{
return -1;
}
var lstServer = WireguardFmt.ResolveConfig(strData);
if (lstServer?.Count > 0)
{
var counter = 0;
foreach (var item in lstServer)
{
item.Subid = subid;
item.IsSub = isSub;
if (await AddWireguardServer(config, item) == 0)
{
counter++;
}
}
await SaveConfig(config);
return counter;
}
return -1;
}
private static async Task<int> AddBatchServers4InnerUri(Config config, string strData, string subid, bool isSub)
{
if (strData.IsNullOrEmpty())
{
return -1;
}
var lstServer = InnerFmt.Resolve(strData, subid);
if (lstServer?.Count > 0)
{
var counter = 0;
List<ProfileItem> lstAdd = [];
foreach (var profileItem in lstServer)
{
profileItem.Subid = subid;
profileItem.IsSub = isSub;
var addStatus = profileItem.ConfigType switch
{
EConfigType.VMess => await AddVMessServer(config, profileItem, false),
EConfigType.Shadowsocks => await AddShadowsocksServer(config, profileItem, false),
EConfigType.HTTP => await AddHttpServer(config, profileItem, false),
EConfigType.SOCKS => await AddSocksServer(config, profileItem, false),
EConfigType.Trojan => await AddTrojanServer(config, profileItem, false),
EConfigType.VLESS => await AddVlessServer(config, profileItem, false),
EConfigType.Hysteria2 => await AddHysteria2Server(config, profileItem, false),
EConfigType.TUIC => await AddTuicServer(config, profileItem, false),
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
_ => -1,
};
if (addStatus == 0)
{
counter++;
lstAdd.Add(profileItem);
}
}
if (lstAdd.Count > 0)
{
await SQLiteHelper.Instance.InsertAllAsync(lstAdd);
}
await SaveConfig(config);
return counter;
}
return -1;
}
/// <summary>
/// Main entry point for adding batch servers from various formats
/// Tries different parsing methods to import as many servers as possible
@@ -1744,6 +1821,7 @@ public static class ConfigHandler
{
lstOriSub = await AppManager.Instance.ProfileItems(subid);
activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId);
await RemoveServersViaSubid(config, subid, true);
}
var counter = 0;
@@ -1765,6 +1843,38 @@ public static class ConfigHandler
counter = await AddBatchServers4SsSIP008(config, strData, subid, isSub);
}
//maybe wireguard config
if (counter < 1)
{
counter = await AddBatchServers4Wireguard(config, strData, subid, isSub);
}
//May be standard uri mixed with internal uri
var innerUriCount = 0;
if (Utils.IsBase64String(strData))
{
innerUriCount = await AddBatchServers4InnerUri(config, Utils.Base64Decode(strData), subid, isSub);
}
if (innerUriCount < 1)
{
innerUriCount = await AddBatchServers4InnerUri(config, strData, subid, isSub);
}
if (innerUriCount < 1)
{
innerUriCount = await AddBatchServers4InnerUri(config, Utils.Base64Decode(strData), subid, isSub);
}
if (innerUriCount > 0)
{
if (counter > 0)
{
counter += innerUriCount;
}
else
{
counter = innerUriCount;
}
}
//maybe other sub
if (counter < 1)
{

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

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

View File

@@ -0,0 +1,302 @@
namespace ServiceLib.Handler.Fmt;
public class InnerFmt
{
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
public static List<ProfileItem>? Resolve(string strData, string subid)
{
var list = new List<ProfileItem>();
// Overwrite externally imported indexIds to avoid possible sources of attacks
var indexIdMap = new Dictionary<string, string>();
using (var reader = new StringReader(strData))
{
while (reader.ReadLine() is { } line)
{
if (line.IsNullOrEmpty())
{
continue;
}
var trimmedLine = line.Trim();
if (!trimmedLine.StartsWith(Global.InnerUriProtocol, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var profileItem = ResolveSingle(trimmedLine);
if (profileItem is null)
{
continue;
}
if (profileItem.ConfigType == EConfigType.Custom)
{
// Unsupported, also to avoid possible sources of attacks, skip it
continue;
}
// overwrite indexId
var newIndexId = Utils.GetGuid(false);
if (!profileItem.IndexId.IsNullOrEmpty())
{
// Ignore duplicated indexId
indexIdMap[profileItem.IndexId] = newIndexId;
}
profileItem.IndexId = newIndexId;
list.Add(profileItem);
}
}
// For group-type profile items, also overwrite the ChildItems and ChildSubId
var emptyGroupProfileList = new List<ProfileItem>();
foreach (var item in list.Where(i => i.ConfigType.IsGroupType()))
{
var protocolExtra = item.GetProtocolExtra();
// Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks,
// which means it will be replaced with the subid, otherwise set it to null
//if (!protocolExtra.SubChildItems.IsNullOrEmpty())
if (protocolExtra.SubChildItems == "self")
{
protocolExtra = protocolExtra with
{
SubChildItems = subid
};
}
else
{
protocolExtra = protocolExtra with
{
SubChildItems = null
};
}
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
{
var newChildIndexIds = childIndexIds
.Select(id => indexIdMap.GetValueOrDefault(id, null))
.Where(id => !id.IsNullOrEmpty())
.ToList();
protocolExtra = protocolExtra with
{
ChildItems = Utils.List2String(newChildIndexIds)
};
}
else
{
protocolExtra = protocolExtra with
{
ChildItems = null
};
}
item.SetProtocolExtra(protocolExtra);
if (protocolExtra.SubChildItems.IsNullOrEmpty()
&& protocolExtra.ChildItems.IsNullOrEmpty())
{
emptyGroupProfileList.Add(item);
}
}
// Remove empty group profile items
list.RemoveAll(emptyGroupProfileList.Contains);
return list;
}
public static string? ToUri(List<ProfileItem> items)
{
var sb = new StringBuilder();
foreach (var item in items)
{
if (item.ConfigType == EConfigType.Custom)
{
continue;
}
var itemClone = JsonUtils.DeepCopy(item);
if (itemClone is null)
{
continue;
}
// overwrite indexId
var originalIndexId = itemClone.IndexId;
var newIndexId = GetReproducibleExportId(originalIndexId);
itemClone.IndexId = newIndexId;
if (itemClone.ConfigType.IsGroupType())
{
var protocolExtra = itemClone.GetProtocolExtra();
if (!protocolExtra.SubChildItems.IsNullOrEmpty())
{
protocolExtra = protocolExtra with
{
SubChildItems = "self"
};
}
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
{
var newChildIndexIds = childIndexIds
.Select(GetReproducibleExportId)
.Where(id => !id.IsNullOrEmpty())
.ToList();
protocolExtra = protocolExtra with
{
ChildItems = Utils.List2String(newChildIndexIds)
};
}
itemClone.SetProtocolExtra(protocolExtra);
}
var uri = ToUriSingle(itemClone);
if (!uri.IsNullOrEmpty())
{
sb.AppendLine(uri);
}
}
return sb.Length > 0 ? sb.ToString() : null;
}
private static ProfileItem? ResolveSingle(string str)
{
// format: v2rayn://vless/{url-safe base64 encoded_string}
var parsedUri = Utils.TryUri(str);
if (parsedUri is null)
{
return null;
}
var segment = parsedUri.AbsolutePath.TrimStart('/');
var decodedResult = Utils.Base64Decode(segment);
var jsonNode = JsonUtils.ParseJson(decodedResult);
if (jsonNode is not JsonObject jsonObj)
{
return null;
}
// flatten
// move jsonObj.ProtoExtraObj to jsonObj.ProtoExtra (string)
// move jsonObj.TransportExtraObj to jsonObj.TransportExtra (string)
if (jsonObj.TryGetPropertyValue("ProtoExtraObj", out var protoExtraNode)
&& protoExtraNode is JsonObject protoExtraObj)
{
jsonObj["ProtoExtra"] = JsonUtils.Serialize(protoExtraObj, false);
jsonObj.Remove("ProtoExtraObj");
}
if (jsonObj.TryGetPropertyValue("TransportExtraObj", out var transportExtraNode)
&& transportExtraNode is JsonObject transportExtraObj)
{
jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
jsonObj.Remove("TransportExtraObj");
}
var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false));
if (profileItem is null)
{
return null;
}
if (profileItem.ConfigVersion != 4)
{
return null;
}
// Check Enum.IsDefined
if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType))
{
return null;
}
if (profileItem.CoreType is not (null or ECoreType.Xray or ECoreType.sing_box))
{
return null;
}
var protocolExtra = profileItem.GetProtocolExtra();
var multipleLoad = protocolExtra.MultipleLoad;
if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad))
{
return null;
}
return profileItem;
}
private static string? ToUriSingle(ProfileItem item)
{
var jsonNode = JsonUtils.ParseJson(JsonUtils.Serialize(item, false));
if (jsonNode is not JsonObject jsonObj)
{
return null;
}
// unflatten
// move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
// move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj
if (jsonObj.TryGetPropertyValue("ProtoExtra", out var protoExtraNode)
&& protoExtraNode is JsonValue protoExtraValue
&& protoExtraValue.TryGetValue<string>(out var protoExtraStr)
&& !protoExtraStr.IsNullOrEmpty()
&& JsonUtils.ParseJson(protoExtraStr) is JsonObject protoExtraObj)
{
jsonObj["ProtoExtraObj"] = protoExtraObj;
jsonObj.Remove("ProtoExtra");
}
if (jsonObj.TryGetPropertyValue("TransportExtra", out var transportExtraNode)
&& transportExtraNode is JsonValue transportExtraValue
&& transportExtraValue.TryGetValue<string>(out var transportExtraStr)
&& !transportExtraStr.IsNullOrEmpty()
&& JsonUtils.ParseJson(transportExtraStr) is JsonObject transportExtraObj)
{
jsonObj["TransportExtraObj"] = transportExtraObj;
jsonObj.Remove("TransportExtra");
}
// remove subid and isSub
jsonObj.Remove("Subid");
jsonObj.Remove("IsSub");
// Remove empty properties to reduce the length of the exported string
RemoveEmptyJson(jsonObj);
var jsonStr = JsonUtils.Serialize(jsonObj, false);
var encodedStr = Utils.Base64Encode(jsonStr).Replace('+', '-').Replace('/', '_').Replace("=", "");
return $"{Global.InnerUriProtocol}{item.ConfigType.ToString().ToLower()}/{encodedStr}";
}
private static string GetReproducibleExportId(string originalIndexId)
{
if (originalIndexId.IsNullOrEmpty())
{
return originalIndexId;
}
var hash = HashCode.Combine(SessionSalt.Value, originalIndexId) & 0x7FFFFFFF;
var bytes = BitConverter.GetBytes(hash);
return Convert.ToBase64String(bytes).Replace("=", "");
}
private static void RemoveEmptyJson(JsonNode? node)
{
// ReSharper disable once ConvertIfStatementToSwitchStatement
if (node is JsonObject jsonObject)
{
var propertiesToRemove = new List<string>();
foreach (var property in jsonObject)
{
RemoveEmptyJson(property.Value);
if (IsEmpty(property.Value))
{
propertiesToRemove.Add(property.Key);
}
}
foreach (var key in propertiesToRemove)
{
jsonObject.Remove(key);
}
}
else if (node is JsonArray jsonArray)
{
for (var i = jsonArray.Count - 1; i >= 0; i--)
{
RemoveEmptyJson(jsonArray[i]);
if (IsEmpty(jsonArray[i]))
{
jsonArray.RemoveAt(i);
}
}
}
}
private static bool IsEmpty(JsonNode? node)
{
return node switch
{
null => true,
JsonValue value when value.TryGetValue<string>(out var str) => string.IsNullOrEmpty(str),
JsonObject obj => obj.Count == 0,
JsonArray arr => arr.Count == 0,
_ => false
};
}
}

View File

@@ -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

@@ -27,9 +27,10 @@ public class WireguardFmt : BaseFmt
item.SetProtocolExtra(item.GetProtocolExtra() with
{
WgPublicKey = GetQueryDecoded(query, "publickey"),
WgPresharedKey = GetQueryDecoded(query, "presharedkey"),
WgReserved = GetQueryDecoded(query, "reserved"),
WgInterfaceAddress = GetQueryDecoded(query, "address"),
WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : 1280,
WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : null,
});
return item;
@@ -48,20 +49,183 @@ public class WireguardFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks);
}
var protoExtra = item.GetProtocolExtra();
var dicQuery = new Dictionary<string, string>();
if (!item.GetProtocolExtra().WgPublicKey.IsNullOrEmpty())
if (!protoExtra.WgPublicKey.IsNullOrEmpty())
{
dicQuery.Add("publickey", Utils.UrlEncode(item.GetProtocolExtra().WgPublicKey));
dicQuery.Add("publickey", Utils.UrlEncode(protoExtra.WgPublicKey));
}
if (!item.GetProtocolExtra().WgReserved.IsNullOrEmpty())
if (!protoExtra.WgPresharedKey.IsNullOrEmpty())
{
dicQuery.Add("reserved", Utils.UrlEncode(item.GetProtocolExtra().WgReserved));
dicQuery.Add("presharedkey", Utils.UrlEncode(protoExtra.WgPresharedKey));
}
if (!item.GetProtocolExtra().WgInterfaceAddress.IsNullOrEmpty())
if (!protoExtra.WgReserved.IsNullOrEmpty())
{
dicQuery.Add("address", Utils.UrlEncode(item.GetProtocolExtra().WgInterfaceAddress));
dicQuery.Add("reserved", Utils.UrlEncode(protoExtra.WgReserved));
}
if (!protoExtra.WgInterfaceAddress.IsNullOrEmpty())
{
dicQuery.Add("address", Utils.UrlEncode(protoExtra.WgInterfaceAddress));
}
if (protoExtra.WgMtu > 0)
{
dicQuery.Add("mtu", protoExtra.WgMtu.ToString());
}
dicQuery.Add("mtu", Utils.UrlEncode(item.GetProtocolExtra().WgMtu > 0 ? item.GetProtocolExtra().WgMtu.ToString() : "1280"));
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
}
public static List<ProfileItem>? ResolveConfig(string strData)
{
var interfaceDic = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var peerDicList = new List<Dictionary<string, string>>();
var currentDicRef = interfaceDic;
using (var reader = new StringReader(strData))
{
while (reader.ReadLine() is { } line)
{
if (line.IsNullOrEmpty())
{
continue;
}
var trimmedLine = line.Trim();
if (trimmedLine.Equals("[Interface]", StringComparison.OrdinalIgnoreCase))
{
currentDicRef = interfaceDic;
continue;
}
if (trimmedLine.Equals("[Peer]", StringComparison.OrdinalIgnoreCase))
{
var peerDic = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
peerDicList.Add(peerDic);
currentDicRef = peerDic;
continue;
}
if (trimmedLine.StartsWith('[') || trimmedLine.StartsWith('#') || trimmedLine.StartsWith(';'))
{
continue;
}
var idx = line.IndexOf('=');
if (idx <= 0)
{
continue;
}
var key = line[..idx].Trim();
var value = line[(idx + 1)..].Trim();
var commentPos = value.IndexOfAny(['#', ';']);
if (commentPos >= 0)
{
value = value[..commentPos].TrimEnd();
}
currentDicRef[key] = value;
}
}
if (!interfaceDic.TryGetValue("PrivateKey", out var privateKey) || privateKey.IsNullOrEmpty())
{
return null;
}
var wgMtu = interfaceDic.TryGetValue("MTU", out var mtuStr) && int.TryParse(mtuStr, out var mtuVal) ? mtuVal : 0;
var wgInterfaceAddress = interfaceDic.TryGetValue("Address", out var interfaceAddress) ? interfaceAddress : string.Empty;
var index = 0;
var resultList = new List<ProfileItem>();
foreach (var peerDic in peerDicList)
{
if (!peerDic.TryGetValue("Endpoint", out var endpoint) || endpoint.IsNullOrEmpty())
{
continue;
}
if (!TryParseEndpoint(endpoint, out var peerAddress, out var peerPort))
{
continue;
}
var protoExtra = new ProtocolExtraItem
{
WgPublicKey = (peerDic.TryGetValue("PublicKey", out var publicKey) ? publicKey : string.Empty).NullIfEmpty(),
WgPresharedKey = (peerDic.TryGetValue("PresharedKey", out var presharedKey) ? presharedKey : string.Empty).NullIfEmpty(),
WgInterfaceAddress = wgInterfaceAddress,
WgReserved = (peerDic.TryGetValue("Reserved", out var reserved) ? reserved : string.Empty).NullIfEmpty(),
WgMtu = wgMtu > 0 ? wgMtu : null,
};
var item = new ProfileItem
{
Remarks = $"{nameof(EConfigType.WireGuard)} Peer {index + 1}",
ConfigType = EConfigType.WireGuard,
Address = peerAddress,
Port = peerPort,
Password = privateKey,
};
item.SetProtocolExtra(protoExtra);
resultList.Add(item);
index += 1;
}
return resultList;
}
private static bool TryParseEndpoint(string endpoint, out string address, out int port)
{
address = string.Empty;
port = 2408;
var trimmedEndpoint = endpoint.Trim();
if (trimmedEndpoint.IsNullOrEmpty())
{
return false;
}
if (trimmedEndpoint[0] == '[')
{
var closeIndex = trimmedEndpoint.IndexOf(']');
if (closeIndex <= 1)
{
return false;
}
address = trimmedEndpoint[1..closeIndex].Trim();
var portIndex = closeIndex + 1;
if (portIndex < trimmedEndpoint.Length && trimmedEndpoint[portIndex] == ':' &&
int.TryParse(trimmedEndpoint[(portIndex + 1)..].Trim(), out var bracketedPort) && bracketedPort is > 0 and <= 65535)
{
port = bracketedPort;
}
return address.IsNotEmpty();
}
var lastColonIndex = trimmedEndpoint.LastIndexOf(':');
if (lastColonIndex <= 0)
{
address = trimmedEndpoint;
return true;
}
address = trimmedEndpoint[..lastColonIndex].Trim();
var portText = trimmedEndpoint[(lastColonIndex + 1)..].Trim();
if (address.IsNullOrEmpty())
{
return false;
}
if (int.TryParse(portText, out var parsedPortValue) && parsedPortValue is > 0 and <= 65535)
{
port = parsedPortValue;
return true;
}
address = trimmedEndpoint;
return true;
}
}

View File

@@ -1,5 +1,6 @@
namespace ServiceLib.Handler.SysProxy;
[SupportedOSPlatform("linux")]
public static class ProxySettingLinux
{
private static readonly string _proxySetFileName = $"{Global.ProxySetLinuxShellFileName.Replace(Global.NamespaceSample, "")}.sh";

View File

@@ -1,5 +1,6 @@
namespace ServiceLib.Handler.SysProxy;
[SupportedOSPlatform("macos")]
public static class ProxySettingOSX
{
private static readonly string _proxySetFileName = $"{Global.ProxySetOSXShellFileName.Replace(Global.NamespaceSample, "")}.sh";

View File

@@ -2,6 +2,7 @@ using static ServiceLib.Handler.SysProxy.ProxySettingWindows.InternetConnectionO
namespace ServiceLib.Handler.SysProxy;
[SupportedOSPlatform("windows")]
public static class ProxySettingWindows
{
private const string _regPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";

View File

@@ -88,6 +88,7 @@ public static class SysProxyHandler
}
}
[SupportedOSPlatform("windows")]
private static async Task SetWindowsProxyPac(int port)
{
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);

View File

@@ -48,6 +48,13 @@ public sealed class AppManager
}
}
public Dictionary<ECoreType, string> LastCheckUpdateResults { get; set; } = new();
public void SetLastCheckUpdateResult(ECoreType coreType, string result)
{
LastCheckUpdateResults[coreType] = result;
}
#endregion Property
#region App

View File

@@ -1,4 +1,4 @@
using static ServiceLib.Models.ClashProxies;
using static ServiceLib.Models.Dto.ClashProxies;
namespace ServiceLib.Manager;

View File

@@ -50,6 +50,50 @@ public sealed class CoreInfoManager
return fileName;
}
public List<ECoreType> GetCheckUpdateCoreTypes()
{
var lst = new List<ECoreType>();
if (RuntimeInformation.ProcessArchitecture != Architecture.X86)
{
if (IsCheckUpdateSupported(ECoreType.v2rayN))
{
lst.Add(ECoreType.v2rayN);
}
if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10))
{
lst.Add(ECoreType.Xray);
lst.Add(ECoreType.mihomo);
lst.Add(ECoreType.sing_box);
}
}
return lst;
}
public bool IsCheckUpdateSupported(ECoreType type)
{
return type switch
{
ECoreType.v2rayN => !Utils.IsPackagedInstall(),
ECoreType.Xray => true,
ECoreType.mihomo => true,
ECoreType.sing_box => true,
_ => false,
};
}
public bool GetCheckPreRelease(ECoreType type, bool preRelease)
{
return type switch
{
ECoreType.v2rayN => preRelease,
ECoreType.Xray => preRelease,
_ => false,
};
}
private void InitCoreInfo()
{
var urlN = GetCoreUrl(ECoreType.v2rayN);
@@ -69,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",
},
@@ -113,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",
@@ -136,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",
@@ -179,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",
@@ -270,6 +318,7 @@ public sealed class CoreInfoManager
names.Add("mihomo-linux-amd64");
names.Add("mihomo-linux-arm64");
names.Add("mihomo-linux-riscv64");
names.Add("mihomo-linux-loong64-abi2");
}
else if (Utils.IsMacOS())
{

View File

@@ -8,6 +8,7 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value;
private Config _config;
[SupportedOSPlatform("windows")]
private WindowsJobService? _processJob;
private ProcessService? _processService;
private ProcessService? _processPreService;

View File

@@ -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);

View File

@@ -70,6 +70,18 @@ public class TaskManager
}
}
//Execute once 24 hour
if (numOfExecuted % 1440 == 1)
{
try
{
await UpdateTaskRunCheckUpdate();
}
catch (Exception ex)
{
Logging.SaveLog("ScheduledTasks - UpdateTaskRunCheckUpdate", ex);
}
}
numOfExecuted++;
}
}
@@ -117,4 +129,23 @@ public class TaskManager
}).UpdateGeoFileAll();
}
}
private async Task UpdateTaskRunCheckUpdate()
{
Logging.SaveLog("Execute check update");
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.CheckPreReleaseUpdate);
foreach (var msg in msgs)
{
await _updateFunc?.Invoke(false, msg);
}
NoticeManager.Instance.Enqueue(string.Join("\n", msgs));
if (msgs.Count > 0)
{
AppEvents.HasUpdateNotified.Publish(true);
}
}
}

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Configs;
[Serializable]
public class Config

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Configs;
[Serializable]
public class CoreBasicItem
@@ -17,6 +17,8 @@ public class CoreBasicItem
public string? SendThrough { get; set; }
public string? BindInterface { get; set; }
public bool EnableFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true;
@@ -157,6 +159,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]

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.CoreConfigs;
public record CoreConfigContext
{
@@ -17,4 +17,7 @@ public record CoreConfigContext
// TUN Compatibility
public bool IsTunEnabled { get; init; } = false;
public HashSet<string> ProtectDomainList { get; init; } = [];
public bool IsWindows { get; init; }
public bool IsMacOS { get; init; }
}

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.CoreConfigs;
[Serializable]
public class CoreInfo
@@ -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

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.CoreConfigs;
public class SingboxConfig
{
@@ -173,7 +173,7 @@ public class Peer4Sbox
public string? pre_shared_key { get; set; }
public List<string> allowed_ips { get; set; }
public int? persistent_keepalive_interval { get; set; }
public List<int> reserved { get; set; }
public List<int>? reserved { get; set; }
}
public class Tls4Sbox

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.CoreConfigs;
public class V2rayConfig
{
@@ -163,6 +163,7 @@ public class WireguardPeer4Ray
{
public string endpoint { get; set; }
public string publicKey { get; set; }
public string? preSharedKey { get; set; }
}
public class VnextItem4Ray
@@ -275,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

View File

@@ -1,6 +1,6 @@
using System.Collections;
namespace ServiceLib.Models;
namespace ServiceLib.Models.CoreConfigs;
internal class V2rayMetricsVars
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.CoreConfigs;
public class V2rayTcpRequest
{

View File

@@ -1,10 +1,12 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class CheckUpdateModel : ReactiveObject
{
public bool? IsSelected { get; set; }
public string? CoreType { get; set; }
public ECoreType? CoreType { get; set; }
[Reactive] public string? Remarks { get; set; }
public string? FileName { get; set; }
public bool? IsFinished { get; set; }
public bool IsGeoFile { get; set; }
public string CoreTypeForStorage => IsGeoFile ? "GeoFiles" : (CoreType?.ToString() ?? "");
}

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class ClashConnectionModel
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class ClashConnections
{

View File

@@ -1,6 +1,6 @@
using static ServiceLib.Models.ClashProxies;
using static ServiceLib.Models.Dto.ClashProxies;
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class ClashProviders
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class ClashProxies
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
[Serializable]
public class ClashProxyModel : ReactiveObject

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class CmdItem
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class ComboItem
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class GitHubReleaseAsset
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
internal class IPAPIInfo
{
@@ -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 = Country.CountryToEmoji();
return $"{emoji}({Country}) {Ip}";
}
}

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
[Serializable]
public class ProfileItemModel : ReactiveObject
@@ -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,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class RetResult
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
[Serializable]
public class RoutingItemModel : RoutingItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
[Serializable]
public class RoutingTemplate

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
[Serializable]
public class RulesItemModel : RulesItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class SemanticVersion
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
[Serializable]
public class ServerSpeedItem : ServerStatItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
[Serializable]
public class ServerTestItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
[Serializable]
public class SpeedTestResult
@@ -8,4 +8,6 @@ public class SpeedTestResult
public string? Delay { get; set; }
public string? Speed { get; set; }
public string? IpInfo { get; set; }
}

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class SsSIP008
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
public class UpdateResult
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Dto;
/// <summary>
/// https://github.com/2dust/v2rayN/wiki/

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Serializable]
public class DNSItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Serializable]
public class FullConfigTemplateItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Serializable]
public class ProfileExItem
@@ -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

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Obsolete("Use ProtocolExtraItem instead.")]
[Serializable]

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Serializable]
public class ProfileItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
public record ProtocolExtraItem
{

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Serializable]
public class RoutingItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Serializable]
public class RulesItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Serializable]
public class ServerStatItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
[Serializable]
public class SubItem

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Models;
namespace ServiceLib.Models.Entities;
public record TransportExtraItem
{

View File

@@ -564,6 +564,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 IP Info 的本地化字符串。
/// </summary>
public static string LvTestIpInfo {
get {
return ResourceManager.GetString("LvTestIpInfo", resourceCulture);
}
}
/// <summary>
/// 查找类似 Speed (MB/s) 的本地化字符串。
/// </summary>
@@ -870,6 +879,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Only Check 的本地化字符串。
/// </summary>
public static string menuCheckOnly {
get {
return ResourceManager.GetString("menuCheckOnly", resourceCulture);
}
}
/// <summary>
/// 查找类似 Check Update 的本地化字符串。
/// </summary>
@@ -1023,6 +1041,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Export v2rayN Internal Share Link to Clipboard 的本地化字符串。
/// </summary>
public static string menuExport2InnerUri {
get {
return ResourceManager.GetString("menuExport2InnerUri", resourceCulture);
}
}
/// <summary>
/// 查找类似 Export Share Link to Clipboard 的本地化字符串。
/// </summary>
@@ -1293,6 +1320,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 New Update 的本地化字符串。
/// </summary>
public static string menuNewUpdate {
get {
return ResourceManager.GetString("menuNewUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 Open the storage location 的本地化字符串。
/// </summary>
@@ -1878,6 +1914,24 @@ 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>
public static string MsgCheckUpdateHasNewVersion {
get {
return ResourceManager.GetString("MsgCheckUpdateHasNewVersion", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support network type &apos;{1}&apos; 的本地化字符串。
/// </summary>
@@ -2031,6 +2085,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Not Support 的本地化字符串。
/// </summary>
public static string MsgNotSupport {
get {
return ResourceManager.GetString("MsgNotSupport", resourceCulture);
}
}
/// <summary>
/// 查找类似 Not support protocol &apos;{0}&apos; 的本地化字符串。
/// </summary>
@@ -3222,6 +3285,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 MTU 的本地化字符串。
/// </summary>
public static string TbMtu {
get {
return ResourceManager.GetString("TbMtu", resourceCulture);
}
}
/// <summary>
/// 查找类似 Transport protocol(network) 的本地化字符串。
/// </summary>
@@ -3312,6 +3384,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 PreSharedKey 的本地化字符串。
/// </summary>
public static string TbPreSharedKey {
get {
return ResourceManager.GetString("TbPreSharedKey", resourceCulture);
}
}
/// <summary>
/// 查找类似 Socks port 的本地化字符串。
/// </summary>
@@ -3430,7 +3511,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Reserved (2,3,4) 的本地化字符串。
/// 查找类似 Reserved 的本地化字符串。
/// </summary>
public static string TbReserved {
get {
@@ -3457,7 +3538,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// </summary>
public static string TbRoutingInboundTagTips {
get {
@@ -3681,6 +3762,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Bind Interface 的本地化字符串。
/// </summary>
public static string TbSettingsBindInterface {
get {
return ResourceManager.GetString("TbSettingsBindInterface", resourceCulture);
}
}
/// <summary>
/// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode 的本地化字符串。
/// </summary>
public static string TbSettingsBindInterfaceTip {
get {
return ResourceManager.GetString("TbSettingsBindInterfaceTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Users in China region can ignore this item 的本地化字符串。
/// </summary>
@@ -3880,7 +3979,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Check for pre-release updates 的本地化字符串。
/// 查找类似 Check for pre-release 的本地化字符串。
/// </summary>
public static string TbSettingsEnableCheckPreReleaseUpdate {
get {
@@ -4347,15 +4446,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 MTU 的本地化字符串。
/// </summary>
public static string TbSettingsTunMtu {
get {
return ResourceManager.GetString("TbSettingsTunMtu", resourceCulture);
}
}
/// <summary>
/// 查找类似 Stack 的本地化字符串。
/// </summary>

View File

@@ -1062,7 +1062,7 @@
<data name="TbSettingsTunStack" xml:space="preserve">
<value>پشته شبکه</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<data name="TbMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
@@ -1075,7 +1075,7 @@
<value>کلید خصوصی</value>
</data>
<data name="TbReserved" xml:space="preserve">
<value>Reserved (2,3,4)</value>
<value>Reserved</value>
</data>
<data name="TbLocalAddress" xml:space="preserve">
<value>آدرس (IPv4, IPv6)</value>
@@ -1689,9 +1689,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="TbCamouflageDomain" xml:space="preserve">
<value>Camouflage domain</value>
</data>
@@ -1713,4 +1710,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>UDP Test Url</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Local outbound address (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value>
</data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value>
</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>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
</data>
<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

@@ -1059,7 +1059,7 @@
<data name="TbSettingsTunStack" xml:space="preserve">
<value>Pile de protocoles</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<data name="TbMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
@@ -1072,7 +1072,7 @@
<value>PrivateKey</value>
</data>
<data name="TbReserved" xml:space="preserve">
<value>Reserved (2,3,4)</value>
<value>Reserved</value>
</data>
<data name="TbLocalAddress" xml:space="preserve">
<value>Address (IPv4,IPv6)</value>
@@ -1311,15 +1311,6 @@
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>Le mot de passe sera vérifié en ligne de commande. En cas déchec ou de dysfonctionnement, redémarrez lapplication. Il nest pas stocké et doit être saisi à chaque redémarrage.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Adresse sortante 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="TransportHeaderType5" xml:space="preserve">
<value>Mode XHTTP</value>
</data>
@@ -1684,7 +1675,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>
@@ -1708,6 +1699,48 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<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>
<value>Pour obtenir des certificats auto-signés uniquement. Risque MITM.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<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>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<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>Tester la latence UDP des profils</value>
</data>
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>URL de test UDP</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value>
</data>
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Exporter le lien interne v2rayN vers le presse-papiers</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} a une nouvelle version disponible: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Uniquement vérifier</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<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

@@ -1062,7 +1062,7 @@
<data name="TbSettingsTunStack" xml:space="preserve">
<value>Hálózati verem</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<data name="TbMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
@@ -1075,7 +1075,7 @@
<value>Privát kulcs</value>
</data>
<data name="TbReserved" xml:space="preserve">
<value>Fenntartott (2,3,4)</value>
<value>Fenntartott</value>
</data>
<data name="TbLocalAddress" xml:space="preserve">
<value>Cím (IPv4, IPv6)</value>
@@ -1689,9 +1689,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="TbCamouflageDomain" xml:space="preserve">
<value>Álcázási tartomány</value>
</data>
@@ -1713,4 +1710,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>UDP Test Url</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Local outbound address (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value>
</data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value>
</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>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
</data>
<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

@@ -691,7 +691,7 @@
<value>Automatically adjust column width after subscription update</value>
</data>
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>Check for pre-release updates</value>
<value>Check for pre-release</value>
</data>
<data name="TbSettingsException" xml:space="preserve">
<value>Exception</value>
@@ -1062,7 +1062,7 @@
<data name="TbSettingsTunStack" xml:space="preserve">
<value>Stack</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<data name="TbMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
@@ -1075,7 +1075,7 @@
<value>Private Key</value>
</data>
<data name="TbReserved" xml:space="preserve">
<value>Reserved (2,3,4)</value>
<value>Reserved</value>
</data>
<data name="TbLocalAddress" xml:space="preserve">
<value>Address (IPv4, IPv6)</value>
@@ -1314,15 +1314,6 @@
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Local outbound address (SendThrough)</value>
</data>
<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="TransportHeaderType5" xml:space="preserve">
<value>xhttp mode</value>
</data>
@@ -1345,7 +1336,7 @@
<value>Enable second mixed port</value>
</data>
<data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks: local port, socks2: second local port, socks3: LAN port</value>
<value>tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>Theme</value>
@@ -1719,4 +1710,43 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>UDP Test Url</value>
</data>
</root>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Local outbound address (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value>
</data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value>
</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>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
</data>
<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>
</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>
@@ -1062,7 +1062,7 @@
<data name="TbSettingsTunStack" xml:space="preserve">
<value>Сетевой стек</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<data name="TbMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
@@ -1075,7 +1075,7 @@
<value>Приватный ключ</value>
</data>
<data name="TbReserved" xml:space="preserve">
<value>Зарезервировано (2, 3, 4)</value>
<value>Зарезервировано</value>
</data>
<data name="TbLocalAddress" xml:space="preserve">
<value>Адрес (IPv4, IPv6)</value>
@@ -1177,7 +1177,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 +1210,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 +1300,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 +1318,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 +1336,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 +1357,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 +1369,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 +1384,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 +1489,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 +1534,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 +1547,7 @@
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Привязанный сертификат (заполните любое из полей)
При указании сертификат будет привязан, а «Разрешить небезопасные» отключится.
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
</data>
@@ -1689,9 +1689,6 @@
<data name="TbLegacyProtect" xml:space="preserve">
<value>Устаревшая защита TUN (Legacy Protect)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
</data>
<data name="TbCamouflageDomain" xml:space="preserve">
<value>Камуфляжный домен</value>
</data>
@@ -1713,10 +1710,40 @@
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>URL для UDP-теста</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Локальный исходящий адрес (SendThrough)</value>
</data>
</root>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve">
<value>Привязать интерфейс</value>
</data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>Общий ключ (PSK)</value>
</data>
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>Доступна новая версия {0}: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Проверить</value>
</data>
<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>
</root>

View File

@@ -691,7 +691,7 @@
<value>自动调整配置列宽在更新订阅后</value>
</data>
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>检查 Pre-Release 更新 (请谨慎启用)</value>
<value>检查 Pre-Release</value>
</data>
<data name="TbSettingsException" xml:space="preserve">
<value>例外</value>
@@ -1059,7 +1059,7 @@
<data name="TbSettingsTunStack" xml:space="preserve">
<value>协议栈</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<data name="TbMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
@@ -1072,7 +1072,7 @@
<value>PrivateKey</value>
</data>
<data name="TbReserved" xml:space="preserve">
<value>Reserved (2,3,4)</value>
<value>Reserved</value>
</data>
<data name="TbLocalAddress" xml:space="preserve">
<value>Address (IPv4,IPv6)</value>
@@ -1311,15 +1311,6 @@
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>本地出站地址 (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>用于多网口环境,请填写本机 IPv4 地址</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>请填写正确的 SendThrough IPv4 地址。</value>
</data>
<data name="TransportHeaderType5" xml:space="preserve">
<value>XHTTP 模式</value>
</data>
@@ -1342,7 +1333,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>
@@ -1716,4 +1707,43 @@
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>UDP 测试地址</value>
</data>
</root>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>本地出站地址 (SendThrough)</value>
</data>
<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>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>用于多网口环境,填写要绑定的网口名称,仅生效于 Windows 系统或 TUN 模式</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value>
</data>
<data name="menuExport2InnerUri" xml:space="preserve">
<value>导出 v2rayN 内部分享链接至剪贴板 (多选)</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} 有新版本可用:{1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>仅检查</value>
</data>
<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>
</root>

View File

@@ -691,7 +691,7 @@
<value>在更新訂閱後自動調整列寬</value>
</data>
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>檢查 Pre-Release 更新 (請謹慎啟用)</value>
<value>檢查 Pre-Release</value>
</data>
<data name="TbSettingsException" xml:space="preserve">
<value>例外</value>
@@ -1059,7 +1059,7 @@
<data name="TbSettingsTunStack" xml:space="preserve">
<value>協定堆疊</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<data name="TbMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
@@ -1072,7 +1072,7 @@
<value>PrivateKey</value>
</data>
<data name="TbReserved" xml:space="preserve">
<value>Reserved (2,3,4)</value>
<value>Reserved</value>
</data>
<data name="TbLocalAddress" xml:space="preserve">
<value>Address (Ipv4,Ipv6)</value>
@@ -1684,10 +1684,7 @@
<value>ICMP 路由策略</value>
</data>
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
<value>舊版 TUN 保護</value>
</data>
<data name="TbCamouflageDomain" xml:space="preserve">
<value>偽裝域名</value>
@@ -1705,9 +1702,48 @@
<value>僅用於抓取自簽證書,存在中間人風險。</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value>
<value>測試 UDP 延遲(多選)</value>
</data>
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>UDP Test Url</value>
<value>UDP 測試網址</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>本機出站位址 (SendThrough)</value>
</data>
<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>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>適用於多網路介面環境請填寫要綁定的介面名稱Windows 系統有效,其他系統僅在 TUN 模式下生效</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value>
</data>
<data name="menuExport2InnerUri" xml:space="preserve">
<value>匯出 v2rayN 內部分享連結至剪貼簿(多選)</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} 有新版本可用:{1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>僅檢查</value>
</data>
<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>
</root>

View File

@@ -1,6 +1,6 @@
{
"type": "tun",
"tag": "tun-in",
"tag": "tun",
"interface_name": "singbox_tun",
"address": [
"172.18.0.1/30",

View File

@@ -10,7 +10,8 @@
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="ReactiveUI.Fody" />
<PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="sqlite-net-e" />
<PackageReference Include="Repobot.SQLite.Unofficial" />
<PackageReference Include="NLog" />
<PackageReference Include="WebDav.Client" />
<PackageReference Include="YamlDotNet" />

View File

@@ -57,6 +57,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ConvertGeo2Ruleset();
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
@@ -170,6 +171,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
_coreConfig.route.rules.Add(rule);
}
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig);
@@ -229,6 +231,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
});
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");

View File

@@ -59,22 +59,38 @@ public partial class CoreConfigSingboxService
return JsonUtils.Serialize(fullConfigTemplateNode);
}
private void ApplyOutboundSendThrough()
private void ApplyOutboundBindInterface()
{
var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (bindInterface.IsNullOrEmpty())
{
return;
}
if (!(context.IsTunEnabled || context.IsWindows))
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? [])
{
outbound.inet4_bind_address = ShouldApplySendThrough(outbound, sendThrough) ? sendThrough : null;
outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
}
}
private static bool ShouldApplySendThrough(Outbound4Sbox outbound, string? sendThrough)
private void ApplyOutboundSendThrough()
{
var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
if (sendThrough.IsNullOrEmpty())
{
return false;
return;
}
foreach (var outbound in _coreConfig.outbounds ?? [])
{
outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;
}
}
private static bool ShouldBindNet(Outbound4Sbox outbound)
{
if (outbound.type is "direct" or "block" or "dns" or "selector" or "urltest")
{
return false;

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