Compare commits

..

47 Commits

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

* Update build-linux.yml

* Update build.yml

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

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

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

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

* Update logo image in README.md

* Update README.md

* Update README.md

* Update README.md

---------

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

* Core config

* Fix

* Fix

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

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

* feat: add customizable packet fragmentation for Xray-core

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

* fix: address 2dust review feedback

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

* Adjust ui

---------

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

* Update build-all.yml

* Update build-linux.yml

* Update build-osx.yml

* Update build-windows-desktop.yml

* Update build-windows-x86.yml

* Update build-windows.yml

* Update package-zip.yml

* Update build-linux.yml

* Update build-osx.yml

* Update build-windows-x86.yml

* Update package-zip.yml

* Update upload-sign.yml

* Update pub-key.yml

* Update README.md

---------

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

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 17:31:22 +08:00
2dust ff456a9389 Add DNS via NetBridge 2026-06-19 17:13:51 +08:00
2dust 0d225cd29d Create NetBridge 2026-06-19 11:48:27 +08:00
2dust 67426cff12 Add NetBridge process hijacking feature for windows (#9588)
https://github.com/2dust/NetBridge
https://github.com/InterceptSuite/ProxyBridge
2026-06-19 11:21:15 +08:00
JieXu a8c27027b4 bump dependencies && Purge SDKs before build (#9576)
* Update package-rhel-loong.sh

* Update package-debian-loong.sh

* Update build.yml

* Update build-windows-x86.yml

* Enhance SDK purge step with echo statements

Added echo statements to display SDKs before and after purging.

* Update build-windows-x86.yml

* Update build.yml

* Update build-windows-x86.yml

* Update build.yml

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

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

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

* Translate menuRouting to Perutean in Indonesian locale

---------

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

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

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

* Create package-rhel-loong.sh

* Update package-debian-riscv.sh

* Update package-rhel-riscv.sh

* Update package-rhel-riscv.sh

* Update package-rhel.sh

* Update build-linux.yml

* Update build-linux.yml

* Create build-windows-x86.yml

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

* Update MainWindow.axaml.cs

* Update MacAppUtils.cs

* Update App.axaml.cs

* Add regions for MacOS activation and app events

---------

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

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

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

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

* Update WindowBase.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-07 14:59:50 +08:00
2dust 5fed566a32 Add ReplaceLineBreaks extension and Fix bug
https://github.com/2dust/v2rayN/issues/9497
2026-06-07 14:49:39 +08:00
92 changed files with 5387 additions and 836 deletions
+2 -2
View File
@@ -18,8 +18,8 @@ body:
- type: input - type: input
id: "os-version" id: "os-version"
attributes: attributes:
label: "操作系统和版本" label: "操作系统 / v2rayN / Core 版本"
description: "操作系统和版本" description: "操作系统 / v2rayN / Core 版本"
validations: validations:
required: true required: true
+23
View File
@@ -9,8 +9,17 @@ on:
permissions: permissions:
actions: write actions: write
contents: write
jobs: jobs:
public-key:
if: inputs.release_tag != ''
uses: ./.github/workflows/pub-key.yml
with:
release_tag: ${{ inputs.release_tag }}
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
update: update:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -70,3 +79,17 @@ jobs:
\"release_tag\": \"${{ inputs.release_tag }}\" \"release_tag\": \"${{ inputs.release_tag }}\"
} }
}" }"
- name: Trigger build windows x86
if: inputs.release_tag != ''
run: |
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-windows-x86.yml/dispatches \
-d "{
\"ref\": \"master\",
\"inputs\": {
\"release_tag\": \"${{ inputs.release_tag }}\"
}
}"
+289 -246
View File
@@ -12,6 +12,7 @@ on:
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
build: build:
@@ -26,17 +27,14 @@ jobs:
with: with:
target: linux target: linux
release_tag: ${{ inputs.release_tag }} release_tag: ${{ inputs.release_tag }}
secrets: inherit
deb: deb:
name: build and release deb x64 & arm64 name: build and release deb x64 & arm64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || runs-on: ubuntu-26.04
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
container: debian:13 container: debian:13
env: env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps: steps:
- name: Prepare tools (Debian) - name: Prepare tools (Debian)
shell: bash shell: bash
@@ -44,124 +42,73 @@ jobs:
set -euo pipefail set -euo pipefail
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update apt-get update
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \ apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \
libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
- name: Checkout repo (for scripts) - name: Checkout repo (for scripts)
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with: { submodules: 'recursive', fetch-depth: '0' }
submodules: 'recursive'
fetch-depth: '0'
- name: Ensure script permissions - name: Ensure script permissions
run: chmod 755 package-debian.sh run: chmod 755 package-debian.sh
- name: Package DEB (Debian-family) - name: Package DEB (Debian-family)
run: ./package-debian.sh "${RELEASE_TAG}" --arch all run: ./package-debian.sh "${RELEASE_TAG}" --arch all
- name: Collect DEBs into workspace - name: Collect DEBs into workspace
run: | run: |
mkdir -p "$GITHUB_WORKSPACE/dist/deb" mkdir -p "$GITHUB_WORKSPACE/dist/deb"
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" \ find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
echo "==== Dist tree ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb" || true ls -R "$GITHUB_WORKSPACE/dist/deb" || true
- name: Upload DEB artifact
- name: Upload DEBs to release uses: actions/upload-artifact@v7
uses: svenstaro/upload-release-action@v2 with: { name: release-deb, path: "dist/deb/**/*.deb", if-no-files-found: error }
with:
file: dist/deb/**/*.deb
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true
rpm: rpm:
name: build and release rpm x64 & arm64 name: build and release rpm x64 & arm64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || runs-on: ubuntu-26.04
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
container: registry.access.redhat.com/ubi10/ubi container: registry.access.redhat.com/ubi10/ubi
env: env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps: steps:
- name: Prepare tools (Red Hat) - name: Prepare tools (Red Hat)
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
. /etc/os-release . /etc/os-release
EL_MAJOR="${VERSION_ID%%.*}" EL_MAJOR="${VERSION_ID%%.*}"
echo "EL_MAJOR=${EL_MAJOR}" echo "EL_MAJOR=${EL_MAJOR}"
dnf -y makecache || true dnf -y makecache || true
command -v curl >/dev/null || dnf -y install curl ca-certificates command -v curl >/dev/null || dnf -y install curl ca-certificates
ARCH="$(uname -m)" ARCH="$(uname -m)"
case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac
install_epel_from_dir() { install_epel_from_dir() {
local base="$1" rpm local base="$1" rpm
echo "Try: $base" echo "Try: $base"
rpm="$({ curl -fsSL "$base/Packages/" 2>/dev/null; curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'; } |
rpm="$( sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' | sort -V | tail -n1)" || true
{ [[ -n "$rpm" ]] || return 1
curl -fsSL "$base/Packages/" 2>/dev/null dnf -y install "$base/Packages/$rpm"
curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'
} |
sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' |
sort -V | tail -n1
)" || true
if [[ -n "$rpm" ]]; then
dnf -y install "$base/Packages/$rpm"
return 0
fi
return 1
} }
FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm" FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm"
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os"
ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os"
echo "Try Fedora: $FEDORA" echo "Try Fedora: $FEDORA"
curl -fsSLI "$FEDORA" >/dev/null && dnf -y install "$FEDORA" ||
install_epel_from_dir "$ROCKY" ||
install_epel_from_dir "$ALMA" ||
{ echo "EPEL bootstrap failed (Fedora/Rocky/Alma)"; exit 1; }
if curl -fsSLI "$FEDORA" >/dev/null; then dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which
dnf -y install "$FEDORA"
else
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os"
if install_epel_from_dir "$ROCKY"; then
:
else
ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os"
if install_epel_from_dir "$ALMA"; then
:
else
echo "EPEL bootstrap failed (Fedora/Rocky/Alma)"
exit 1
fi
fi
fi
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core \
rsync findutils tar gzip unzip which
dnf repolist | grep -i epel || true dnf repolist | grep -i epel || true
- name: Checkout repo (for scripts) - name: Checkout repo (for scripts)
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with: { submodules: 'recursive', fetch-depth: '0' }
submodules: 'recursive'
fetch-depth: '0'
- name: Ensure script permissions - name: Ensure script permissions
run: chmod 755 package-rhel.sh run: chmod 755 package-rhel.sh
- name: Package RPM (RHEL-family) - name: Package RPM (RHEL-family)
run: ./package-rhel.sh "${RELEASE_TAG}" --arch all run: ./package-rhel.sh "${RELEASE_TAG}" --arch all
- name: Collect RPMs into workspace - name: Collect RPMs into workspace
run: | run: |
mkdir -p "$GITHUB_WORKSPACE/dist/rpm" mkdir -p "$GITHUB_WORKSPACE/dist/rpm"
@@ -170,39 +117,26 @@ jobs:
find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
echo "==== Dist tree ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
- name: Upload RPM artifact
- name: Upload RPMs to release uses: actions/upload-artifact@v7
uses: svenstaro/upload-release-action@v2 with: { name: release-rpm, path: "dist/rpm/**/*.rpm", if-no-files-found: error }
with:
file: dist/rpm/**/*.rpm
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true
rpm-riscv64: rpm-riscv64:
name: build and release rpm riscv64 name: build and release rpm riscv64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04-riscv runs-on: ubuntu-24.04-riscv
container: rockylinux/rockylinux:10 container: rockylinux/rockylinux:10
env: env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps: steps:
- name: Prepare tools (Red Hat) - name: Prepare tools (Red Hat)
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
dnf -y makecache dnf -y makecache
dnf -y install \ dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which jq
sudo git rpm-build rpmdevtools dnf-plugins-core \
rsync findutils tar gzip unzip which jq
- name: Checkout repo (for scripts) - name: Checkout repo (for scripts)
shell: bash shell: bash
env: env: { GITHUB_TOKEN: "${{ github.token }}" }
GITHUB_TOKEN: ${{ github.token }}
run: | run: |
set -euo pipefail set -euo pipefail
rm -rf ./* rm -rf ./*
@@ -212,56 +146,27 @@ jobs:
git fetch --depth=1 origin "${GITHUB_SHA}" git fetch --depth=1 origin "${GITHUB_SHA}"
git checkout FETCH_HEAD git checkout FETCH_HEAD
git submodule update --init --recursive git submodule update --init --recursive
- name: Ensure script permissions - name: Ensure script permissions
run: chmod 755 package-rhel-riscv.sh run: chmod 755 package-rhel-riscv.sh
- name: Package RPM (RHEL-family) - name: Package RPM (RHEL-family)
run: ./package-rhel-riscv.sh "${RELEASE_TAG}" run: ./package-rhel-riscv.sh "${RELEASE_TAG}"
- name: Collect RPMs into workspace - name: Collect RPMs into workspace
run: | run: |
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64" mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64"
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" \ find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
-exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
echo "==== Dist tree ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true
- name: Upload RPM artifact
- name: Upload RPMs to release uses: actions/upload-artifact@v7
shell: bash with: { name: release-rpm-riscv64, path: "dist/rpm-riscv64/**/*.rpm", if-no-files-found: error }
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
shopt -s globstar nullglob
files=(dist/rpm-riscv64/**/*.rpm)
(( ${#files[@]} )) || { echo "No RPMs found."; exit 1; }
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
upload_url="$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$api" | jq -r '.upload_url // empty' | sed 's/{?name,label}//')"
[[ "$upload_url" ]] || { echo "Release upload URL not found: ${RELEASE_TAG}"; exit 1; }
for f in "${files[@]}"; do
echo "Uploading ${f##*/}"
curl -fsSL -X POST \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/x-rpm" \
--data-binary @"$f" \
"${upload_url}?name=${f##*/}"
done
deb-riscv64: deb-riscv64:
name: build and release deb riscv64 name: build and release deb riscv64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04-riscv runs-on: ubuntu-24.04-riscv
container: debian:13 container: debian:13
env: env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
steps: steps:
- name: Prepare tools (Debian) - name: Prepare tools (Debian)
shell: bash shell: bash
@@ -269,14 +174,10 @@ jobs:
set -euo pipefail set -euo pipefail
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update apt-get update
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \ apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1
ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \
gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1
- name: Checkout repo (for scripts) - name: Checkout repo (for scripts)
shell: bash shell: bash
env: env: { GITHUB_TOKEN: "${{ github.token }}" }
GITHUB_TOKEN: ${{ github.token }}
run: | run: |
set -euo pipefail set -euo pipefail
rm -rf ./* rm -rf ./*
@@ -286,53 +187,25 @@ jobs:
git fetch --depth=1 origin "${GITHUB_SHA}" git fetch --depth=1 origin "${GITHUB_SHA}"
git checkout FETCH_HEAD git checkout FETCH_HEAD
git submodule update --init --recursive git submodule update --init --recursive
- name: Ensure script permissions - name: Ensure script permissions
run: chmod 755 package-debian-riscv.sh run: chmod 755 package-debian-riscv.sh
- name: Package DEB (Debian-family) - name: Package DEB (Debian-family)
run: ./package-debian-riscv.sh "${RELEASE_TAG}" run: ./package-debian-riscv.sh "${RELEASE_TAG}"
- name: Collect DEBs into workspace - name: Collect DEBs into workspace
run: | run: |
mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64" mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64"
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" \ find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
echo "==== Dist tree ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true
- name: Upload DEB artifact
- name: Upload DEBs to release uses: actions/upload-artifact@v7
shell: bash with: { name: release-deb-riscv64, path: "dist/deb-riscv64/**/*.deb", if-no-files-found: error }
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
shopt -s globstar nullglob
files=(dist/deb-riscv64/**/*.deb)
(( ${#files[@]} )) || { echo "No DEBs found."; exit 1; }
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
upload_url="$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$api" | jq -r '.upload_url // empty' | sed 's/{?name,label}//')"
[[ "$upload_url" ]] || { echo "Release upload URL not found: ${RELEASE_TAG}"; exit 1; }
for f in "${files[@]}"; do
echo "Uploading ${f##*/}"
curl -fsSL -X POST \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/vnd.debian.binary-package" \
--data-binary @"$f" \
"${upload_url}?name=${f##*/}"
done
deb-loong64: deb-loong64:
name: build and release deb loong64 name: build and release deb loong64
if: | if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || runs-on: ubuntu-26.04
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
env: env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2 QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
@@ -341,75 +214,73 @@ jobs:
QCOW2_IMAGE: debian13-loong64.qcow2 QCOW2_IMAGE: debian13-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.3 QEMU_VERSION: 10.2.4
steps: steps:
- name: Prepare host tools - name: Prepare host tools
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
sudo apt-get update sudo apt-get update
sudo apt-get install -y rsync qemu-utils expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
- name: Checkout repo virtiofsd --version || virtiofsd --help | head
uses: actions/checkout@v6 - name: Host clone repo
with: shell: bash
submodules: recursive env: { GITHUB_TOKEN: "${{ github.token }}" }
fetch-depth: 0 run: |
set -euo pipefail
rm -rf repo
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
- name: Download QEMU prebuild - name: Download QEMU prebuild
run: | run: |
set -euo pipefail set -euo pipefail
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz"
wget -O qemu-linux-x64.tar.gz \ tar -xzf qemu-linux-x64-v3.tar.gz
"https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64.tar.gz"
tar -xzf qemu-linux-x64.tar.gz
mkdir -p "$HOME/qemu-install" mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/"
rsync -a qemu-linux-x64/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version "$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware - name: Download loong64 qcow2 and EFI firmware
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL" wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL" wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL" wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE" qemu-img info "$QCOW2_IMAGE"
- name: Build loong64 DEB in VM through serial console - name: Build loong64 DEB in VM through serial console
shell: bash shell: bash
timeout-minutes: 180 timeout-minutes: 180
env: env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
RELEASE_TAG: ${{ env.RELEASE_TAG }}
run: | run: |
set -euo pipefail set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64" mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock"
rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" -o source="$GITHUB_WORKSPACE" -o cache=auto &
VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
for i in {1..100}; do
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
sleep 0.1
done
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
expect <<'EOF' expect <<'EOF'
log_user 1 log_user 1
set timeout -1 set timeout -1
set release_tag $env(RELEASE_TAG) set release_tag $env(RELEASE_TAG)
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64" set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu" set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
set workspace $env(GITHUB_WORKSPACE)
set repo $env(GITHUB_REPOSITORY)
set sha $env(GITHUB_SHA)
set qcow2 $env(QCOW2_IMAGE) set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE) set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS) set efi_vars $env(EFI_VARS)
proc wait_prompt {} { proc wait_prompt {} {
expect { expect {
-re "__CI_PROMPT__# " {} -re "__CI_PROMPT__# " {}
@@ -417,86 +288,258 @@ jobs:
eof { exit 1 } eof { exit 1 }
} }
} }
proc run_cmd {cmd} { proc run_cmd {cmd} {
send -- "$cmd\r" send -- "$cmd\r"
wait_prompt wait_prompt
} }
spawn $qemu_bin \ spawn $qemu_bin \
-L $qemu_rom_dir \ -L $qemu_rom_dir \
-machine virt \ -machine virt \
-accel tcg,thread=multi,tb-size=2048 \ -accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \ -cpu la464 \
-m 9216 \ -m 9216 \
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
-numa node,memdev=mem0 \
-smp 4 \ -smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \ -drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \ -drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \ -device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \ -drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \ -netdev user,id=net0 \
-device virtio-net-pci,netdev=net0 \ -device virtio-net-pci-non-transitional,netdev=net0 \
-virtfs local,path=$workspace,mount_tag=workspace,security_model=none,id=workspace \ -chardev socket,id=charfs0,path=$virtiofs_socket \
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
-display none \ -display none \
-serial stdio \ -serial stdio \
-monitor none -monitor none
expect -re "login:|debian login:" expect -re "login:|debian login:"
send -- "root\r" send -- "root\r"
expect -re "Password:|密码:|密码:" expect -re "Password:|密码:|密码:"
send -- "password\r" send -- "password\r"
expect { expect {
-re "# " {} -re "# " {}
timeout { exit 1 } timeout { exit 1 }
eof { exit 1 } eof { exit 1 }
} }
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r" send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt wait_prompt
run_cmd "mkdir -p /workspace" run_cmd "mkdir -p /workspace"
run_cmd "mount -t 9p -o trans=virtio,version=9p2000.L workspace /workspace || mount -t 9p -o trans=virtio workspace /workspace" run_cmd "mount -t virtiofs workspace /workspace"
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true" run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
run_cmd "dhclient \$IFACE || true" run_cmd "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf" run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
run_cmd "apt-get update || apt-get update || apt-get update" run_cmd "apt-get update || apt-get update || apt-get update"
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1" run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 fuse3"
run_cmd "rm -rf /root/v2rayN-src" run_cmd "rm -rf /root/v2rayN-src"
run_cmd "git clone --recursive https://github.com/$repo.git /root/v2rayN-src" run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
run_cmd "cd /root/v2rayN-src && git fetch --depth=1 origin $sha && git checkout FETCH_HEAD && git submodule update --init --recursive" run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh" run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh"
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r" send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r"
expect { expect {
-re "__BUILD_DONE__0" { -re "__BUILD_DONE__0" { send -- "poweroff\r" }
send -- "poweroff\r" default { exit 1 }
}
default {
exit 1
}
} }
EOF EOF
- name: Collect DEBs - name: Collect DEBs
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64" mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
echo "==== Dist tree ====" echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64" ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
- name: Upload DEB artifact
uses: actions/upload-artifact@v7
with: { name: release-deb-loong64, path: "dist/deb-loong64/**/*.deb", if-no-files-found: error }
- name: Upload DEBs to release rpm-loong64:
uses: svenstaro/upload-release-action@v2 name: build and release rpm loong64
with: if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
file: dist/deb-loong64/**/*.deb runs-on: ubuntu-26.04
tag: ${{ env.RELEASE_TAG }} env:
file_glob: true RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
prerelease: true QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-vars.fd
QCOW2_IMAGE: fedora43-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.4
steps:
- name: Prepare host tools
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
virtiofsd --version || virtiofsd --help | head
- name: Host clone repo
shell: bash
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf repo
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
- name: Download QEMU prebuild
run: |
set -euo pipefail
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz"
tar -xzf qemu-linux-x64-v3.tar.gz
mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware
shell: bash
run: |
set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE"
- name: Build loong64 RPM in VM through serial console
shell: bash
timeout-minutes: 180
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock"
rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" -o source="$GITHUB_WORKSPACE" -o cache=auto &
VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
for i in {1..100}; do
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
sleep 0.1
done
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
expect <<'EOF'
log_user 1
set timeout -1
set release_tag $env(RELEASE_TAG)
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS)
proc wait_prompt {} {
expect {
-re "__CI_PROMPT__# " {}
timeout { exit 1 }
eof { exit 1 }
}
}
proc run_cmd {cmd} {
send -- "$cmd\r"
wait_prompt
}
spawn $qemu_bin \
-L $qemu_rom_dir \
-machine virt \
-accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \
-m 9216 \
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
-numa node,memdev=mem0 \
-smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \
-device virtio-net-pci-non-transitional,netdev=net0 \
-chardev socket,id=charfs0,path=$virtiofs_socket \
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
-display none \
-serial stdio \
-monitor none
expect -re "login:|fedora login:"
send -- "root\r"
expect -re "Password:|密码:|密码:"
send -- "password\r"
expect {
-re "# " {}
timeout { exit 1 }
eof { exit 1 }
}
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt
run_cmd "mkdir -p /workspace"
run_cmd "mount -t virtiofs workspace /workspace"
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
run_cmd "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
run_cmd "dnf -y makecache || dnf -y makecache || dnf -y makecache"
run_cmd "dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which curl jq file ca-certificates fuse3 --nogpgcheck"
run_cmd "rm -rf /root/v2rayN-src"
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
run_cmd "cd /root/v2rayN-src && chmod 755 package-rhel-loong.sh"
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-rpm-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-rhel-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/rpm-loong64\nfind /root/rpmbuild/RPMS -name '*.rpm' -exec cp -av {} /workspace/dist/rpm-loong64/ \\; 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-rpm-build.sh\r"
expect {
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
default { exit 1 }
}
EOF
- name: Collect RPMs
shell: bash
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
find "$GITHUB_WORKSPACE/dist/rpm-loong64" -name "*.loongarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-loong64/v2rayN-linux-rhel-loong64.rpm" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm-loong64"
- name: Upload RPM artifact
uses: actions/upload-artifact@v7
with: { name: release-rpm-loong64, path: "dist/rpm-loong64/**/*.rpm", if-no-files-found: error }
release-deb:
name: sign and upload deb x64 arm64
needs: deb
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb, file_pattern: "*.deb" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-rpm:
name: sign and upload rpm x64 arm64
needs: rpm
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm, file_pattern: "*.rpm" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-rpm-riscv64:
name: sign and upload rpm riscv64
needs: rpm-riscv64
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm-riscv64, file_pattern: "*.rpm" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-deb-riscv64:
name: sign and upload deb riscv64
needs: deb-riscv64
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb-riscv64, file_pattern: "*.deb" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-deb-loong64:
name: sign and upload deb loong64
needs: deb-loong64
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb-loong64, file_pattern: "*.deb" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-rpm-loong64:
name: sign and upload rpm loong64
needs: rpm-loong64
uses: ./.github/workflows/upload-sign.yml
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm-loong64, file_pattern: "*.rpm" }
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
+23 -10
View File
@@ -12,6 +12,7 @@ on:
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
build: build:
@@ -26,6 +27,7 @@ jobs:
with: with:
target: macos target: macos
release_tag: ${{ inputs.release_tag }} release_tag: ${{ inputs.release_tag }}
secrets: inherit
dmg: dmg:
name: package and release macOS dmg name: package and release macOS dmg
@@ -45,7 +47,7 @@ jobs:
}} }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Restore build artifacts - name: Restore build artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v8
@@ -62,13 +64,24 @@ jobs:
- name: Package dmg - name: Package dmg
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }} run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
- name: Sleep for race condition between matrix jobs - name: Upload dmg artifact
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')" uses: actions/upload-artifact@v7
- name: Upload dmg to release
uses: svenstaro/upload-release-action@v2
with: with:
file: ${{ github.workspace }}/v2rayN*.dmg name: release-dmg-${{ matrix.arch }}
tag: ${{ inputs.release_tag }} path: v2rayN*.dmg
file_glob: true if-no-files-found: error
prerelease: true
release-dmg:
name: sign and upload macOS dmg
if: inputs.release_tag != ''
needs: dmg
strategy:
matrix:
arch: [ x64, arm64 ]
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: release-dmg-${{ matrix.arch }}
file_pattern: '*.dmg'
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
@@ -12,6 +12,7 @@ on:
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
build: build:
@@ -26,3 +27,4 @@ jobs:
with: with:
target: windows-desktop target: windows-desktop
release_tag: ${{ inputs.release_tag }} release_tag: ${{ inputs.release_tag }}
secrets: inherit
+247
View File
@@ -0,0 +1,247 @@
name: release Windows x86
on:
workflow_dispatch:
inputs:
release_tag:
required: false
type: string
permissions:
contents: write
actions: read
jobs:
package-x86:
name: build and release windows x86 WPF & Avalonia
if: inputs.release_tag != ''
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- flavor: wpf
project: ./v2rayN/v2rayN/v2rayN.csproj
zip_name: v2rayN-windows-86.zip
- flavor: desktop
project: ./v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj
zip_name: v2rayN-windows-86-desktop.zip
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Purge SDKs
shell: pwsh
run: |
echo "Before:"
dotnet --list-sdks
dotnet --list-sdks | % {
$_ -match '^\S+\s+\[(.+)\]$' > $null
$d = Join-Path $Matches[1] $_.Split()[0]
Write-Host "Removing $d"
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
}
echo "After:"
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET 10.0.1xx
uses: actions/setup-dotnet@v5.4.0
with:
dotnet-version: 10.0.1xx
- name: Publish ${{ matrix.flavor }} win-x86
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
dotnet restore "${{ matrix.project }}" -r win-x86
dotnet publish "${{ matrix.project }}" `
-c Release `
-r win-x86 `
-p:SelfContained=true `
-p:EnableWindowsTargeting=true `
-o "publish/${{ matrix.flavor }}"
- name: Publish AmazTool win-x86
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
dotnet publish "./v2rayN/AmazTool/AmazTool.csproj" `
-c Release `
-r win-x86 `
-p:SelfContained=true `
-p:PublishTrimmed=true `
-p:EnableWindowsTargeting=true `
-o "publish/${{ matrix.flavor }}"
- name: Detect bundled core versions
id: detect-core
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force -Path "tmp/probe" | Out-Null
Invoke-WebRequest -Uri "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-windows-64/bin/xray/xray.exe" -OutFile "tmp/probe/xray.exe"
$xrayOutput = (& "tmp/probe/xray.exe" version) -join "`n"
Write-Host $xrayOutput
$xrayMatch = [regex]::Match($xrayOutput, 'Xray\s+([0-9.]+)')
if ($xrayMatch.Success) {
$xrayVer = $xrayMatch.Groups[1].Value
} else {
throw "Failed to detect Xray version"
}
Invoke-WebRequest -Uri "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-windows-64/bin/sing_box/sing-box.exe" -OutFile "tmp/probe/sing-box.exe"
$singOutput = (& "tmp/probe/sing-box.exe" version) -join "`n"
Write-Host $singOutput
$singMatch = [regex]::Match($singOutput, 'sing-box version\s+([0-9.]+)')
if ($singMatch.Success) {
$singVer = $singMatch.Groups[1].Value
} else {
throw "Failed to detect sing-box version"
}
Write-Host "Bundled Xray version: $xrayVer"
Write-Host "Bundled sing-box version: $singVer"
"xray_version=$xrayVer" >> $env:GITHUB_OUTPUT
"sing_version=$singVer" >> $env:GITHUB_OUTPUT
- name: Download x86 cores and rule assets
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$ErrorActionPreference = "Stop"
$headers = @{
Authorization = "Bearer $env:GH_TOKEN"
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}
$root = "publish/${{ matrix.flavor }}"
$bin = Join-Path $root "bin"
$xrayDir = Join-Path $bin "xray"
$singDir = Join-Path $bin "sing_box"
$srssDir = Join-Path $bin "srss"
New-Item -ItemType Directory -Force -Path $xrayDir, $singDir, $srssDir | Out-Null
$xrayVer = "${{ steps.detect-core.outputs.xray_version }}"
$singVer = "${{ steps.detect-core.outputs.sing_version }}"
$xrayTag = "v$xrayVer"
$singTag = "v$singVer"
$xrayUrl = "https://github.com/autorepobot/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
Write-Host "Bundled Xray version: $xrayVer"
Write-Host "Bundled sing-box version: $singVer"
Write-Host "Xray: $xrayUrl"
Write-Host "sing-box: $singUrl"
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
Invoke-WebRequest -Uri $xrayUrl -OutFile "tmp/xray.zip"
Expand-Archive -Force "tmp/xray.zip" "tmp/xray"
Get-ChildItem "tmp/xray" -Recurse -Include "*.exe","*.dll" | ForEach-Object {
Copy-Item $_.FullName (Join-Path $xrayDir $_.Name) -Force
}
Invoke-WebRequest -Uri $singUrl -OutFile "tmp/sing-box.zip"
Expand-Archive -Force "tmp/sing-box.zip" "tmp/sing-box"
$singExe = Get-ChildItem "tmp/sing-box" -Recurse -Filter "sing-box.exe" | Select-Object -First 1
if (-not $singExe) {
throw "sing-box.exe not found"
}
Copy-Item $singExe.FullName "$singDir/sing-box.exe" -Force
$cronet = Get-ChildItem "tmp/sing-box" -Recurse -Filter "libcronet*.dll" | Select-Object -First 1
if ($cronet) {
Copy-Item $cronet.FullName "$singDir/libcronet.dll" -Force
}
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "$bin/geosite.dat"
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "$bin/geoip.dat"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat" -OutFile "$bin/geoip-only-cn-private.dat"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb" -OutFile "$bin/Country.mmdb"
Invoke-WebRequest -Uri "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" -OutFile "$bin/geoip.metadb"
$geoipRules = @(
"geoip-private.srs",
"geoip-cn.srs",
"geoip-facebook.srs",
"geoip-fastly.srs",
"geoip-google.srs",
"geoip-netflix.srs",
"geoip-telegram.srs",
"geoip-twitter.srs"
)
foreach ($f in $geoipRules) {
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geoip/$f" -OutFile "$srssDir/$f"
}
$geositeRules = @(
"geosite-cn.srs",
"geosite-gfw.srs",
"geosite-google.srs",
"geosite-greatfire.srs",
"geosite-geolocation-cn.srs",
"geosite-category-ads-all.srs",
"geosite-private.srs"
)
foreach ($f in $geositeRules) {
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geosite/$f" -OutFile "$srssDir/$f"
}
- name: Package zip
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Compress-Archive `
-Path "publish/${{ matrix.flavor }}/*" `
-DestinationPath "${{ matrix.zip_name }}" `
-Force
- name: Upload zip artifact
uses: actions/upload-artifact@v7
with:
name: release-windows-x86-${{ matrix.flavor }}
path: ${{ matrix.zip_name }}
if-no-files-found: error
release-x86:
name: sign and upload windows x86
if: inputs.release_tag != ''
needs: package-x86
strategy:
fail-fast: false
matrix:
flavor: [ wpf, desktop ]
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: release-windows-x86-${{ matrix.flavor }}
file_pattern: '*.zip'
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
+2
View File
@@ -12,6 +12,7 @@ on:
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
build: build:
@@ -27,3 +28,4 @@ jobs:
with: with:
target: windows target: windows
release_tag: ${{ inputs.release_tag }} release_tag: ${{ inputs.release_tag }}
secrets: inherit
+21 -2
View File
@@ -22,6 +22,7 @@ jobs:
case( case(
inputs.target == 'macos', 'macos-latest', inputs.target == 'macos', 'macos-latest',
inputs.target == 'linux', 'ubuntu-24.04', inputs.target == 'linux', 'ubuntu-24.04',
inputs.target == 'windows', 'windows-latest',
'ubuntu-latest' 'ubuntu-latest'
) )
}} }}
@@ -46,21 +47,39 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: 'recursive' submodules: 'recursive'
fetch-depth: '0' fetch-depth: '0'
- name: Purge SDKs
shell: pwsh
run: |
echo "Before:"
dotnet --list-sdks
dotnet --list-sdks | % {
$_ -match '^\S+\s+\[(.+)\]$' > $null
$d = Join-Path $Matches[1] $_.Split()[0]
Write-Host "Removing $d"
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
}
echo "After:"
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v5.3.0 uses: actions/setup-dotnet@v5.4.0
with: with:
dotnet-version: '10.0.1xx' dotnet-version: '10.0.1xx'
- name: Build v2rayN - name: Build v2rayN
shell: bash
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
- name: Build AmazTool - name: Build AmazTool
shell: bash
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
+24 -10
View File
@@ -9,9 +9,13 @@ on:
target: # windows linux macos windows-desktop target: # windows linux macos windows-desktop
required: true required: true
type: string type: string
secrets:
GPG_PRIVATE_KEY:
required: true
permissions: permissions:
contents: write contents: write
actions: read
jobs: jobs:
package: package:
@@ -37,7 +41,7 @@ jobs:
}} }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Restore build artifacts - name: Restore build artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v8
@@ -55,13 +59,23 @@ jobs:
if: inputs.target == 'windows-desktop' if: inputs.target == 'windows-desktop'
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip" run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
- name: Sleep for race condition between matrix jobs - name: Upload zip artifact
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')" uses: actions/upload-artifact@v7
- name: Upload zip archive to release
uses: svenstaro/upload-release-action@v2
with: with:
file: ${{ github.workspace }}/v2rayN*.zip name: release-${{ inputs.target }}-${{ matrix.arch }}
tag: ${{ inputs.release_tag }} path: v2rayN*.zip
file_glob: true if-no-files-found: error
prerelease: true
release:
name: sign and upload zip x64 arm64
needs: package
strategy:
matrix:
arch: [ x64, arm64 ]
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: release-${{ inputs.target }}-${{ matrix.arch }}
file_pattern: '*.zip'
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
+48
View File
@@ -0,0 +1,48 @@
name: publish GPG public key
on:
workflow_call:
inputs:
release_tag:
required: true
type: string
secrets:
GPG_PRIVATE_KEY:
required: true
workflow_dispatch:
inputs:
release_tag:
required: true
type: string
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-26.04
steps:
- name: Import GPG private key
shell: bash
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
set -euo pipefail
install -m 700 -d ~/.gnupg
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
- name: Export GPG public key
shell: bash
run: |
set -euo pipefail
fingerprint="$(gpg --batch --list-secret-keys --with-colons | awk -F: '$1 == "fpr" { print $10; exit }')"
[[ -n "$fingerprint" ]] || { echo "No GPG secret key found."; exit 1; }
gpg --batch --armor --export "$fingerprint" > v2rayN-public-key.asc
- name: Upload GPG public key to release
uses: svenstaro/upload-release-action@v2
with:
file: v2rayN-public-key.asc
tag: ${{ inputs.release_tag }}
prerelease: true
overwrite: true
+2 -2
View File
@@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: 'recursive' submodules: 'recursive'
fetch-depth: '0' fetch-depth: '0'
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v5.3.0 uses: actions/setup-dotnet@v5.4.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
+76
View File
@@ -0,0 +1,76 @@
name: sign and upload release files
on:
workflow_call:
inputs:
release_tag:
required: true
type: string
artifact_name:
required: true
type: string
file_pattern:
required: false
type: string
default: '**/*'
content_type:
required: false
type: string
default: application/octet-stream
secrets:
GPG_PRIVATE_KEY:
required: true
permissions:
contents: write
actions: read
jobs:
upload:
runs-on: ubuntu-26.04
steps:
- name: Download release artifact
uses: actions/download-artifact@v8
with:
name: ${{ inputs.artifact_name }}
path: dist
- name: Import GPG private key
shell: bash
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
set -euo pipefail
install -m 700 -d ~/.gnupg
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
- name: Sign release files
shell: bash
run: |
set -euo pipefail
cd dist
shopt -s globstar nullglob
files=( ${{ inputs.file_pattern }} )
(( ${#files[@]} )) || { echo "No files matched: ${{ inputs.file_pattern }}"; exit 1; }
for f in "${files[@]}"; do
[[ -f "$f" ]] || continue
gpg --batch --yes --armor --detach-sign --output "$f.sig" "$f"
done
- name: Upload release files
uses: svenstaro/upload-release-action@v2
with:
file: dist/${{ inputs.file_pattern }}
tag: ${{ inputs.release_tag }}
file_glob: true
prerelease: true
overwrite: true
- name: Upload release signatures
uses: svenstaro/upload-release-action@v2
with:
file: dist/${{ inputs.file_pattern }}.sig
tag: ${{ inputs.release_tag }}
file_glob: true
prerelease: true
overwrite: true
+3
View File
@@ -84,6 +84,9 @@ StyleCopReport.xml
*.rsp *.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults # but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp !Directory.Build.rsp
bin/
obj/
build/
*.sbr *.sbr
*.tlb *.tlb
*.tli *.tli
+70 -10
View File
@@ -1,18 +1,78 @@
# v2rayN # v2rayN
A GUI client for Windows, Linux and macOS, support [Xray](https://github.com/XTLS/Xray-core) ### A GUI client for Windows, Linux and macOS. Support [Xray](https://github.com/XTLS/Xray-core) and [sing-box](https://github.com/SagerNet/sing-box) and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores)
and [sing-box](https://github.com/SagerNet/sing-box)
and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayN)](https://github.com/2dust/v2rayN/commits/master)
[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayn/badge)](https://www.codefactor.io/repository/github/2dust/v2rayn) [![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayn/badge)](https://www.codefactor.io/repository/github/2dust/v2rayn)
[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayN/latest/total?logo=github)](https://github.com/2dust/v2rayN/releases) [![Release](https://img.shields.io/github/v/release/2dust/v2rayN?logo=github&label=Release)](https://github.com/2dust/v2rayN/releases)
[![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/v2rayn) [![Downloads](https://img.shields.io/github/downloads/2dust/v2rayN/latest/total?logo=github&label=Downloads)](https://github.com/2dust/v2rayN/releases)
[![Telegram](https://img.shields.io/badge/Telegram-Chat-26A5E4?logo=telegram)](https://t.me/v2rayn)
## How to use [![Windows](https://img.shields.io/badge/Windows-supported-0078D6?logo=windows)](https://github.com/2dust/v2rayN)
[![Linux](https://img.shields.io/badge/Linux-supported-FCC624?logo=linux&logoColor=000)](https://github.com/2dust/v2rayN)
[![macOS](https://img.shields.io/badge/macOS-supported-000000?logo=apple)](https://github.com/2dust/v2rayN)
[![GPG Signed](https://img.shields.io/badge/GPG-signed-4B32C3?logo=gnuprivacyguard)](https://github.com/2dust/v2rayN)
Read the [Wiki](https://github.com/2dust/v2rayN/wiki) for details.
## Telegram Channel ---
[github_2dust](https://t.me/github_2dust) ## Download / 下载
Download the latest release here:
在这里下载最新版本:
[https://github.com/2dust/v2rayN/releases](https://github.com/2dust/v2rayN/releases)
> [!TIP]
> v2rayN is the desktop version. For the mobile version, please visit the v2rayNG \
> v2rayN 是电脑版,手机版请访问 v2rayNG
>
> https://github.com/2dust/v2rayNG
---
## Documentation / 使用文档
Read the Wiki for usage guides and configuration details.
请阅读 Wiki 获取使用说明和配置教程。
[https://github.com/2dust/v2rayN/wiki](https://github.com/2dust/v2rayN/wiki)
---
## Supported Platforms / 支持平台
| Platform / 平台 | x64 | x86 | arm64 | riscv64 | loong64 |
| --- | --- | --- | --- | --- | --- |
| Windows | ✅ | ✅ | ✅ | - | - |
| Linux | ✅ | - | ✅ | ✅ | ✅ |
| macOS | ✅ | - | ✅ | - | - |
---
## GPG Verification / GPG 签名校验
Release files are signed with GPG to verify authenticity and integrity, helping prevent mirror, ISP, or CDN hijacking.
发布文件已使用 GPG 签名,可用于校验文件真实性与完整性,预防镜像站、运营商或 CDN 劫持。
### Fingerprint / 公钥指纹
```text
7694 5E9F 3E9A 168F 8070 F195 805D 661C
134D FAF6 8903 C199 463C 31E5 AE90 3AE0
```
---
## Community / 社区
Telegram Group / Telegram 群组:
[https://t.me/v2rayN](https://t.me/v2rayN)
Telegram Channel / Telegram 频道:
[https://t.me/github_2dust](https://t.me/github_2dust)
+2 -2
View File
@@ -13,8 +13,8 @@ PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0" DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.108" DOTNET_LOONGARCH_VERSION="10.0.109"
DOTNET_LOONGARCH_TAG="v10.0.108-loongarch64" DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
+1 -1
View File
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_RISCV_VERSION="10.0.108" DOTNET_RISCV_VERSION="10.0.109"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
+709
View File
@@ -0,0 +1,709 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION_ARG=""
WITH_CORE="both"
FORCE_NETCORE=0
BUILD_FROM=""
XRAY_VER="${XRAY_VER:-}"
SING_VER="${SING_VER:-}"
MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_LOONGARCH_VERSION="10.0.109"
DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
OS_ID=""
OS_NAME=""
OS_VERSION_ID=""
HOST_ARCH=""
SCRIPT_DIR=""
PROJECT=""
VERSION=""
declare -a BUILT_RPMS=()
die() {
echo "$*" >&2
exit 1
}
parse_args() {
local first_arg="${1:-}"
if [[ -n "$first_arg" && "$first_arg" != --* ]]; then
VERSION_ARG="$first_arg"
shift || true
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--with-core) WITH_CORE="${2:-both}"; shift 2 ;;
--xray-ver) XRAY_VER="${2:-}"; shift 2 ;;
--singbox-ver) SING_VER="${2:-}"; shift 2 ;;
--netcore) FORCE_NETCORE=1; shift ;;
--buildfrom) BUILD_FROM="${2:-}"; shift 2 ;;
*)
[[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1"
shift
;;
esac
done
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
die "You cannot specify both an explicit version and --buildfrom at the same time.
Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
fi
}
detect_environment() {
local current_kernel=""
local lowest=""
. /etc/os-release
OS_ID="${ID:-}"
OS_NAME="${NAME:-$OS_ID}"
OS_VERSION_ID="${VERSION_ID:-}"
HOST_ARCH="$(uname -m)"
case "$OS_ID" in
rhel|rocky|almalinux|fedora|centos)
echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}"
;;
*)
die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}).
This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS."
;;
esac
case "$HOST_ARCH" in
loongarch64) ;;
*) die "Only supports loongarch64" ;;
esac
current_kernel="$(uname -r)"
lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)"
[[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL"
echo "[OK] Kernel $current_kernel verified."
}
install_dependencies() {
local install_ok=0
local tmp_dotnet=""
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y --nogpgcheck install \
rpm-build rpmdevtools curl unzip tar jq rsync git python3 \
glibc-devel kernel-headers libatomic file ca-certificates libicu \
&& install_ok=1
mkdir -p "$HOME/.dotnet"
tmp_dotnet="$(mktemp -d)"
curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_LOONGARCH_FILE"
tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_LOONGARCH_FILE"
rm -rf "$tmp_dotnet"
export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
mkdir -p "$HOME/.nuget/NuGet"
cat > "$HOME/.nuget/NuGet/NuGet.Config" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="loongnix" value="https://lnuget.loongnix.cn/v3/index.json" allowInsecureConnections="true" />
</packageSources>
</configuration>
EOF
dotnet --info >/dev/null 2>&1 || install_ok=0
fi
if [[ "$install_ok" -ne 1 ]]; then
echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:"
echo "dotnet-loongarch SDK, curl, unzip, tar, rsync, git, python3, rpm, rpmdevtools, rpm-build (on Red Hat branch)"
exit 1
fi
}
prepare_workspace() {
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
PROJECT="$PROJECT_HINT"
[[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
[[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found"
}
choose_channel() {
local ch="latest"
local sel=""
if [[ -n "${BUILD_FROM:-}" ]]; then
case "$BUILD_FROM" in
1) echo "latest"; return 0 ;;
2) echo "prerelease"; return 0 ;;
3) echo "keep"; return 0 ;;
*) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;;
esac
fi
if [[ -t 0 ]]; then
echo "[?] Choose v2rayN release channel:" >&2
echo " 1) Latest (stable) [default]" >&2
echo " 2) Pre-release (preview)" >&2
echo " 3) Keep current (do nothing)" >&2
printf "Enter 1, 2 or 3 [default 1]: " >&2
if read -r sel </dev/tty; then
case "${sel:-}" in
2) ch="prerelease" ;;
3) ch="keep" ;;
esac
fi
fi
echo "$ch"
}
get_latest_tag_latest() {
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
| jq -re '.tag_name' \
| sed 's/^v//'
}
get_latest_tag_prerelease() {
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20" \
| jq -re 'first(.[] | select(.prerelease == true) | .tag_name)' \
| sed 's/^v//'
}
sync_submodules() {
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
}
git_try_checkout() {
local want="$1"
local ref=""
if git rev-parse --git-dir >/dev/null 2>&1; then
git fetch --tags --force --prune --depth=1 || true
git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want"
if [[ -n "$ref" ]]; then
echo "[OK] Found ref '${ref}', checking out..."
git checkout -f "$ref"
sync_submodules
return 0
fi
fi
return 1
}
apply_channel_or_keep() {
local ch="$1"
local tag=""
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
VERSION="$(git describe --tags --abbrev=0 2>/dev/null || echo '0.0.0+git')"
VERSION="${VERSION#v}"
return 0
fi
echo "[*] Resolving ${ch} tag from GitHub releases..."
case "$ch" in
latest) tag="$(get_latest_tag_latest || true)" ;;
prerelease) tag="$(get_latest_tag_prerelease || true)" ;;
*) die "Failed to resolve latest tag for channel '${ch}'." ;;
esac
[[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'."
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || die "Failed to checkout '${tag}'."
VERSION="${tag#v}"
}
resolve_version() {
if git rev-parse --git-dir >/dev/null 2>&1; then
if [[ -n "${VERSION_ARG:-}" ]]; then
local clean_ver="${VERSION_ARG#v}"
if git_try_checkout "$clean_ver"; then
VERSION="$clean_ver"
else
echo "[WARN] Tag '${VERSION_ARG}' not found."
apply_channel_or_keep "$(choose_channel)"
fi
else
apply_channel_or_keep "$(choose_channel)"
fi
else
echo "Current directory is not a git repo; proceeding on current tree."
VERSION="${VERSION_ARG:-0.0.0}"
fi
VERSION="${VERSION#v}"
echo "[*] GUI version resolved as: ${VERSION}"
}
xray_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-loongarch64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-loong64.zip" ;;
*) return 1 ;;
esac
}
singbox_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-loongarch64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-loong64.tar.gz" ;;
*) return 1 ;;
esac
}
bundle_url_for_rid() {
local rid="$1"
case "$rid" in
linux-loongarch64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-loong64.zip" ;;
*) return 1 ;;
esac
}
download_xray() {
local outdir="$1"
local rid="$2"
local ver="${XRAY_VER:-}"
local url=""
local tmp=""
mkdir -p "$outdir"
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' \
| sed -E 's/.*"v([^"]+)".*/\1/' \
| head -n1)" || true
fi
[[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; }
url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; }
echo "[+] Download xray: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; }
unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; }
install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; }
rm -rf "$tmp"
}
download_singbox() {
local outdir="$1"
local rid="$2"
local ver="${SING_VER:-}"
local url=""
local tmp=""
local bin=""
local cronet=""
mkdir -p "$outdir"
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' \
| sed -E 's/.*"v([^"]+)".*/\1/' \
| head -n1)" || true
fi
[[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; }
url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; }
echo "[+] Download sing-box: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; }
tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; }
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)"
[[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; rm -rf "$tmp"; return 1; }
install -m 755 "$bin" "$outdir/sing-box" || { rm -rf "$tmp"; return 1; }
cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)"
[[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true
rm -rf "$tmp"
}
unify_geo_layout() {
local outroot="$1"
local n
local names=(
geosite.dat
geoip.dat
geoip-only-cn-private.dat
Country.mmdb
geoip.metadb
)
mkdir -p "$outroot/bin"
for n in "${names[@]}"; do
if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
done
}
download_geo_assets() {
local outroot="$1"
local bin_dir="$outroot/bin"
local srss_dir="$bin_dir/srss"
local f=""
mkdir -p "$bin_dir" "$srss_dir"
echo "[+] Download Xray Geo to ${bin_dir}"
curl -fsSL -o "$bin_dir/geosite.dat" "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat"
curl -fsSL -o "$bin_dir/geoip.dat" "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat"
curl -fsSL -o "$bin_dir/geoip-only-cn-private.dat" "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat"
curl -fsSL -o "$bin_dir/Country.mmdb" "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
echo "[+] Download sing-box rule DB & rule-sets"
curl -fsSL -o "$bin_dir/geoip.metadb" "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb"
for f in geoip-private.srs geoip-cn.srs geoip-facebook.srs geoip-fastly.srs geoip-google.srs geoip-netflix.srs geoip-telegram.srs geoip-twitter.srs; do
curl -fsSL -o "$srss_dir/$f" "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geoip/$f"
done
for f in geosite-cn.srs geosite-gfw.srs geosite-google.srs geosite-greatfire.srs geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do
curl -fsSL -o "$srss_dir/$f" "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geosite/$f"
done
unify_geo_layout "$outroot"
}
populate_assets_zip_mode() {
local outroot="$1"
local rid="$2"
local url=""
local tmp=""
local nested_dir=""
url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; }
echo "[+] Try v2rayN bundle archive: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; }
unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; return 1; }
if [[ -d "$tmp/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$tmp/bin/" "$outroot/bin/"
else
rsync -a "$tmp/" "$outroot/"
fi
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
if [[ -n "$nested_dir" && -d "$nested_dir/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$nested_dir/bin/" "$outroot/bin/"
rm -rf "$nested_dir"
fi
unify_geo_layout "$outroot"
rm -rf "$tmp"
echo "[+] Bundle extracted to $outroot"
}
populate_assets_netcore_mode() {
local outroot="$1"
local rid="$2"
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)"
}
stage_runtime_assets() {
local outroot="$1"
local rid="$2"
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
if [[ "$FORCE_NETCORE" -eq 0 ]]; then
if populate_assets_zip_mode "$outroot" "$rid"; then
echo "[*] Using v2rayN bundle archive."
else
echo "[*] Bundle failed, fallback to separate core + rules."
populate_assets_netcore_mode "$outroot" "$rid"
fi
else
echo "[*] --netcore specified: use separate core + rules."
populate_assets_netcore_mode "$outroot" "$rid"
fi
}
describe_target() {
local short="$1"
case "$short" in
loongarch64) printf '%s\n%s\n%s\n' "linux-loongarch64" "loongarch64" "loongarch64" ;;
*) echo "Unknown arch '$short' (use loongarch64)" >&2; return 1 ;;
esac
}
publish_binary() {
local rid="$1"
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true
dotnet restore "$PROJECT"
dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true
}
write_spec_file() {
local specfile="$1"
cat > "$specfile" <<'SPEC'
%global debug_package %{nil}
%undefine _debuginfo_subpackages
%undefine _debugsource_packages
%global __requires_exclude ^liblttng-ust\.so\..*$
Name: v2rayN
Version: __VERSION__
Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues
ExclusiveArch: loongarch64
Source0: __PKGROOT__.tar.gz
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.34
Requires: fontconfig >= 2.13.1
Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32
Requires: bash >= 5.1
Requires: freetype >= 2.10
%description
v2rayN Linux for Red Hat Enterprise Linux
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS
For more information, Please visit our website
https://github.com/2dust/v2rayN
%prep
%setup -q -n __PKGROOT__
%build
%install
install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/
find %{buildroot}/opt/v2rayN -type d -exec chmod 0755 {} +
find %{buildroot}/opt/v2rayN -type f -exec chmod 0644 {} +
[ -f %{buildroot}/opt/v2rayN/v2rayN ] && chmod 0755 %{buildroot}/opt/v2rayN/v2rayN || :
install -dm0755 %{buildroot}%{_bindir}
install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash
set -euo pipefail
DIR="/opt/v2rayN"
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi
for dll in v2rayN.Desktop.dll v2rayN.dll; do
if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi
done
echo "v2rayN launcher: no executable found in $DIR" >&2
ls -l "$DIR" >&2 || true
exit 1
EOF
install -dm0755 %{buildroot}%{_datadir}/applications
install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=v2rayN
Comment=v2rayN for Red Hat Enterprise Linux
Exec=v2rayn
Icon=v2rayn
Terminal=false
Categories=Network;
EOF
install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
%post
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
%postun
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
%files
%{_bindir}/v2rayn
/opt/v2rayN
%{_datadir}/applications/v2rayn.desktop
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
SPEC
sed -i "s/__VERSION__/${VERSION}/g" "$specfile"
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$specfile"
}
package_binary() {
local short="$1"
local rid="$2"
local rpm_target="$3"
local archdir="$4"
local pubdir=""
local workdir=""
local specfile=""
local sourcedir=""
local specdir=""
local project_dir=""
local icon_candidate=""
local f=""
pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish"
[[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; }
workdir="$(mktemp -d)"
trap '[[ -n "${workdir:-}" ]] && rm -rf "$workdir"' RETURN
mkdir -p "$workdir/$PKGROOT"
cp -a "$pubdir/." "$workdir/$PKGROOT/"
project_dir="$(cd "$(dirname "$PROJECT")" && pwd)"
icon_candidate="$project_dir/v2rayN.png"
[[ -f "$icon_candidate" ]] || { echo "Required icon not found: $icon_candidate"; return 1; }
cp "$icon_candidate" "$workdir/$PKGROOT/v2rayn.png"
stage_runtime_assets "$workdir/$PKGROOT" "$rid"
rpmdev-setuptree
sourcedir="${RPM_TOPDIR}/SOURCES"
specdir="${RPM_TOPDIR}/SPECS"
specfile="${specdir}/v2rayN.spec"
mkdir -p "$sourcedir" "$specdir"
tar -C "$workdir" -czf "$sourcedir/$PKGROOT.tar.gz" "$PKGROOT"
write_spec_file "$specfile"
rpmbuild -ba "$specfile" --target "$rpm_target"
echo "Build done for $short. RPM at:"
for f in "${RPM_TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do
[[ -e "$f" ]] || continue
echo " $f"
BUILT_RPMS+=("$f")
done
}
select_targets() {
printf '%s\n' loongarch64
}
build_one_target() {
local short="$1"
local meta=()
local rid=""
local rpm_target=""
local archdir=""
mapfile -t meta < <(describe_target "$short") || return 1
rid="${meta[0]}"
rpm_target="${meta[1]}"
archdir="${meta[2]}"
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
publish_binary "$rid"
package_binary "$short" "$rid" "$rpm_target" "$archdir"
}
print_summary() {
local rp=""
echo ""
echo "================ Build Summary ================"
if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then
for rp in "${BUILT_RPMS[@]}"; do
echo "$rp"
done
else
echo "No RPMs detected in summary (check build logs above)."
fi
echo "=============================================="
}
main() {
local targets=()
local arch=""
parse_args "$@"
detect_environment
install_dependencies
prepare_workspace
resolve_version
mapfile -t targets < <(select_targets)
for arch in "${targets[@]}"; do
build_one_target "$arch"
done
print_summary
}
main "$@"
+2 -2
View File
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild" RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_RISCV_VERSION="10.0.108" DOTNET_RISCV_VERSION="10.0.109"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -501,7 +501,7 @@ write_spec_file() {
Name: v2rayN Name: v2rayN
Version: __VERSION__ Version: __VERSION__
Release: 1%{?dist} Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (riscv64) Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues BugURL: https://github.com/2dust/v2rayN/issues
+1 -1
View File
@@ -490,7 +490,7 @@ write_spec_file() {
Name: v2rayN Name: v2rayN
Version: __VERSION__ Version: __VERSION__
Release: 1%{?dist} Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64) Summary: v2rayN (Avalonia) GUI client for Linux
License: GPL-3.0-only License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues BugURL: https://github.com/2dust/v2rayN/issues
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.22.6</Version> <Version>7.23.2</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
+37 -37
View File
@@ -1,39 +1,39 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled> <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.17" /> <PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.17" /> <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.18" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" /> <PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" />
<PackageVersion Include="CliWrap" Version="3.10.1" /> <PackageVersion Include="CliWrap" Version="3.10.2" />
<PackageVersion Include="Downloader" Version="5.7.0" /> <PackageVersion Include="Downloader" Version="5.9.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" /> <PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.28" /> <PackageVersion Include="ReactiveUI" Version="23.2.28" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" /> <PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" /> <PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.3" /> <PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" /> <PackageVersion Include="sqlite-net-e" Version="1.11.0" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.2" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" /> <PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" /> <PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="18.0.0" /> <PackageVersion Include="YamlDotNet" Version="18.0.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" /> <PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -1,8 +1,10 @@
using AwesomeAssertions; using AwesomeAssertions;
using ServiceLib.Common; using ServiceLib.Common;
using ServiceLib.Enums; using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager; using ServiceLib.Manager;
using ServiceLib.Models; using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig; using ServiceLib.Services.CoreConfig;
using Xunit; using Xunit;
@@ -557,4 +559,37 @@ public class CoreConfigSingboxServiceTests
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global)); cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct)); cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
} }
[Fact]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{
var shareLink =
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreTypeItem =
[
new CoreTypeItem { ConfigType = EConfigType.Hysteria2, CoreType = ECoreType.sing_box }
];
CoreConfigTestFactory.BindAppManagerConfig(config);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2");
proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public");
proxy.realm.realm_id.Should().Be("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull();
}
} }
@@ -0,0 +1,91 @@
using AwesomeAssertions;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models.Dto;
using Xunit;
namespace ServiceLib.Tests.Fmt;
public class HyRealmTests
{
[Fact]
public void TryParse_ShouldParseValidRealm()
{
var str = "realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478";
var result = HyRealm.TryParse(str, out var realm);
result.Should().BeTrue();
realm.Should().NotBeNull();
realm.IsHttp.Should().BeFalse();
realm.Token.Should().Be("public");
realm.RendezvousHost.Should().Be("realm.hy2.io");
realm.RendezvousPort.Should().Be(443);
realm.RealmName.Should().Be("57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
realm.StunList.Should().HaveCount(2);
realm.StunList.Should().Contain("example.stun:3478");
realm.StunList.Should().Contain("example2.stun:3478");
}
[Fact]
public void ToUri_ShouldGenerateValidUri()
{
var realm = new HyRealm(
IsHttp: false,
Token: "public",
RendezvousHost: "realm.hy2.io",
RendezvousPort: 443,
RealmName: "57f9be7c-2810-4f5b-8cb9-260bc84d6c90",
StunList: ["example.stun:3478", "example2.stun:3478"]
);
var uri = realm.ToUri();
uri.Should().Contain("realm://public@realm.hy2.io");
uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
uri.Should().Contain("stun=example.stun:3478");
uri.Should().Contain("stun=example2.stun:3478");
}
[Fact]
public void GetShareUriAndResolveConfig_Hy2Realm_ShouldRoundTripBasicFields()
{
var str = "hysteria2+realm://mytoken@rendezvous.example.com/my-cabin-1f3a8c2e9b?auth=your_password&insecure=1&pinSHA256=deadbeef#remark";
var resolved = Hysteria2Fmt.ResolveRealm(str, out var msg);
resolved.Should().NotBeNull();
resolved.Password.Should().Be("your_password");
var result = HyRealm.TryParse(resolved.GetProtocolExtra().Hy2RealmUrl, out var realm);
result.Should().BeTrue();
realm.Should().NotBeNull();
realm.Token.Should().Be("mytoken");
// To uri
var uri = Hysteria2Fmt.ToUri(resolved);
uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
uri.Should().EndWith("#remark");
}
[Fact]
public void ToServerUrl_ShouldIncludeSchemeForSingbox()
{
var realm = new HyRealm(
IsHttp: false,
Token: "public",
RendezvousHost: "realm.hy2.io",
RendezvousPort: 443,
RealmName: "my-realm-id",
StunList: ["turn.cloudflare.com:3478"]
);
realm.ToServerUrl().Should().Be("https://realm.hy2.io:443");
}
[Fact]
public void ResolveRealm_Issue9635_ShouldProduceHttpsServerUrl()
{
var str = "hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var resolved = Hysteria2Fmt.ResolveRealm(str, out _);
resolved.Should().NotBeNull();
HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
realm!.ToServerUrl().Should().StartWith("https://");
realm.ToServerUrl().Should().Contain("realm.hy2.io");
realm.StunList.Should().Contain("turn.cloudflare.com:3478");
}
}
+16
View File
@@ -118,4 +118,20 @@ public static class Extension
destination.Add(item); destination.Add(item);
} }
} }
/// <summary>
/// Replace all cross-platform newline characters with the specified string
/// </summary>
public static string ReplaceLineBreaks(this string input, string replacement)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
// You must replace \r\n first, and then replace the single characters \r and \n.
return input.Replace("\r\n", replacement)
.Replace("\r", replacement)
.Replace("\n", replacement);
}
} }
+5 -1
View File
@@ -202,7 +202,7 @@ public static class FileUtils
} }
} }
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine) public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine, string? contains = null)
{ {
try try
{ {
@@ -214,6 +214,10 @@ public static class FileUtils
{ {
continue; continue;
} }
if (contains.IsNotEmpty() && !file.Name.Contains(contains))
{
continue;
}
file.Delete(); file.Delete();
} }
} }
+98 -5
View File
@@ -51,7 +51,7 @@ public class Utils
try try
{ {
str = str.Replace(Environment.NewLine, string.Empty); str = str.ReplaceLineBreaks(string.Empty);
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries)); return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
} }
catch (Exception ex) catch (Exception ex)
@@ -114,9 +114,7 @@ public class Utils
} }
plainText = plainText.Trim() plainText = plainText.Trim()
.Replace(Environment.NewLine, "") .ReplaceLineBreaks("")
.Replace("\n", "")
.Replace("\r", "")
.Replace('_', '/') .Replace('_', '/')
.Replace('-', '+') .Replace('-', '+')
.Replace(" ", ""); .Replace(" ", "");
@@ -321,7 +319,32 @@ public class Utils
return text; return text;
} }
return text.Replace(",", ",").Replace(Environment.NewLine, ","); return text.Replace(",", ",")
.Replace(" ", "")
.ReplaceLineBreaks(",");
}
public static string ParseProcess(string text)
{
if (text.IsNullOrEmpty())
{
return string.Empty;
}
if (text.StartsWith('"'))
{
text = text[1..];
}
if (text.EndsWith('"'))
{
text = text[..^1];
}
return List2String(text.Replace(",", ",")
.Replace("\\", "/")
.ReplaceLineBreaks(",")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.TrimEx())
.Where(x => x.IsNotEmpty())
.ToList());
} }
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
@@ -576,6 +599,58 @@ public class Utils
} }
} }
public static bool TryParseRange(string? input, int min, int max, out int from, out int to)
{
from = to = 0;
if (string.IsNullOrWhiteSpace(input))
{
return true;
}
var parts = input.Split('-');
if (parts.Length == 1)
{
if (!int.TryParse(parts[0], out from))
{
return false;
}
to = from;
return from >= min && to <= max;
}
if (parts.Length != 2
|| !int.TryParse(parts[0], out from)
|| !int.TryParse(parts[1], out to))
{
return false;
}
return from >= min && to <= max && from <= to;
}
public static bool TryParseMaxSplit(string? input, int min, int max, out int from, out int to)
{
from = to = 0;
if (string.IsNullOrWhiteSpace(input))
{
return true;
}
var parts = input.Split('-');
if (parts.Length == 1)
{
if (!int.TryParse(parts[0], out from))
{
return false;
}
to = from;
return from >= min && to <= max;
}
if (parts.Length != 2
|| !int.TryParse(parts[0], out from)
|| !int.TryParse(parts[1], out to))
{
return false;
}
return from >= min && to <= max && from <= to;
}
public static bool IsPrivateNetwork(string ip) public static bool IsPrivateNetwork(string ip)
{ {
if (IPAddress.TryParse(ip, out var address)) if (IPAddress.TryParse(ip, out var address))
@@ -716,6 +791,24 @@ public class Utils
return (endpoints, connections); return (endpoints, connections);
} }
public static bool IsLocalIP(string ipAddress)
{
if (!IPAddress.TryParse(ipAddress, out var targetAddress))
{
return false;
}
return NetworkInterface.GetAllNetworkInterfaces()
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
.Any(ua => ua.Address.Equals(targetAddress));
}
public static bool ContainsInterfaceName(string inInterfaceName)
{
return NetworkInterface.GetAllNetworkInterfaces()
.Any(ni => ni.Name.Equals(inInterfaceName, StringComparison.OrdinalIgnoreCase));
}
#endregion Speed Test #endregion Speed Test
#region Miscellaneous #region Miscellaneous
+25 -1
View File
@@ -92,6 +92,7 @@ public class Global
public const string LinuxBash = "/bin/bash"; public const string LinuxBash = "/bin/bash";
public const string StringTrue = "true"; public const string StringTrue = "true";
public const string StringFalse = "false"; public const string StringFalse = "false";
public const int SqliteMaxBatchSize = 10000;
public const string SingboxDirectDNSTag = "direct_dns"; public const string SingboxDirectDNSTag = "direct_dns";
public const string SingboxRemoteDNSTag = "remote_dns"; public const string SingboxRemoteDNSTag = "remote_dns";
@@ -210,6 +211,10 @@ public class Global
public const string Hysteria2ProtocolShare = "hy2://"; public const string Hysteria2ProtocolShare = "hy2://";
public const string Hysteria2RealmProtocolShare = "hysteria2+realm://";
public const string Hysteria2HttpRealmProtocolShare = "hysteria2+realm+http://";
public const string NaiveHttpsProtocolShare = "naive+https://"; public const string NaiveHttpsProtocolShare = "naive+https://";
public const string NaiveQuicProtocolShare = "naive+quic://"; public const string NaiveQuicProtocolShare = "naive+quic://";
@@ -406,6 +411,16 @@ public class Global
"" ""
]; ];
public static readonly List<string> FragmentPacketsOptions =
[
"tlshello",
"1-1",
"1-2",
"1-3",
"1-4",
"1-5"
];
public static readonly List<string> UserAgent = public static readonly List<string> UserAgent =
[ [
"chrome", "chrome",
@@ -476,7 +491,8 @@ public class Global
"fa", "fa",
"fr", "fr",
"ru", "ru",
"hu" "hu",
"id"
]; ];
public static readonly List<string> Alpns = public static readonly List<string> Alpns =
@@ -666,6 +682,14 @@ public class Global
"mcbe:bedrock.talonmc.net", "mcbe:bedrock.talonmc.net",
]; ];
public static readonly List<string> DefaultRealmStunList =
[
"turn.cloudflare.com:3478",
"stun.nextcloud.com:3478",
"stun.sip.us:3478",
"global.stun.twilio.com:3478",
];
public static readonly List<string> OutboundTags = public static readonly List<string> OutboundTags =
[ [
ProxyTag, ProxyTag,
@@ -6,8 +6,8 @@ public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeVali
} }
/// <summary> /// <summary>
/// Holds the results of a full context build, including the main context and an optional /// Holds the results of a full context build, including the main context and an optional
/// pre-socks context (e.g. for TUN protection or pre-socks chaining). /// pre-socks context (e.g. for TUN protection or pre-socks chaining).
/// </summary> /// </summary>
public record CoreConfigContextBuilderAllResult( public record CoreConfigContextBuilderAllResult(
CoreConfigContextBuilderResult MainResult, CoreConfigContextBuilderResult MainResult,
@@ -17,8 +17,8 @@ public record CoreConfigContextBuilderAllResult(
public bool Success => MainResult.Success && (PreSocksResult?.Success ?? true); public bool Success => MainResult.Success && (PreSocksResult?.Success ?? true);
/// <summary> /// <summary>
/// Merges all errors and warnings from the main result and the optional pre-socks result /// Merges all errors and warnings from the main result and the optional pre-socks result
/// into a single <see cref="NodeValidatorResult"/> for unified notification. /// into a single <see cref="NodeValidatorResult" /> for unified notification.
/// </summary> /// </summary>
public NodeValidatorResult CombinedValidatorResult => new( public NodeValidatorResult CombinedValidatorResult => new(
[.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []], [.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []],
@@ -28,14 +28,14 @@ public record CoreConfigContextBuilderAllResult(
public class CoreConfigContextBuilder public class CoreConfigContextBuilder
{ {
/// <summary> /// <summary>
/// Builds a <see cref="CoreConfigContext"/> for the given node, resolves its proxy map, /// Builds a <see cref="CoreConfigContext" /> for the given node, resolves its proxy map,
/// and processes outbound nodes referenced by routing rules. /// and processes outbound nodes referenced by routing rules.
/// </summary> /// </summary>
public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node) public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node)
{ {
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray; var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
var context = new CoreConfigContext() var context = new CoreConfigContext
{ {
Node = node, Node = node,
RunCoreType = runCoreType, RunCoreType = runCoreType,
@@ -61,7 +61,8 @@ public class CoreConfigContextBuilder
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true)) if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
{ {
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? []; var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? [];
foreach (var ruleItem in rules.Where(ruleItem => ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag))) foreach (var ruleItem in rules.Where(ruleItem =>
ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
{ {
if (ruleItem.OutboundTag.IsNullOrEmpty()) if (ruleItem.OutboundTag.IsNullOrEmpty())
{ {
@@ -72,7 +73,8 @@ public class CoreConfigContextBuilder
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag); var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
if (ruleOutboundNode == null) if (ruleOutboundNode == null)
{ {
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound, ruleItem.Remarks, ruleItem.OutboundTag)); validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound,
ruleItem.Remarks, ruleItem.OutboundTag));
ruleItem.OutboundTag = Global.ProxyTag; ruleItem.OutboundTag = Global.ProxyTag;
continue; continue;
} }
@@ -83,7 +85,8 @@ public class CoreConfigContextBuilder
if (!ruleNodeValidatorResult.Success) if (!ruleNodeValidatorResult.Success)
{ {
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e => validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e =>
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag, e))); string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag,
e)));
ruleItem.OutboundTag = Global.ProxyTag; ruleItem.OutboundTag = Global.ProxyTag;
continue; continue;
} }
@@ -93,26 +96,48 @@ public class CoreConfigContextBuilder
} }
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 }) if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{ {
var appConfig = JsonUtils.DeepCopy(config);
var routeExcludeAddressList = new List<string>();
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress) foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
{ {
try try
{ {
IPNetwork2.Parse(addr); IPNetwork2.Parse(addr);
routeExcludeAddressList.Add(addr);
} }
catch catch
{ {
validatorResult.Errors.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr)); validatorResult.Warnings.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr));
} }
} }
appConfig.TunModeItem.RouteExcludeAddress = routeExcludeAddressList;
context = context with { AppConfig = appConfig };
}
if (!context.AppConfig.CoreBasicItem.SendThrough.IsNullOrEmpty()
&& !Utils.IsLocalIP(context.AppConfig.CoreBasicItem.SendThrough))
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsSendThrough));
var appConfig = JsonUtils.DeepCopy(config);
appConfig.CoreBasicItem.SendThrough = string.Empty;
context = context with { AppConfig = appConfig };
}
if (!context.AppConfig.CoreBasicItem.BindInterface.IsNullOrEmpty()
&& (!Utils.ContainsInterfaceName(context.AppConfig.CoreBasicItem.BindInterface)
|| !(context.IsTunEnabled || context.IsWindows)))
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsBindInterface));
var appConfig = JsonUtils.DeepCopy(config);
appConfig.CoreBasicItem.BindInterface = string.Empty;
context = context with { AppConfig = appConfig };
} }
return new CoreConfigContextBuilderResult(context, validatorResult); return new CoreConfigContextBuilderResult(context, validatorResult);
} }
/// <summary> /// <summary>
/// Builds the main <see cref="CoreConfigContext"/> for <paramref name="node"/> and, when /// Builds the main <see cref="CoreConfigContext" /> for <paramref name="node" /> and, when
/// the main build succeeds, also builds the optional pre-socks context required for TUN /// the main build succeeds, also builds the optional pre-socks context required for TUN
/// protection or pre-socks proxy chaining. /// protection or pre-socks proxy chaining.
/// </summary> /// </summary>
public static async Task<CoreConfigContextBuilderAllResult> BuildAll(Config config, ProfileItem node) public static async Task<CoreConfigContextBuilderAllResult> BuildAll(Config config, ProfileItem node)
{ {
@@ -132,16 +157,31 @@ public class CoreConfigContextBuilder
{ {
Context = mainResult.Context with Context = mainResult.Context with
{ {
IsTunEnabled = false, // main core doesn't handle tun directly when pre-socks is used IsTunEnabled = false,
// main core doesn't handle tun directly when pre-socks is used
ProtectDomainList = [.. mainResult.Context.ProtectDomainList, .. preResult.Context.ProtectDomainList], ProtectDomainList = [.. mainResult.Context.ProtectDomainList, .. preResult.Context.ProtectDomainList],
} },
}; };
if (mainResult.Context.IsTunEnabled
&& mainResult.Context.AppConfig.TunModeItem.StrictRoute)
{
var appConfig = JsonUtils.DeepCopy(mainResult.Context.AppConfig);
appConfig.CoreBasicItem.BindInterface = string.Empty;
appConfig.CoreBasicItem.SendThrough = string.Empty;
resolvedMainResult = resolvedMainResult with
{
Context = resolvedMainResult.Context with
{
AppConfig = appConfig,
},
};
}
return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult); return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult);
} }
/// <summary> /// <summary>
/// Determines whether a pre-socks context is required for <paramref name="nodeContext"/> /// Determines whether a pre-socks context is required for <paramref name="nodeContext" />
/// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed. /// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed.
/// </summary> /// </summary>
private static async Task<CoreConfigContextBuilderResult?> BuildPreSocksIfNeeded(CoreConfigContext nodeContext) private static async Task<CoreConfigContextBuilderResult?> BuildPreSocksIfNeeded(CoreConfigContext nodeContext)
{ {
@@ -157,8 +197,9 @@ public class CoreConfigContextBuilder
{ {
Context = preSocksResult.Context with Context = preSocksResult.Context with
{ {
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []], ProtectDomainList =
} [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
},
}; };
} }
@@ -166,8 +207,8 @@ public class CoreConfigContextBuilder
} }
/// <summary> /// <summary>
/// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain. /// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain.
/// Returns the effective (possibly replaced) node and the validation result. /// Returns the effective (possibly replaced) node and the validation result.
/// </summary> /// </summary>
public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context, public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context,
ProfileItem node, ProfileItem node,
@@ -202,12 +243,13 @@ public class CoreConfigContextBuilder
} }
/// <summary> /// <summary>
/// If the node's subscription defines prev/next profiles, creates a virtual /// If the node's subscription defines prev/next profiles, creates a virtual
/// <see cref="EConfigType.ProxyChain"/> node that wraps them together. /// <see cref="EConfigType.ProxyChain" /> node that wraps them together.
/// Returns <c>null</c> as the chain item when no chain is needed. /// Returns <c>null</c> as the chain item when no chain is needed.
/// Any warnings (e.g. missing prev/next profile) are returned in the validator result. /// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
/// </summary> /// </summary>
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)> BuildSubscriptionChainNodeAsync(ProfileItem node) private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)>
BuildSubscriptionChainNodeAsync(ProfileItem node)
{ {
var result = NodeValidatorResult.Empty(); var result = NodeValidatorResult.Empty();
@@ -248,7 +290,7 @@ public class CoreConfigContextBuilder
} }
// Build new proxy chain node // Build new proxy chain node
var chainNode = new ProfileItem() var chainNode = new ProfileItem
{ {
IndexId = $"inner-{Utils.GetGuid(false)}", IndexId = $"inner-{Utils.GetGuid(false)}",
ConfigType = EConfigType.ProxyChain, ConfigType = EConfigType.ProxyChain,
@@ -266,8 +308,8 @@ public class CoreConfigContextBuilder
} }
/// <summary> /// <summary>
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync"/> or /// Dispatches registration to either <see cref="RegisterGroupNodeAsync" /> or
/// <see cref="RegisterSingleNodeAsync"/> based on the node's config type. /// <see cref="RegisterSingleNodeAsync" /> based on the node's config type.
/// </summary> /// </summary>
private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node) private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node)
{ {
@@ -275,15 +317,12 @@ public class CoreConfigContextBuilder
{ {
return await RegisterGroupNodeAsync(context, node); return await RegisterGroupNodeAsync(context, node);
} }
else return RegisterSingleNodeAsync(context, node);
{
return RegisterSingleNodeAsync(context, node);
}
} }
/// <summary> /// <summary>
/// Validates a single (non-group) node and, on success, adds it to the proxy map /// Validates a single (non-group) node and, on success, adds it to the proxy map
/// and records any domain addresses that should bypass the proxy. /// and records any domain addresses that should bypass the proxy.
/// </summary> /// </summary>
private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node) private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
{ {
@@ -347,8 +386,8 @@ public class CoreConfigContextBuilder
} }
/// <summary> /// <summary>
/// Entry point for registering a group node. Initialises the visited/ancestor sets /// Entry point for registering a group node. Initialises the visited/ancestor sets
/// and delegates to <see cref="TraverseGroupNodeAsync"/>. /// and delegates to <see cref="TraverseGroupNodeAsync" />.
/// </summary> /// </summary>
private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context, private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context,
ProfileItem node) ProfileItem node)
@@ -364,9 +403,9 @@ public class CoreConfigContextBuilder
} }
/// <summary> /// <summary>
/// Recursively walks the children of a group node, registering valid leaf nodes /// Recursively walks the children of a group node, registering valid leaf nodes
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup"/> and /// and nested groups. Detects cycles via <paramref name="ancestorsGroup" /> and
/// deduplicates shared nodes via <paramref name="globalVisitedGroup"/>. /// deduplicates shared nodes via <paramref name="globalVisitedGroup" />.
/// </summary> /// </summary>
private static async Task<NodeValidatorResult> TraverseGroupNodeAsync( private static async Task<NodeValidatorResult> TraverseGroupNodeAsync(
CoreConfigContext context, CoreConfigContext context,
@@ -430,13 +469,10 @@ public class CoreConfigContextBuilder
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks)); childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
return childNodeValidatorResult; return childNodeValidatorResult;
} }
else childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
{ childNodeValidatorResult.Errors.Clear();
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
childNodeValidatorResult.Errors.Clear();
}
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList), }); node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList) });
context.AllProxiesMap[node.IndexId] = node; context.AllProxiesMap[node.IndexId] = node;
return childNodeValidatorResult; return childNodeValidatorResult;
} }
@@ -43,8 +43,8 @@ public class NodeValidator
} }
// Basic Property Validation // Basic Property Validation
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address")); v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbAddress));
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port")); v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, ResUI.TbPort));
// Network & Core Logic // Network & Core Logic
var net = item.GetNetwork(); var net = item.GetNetwork();
@@ -75,25 +75,25 @@ public class NodeValidator
{ {
case EConfigType.VMess: case EConfigType.VMess:
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password), v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
string.Format(ResUI.MsgInvalidProperty, "Password")); string.Format(ResUI.MsgInvalidProperty, ResUI.TbId));
break; break;
case EConfigType.VLESS: case EConfigType.VLESS:
v.Assert( v.Assert(
!item.Password.IsNullOrEmpty() !item.Password.IsNullOrEmpty()
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30), && (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30),
string.Format(ResUI.MsgInvalidProperty, "Password") string.Format(ResUI.MsgInvalidProperty, ResUI.TbId5)
); );
v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty), v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
string.Format(ResUI.MsgInvalidProperty, "Flow")); string.Format(ResUI.MsgInvalidProperty, ResUI.TbFlow5));
break; break;
case EConfigType.Shadowsocks: case EConfigType.Shadowsocks:
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password")); v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbId3));
v.Assert( v.Assert(
!string.IsNullOrEmpty(protocolExtra.SsMethod) && !string.IsNullOrEmpty(protocolExtra.SsMethod) &&
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod), Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
string.Format(ResUI.MsgInvalidProperty, "SsMethod")); string.Format(ResUI.MsgInvalidProperty, ResUI.TbSecurity3));
break; break;
} }
@@ -115,10 +115,11 @@ public class NodeValidator
// TLS & Security // TLS & Security
if (item.StreamSecurity == Global.StreamSecurity) if (item.StreamSecurity == Global.StreamSecurity)
{ {
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 && var isCertProvided = !item.Cert.IsNullOrEmpty();
!item.CertSha.IsNullOrEmpty()) if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0)
{ {
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate")); v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFullCertTips));
isCertProvided = false;
} }
// Check for deprecated allowInsecure property when TLS is enabled // Check for deprecated allowInsecure property when TLS is enabled
@@ -131,19 +132,19 @@ public class NodeValidator
if ((coreType == ECoreType.Xray if ((coreType == ECoreType.Xray
&& item.GetAllowInsecure() && item.GetAllowInsecure()
&& item.Cert.IsNullOrEmpty() && !isCertProvided
&& item.CertSha.IsNullOrEmpty()) && item.CertSha.IsNullOrEmpty())
|| (coreType == ECoreType.sing_box || (coreType == ECoreType.sing_box
&& item.GetAllowInsecure() && item.GetAllowInsecure()
&& item.Cert.IsNullOrEmpty())) && !isCertProvided))
{ {
v.Warning("Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks."); v.Warning(ResUI.MsgInsecureConfiguration);
} }
} }
if (item.StreamSecurity == Global.StreamSecurityReality) if (item.StreamSecurity == Global.StreamSecurityReality)
{ {
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey")); v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbPublicKey));
} }
var transport = item.GetTransportExtra(); var transport = item.GetTransportExtra();
@@ -151,7 +152,7 @@ public class NodeValidator
{ {
if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject) if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject)
{ {
v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra")); v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TransportExtra));
} }
} }
@@ -159,7 +160,7 @@ public class NodeValidator
{ {
if (JsonUtils.ParseJson(item.Finalmask) is not JsonObject) if (JsonUtils.ParseJson(item.Finalmask) is not JsonObject)
{ {
v.Error(string.Format(ResUI.MsgInvalidProperty, "Finalmask")); v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFinalmask));
} }
} }
} }
+45 -6
View File
@@ -165,7 +165,8 @@ public static class ConfigHandler
{ {
Packets = "tlshello", Packets = "tlshello",
Length = "50-100", Length = "50-100",
Interval = "10-20" Interval = "10-20",
MaxSplit = "0"
}; };
config.GlobalHotkeys ??= []; config.GlobalHotkeys ??= [];
@@ -720,12 +721,51 @@ public static class ConfigHandler
{ {
return -1; return -1;
} }
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with var protocolExtra = profileItem.GetProtocolExtra();
profileItem.SetProtocolExtra(protocolExtra with
{ {
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(), SalamanderPass = protocolExtra.SalamanderPass?.TrimEx(),
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(), HopInterval = protocolExtra.HopInterval?.TrimEx(),
}); });
if (!protocolExtra.Hy2RealmUrl.IsNullOrEmpty())
{
var realmResult = HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm);
if (!realmResult || realm is null)
{
return -1;
}
if (realm.StunList.Count == 0)
{
realm = realm with
{
StunList = Global.DefaultRealmStunList,
};
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
Hy2RealmUrl = realm.ToUri(),
});
}
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
if (isGecko)
{
var minPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
var maxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
if (minPacketSize <= 0
|| minPacketSize > maxPacketSize
|| maxPacketSize > 2048)
{
minPacketSize = 512;
maxPacketSize = 1200;
}
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
GeckoMinPacketSize = minPacketSize.ToString(),
GeckoMaxPacketSize = maxPacketSize.ToString(),
});
}
await AddServerCommon(config, profileItem, toFile); await AddServerCommon(config, profileItem, toFile);
return 0; return 0;
@@ -1450,8 +1490,7 @@ public static class ConfigHandler
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType) public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
{ {
ProfileItem? itemSocks = null; ProfileItem? itemSocks = null;
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect;
|| Utils.IsNonWindows();
if (node.ConfigType != EConfigType.Custom if (node.ConfigType != EConfigType.Custom
&& coreType != ECoreType.sing_box && coreType != ECoreType.sing_box
&& config.TunModeItem.EnableTun && config.TunModeItem.EnableTun
@@ -38,7 +38,7 @@ public static class ConnectionHandler
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
responseTime = await GetRealPingTime(webProxy, 10); responseTime = await GetRealPingTime(webProxy);
if (responseTime > 0) if (responseTime > 0)
{ {
break; break;
@@ -66,7 +66,7 @@ public static class ConnectionHandler
/// <summary> /// <summary>
/// Measures response time by sending HTTP requests through proxy. /// Measures response time by sending HTTP requests through proxy.
/// </summary> /// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout) public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9)
{ {
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1; var responseTime = -1;
@@ -77,7 +77,8 @@ public static class ConnectionHandler
using var client = new HttpClient(new SocketsHttpHandler() using var client = new HttpClient(new SocketsHttpHandler()
{ {
Proxy = webProxy, Proxy = webProxy,
UseProxy = webProxy != null UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(3)
}); });
List<int> oneTime = []; List<int> oneTime = [];
@@ -72,6 +72,11 @@ public class FmtHandler
{ {
return Hysteria2Fmt.Resolve(str, out msg); return Hysteria2Fmt.Resolve(str, out msg);
} }
else if (str.StartsWith(Global.Hysteria2RealmProtocolShare)
|| str.StartsWith(Global.Hysteria2HttpRealmProtocolShare))
{
return Hysteria2Fmt.ResolveRealm(str, out msg);
}
else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC])) else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC]))
{ {
return TuicFmt.Resolve(str, out msg); return TuicFmt.Resolve(str, out msg);
+160 -32
View File
@@ -1,3 +1,5 @@
using System.Collections.Specialized;
namespace ServiceLib.Handler.Fmt; namespace ServiceLib.Handler.Fmt;
public class Hysteria2Fmt : BaseFmt public class Hysteria2Fmt : BaseFmt
@@ -23,15 +25,7 @@ public class Hysteria2Fmt : BaseFmt
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
if (item.CertSha.IsNullOrEmpty()) ResolveHy2UriQuery(query, ref item);
{
item.CertSha = GetQueryDecoded(query, "pinSHA256");
}
item.SetProtocolExtra(item.GetProtocolExtra() with
{
Ports = GetQueryDecoded(query, "mport"),
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
});
return item; return item;
} }
@@ -42,8 +36,11 @@ public class Hysteria2Fmt : BaseFmt
{ {
return null; return null;
} }
var protocolExtraItem = item.GetProtocolExtra();
var url = string.Empty; if (!protocolExtraItem.Hy2RealmUrl.TrimEx().IsNullOrEmpty())
{
return ToUriForRealm(item);
}
var remark = string.Empty; var remark = string.Empty;
if (item.Remarks.IsNotEmpty()) if (item.Remarks.IsNotEmpty())
@@ -52,27 +49,7 @@ public class Hysteria2Fmt : BaseFmt
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
ToUriQueryLite(item, ref dicQuery); ToUriQueryLite(item, ref dicQuery);
var protocolExtraItem = item.GetProtocolExtra(); ToHy2UriQuery(item, ref dicQuery);
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
{
dicQuery.Add("obfs", "salamander");
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
}
if (!protocolExtraItem.Ports.IsNullOrEmpty())
{
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
}
if (!item.CertSha.IsNullOrEmpty())
{
var sha = item.CertSha;
var idx = sha.IndexOf(',');
if (idx > 0)
{
sha = sha[..idx];
}
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
}
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark); return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
} }
@@ -94,4 +71,155 @@ public class Hysteria2Fmt : BaseFmt
return null; return null;
} }
public static ProfileItem? ResolveRealm(string str, out string msg)
{
msg = ResUI.ConfigurationFormatIncorrect;
ProfileItem item = new()
{
ConfigType = EConfigType.Hysteria2
};
var realmStr = str["hysteria2+".Length..];
var result = HyRealm.TryParse(realmStr, out var realm);
if (!result || realm == null)
{
return null;
}
var url = Utils.TryUri(str);
if (url == null)
{
return null;
}
item.Address = realm.RendezvousHost;
item.Port = realm.RendezvousPort;
item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
var query = Utils.ParseQueryString(url.Query);
ResolveUriQuery(query, ref item);
var authPassword = GetQueryDecoded(query, "auth");
if (authPassword.IsNullOrEmpty())
{
return null;
}
item.Password = authPassword;
ResolveHy2UriQuery(query, ref item);
item.SetProtocolExtra(item.GetProtocolExtra() with
{
Hy2RealmUrl = realm.ToUri(),
});
msg = string.Empty;
return item;
}
public static string? ToUriForRealm(ProfileItem? item)
{
if (item == null)
{
return null;
}
var protocolExtraItem = item.GetProtocolExtra();
var result = HyRealm.TryParse(protocolExtraItem.Hy2RealmUrl, out var realm);
if (!result || realm == null)
{
return null;
}
var remark = string.Empty;
if (item.Remarks.IsNotEmpty())
{
remark = "#" + Utils.UrlEncode(item.Remarks);
}
var dicQuery = new Dictionary<string, string>();
ToUriQueryLite(item, ref dicQuery);
ToHy2UriQuery(item, ref dicQuery);
dicQuery.Add("auth", Utils.UrlEncode(item.Password));
var queryBuilder = new StringBuilder();
queryBuilder.Append('?');
foreach (var kv in dicQuery)
{
queryBuilder.Append(kv.Key);
queryBuilder.Append('=');
queryBuilder.Append(kv.Value);
queryBuilder.Append('&');
}
foreach (var stun in realm.StunList)
{
queryBuilder.Append("stun=");
queryBuilder.Append(Utils.UrlEncode(stun));
queryBuilder.Append('&');
}
var query = queryBuilder.ToString().TrimEnd('&');
var url = $"{Utils.UrlEncode(realm.Token)}@{GetIpv6(realm.RendezvousHost)}:{realm.RendezvousPort}";
var scheme = realm.IsHttp ? Global.Hysteria2HttpRealmProtocolShare : Global.Hysteria2RealmProtocolShare;
return $"{scheme}{url}/{realm.RealmName}{query}{remark}";
}
private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item)
{
if (item.CertSha.IsNullOrEmpty())
{
item.CertSha = GetQueryDecoded(query, "pinSHA256");
}
item.SetProtocolExtra(item.GetProtocolExtra() with
{
Ports = GetQueryDecoded(query, "mport"),
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
GeckoMinPacketSize = GetQueryDecoded(query, "minPacketSize"),
GeckoMaxPacketSize = GetQueryDecoded(query, "maxPacketSize"),
});
if (GetQueryDecoded(query, "obfs") == "gecko")
{
// Ensure the "PacketSize" parameters are present for gecko obfs.
var protocolExtraItem = item.GetProtocolExtra();
if (protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty())
{
protocolExtraItem = protocolExtraItem with
{
GeckoMinPacketSize = "512",
};
}
if (protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty())
{
protocolExtraItem = protocolExtraItem with
{
GeckoMaxPacketSize = "1200",
};
}
item.SetProtocolExtra(protocolExtraItem);
}
}
private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (!item.CertSha.IsNullOrEmpty()
&& !item.CertSha.Contains(','))
{
var sha = item.CertSha;
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
}
var protocolExtraItem = item.GetProtocolExtra();
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
{
dicQuery.Add("obfs", isGecko ? "gecko" : "salamander");
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
if (isGecko)
{
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
dicQuery.Add("minPacketSize", protocolExtraItem.GeckoMinPacketSize);
dicQuery.Add("maxPacketSize", protocolExtraItem.GeckoMaxPacketSize);
}
}
if (!protocolExtraItem.Ports.IsNullOrEmpty())
{
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
}
}
} }
+5 -1
View File
@@ -66,7 +66,9 @@ public class VmessFmt : BaseFmt
sni = item.Sni, sni = item.Sni,
alpn = item.Alpn, alpn = item.Alpn,
fp = item.Fingerprint, fp = item.Fingerprint,
insecure = item.GetAllowInsecure() ? "1" : "0" insecure = item.GetAllowInsecure() ? "1" : "0",
vcn = item.VerifyPeerCertByName,
pcs = item.CertSha
}; };
var url = JsonUtils.Serialize(vmessQRCode); var url = JsonUtils.Serialize(vmessQRCode);
@@ -141,6 +143,8 @@ public class VmessFmt : BaseFmt
item.Alpn = Utils.ToString(vmessQRCode.alpn); item.Alpn = Utils.ToString(vmessQRCode.alpn);
item.Fingerprint = Utils.ToString(vmessQRCode.fp); item.Fingerprint = Utils.ToString(vmessQRCode.fp);
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.StringTrue : string.Empty; item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.StringTrue : string.Empty;
item.VerifyPeerCertByName = Utils.ToString(vmessQRCode.vcn);
item.CertSha = Utils.ToString(vmessQRCode.pcs);
return item; return item;
} }
+22 -16
View File
@@ -14,6 +14,8 @@ public class DownloaderHelper
return null; return null;
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
Uri uri = new(url); Uri uri = new(url);
//Authorization Header //Authorization Header
var headers = new WebHeaderCollection(); var headers = new WebHeaderCollection();
@@ -27,12 +29,12 @@ public class DownloaderHelper
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
Headers = headers, Headers = headers,
UserAgent = userAgent, UserAgent = userAgent,
ConnectTimeout = timeout * 1000, ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };
await using var downloader = new Downloader.DownloadService(downloadOpt); await using var downloader = new Downloader.DownloadService(downloadOpt);
@@ -45,7 +47,9 @@ public class DownloaderHelper
}; };
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
using StreamReader reader = new(stream); using StreamReader reader = new(stream);
downloadOpt = null; downloadOpt = null;
@@ -60,15 +64,16 @@ public class DownloaderHelper
throw new ArgumentNullException(nameof(url)); throw new ArgumentNullException(nameof(url));
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
ConnectTimeout= timeout * 1000, ConnectTimeout= connectTimeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };
var lastUpdateTime = DateTime.Now; var lastUpdateTime = DateTime.Now;
@@ -116,7 +121,7 @@ public class DownloaderHelper
}; };
//progress.Report("......"); //progress.Report("......");
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
cts.CancelAfter(timeout * 1000); cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token); await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
downloadOpt = null; downloadOpt = null;
@@ -137,15 +142,16 @@ public class DownloaderHelper
File.Delete(fileName); File.Delete(fileName);
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
ConnectTimeout= timeout * 1000, ConnectTimeout= connectTimeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };
var progressPercentage = 0; var progressPercentage = 0;
+23 -13
View File
@@ -244,9 +244,26 @@ public sealed class AppManager
{ {
return []; return [];
} }
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId)) if (ids.Count <= Global.SqliteMaxBatchSize)
.ToListAsync(); {
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId))
.ToListAsync();
}
var items = new List<ProfileItem>();
for (var size = 0; size < ids.Count; size += Global.SqliteMaxBatchSize)
{
var chunk = ids.Skip(size).Take(Global.SqliteMaxBatchSize).ToList();
var chunkItems = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => chunk.Contains(it.IndexId))
.ToListAsync();
items.AddRange(chunkItems);
}
return items;
} }
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds) public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
@@ -257,18 +274,11 @@ public sealed class AppManager
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds) public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
{ {
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList(); var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (idList.Count == 0) var items = await GetProfileItemsByIndexIds(ids);
{
return [];
}
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => idList.Contains(it.IndexId))
.ToListAsync();
var itemMap = items.ToDictionary(it => it.IndexId); var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(itemMap.GetValueOrDefault) return ids.Select(itemMap.GetValueOrDefault)
.Where(item => item != null) .Where(item => item != null)
.ToList(); .ToList();
} }
+7 -16
View File
@@ -190,8 +190,8 @@ public class CertPemManager
/// <summary> /// <summary>
/// Get certificate in PEM format from a server with CA pinning validation /// Get certificate in PEM format from a server with CA pinning validation
/// </summary> /// </summary>
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, bool allowInsecure = false) List<string>? verifyPeerCertByName = null, int timeout = 4)
{ {
try try
{ {
@@ -204,8 +204,7 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [], ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
allowInsecure));
await using var ssl = new SslStream(client.GetStream(), false, callback); await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions var sslOptions = new SslClientAuthenticationOptions
@@ -240,8 +239,8 @@ public class CertPemManager
/// <summary> /// <summary>
/// Get certificate chain in PEM format from a server with CA pinning validation /// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary> /// </summary>
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, bool allowInsecure = false) List<string>? verifyPeerCertByName = null, int timeout = 4)
{ {
var pemList = new List<string>(); var pemList = new List<string>();
try try
@@ -255,8 +254,7 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [], ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
allowInsecure));
await using var ssl = new SslStream(client.GetStream(), false, callback); await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions var sslOptions = new SslClientAuthenticationOptions
@@ -299,20 +297,13 @@ public class CertPemManager
X509Certificate? certificate, X509Certificate? certificate,
X509Chain? chain, X509Chain? chain,
SslPolicyErrors sslPolicyErrors, SslPolicyErrors sslPolicyErrors,
List<string> verifyPeerCertByName, List<string> verifyPeerCertByName)
bool allowInsecure)
{ {
if (certificate == null) if (certificate == null)
{ {
return false; return false;
} }
// In insecure mode, accept any certificate so self-signed certs can be fetched.
if (allowInsecure)
{
return true;
}
// Build certificate chain // Build certificate chain
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate); var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
var certChain = chain ?? new X509Chain(); var certChain = chain ?? new X509Chain();
+71 -1
View File
@@ -90,6 +90,7 @@ public class CoreManager
} }
await CoreStart(mainContext); await CoreStart(mainContext);
await WaitForProxyPort(preContext);
await CoreStartPreService(preContext); await CoreStartPreService(preContext);
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType; AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
@@ -213,6 +214,75 @@ public class CoreManager
await _updateFunc?.Invoke(notify, msg); await _updateFunc?.Invoke(notify, msg);
} }
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
{
if (preContext is null)
{
return;
}
if (!preContext.AppConfig.TunModeItem.EnableTun)
{
return;
}
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
var rootToken = rootCts.Token;
var port = preContext.Node.Port;
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
var buf = new byte[2];
while (!rootToken.IsCancellationRequested)
{
using var tcp = new TcpClient();
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
var linkedToken = linkedCts.Token;
try
{
await tcp.ConnectAsync(Global.Loopback, port, linkedToken);
var stream = tcp.GetStream();
await stream.WriteAsync(greeting, linkedToken);
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
if (read == 2 && buf[0] == 0x05)
{
return;
}
}
catch (OperationCanceledException)
{
if (!rootToken.IsCancellationRequested)
{
continue;
}
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
return;
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
// Connection refused, proxy not ready yet, wait 50ms before retrying
try
{
await Task.Delay(50, rootToken);
}
catch (OperationCanceledException)
{
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
return;
}
}
catch
{
// Ignore other exceptions and continue
}
}
}
#endregion Private #endregion Private
#region Process #region Process
@@ -230,7 +300,7 @@ public class CoreManager
{ {
if (mayNeedSudo if (mayNeedSudo
&& _config.TunModeItem.EnableTun && _config.TunModeItem.EnableTun
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo) && (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray)
&& Utils.IsNonWindows()) && Utils.IsNonWindows())
{ {
_linuxSudo = true; _linuxSudo = true;
+1 -1
View File
@@ -56,7 +56,7 @@ public class TaskManager
{ {
//Logging.SaveLog("Execute delete expired files"); //Logging.SaveLog("Execute delete expired files");
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1)); FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1), "Test");
FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1)); FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1)); FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
@@ -249,6 +249,7 @@ public class Fragment4RayItem
public string? Packets { get; set; } public string? Packets { get; set; }
public string? Length { get; set; } public string? Length { get; set; }
public string? Interval { get; set; } public string? Interval { get; set; }
public string? MaxSplit { get; set; }
} }
[Serializable] [Serializable]
@@ -248,6 +248,16 @@ public class HyObfs4Sbox
{ {
public string? type { get; set; } public string? type { get; set; }
public string? password { get; set; } public string? password { get; set; }
public int? min_packet_size { get; set; }
public int? max_packet_size { get; set; }
}
public class HyRealm4Sbox
{
public string? server_url { get; set; }
public string? token { get; set; }
public string? realm_id { get; set; }
public List<string>? stun_servers { get; set; }
} }
public class Server4Sbox : BaseServer4Sbox public class Server4Sbox : BaseServer4Sbox
@@ -334,6 +344,7 @@ public abstract class DialFields4Sbox
public Multiplex4Sbox? multiplex { get; set; } public Multiplex4Sbox? multiplex { get; set; }
public Transport4Sbox? transport { get; set; } public Transport4Sbox? transport { get; set; }
public HyObfs4Sbox? obfs { get; set; } public HyObfs4Sbox? obfs { get; set; }
public HyRealm4Sbox? realm { get; set; }
} }
public abstract class BaseServer4Sbox : DialFields4Sbox public abstract class BaseServer4Sbox : DialFields4Sbox
@@ -20,7 +20,7 @@ public class Stats4Ray
public class Metrics4Ray public class Metrics4Ray
{ {
public string tag { get; set; } public string listen { get; set; }
} }
public class Policy4Ray public class Policy4Ray
@@ -289,16 +289,7 @@ public class BalancersStrategySettings4Ray
{ {
public int? expected { get; set; } public int? expected { get; set; }
public string? maxRTT { get; set; } public string? maxRTT { get; set; }
public float? tolerance { get; set; } public double? tolerance { get; set; }
public List<string>? baselines { get; set; }
public List<BalancersStrategySettingsCosts4Ray>? costs { get; set; }
}
public class BalancersStrategySettingsCosts4Ray
{
public bool? regexp { get; set; }
public string? match { get; set; }
public float? value { get; set; }
} }
public class Observatory4Ray public class Observatory4Ray
@@ -507,11 +498,16 @@ public class MaskSettings4Ray
public string? password { get; set; } public string? password { get; set; }
public string? url { get; set; }
public List<string>? stunServers { get; set; }
public string? packetSize { get; set; }
// fragment // fragment
public string? packets { get; set; } public string? packets { get; set; }
public string? length { get; set; } public string? length { get; set; }
public string? delay { get; set; } public string? delay { get; set; }
public int? maxSplit { get; set; }
// noise // noise
public int? reset { get; set; } public int? reset { get; set; }
+115
View File
@@ -0,0 +1,115 @@
namespace ServiceLib.Models.Dto;
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
// realm+http://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
// example:
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
public record HyRealm(
bool IsHttp,
string Token,
string RendezvousHost,
int RendezvousPort,
string RealmName,
List<string> StunList
)
{
public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost;
/// <summary>
/// sing-box realm.server_url requires a scheme (https:// or http://).
/// </summary>
public string ToServerUrl()
{
var scheme = IsHttp ? "http" : "https";
return $"{scheme}://{RendezvousHostPort}";
}
public static bool TryParse(string? str, out HyRealm? realm)
{
realm = null;
if (str == null)
{
return false;
}
try
{
var isHttp = str.StartsWith("realm+http://");
var prefix = isHttp ? "realm+http://" : "realm://";
if (!str.StartsWith(prefix))
{
return false;
}
var uri = new Uri(str);
var token = Utils.UrlDecode(uri.UserInfo);
var rendezvousHost = uri.Host;
var rendezvousPort = uri.IsDefaultPort ? (isHttp ? 80 : 443) : uri.Port;
var realmName = uri.AbsolutePath.TrimStart('/');
var stunList = new List<string>();
var query = uri.Query;
if (!query.IsNullOrEmpty())
{
if (query.StartsWith('?'))
{
query = query.Substring(1);
}
var pairs = query.Split('&', StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs)
{
var idx = pair.IndexOf('=');
if (idx <= 0)
{
continue;
}
var key = pair.Substring(0, idx);
if (!key.Equals("stun", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var val = pair.Substring(idx + 1);
stunList.Add(Utils.UrlDecode(val));
}
}
realm = new HyRealm(isHttp, token, rendezvousHost, rendezvousPort, realmName, stunList);
return true;
}
catch
{
return false;
}
}
public string ToUri()
{
var prefix = IsHttp ? "realm+http://" : "realm://";
var uriBuilder = new UriBuilder
{
Scheme = IsHttp ? "http" : "realm",
Host = RendezvousHost,
Port = RendezvousPort,
Path = "/" + RealmName,
UserName = Utils.UrlEncode(Token),
};
if (StunList is { Count: > 0 })
{
// var query = string.Join('&', StunList.Select(s => "stun=" + Utils.UrlEncode(s)));
// NOTE: maybe we don't need to encode the stun host:port, since it should be a valid URI component already, and encoding will make it unreadable
var query = string.Join('&', StunList.Select(s => "stun=" + s));
uriBuilder.Query = query;
}
return prefix + uriBuilder.Uri.GetComponents(
UriComponents.UserInfo | UriComponents.HostAndPort | UriComponents.PathAndQuery, UriFormat.UriEscaped);
}
public string ToUriForFinalmask()
{
var prefix = IsHttp ? "realm+http://" : "realm://";
var uriBuilder = new StringBuilder();
uriBuilder.Append(prefix);
uriBuilder.Append(Utils.UrlEncode(Token));
uriBuilder.Append('@');
uriBuilder.Append(RendezvousHostPort);
uriBuilder.Append('/');
uriBuilder.Append(Utils.UrlEncode(RealmName));
return uriBuilder.ToString();
}
}
@@ -40,4 +40,8 @@ public class VmessQRCode
public string fp { get; set; } = string.Empty; public string fp { get; set; } = string.Empty;
public string insecure { get; set; } = string.Empty; public string insecure { get; set; } = string.Empty;
public string vcn { get; set; } = string.Empty;
public string pcs { get; set; } = string.Empty;
} }
@@ -31,6 +31,12 @@ public record ProtocolExtraItem
public int? DownMbps { get; init; } public int? DownMbps { get; init; }
public string? Ports { get; init; } public string? Ports { get; init; }
public string? HopInterval { get; init; } public string? HopInterval { get; init; }
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
// example:
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
public string? Hy2RealmUrl { get; init; }
public string? GeckoMinPacketSize { get; init; }
public string? GeckoMaxPacketSize { get; init; }
// naiveproxy // naiveproxy
public int? InsecureConcurrency { get; init; } public int? InsecureConcurrency { get; init; }
+163 -28
View File
@@ -222,15 +222,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。
/// </summary>
public static string FillCorrectSendThroughIPv4 {
get {
return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Please enter the correct port format. 的本地化字符串。 /// 查找类似 Please enter the correct port format. 的本地化字符串。
/// </summary> /// </summary>
@@ -240,6 +231,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Invalid range format. Use &apos;from-to&apos; (e.g., 50-100). 的本地化字符串。
/// </summary>
public static string FillFragmentParameterError {
get {
return ResourceManager.GetString("FillFragmentParameterError", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Please enter the local listening port. 的本地化字符串。 /// 查找类似 Please enter the local listening port. 的本地化字符串。
/// </summary> /// </summary>
@@ -321,6 +321,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Invalid Realm URL. 的本地化字符串。
/// </summary>
public static string InvalidHy2RealmUrl {
get {
return ResourceManager.GetString("InvalidHy2RealmUrl", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Invalid address (URL) 的本地化字符串。 /// 查找类似 Invalid address (URL) 的本地化字符串。
/// </summary> /// </summary>
@@ -2067,6 +2076,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks. 的本地化字符串。
/// </summary>
public static string MsgInsecureConfiguration {
get {
return ResourceManager.GetString("MsgInsecureConfiguration", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。 /// 查找类似 The {0} property is invalid, please check 的本地化字符串。
/// </summary> /// </summary>
@@ -2670,24 +2688,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Allow insecure cert fetch (self-signed) 的本地化字符串。
/// </summary>
public static string TbAllowInsecureCertFetch {
get {
return ResourceManager.GetString("TbAllowInsecureCertFetch", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only for fetching self-signed certificates. This may expose you to MITM risks. 的本地化字符串。
/// </summary>
public static string TbAllowInsecureCertFetchTips {
get {
return ResourceManager.GetString("TbAllowInsecureCertFetchTips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 ALPN 的本地化字符串。 /// 查找类似 ALPN 的本地化字符串。
/// </summary> /// </summary>
@@ -3051,6 +3051,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 DNS via Bridge 的本地化字符串。
/// </summary>
public static string TbEnabletDnsViaProxy {
get {
return ResourceManager.GetString("TbEnabletDnsViaProxy", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Enable Tun 的本地化字符串。 /// 查找类似 Enable Tun 的本地化字符串。
/// </summary> /// </summary>
@@ -3159,6 +3168,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
/// </summary>
public static string TbGeckoPacketSize {
get {
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Global Hotkey Settings 的本地化字符串。 /// 查找类似 Global Hotkey Settings 的本地化字符串。
/// </summary> /// </summary>
@@ -3222,6 +3240,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Realm URL 的本地化字符串。
/// </summary>
public static string TbHy2RealmUrl {
get {
return ResourceManager.GetString("TbHy2RealmUrl", resourceCulture);
}
}
/// <summary>
/// 查找类似 Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port] 的本地化字符串。
/// </summary>
public static string TbHy2RealmUrlTip {
get {
return ResourceManager.GetString("TbHy2RealmUrlTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 ICMP routing policy 的本地化字符串。 /// 查找类似 ICMP routing policy 的本地化字符串。
/// </summary> /// </summary>
@@ -3303,6 +3339,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 If enabled, use sing-box TUN; otherwise, use xray TUN. 的本地化字符串。
/// </summary>
public static string TbLegacyProtectTip {
get {
return ResourceManager.GetString("TbLegacyProtectTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Address (IPv4, IPv6) 的本地化字符串。 /// 查找类似 Address (IPv4, IPv6) 的本地化字符串。
/// </summary> /// </summary>
@@ -3826,7 +3871,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode 的本地化字符串。 /// 查找类似 For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled. 的本地化字符串。
/// </summary> /// </summary>
public static string TbSettingsBindInterfaceTip { public static string TbSettingsBindInterfaceTip {
get { get {
@@ -4113,6 +4158,96 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Fallback Delay 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentFallbackDelay {
get {
return ResourceManager.GetString("TbSettingsFragmentFallbackDelay", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentFallbackDelayTip {
get {
return ResourceManager.GetString("TbSettingsFragmentFallbackDelayTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Interval 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentInterval {
get {
return ResourceManager.GetString("TbSettingsFragmentInterval", resourceCulture);
}
}
/// <summary>
/// 查找类似 Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be &lt;= second. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentIntervalTip {
get {
return ResourceManager.GetString("TbSettingsFragmentIntervalTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Length 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentLength {
get {
return ResourceManager.GetString("TbSettingsFragmentLength", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment size range in bytes (e.g., 50-100). First value must be &lt;= second. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentLengthTip {
get {
return ResourceManager.GetString("TbSettingsFragmentLengthTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Max Split 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentMaxSplit {
get {
return ResourceManager.GetString("TbSettingsFragmentMaxSplit", resourceCulture);
}
}
/// <summary>
/// 查找类似 Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default. 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentMaxSplitTip {
get {
return ResourceManager.GetString("TbSettingsFragmentMaxSplitTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fragment Packets 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentPackets {
get {
return ResourceManager.GetString("TbSettingsFragmentPackets", resourceCulture);
}
}
/// <summary>
/// 查找类似 Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets) 的本地化字符串。
/// </summary>
public static string TbSettingsFragmentPacketsTip {
get {
return ResourceManager.GetString("TbSettingsFragmentPacketsTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Geo files source (optional) 的本地化字符串。 /// 查找类似 Geo files source (optional) 的本地化字符串。
/// </summary> /// </summary>
+33 -9
View File
@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>فعال کردن فرگمنت</value> <value>فعال کردن فرگمنت</value>
</data> </data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>پکت‌های فرگمنت</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>پکت‌های قابل فرگمنت: tlshello (TLS ClientHello) یا 1-1 تا 1-5 (N اول پکت TCP)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>طول فرگمنت</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>محدوده اندازه فرگمنت بایت (مثلا 50-100). اول ≤ دوم. خالی = پیش‌فرض.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>فاصله فرگمنت</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>تأخیر بین فرگمنت‌ها میلی‌ثانیه (مثلا 10-20). بازه 1-100. اول ≤ دوم. خالی = پیش‌فرض.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>حداکثر تقسیم</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>مکسیمم تعداد تقسیم (0 = نامحدود، 0-10000). فقط Xray-core. خالی = پیش‌فرض.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>تأخیر فول‌بک</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>تأخیر فول‌بک فرگمنت TLS. فقط sing-box. خالی = پیش‌فرض.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>فرمت محدوده نامعتبر. از 'from-to' استفاده کنید (مثلا 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value> <value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value>
</data> </data>
@@ -1698,12 +1731,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value> <value>Test Configurations UDP Delay</value>
</data> </data>
@@ -1716,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value> <value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value> <value>Bind Interface</value>
</data> </data>
+33 -9
View File
@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Activer le fragmentation (Fragment)</value> <value>Activer le fragmentation (Fragment)</value>
</data> </data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Paquets fragmentés</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Paquets à fragmenter : tlshello (TLS ClientHello) ou 1-1 à 1-5 (premiers N paquets TCP)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Longueur fragment</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Plage de taille fragment (ex: 50-100). Première valeur ≤ seconde. Vide = défaut.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Intervalle fragment</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Délai entre fragments en ms (ex: 10-20). Plage 1-100. Première valeur ≤ seconde. Vide = défaut.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Max Split</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Nb max de splits (0 = illimité, 0-10000). Xray-core seulement. Vide = défaut.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Délai fallback</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Délai fallback pour fragment TLS. sing-box seulement. Vide = défaut.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Format de plage invalide. Utilisez 'from-to' (ex: 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Activer le fichier de cache de sing-box (fichiers règles)</value> <value>Activer le fichier de cache de sing-box (fichiers règles)</value>
</data> </data>
@@ -1695,21 +1728,12 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Pour obtenir des certificats auto-signés uniquement. Risque MITM.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve"> <data name="TbSettingsSendThrough" xml:space="preserve">
<value>Adresse de sortie locale (SendThrough)</value> <value>Adresse de sortie locale (SendThrough)</value>
</data> </data>
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value> <value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Veuillez saisir l’adresse IPv4 correcte de SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Lier l'interface</value> <value>Lier l'interface</value>
</data> </data>
+33 -9
View File
@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Fragment engedélyezése</value> <value>Fragment engedélyezése</value>
</data> </data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Fragmentált csomagok</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Fragmentálandó csomagok: tlshello (TLS ClientHello) vagy 1-1 – 1-5 (az első N TCP csomag)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Fragment hossz</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Fragment méret tartománya bájtokban (pl. 50-100). Az első érték ≤ második. Üres = alapértelmezett.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Fragment intervallum</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Késleltetés a fragmented csomagok között ms-ben (pl. 10-20). Tartomány 1-100. Az első érték ≤ második. Üres = alapértelmezett.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Max darabolás</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Maximális darabolások száma (0 = korlátlan, 0-10000). Csak Xray-core. Üres = alapértelmezett.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Fallback késleltetés</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>TLS fragment fallback késleltetés. Csak sing-box. Üres = alapértelmezett.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Érvénytelen tartomány formátum. Használja 'from-to' (pl. 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value> <value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value>
</data> </data>
@@ -1698,12 +1731,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value> <value>Test Configurations UDP Delay</value>
</data> </data>
@@ -1716,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value> <value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value> <value>Bind Interface</value>
</data> </data>
File diff suppressed because it is too large Load Diff
+55 -10
View File
@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Enable fragment</value> <value>Enable fragment</value>
</data> </data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Fragment Packets</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Fragment Length</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Fragment size range in bytes (e.g., 50-100). First value must be &lt;= second. Empty = default.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Fragment Interval</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be &lt;= second. Empty = default.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Max Split</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Fallback Delay</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Invalid range format. Use 'from-to' (e.g., 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Enable cache file for sing-box (ruleset files)</value> <value>Enable cache file for sing-box (ruleset files)</value>
</data> </data>
@@ -1701,12 +1734,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value> <value>Test Configurations UDP Delay</value>
</data> </data>
@@ -1719,14 +1746,11 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value> <value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Bind Interface</value> <value>Bind Interface</value>
</data> </data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve"> <data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value> <value>For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled.</value>
</data> </data>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value> <value>PreSharedKey</value>
@@ -1770,4 +1794,25 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbEnableFinalFragmentTip" xml:space="preserve"> <data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value> <value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
</data> </data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS via Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Invalid Realm URL.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko Packet Size (min/max)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>If enabled, use sing-box TUN; otherwise, use xray TUN.</value>
</data>
</root> </root>
+34 -10
View File
@@ -1104,6 +1104,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Включить фрагментацию (Fragment)</value> <value>Включить фрагментацию (Fragment)</value>
</data> </data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>Пакеты для фрагментации</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>Пакеты для фрагментации: tlshello (TLS ClientHello) или 1-1 — 1-5 (первые N TCP-пакетов)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>Длина фрагмента</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Интервал фрагментации</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Макс. разделение</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Резервная задержка</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100).</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Включить файл кэша для sing-box (файлы наборов правил)</value> <value>Включить файл кэша для sing-box (файлы наборов правил)</value>
</data> </data>
@@ -1698,12 +1731,6 @@
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>Дополнительные параметры XHTTP (Extra)</value> <value>Дополнительные параметры XHTTP (Extra)</value>
</data> </data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>Разрешить небезопасную загрузку сертификата (самоподписанного)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Только для загрузки самоподписанных сертификатов. Это может подвергнуть вас риску атаки «человек посередине» (MITM).</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve"> <data name="menuUdpTestServer" xml:space="preserve">
<value>Тест UDP-задержки конфигураций</value> <value>Тест UDP-задержки конфигураций</value>
</data> </data>
@@ -1716,14 +1743,11 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value> <value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>Привязать интерфейс</value> <value>Привязать интерфейс</value>
</data> </data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve"> <data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value> <value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value>
</data> </data>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>Общий ключ (PSK)</value> <value>Общий ключ (PSK)</value>
+55 -10
View File
@@ -1030,7 +1030,7 @@
<value>添加 [Hysteria2]</value> <value>添加 [Hysteria2]</value>
</data> </data>
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve"> <data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
<value>Hysteria 最大带宽 (Up/Dw)</value> <value>Hysteria 最大带宽 (上传/下载)</value>
</data> </data>
<data name="TbSettingsUseSystemHosts" xml:space="preserve"> <data name="TbSettingsUseSystemHosts" xml:space="preserve">
<value>使用系统 hosts</value> <value>使用系统 hosts</value>
@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>启用分片 (Fragment)</value> <value>启用分片 (Fragment)</value>
</data> </data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>分片包类型</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>要分片的包类型: tlshello (TLS ClientHello) 或 1-1 到 1-5 (前 N 个 TCP 包)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>分片长度</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>分片大小范围(字节), 如 50-100. 第一个值必须 &lt;= 第二个值. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>分片间隔</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>分片间延迟(毫秒), 如 10-20. 范围 1-100. 第一个值必须 &lt;= 第二个值. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>最大分片数</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>每个包最大分片数 (0-10000, 0=无限制). 仅 Xray. 留空使用默认值.</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>回退延迟</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>ACK 检测不可用时的回退延迟 (如 500ms). 仅 sing-box. 留空使用默认值.</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>无效的范围格式。请使用 'from-to' (如 50-100)。</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>启用 sing-box (规则集文件) 的缓存文件</value> <value>启用 sing-box (规则集文件) 的缓存文件</value>
</data> </data>
@@ -1695,12 +1728,6 @@
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>允许不安全获取证书(自签名)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>仅用于抓取自签证书,存在中间人风险。</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve"> <data name="menuUdpTestServer" xml:space="preserve">
<value>测试 UDP 延迟 (多选)</value> <value>测试 UDP 延迟 (多选)</value>
</data> </data>
@@ -1713,9 +1740,6 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>用于多网口环境,请填写本机 IPv4 地址</value> <value>用于多网口环境,请填写本机 IPv4 地址</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>请填写正确的 SendThrough IPv4 地址。</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>绑定网口</value> <value>绑定网口</value>
</data> </data>
@@ -1767,4 +1791,25 @@
<data name="TbEnableFinalFragmentTip" xml:space="preserve"> <data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。</value> <value>将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。</value>
</data> </data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS 通过 Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>检测到不安全配置:AllowInsecure 已启用,但未提供证书。这可能会导致中间人攻击。</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Realm URL 不正确。</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>格式:realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko 包大小 (最小/最大)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>启用则使用 sing-box TUN ,否则使用 xray TUN</value>
</data>
</root> </root>
+39 -9
View File
@@ -1101,6 +1101,39 @@
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>啟用分片(Fragment)</value> <value>啟用分片(Fragment)</value>
</data> </data>
<data name="TbSettingsFragmentPackets" xml:space="preserve">
<value>分片封包類型</value>
</data>
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
<value>要分片的封包類型:tlshello (TLS ClientHello) 或 1-1 到 1-5 (前 N 個 TCP 封包)</value>
</data>
<data name="TbSettingsFragmentLength" xml:space="preserve">
<value>分片長度</value>
</data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>分片大小範圍(位元組),如 50-100。第一個值必須 &lt;= 第二個值。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>分片間隔</value>
</data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>分片間延遲(毫秒),如 10-20。範圍 1-100。第一個值必須 &lt;= 第二個值。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>最大分片數</value>
</data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>每個封包最大分片數 (0-10000, 0=無限制)。僅 Xray。留空使用預設值。</value>
</data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>回退延遲</value>
</data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>ACK 偵測不可用時的回退延遲 (如 500ms)。僅 sing-box。留空使用預設值。</value>
</data>
<data name="FillFragmentParameterError" xml:space="preserve">
<value>無效的範圍格式。請使用 'from-to' (如 50-100)。</value>
</data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>啟用 sing-box(規則集檔案)的快取檔案</value> <value>啟用 sing-box(規則集檔案)的快取檔案</value>
</data> </data>
@@ -1695,12 +1728,6 @@
<data name="TransportExtra" xml:space="preserve"> <data name="TransportExtra" xml:space="preserve">
<value>XHTTP Extra</value> <value>XHTTP Extra</value>
</data> </data>
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
<value>允許不安全獲取證書(自簽名)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>僅用於抓取自簽證書,存在中間人風險。</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve"> <data name="menuUdpTestServer" xml:space="preserve">
<value>測試 UDP 延遲(多選)</value> <value>測試 UDP 延遲(多選)</value>
</data> </data>
@@ -1713,9 +1740,6 @@
<data name="TbSettingsSendThroughTip" xml:space="preserve"> <data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>適用於多網路介面環境,請填寫本機 IPv4 位址</value> <value>適用於多網路介面環境,請填寫本機 IPv4 位址</value>
</data> </data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>請填寫正確的 SendThrough IPv4 位址。</value>
</data>
<data name="TbSettingsBindInterface" xml:space="preserve"> <data name="TbSettingsBindInterface" xml:space="preserve">
<value>綁定網路介面</value> <value>綁定網路介面</value>
</data> </data>
@@ -1767,4 +1791,10 @@
<data name="TbEnableFinalFragmentTip" xml:space="preserve"> <data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>將封包末端拆分為更小的片段進行傳送,可能會影響吞吐量與延遲</value> <value>將封包末端拆分為更小的片段進行傳送,可能會影響吞吐量與延遲</value>
</data> </data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS 透過 Bridge</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value>
</data>
</root> </root>
+1 -9
View File
@@ -17,14 +17,6 @@
], ],
"routing": { "routing": {
"domainStrategy": "IPIfNonMatch", "domainStrategy": "IPIfNonMatch",
"rules": [ "rules": []
{
"inboundTag": [
"api"
],
"outboundTag": "api",
"type": "field"
}
]
} }
} }
+3
View File
@@ -67,6 +67,9 @@
<EmbeddedResource Update="Resx\ResUI.hu.resx"> <EmbeddedResource Update="Resx\ResUI.hu.resx">
<Generator>PublicResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.id.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.resx"> <EmbeddedResource Update="Resx\ResUI.resx">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<LastGenOutput>ResUI.Designer.cs</LastGenOutput> <LastGenOutput>ResUI.Designer.cs</LastGenOutput>
@@ -66,10 +66,6 @@ public partial class CoreConfigSingboxService
{ {
return; return;
} }
if (!(context.IsTunEnabled || context.IsWindows))
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? []) foreach (var outbound in _coreConfig.outbounds ?? [])
{ {
outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null; outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
@@ -83,6 +79,7 @@ public partial class CoreConfigSingboxService
{ {
return; return;
} }
foreach (var outbound in _coreConfig.outbounds ?? []) foreach (var outbound in _coreConfig.outbounds ?? [])
{ {
outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null; outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;
@@ -217,11 +217,17 @@ public partial class CoreConfigSingboxService
if (!protocolExtra.SalamanderPass.IsNullOrEmpty()) if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
{ {
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
outbound.obfs = new() outbound.obfs = new()
{ {
type = "salamander", type = isGecko ? "gecko" : "salamander",
password = protocolExtra.SalamanderPass.TrimEx(), password = protocolExtra.SalamanderPass.TrimEx(),
}; };
if (isGecko)
{
outbound.obfs.min_packet_size = protocolExtra.GeckoMinPacketSize.ToInt();
outbound.obfs.max_packet_size = protocolExtra.GeckoMaxPacketSize.ToInt();
}
} }
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0 int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
? su ? su
@@ -264,6 +270,22 @@ public partial class CoreConfigSingboxService
} }
} }
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
&& realm is not null)
{
var realm4Sbox = new HyRealm4Sbox()
{
server_url = realm.ToServerUrl(),
token = realm.Token,
realm_id = realm.RealmName,
stun_servers = realm.StunList?.Count > 0 ? realm.StunList : null,
};
outbound.realm = realm4Sbox;
outbound.server = null;
outbound.server_port = null;
outbound.server_ports = null;
}
break; break;
} }
case EConfigType.TUIC: case EConfigType.TUIC:
@@ -89,22 +89,25 @@ public partial class CoreConfigV2rayService
EMultipleLoad.RoundRobin => "roundRobin", EMultipleLoad.RoundRobin => "roundRobin",
EMultipleLoad.LeastPing => "leastPing", EMultipleLoad.LeastPing => "leastPing",
EMultipleLoad.LeastLoad => "leastLoad", EMultipleLoad.LeastLoad => "leastLoad",
EMultipleLoad.Fallback => "leastLoad",
_ => "roundRobin", _ => "roundRobin",
}; };
var balancerTag = $"{selector}{Global.BalancerTagSuffix}"; var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
var balancer = new BalancersItem4Ray var balancer = new BalancersItem4Ray
{ {
selector = [selector], selector = [selector],
strategy = new() strategy = strategyType == "leastLoad" ? new()
{ {
type = strategyType, type = strategyType,
settings = new() settings = new()
{ {
expected = 1, expected = 1,
tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null,
maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null,
}, },
}, } : null,
tag = balancerTag, tag = balancerTag,
fallbackTag = multipleLoad == EMultipleLoad.Fallback ? Global.DirectTag : null, fallbackTag = _coreConfig.outbounds?.FirstOrDefault(o => o.tag.StartsWith(selector))?.tag,
}; };
_coreConfig.routing.balancers ??= []; _coreConfig.routing.balancers ??= [];
_coreConfig.routing.balancers.Add(balancer); _coreConfig.routing.balancers.Add(balancer);
@@ -138,10 +138,6 @@ public partial class CoreConfigV2rayService
{ {
return; return;
} }
if (!(context.IsTunEnabled || context.IsWindows))
{
return;
}
foreach (var outbound in _coreConfig.outbounds ?? []) foreach (var outbound in _coreConfig.outbounds ?? [])
{ {
if (!ShouldBindNet(outbound)) if (!ShouldBindNet(outbound))
@@ -176,6 +172,7 @@ public partial class CoreConfigV2rayService
{ {
return; return;
} }
foreach (var outbound in _coreConfig.outbounds ?? []) foreach (var outbound in _coreConfig.outbounds ?? [])
{ {
outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null; outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null;
@@ -607,16 +607,32 @@ public partial class CoreConfigV2rayService
quicParams.congestion = "bbr"; quicParams.congestion = "bbr";
} }
hy2Finalmask.quicParams = quicParams; hy2Finalmask.quicParams = quicParams;
hy2Finalmask.udp ??= [];
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
&& realm is not null)
{
hy2Finalmask.udp.Add(new Mask4Ray
{
type = "realm",
settings = new MaskSettings4Ray { url = realm.ToUriForFinalmask(), stunServers = realm.StunList },
});
}
if (!protocolExtra.SalamanderPass.IsNullOrEmpty()) if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
{ {
hy2Finalmask.udp = var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
[ var salamanderSettings = new MaskSettings4Ray
new Mask4Ray {
{ password = protocolExtra.SalamanderPass.TrimEx(),
type = "salamander", };
settings = new MaskSettings4Ray { password = protocolExtra.SalamanderPass.TrimEx(), } if (isGecko)
} {
]; salamanderSettings.packetSize = $"{protocolExtra.GeckoMinPacketSize}-{protocolExtra.GeckoMaxPacketSize}";
}
hy2Finalmask.udp.Add(new Mask4Ray
{
type = "salamander",
settings = salamanderSettings,
});
} }
streamSettings.hysteriaSettings = new() streamSettings.hysteriaSettings = new()
{ {
@@ -896,9 +912,17 @@ public partial class CoreConfigV2rayService
private (Mask4Ray tcpFragment, Mask4Ray udpNoise) BuildFragmentsMasks() private (Mask4Ray tcpFragment, Mask4Ray udpNoise) BuildFragmentsMasks()
{ {
var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello"; var configPackets = _config.Fragment4RayItem?.Packets.NullIfEmpty() ?? "tlshello";
var configLength = _config.Fragment4RayItem?.Length ?? "50-100"; var configLength = _config.Fragment4RayItem?.Length.NullIfEmpty() ?? "50-100";
var configDelay = _config.Fragment4RayItem?.Interval ?? "10-20"; var configDelay = _config.Fragment4RayItem?.Interval.NullIfEmpty() ?? "10-20";
var configMaxSplit = _config.Fragment4RayItem?.MaxSplit.NullIfEmpty() ?? "0";
var maxSplit = 0;
var parts = configMaxSplit.Split('-');
if (parts.Length > 0 && int.TryParse(parts[0], out var ms))
{
maxSplit = ms;
}
var fragmentMask = new Mask4Ray var fragmentMask = new Mask4Ray
{ {
@@ -908,6 +932,7 @@ public partial class CoreConfigV2rayService
packets = configPackets, packets = configPackets,
length = configLength, length = configLength,
delay = configDelay, delay = configDelay,
maxSplit = maxSplit,
} }
}; };
var noiseMask = new Mask4Ray var noiseMask = new Mask4Ray
@@ -6,45 +6,19 @@ public partial class CoreConfigV2rayService
{ {
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{ {
const string tag = nameof(EInboundProtocol.api); Metrics4Ray metricsObj = new();
Metrics4Ray apiObj = new();
Policy4Ray policyObj = new(); Policy4Ray policyObj = new();
SystemPolicy4Ray policySystemSetting = new(); SystemPolicy4Ray policySystemSetting = new();
_coreConfig.stats = new Stats4Ray(); _coreConfig.stats = new Stats4Ray();
apiObj.tag = tag; metricsObj.listen = $"{Global.Loopback}:{AppManager.Instance.StatePort}";
_coreConfig.metrics = apiObj; _coreConfig.metrics = metricsObj;
policySystemSetting.statsOutboundDownlink = true; policySystemSetting.statsOutboundDownlink = true;
policySystemSetting.statsOutboundUplink = true; policySystemSetting.statsOutboundUplink = true;
policyObj.system = policySystemSetting; policyObj.system = policySystemSetting;
_coreConfig.policy = policyObj; _coreConfig.policy = policyObj;
if (!_coreConfig.inbounds.Exists(item => item.tag == tag))
{
Inbounds4Ray apiInbound = new();
Inboundsettings4Ray apiInboundSettings = new();
apiInbound.tag = tag;
apiInbound.listen = Global.Loopback;
apiInbound.port = AppManager.Instance.StatePort;
apiInbound.protocol = Global.InboundAPIProtocol;
apiInboundSettings.address = Global.Loopback;
apiInbound.settings = apiInboundSettings;
_coreConfig.inbounds.Add(apiInbound);
}
if (!_coreConfig.routing.rules.Exists(item => item.outboundTag == tag))
{
RulesItem4Ray apiRoutingRule = new()
{
inboundTag = new List<string> { tag },
outboundTag = tag,
type = "field"
};
_coreConfig.routing.rules.Add(apiRoutingRule);
}
} }
} }
} }
+17 -7
View File
@@ -109,9 +109,10 @@ public class DownloadService
/// </summary> /// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent) public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
{ {
var timeout = 15;
try try
{ {
var result1 = await DownloadStringAsync(url, webProxy, userAgent, 15); var result1 = await DownloadStringAsync(url, webProxy, userAgent, timeout);
if (result1.IsNotEmpty()) if (result1.IsNotEmpty())
{ {
return result1; return result1;
@@ -129,7 +130,7 @@ public class DownloadService
try try
{ {
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, 15); var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, timeout);
if (result2.IsNotEmpty()) if (result2.IsNotEmpty())
{ {
return result2; return result2;
@@ -155,11 +156,18 @@ public class DownloadService
{ {
try try
{ {
var client = new HttpClient(new SocketsHttpHandler() var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var handler = new SocketsHttpHandler
{ {
Proxy = webProxy, Proxy = webProxy,
UseProxy = webProxy != null UseProxy = webProxy != null,
}); ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
};
using var client = new HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
if (userAgent.IsNullOrEmpty()) if (userAgent.IsNullOrEmpty())
{ {
@@ -175,8 +183,9 @@ public class DownloadService
} }
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
var result = await client.GetStringAsync(url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); cts.CancelAfter(TimeSpan.FromSeconds(timeout));
return result;
return await client.GetStringAsync(url, cts.Token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -187,6 +196,7 @@ public class DownloadService
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
} }
} }
return null; return null;
} }
+22 -3
View File
@@ -46,7 +46,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
switch (actionType) switch (actionType)
{ {
case ESpeedActionType.Tcping: case ESpeedActionType.Tcping:
await RunTcpingAsync(lstSelected); await RunTcpingAsync(lstSelected, exitLoopKey);
break; break;
case ESpeedActionType.Realping: case ESpeedActionType.Realping:
@@ -135,16 +135,28 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return lstSelected; return lstSelected;
} }
private async Task RunTcpingAsync(List<ServerTestItem> selecteds) private async Task RunTcpingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
{ {
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize); var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
var lstBatch = GetTestBatchItem(selecteds, pageSize); var lstBatch = GetTestBatchItem(selecteds, pageSize);
foreach (var lst in lstBatch) foreach (var lst in lstBatch)
{ {
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
List<Task> tasks = []; List<Task> tasks = [];
foreach (var it in lst) foreach (var it in lst)
{ {
if (ShouldStopTest(exitLoopKey))
{
return;
}
tasks.Add(Task.Run(async () => tasks.Add(Task.Run(async () =>
{ {
try try
@@ -160,7 +172,14 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
})); }));
} }
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
if (ShouldStopTest(exitLoopKey))
{
return;
}
await Task.Delay(_delayInterval); await Task.Delay(_delayInterval);
} }
} }
@@ -401,7 +420,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<int> DoRealPing(ServerTestItem it) private async Task<int> DoRealPing(ServerTestItem it)
{ {
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, 10); var responseTime = await ConnectionHandler.GetRealPingTime(webProxy);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
@@ -23,9 +23,6 @@ public class AddServerViewModel : MyReactiveObject
[Reactive] [Reactive]
public string CertSha { get; set; } public string CertSha { get; set; }
[Reactive]
public bool AllowInsecureCertFetch { get; set; }
[Reactive] [Reactive]
public string SalamanderPass { get; set; } public string SalamanderPass { get; set; }
@@ -83,6 +80,15 @@ public class AddServerViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool NaiveQuic { get; set; } public bool NaiveQuic { get; set; }
[Reactive]
public string Hy2RealmUrl { get; set; }
[Reactive]
public int GeckoMinPacketSize { get; set; }
[Reactive]
public int GeckoMaxPacketSize { get; set; }
[Reactive] [Reactive]
public string RawHeaderType { get; set; } public string RawHeaderType { get; set; }
@@ -305,6 +311,9 @@ public class AddServerViewModel : MyReactiveObject
CongestionControl = protocolExtra.CongestionControl ?? string.Empty; CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null; InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
NaiveQuic = protocolExtra.NaiveQuic ?? false; NaiveQuic = protocolExtra.NaiveQuic ?? false;
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
RawHeaderType = transport.RawHeaderType ?? Global.None; RawHeaderType = transport.RawHeaderType ?? Global.None;
Host = transport.Host ?? string.Empty; Host = transport.Host ?? string.Empty;
@@ -360,6 +369,16 @@ public class AddServerViewModel : MyReactiveObject
return; return;
} }
} }
HyRealm? realm = null;
if (!Hy2RealmUrl.IsNullOrEmpty())
{
var realmResult = HyRealm.TryParse(Hy2RealmUrl, out realm);
if (!realmResult)
{
NoticeManager.Instance.Enqueue(ResUI.InvalidHy2RealmUrl);
return;
}
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType); SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse; SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse;
SelectedSource.MuxEnabled = MuxEnabled; SelectedSource.MuxEnabled = MuxEnabled;
@@ -406,6 +425,9 @@ public class AddServerViewModel : MyReactiveObject
CongestionControl = CongestionControl.NullIfEmpty(), CongestionControl = CongestionControl.NullIfEmpty(),
InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null, InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null,
NaiveQuic = NaiveQuic ? true : null, NaiveQuic = NaiveQuic ? true : null,
Hy2RealmUrl = realm?.ToUri().NullIfEmpty(),
GeckoMinPacketSize = GeckoMinPacketSize > 0 ? GeckoMinPacketSize.ToString() : null,
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
}); });
SelectedSource.SetTransportExtra(transport); SelectedSource.SetTransportExtra(transport);
@@ -475,8 +497,7 @@ public class AddServerViewModel : MyReactiveObject
} }
(Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName, (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName,
verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName), verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName));
allowInsecure: AllowInsecureCertFetch);
UpdateCertTip(certError); UpdateCertTip(certError);
} }
@@ -502,8 +523,7 @@ public class AddServerViewModel : MyReactiveObject
} }
var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName, var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName,
verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName), verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName));
allowInsecure: AllowInsecureCertFetch);
Cert = CertPemManager.ConcatenatePemChain(certs); Cert = CertPemManager.ConcatenatePemChain(certs);
UpdateCertTip(certError); UpdateCertTip(certError);
} }
@@ -43,14 +43,14 @@ public class CheckUpdateViewModel : MyReactiveObject
private void RefreshCheckUpdateItems() private void RefreshCheckUpdateItems()
{ {
var models = CoreInfoManager.Instance.GetCheckUpdateCoreTypes()
.Select(t => GetCheckUpdateModel(t))
.ToList();
models.Add(GetGeoFileCheckUpdateModel());
CheckUpdateModels.Clear(); CheckUpdateModels.Clear();
CheckUpdateModels.AddRange(models);
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
{
CheckUpdateModels.Add(GetCheckUpdateModel(type));
}
CheckUpdateModels.Add(GetGeoFileCheckUpdateModel());
} }
private CheckUpdateModel GetCheckUpdateModel(ECoreType coreType) private CheckUpdateModel GetCheckUpdateModel(ECoreType coreType)
@@ -187,6 +187,7 @@ public class ClashProxiesViewModel : MyReactiveObject
var selectedName = SelectedGroup?.Name; var selectedName = SelectedGroup?.Name;
ProxyGroups.Clear(); ProxyGroups.Clear();
var lstProxyGroups = new List<ClashProxyModel>();
var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups(); var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups();
if (proxyGroups is { Count: > 0 }) if (proxyGroups is { Count: > 0 })
{ {
@@ -200,7 +201,7 @@ public class ClashProxiesViewModel : MyReactiveObject
{ {
continue; continue;
} }
ProxyGroups.Add(new ClashProxyModel() lstProxyGroups.Add(new ClashProxyModel()
{ {
Now = item.now, Now = item.now,
Name = item.name, Name = item.name,
@@ -216,12 +217,12 @@ public class ClashProxiesViewModel : MyReactiveObject
{ {
continue; continue;
} }
var item = ProxyGroups.FirstOrDefault(t => t.Name == kv.Key); var item = lstProxyGroups.FirstOrDefault(t => t.Name == kv.Key);
if (item != null && item.Name.IsNotEmpty()) if (item != null && item.Name.IsNotEmpty())
{ {
continue; continue;
} }
ProxyGroups.Add(new ClashProxyModel() lstProxyGroups.Add(new ClashProxyModel()
{ {
Now = kv.Value.now, Now = kv.Value.now,
Name = kv.Key, Name = kv.Key,
@@ -229,6 +230,8 @@ public class ClashProxiesViewModel : MyReactiveObject
}); });
} }
ProxyGroups.AddRange(lstProxyGroups);
if (ProxyGroups is { Count: > 0 }) if (ProxyGroups is { Count: > 0 })
{ {
if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName)) if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName))
@@ -26,6 +26,10 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public int? HyDownMbps { get; set; } [Reactive] public int? HyDownMbps { get; set; }
[Reactive] public bool EnableFragment { get; set; } [Reactive] public bool EnableFragment { get; set; }
[Reactive] public bool EnableFinalFragment { get; set; } [Reactive] public bool EnableFinalFragment { get; set; }
[Reactive] public string FragmentPackets { get; set; }
[Reactive] public string FragmentLength { get; set; }
[Reactive] public string FragmentInterval { get; set; }
[Reactive] public string FragmentMaxSplit { get; set; }
#endregion Core #endregion Core
@@ -163,6 +167,10 @@ public class OptionSettingViewModel : MyReactiveObject
HyDownMbps = _config.HysteriaItem.DownMbps; HyDownMbps = _config.HysteriaItem.DownMbps;
EnableFragment = _config.CoreBasicItem.EnableFragment; EnableFragment = _config.CoreBasicItem.EnableFragment;
EnableFinalFragment = _config.CoreBasicItem.EnableFinalFragment; EnableFinalFragment = _config.CoreBasicItem.EnableFinalFragment;
FragmentPackets = _config.Fragment4RayItem?.Packets;
FragmentLength = _config.Fragment4RayItem?.Length;
FragmentInterval = _config.Fragment4RayItem?.Interval;
FragmentMaxSplit = _config.Fragment4RayItem?.MaxSplit;
#endregion Core #endregion Core
@@ -300,12 +308,6 @@ public class OptionSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort); NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return; return;
} }
var sendThroughValue = SendThrough.TrimEx();
if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue))
{
NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4);
return;
}
var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics
|| DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed || DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed
|| EnableDragDropSort != _config.UiItem.EnableDragDropSort || EnableDragDropSort != _config.UiItem.EnableDragDropSort
@@ -349,8 +351,31 @@ public class OptionSettingViewModel : MyReactiveObject
_config.CoreBasicItem.EnableCacheFile4Sbox = EnableCacheFile4Sbox; _config.CoreBasicItem.EnableCacheFile4Sbox = EnableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = HyUpMbps ?? 0; _config.HysteriaItem.UpMbps = HyUpMbps ?? 0;
_config.HysteriaItem.DownMbps = HyDownMbps ?? 0; _config.HysteriaItem.DownMbps = HyDownMbps ?? 0;
if (EnableFragment)
{
if (!Utils.TryParseRange(FragmentLength, 0, int.MaxValue, out _, out _))
{
NoticeManager.Instance.Enqueue(ResUI.FillFragmentParameterError);
return;
}
if (!Utils.TryParseRange(FragmentInterval, 1, 100, out _, out _))
{
NoticeManager.Instance.Enqueue(ResUI.FillFragmentParameterError);
return;
}
if (FragmentMaxSplit.IsNotEmpty()
&& !Utils.TryParseMaxSplit(FragmentMaxSplit, 0, 10000, out _, out _))
{
NoticeManager.Instance.Enqueue(ResUI.FillFragmentParameterError);
return;
}
}
_config.CoreBasicItem.EnableFragment = EnableFragment; _config.CoreBasicItem.EnableFragment = EnableFragment;
_config.CoreBasicItem.EnableFinalFragment = EnableFinalFragment; _config.CoreBasicItem.EnableFinalFragment = EnableFinalFragment;
_config.Fragment4RayItem.Packets = FragmentPackets;
_config.Fragment4RayItem.Length = FragmentLength;
_config.Fragment4RayItem.Interval = FragmentInterval;
_config.Fragment4RayItem.MaxSplit = FragmentMaxSplit;
_config.GuiItem.AutoRun = AutoRun; _config.GuiItem.AutoRun = AutoRun;
_config.GuiItem.EnableStatistics = EnableStatistics; _config.GuiItem.EnableStatistics = EnableStatistics;
@@ -161,19 +161,17 @@ public class ProfilesSelectViewModel : MyReactiveObject
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
} }
public async Task RefreshSubscriptions() private async Task RefreshSubscriptions()
{ {
var subItems = await AppManager.Instance.SubItems();
subItems.Insert(0, new SubItem { Remarks = ResUI.AllGroupServers });
SubItems.Clear(); SubItems.Clear();
SubItems.AddRange(subItems);
SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
foreach (var item in await AppManager.Instance.SubItems())
{
SubItems.Add(item);
}
SelectedSub = (_config.SubIndexId.IsNotEmpty() SelectedSub = (_config.SubIndexId.IsNotEmpty()
? SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) ? subItems.FirstOrDefault(t => t.Id == _config.SubIndexId)
: null) ?? SubItems.FirstOrDefault(); : null) ?? subItems.FirstOrDefault();
} }
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
@@ -397,17 +397,15 @@ public class ProfilesViewModel : MyReactiveObject
private async Task RefreshSubscriptions() private async Task RefreshSubscriptions()
{ {
var subItems = await AppManager.Instance.SubItems();
subItems.Insert(0, new SubItem { Remarks = ResUI.AllGroupServers });
SubItems.Clear(); SubItems.Clear();
SubItems.AddRange(subItems);
SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
foreach (var item in await AppManager.Instance.SubItems())
{
SubItems.Add(item);
}
SelectedSub = (_config.SubIndexId.IsNotEmpty() SelectedSub = (_config.SubIndexId.IsNotEmpty()
? SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) ? subItems.FirstOrDefault(t => t.Id == _config.SubIndexId)
: null) ?? SubItems.FirstOrDefault(); : null) ?? subItems.FirstOrDefault();
} }
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
@@ -57,7 +57,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
{ {
Domain = Utils.Convert2Comma(Domain); Domain = Utils.Convert2Comma(Domain);
IP = Utils.Convert2Comma(IP); IP = Utils.Convert2Comma(IP);
Process = Utils.Convert2Comma(Process); Process = Utils.ParseProcess(Process);
if (AutoSort) if (AutoSort)
{ {
@@ -95,6 +95,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
{ {
RulesItems.Clear(); RulesItems.Clear();
var models = new List<RulesItemModel>();
foreach (var item in _rules) foreach (var item in _rules)
{ {
var it = new RulesItemModel() var it = new RulesItemModel()
@@ -110,8 +111,9 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
Enabled = item.Enabled, Enabled = item.Enabled,
Remarks = item.Remarks, Remarks = item.Remarks,
}; };
RulesItems.Add(it); models.Add(it);
} }
RulesItems.AddRange(models);
} }
public async Task RuleEditAsync(bool blNew) public async Task RuleEditAsync(bool blNew)
@@ -83,6 +83,7 @@ public class RoutingSettingViewModel : MyReactiveObject
public async Task RefreshRoutingItems() public async Task RefreshRoutingItems()
{ {
RoutingItems.Clear(); RoutingItems.Clear();
var models = new List<RoutingItemModel>();
var routings = await AppManager.Instance.RoutingItems(); var routings = await AppManager.Instance.RoutingItems();
foreach (var item in routings) foreach (var item in routings)
@@ -98,8 +99,9 @@ public class RoutingSettingViewModel : MyReactiveObject
CustomRulesetPath4Singbox = item.CustomRulesetPath4Singbox, CustomRulesetPath4Singbox = item.CustomRulesetPath4Singbox,
Sort = item.Sort, Sort = item.Sort,
}; };
RoutingItems.Add(it); models.Add(it);
} }
RoutingItems.AddRange(models);
} }
/// <summary> /// <summary>
@@ -234,6 +234,8 @@ public class StatusBarViewModel : MyReactiveObject
await RefreshRoutingsMenu(); await RefreshRoutingsMenu();
await InboundDisplayStatus(); await InboundDisplayStatus();
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true); await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true);
BlRouting = true;
} }
public void InitUpdateView(Func<EViewAction, object?, Task<bool>>? updateView) public void InitUpdateView(Func<EViewAction, object?, Task<bool>>? updateView)
@@ -312,18 +314,20 @@ public class StatusBarViewModel : MyReactiveObject
return; return;
} }
var models = new List<ComboItem>();
BlServers = true; BlServers = true;
foreach (var it in lstModel) foreach (var it in lstModel)
{ {
var name = it.GetSummary(); var name = it.GetSummary();
var item = new ComboItem() { ID = it.IndexId, Text = name }; var item = new ComboItem() { ID = it.IndexId, Text = name };
Servers.Add(item); models.Add(item);
if (_config.IndexId == it.IndexId) if (_config.IndexId == it.IndexId)
{ {
SelectedServer = item; SelectedServer = item;
} }
} }
Servers.AddRange(models);
} }
private void ServerSelectedChanged(bool c) private void ServerSelectedChanged(bool c)
@@ -408,18 +412,12 @@ public class StatusBarViewModel : MyReactiveObject
private async Task RefreshRoutingsMenu() private async Task RefreshRoutingsMenu()
{ {
RoutingItems.Clear();
BlRouting = true;
var routings = await AppManager.Instance.RoutingItems(); var routings = await AppManager.Instance.RoutingItems();
foreach (var item in routings)
{ RoutingItems.Clear();
RoutingItems.Add(item); RoutingItems.AddRange(routings);
if (item.IsActive)
{ SelectedRouting = routings.FirstOrDefault(t => t.IsActive == true);
SelectedRouting = item;
}
}
} }
private async Task RoutingSelectedChangedAsync(bool c) private async Task RoutingSelectedChangedAsync(bool c)
+59 -7
View File
@@ -23,8 +23,8 @@ public partial class App : Application
DataContext = StatusBarViewModel.Instance; DataContext = StatusBarViewModel.Instance;
} }
desktop.Exit += OnExit; var mainWindow = new MainWindow();
desktop.MainWindow = new MainWindow(); desktop.MainWindow = mainWindow;
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS())
{ {
@@ -35,6 +35,8 @@ public partial class App : Application
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
#region MacOS Activation
private void OnMacOSActivated(object? sender, ActivatedEventArgs args) private void OnMacOSActivated(object? sender, ActivatedEventArgs args)
{ {
if (args.Kind != ActivationKind.Reopen) if (args.Kind != ActivationKind.Reopen)
@@ -42,17 +44,69 @@ public partial class App : Application
return; return;
} }
if ((ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow is not MainWindow mainWindow)
{
return;
}
var isMiniaturized = MacAppUtils.IsWindowMiniaturized(mainWindow);
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
if (isMiniaturized)
{
RestoreMacOSAccessoryPolicyAfterMiniaturize(mainWindow);
mainWindow.ShowHideWindow(true);
return;
}
if (!AppManager.Instance.Config.UiItem.MacOSShowInDock) if (!AppManager.Instance.Config.UiItem.MacOSShowInDock)
{ {
MacAppUtils.SetActivationPolicyAccessory(); MacAppUtils.SetActivationPolicyAccessory();
} }
((ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow)?.ShowHideWindow(true); mainWindow.ShowHideWindow(true);
}); });
} }
private static void RestoreMacOSAccessoryPolicyAfterMiniaturize(MainWindow mainWindow)
{
if (AppManager.Instance.Config.UiItem.MacOSShowInDock)
{
return;
}
mainWindow
.GetObservable(Window.WindowStateProperty)
.Skip(1)
.Where(state => state != WindowState.Minimized)
.Take(1)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(_ => QueueMacOSAccessoryPolicyRestore(mainWindow));
}
private static void QueueMacOSAccessoryPolicyRestore(MainWindow mainWindow)
{
// AppKit may keep isMiniaturized set until the Dock restore animation finishes.
DispatcherTimer.RunOnce(() => RestoreMacOSAccessoryPolicy(mainWindow), TimeSpan.FromMilliseconds(300));
}
private static void RestoreMacOSAccessoryPolicy(MainWindow mainWindow)
{
if (AppManager.Instance.Config.UiItem.MacOSShowInDock || MacAppUtils.IsWindowMiniaturized(mainWindow))
{
return;
}
MacAppUtils.SetActivationPolicyAccessory();
mainWindow.Activate();
mainWindow.Focus();
}
#endregion MacOS Activation
#region App Event
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{ {
if (e.ExceptionObject != null) if (e.ExceptionObject != null)
@@ -66,10 +120,6 @@ public partial class App : Application
Logging.SaveLog("TaskScheduler_UnobservedTaskException", e.Exception); Logging.SaveLog("TaskScheduler_UnobservedTaskException", e.Exception);
} }
private void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs e)
{
}
private async void MenuAddServerViaClipboardClick(object? sender, EventArgs e) private async void MenuAddServerViaClipboardClick(object? sender, EventArgs e)
{ {
try try
@@ -91,4 +141,6 @@ public partial class App : Application
await AppManager.Instance.AppExitAsync(false); await AppManager.Instance.AppExitAsync(false);
AppManager.Instance.Shutdown(true); AppManager.Instance.Shutdown(true);
} }
#endregion App Event
} }
+22 -11
View File
@@ -4,7 +4,15 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
{ {
public WindowBase() public WindowBase()
{ {
Initialized += OnWindowInitialized;
Loaded += OnLoaded; Loaded += OnLoaded;
Loaded += (s, e) =>
{
if (Owner != null && !ShowInTaskbar)
{
CanMinimize = false;
}
};
} }
private void ReactiveWindowBase_Closed(object? sender, EventArgs e) private void ReactiveWindowBase_Closed(object? sender, EventArgs e)
@@ -12,30 +20,33 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
throw new NotImplementedException(); throw new NotImplementedException();
} }
protected virtual void OnLoaded(object? sender, RoutedEventArgs e) private void OnWindowInitialized(object? sender, EventArgs e)
{ {
try try
{ {
var sizeItem = ConfigHandler.GetWindowSizeItem(AppManager.Instance.Config, GetType().Name); var sizeItem = ConfigHandler.GetWindowSizeItem(AppManager.Instance.Config, GetType().Name);
if (sizeItem == null) if (sizeItem is null)
{ {
return; return;
} }
Width = sizeItem.Width; if (sizeItem.Width > 0 && !Width.Equals(sizeItem.Width))
Height = sizeItem.Height; {
Width = sizeItem.Width;
}
var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea; if (sizeItem.Height > 0 && !Height.Equals(sizeItem.Height))
var scaling = (Utils.IsMacOS() ? null : VisualRoot?.RenderScaling) ?? 1.0; {
Height = sizeItem.Height;
var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2); }
var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2);
Position = new PixelPoint((int)x, (int)y);
} }
catch { } catch { }
} }
protected virtual void OnLoaded(object? sender, RoutedEventArgs e)
{
}
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
{ {
base.OnClosed(e); base.OnClosed(e);
@@ -18,18 +18,18 @@ public static class AppBuilderExtension
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
AddFontFallback(fallbacks, "Segoe UI Symbol");
AddFontFallback(fallbacks, "Segoe UI Emoji"); AddFontFallback(fallbacks, "Segoe UI Emoji");
AddFontFallback(fallbacks, "Segoe UI Symbol");
} }
else if (OperatingSystem.IsMacOS()) else if (OperatingSystem.IsMacOS())
{ {
AddFontFallback(fallbacks, "Apple Symbols");
AddFontFallback(fallbacks, "Apple Color Emoji"); AddFontFallback(fallbacks, "Apple Color Emoji");
AddFontFallback(fallbacks, "Apple Symbols");
} }
else if (OperatingSystem.IsLinux()) else if (OperatingSystem.IsLinux())
{ {
AddFontFallback(fallbacks, "Noto Sans Symbols");
AddFontFallback(fallbacks, "Noto Color Emoji"); AddFontFallback(fallbacks, "Noto Color Emoji");
AddFontFallback(fallbacks, "Noto Sans Symbols");
} }
return appBuilder.With(new FontManagerOptions return appBuilder.With(new FontManagerOptions
@@ -13,6 +13,10 @@ internal static class MacAppUtils
sel_registerName("setActivationPolicy:"), sel_registerName("setActivationPolicy:"),
ActivationPolicyAccessory); ActivationPolicyAccessory);
public static bool IsWindowMiniaturized(Window window)
=> window.TryGetPlatformHandle() is IMacOSTopLevelPlatformHandle { NSWindow: not 0 } handle
&& objc_msgSend_bool(handle.NSWindow, sel_registerName("isMiniaturized"));
[DllImport(LibObjC)] [DllImport(LibObjC)]
private static extern nint objc_getClass(string name); private static extern nint objc_getClass(string name);
@@ -22,6 +26,10 @@ internal static class MacAppUtils
[DllImport(LibObjC, EntryPoint = "objc_msgSend")] [DllImport(LibObjC, EntryPoint = "objc_msgSend")]
private static extern nint objc_msgSend(nint receiver, nint selector); private static extern nint objc_msgSend(nint receiver, nint selector);
[DllImport(LibObjC, EntryPoint = "objc_msgSend")]
[return: MarshalAs(UnmanagedType.I1)]
private static extern bool objc_msgSend_bool(nint receiver, nint selector);
[DllImport(LibObjC, EntryPoint = "objc_msgSend")] [DllImport(LibObjC, EntryPoint = "objc_msgSend")]
private static extern void objc_msgSend(nint receiver, nint selector, nint argument); private static extern void objc_msgSend(nint receiver, nint selector, nint argument);
} }
@@ -374,7 +374,7 @@
Grid.Row="2" Grid.Row="2"
ColumnDefinitions="300,Auto,Auto" ColumnDefinitions="300,Auto,Auto"
IsVisible="False" IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
@@ -388,61 +388,110 @@
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}" />
<Button
<TextBlock Grid.Row="1"
Grid.Row="2" Grid.Column="2"
Grid.Column="0" Margin="{StaticResource MarginLr4}"
Margin="{StaticResource Margin4}" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPath7}" /> Classes="IconButton">
<TextBox <Button.Content>
x:Name="txtPath7" <PathIcon Data="{StaticResource SemiIconMore}">
Grid.Row="2" <PathIcon.RenderTransform>
Grid.Column="1" <RotateTransform Angle="90" />
Width="400" </PathIcon.RenderTransform>
Margin="{StaticResource Margin4}" /> </PathIcon>
</Button.Content>
<Button.Flyout>
<Flyout>
<StackPanel>
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPath7}" />
<TextBox
x:Name="txtPath7"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbGeckoPacketSize}" />
<StackPanel Orientation="Horizontal">
<TextBox
x:Name="txtMinGeckoPacketSize"
Width="90"
Margin="{StaticResource Margin4}"
Watermark="Min" />
<TextBox
x:Name="txtMaxGeckoPacketSize"
Width="90"
Margin="{StaticResource Margin4}"
Watermark="Max" />
</StackPanel>
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbHy2RealmUrl}" />
<TextBox
x:Name="txtHy2RealmUrl"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbHy2RealmUrlTip}"
TextWrapping="Wrap" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="2"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPorts7}" /> Text="{x:Static resx:ResUI.TbPorts7}" />
<TextBox <TextBox
x:Name="txtPorts7" x:Name="txtPorts7"
Grid.Row="3" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="1000-2000,3000,4000" /> Watermark="1000-2000,3000,4000" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="2"
Grid.Column="2" Grid.Column="2"
Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPorts7Tips}" /> Text="{x:Static resx:ResUI.TbPorts7Tips}"
TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="4" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbHopInt7}" /> Text="{x:Static resx:ResUI.TbHopInt7}" />
<TextBox <TextBox
x:Name="txtHopInt7" x:Name="txtHopInt7"
Grid.Row="4" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
Width="400" Width="200"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock <TextBlock
Grid.Row="5" Grid.Row="4"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsHysteriaBandwidth}" /> Text="{x:Static resx:ResUI.TbSettingsHysteriaBandwidth}" />
<StackPanel <StackPanel
Grid.Row="5" Grid.Row="4"
Grid.Column="1" Grid.Column="1"
Orientation="Horizontal"> Orientation="Horizontal">
<TextBox <TextBox
@@ -1189,25 +1238,6 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbFetchCertChain}" /> Content="{x:Static resx:ResUI.TbFetchCertChain}" />
</StackPanel> </StackPanel>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbAllowInsecureCertFetch}" />
<ToggleSwitch
x:Name="togAllowInsecureCertFetch"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</StackPanel>
<TextBlock
x:Name="txtAllowInsecureCertFetchTips"
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Foreground="#FFD32F2F"
IsVisible="False"
Text="{x:Static resx:ResUI.TbAllowInsecureCertFetchTips}"
TextWrapping="Wrap" />
<TextBlock <TextBlock
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -173,6 +173,9 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hy2RealmUrl, v => v.txtHy2RealmUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeckoMinPacketSize, v => v.txtMinGeckoPacketSize.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeckoMaxPacketSize, v => v.txtMaxGeckoPacketSize.Text).DisposeWith(disposables);
break; break;
case EConfigType.TUIC: case EConfigType.TUIC:
@@ -236,8 +239,6 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AllowInsecureCertFetch, v => v.togAllowInsecureCertFetch.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AllowInsecureCertFetch, v => v.txtAllowInsecureCertFetchTips.IsVisible).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables);
@@ -413,7 +413,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
public void ShowHideWindow(bool? blShow) public void ShowHideWindow(bool? blShow)
{ {
var bl = blShow ?? var bl = blShow ??
(Utils.IsLinux() (Utils.IsLinux() || Utils.IsMacOS()
? (!AppManager.Instance.ShowInTaskbar ^ (WindowState == WindowState.Minimized)) ? (!AppManager.Instance.ShowInTaskbar ^ (WindowState == WindowState.Minimized))
: !AppManager.Instance.ShowInTaskbar); : !AppManager.Instance.ShowInTaskbar);
if (bl) if (bl)
@@ -18,6 +18,8 @@ public partial class MessageBoxDialog : Window
btnYes.Click += BtnYes_Click; btnYes.Click += BtnYes_Click;
btnNo.Click += BtnNo_Click; btnNo.Click += BtnNo_Click;
CanMinimize = false;
} }
private void BtnYes_Click(object? sender, RoutedEventArgs e) private void BtnYes_Click(object? sender, RoutedEventArgs e)
@@ -300,6 +300,115 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<Button
Grid.Row="20"
Grid.Column="2"
Margin="{StaticResource MarginLr8}"
HorizontalAlignment="Left"
Classes="IconButton">
<Button.Content>
<PathIcon Data="{StaticResource SemiIconMore}">
<PathIcon.RenderTransform>
<RotateTransform Angle="90" />
</PathIcon.RenderTransform>
</PathIcon>
</Button.Content>
<Button.Flyout>
<Flyout>
<StackPanel Margin="{StaticResource Margin8}">
<Grid ColumnDefinitions="Auto,Auto,400" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsFragmentPackets}" />
<ComboBox
x:Name="cmbFragmentPackets"
Grid.Row="0"
Grid.Column="1"
Width="120"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="0"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsFragmentPacketsTip}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsFragmentLength}" />
<TextBox
x:Name="txtFragmentLength"
Grid.Row="1"
Grid.Column="1"
Width="120"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
Watermark="50-100" />
<TextBlock
Grid.Row="1"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsFragmentLengthTip}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsFragmentInterval}" />
<TextBox
x:Name="txtFragmentInterval"
Grid.Row="2"
Grid.Column="1"
Width="120"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
Watermark="10-20" />
<TextBlock
Grid.Row="2"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsFragmentIntervalTip}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsFragmentMaxSplit}" />
<TextBox
x:Name="txtFragmentMaxSplit"
Grid.Row="3"
Grid.Column="1"
Width="120"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
Watermark="0" />
<TextBlock
Grid.Row="3"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsFragmentMaxSplitTip}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<TextBlock <TextBlock
Grid.Row="21" Grid.Row="21"
Grid.Column="0" Grid.Column="0"
@@ -913,6 +1022,13 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbLegacyProtectTip}"
TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="9" Grid.Row="9"
@@ -1,4 +1,3 @@
using Avalonia.Media;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
@@ -36,6 +35,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbMtu.ItemsSource = Global.TunMtus; cmbMtu.ItemsSource = Global.TunMtus;
cmbStack.ItemsSource = Global.TunStacks; cmbStack.ItemsSource = Global.TunStacks;
cmbIcmpRoutingPolicy.ItemsSource = Global.TunIcmpRoutingPolicies; cmbIcmpRoutingPolicy.ItemsSource = Global.TunIcmpRoutingPolicies;
cmbFragmentPackets.ItemsSource = Global.FragmentPacketsOptions;
cmbCoreType1.ItemsSource = Global.CoreTypes; cmbCoreType1.ItemsSource = Global.CoreTypes;
cmbCoreType2.ItemsSource = Global.CoreTypes; cmbCoreType2.ItemsSource = Global.CoreTypes;
@@ -84,6 +84,10 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.HyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.HyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableFinalFragment, v => v.togenableFinalFragment.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableFinalFragment, v => v.togenableFinalFragment.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FragmentPackets, v => v.cmbFragmentPackets.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FragmentLength, v => v.txtFragmentLength.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FragmentInterval, v => v.txtFragmentInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FragmentMaxSplit, v => v.txtFragmentMaxSplit.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRun, v => v.togAutoRun.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRun, v => v.togAutoRun.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableStatistics, v => v.togEnableStatistics.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableStatistics, v => v.togEnableStatistics.IsChecked).DisposeWith(disposables);
+13 -5
View File
@@ -277,11 +277,19 @@
Header="{x:Static resx:ResUI.LvTestSpeed}" Header="{x:Static resx:ResUI.LvTestSpeed}"
Tag="SpeedVal" /> Tag="SpeedVal" />
<DataGridTextColumn <DataGridTemplateColumn SortMemberPath="IpInfo" Tag="IpInfo">
Width="100" <DataGridTemplateColumn.Header>
Binding="{Binding IpInfo}" <TextBlock Text="{x:Static resx:ResUI.LvTestIpInfo}" />
Header="{x:Static resx:ResUI.LvTestIpInfo}" </DataGridTemplateColumn.Header>
Tag="IpInfo" /> <DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<SelectableTextBlock
Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{Binding Path=IpInfo, Mode=OneWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn <DataGridTextColumn
Width="100" Width="100"
@@ -7,6 +7,7 @@
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<AssemblyName>v2rayN</AssemblyName> <AssemblyName>v2rayN</AssemblyName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CETCompat>false</CETCompat>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+1 -2
View File
@@ -1,7 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18 # Visual Studio Version 18
VisualStudioVersion = 18.5.11709.299 stable VisualStudioVersion = 18.5.11709.299
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "v2rayN", "v2rayN\v2rayN.csproj", "{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "v2rayN", "v2rayN\v2rayN.csproj", "{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}"
EndProject EndProject
+59 -36
View File
@@ -505,6 +505,7 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="300" /> <ColumnDefinition Width="300" />
@@ -526,21 +527,62 @@
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" /> Style="{StaticResource DefTextBox}" />
<materialDesign:PopupBox
<TextBlock Grid.Row="1"
Grid.Row="2" Grid.Column="2"
Grid.Column="0" HorizontalAlignment="Left"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" StaysOpen="True"
Text="{x:Static resx:ResUI.TbPath7}" /> Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
<TextBox <StackPanel>
x:Name="txtPath7" <TextBlock
Grid.Row="2" Margin="{StaticResource Margin4}"
Grid.Column="1" VerticalAlignment="Center"
Width="400" Style="{StaticResource ToolbarTextBlock}"
Margin="{StaticResource Margin4}" Text="{x:Static resx:ResUI.TbPath7}" />
Style="{StaticResource DefTextBox}" /> <TextBox
x:Name="txtPath7"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbGeckoPacketSize}" />
<StackPanel Orientation="Horizontal">
<TextBox
x:Name="txtMinGeckoPacketSize"
Width="90"
Margin="{StaticResource Margin4}"
materialDesign:HintAssist.Hint="Min"
Style="{StaticResource DefTextBox}" />
<TextBox
x:Name="txtMaxGeckoPacketSize"
Width="90"
Margin="{StaticResource Margin4}"
materialDesign:HintAssist.Hint="Max"
Style="{StaticResource DefTextBox}" />
</StackPanel>
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbHy2RealmUrl}" />
<TextBox
x:Name="txtHy2RealmUrl"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbHy2RealmUrlTip}"
TextWrapping="Wrap" />
</StackPanel>
</materialDesign:PopupBox>
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
@@ -561,10 +603,12 @@
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="2" Grid.Column="2"
Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbPorts7Tips}" /> Text="{x:Static resx:ResUI.TbPorts7Tips}"
TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="4" Grid.Row="4"
@@ -1519,27 +1563,6 @@
Content="{x:Static resx:ResUI.TbFetchCertChain}" Content="{x:Static resx:ResUI.TbFetchCertChain}"
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
</StackPanel> </StackPanel>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbAllowInsecureCertFetch}" />
<ToggleButton
x:Name="togAllowInsecureCertFetch"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</StackPanel>
<TextBlock
x:Name="txtAllowInsecureCertFetchTips"
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Foreground="#FFD32F2F"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbAllowInsecureCertFetchTips}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<TextBlock <TextBlock
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
+3 -4
View File
@@ -172,6 +172,9 @@ public partial class AddServerWindow
this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hy2RealmUrl, v => v.txtHy2RealmUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeckoMinPacketSize, v => v.txtMinGeckoPacketSize.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeckoMaxPacketSize, v => v.txtMaxGeckoPacketSize.Text).DisposeWith(disposables);
break; break;
case EConfigType.TUIC: case EConfigType.TUIC:
@@ -234,10 +237,6 @@ public partial class AddServerWindow
this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AllowInsecureCertFetch, v => v.togAllowInsecureCertFetch.IsChecked).DisposeWith(disposables);
this.WhenAnyValue(x => x.ViewModel.AllowInsecureCertFetch)
.Select(b => b ? Visibility.Visible : Visibility.Collapsed)
.BindTo(this, v => v.txtAllowInsecureCertFetchTips.Visibility);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables);
@@ -17,6 +17,13 @@
Style="{StaticResource WindowGlobal}" Style="{StaticResource WindowGlobal}"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Popupbox.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<DockPanel Margin="{StaticResource Margin8}"> <DockPanel Margin="{StaticResource Margin8}">
<StackPanel <StackPanel
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -366,6 +373,100 @@
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<materialDesign:PopupBox
Grid.Row="20"
Grid.Column="2"
HorizontalAlignment="Left"
StaysOpen="True"
Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
<StackPanel Margin="{StaticResource Margin8}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="400" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsFragmentPackets}"
TextWrapping="Wrap" />
<ComboBox
x:Name="cmbFragmentPackets"
Grid.Row="0"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsFragmentLength}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtFragmentLength"
Grid.Row="1"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
materialDesign:HintAssist.Hint="50-100"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsFragmentInterval}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtFragmentInterval"
Grid.Row="2"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
materialDesign:HintAssist.Hint="10-20"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsFragmentMaxSplit}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtFragmentMaxSplit"
Grid.Row="3"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
materialDesign:HintAssist.Hint="0"
Style="{StaticResource DefTextBox}" />
</Grid>
</StackPanel>
</materialDesign:PopupBox>
<TextBlock <TextBlock
Grid.Row="21" Grid.Row="21"
Grid.Column="0" Grid.Column="0"
@@ -1184,6 +1285,14 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbLegacyProtectTip}"
TextWrapping="Wrap" />
<TextBlock <TextBlock
Grid.Row="9" Grid.Row="9"
@@ -32,6 +32,7 @@ public partial class OptionSettingWindow
cmbMtu.ItemsSource = Global.TunMtus; cmbMtu.ItemsSource = Global.TunMtus;
cmbStack.ItemsSource = Global.TunStacks; cmbStack.ItemsSource = Global.TunStacks;
cmbIcmpRoutingPolicy.ItemsSource = Global.TunIcmpRoutingPolicies; cmbIcmpRoutingPolicy.ItemsSource = Global.TunIcmpRoutingPolicies;
cmbFragmentPackets.ItemsSource = Global.FragmentPacketsOptions;
cmbCoreType1.ItemsSource = Global.CoreTypes; cmbCoreType1.ItemsSource = Global.CoreTypes;
cmbCoreType2.ItemsSource = Global.CoreTypes; cmbCoreType2.ItemsSource = Global.CoreTypes;
@@ -80,6 +81,10 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.HyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.HyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableFinalFragment, v => v.togenableFinalFragment.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableFinalFragment, v => v.togenableFinalFragment.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FragmentPackets, v => v.cmbFragmentPackets.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FragmentLength, v => v.txtFragmentLength.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FragmentInterval, v => v.txtFragmentInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FragmentMaxSplit, v => v.txtFragmentMaxSplit.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.Kcpmtu, v => v.txtKcpmtu.Text).DisposeWith(disposables); //this.Bind(ViewModel, vm => vm.Kcpmtu, v => v.txtKcpmtu.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.Kcptti, v => v.txtKcptti.Text).DisposeWith(disposables); //this.Bind(ViewModel, vm => vm.Kcptti, v => v.txtKcptti.Text).DisposeWith(disposables);
+1
View File
@@ -10,6 +10,7 @@
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode> <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CETCompat>false</CETCompat>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>