Compare commits

..

35 commits

Author SHA1 Message Date
DHR60
9f9b90cb97
Add hysteria2 uri cert sha pinning support (#8657)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2026-01-17 16:22:26 +08:00
DHR60
c42dcd2876
Add process matching rules support (#8643)
* Add process matching rules support

* Fix
2026-01-17 16:08:36 +08:00
2dust
2fefafdd37 Add support for CoreType7 (Hysteria2) in option settings 2026-01-17 16:06:29 +08:00
DHR60
2c9a90c878
Add xray hysteria2 outbound support (#8630) 2026-01-17 15:49:44 +08:00
DHR60
4e5f1838a2
Add Cert SHA-256 pinning support (#8613) 2026-01-17 15:42:40 +08:00
2dust
a45a1dc982 Ensure WebDAV base URL ends with trailing slash 2026-01-17 15:08:08 +08:00
2dust
fe183798b6 Refactor child item aggregation in managers
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2026-01-13 20:24:52 +08:00
2dust
947c84cf10 Refactor 'Move to Group' menu in ProfilesView
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2026-01-10 15:14:58 +08:00
2dust
9c74b51d74 up 7.17.0
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2026-01-09 18:44:28 +08:00
2dust
abd962ab31 Update Global.cs
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2026-01-08 17:15:59 +08:00
DHR60
f3b894015e
Add sing-box ech support (#8603)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
* Add sing-box ech support

* Support group config type

* Simplified code
2026-01-08 13:56:45 +08:00
2dust
4562d4cf00 Add ECH config support to profile and UI
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
Introduces EchConfigList and EchForceQuery fields to ProfileItem and V2rayConfig models, updates related handlers and services to process these fields, and extends the AddServerWindow UI to allow user input for ECH configuration. Also adds localization entries for the new fields and updates extension methods for string handling.
2026-01-07 11:34:13 +08:00
JieXu
bc36cf8a47
Code Clean (#8586)
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2026-01-05 09:56:43 +08:00
Kazuto Iris
cbdfe2e15a
fix: Fix failure to follow system theme changes (#8584)
Fix the issue where the application failed to sync with system dark/light mode changes in specific scenarios such as triggering system theme switching via scheduled tasks while waking from hibernation, caused by the unreliable HWND hook implementation that missed critical events.
2026-01-05 09:56:33 +08:00
2dust
68583e20bc Update package versions in Directory.Packages.props
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2026-01-03 19:06:17 +08:00
DHR60
6d6459b009
Fix edge cases (#8564)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2026-01-03 10:20:27 +08:00
2dust
807562b69e Set all .NET publish tasks to self-contained
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2025-12-28 14:10:00 +08:00
2dust
654d7d83d0 up 7.16.9
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2025-12-25 18:34:10 +08:00
2dust
027252e687 Move ShowInTaskbar and RunningCoreType to AppManager
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-12-24 16:01:28 +08:00
2dust
5478c90180 Bug fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/8515
2025-12-24 14:19:36 +08:00
DHR60
28f30d7e97
Revert "Add TLS ALPN check for WS (#8469)" (#8517)
This reverts commit 6e27dca6cd.
2025-12-24 13:38:08 +08:00
2dust
ae7d54c2e5 up 7.16.8
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2025-12-22 19:04:36 +08:00
2dust
56d0d65b06 Reduce minimum width of MainWindow 2025-12-22 19:03:47 +08:00
2dust
5e8e189c27 Increase MenuItemHeight to 32 in App.xaml
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-12-21 18:53:09 +08:00
2dust
3fee86d44a Add context menu to subscription DataGrid 2025-12-21 18:53:00 +08:00
2dust
dd77eb79c6 Remove .NET self-contained zip check in UpdateService
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-12-20 14:47:40 +08:00
2dust
d26a2559a6 up 7.16.7 2025-12-20 14:12:41 +08:00
2dust
e5ba1759aa Update Directory.Packages.props 2025-12-20 14:12:16 +08:00
dependabot[bot]
bfdee37cc1
Bump actions/upload-artifact from 5.0.0 to 6.0.0 (#8493)
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 6.0.0
  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>
2025-12-16 09:19:09 +08:00
dependabot[bot]
cf89cfcd95
Bump actions/download-artifact from 6 to 7 (#8492)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  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>
2025-12-16 09:19:00 +08:00
2dust
39a988c704 Update Directory.Packages.props
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2025-12-13 15:18:52 +08:00
JieXu
2b28254fbc
Update ResUI.fr.resx (#8472)
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
2025-12-10 17:56:19 +08:00
DHR60
6e27dca6cd
Add TLS ALPN check for WS (#8469)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-12-09 20:22:13 +08:00
DHR60
7cee98887b
Refactor Node Precheck (#8464) 2025-12-09 20:03:07 +08:00
DHR60
3885ff8b31
Fix Shadowsocks Fmt (#8462)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-12-08 19:55:27 +08:00
73 changed files with 1358 additions and 637 deletions

View file

@ -50,7 +50,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o "$OutputPathArm64"
- name: Upload build artifacts
uses: actions/upload-artifact@v5.0.0
uses: actions/upload-artifact@v6.0.0
with:
name: v2rayN-linux
path: |
@ -116,7 +116,7 @@ jobs:
fetch-depth: '0'
- name: Restore build artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: v2rayN-linux
path: ${{ github.workspace }}/v2rayN/Release
@ -137,7 +137,7 @@ jobs:
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
- name: Upload RPM artifacts
uses: actions/upload-artifact@v5.0.0
uses: actions/upload-artifact@v6.0.0
with:
name: v2rayN-rpm
path: dist/rpm/**/*.rpm

View file

@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v5.0.0
uses: actions/upload-artifact@v6.0.0
with:
name: v2rayN-macos
path: |

View file

@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v5.0.0
uses: actions/upload-artifact@v6.0.0
with:
name: v2rayN-windows-desktop
path: |

View file

@ -37,16 +37,16 @@ jobs:
- name: Build
run: |
cd v2rayN
dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-x64 -p:SelfContained=false -p:EnableWindowsTargeting=true -o $OutputPath64
dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-arm64 -p:SelfContained=false -p:EnableWindowsTargeting=true -o $OutputPathArm64
dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPath64
dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPathArm64
dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPath64Sc
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 -p:SelfContained=false -p:EnableWindowsTargeting=true -o $OutputPath64
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=false -p:EnableWindowsTargeting=true -o $OutputPathArm64
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPath64
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPath64Sc
- name: Upload build artifacts
uses: actions/upload-artifact@v5.0.0
uses: actions/upload-artifact@v6.0.0
with:
name: v2rayN-windows
path: |

View file

@ -1,11 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
# == Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ==
# ====== Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS ======
if [[ -r /etc/os-release ]]; then
. /etc/os-release
case "$ID" in
rhel|rocky|almalinux|fedora|centos|ubuntu|debian)
rhel|rocky|almalinux|fedora|centos)
echo "[OK] Detected supported system: $NAME $VERSION_ID"
;;
*)
@ -30,7 +30,7 @@ echo "[INFO] Detected kernel version: $KERNEL_FULL"
if (( KERNEL_MAJOR < MIN_KERNEL_MAJOR )) || { (( KERNEL_MAJOR == MIN_KERNEL_MAJOR )) && (( KERNEL_MINOR < MIN_KERNEL_MINOR )); }; then
echo "[ERROR] Kernel $KERNEL_FULL is too old. Requires Linux >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}."
echo "Please upgrade your system or use a newer container (e.g. Fedora 42+, RHEL 10+, Debian 13+)."
echo "Please upgrade your system or use a newer container (e.g. Fedora 42+, RHEL 10+)."
exit 1
fi
@ -80,7 +80,6 @@ host_arch="$(uname -m)"
install_ok=0
case "$ID" in
# ------------------------------ RHEL family (UNCHANGED) ------------------------------
rhel|rocky|almalinux|centos)
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \
@ -92,58 +91,7 @@ case "$ID" in
install_ok=1
fi
;;
# ------------------------------ Ubuntu ----------------------------------------------
ubuntu)
sudo apt-get update
# Ensure 'universe' (Ubuntu) to get 'rpm'
if ! apt-cache policy | grep -q '^500 .*ubuntu.com/ubuntu.* universe'; then
sudo apt-get -y install software-properties-common || true
sudo add-apt-repository -y universe || true
sudo apt-get update
fi
# Base tools + rpm (provides rpmbuild)
sudo apt-get -y install curl unzip tar rsync rpm || true
# Cross-arch binutils so strip matches target arch + objdump for brp scripts
sudo apt-get -y install binutils binutils-x86-64-linux-gnu binutils-aarch64-linux-gnu || true
# rpmbuild presence check
if ! command -v rpmbuild >/dev/null 2>&1; then
echo "[ERROR] 'rpmbuild' not found after installing 'rpm'."
echo " Please ensure the 'rpm' package is available from your repos (universe on Ubuntu)."
exit 1
fi
# .NET SDK 8 (best effort via apt)
if ! command -v dotnet >/dev/null 2>&1; then
sudo apt-get -y install dotnet-sdk-8.0 || true
sudo apt-get -y install dotnet-sdk-8 || true
sudo apt-get -y install dotnet-sdk || true
fi
install_ok=1
;;
# ------------------------------ Debian (KEEP, with local dotnet install) ------------
debian)
sudo apt-get update
# Base tools + rpm (provides rpmbuild on Debian) + objdump/strip
sudo apt-get -y install curl unzip tar rsync rpm binutils || true
# rpmbuild presence check
if ! command -v rpmbuild >/dev/null 2>&1; then
echo "[ERROR] 'rpmbuild' not found after installing 'rpm'."
echo " Please ensure 'rpm' is available from Debian repos."
exit 1
fi
# Try apt for dotnet; fallback to official installer into $HOME/.dotnet
if ! command -v dotnet >/dev/null 2>&1; then
echo "[INFO] 'dotnet' not found. Installing .NET 8 SDK locally to \$HOME/.dotnet ..."
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
curl -fsSL https://dot.net/v1/dotnet-install.sh -o "$tmp/dotnet-install.sh"
bash "$tmp/dotnet-install.sh" --channel 8.0 --install-dir "$HOME/.dotnet"
export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
if ! command -v dotnet >/dev/null 2>&1; then
echo "[ERROR] dotnet installation failed."
exit 1
fi
fi
install_ok=1
*)
;;
esac
@ -154,7 +102,7 @@ fi
command -v curl >/dev/null
# Root directory = the script's location
# Root directory
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
@ -164,14 +112,14 @@ if [[ -f .gitmodules ]]; then
git submodule update --init --recursive || true
fi
# ===== Locate project ================================================================
# Locate project
PROJECT="v2rayN.Desktop/v2rayN.Desktop.csproj"
if [[ ! -f "$PROJECT" ]]; then
PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
fi
[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; }
# ===== Resolve GUI version & auto checkout ============================================
# Resolve GUI version & auto checkout
VERSION=""
choose_channel() {
@ -391,22 +339,6 @@ download_singbox() {
install -Dm755 "$bin" "$outdir/sing-box"
}
# ---- NEW: download_mihomo (REQUIRED in --netcore mode) ----
download_mihomo() {
# Download mihomo into outroot/bin/mihomo/mihomo
local outroot="$1"
local url=""
if [[ "$RID_DIR" == "linux-arm64" ]]; then
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64/bin/mihomo/mihomo"
else
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64/bin/mihomo/mihomo"
fi
echo "[+] Download mihomo: $url"
mkdir -p "$outroot/bin/mihomo"
curl -fL "$url" -o "$outroot/bin/mihomo/mihomo"
chmod +x "$outroot/bin/mihomo/mihomo" || true
}
# Move geo files to a unified path: outroot/bin
unify_geo_layout() {
local outroot="$1"
@ -491,8 +423,7 @@ download_v2rayn_bundle() {
fi
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
# keep mihomo
# find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
local nested_dir
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
@ -603,7 +534,7 @@ build_for_arch() {
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
# ---- REQUIRED: always fetch mihomo in netcore mode, per-arch ----
download_mihomo "$WORKDIR/$PKGROOT" || echo "[!] mihomo download failed (skipped)"
# download_mihomo "$WORKDIR/$PKGROOT" || echo "[!] mihomo download failed (skipped)"
fi
# Tarball

View file

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.16.6</Version>
<Version>7.17.0</Version>
</PropertyGroup>
<PropertyGroup>

View file

@ -6,23 +6,23 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.9" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.9" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.9" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.10" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.10" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.10" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageVersion Include="CliWrap" Version="3.10.0" />
<PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.3.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.3.1.1" />
<PackageVersion Include="QRCoder" Version="1.7.0" />
<PackageVersion Include="ReactiveUI" Version="22.3.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="22.3.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.7.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.7.2" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.1" />
<PackageVersion Include="NLog" Version="6.0.6" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.2" />
<PackageVersion Include="NLog" Version="6.0.7" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />

View file

@ -6,17 +6,17 @@ public static class Extension
{
public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value)
{
return string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value);
}
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value)
{
return string.IsNullOrWhiteSpace(value);
return string.IsNullOrWhiteSpace(value) || string.IsNullOrEmpty(value);
}
public static bool IsNotEmpty([NotNullWhen(false)] this string? value)
{
return !string.IsNullOrEmpty(value);
return !string.IsNullOrWhiteSpace(value);
}
public static string? NullIfEmpty(this string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value;
}
public static bool BeginWithAny(this string s, IEnumerable<char> chars)

View file

@ -88,6 +88,7 @@ public class Global
public const string SingboxLocalDNSTag = "local_local";
public const string SingboxHostsDNSTag = "hosts_dns";
public const string SingboxFakeDNSTag = "fake_dns";
public const string SingboxEchDNSTag = "ech_dns";
public static readonly List<string> IEProxyProtocols =
[
@ -107,7 +108,9 @@ public class Global
];
public static readonly List<string> SubConvertConfig =
[@"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online.ini"];
[
@"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online.ini"
];
public static readonly List<string> SubConvertTargets =
[
@ -142,11 +145,11 @@ public class Global
];
public static readonly List<string> SingboxRulesetSources =
[
[
"",
@"https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/sing-box/rule-set-{0}/{1}.srs",
@"https://raw.githubusercontent.com/chocolate4u/Iran-sing-box-rules/rule-set/{1}.srs"
];
];
public static readonly List<string> RoutingRulesSources =
[
@ -297,6 +300,7 @@ public class Global
EConfigType.VLESS,
EConfigType.Shadowsocks,
EConfigType.Trojan,
EConfigType.Hysteria2,
EConfigType.WireGuard,
EConfigType.SOCKS,
EConfigType.HTTP,
@ -626,5 +630,13 @@ public class Global
""
];
public static readonly List<string> EchForceQuerys =
[
"none",
"half",
"full",
""
];
#endregion const
}

View file

@ -253,6 +253,9 @@ public static class ConfigHandler
item.Extra = profileItem.Extra;
item.MuxEnabled = profileItem.MuxEnabled;
item.Cert = profileItem.Cert;
item.CertSha = profileItem.CertSha;
item.EchConfigList = profileItem.EchConfigList;
item.EchForceQuery = profileItem.EchForceQuery;
}
var ret = item.ConfigType switch
@ -700,7 +703,7 @@ public static class ConfigHandler
public static async Task<int> AddHysteria2Server(Config config, ProfileItem profileItem, bool toFile = true)
{
profileItem.ConfigType = EConfigType.Hysteria2;
profileItem.CoreType = ECoreType.sing_box;
//profileItem.CoreType = ECoreType.sing_box;
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Id = profileItem.Id.TrimEx();
@ -1273,7 +1276,7 @@ public static class ConfigHandler
}
else if (node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)
{
var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
var preCoreType = AppManager.Instance.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
itemSocks = new ProfileItem()
{
CoreType = preCoreType,

View file

@ -70,6 +70,14 @@ public class BaseFmt
}
ToUriQueryAllowInsecure(item, ref dicQuery);
}
if (item.EchConfigList.IsNotEmpty())
{
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
}
if (item.CertSha.IsNotEmpty())
{
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
}
dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp));
@ -209,6 +217,8 @@ public class BaseFmt
item.ShortId = GetQueryDecoded(query, "sid");
item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.EchConfigList = GetQueryDecoded(query, "ech");
item.CertSha = GetQueryDecoded(query, "pcs");
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{

View file

@ -25,6 +25,10 @@ public class Hysteria2Fmt : BaseFmt
ResolveUriQuery(query, ref item);
item.Path = GetQueryDecoded(query, "obfs-password");
item.Ports = GetQueryDecoded(query, "mport");
if (item.CertSha.IsNullOrEmpty())
{
item.CertSha = GetQueryDecoded(query, "pinSHA256");
}
return item;
}
@ -55,6 +59,16 @@ public class Hysteria2Fmt : BaseFmt
{
dicQuery.Add("mport", Utils.UrlEncode(item.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.Id, dicQuery, remark);
}

View file

@ -57,7 +57,10 @@ public class ShadowsocksFmt : BaseFmt
{
pluginArgs += "mode=websocket;";
pluginArgs += $"host={item.RequestHost};";
pluginArgs += $"path={item.Path};";
// https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172
// Equal signs and commas [and backslashes] must be escaped with a backslash.
var path = item.Path.Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,");
pluginArgs += $"path={path};";
}
else if (item.Network == nameof(ETransport.quic))
{
@ -75,8 +78,6 @@ public class ShadowsocksFmt : BaseFmt
var base64Content = cert.Replace(beginMarker, "").Replace(endMarker, "").Trim();
// https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172
// Equal signs and commas [and backslashes] must be escaped with a backslash.
base64Content = base64Content.Replace("=", "\\=");
pluginArgs += $"certRaw={base64Content};";
@ -85,6 +86,7 @@ public class ShadowsocksFmt : BaseFmt
if (pluginArgs.Length > 0)
{
plugin = "v2ray-plugin";
pluginArgs += "mux=0;";
}
}
@ -222,6 +224,7 @@ public class ShadowsocksFmt : BaseFmt
var path = pluginParts.FirstOrDefault(t => t.StartsWith("path="));
var hasTls = pluginParts.Any(t => t == "tls");
var certRaw = pluginParts.FirstOrDefault(t => t.StartsWith("certRaw="));
var mux = pluginParts.FirstOrDefault(t => t.StartsWith("mux="));
var modeValue = mode.Replace("mode=", "");
if (modeValue == "websocket")
@ -234,7 +237,9 @@ public class ShadowsocksFmt : BaseFmt
}
if (!path.IsNullOrEmpty())
{
item.Path = path.Replace("path=", "");
var pathValue = path.Replace("path=", "");
pathValue = pathValue.Replace("\\=", "=").Replace("\\,", ",").Replace("\\\\", "\\");
item.Path = pathValue;
}
}
else if (modeValue == "quic")
@ -258,6 +263,16 @@ public class ShadowsocksFmt : BaseFmt
item.Cert = certPem;
}
}
if (!mux.IsNullOrEmpty())
{
var muxValue = mux.Replace("mux=", "");
var muxCount = muxValue.ToInt();
if (muxCount > 0)
{
return null;
}
}
}
}

View file

@ -3,13 +3,11 @@ namespace ServiceLib.Manager;
/// <summary>
/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.).
/// </summary>
public class ActionPrecheckManager(Config config)
public class ActionPrecheckManager
{
private static readonly Lazy<ActionPrecheckManager> _instance = new(() => new ActionPrecheckManager(AppManager.Instance.Config));
private static readonly Lazy<ActionPrecheckManager> _instance = new();
public static ActionPrecheckManager Instance => _instance.Value;
private readonly Config _config = config;
// sing-box supported transports for different protocol types
private static readonly HashSet<string> SingboxUnsupportedTransports = [nameof(ETransport.kcp), nameof(ETransport.xhttp)];
@ -56,6 +54,7 @@ public class ActionPrecheckManager(Config config)
{
return [];
}
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
return await ValidateNodeAndCoreSupport(item, coreType);
}
@ -71,21 +70,64 @@ public class ActionPrecheckManager(Config config)
errors.Add(string.Format(ResUI.NotSupportProtocol, item.ConfigType.ToString()));
return errors;
}
if (!item.IsComplex())
else if (item.ConfigType.IsGroupType())
{
var groupErrors = await ValidateGroupNode(item, coreType);
errors.AddRange(groupErrors);
return errors;
}
else if (!item.IsComplex())
{
var normalErrors = await ValidateNormalNode(item, coreType);
errors.AddRange(normalErrors);
return errors;
}
return errors;
}
private async Task<List<string>> ValidateNormalNode(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
if (item.Address.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "Address"));
return errors;
}
if (item.Port is <= 0 or >= 65536)
if (item.Port is <= 0 or > 65535)
{
errors.Add(string.Format(ResUI.InvalidProperty, "Port"));
return errors;
}
var net = item.GetNetwork();
if (coreType == ECoreType.sing_box)
{
var transportError = ValidateSingboxTransport(item.ConfigType, net);
if (transportError != null)
{
errors.Add(transportError);
}
if (!Global.SingboxSupportConfigType.Contains(item.ConfigType))
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol,
nameof(ECoreType.sing_box), item.ConfigType.ToString()));
}
}
else if (coreType is ECoreType.Xray)
{
// Xray core does not support these protocols
if (!Global.XraySupportConfigType.Contains(item.ConfigType))
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol,
nameof(ECoreType.Xray), item.ConfigType.ToString()));
}
}
switch (item.ConfigType)
{
case EConfigType.VMess:
@ -123,21 +165,43 @@ public class ActionPrecheckManager(Config config)
break;
}
if (item.ConfigType is EConfigType.VLESS or EConfigType.Trojan
&& item.StreamSecurity == Global.StreamSecurityReality
&& item.PublicKey.IsNullOrEmpty())
if (item.StreamSecurity == Global.StreamSecurity)
{
// check certificate validity
if (!item.Cert.IsNullOrEmpty()
&& (CertPemManager.ParsePemChain(item.Cert).Count == 0)
&& !item.CertSha.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "TLS Certificate"));
}
}
if (item.StreamSecurity == Global.StreamSecurityReality)
{
if (item.PublicKey.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey"));
}
}
if (errors.Count > 0)
if (item.Network == nameof(ETransport.xhttp)
&& !item.Extra.IsNullOrEmpty())
{
// check xhttp extra json validity
var xhttpExtra = JsonUtils.ParseJson(item.Extra);
if (xhttpExtra is null)
{
errors.Add(string.Format(ResUI.InvalidProperty, "XHTTP Extra"));
}
}
return errors;
}
}
if (item.ConfigType.IsGroupType())
private async Task<List<string>> ValidateGroupNode(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group);
if (group is null || group.NotHasChild())
{
@ -152,9 +216,10 @@ public class ActionPrecheckManager(Config config)
return errors;
}
var childIds = Utils.String2List(group.ChildItems) ?? [];
var childIds = new List<string>();
var subItems = await ProfileGroupItemManager.GetSubChildProfileItems(group);
childIds.AddRange(subItems.Select(p => p.IndexId));
childIds.AddRange(Utils.String2List(group.ChildItems));
foreach (var child in childIds)
{
@ -178,36 +243,11 @@ public class ActionPrecheckManager(Config config)
}
childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType));
errors.AddRange(childErrors);
errors.AddRange(childErrors.Select(s => s.Insert(0, $"{childItem.Remarks}: ")));
}
return errors;
}
var net = item.GetNetwork();
if (coreType == ECoreType.sing_box)
{
var transportError = ValidateSingboxTransport(item.ConfigType, net);
if (transportError != null)
{
errors.Add(transportError);
return errors;
}
}
else if (coreType is ECoreType.Xray)
{
// Xray core does not support these protocols
if (!Global.XraySupportConfigType.Contains(item.ConfigType)
&& !item.IsComplex())
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString()));
return errors;
}
}
return errors;
}
private static string? ValidateSingboxTransport(EConfigType configType, string net)
{
// sing-box does not support xhttp / kcp transports
@ -271,7 +311,7 @@ public class ActionPrecheckManager(Config config)
if (node is not null)
{
var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType);
errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + s));
errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + $"{node.Remarks}: " + s));
}
else if (tag.IsNotEmpty())
{
@ -289,7 +329,7 @@ public class ActionPrecheckManager(Config config)
}
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
var routing = await ConfigHandler.GetDefaultRouting(_config);
var routing = await ConfigHandler.GetDefaultRouting(AppManager.Instance.Config);
if (routing == null)
{
return errors;
@ -317,7 +357,7 @@ public class ActionPrecheckManager(Config config)
}
var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType);
errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + s));
errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + $"{tagItem.Remarks}: " + s));
}
return errors;

View file

@ -31,6 +31,23 @@ public sealed class AppManager
public string LinuxSudoPwd { get; set; }
public bool ShowInTaskbar { get; set; }
public ECoreType RunningCoreType { get; set; }
public bool IsRunningCore(ECoreType type)
{
switch (type)
{
case ECoreType.Xray when RunningCoreType is ECoreType.Xray or ECoreType.v2fly or ECoreType.v2fly_v5:
case ECoreType.sing_box when RunningCoreType is ECoreType.sing_box or ECoreType.mihomo:
return true;
default:
return false;
}
}
#endregion Property
#region App

View file

@ -416,4 +416,22 @@ public class CertPemManager
return string.Concat(pemList);
}
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
{
try
{
var cert = X509Certificate2.CreateFromPem(pemCert);
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
if (includeColon)
{
return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c)));
}
return thumbprint;
}
catch
{
return string.Empty;
}
}
}

View file

@ -125,7 +125,7 @@ public sealed class CoreInfoManager
new CoreInfo
{
CoreType = ECoreType.mihomo,
CoreExes = ["mihomo-windows-amd64-v1", "mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "clash", "mihomo"],
CoreExes = GetMihomoCoreExes(),
Arguments = "-f {0}" + PortableMode(),
Url = GetCoreUrl(ECoreType.mihomo),
ReleaseApiUrl = urlMihomo.Replace(Global.GithubUrl, Global.GithubApiUrl),
@ -248,4 +248,34 @@ public sealed class CoreInfoManager
{
return $"{Global.GithubUrl}/{Global.CoreUrls[eCoreType]}/releases";
}
private static List<string>? GetMihomoCoreExes()
{
var names = new List<string>();
if (Utils.IsWindows())
{
names.Add("mihomo-windows-amd64-v1");
names.Add("mihomo-windows-amd64-compatible");
names.Add("mihomo-windows-amd64");
names.Add("mihomo-windows-arm64");
}
else if (Utils.IsLinux())
{
names.Add("mihomo-linux-amd64-v1");
names.Add("mihomo-linux-amd64");
names.Add("mihomo-linux-arm64");
}
else if (Utils.IsMacOS())
{
names.Add("mihomo-darwin-amd64-v1");
names.Add("mihomo-darwin-amd64");
names.Add("mihomo-darwin-arm64");
}
names.Add("clash");
names.Add("mihomo");
return names;
}
}

View file

@ -167,7 +167,7 @@ public class CoreManager
private async Task CoreStart(ProfileItem node)
{
var coreType = _config.RunningCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreType = AppManager.Instance.RunningCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;

View file

@ -230,9 +230,10 @@ public class ProfileGroupItemManager
{
return (new List<ProfileItem>(), profileGroupItem);
}
var items = await GetChildProfileItems(profileGroupItem);
var subItems = await GetSubChildProfileItems(profileGroupItem);
items.AddRange(subItems);
var items = new List<ProfileItem>();
items.AddRange(await GetSubChildProfileItems(profileGroupItem));
items.AddRange(await GetChildProfileItems(profileGroupItem));
return (items, profileGroupItem);
}
@ -316,5 +317,72 @@ public class ProfileGroupItemManager
return childAddresses;
}
public static async Task<HashSet<string>> GetAllChildEchQuerySni(string indexId)
{
// include grand children
var childAddresses = new HashSet<string>();
if (!Instance.TryGet(indexId, out var groupItem) || groupItem == null)
{
return childAddresses;
}
if (groupItem.SubChildItems.IsNotEmpty())
{
var subItems = await GetSubChildProfileItems(groupItem);
foreach (var childNode in subItems)
{
if (childNode.EchConfigList.IsNullOrEmpty())
{
continue;
}
if (childNode.StreamSecurity == Global.StreamSecurity
&& childNode.EchConfigList?.Contains("://") == true)
{
var idx = childNode.EchConfigList.IndexOf('+');
childAddresses.Add(idx > 0 ? childNode.EchConfigList[..idx] : childNode.Sni);
}
else
{
childAddresses.Add(childNode.Sni);
}
}
}
var childIds = Utils.String2List(groupItem.ChildItems) ?? [];
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
{
continue;
}
if (!childNode.IsComplex() && !childNode.EchConfigList.IsNullOrEmpty())
{
if (childNode.StreamSecurity == Global.StreamSecurity
&& childNode.EchConfigList?.Contains("://") == true)
{
var idx = childNode.EchConfigList.IndexOf('+');
childAddresses.Add(idx > 0 ? childNode.EchConfigList[..idx] : childNode.Sni);
}
else
{
childAddresses.Add(childNode.Sni);
}
}
else if (childNode.ConfigType.IsGroupType())
{
var subAddresses = await GetAllChildDomainAddresses(childNode.IndexId);
foreach (var addr in subAddresses)
{
childAddresses.Add(addr);
}
}
}
return childAddresses;
}
#endregion Helper
}

View file

@ -43,9 +43,12 @@ public sealed class WebDavManager
_webDir = _config.WebDavItem.DirName.TrimEx();
}
// Ensure BaseAddress URL ends with a trailing slash
var baseUrl = _config.WebDavItem.Url.Trim().TrimEnd('/') + "/";
var clientParams = new WebDavClientParams
{
BaseAddress = new Uri(_config.WebDavItem.Url),
BaseAddress = new Uri(baseUrl),
Credentials = new NetworkCredential(_config.WebDavItem.UserName, _config.WebDavItem.Password)
};
_client = new WebDavClient(clientParams);

View file

@ -8,21 +8,6 @@ public class Config
public string IndexId { get; set; }
public string SubIndexId { get; set; }
public ECoreType RunningCoreType { get; set; }
public bool IsRunningCore(ECoreType type)
{
switch (type)
{
case ECoreType.Xray when RunningCoreType is ECoreType.Xray or ECoreType.v2fly or ECoreType.v2fly_v5:
case ECoreType.sing_box when RunningCoreType is ECoreType.sing_box or ECoreType.mihomo:
return true;
default:
return false;
}
}
#endregion property
#region other entities

View file

@ -100,7 +100,6 @@ public class UIItem
public bool DoubleClick2Activate { get; set; }
public bool AutoHideStartup { get; set; }
public bool Hide2TrayWhenClose { get; set; }
public bool ShowInTaskbar { get; set; }
public bool MacOSShowInDock { get; set; }
public List<ColumnItem> MainColumnItem { get; set; }
public List<WindowSizeItem> WindowSizeItem { get; set; }

View file

@ -161,4 +161,7 @@ public class ProfileItem : ReactiveObject
public string Extra { get; set; }
public bool? MuxEnabled { get; set; }
public string Cert { get; set; }
public string CertSha { get; set; }
public string EchConfigList { get; set; }
public string EchForceQuery { get; set; }
}

View file

@ -68,6 +68,7 @@ public class Rule4Sbox
public List<string>? ip_cidr { get; set; }
public List<string>? source_ip_cidr { get; set; }
public List<string>? process_name { get; set; }
public List<string>? process_path { get; set; }
public List<string>? rule_set { get; set; }
public List<Rule4Sbox>? rules { get; set; }
public string? action { get; set; }
@ -182,6 +183,14 @@ public class Tls4Sbox
public string? fragment_fallback_delay { get; set; }
public bool? record_fragment { get; set; }
public List<string>? certificate { get; set; }
public Ech4Sbox? ech { get; set; }
}
public class Ech4Sbox
{
public bool enabled { get; set; }
public List<string>? config { get; set; }
public string? query_server_name { get; set; }
}
public class Multiplex4Sbox

View file

@ -128,7 +128,8 @@ public class Outboundsettings4Ray
public string? secretKey { get; set; }
public List<string>? address { get; set; }
public Object? address { get; set; }
public int? port { get; set; }
public List<WireguardPeer4Ray>? peers { get; set; }
@ -139,6 +140,8 @@ public class Outboundsettings4Ray
public List<int>? reserved { get; set; }
public int? workers { get; set; }
public int? version { get; set; }
}
public class WireguardPeer4Ray
@ -256,6 +259,8 @@ public class RulesItem4Ray
public List<string>? domain { get; set; }
public List<string>? protocol { get; set; }
public List<string>? process { get; set; }
}
public class BalancersItem4Ray
@ -336,6 +341,10 @@ public class StreamSettings4Ray
public GrpcSettings4Ray? grpcSettings { get; set; }
public HysteriaSettings4Ray? hysteriaSettings { get; set; }
public List<UdpMasks4Ray>? udpmasks { get; set; }
public Sockopt4Ray? sockopt { get; set; }
}
@ -355,7 +364,10 @@ public class TlsSettings4Ray
public string? spiderX { get; set; }
public string? mldsa65Verify { get; set; }
public List<CertificateSettings4Ray>? certificates { get; set; }
public string? pinnedPeerCertSha256 { get; set; }
public bool? disableSystemRoot { get; set; }
public string? echConfigList { get; set; }
public string? echForceQuery { get; set; }
}
public class CertificateSettings4Ray
@ -457,6 +469,32 @@ public class GrpcSettings4Ray
public int? initial_windows_size { get; set; }
}
public class HysteriaSettings4Ray
{
public int version { get; set; }
public string? auth { get; set; }
public string? up { get; set; }
public string? down { get; set; }
public HysteriaUdpHop4Ray? udphop { get; set; }
}
public class HysteriaUdpHop4Ray
{
public string? ports { get; set; }
public int? interval { get; set; }
}
public class UdpMasks4Ray
{
public string type { get; set; }
public UdpMasksSettings4Ray? settings { get; set; }
}
public class UdpMasksSettings4Ray
{
public string? password { get; set; }
}
public class AccountsItem4Ray
{
public string user { get; set; }

View file

@ -19,7 +19,7 @@ namespace ServiceLib.Resx {
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class ResUI {
@ -2617,7 +2617,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Server Certificate (PEM format, optional)
/// 查找类似 Pinned certificate (fill in either one)
///When specified, the certificate will be pinned, and &quot;Allow Insecure&quot; will be disabled.
///
///The &quot;Get Certificate&quot; action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. 的本地化字符串。
@ -2628,6 +2628,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Certificate fingerprint (SHA-256) 的本地化字符串。
/// </summary>
public static string TbCertSha256Tips {
get {
return ResourceManager.GetString("TbCertSha256Tips", resourceCulture);
}
}
/// <summary>
/// 查找类似 Clear system proxy 的本地化字符串。
/// </summary>
@ -2790,6 +2799,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 EchConfigList 的本地化字符串。
/// </summary>
public static string TbEchConfigList {
get {
return ResourceManager.GetString("TbEchConfigList", resourceCulture);
}
}
/// <summary>
/// 查找类似 EchForceQuery 的本地化字符串。
/// </summary>
public static string TbEchForceQuery {
get {
return ResourceManager.GetString("TbEchForceQuery", resourceCulture);
}
}
/// <summary>
/// 查找类似 Edit 的本地化字符串。
/// </summary>
@ -2871,6 +2898,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Full certificate (chain), PEM format 的本地化字符串。
/// </summary>
public static string TbFullCertTips {
get {
return ResourceManager.GetString("TbFullCertTips", resourceCulture);
}
}
/// <summary>
/// 查找类似 This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core&apos;s basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you. 的本地化字符串。
/// </summary>
@ -3250,7 +3286,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Full process name (Tun mode) 的本地化字符串。
/// 查找类似 Process (Tun mode) 的本地化字符串。
/// </summary>
public static string TbRoutingRuleProcess {
get {

View file

@ -1027,7 +1027,7 @@
<value>پروتکل sing-box Mux</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>نام کامل فرانید (حالت Tun)</value>
<value>Process (Tun mode)</value>
</data>
<data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP or IP CIDR</value>
@ -1641,4 +1641,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuServerList2" xml:space="preserve">
<value>Configuration Item 2, Select and add from self-built</value>
</data>
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root>

View file

@ -1024,7 +1024,7 @@
<value>Protocole de multiplexage Mux (sing-box)</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Nom complet du processus (mode Tun)</value>
<value>Process (Tun mode)</value>
</data>
<data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP ou IP CIDR</value>
@ -1606,10 +1606,10 @@
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Certificat serveur (format PEM, facultatif)
Si le certificat est défini, il est fixé et loption « Ignorer la vérification » est désactivée.
<value>Pinned certificate (fill in either one)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
Si un certificat auto-signé est utilisé ou si le système contient une CA non fiable ou malveillante, laction « Obtenir le certificat » peut échouer.</value>
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>Obtenir le certificat</value>
@ -1636,6 +1636,18 @@ Si un certificat auto-signé est utilisé ou si le système contient une CA non
<value>Afficher dans le Dock de macOS (redém. requis)</value>
</data>
<data name="menuServerList2" xml:space="preserve">
<value>Configuration Item 2, Select and add from self-built</value>
<value>Élément de config 2 : choisir et ajouter depuis self-hosted</value>
</data>
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root>

View file

@ -1027,7 +1027,7 @@
<value>sing-box Mux protokoll</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Teljes folyamatnév (Tun mód)</value>
<value>Process (Tun mode)</value>
</data>
<data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP vagy IP CIDR</value>
@ -1609,7 +1609,7 @@
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional)
<value>Pinned certificate (fill in either one)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
@ -1641,4 +1641,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuServerList2" xml:space="preserve">
<value>Configuration Item 2, Select and add from self-built</value>
</data>
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root>

View file

@ -1027,7 +1027,7 @@
<value>sing-box Mux Protocol</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Full process name (Tun mode)</value>
<value>Process (Tun mode)</value>
</data>
<data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP or IP CIDR</value>
@ -1609,7 +1609,7 @@
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional)
<value>Pinned certificate (fill in either one)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
@ -1641,4 +1641,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuServerList2" xml:space="preserve">
<value>Configuration Item 2, Select and add from self-built</value>
</data>
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root>

View file

@ -1027,7 +1027,7 @@
<value>Протокол Mux для sing-box</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Полное имя процесса (режим TUN)</value>
<value>Process (Tun mode)</value>
</data>
<data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP-адрес или сеть CIDR</value>
@ -1609,7 +1609,7 @@
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional)
<value>Pinned certificate (fill in either one)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
@ -1641,4 +1641,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuServerList2" xml:space="preserve">
<value>Configuration Item 2, Select and add from self-built</value>
</data>
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root>

View file

@ -1024,7 +1024,7 @@
<value>sing-box Mux 多路复用协议</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>进程名全称 (Tun 模式)</value>
<value>进程 (Tun 模式)</value>
</data>
<data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP 或 IP CIDR</value>
@ -1606,10 +1606,10 @@
<value>固定证书</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>服务器证书PEM 格式,可选
<value>固定证书(二选一填写即可
当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。
“获取证书”操作可能失败,原因可能是使用了自签证书,或系统中存在不受信任或恶意的 CA。</value>
“获取证书”操作可能失败,原因包括使用了自签名证书,或系统中存在不受信任甚至恶意的 CA。</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>获取证书</value>
@ -1638,4 +1638,16 @@
<data name="menuServerList2" xml:space="preserve">
<value>子配置项二,从自建中选择添加</value>
</data>
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>完整证书PEM 格式</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>证书指纹SHA-256</value>
</data>
</root>

View file

@ -1024,7 +1024,7 @@
<value>sing-box Mux 多路復用協定</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>行程名全稱 (Tun 模式)</value>
<value>行程 (Tun 模式)</value>
</data>
<data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP 或 IP CIDR</value>
@ -1606,7 +1606,7 @@
<value>憑證綁定</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>伺服器憑證PEM 格式,可選
<value>固定憑證(二選一填寫即可
若已指定,憑證將會被綁定,並且「跳過憑證驗證」將被停用。
若使用自簽憑證,或系統中存在不受信任或惡意的 CA「取得憑證」動作可能會失敗。</value>
@ -1638,4 +1638,16 @@
<data name="menuServerList2" xml:space="preserve">
<value>子配置項二,從自建中選擇新增</value>
</data>
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root>

View file

@ -371,7 +371,7 @@ public partial class CoreConfigSingboxService(Config config)
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(null, singboxConfig);
await GenDns(parentNode, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
ret.Success = true;
@ -428,7 +428,7 @@ public partial class CoreConfigSingboxService(Config config)
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(null, singboxConfig);
await GenDns(parentNode, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
ret.Success = true;

View file

@ -13,8 +13,8 @@ public partial class CoreConfigSingboxService
}
var simpleDNSItem = _config.SimpleDNSItem;
await GenDnsServers(singboxConfig, simpleDNSItem);
await GenDnsRules(singboxConfig, simpleDNSItem);
await GenDnsServers(node, singboxConfig, simpleDNSItem);
await GenDnsRules(node, singboxConfig, simpleDNSItem);
singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.independent_cache = true;
@ -52,7 +52,7 @@ public partial class CoreConfigSingboxService
return 0;
}
private async Task<int> GenDnsServers(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem)
private async Task<int> GenDnsServers(ProfileItem? node, SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem)
{
var finalDns = await GenDnsDomains(singboxConfig, simpleDNSItem);
@ -133,6 +133,29 @@ public partial class CoreConfigSingboxService
singboxConfig.dns.servers.Add(fakeip);
}
// ech
var (_, dnsServer) = ParseEchParam(node?.EchConfigList);
if (dnsServer is not null)
{
dnsServer.tag = Global.SingboxEchDNSTag;
if (dnsServer.server is not null
&& hostsDns.predefined.ContainsKey(dnsServer.server))
{
dnsServer.domain_resolver = Global.SingboxHostsDNSTag;
}
else
{
dnsServer.domain_resolver = Global.SingboxLocalDNSTag;
}
singboxConfig.dns.servers.Add(dnsServer);
}
else if (node?.ConfigType.IsGroupType() == true)
{
var echDnsObject = JsonUtils.DeepCopy(directDns);
echDnsObject.tag = Global.SingboxEchDNSTag;
singboxConfig.dns.servers.Add(echDnsObject);
}
return await Task.FromResult(0);
}
@ -146,7 +169,7 @@ public partial class CoreConfigSingboxService
return await Task.FromResult(finalDns);
}
private async Task<int> GenDnsRules(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem)
private async Task<int> GenDnsRules(ProfileItem? node, SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem)
{
singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
@ -157,17 +180,42 @@ public partial class CoreConfigSingboxService
new Rule4Sbox
{
server = Global.SingboxRemoteDNSTag,
strategy = simpleDNSItem.SingboxStrategy4Proxy.IsNullOrEmpty() ? null : simpleDNSItem.SingboxStrategy4Proxy,
strategy = simpleDNSItem.SingboxStrategy4Proxy.NullIfEmpty(),
clash_mode = ERuleMode.Global.ToString()
},
new Rule4Sbox
{
server = Global.SingboxDirectDNSTag,
strategy = simpleDNSItem.SingboxStrategy4Direct.IsNullOrEmpty() ? null : simpleDNSItem.SingboxStrategy4Direct,
strategy = simpleDNSItem.SingboxStrategy4Direct.NullIfEmpty(),
clash_mode = ERuleMode.Direct.ToString()
}
});
var (ech, _) = ParseEchParam(node?.EchConfigList);
if (ech is not null)
{
var echDomain = ech.query_server_name ?? node?.Sni;
singboxConfig.dns.rules.Add(new()
{
query_type = new List<int> { 64, 65 },
server = Global.SingboxEchDNSTag,
domain = echDomain is not null ? new List<string> { echDomain } : null,
});
}
else if (node?.ConfigType.IsGroupType() == true)
{
var queryServerNames = (await ProfileGroupItemManager.GetAllChildEchQuerySni(node.IndexId)).ToList();
if (queryServerNames.Count > 0)
{
singboxConfig.dns.rules.Add(new()
{
query_type = new List<int> { 64, 65 },
server = Global.SingboxEchDNSTag,
domain = queryServerNames,
});
}
}
if (simpleDNSItem.BlockBindingQuery == true)
{
singboxConfig.dns.rules.Add(new()

View file

@ -10,7 +10,7 @@ public partial class CoreConfigSingboxService
singboxConfig.inbounds = [];
if (!_config.TunModeItem.EnableTun
|| (_config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && _config.RunningCoreType == ECoreType.sing_box))
|| (_config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && AppManager.Instance.RunningCoreType == ECoreType.sing_box))
{
var inbound = new Inbound4Sbox()
{

View file

@ -46,7 +46,10 @@ public partial class CoreConfigSingboxService
{
pluginArgs += "mode=websocket;";
pluginArgs += $"host={node.RequestHost};";
pluginArgs += $"path={node.Path};";
// https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172
// Equal signs and commas [and backslashes] must be escaped with a backslash.
var path = node.Path.Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,");
pluginArgs += $"path={path};";
}
else if (node.Network == nameof(ETransport.quic))
{
@ -64,8 +67,6 @@ public partial class CoreConfigSingboxService
var base64Content = cert.Replace(beginMarker, "").Replace(endMarker, "").Trim();
// https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172
// Equal signs and commas [and backslashes] must be escaped with a backslash.
base64Content = base64Content.Replace("=", "\\=");
pluginArgs += $"certRaw={base64Content};";
@ -74,6 +75,9 @@ public partial class CoreConfigSingboxService
if (pluginArgs.Length > 0)
{
outbound.plugin = "v2ray-plugin";
pluginArgs += "mux=0;";
// pluginStr remove last ';'
pluginArgs = pluginArgs[..^1];
outbound.plugin_opts = pluginArgs;
}
}
@ -330,6 +334,11 @@ public partial class CoreConfigSingboxService
};
tls.insecure = false;
}
var (ech, _) = ParseEchParam(node.EchConfigList);
if (ech is not null)
{
tls.ech = ech;
}
outbound.tls = tls;
}
catch (Exception ex)
@ -350,7 +359,7 @@ public partial class CoreConfigSingboxService
case nameof(ETransport.h2):
transport.type = nameof(ETransport.http);
transport.host = node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(node.RequestHost);
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
transport.path = node.Path.NullIfEmpty();
break;
case nameof(ETransport.tcp): //http
@ -358,7 +367,7 @@ public partial class CoreConfigSingboxService
{
transport.type = nameof(ETransport.http);
transport.host = node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(node.RequestHost);
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
transport.path = node.Path.NullIfEmpty();
}
break;
@ -392,7 +401,7 @@ public partial class CoreConfigSingboxService
}
}
transport.path = wsPath.IsNullOrEmpty() ? null : wsPath;
transport.path = wsPath.NullIfEmpty();
if (node.RequestHost.IsNotEmpty())
{
transport.headers = new()
@ -404,8 +413,8 @@ public partial class CoreConfigSingboxService
case nameof(ETransport.httpupgrade):
transport.type = nameof(ETransport.httpupgrade);
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
transport.host = node.RequestHost.IsNullOrEmpty() ? null : node.RequestHost;
transport.path = node.Path.NullIfEmpty();
transport.host = node.RequestHost.NullIfEmpty();
break;
@ -900,4 +909,31 @@ public partial class CoreConfigSingboxService
}
return await Task.FromResult(0);
}
private (Ech4Sbox? ech, Server4Sbox? dnsServer) ParseEchParam(string? echConfig)
{
if (echConfig.IsNullOrEmpty())
{
return (null, null);
}
if (!echConfig.Contains("://"))
{
return (new Ech4Sbox()
{
enabled = true,
config = [$"-----BEGIN ECH CONFIGS-----\n" +
$"{echConfig}\n" +
$"-----END ECH CONFIGS-----"],
}, null);
}
var idx = echConfig.IndexOf('+');
// NOTE: query_server_name, since sing-box 1.13.0
//var queryServerName = idx > 0 ? echConfig[..idx] : null;
var echDnsServer = idx > 0 ? echConfig[(idx + 1)..] : echConfig;
return (new Ech4Sbox()
{
enabled = true,
query_server_name = null,
}, ParseDnsAddress(echDnsServer));
}
}

View file

@ -113,7 +113,7 @@ public partial class CoreConfigSingboxService
clash_mode = ERuleMode.Global.ToString()
});
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.IsNullOrEmpty() ? null : _config.RoutingBasicItem.DomainStrategy4Singbox;
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty();
var defaultRouting = await ConfigHandler.GetDefaultRouting(_config);
if (defaultRouting.DomainStrategy4Singbox.IsNotEmpty())
{
@ -280,11 +280,51 @@ public partial class CoreConfigSingboxService
if (_config.TunModeItem.EnableTun && item.Process?.Count > 0)
{
rule3.process_name = item.Process;
rules.Add(rule3);
var ruleProcName = JsonUtils.DeepCopy(rule3);
var ruleProcPath = JsonUtils.DeepCopy(rule3);
foreach (var process in item.Process)
{
// sing-box doesn't support this, fall back to process name match
if (process is "self/" or "xray/")
{
ruleProcName.process_name.Add(Utils.GetExeName("sing-box"));
continue;
}
if (process.Contains('/') || process.Contains('\\'))
{
var procPath = process;
if (Utils.IsWindows())
{
procPath = procPath.Replace('/', '\\');
}
ruleProcPath.process_path.Add(procPath);
continue;
}
// sing-box strictly matches the exe suffix on Windows
var procName = process;
if (Utils.IsWindows() && !procName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
procName += ".exe";
}
ruleProcName.process_name.Add(procName);
}
if (ruleProcName.process_name.Count > 0)
{
rules.Add(ruleProcName);
hasDomainIp = true;
}
if (ruleProcPath.process_path.Count > 0)
{
rules.Add(ruleProcPath);
hasDomainIp = true;
}
}
if (!hasDomainIp
&& (rule.port != null || rule.port_range != null || rule.protocol != null || rule.inbound != null || rule.network != null))
{

View file

@ -197,7 +197,7 @@ public partial class CoreConfigV2rayService
if (item.OutboundTag == Global.DirectTag)
{
if (normalizedDomain.StartsWith("geosite:"))
if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:"))
{
(regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain);
}
@ -208,7 +208,7 @@ public partial class CoreConfigV2rayService
}
else if (item.OutboundTag != Global.BlockTag)
{
if (normalizedDomain.StartsWith("geosite:"))
if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:"))
{
proxyGeositeList.Add(normalizedDomain);
}

View file

@ -178,6 +178,18 @@ public partial class CoreConfigV2rayService
outbound.settings.vnext = null;
break;
}
case EConfigType.Hysteria2:
{
outbound.settings = new()
{
version = 2,
address = node.Address,
port = node.Port,
};
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
case EConfigType.WireGuard:
{
var address = node.Address;
@ -206,6 +218,10 @@ public partial class CoreConfigV2rayService
}
outbound.protocol = Global.ProtocolTypes[node.ConfigType];
if (node.ConfigType == EConfigType.Hysteria2)
{
outbound.protocol = "hysteria";
}
await GenBoundStreamSettings(node, outbound);
}
catch (Exception ex)
@ -246,7 +262,12 @@ public partial class CoreConfigV2rayService
try
{
var streamSettings = outbound.streamSettings;
streamSettings.network = node.GetNetwork();
var network = node.GetNetwork();
if (node.ConfigType == EConfigType.Hysteria2)
{
network = "hysteria";
}
streamSettings.network = network;
var host = node.RequestHost.TrimEx();
var path = node.Path.TrimEx();
var sni = node.Sni.TrimEx();
@ -272,7 +293,9 @@ public partial class CoreConfigV2rayService
{
allowInsecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure),
alpn = node.GetAlpn(),
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint,
echConfigList = node.EchConfigList.NullIfEmpty(),
echForceQuery = node.EchForceQuery.NullIfEmpty()
};
if (sni.IsNotEmpty())
{
@ -299,6 +322,10 @@ public partial class CoreConfigV2rayService
tlsSettings.disableSystemRoot = true;
tlsSettings.allowInsecure = false;
}
else if (!node.CertSha.IsNullOrEmpty())
{
tlsSettings.pinnedPeerCertSha256 = node.CertSha;
}
streamSettings.tlsSettings = tlsSettings;
}
@ -322,7 +349,7 @@ public partial class CoreConfigV2rayService
}
//streamSettings
switch (node.GetNetwork())
switch (network)
{
case nameof(ETransport.kcp):
KcpSettings4Ray kcpSettings = new()
@ -340,7 +367,7 @@ public partial class CoreConfigV2rayService
kcpSettings.header = new Header4Ray
{
type = node.HeaderType,
domain = host.IsNullOrEmpty() ? null : host
domain = host.NullIfEmpty()
};
if (path.IsNotEmpty())
{
@ -450,7 +477,7 @@ public partial class CoreConfigV2rayService
case nameof(ETransport.grpc):
GrpcSettings4Ray grpcSettings = new()
{
authority = host.IsNullOrEmpty() ? null : host,
authority = host.NullIfEmpty(),
serviceName = path,
multiMode = node.HeaderType == Global.GrpcMultiMode,
idle_timeout = _config.GrpcItem.IdleTimeout,
@ -461,6 +488,35 @@ public partial class CoreConfigV2rayService
streamSettings.grpcSettings = grpcSettings;
break;
case "hysteria":
HysteriaUdpHop4Ray? udpHop = null;
if (node.Ports.IsNotEmpty() &&
(node.Ports.Contains(':') || node.Ports.Contains('-') || node.Ports.Contains(',')))
{
udpHop = new()
{
ports = node.Ports.Replace(':', '-'),
interval = _config.HysteriaItem.HopInterval > 0
? _config.HysteriaItem.HopInterval
: null,
};
}
HysteriaSettings4Ray hysteriaSettings = new()
{
version = 2,
auth = node.Id,
up = _config.HysteriaItem.UpMbps > 0 ? $"{_config.HysteriaItem.UpMbps}mbps" : null,
down = _config.HysteriaItem.DownMbps > 0 ? $"{_config.HysteriaItem.DownMbps}mbps" : null,
udphop = udpHop,
};
streamSettings.hysteriaSettings = hysteriaSettings;
if (node.Path.IsNotEmpty())
{
streamSettings.udpmasks =
[new() { type = "salamander", settings = new() { password = node.Path.TrimEx(), } }];
}
break;
default:
//tcp
if (node.HeaderType == Global.TcpHeaderHttp)
@ -564,7 +620,7 @@ public partial class CoreConfigV2rayService
var fragmentOutbound = new Outbounds4Ray
{
protocol = "freedom",
tag = $"{Global.ProxyTag}3",
tag = $"frag-{Global.ProxyTag}",
settings = new()
{
fragment = new()

View file

@ -77,12 +77,17 @@ public partial class CoreConfigV2rayService
{
rule.inboundTag = null;
}
if (rule.process?.Count == 0)
{
rule.process = null;
}
var hasDomainIp = false;
if (rule.domain?.Count > 0)
{
var it = JsonUtils.DeepCopy(rule);
it.ip = null;
it.process = null;
it.type = "field";
for (var k = it.domain.Count - 1; k >= 0; k--)
{
@ -99,6 +104,16 @@ public partial class CoreConfigV2rayService
{
var it = JsonUtils.DeepCopy(rule);
it.domain = null;
it.process = null;
it.type = "field";
v2rayConfig.routing.rules.Add(it);
hasDomainIp = true;
}
if (_config.TunModeItem.EnableTun && rule.process?.Count > 0)
{
var it = JsonUtils.DeepCopy(rule);
it.domain = null;
it.ip = null;
it.type = "field";
v2rayConfig.routing.rules.Add(it);
hasDomainIp = true;

View file

@ -198,6 +198,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
if (!it.AllowTest)
{
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
continue;
}

View file

@ -61,7 +61,7 @@ public class StatisticsSingboxService
await Task.Delay(1000);
try
{
if (!_config.IsRunningCore(ECoreType.sing_box))
if (!AppManager.Instance.IsRunningCore(ECoreType.sing_box))
{
continue;
}

View file

@ -30,7 +30,7 @@ public class StatisticsXrayService
await Task.Delay(1000);
try
{
if (_config.RunningCoreType != ECoreType.Xray)
if (AppManager.Instance.RunningCoreType != ECoreType.Xray)
{
continue;
}

View file

@ -291,13 +291,6 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
return url;
}
//Check for standalone windows .Net version
if (File.Exists(Path.Combine(Utils.GetBaseDirectory(), "wpfgfx_cor3.dll"))
&& File.Exists(Path.Combine(Utils.GetBaseDirectory(), "D3DCompiler_47_cor3.dll")))
{
return url?.Replace(".zip", "-SelfContained.zip");
}
//Check for avalonia desktop windows version
if (File.Exists(Path.Combine(Utils.GetBaseDirectory(), "libHarfBuzzSharp.dll")))
{

View file

@ -14,6 +14,9 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public string CertTip { get; set; }
[Reactive]
public string CertSha { get; set; }
public ReactiveCommand<Unit, Unit> FetchCertCmd { get; }
public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
@ -39,6 +42,12 @@ public class AddServerViewModel : MyReactiveObject
this.WhenAnyValue(x => x.Cert)
.Subscribe(_ => UpdateCertTip());
this.WhenAnyValue(x => x.CertSha)
.Subscribe(_ => UpdateCertTip());
this.WhenAnyValue(x => x.Cert)
.Subscribe(_ => UpdateCertSha());
if (profileItem.IndexId.IsNullOrEmpty())
{
profileItem.Network = Global.DefaultNetwork;
@ -97,7 +106,8 @@ public class AddServerViewModel : MyReactiveObject
}
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
SelectedSource.Cert = Cert.IsNullOrEmpty() ? null : Cert;
SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert;
SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha;
if (await ConfigHandler.AddServer(_config, SelectedSource) == 0)
{
@ -113,10 +123,36 @@ public class AddServerViewModel : MyReactiveObject
private void UpdateCertTip(string? errorMessage = null)
{
CertTip = errorMessage.IsNullOrEmpty()
? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet)
? ((Cert.IsNullOrEmpty() && CertSha.IsNullOrEmpty()) ? ResUI.CertNotSet : ResUI.CertSet)
: errorMessage;
}
private void UpdateCertSha()
{
if (Cert.IsNullOrEmpty())
{
return;
}
var certList = CertPemManager.ParsePemChain(Cert);
if (certList.Count == 0)
{
return;
}
List<string> shaList = new();
foreach (var cert in certList)
{
var sha = CertPemManager.GetCertSha256Thumbprint(cert);
if (sha.IsNullOrEmpty())
{
return;
}
shaList.Add(sha);
}
CertSha = string.Join('~', shaList);
}
private async Task FetchCert()
{
if (SelectedSource.StreamSecurity != Global.StreamSecurity)
@ -142,8 +178,8 @@ public class AddServerViewModel : MyReactiveObject
{
domain += $":{SelectedSource.Port}";
}
string certError;
(Cert, certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName);
(Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName);
UpdateCertTip(certError);
}
@ -172,8 +208,8 @@ public class AddServerViewModel : MyReactiveObject
{
domain += $":{SelectedSource.Port}";
}
string certError;
(var certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName);
var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName);
Cert = CertPemManager.ConcatenatePemChain(certs);
UpdateCertTip(certError);
}

View file

@ -128,7 +128,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
{
await Task.Delay(1000 * 5);
numOfExecuted++;
if (!(AutoRefresh && _config.UiItem.ShowInTaskbar && _config.IsRunningCore(ECoreType.sing_box)))
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}

View file

@ -437,7 +437,7 @@ public class ClashProxiesViewModel : MyReactiveObject
{
await Task.Delay(1000 * 60);
numOfExecuted++;
if (!(AutoRefresh && _config.UiItem.ShowInTaskbar && _config.IsRunningCore(ECoreType.sing_box)))
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}

View file

@ -253,7 +253,7 @@ public class MainWindowViewModel : MyReactiveObject
private async Task Init()
{
_config.UiItem.ShowInTaskbar = true;
AppManager.Instance.ShowInTaskbar = true;
//await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinDNS(_config);
@ -306,7 +306,7 @@ public class MainWindowViewModel : MyReactiveObject
private async Task UpdateStatisticsHandler(ServerSpeedItem update)
{
if (!_config.UiItem.ShowInTaskbar)
if (!AppManager.Instance.ShowInTaskbar)
{
return;
}
@ -560,7 +560,7 @@ public class MainWindowViewModel : MyReactiveObject
});
AppEvents.TestServerRequested.Publish();
var showClashUI = _config.IsRunningCore(ECoreType.sing_box);
var showClashUI = AppManager.Instance.IsRunningCore(ECoreType.sing_box);
if (showClashUI)
{
AppEvents.ProxiesReloadRequested.Publish();

View file

@ -44,7 +44,7 @@ public class MsgViewModel : MyReactiveObject
EnqueueQueueMsg(msg);
if (!_config.UiItem.ShowInTaskbar)
if (!AppManager.Instance.ShowInTaskbar)
{
return;
}

View file

@ -108,6 +108,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public string CoreType4 { get; set; }
[Reactive] public string CoreType5 { get; set; }
[Reactive] public string CoreType6 { get; set; }
[Reactive] public string CoreType7 { get; set; }
[Reactive] public string CoreType9 { get; set; }
#endregion CoreType
@ -276,6 +277,10 @@ public class OptionSettingViewModel : MyReactiveObject
CoreType6 = type;
break;
case 7:
CoreType7 = type;
break;
case 9:
CoreType9 = type;
break;
@ -427,6 +432,10 @@ public class OptionSettingViewModel : MyReactiveObject
type = CoreType6;
break;
case 7:
type = CoreType7;
break;
case 9:
type = CoreType9;
break;

View file

@ -57,6 +57,7 @@ public class ProfilesViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> MoveUpCmd { get; }
public ReactiveCommand<Unit, Unit> MoveDownCmd { get; }
public ReactiveCommand<Unit, Unit> MoveBottomCmd { get; }
public ReactiveCommand<SubItem, Unit> MoveToGroupCmd { get; }
//servers ping
public ReactiveCommand<Unit, Unit> MixedTestServerCmd { get; }
@ -179,6 +180,10 @@ public class ProfilesViewModel : MyReactiveObject
{
await MoveServer(EMove.Bottom);
}, canEditRemove);
MoveToGroupCmd = ReactiveCommand.CreateFromTask<SubItem>(async sub =>
{
SelectedMoveToGroup = sub;
});
//servers ping
FastRealPingCmd = ReactiveCommand.CreateFromTask(async () =>

View file

@ -549,7 +549,7 @@ public class StatusBarViewModel : MyReactiveObject
try
{
if (_config.IsRunningCore(ECoreType.sing_box))
if (AppManager.Instance.IsRunningCore(ECoreType.sing_box))
{
SpeedProxyDisplay = string.Format(ResUI.SpeedDisplayText, EInboundProtocol.mixed, Utils.HumanFy(update.ProxyUp), Utils.HumanFy(update.ProxyDown));
SpeedDirectDisplay = string.Empty;

View file

@ -713,7 +713,7 @@
Grid.Row="7"
ColumnDefinitions="300,Auto"
IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="1"
@ -768,15 +768,41 @@
Width="200"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="5"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbEchConfigList}" />
<TextBox
x:Name="txtEchConfigList"
Grid.Row="5"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="6"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbEchForceQuery}" />
<ComboBox
x:Name="cmbEchForceQuery"
Grid.Row="6"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="7"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertPinning}" />
<StackPanel
Grid.Row="5"
Grid.Row="7"
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal">
@ -815,6 +841,23 @@
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbFetchCertChain}" />
</StackPanel>
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertSha256Tips}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtCertSha256Pinning"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbFullCertTips}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtCert"
Width="400"

View file

@ -28,6 +28,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
cmbFingerprint2.ItemsSource = Global.Fingerprints;
cmbAllowInsecure.ItemsSource = Global.AllowInsecure;
cmbAlpn.ItemsSource = Global.Alpns;
cmbEchForceQuery.ItemsSource = Global.EchForceQuerys;
var lstStreamSecurity = new List<string>();
lstStreamSecurity.Add(string.Empty);
@ -74,7 +75,6 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
gridHysteria2.IsVisible = true;
sepa2.IsVisible = false;
gridTransport.IsVisible = false;
cmbCoreType.IsEnabled = false;
cmbFingerprint.IsEnabled = false;
cmbFingerprint.SelectedValue = string.Empty;
break;
@ -185,8 +185,12 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).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.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.EchForceQuery, v => v.cmbEchForceQuery.SelectedValue).DisposeWith(disposables);
//reality
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables);

View file

@ -11,7 +11,7 @@
Title="v2rayN"
Width="1200"
Height="800"
MinWidth="900"
MinWidth="600"
x:DataType="vms:MainWindowViewModel"
Icon="/Assets/NotifyIcon1.ico"
ShowInTaskbar="True"
@ -43,11 +43,11 @@
<MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" />
<MenuItem x:Name="menuAddShadowsocksServer" Header="{x:Static resx:ResUI.menuAddShadowsocksServer}" />
<MenuItem x:Name="menuAddTrojanServer" Header="{x:Static resx:ResUI.menuAddTrojanServer}" />
<MenuItem x:Name="menuAddHysteria2Server" Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem x:Name="menuAddWireguardServer" Header="{x:Static resx:ResUI.menuAddWireguardServer}" />
<MenuItem x:Name="menuAddSocksServer" Header="{x:Static resx:ResUI.menuAddSocksServer}" />
<MenuItem x:Name="menuAddHttpServer" Header="{x:Static resx:ResUI.menuAddHttpServer}" />
<Separator />
<MenuItem x:Name="menuAddHysteria2Server" Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem x:Name="menuAddTuicServer" Header="{x:Static resx:ResUI.menuAddTuicServer}" />
<MenuItem x:Name="menuAddAnytlsServer" Header="{x:Static resx:ResUI.menuAddAnytlsServer}" />
</MenuItem>

View file

@ -409,8 +409,8 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
{
var bl = blShow ??
(Utils.IsLinux()
? (!_config.UiItem.ShowInTaskbar ^ (WindowState == WindowState.Minimized))
: !_config.UiItem.ShowInTaskbar);
? (!AppManager.Instance.ShowInTaskbar ^ (WindowState == WindowState.Minimized))
: !AppManager.Instance.ShowInTaskbar);
if (bl)
{
Show();
@ -436,7 +436,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
Hide();
}
_config.UiItem.ShowInTaskbar = bl;
AppManager.Instance.ShowInTaskbar = bl;
}
protected override void OnLoaded(object? sender, RoutedEventArgs e)

View file

@ -477,7 +477,7 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
IsVisible="{Binding BlIsIsMacOS}"/>
IsVisible="{Binding BlIsIsMacOS}" />
<TextBlock
Grid.Row="11"
@ -876,7 +876,7 @@
<Grid
Margin="{StaticResource Margin4}"
ColumnDefinitions="Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="1"
Grid.Column="0"
@ -960,10 +960,23 @@
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="Hysteria2" />
<ComboBox
x:Name="cmbCoreType7"
Grid.Row="7"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="8"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="Wireguard" />
<ComboBox
x:Name="cmbCoreType9"
Grid.Row="7"
Grid.Row="8"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />

View file

@ -41,6 +41,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbCoreType4.ItemsSource = Global.CoreTypes;
cmbCoreType5.ItemsSource = Global.CoreTypes;
cmbCoreType6.ItemsSource = Global.CoreTypes;
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
@ -122,6 +123,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.CoreType4, v => v.cmbCoreType4.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType5, v => v.cmbCoreType5.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType6, v => v.cmbCoreType6.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType7, v => v.cmbCoreType7.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType9, v => v.cmbCoreType9.SelectedValue).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);

View file

@ -7,6 +7,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
x:Name="Root"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="vms:ProfilesViewModel"
@ -141,19 +142,18 @@
InputGesture="Ctrl+T" />
<MenuItem x:Name="menuSortServerResult" Header="{x:Static resx:ResUI.menuSortServerResult}" />
<Separator />
<MenuItem x:Name="menuMoveToGroup" Header="{x:Static resx:ResUI.menuMoveToGroup}">
<MenuItem>
<MenuItem.Header>
<DockPanel>
<ComboBox
x:Name="cmbMoveToGroup"
Width="200"
DisplayMemberBinding="{Binding Remarks}"
ItemsSource="{Binding SubItems}"
ToolTip.Tip="{x:Static resx:ResUI.menuSubscription}" />
</DockPanel>
</MenuItem.Header>
</MenuItem>
<MenuItem
x:Name="menuMoveToGroup"
Header="{x:Static resx:ResUI.menuMoveToGroup}"
ItemsSource="{Binding DataContext.SubItems, ElementName=Root}">
<MenuItem.ItemTemplate>
<DataTemplate>
<MenuItem
Command="{Binding DataContext.MoveToGroupCmd, ElementName=Root}"
CommandParameter="{Binding}"
Header="{Binding Remarks}" />
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
<MenuItem Header="{x:Static resx:ResUI.menuMoveTo}">
<MenuItem

View file

@ -69,7 +69,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
//servers move
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedMoveToGroup, v => v.cmbMoveToGroup.SelectedItem).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.SelectedMoveToGroup, v => v.cmbMoveToGroup.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);

View file

@ -40,6 +40,14 @@
<DataGrid.KeyBindings>
<KeyBinding Command="{Binding SubDeleteCmd}" Gesture="Delete" />
</DataGrid.KeyBindings>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuSubAdd2" Header="{x:Static resx:ResUI.menuSubAdd}" />
<MenuItem x:Name="menuSubDelete2" Header="{x:Static resx:ResUI.menuSubDelete}" />
<MenuItem x:Name="menuSubEdit2" Header="{x:Static resx:ResUI.menuSubEdit}" />
<MenuItem x:Name="menuSubShare2" Header="{x:Static resx:ResUI.menuSubShare}" />
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn
Width="*"

View file

@ -29,6 +29,11 @@ public partial class SubSettingWindow : WindowBase<SubSettingViewModel>
this.BindCommand(ViewModel, vm => vm.SubDeleteCmd, v => v.menuSubDelete).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubEditCmd, v => v.menuSubEdit).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubShareCmd, v => v.menuSubShare).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubAddCmd, v => v.menuSubAdd2).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubDeleteCmd, v => v.menuSubDelete2).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubEditCmd, v => v.menuSubEdit2).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubShareCmd, v => v.menuSubShare2).DisposeWith(disposables);
});
}

View file

@ -15,7 +15,7 @@
SecondaryColor="Lime" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign2.Defaults.xaml" />
</ResourceDictionary.MergedDictionaries>
<system:Double x:Key="MenuItemHeight">26</system:Double>
<system:Double x:Key="MenuItemHeight">32</system:Double>
<system:Double x:Key="StdFontSize">12</system:Double>
<system:Double x:Key="StdFontSize1">13</system:Double>
<system:Double x:Key="StdFontSize-1">11</system:Double>

View file

@ -1,6 +1,7 @@
using MaterialDesignColors;
using MaterialDesignColors.ColorManipulation;
using MaterialDesignThemes.Wpf;
using Microsoft.Win32;
namespace v2rayN.ViewModels;
@ -24,7 +25,7 @@ public class ThemeSettingViewModel : MyReactiveObject
{
_config = AppManager.Instance.Config;
RegisterSystemColorSet(_config, Application.Current.MainWindow, ModifyTheme);
RegisterSystemColorSet(_config, ModifyTheme);
BindingUI();
RestoreUI();
@ -158,25 +159,15 @@ public class ThemeSettingViewModel : MyReactiveObject
_paletteHelper.SetTheme(theme);
}
public void RegisterSystemColorSet(Config config, Window window, Action updateFunc)
public static void RegisterSystemColorSet(Config config, Action updateFunc)
{
var helper = new WindowInteropHelper(window);
var hwndSource = HwndSource.FromHwnd(helper.EnsureHandle());
hwndSource.AddHook((IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) =>
SystemEvents.UserPreferenceChanged += (s, e) =>
{
if (config.UiItem.CurrentTheme == nameof(ETheme.FollowSystem))
{
const int WM_SETTINGCHANGE = 0x001A;
if (msg == WM_SETTINGCHANGE)
{
if (wParam == IntPtr.Zero && Marshal.PtrToStringUni(lParam) == "ImmersiveColorSet")
if ((e.Category == UserPreferenceCategory.Color || e.Category == UserPreferenceCategory.General)
&& config.UiItem.CurrentTheme == nameof(ETheme.FollowSystem))
{
updateFunc?.Invoke();
}
}
}
return IntPtr.Zero;
});
};
}
}

View file

@ -929,6 +929,8 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
@ -1003,9 +1005,40 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbEchConfigList}" />
<TextBox
x:Name="txtEchConfigList"
Grid.Row="5"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="6"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbEchForceQuery}" />
<ComboBox
x:Name="cmbEchForceQuery"
Grid.Row="6"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefComboBox}" />
<TextBlock
Grid.Row="7"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCertPinning}" />
<StackPanel
Grid.Row="5"
Grid.Row="7"
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal">
@ -1039,6 +1072,26 @@
Content="{x:Static resx:ResUI.TbFetchCertChain}"
Style="{StaticResource DefButton}" />
</StackPanel>
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCertSha256Tips}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtCertSha256Pinning"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
Style="{StaticResource DefTextBox}" />
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbFullCertTips}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtCert"
Width="400"

View file

@ -23,6 +23,7 @@ public partial class AddServerWindow
cmbFingerprint2.ItemsSource = Global.Fingerprints;
cmbAllowInsecure.ItemsSource = Global.AllowInsecure;
cmbAlpn.ItemsSource = Global.Alpns;
cmbEchForceQuery.ItemsSource = Global.EchForceQuerys;
var lstStreamSecurity = new List<string>();
lstStreamSecurity.Add(string.Empty);
@ -69,7 +70,6 @@ public partial class AddServerWindow
gridHysteria2.Visibility = Visibility.Visible;
sepa2.Visibility = Visibility.Collapsed;
gridTransport.Visibility = Visibility.Collapsed;
cmbCoreType.IsEnabled = false;
cmbFingerprint.IsEnabled = false;
cmbFingerprint.Text = string.Empty;
break;
@ -180,8 +180,13 @@ public partial class AddServerWindow
this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.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.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.EchForceQuery, v => v.cmbEchForceQuery.Text).DisposeWith(disposables);
//reality
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.Text).DisposeWith(disposables);

View file

@ -13,7 +13,7 @@
Title="v2rayN"
Width="1200"
Height="800"
MinWidth="900"
MinWidth="800"
x:TypeArguments="vms:MainWindowViewModel"
Icon="/Resources/v2rayN.ico"
ResizeMode="CanResizeWithGrip"
@ -99,6 +99,10 @@
x:Name="menuAddTrojanServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddTrojanServer}" />
<MenuItem
x:Name="menuAddHysteria2Server"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem
x:Name="menuAddWireguardServer"
Height="{StaticResource MenuItemHeight}"
@ -112,10 +116,6 @@
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddHttpServer}" />
<Separator Margin="-40,5" />
<MenuItem
x:Name="menuAddHysteria2Server"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem
x:Name="menuAddTuicServer"
Height="{StaticResource MenuItemHeight}"

View file

@ -376,7 +376,7 @@ public partial class MainWindow
public void ShowHideWindow(bool? blShow)
{
var bl = blShow ?? !_config.UiItem.ShowInTaskbar;
var bl = blShow ?? !AppManager.Instance.ShowInTaskbar;
if (bl)
{
this?.Show();
@ -391,7 +391,7 @@ public partial class MainWindow
{
this?.Hide();
}
_config.UiItem.ShowInTaskbar = bl;
AppManager.Instance.ShowInTaskbar = bl;
}
protected override void OnLoaded(object? sender, RoutedEventArgs e)

View file

@ -1138,6 +1138,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -1239,10 +1240,25 @@
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="Hysteria2" />
<ComboBox
x:Name="cmbCoreType7"
Grid.Row="7"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefComboBox}" />
<TextBlock
Grid.Row="8"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="Wireguard" />
<ComboBox
x:Name="cmbCoreType9"
Grid.Row="7"
Grid.Row="8"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"

View file

@ -38,6 +38,7 @@ public partial class OptionSettingWindow
cmbCoreType4.ItemsSource = Global.CoreTypes;
cmbCoreType5.ItemsSource = Global.CoreTypes;
cmbCoreType6.ItemsSource = Global.CoreTypes;
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
@ -127,6 +128,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.CoreType4, v => v.cmbCoreType4.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType5, v => v.cmbCoreType5.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType6, v => v.cmbCoreType6.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType7, v => v.cmbCoreType7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType9, v => v.cmbCoreType9.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);

View file

@ -93,6 +93,26 @@
HeadersVisibility="Column"
IsReadOnly="True"
Style="{StaticResource DefDataGrid}">
<DataGrid.ContextMenu>
<ContextMenu Style="{StaticResource DefContextMenu}">
<MenuItem
x:Name="menuSubAdd2"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSubAdd}" />
<MenuItem
x:Name="menuSubDelete2"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSubDelete}" />
<MenuItem
x:Name="menuSubEdit2"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSubEdit}" />
<MenuItem
x:Name="menuSubShare2"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSubShare}" />
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn
Width="*"

View file

@ -25,6 +25,11 @@ public partial class SubSettingWindow
this.BindCommand(ViewModel, vm => vm.SubDeleteCmd, v => v.menuSubDelete).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubEditCmd, v => v.menuSubEdit).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubShareCmd, v => v.menuSubShare).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubAddCmd, v => v.menuSubAdd2).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubDeleteCmd, v => v.menuSubDelete2).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubEditCmd, v => v.menuSubEdit2).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SubShareCmd, v => v.menuSubShare2).DisposeWith(disposables);
});
WindowsUtils.SetDarkBorder(this, AppManager.Instance.Config.UiItem.CurrentTheme);
}