Compare commits

..

26 Commits

Author SHA1 Message Date
2f0299a32d Revert "Add patch to fix #8276"
This reverts commit d6e40fb7f5.

We do not need this manual patch as it is the part of the code now.
2025-11-10 00:01:57 +03:00
65a8bedb45 Merge tag '7.16.1' 2025-11-10 00:00:57 +03:00
2dust
d5460d758b up 7.16.1 2025-11-09 15:17:08 +08:00
2dust
6e38357b7d Add macOS Dock visibility option to settings 2025-11-09 14:47:53 +08:00
DHR60
1990850d9a Optimize Cert Pinning (#8282) 2025-11-09 11:20:30 +08:00
2dust
e6cb146671 Refactor UI platform visibility to use ViewModel properties 2025-11-09 11:11:23 +08:00
2dust
4da59cd767 Rename IsOSX to IsMacOS in Utils and usages 2025-11-09 10:52:46 +08:00
d6e40fb7f5 Add patch to fix #8276 2025-11-08 22:05:24 +03:00
4db65c2132 Merge tag '7.16.0' 2025-11-08 21:37:17 +03:00
2dust
e20c11c1a7 Refactor reload logic with semaphore for concurrency 2025-11-08 20:48:55 +08:00
2dust
a6af95e083 Bug fix
https://github.com/2dust/v2rayN/issues/8276
2025-11-08 20:10:20 +08:00
2dust
6f06b16c76 up 7.16.0 2025-11-08 11:29:18 +08:00
2dust
70ddf4ecfc Add allowInsecure and insecure to the shared URI
https://github.com/2dust/v2rayN/issues/8267
2025-11-08 11:14:01 +08:00
JieXu
187356cb9e Update ResUI.fr.resx (#8270) 2025-11-08 11:10:04 +08:00
2dust
32583ea8b3 Bug fix
Replaced direct assignments to BlReloadEnabled with a new SetReloadEnabled method that schedules updates on the main thread.
2025-11-07 21:06:43 +08:00
2dust
69797c10f2 Update ConfigHandler.cs 2025-11-07 19:52:03 +08:00
2dust
ddc8c9b1cd Add support for custom PAC and proxy script paths
Introduces options to specify custom PAC file and system proxy script paths for system proxy settings. Updates configuration models, view models, UI bindings, and logic for Linux/OSX proxy handling and PAC management to use these custom paths if provided. Also adds UI elements and localization for the new settings.
2025-11-07 19:28:16 +08:00
7f9ac74b86 Merge tag '7.15.7' 2025-11-04 19:24:19 +03:00
2dust
753e7b81b6 Add timeout and error handling to certificate fetching 2025-11-04 20:43:51 +08:00
2dust
725b094fb1 Update Directory.Packages.props 2025-11-04 20:43:28 +08:00
1cb0ef2f72 Compress deb packages better 2025-10-27 19:17:46 +03:00
8e8035af36 Package repository configuration as a package 2025-10-26 15:05:42 +03:00
77aa28f46a Merge tag '7.15.6' into stable 2025-10-26 07:35:12 +03:00
62a2558174 Change build scripts 2025-10-25 08:39:53 +03:00
45b6fe4d5a Format build scripts with shfmt 2025-10-25 03:24:28 +03:00
6b8b2d0b1b Make all build scripts executable 2025-10-24 13:36:52 +03:00
53 changed files with 964 additions and 791 deletions

View File

@@ -9,6 +9,10 @@ end_of_line = crlf
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.sh]
end_of_line = lf
indent_size = 2
[*.{yml,yaml}] [*.{yml,yaml}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

47
package-debian-repo.sh Executable file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
# Root directory = the script's location
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
Version="$1"
PackagePath="v2rayn-unofficial-repo"
mkdir -p "${PackagePath}/DEBIAN"
mkdir -p "${PackagePath}"/etc/apt/{keyrings,sources.list.d}
curl -fsSLo "${PackagePath}/etc/apt/keyrings/v2rayn-unofficial.asc" "https://git.vlyaii.ru/api/packages/voronin9032/debian/repository.key"
# basic
cat >"${PackagePath}/DEBIAN/control" <<-EOF
Package: v2rayn-unofficial-repo
Version: $Version
Maintainer: Vlyaii <voronin9032n3@gmail.com>
Homepage: https://git.vlyaii.ru/voronin9032/v2rayN
Architecture: all
Depends: ca-certificates
Description: v2rayn-unofficial repository configuration
EOF
cat >"${PackagePath}/etc/apt/sources.list.d/v2rayn-unofficial.sources" <<-EOF
Types: deb
URIs: https://git.vlyaii.ru/api/packages/voronin9032/debian
Suites: debian
Components: stable
Architectures: amd64 all
Signed-By: /etc/apt/keyrings/v2rayn-unofficial.asc
EOF
# Patch
# set owner to root:root
sudo chown -R root:root "${PackagePath}"
# set all directories to 755 (readable & traversable by all users)
sudo find "${PackagePath}/etc" -type d -exec chmod 755 {} +
# set all regular files to 644 (readable by all users)
sudo find "${PackagePath}/etc" -type f -exec chmod 644 {} +
# ensure main binaries are 755 (executable by all users)
# build deb package
sudo dpkg-deb -Zzstd -z19 --build "$PackagePath"
sudo mv "${PackagePath}.deb" "v2rayn-unofficial-repo_${Version}_all.deb"

59
package-debian.sh Normal file → Executable file
View File

@@ -1,38 +1,58 @@
#!/bin/bash #!/usr/bin/env bash
set -euo pipefail
Arch="$1" # Root directory = the script's location
OutputPath="$2" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
Version="$3" cd "$SCRIPT_DIR"
source ./utils.sh
FileName="v2rayN-${Arch}.zip" Arch="linux-64"
wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/$FileName" OutputPath="$(mktemp -d)"
7z x $FileName Version="$1"
cp -rf v2rayN-${Arch}/* $OutputPath
PackagePath="v2rayN-Package-${Arch}" PROJ="./v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj"
dotnet restore "$PROJ"
sudo rm -rf "$(dirname "$PROJ")/bin/Release/net8.0"
dotnet publish "${PROJ}" -c Release -r "linux-x64" --self-contained -p:StripSymbols=true -o "$OutputPath"
PROJ="./v2rayN/AmazTool/AmazTool.csproj"
dotnet restore "$PROJ"
sudo rm -rf "$(dirname "$PROJ")/bin/Release/net8.0"
dotnet publish "${PROJ}" -c Release -r "linux-x64" --self-contained -p:StripSymbols=true -p:PublishTrimmed=true -o "$OutputPath"
export RID_DIR="linux-x64"
download_xray "$OutputPath/bin/xray"
download_singbox "$OutputPath/bin/sing_box"
download_geo_assets "$OutputPath"
PackagePath="v2rayn-unofficial"
mkdir -p "${PackagePath}/DEBIAN" mkdir -p "${PackagePath}/DEBIAN"
mkdir -p "${PackagePath}/opt" mkdir -p "${PackagePath}/opt"
cp -rf $OutputPath "${PackagePath}/opt/v2rayN" cp -rf "$OutputPath" "${PackagePath}/opt/v2rayN"
echo "When this file exists, app will not store configs under this folder" > "${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt" echo "When this file exists, app will not store configs under this folder" >"${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt"
sudo find "${PackagePath}/opt/v2rayN" -type f -name "*.so" -exec strip {} +
if [ $Arch = "linux-64" ]; then if [ "$Arch" = "linux-64" ]; then
Arch2="amd64" Arch2="amd64"
else else
Arch2="arm64" Arch2="arm64"
fi fi
echo $Arch2
# basic # basic
cat >"${PackagePath}/DEBIAN/control" <<-EOF cat >"${PackagePath}/DEBIAN/control" <<-EOF
Package: v2rayN Package: v2rayn-unofficial
Version: $Version Version: $Version
Maintainer: Vlyaii <voronin9032n3@gmail.com>
Homepage: https://git.vlyaii.ru/voronin9032/v2rayN
Architecture: $Arch2 Architecture: $Arch2
Maintainer: https://github.com/2dust/v2rayN Replaces: v2rayn
Depends: libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1) Depends: libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1)
Breaks: v2rayn
Conflicts: v2rayn
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
EOF EOF
cat >"${PackagePath}/DEBIAN/postinst" <<-EOF cat >"${PackagePath}/DEBIAN/postinst" <<-EOF
#!/bin/sh
if [ ! -s /usr/share/applications/v2rayN.desktop ]; then if [ ! -s /usr/share/applications/v2rayN.desktop ]; then
cat >/usr/share/applications/v2rayN.desktop<<-END cat >/usr/share/applications/v2rayN.desktop<<-END
[Desktop Entry] [Desktop Entry]
@@ -65,5 +85,6 @@ sudo chmod 755 "${PackagePath}/opt/v2rayN/v2rayN" 2>/dev/null || true
sudo chmod 755 "${PackagePath}/opt/v2rayN/AmazTool" 2>/dev/null || true sudo chmod 755 "${PackagePath}/opt/v2rayN/AmazTool" 2>/dev/null || true
# build deb package # build deb package
sudo dpkg-deb -Zxz --build $PackagePath sudo dpkg-deb -Zzstd -z19 --build "$PackagePath"
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb" sudo mv "${PackagePath}.deb" "v2rayn-unofficial_${Version}_${Arch2}.deb"
sudo rm -rf "$OutputPath"

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
Arch="$1" Arch="$1"
OutputPath="$2" OutputPath="$2"
@@ -13,7 +13,7 @@ PackagePath="v2rayN-Package-${Arch}"
mkdir -p "$PackagePath/v2rayN.app/Contents/Resources" mkdir -p "$PackagePath/v2rayN.app/Contents/Resources"
cp -rf "$OutputPath" "$PackagePath/v2rayN.app/Contents/MacOS" cp -rf "$OutputPath" "$PackagePath/v2rayN.app/Contents/MacOS"
cp -f "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.icns" "$PackagePath/v2rayN.app/Contents/Resources/AppIcon.icns" cp -f "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.icns" "$PackagePath/v2rayN.app/Contents/Resources/AppIcon.icns"
echo "When this file exists, app will not store configs under this folder" > "$PackagePath/v2rayN.app/Contents/MacOS/NotStoreConfigHere.txt" echo "When this file exists, app will not store configs under this folder" >"$PackagePath/v2rayN.app/Contents/MacOS/NotStoreConfigHere.txt"
chmod +x "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN" chmod +x "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN"
cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
@@ -48,11 +48,11 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
EOF EOF
create-dmg \ create-dmg \
--volname "v2rayN Installer" \ --volname "v2rayN Installer" \
--window-size 700 420 \ --window-size 700 420 \
--icon-size 100 \ --icon-size 100 \
--icon "v2rayN.app" 160 185 \ --icon "v2rayN.app" 160 185 \
--hide-extension "v2rayN.app" \ --hide-extension "v2rayN.app" \
--app-drop-link 500 185 \ --app-drop-link 500 185 \
"v2rayN-${Arch}.dmg" \ "v2rayN-${Arch}.dmg" \
"$PackagePath/v2rayN.app" "$PackagePath/v2rayN.app"

2
package-release-zip.sh Normal file → Executable file
View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
Arch="$1" Arch="$1"
OutputPath="$2" OutputPath="$2"

665
package-rhel.sh Normal file → Executable file
View File

@@ -1,18 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# == Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian == # == Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS OR Ubuntu ==
if [[ -r /etc/os-release ]]; then if [[ -r /etc/os-release ]]; then
. /etc/os-release source /etc/os-release
case "$ID" in case "$ID" in
rhel|rocky|almalinux|fedora|centos|ubuntu|debian) rhel | rocky | almalinux | fedora | centos | ubuntu)
echo "[OK] Detected supported system: $NAME $VERSION_ID" echo "[OK] Detected supported system: $NAME $VERSION_ID"
;; ;;
*) *)
echo "[ERROR] Unsupported system: $NAME ($ID)." echo "[ERROR] Unsupported system: $NAME ($ID)."
echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS or Ubuntu/Debian." echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS or Ubuntu."
exit 1 exit 1
;; ;;
esac esac
else else
echo "[ERROR] Cannot detect system (missing /etc/os-release)." echo "[ERROR] Cannot detect system (missing /etc/os-release)."
@@ -37,12 +37,9 @@ fi
echo "[OK] Kernel version >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}." echo "[OK] Kernel version >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}."
# ===== Config & Parse arguments ========================================================= # ===== Config & Parse arguments =========================================================
VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty
WITH_CORE="both" # Default: bundle both xray+sing-box WITH_CORE="both" # Default: bundle both xray+sing-box
AUTOSTART=0 # 1 = enable system-wide autostart (/etc/xdg/autostart) ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target)
FORCE_NETCORE=0 # --netcore => skip archive bundle, use separate downloads
ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target)
BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively
# If the first argument starts with --, do not treat it as a version number # If the first argument starts with --, do not treat it as a version number
if [[ "${VERSION_ARG:-}" == --* ]]; then if [[ "${VERSION_ARG:-}" == --* ]]; then
@@ -54,97 +51,86 @@ if [[ -n "${VERSION_ARG:-}" ]]; then shift || true; fi
# Parse remaining optional arguments # Parse remaining optional arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--with-core) WITH_CORE="${2:-both}"; shift 2;; --with-core)
--autostart) AUTOSTART=1; shift;; WITH_CORE="${2:-both}"
--xray-ver) XRAY_VER="${2:-}"; shift 2;; shift 2
--singbox-ver) SING_VER="${2:-}"; shift 2;; ;;
--netcore) FORCE_NETCORE=1; shift;; --xray-ver)
--arch) ARCH_OVERRIDE="${2:-}"; shift 2;; XRAY_VER="${2:-}"
--buildfrom) BUILD_FROM="${2:-}"; shift 2;; shift 2
*) ;;
if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi --singbox-ver)
shift;; SING_VER="${2:-}"
shift 2
;;
--arch)
ARCH_OVERRIDE="${2:-}"
shift 2
;;
--release)
RPM_RELEASE="${2:-}"
shift 2
;;
*)
if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi
shift
;;
esac esac
done done
# Conflict: version number AND --buildfrom cannot be used together if [[ -z "${RPM_RELEASE:-}" ]]; then
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then echo "--release is required"
echo "[ERROR] You cannot specify both an explicit version and --buildfrom at the same time."
echo " Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
exit 1 exit 1
fi fi
# ===== Environment check + Dependencies ======================================== # ===== Environment check + Dependencies ========================================
host_arch="$(uname -m)" host_arch="$(uname -m)"
[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; } if ! [[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]]; then
echo "Only supports aarch64 / x86_64"
exit 1
fi
install_ok=0 install_ok=0
case "$ID" in case "$ID" in
# ------------------------------ RHEL family (UNCHANGED) ------------------------------ # ------------------------------ RHEL family (UNCHANGED) ------------------------------
rhel|rocky|almalinux|centos) rhel | rocky | almalinux | centos)
if command -v dnf >/dev/null 2>&1; then if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \ sudo dnf -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync ||
sudo dnf -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync sudo dnf -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
install_ok=1 install_ok=1
elif command -v yum >/dev/null 2>&1; then elif command -v yum >/dev/null 2>&1; then
sudo yum -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \ sudo yum -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync ||
sudo yum -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync sudo yum -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
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 install_ok=1
;; fi
# ------------------------------ Debian (KEEP, with local dotnet install) ------------ ;;
debian) # ------------------------------ 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 sudo apt-get update
# Base tools + rpm (provides rpmbuild on Debian) + objdump/strip fi
sudo apt-get -y install curl unzip tar rsync rpm binutils || true # Base tools + rpm (provides rpmbuild)
# rpmbuild presence check sudo apt-get -y install curl unzip tar rsync rpm || true
if ! command -v rpmbuild >/dev/null 2>&1; then # Cross-arch binutils so strip matches target arch + objdump for brp scripts
echo "[ERROR] 'rpmbuild' not found after installing 'rpm'." sudo apt-get -y install binutils binutils-x86-64-linux-gnu binutils-aarch64-linux-gnu || true
echo " Please ensure 'rpm' is available from Debian repos." # rpmbuild presence check
exit 1 if ! command -v rpmbuild >/dev/null 2>&1; then
fi echo "[ERROR] 'rpmbuild' not found after installing 'rpm'."
# Try apt for dotnet; fallback to official installer into $HOME/.dotnet echo " Please ensure the 'rpm' package is available from your repos (universe on Ubuntu)."
if ! command -v dotnet >/dev/null 2>&1; then exit 1
echo "[INFO] 'dotnet' not found. Installing .NET 8 SDK locally to \$HOME/.dotnet ..." fi
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN # .NET SDK 8 (best effort via apt)
curl -fsSL https://dot.net/v1/dotnet-install.sh -o "$tmp/dotnet-install.sh" if ! command -v dotnet >/dev/null 2>&1; then
bash "$tmp/dotnet-install.sh" --channel 8.0 --install-dir "$HOME/.dotnet" sudo apt-get -y install dotnet-sdk-8.0 || true
export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:$PATH" sudo apt-get -y install dotnet-sdk-8 || true
export DOTNET_ROOT="$HOME/.dotnet" sudo apt-get -y install dotnet-sdk || true
if ! command -v dotnet >/dev/null 2>&1; then fi
echo "[ERROR] dotnet installation failed." install_ok=1
exit 1 ;;
fi
fi
install_ok=1
;;
esac esac
if [[ "$install_ok" -ne 1 ]]; then if [[ "$install_ok" -ne 1 ]]; then
@@ -158,6 +144,8 @@ command -v curl >/dev/null
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
source ./utils.sh
# Git submodules (best effort) # Git submodules (best effort)
if [[ -f .gitmodules ]]; then if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true git submodule sync --recursive || true
@@ -169,348 +157,16 @@ PROJECT="v2rayN.Desktop/v2rayN.Desktop.csproj"
if [[ ! -f "$PROJECT" ]]; then if [[ ! -f "$PROJECT" ]]; then
PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)" PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
fi fi
[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; } [[ -f "$PROJECT" ]] || {
echo "v2rayN.Desktop.csproj not found"
# ===== Resolve GUI version & auto checkout ============================================ exit 1
VERSION=""
choose_channel() {
# If --buildfrom provided, map it directly and skip interaction.
if [[ -n "${BUILD_FROM:-}" ]]; then
case "$BUILD_FROM" in
1) echo "latest"; return 0;;
2) echo "prerelease"; return 0;;
3) echo "keep"; return 0;;
*) echo "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." >&2; exit 1;;
esac
fi
# Print menu to stderr and read from /dev/tty so stdout only carries the token.
local ch="latest" sel=""
if [[ -t 0 ]]; then
echo "[?] Choose v2rayN release channel:" >&2
echo " 1) Latest (stable) [default]" >&2
echo " 2) Pre-release (preview)" >&2
echo " 3) Keep current (do nothing)" >&2
printf "Enter 1, 2 or 3 [default 1]: " >&2
if read -r sel </dev/tty; then
case "${sel:-}" in
2) ch="prerelease" ;;
3) ch="keep" ;;
*) ch="latest" ;;
esac
else
ch="latest"
fi
else
ch="latest"
fi
echo "$ch"
} }
get_latest_tag_latest() { VERSION="$VERSION_ARG"
# Resolve /releases/latest → tag_name
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
| grep -Eo '"tag_name":\s*"v?[^"]+"' \
| head -n1 \
| sed -E 's/.*"tag_name":\s*"v?([^"]+)".*/\1/'
}
get_latest_tag_prerelease() {
# Resolve newest prerelease=true tag; prefer jq, fallback to sed/grep (no awk)
local json tag
json="$(curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20")" || return 1
# 1) Use jq if present
if command -v jq >/dev/null 2>&1; then
tag="$(printf '%s' "$json" \
| jq -r '[.[] | select(.prerelease==true)][0].tag_name' 2>/dev/null \
| sed 's/^v//')" || true
fi
# 2) Fallback to sed/grep only
if [[ -z "${tag:-}" || "${tag:-}" == "null" ]]; then
tag="$(printf '%s' "$json" \
| tr '\n' ' ' \
| sed 's/},[[:space:]]*{/\n/g' \
| grep -m1 -E '"prerelease"[[:space:]]*:[[:space:]]*true' \
| grep -Eo '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' \
| head -n1 \
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/')" || true
fi
[[ -n "${tag:-}" && "${tag:-}" != "null" ]] || return 1
printf '%s\n' "$tag"
}
git_try_checkout() {
# Try a series of refs and checkout when found.
local want="$1" ref=""
if git rev-parse --git-dir >/dev/null 2>&1; then
git fetch --tags --force --prune --depth=1 || true
if git rev-parse "refs/tags/v${want}" >/dev/null 2>&1; then
ref="v${want}"
elif git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then
ref="${want}"
elif git rev-parse --verify "${want}" >/dev/null 2>&1; then
ref="${want}"
fi
if [[ -n "$ref" ]]; then
echo "[OK] Found ref '${ref}', checking out..."
git checkout -f "${ref}"
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
return 0
fi
fi
return 1
}
if git rev-parse --git-dir >/dev/null 2>&1; then
if [[ -n "${VERSION_ARG:-}" ]]; then
echo "[*] Trying to switch v2rayN repo to version: ${VERSION_ARG}"
if git_try_checkout "${VERSION_ARG#v}"; then
VERSION="${VERSION_ARG#v}"
else
echo "[WARN] Tag '${VERSION_ARG}' not found."
ch="$(choose_channel)"
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
VERSION="${VERSION#v}"
else
echo "[*] Resolving ${ch} tag from GitHub releases..."
tag=""
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
if [[ -z "$tag" ]]; then
echo "[WARN] Failed to resolve prerelease tag, falling back to latest."
tag="$(get_latest_tag_latest || true)"
fi
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
fi
fi
else
ch="$(choose_channel)"
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
VERSION="${VERSION#v}"
else
echo "[*] Resolving ${ch} tag from GitHub releases..."
tag=""
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
if [[ -z "$tag" ]]; then
echo "[WARN] Failed to resolve prerelease tag, falling back to latest."
tag="$(get_latest_tag_latest || true)"
fi
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
fi
fi
else
echo "[WARN] Current directory is not a git repo; cannot checkout version. Proceeding on current tree."
VERSION="${VERSION_ARG:-}"
if [[ -z "$VERSION" ]]; then
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
fi
VERSION="${VERSION#v}"
fi
echo "[*] GUI version resolved as: ${VERSION}"
# ===== Helpers for core/rules download (use RID_DIR for arch sync) =====================
download_xray() {
# Download Xray core and install to outdir/xray
local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
mkdir -p "$outdir"
if [[ -n "${XRAY_VER:-}" ]]; then ver="${XRAY_VER}"; fi
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
fi
[[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; }
if [[ "$RID_DIR" == "linux-arm64" ]]; then
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip"
else
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip"
fi
echo "[+] Download xray: $url"
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
curl -fL "$url" -o "$tmp/$zipname"
unzip -q "$tmp/$zipname" -d "$tmp"
install -Dm755 "$tmp/xray" "$outdir/xray"
}
download_singbox() {
# Download sing-box core and install to outdir/sing-box
local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
mkdir -p "$outdir"
if [[ -n "${SING_VER:-}" ]]; then ver="${SING_VER}"; fi
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
fi
[[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; }
if [[ "$RID_DIR" == "linux-arm64" ]]; then
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz"
else
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz"
fi
echo "[+] Download sing-box: $url"
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
curl -fL "$url" -o "$tmp/$tarname"
tar -C "$tmp" -xzf "$tmp/$tarname"
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)"
[[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; return 1; }
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"
mkdir -p "$outroot/bin"
local names=( \
"geosite.dat" \
"geoip.dat" \
"geoip-only-cn-private.dat" \
"Country.mmdb" \
"geoip.metadb" \
)
for n in "${names[@]}"; do
# If file exists under bin/xray/, move it up to bin/
if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
# If file already in bin/, leave it as-is
if [[ -f "$outroot/bin/$n" ]]; then
:
fi
done
}
# Download geo/rule assets; then unify to bin/
download_geo_assets() {
local outroot="$1"
local bin_dir="$outroot/bin"
local srss_dir="$bin_dir/srss"
mkdir -p "$bin_dir" "$srss_dir"
echo "[+] Download Xray Geo to ${bin_dir}"
curl -fsSL -o "$bin_dir/geosite.dat" \
"https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat"
curl -fsSL -o "$bin_dir/geoip.dat" \
"https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat"
curl -fsSL -o "$bin_dir/geoip-only-cn-private.dat" \
"https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat"
curl -fsSL -o "$bin_dir/Country.mmdb" \
"https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
echo "[+] Download sing-box rule DB & rule-sets"
curl -fsSL -o "$bin_dir/geoip.metadb" \
"https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" || true
for f in \
geoip-private.srs geoip-cn.srs geoip-facebook.srs geoip-fastly.srs \
geoip-google.srs geoip-netflix.srs geoip-telegram.srs geoip-twitter.srs; do
curl -fsSL -o "$srss_dir/$f" \
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geoip/$f" || true
done
for f in \
geosite-cn.srs geosite-gfw.srs geosite-greatfire.srs \
geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do
curl -fsSL -o "$srss_dir/$f" \
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done
# Unify to bin/
unify_geo_layout "$outroot"
}
# Prefer the prebuilt v2rayN core bundle; then unify geo layout
download_v2rayn_bundle() {
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.zip"
else
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip"
fi
echo "[+] Try v2rayN bundle archive: $url"
local tmp zipname
tmp="$(mktemp -d)"; zipname="$tmp/v2rayn.zip"
curl -fL "$url" -o "$zipname" || { echo "[!] Bundle download failed"; return 1; }
unzip -q "$zipname" -d "$tmp" || { echo "[!] Bundle unzip failed"; return 1; }
if [[ -d "$tmp/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$tmp/bin/" "$outroot/bin/"
else
rsync -a "$tmp/" "$outroot/"
fi
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
# keep mihomo
# find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
local nested_dir
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
if [[ -n "${nested_dir:-}" && -d "$nested_dir/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$nested_dir/bin/" "$outroot/bin/"
rm -rf "$nested_dir"
fi
# Unify to bin/
unify_geo_layout "$outroot"
echo "[+] Bundle extracted to $outroot"
}
# ===== Build results collection for --arch all ======================================== # ===== Build results collection for --arch all ========================================
BUILT_RPMS=() # Will collect absolute paths of built RPMs BUILT_RPMS=() # Will collect absolute paths of built RPMs
BUILT_ALL=0 # Flag to know if we should print the final summary BUILT_ALL=0 # Flag to know if we should print the final summary
# ===== Build (single-arch) function ==================================================== # ===== Build (single-arch) function ====================================================
build_for_arch() { build_for_arch() {
@@ -518,29 +174,41 @@ build_for_arch() {
local short="$1" local short="$1"
local rid rpm_target archdir local rid rpm_target archdir
case "$short" in case "$short" in
x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;; x64)
arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;; rid="linux-x64"
*) echo "[ERROR] Unknown arch '$short' (use x64|arm64)"; return 1;; rpm_target="x86_64"
archdir="x86_64"
;;
arm64)
rid="linux-arm64"
rpm_target="aarch64"
archdir="aarch64"
;;
*)
echo "[ERROR] Unknown arch '$short' (use x64|arm64)"
return 1
;;
esac esac
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)" echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
# .NET publish (self-contained) for this RID # .NET publish (self-contained) for this RID
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net8.0" || true
dotnet restore "$PROJECT" dotnet restore "$PROJECT"
rm -rf "$(dirname "$PROJECT")/bin/Release/net8.0" || true
dotnet publish "$PROJECT" \ dotnet publish "$PROJECT" \
-c Release -r "$rid" \ -c Release -r "$rid" \
--sc \
-p:PublishSingleFile=false \ -p:PublishSingleFile=false \
-p:SelfContained=true \ -p:SelfContained=true \
-p:IncludeNativeLibrariesForSelfExtract=true -p:IncludeNativeLibrariesForSelfExtract=true \
-p:StripSymbols=true
# Per-arch variables (scoped) # Per-arch variables (scoped)
local RID_DIR="$rid" local RID_DIR="$rid"
local PUBDIR local PUBDIR
PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish" PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish"
[[ -d "$PUBDIR" ]] [[ -d "$PUBDIR" ]]
sudo find "$PUBDIR" -type f -name "*.so" -exec strip {} +
# Make RID_DIR visible to download helpers (they read this var) # Make RID_DIR visible to download helpers (they read this var)
export RID_DIR export RID_DIR
@@ -580,31 +248,13 @@ build_for_arch() {
mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box" mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box"
# Bundle / cores per-arch # Bundle / cores per-arch
if [[ "$FORCE_NETCORE" -eq 0 ]]; then if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
if download_v2rayn_bundle "$WORKDIR/$PKGROOT"; then download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
echo "[*] Using v2rayN bundle archive."
else
echo "[*] Bundle failed, fallback to separate core + rules."
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
fi
else
echo "[*] --netcore specified: use separate core + rules."
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
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)"
fi fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
# Tarball # Tarball
mkdir -p "$SOURCEDIR" mkdir -p "$SOURCEDIR"
@@ -613,19 +263,19 @@ build_for_arch() {
# SPEC # SPEC
local SPECFILE="$SPECDIR/v2rayN.spec" local SPECFILE="$SPECDIR/v2rayN.spec"
mkdir -p "$SPECDIR" mkdir -p "$SPECDIR"
cat > "$SPECFILE" <<'SPEC' cat >"$SPECFILE" <<'SPEC'
%global debug_package %{nil} %global debug_package %{nil}
%undefine _debuginfo_subpackages %undefine _debuginfo_subpackages
%undefine _debugsource_packages %undefine _debugsource_packages
# Ignore outdated LTTng dependencies incorrectly reported by the .NET runtime (to avoid installation failures) # Ignore outdated LTTng dependencies incorrectly reported by the .NET runtime (to avoid installation failures)
%global __requires_exclude ^liblttng-ust\.so\..*$ %global __requires_exclude ^liblttng-ust\.so\..*$
Name: v2rayN Name: v2rayn-unofficial
Version: __VERSION__ Version: __VERSION__
Release: 1%{?dist} Release: __RELEASE__
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64) Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64)
License: GPL-3.0-only License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN URL: https://git.vlyaii.ru/voronin9032/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues BugURL: https://github.com/2dust/v2rayN/issues
ExclusiveArch: aarch64 x86_64 ExclusiveArch: aarch64 x86_64
Source0: __PKGROOT__.tar.gz Source0: __PKGROOT__.tar.gz
@@ -639,6 +289,9 @@ Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32 Requires: coreutils >= 8.32
Requires: bash >= 5.1 Requires: bash >= 5.1
Conflicts: v2rayN
Obsoletes: v2rayN
%description %description
v2rayN Linux for Red Hat Enterprise Linux v2rayN Linux for Red Hat Enterprise Linux
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
@@ -711,41 +364,9 @@ fi
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png %{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
SPEC SPEC
# Autostart injection (inside %install) and %files entry
if [[ "$AUTOSTART" -eq 1 ]]; then
awk '
BEGIN{ins=0}
/^%post$/ && !ins {
print "# --- Autostart (.desktop) ---"
print "install -dm0755 %{buildroot}/etc/xdg/autostart"
print "cat > %{buildroot}/etc/xdg/autostart/v2rayn.desktop << '\''EOF'\''"
print "[Desktop Entry]"
print "Type=Application"
print "Name=v2rayN (Autostart)"
print "Exec=v2rayn"
print "X-GNOME-Autostart-enabled=true"
print "NoDisplay=false"
print "EOF"
ins=1
}
{print}
' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE"
awk '
BEGIN{infiles=0; done=0}
/^%files$/ {infiles=1}
infiles && done==0 && $0 ~ /%{_datadir}\/icons\/hicolor\/256x256\/apps\/v2rayn\.png/ {
print
print "%config(noreplace) /etc/xdg/autostart/v2rayn.desktop"
done=1
next
}
{print}
' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE"
fi
# Replace placeholders # Replace placeholders
sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE" sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE"
sed -i "s/__RELEASE__/${RPM_RELEASE}/g" "$SPECFILE"
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE" sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE"
# ----- Select proper 'strip' per target arch on Ubuntu only (cross-binutils) ----- # ----- Select proper 'strip' per target arch on Ubuntu only (cross-binutils) -----
@@ -760,7 +381,7 @@ SPEC
STRIP_BIN="/usr/bin/aarch64-linux-gnu-strip" STRIP_BIN="/usr/bin/aarch64-linux-gnu-strip"
fi fi
if [[ -x "$STRIP_BIN" ]]; then if [[ -x "$STRIP_BIN" ]]; then
STRIP_ARGS=( --define "__strip $STRIP_BIN" ) STRIP_ARGS=(--define "__strip $STRIP_BIN")
fi fi
fi fi
@@ -780,7 +401,7 @@ SPEC
echo "Build done for $short. RPM at:" echo "Build done for $short. RPM at:"
local f local f
for f in "${TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do for f in "${TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-${RPM_RELEASE}"*.rpm; do
[[ -e "$f" ]] || continue [[ -e "$f" ]] || continue
echo " $f" echo " $f"
BUILT_RPMS+=("$f") BUILT_RPMS+=("$f")
@@ -789,30 +410,30 @@ SPEC
# ===== Arch selection and build orchestration ========================================= # ===== Arch selection and build orchestration =========================================
case "${ARCH_OVERRIDE:-}" in case "${ARCH_OVERRIDE:-}" in
"") "")
# No --arch: use host architecture # No --arch: use host architecture
if [[ "$host_arch" == "aarch64" ]]; then if [[ "$host_arch" == "aarch64" ]]; then
build_for_arch arm64
else
build_for_arch x64
fi
;;
x64|amd64)
build_for_arch x64
;;
arm64|aarch64)
build_for_arch arm64 build_for_arch arm64
;; else
all)
BUILT_ALL=1
# Build x64 and arm64 separately; each package contains its own arch-only binaries.
build_for_arch x64 build_for_arch x64
build_for_arch arm64 fi
;; ;;
*) x64 | amd64)
echo "[ERROR] Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all." build_for_arch x64
exit 1 ;;
;; arm64 | aarch64)
build_for_arch arm64
;;
all)
BUILT_ALL=1
# Build x64 and arm64 separately; each package contains its own arch-only binaries.
build_for_arch x64
build_for_arch arm64
;;
*)
echo "[ERROR] Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all."
exit 1
;;
esac esac
# ===== Final summary if building both arches ========================================== # ===== Final summary if building both arches ==========================================

120
utils.sh Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env bash
download_xray() {
# Download Xray core and install to outdir/xray
local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
mkdir -p "$outdir"
if [[ -n "${XRAY_VER:-}" ]]; then ver="${XRAY_VER}"; fi
if [[ -z "$ver" ]]; then
echo "Downloading latest xray"
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest |
grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
fi
if [[ -z "$ver" ]]; then
echo "[xray] Failed to get version"
return 1
fi
if [[ "$RID_DIR" == "linux-arm64" ]]; then
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip"
else
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip"
fi
echo "[+] Download xray: $url"
tmp="$(mktemp -d)"
trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
curl -fL "$url" -o "$tmp/$zipname"
unzip -q "$tmp/$zipname" -d "$tmp"
install -Dm755 "$tmp/xray" "$outdir/xray"
}
download_singbox() {
# Download sing-box core and install to outdir/sing-box
local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
mkdir -p "$outdir"
if [[ -n "${SING_VER:-}" ]]; then ver="${SING_VER}"; fi
if [[ -z "$ver" ]]; then
echo "Downloading latest sing-box"
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest |
grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
fi
if [[ -z "$ver" ]]; then
echo "[sing-box] Failed to get version"
return 1
fi
if [[ "$RID_DIR" == "linux-arm64" ]]; then
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz"
else
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz"
fi
echo "[+] Download sing-box: $url"
tmp="$(mktemp -d)"
trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
curl -fL "$url" -o "$tmp/$tarname"
tar -C "$tmp" -xzf "$tmp/$tarname"
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)"
[[ -n "$bin" ]] || {
echo "[!] sing-box unpack failed"
return 1
}
install -Dm755 "$bin" "$outdir/sing-box"
}
# Move geo files to a unified path: outroot/bin
unify_geo_layout() {
local outroot="$1"
mkdir -p "$outroot/bin"
local names=(
"geosite.dat"
"geoip.dat"
"geoip-only-cn-private.dat"
"Country.mmdb"
"geoip.metadb"
)
for n in "${names[@]}"; do
# If file exists under bin/xray/, move it up to bin/
if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
# If file already in bin/, leave it as-is
if [[ -f "$outroot/bin/$n" ]]; then
:
fi
done
}
# Download geo/rule assets; then unify to bin/
download_geo_assets() {
local outroot="$1"
local bin_dir="$outroot/bin"
local srss_dir="$bin_dir/srss"
mkdir -p "$bin_dir" "$srss_dir"
echo "[+] Download Xray Geo to ${bin_dir}"
curl -fsSL -o "$bin_dir/geosite.dat" \
"https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat"
curl -fsSL -o "$bin_dir/geoip.dat" \
"https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat"
curl -fsSL -o "$bin_dir/geoip-only-cn-private.dat" \
"https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat"
curl -fsSL -o "$bin_dir/Country.mmdb" \
"https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
echo "[+] Download sing-box rule DB & rule-sets"
curl -fsSL -o "$bin_dir/geoip.metadb" \
"https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" || true
for f in \
geoip-private.srs geoip-cn.srs geoip-facebook.srs geoip-fastly.srs \
geoip-google.srs geoip-netflix.srs geoip-telegram.srs geoip-twitter.srs; do
curl -fsSL -o "$srss_dir/$f" \
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geoip/$f" || true
done
for f in \
geosite-cn.srs geosite-gfw.srs geosite-greatfire.srs \
geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do
curl -fsSL -o "$srss_dir/$f" \
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done
# Unify to bin/
unify_geo_layout "$outroot"
}

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.15.7</Version> <Version>7.16.1</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -14,7 +14,7 @@
<PackageVersion Include="Downloader" Version="4.0.3" /> <PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.2" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.2" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.2.0" /> <PackageVersion Include="MessageBox.Avalonia" Version="3.3.0" />
<PackageVersion Include="QRCoder" Version="1.7.0" /> <PackageVersion Include="QRCoder" Version="1.7.0" />
<PackageVersion Include="ReactiveUI" Version="22.2.1" /> <PackageVersion Include="ReactiveUI" Version="22.2.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />

View File

@@ -998,7 +998,7 @@ public class Utils
public static bool IsLinux() => OperatingSystem.IsLinux(); public static bool IsLinux() => OperatingSystem.IsLinux();
public static bool IsOSX() => OperatingSystem.IsMacOS(); public static bool IsMacOS() => OperatingSystem.IsMacOS();
public static bool IsNonWindows() => !OperatingSystem.IsWindows(); public static bool IsNonWindows() => !OperatingSystem.IsWindows();
@@ -1020,7 +1020,7 @@ public class Utils
{ {
try try
{ {
if (IsWindows() || IsOSX()) if (IsWindows() || IsMacOS())
{ {
return false; return false;
} }

View File

@@ -26,7 +26,7 @@ public static class AutoStartupHandler
await SetTaskLinux(); await SetTaskLinux();
} }
} }
else if (Utils.IsOSX()) else if (Utils.IsMacOS())
{ {
await ClearTaskOSX(); await ClearTaskOSX();

View File

@@ -97,7 +97,7 @@ public static class ConfigHandler
config.UiItem ??= new UIItem() config.UiItem ??= new UIItem()
{ {
EnableAutoAdjustMainLvColWidth = true EnableUpdateSubOnlyRemarksExist = true
}; };
config.UiItem.MainColumnItem ??= new(); config.UiItem.MainColumnItem ??= new();
config.UiItem.WindowSizeItem ??= new(); config.UiItem.WindowSizeItem ??= new();

View File

@@ -23,7 +23,7 @@ public class AnytlsFmt : BaseFmt
item.Id = rawUserInfo; item.Id = rawUserInfo;
var query = Utils.ParseQueryString(parsedUrl.Query); var query = Utils.ParseQueryString(parsedUrl.Query);
_ = ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
return item; return item;
} }
@@ -41,7 +41,7 @@ public class AnytlsFmt : BaseFmt
} }
var pw = item.Id; var pw = item.Id;
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
_ = GetStdTransport(item, Global.None, ref dicQuery); ToUriQuery(item, Global.None, ref dicQuery);
return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark); return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark);
} }

View File

@@ -4,6 +4,8 @@ namespace ServiceLib.Handler.Fmt;
public class BaseFmt public class BaseFmt
{ {
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure", "verify" };
protected static string GetIpv6(string address) protected static string GetIpv6(string address)
{ {
if (Utils.IsIpv6(address)) if (Utils.IsIpv6(address))
@@ -17,7 +19,7 @@ public class BaseFmt
} }
} }
protected static int GetStdTransport(ProfileItem item, string? securityDef, ref Dictionary<string, string> dicQuery) protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dictionary<string, string> dicQuery)
{ {
if (item.Flow.IsNotEmpty()) if (item.Flow.IsNotEmpty())
{ {
@@ -37,11 +39,7 @@ public class BaseFmt
} }
if (item.Sni.IsNotEmpty()) if (item.Sni.IsNotEmpty())
{ {
dicQuery.Add("sni", item.Sni); dicQuery.Add("sni", Utils.UrlEncode(item.Sni));
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
} }
if (item.Fingerprint.IsNotEmpty()) if (item.Fingerprint.IsNotEmpty())
{ {
@@ -63,9 +61,14 @@ public class BaseFmt
{ {
dicQuery.Add("pqv", Utils.UrlEncode(item.Mldsa65Verify)); dicQuery.Add("pqv", Utils.UrlEncode(item.Mldsa65Verify));
} }
if (item.AllowInsecure.Equals("true"))
if (item.StreamSecurity.Equals(Global.StreamSecurity))
{ {
dicQuery.Add("allowInsecure", "1"); if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
ToUriQueryAllowInsecure(item, ref dicQuery);
} }
dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp)); dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp));
@@ -153,7 +156,40 @@ public class BaseFmt
return 0; return 0;
} }
protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item) protected static int ToUriQueryLite(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (item.Sni.IsNotEmpty())
{
dicQuery.Add("sni", Utils.UrlEncode(item.Sni));
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
ToUriQueryAllowInsecure(item, ref dicQuery);
return 0;
}
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (item.AllowInsecure.Equals(Global.AllowInsecure.First()))
{
// Add two for compatibility
dicQuery.Add("insecure", "1");
dicQuery.Add("allowInsecure", "1");
}
else
{
dicQuery.Add("insecure", "0");
dicQuery.Add("allowInsecure", "0");
}
return 0;
}
protected static int ResolveUriQuery(NameValueCollection query, ref ProfileItem item)
{ {
item.Flow = GetQueryValue(query, "flow"); item.Flow = GetQueryValue(query, "flow");
item.StreamSecurity = GetQueryValue(query, "security"); item.StreamSecurity = GetQueryValue(query, "security");
@@ -164,7 +200,19 @@ public class BaseFmt
item.ShortId = GetQueryDecoded(query, "sid"); item.ShortId = GetQueryDecoded(query, "sid");
item.SpiderX = GetQueryDecoded(query, "spx"); item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.AllowInsecure = new[] { "allowInsecure", "allow_insecure", "insecure" }.Any(k => (query[k] ?? "") == "1") ? "true" : "";
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{
item.AllowInsecure = Global.AllowInsecure.First();
}
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
{
item.AllowInsecure = Global.AllowInsecure.Skip(1).First();
}
else
{
item.AllowInsecure = string.Empty;
}
item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp)); item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp));
switch (item.Network) switch (item.Network)

View File

@@ -22,10 +22,8 @@ public class Hysteria2Fmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo); item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
item.Path = GetQueryDecoded(query, "obfs-password"); item.Path = GetQueryDecoded(query, "obfs-password");
item.AllowInsecure = GetQueryValue(query, "insecure") == "1" ? "true" : "false";
item.Ports = GetQueryDecoded(query, "mport"); item.Ports = GetQueryDecoded(query, "mport");
return item; return item;
@@ -46,20 +44,13 @@ public class Hysteria2Fmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
if (item.Sni.IsNotEmpty()) ToUriQueryLite(item, ref dicQuery);
{
dicQuery.Add("sni", item.Sni);
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
if (item.Path.IsNotEmpty()) if (item.Path.IsNotEmpty())
{ {
dicQuery.Add("obfs", "salamander"); dicQuery.Add("obfs", "salamander");
dicQuery.Add("obfs-password", Utils.UrlEncode(item.Path)); dicQuery.Add("obfs-password", Utils.UrlEncode(item.Path));
} }
dicQuery.Add("insecure", item.AllowInsecure.ToLower() == "true" ? "1" : "0");
if (item.Ports.IsNotEmpty()) if (item.Ports.IsNotEmpty())
{ {
dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-'))); dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-')));

View File

@@ -23,7 +23,7 @@ public class TrojanFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo); item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
_ = ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
return item; return item;
} }
@@ -40,7 +40,7 @@ public class TrojanFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
_ = GetStdTransport(item, null, ref dicQuery); ToUriQuery(item, null, ref dicQuery);
return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Id, dicQuery, remark); return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Id, dicQuery, remark);
} }

View File

@@ -29,7 +29,7 @@ public class TuicFmt : BaseFmt
} }
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
item.HeaderType = GetQueryValue(query, "congestion_control"); item.HeaderType = GetQueryValue(query, "congestion_control");
return item; return item;
@@ -47,15 +47,10 @@ public class TuicFmt : BaseFmt
{ {
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
if (item.Sni.IsNotEmpty()) ToUriQueryLite(item, ref dicQuery);
{
dicQuery.Add("sni", item.Sni);
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
dicQuery.Add("congestion_control", item.HeaderType); dicQuery.Add("congestion_control", item.HeaderType);
return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Id}:{item.Security}", dicQuery, remark); return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Id}:{item.Security}", dicQuery, remark);

View File

@@ -26,7 +26,7 @@ public class VLESSFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
item.Security = GetQueryValue(query, "encryption", Global.None); item.Security = GetQueryValue(query, "encryption", Global.None);
item.StreamSecurity = GetQueryValue(query, "security"); item.StreamSecurity = GetQueryValue(query, "security");
_ = ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
return item; return item;
} }
@@ -52,7 +52,7 @@ public class VLESSFmt : BaseFmt
{ {
dicQuery.Add("encryption", Global.None); dicQuery.Add("encryption", Global.None);
} }
_ = GetStdTransport(item, Global.None, ref dicQuery); ToUriQuery(item, Global.None, ref dicQuery);
return ToUri(EConfigType.VLESS, item.Address, item.Port, item.Id, dicQuery, remark); return ToUri(EConfigType.VLESS, item.Address, item.Port, item.Id, dicQuery, remark);
} }

View File

@@ -39,7 +39,8 @@ public class VmessFmt : BaseFmt
tls = item.StreamSecurity, tls = item.StreamSecurity,
sni = item.Sni, sni = item.Sni,
alpn = item.Alpn, alpn = item.Alpn,
fp = item.Fingerprint fp = item.Fingerprint,
insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0"
}; };
var url = JsonUtils.Serialize(vmessQRCode); var url = JsonUtils.Serialize(vmessQRCode);
@@ -94,6 +95,7 @@ public class VmessFmt : BaseFmt
item.Sni = Utils.ToString(vmessQRCode.sni); item.Sni = Utils.ToString(vmessQRCode.sni);
item.Alpn = Utils.ToString(vmessQRCode.alpn); item.Alpn = Utils.ToString(vmessQRCode.alpn);
item.Fingerprint = Utils.ToString(vmessQRCode.fp); item.Fingerprint = Utils.ToString(vmessQRCode.fp);
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty;
return item; return item;
} }
@@ -118,7 +120,7 @@ public class VmessFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo); item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
return item; return item;
} }

View File

@@ -18,7 +18,13 @@ public static class ProxySettingLinux
private static async Task ExecCmd(List<string> args) private static async Task ExecCmd(List<string> args)
{ {
var fileName = await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false); var customSystemProxyScriptPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyScriptPath;
var fileName = (customSystemProxyScriptPath.IsNotEmpty() && File.Exists(customSystemProxyScriptPath))
? customSystemProxyScriptPath
: await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false);
// TODO: temporarily notify which script is being used
NoticeManager.Instance.SendMessage(fileName);
await Utils.GetCliWrapOutput(fileName, args); await Utils.GetCliWrapOutput(fileName, args);
} }

View File

@@ -23,7 +23,13 @@ public static class ProxySettingOSX
private static async Task ExecCmd(List<string> args) private static async Task ExecCmd(List<string> args)
{ {
var fileName = await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false); var customSystemProxyScriptPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyScriptPath;
var fileName = (customSystemProxyScriptPath.IsNotEmpty() && File.Exists(customSystemProxyScriptPath))
? customSystemProxyScriptPath
: await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false);
// TODO: temporarily notify which script is being used
NoticeManager.Instance.SendMessage(fileName);
await Utils.GetCliWrapOutput(fileName, args); await Utils.GetCliWrapOutput(fileName, args);
} }

View File

@@ -33,7 +33,7 @@ public static class SysProxyHandler
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions); await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
break; break;
case ESysProxyType.ForcedChange when Utils.IsOSX(): case ESysProxyType.ForcedChange when Utils.IsMacOS():
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions); await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions);
break; break;
@@ -45,7 +45,7 @@ public static class SysProxyHandler
await ProxySettingLinux.UnsetProxy(); await ProxySettingLinux.UnsetProxy();
break; break;
case ESysProxyType.ForcedClear when Utils.IsOSX(): case ESysProxyType.ForcedClear when Utils.IsMacOS():
await ProxySettingOSX.UnsetProxy(); await ProxySettingOSX.UnsetProxy();
break; break;
@@ -91,7 +91,7 @@ public static class SysProxyHandler
private static async Task SetWindowsProxyPac(int port) private static async Task SetWindowsProxyPac(int port)
{ {
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac); var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);
await PacManager.Instance.StartAsync(Utils.GetConfigPath(), port, portPac); await PacManager.Instance.StartAsync(port, portPac);
var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}"; var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
ProxySettingWindows.SetProxy(strProxy, "", 4); ProxySettingWindows.SetProxy(strProxy, "", 4);
} }

View File

@@ -49,7 +49,6 @@ public class HttpClientHelper
return await httpClient.GetStringAsync(url); return await httpClient.GetStringAsync(url);
} }
public async Task PutAsync(string url, Dictionary<string, string> headers) public async Task PutAsync(string url, Dictionary<string, string> headers)
{ {
var jsonContent = JsonUtils.Serialize(headers); var jsonContent = JsonUtils.Serialize(headers);
@@ -72,6 +71,4 @@ public class HttpClientHelper
{ {
await httpClient.DeleteAsync(url); await httpClient.DeleteAsync(url);
} }
} }

View File

@@ -202,55 +202,78 @@ public class CertPemManager
/// <summary> /// <summary>
/// Get certificate in PEM format from a server with CA pinning validation /// Get certificate in PEM format from a server with CA pinning validation
/// </summary> /// </summary>
public async Task<string?> GetCertPemAsync(string target, string serverName) public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4)
{ {
try try
{ {
var (domain, _, port, _) = Utils.ParseUrl(target); var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
await ssl.AuthenticateAsClientAsync(serverName); var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = ValidateServerCertificate
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
var remote = ssl.RemoteCertificate; var remote = ssl.RemoteCertificate;
if (remote == null) if (remote == null)
{ {
return null; return (null, null);
} }
var leaf = new X509Certificate2(remote); var leaf = new X509Certificate2(remote);
return ExportCertToPem(leaf); return (ExportCertToPem(leaf), null);
}
catch (OperationCanceledException)
{
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
return (null, $"Connection timeout after {timeout} seconds");
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
return null; return (null, ex.Message);
} }
} }
/// <summary> /// <summary>
/// Get certificate chain in PEM format from a server with CA pinning validation /// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary> /// </summary>
public async Task<List<string>> GetCertChainPemAsync(string target, string serverName) public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4)
{ {
var pemList = new List<string>();
try try
{ {
var pemList = new List<string>();
var (domain, _, port, _) = Utils.ParseUrl(target); var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
await ssl.AuthenticateAsClientAsync(serverName); var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = ValidateServerCertificate
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
if (ssl.RemoteCertificate is not X509Certificate2 certChain) if (ssl.RemoteCertificate is not X509Certificate2 certChain)
{ {
return pemList; return (pemList, null);
} }
var chain = new X509Chain(); var chain = new X509Chain();
@@ -262,12 +285,17 @@ public class CertPemManager
pemList.Add(pem); pemList.Add(pem);
} }
return pemList; return (pemList, null);
}
catch (OperationCanceledException)
{
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
return (pemList, $"Connection timeout after {timeout} seconds");
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
return new List<string>(); return (pemList, ex.Message);
} }
} }
@@ -314,10 +342,74 @@ public class CertPemManager
return TrustedCaThumbprints.Contains(rootThumbprint); return TrustedCaThumbprints.Contains(rootThumbprint);
} }
public string ExportCertToPem(X509Certificate2 cert) public static string ExportCertToPem(X509Certificate2 cert)
{ {
var der = cert.Export(X509ContentType.Cert); var der = cert.Export(X509ContentType.Cert);
var b64 = Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks); var b64 = Convert.ToBase64String(der);
return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n"; return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n";
} }
/// <summary>
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// </summary>
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
/// <returns>List of individual PEM certificate strings with normalized format</returns>
public static List<string> ParsePemChain(string pemChain)
{
var certs = new List<string>();
if (string.IsNullOrWhiteSpace(pemChain))
{
return certs;
}
// Normalize line endings (CRLF -> LF) at the beginning
pemChain = pemChain.Replace("\r\n", "\n").Replace("\r", "\n");
const string beginMarker = "-----BEGIN CERTIFICATE-----";
const string endMarker = "-----END CERTIFICATE-----";
var index = 0;
while (index < pemChain.Length)
{
var beginIndex = pemChain.IndexOf(beginMarker, index, StringComparison.Ordinal);
if (beginIndex == -1)
break;
var endIndex = pemChain.IndexOf(endMarker, beginIndex, StringComparison.Ordinal);
if (endIndex == -1)
break;
// Extract certificate content
var base64Start = beginIndex + beginMarker.Length;
var base64Content = pemChain.Substring(base64Start, endIndex - base64Start);
// Remove all whitespace from base64 content
base64Content = new string(base64Content.Where(c => !char.IsWhiteSpace(c)).ToArray());
// Reconstruct with clean format: BEGIN marker + base64 (no line breaks) + END marker
var normalizedCert = $"{beginMarker}\n{base64Content}\n{endMarker}\n";
certs.Add(normalizedCert);
// Move to next certificate
index = endIndex + endMarker.Length;
}
return certs;
}
/// <summary>
/// Concatenate a list of PEM certificates into a single string
/// </summary>
/// <param name="pemList">List of individual PEM certificate strings</param>
/// <returns>Concatenated PEM certificates string</returns>
public static string ConcatenatePemChain(IEnumerable<string> pemList)
{
if (pemList == null)
{
return string.Empty;
}
return string.Concat(pemList);
}
} }

View File

@@ -67,7 +67,7 @@ public class CoreAdminManager
try try
{ {
var shellFileName = Utils.IsOSX() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName; var shellFileName = Utils.IsMacOS() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName;
var shFilePath = await FileUtils.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true); var shFilePath = await FileUtils.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true);
if (shFilePath.Contains(' ')) if (shFilePath.Contains(' '))
{ {

View File

@@ -5,7 +5,6 @@ public class PacManager
private static readonly Lazy<PacManager> _instance = new(() => new PacManager()); private static readonly Lazy<PacManager> _instance = new(() => new PacManager());
public static PacManager Instance => _instance.Value; public static PacManager Instance => _instance.Value;
private string _configPath;
private int _httpPort; private int _httpPort;
private int _pacPort; private int _pacPort;
private TcpListener? _tcpListener; private TcpListener? _tcpListener;
@@ -13,11 +12,10 @@ public class PacManager
private bool _isRunning; private bool _isRunning;
private bool _needRestart = true; private bool _needRestart = true;
public async Task StartAsync(string configPath, int httpPort, int pacPort) public async Task StartAsync(int httpPort, int pacPort)
{ {
_needRestart = configPath != _configPath || httpPort != _httpPort || pacPort != _pacPort || !_isRunning; _needRestart = httpPort != _httpPort || pacPort != _pacPort || !_isRunning;
_configPath = configPath;
_httpPort = httpPort; _httpPort = httpPort;
_pacPort = pacPort; _pacPort = pacPort;
@@ -32,22 +30,22 @@ public class PacManager
private async Task InitText() private async Task InitText()
{ {
var path = Path.Combine(_configPath, "pac.txt"); var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyPacPath;
var fileName = (customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath))
? customSystemProxyPacPath
: Path.Combine(Utils.GetConfigPath(), "pac.txt");
// Delete the old pac file // TODO: temporarily notify which script is being used
if (File.Exists(path) && Utils.GetFileHash(path).Equals("b590c07280f058ef05d5394aa2f927fe")) NoticeManager.Instance.SendMessage(fileName);
{
File.Delete(path);
}
if (!File.Exists(path)) if (!File.Exists(fileName))
{ {
var pac = EmbedUtils.GetEmbedText(Global.PacFileName); var pac = EmbedUtils.GetEmbedText(Global.PacFileName);
await File.AppendAllTextAsync(path, pac); await File.AppendAllTextAsync(fileName, pac);
} }
var pacText = var pacText = await File.ReadAllTextAsync(fileName);
(await File.ReadAllTextAsync(path)).Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;"); pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;");
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("HTTP/1.0 200 OK"); sb.AppendLine("HTTP/1.0 200 OK");

View File

@@ -219,6 +219,8 @@ public class SystemProxyItem
public string SystemProxyExceptions { get; set; } public string SystemProxyExceptions { get; set; }
public bool NotProxyLocalAddress { get; set; } = true; public bool NotProxyLocalAddress { get; set; } = true;
public string SystemProxyAdvancedProtocol { get; set; } public string SystemProxyAdvancedProtocol { get; set; }
public string? CustomSystemProxyPacPath { get; set; }
public string? CustomSystemProxyScriptPath { get; set; }
} }
[Serializable] [Serializable]

View File

@@ -38,4 +38,6 @@ public class VmessQRCode
public string alpn { get; set; } = string.Empty; public string alpn { get; set; } = string.Empty;
public string fp { get; set; } = string.Empty; public string fp { get; set; } = string.Empty;
public string insecure { get; set; } = string.Empty;
} }

View File

@@ -2599,8 +2599,10 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Server certificate (PEM format, optional). Entering a certificate will pin it. /// 查找类似 Server Certificate (PEM format, optional)
///Do not use the &quot;Fetch Certificate&quot; button when &quot;Allow Insecure&quot; is enabled. 的本地化字符串。 ///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. 的本地化字符串。
/// </summary> /// </summary>
public static string TbCertPinningTips { public static string TbCertPinningTips {
get { get {
@@ -3508,6 +3510,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom PAC file path 的本地化字符串。
/// </summary>
public static string TbSettingsCustomSystemProxyPacPath {
get {
return ResourceManager.GetString("TbSettingsCustomSystemProxyPacPath", resourceCulture);
}
}
/// <summary>
/// 查找类似 Custom system proxy script file path 的本地化字符串。
/// </summary>
public static string TbSettingsCustomSystemProxyScriptPath {
get {
return ResourceManager.GetString("TbSettingsCustomSystemProxyScriptPath", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Allow Insecure 的本地化字符串。 /// 查找类似 Allow Insecure 的本地化字符串。
/// </summary> /// </summary>
@@ -3832,6 +3852,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 macOS displays this in the Dock (requires restart) 的本地化字符串。
/// </summary>
public static string TbSettingsMacOSShowInDock {
get {
return ResourceManager.GetString("TbSettingsMacOSShowInDock", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Main layout orientation (requires restart) 的本地化字符串。 /// 查找类似 Main layout orientation (requires restart) 的本地化字符串。
/// </summary> /// </summary>

View File

@@ -1606,8 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> 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>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1624,4 +1626,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1603,8 +1603,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Certificat serveur (PEM, optionnel). Lajout dun certificat le fixe. <value>Server Certificate (PEM format, optional)
Ne pas utiliser « Obtenir le certificat » si « Autoriser non sécurisé » est activé.</value> 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>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Obtenir le certificat</value> <value>Obtenir le certificat</value>
@@ -1621,4 +1623,13 @@ Ne pas utiliser « Obtenir le certificat » si « Autoriser non sécurisé » es
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificat configuré </value> <value>Certificat configuré </value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Chemin fichier PAC personnalisé</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Chemin script proxy système personnalisé</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1606,8 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> 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>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1624,4 +1626,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1606,8 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> 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>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1624,4 +1626,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1606,8 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> 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>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1624,4 +1626,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1603,8 +1603,10 @@
<value>固定证书</value> <value>固定证书</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>服务器证书PEM 格式,可选)。填入后将固定该证书。 <value>服务器证书PEM 格式,可选)
启用“跳过证书验证”时,请勿使用 '获取证书'。</value> 当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。
“获取证书”操作可能失败,原因可能是使用了自签证书,或系统中存在不受信任或恶意的 CA。</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>获取证书</value> <value>获取证书</value>
@@ -1621,4 +1623,13 @@
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>证书已设置</value> <value>证书已设置</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>自定义 PAC 文件路径</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>自定义系统代理脚本文件路径</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS 在 Dock 栏中显示 (需重启)</value>
</data>
</root> </root>

View File

@@ -1603,8 +1603,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> 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>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1621,4 +1623,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>自訂 PAC 檔案路徑</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>自訂系統代理程式腳本檔案路徑</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS 在 Dock 欄顯示 (需重啟)</value>
</data>
</root> </root>

View File

@@ -61,7 +61,7 @@ public partial class CoreConfigSingboxService
} }
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { }; var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
tunInbound.interface_name = Utils.IsOSX() ? $"utun{new Random().Next(99)}" : "singbox_tun"; tunInbound.interface_name = Utils.IsMacOS() ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.mtu = _config.TunModeItem.Mtu; tunInbound.mtu = _config.TunModeItem.Mtu;
tunInbound.auto_route = _config.TunModeItem.AutoRoute; tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute; tunInbound.strict_route = _config.TunModeItem.StrictRoute;

View File

@@ -261,13 +261,7 @@ public partial class CoreConfigSingboxService
} }
if (node.StreamSecurity == Global.StreamSecurity) if (node.StreamSecurity == Global.StreamSecurity)
{ {
var certs = node.Cert var certs = CertPemManager.ParsePemChain(node.Cert);
?.Split("-----END CERTIFICATE-----", StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.TrimEx())
.Where(s => !s.IsNullOrEmpty())
.Select(s => s + "\n-----END CERTIFICATE-----")
.Select(s => s.Replace("\r\n", "\n"))
.ToList() ?? new();
if (certs.Count > 0) if (certs.Count > 0)
{ {
tls.certificate = certs; tls.certificate = certs;

View File

@@ -245,13 +245,6 @@ public partial class CoreConfigV2rayService
var host = node.RequestHost.TrimEx(); var host = node.RequestHost.TrimEx();
var path = node.Path.TrimEx(); var path = node.Path.TrimEx();
var sni = node.Sni.TrimEx(); var sni = node.Sni.TrimEx();
var certs = node.Cert
?.Split("-----END CERTIFICATE-----", StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.TrimEx())
.Where(s => !s.IsNullOrEmpty())
.Select(s => s + "\n-----END CERTIFICATE-----")
.Select(s => s.Replace("\r\n", "\n"))
.ToList() ?? new();
var useragent = ""; var useragent = "";
if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty()) if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty())
{ {
@@ -284,6 +277,7 @@ public partial class CoreConfigV2rayService
{ {
tlsSettings.serverName = Utils.String2List(host)?.First(); tlsSettings.serverName = Utils.String2List(host)?.First();
} }
var certs = CertPemManager.ParsePemChain(node.Cert);
if (certs.Count > 0) if (certs.Count > 0)
{ {
var certsettings = new List<CertificateSettings4Ray>(); var certsettings = new List<CertificateSettings4Ray>();

View File

@@ -313,7 +313,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
_ => null, _ => null,
}; };
} }
else if (Utils.IsOSX()) else if (Utils.IsMacOS())
{ {
return RuntimeInformation.ProcessArchitecture switch return RuntimeInformation.ProcessArchitecture switch
{ {

View File

@@ -2,8 +2,6 @@ namespace ServiceLib.ViewModels;
public class AddServerViewModel : MyReactiveObject public class AddServerViewModel : MyReactiveObject
{ {
private string _certError = string.Empty;
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public ProfileItem SelectedSource { get; set; }
@@ -112,11 +110,11 @@ public class AddServerViewModel : MyReactiveObject
} }
} }
private void UpdateCertTip() private void UpdateCertTip(string? errorMessage = null)
{ {
CertTip = _certError.IsNullOrEmpty() CertTip = errorMessage.IsNullOrEmpty()
? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet) ? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet)
: _certError; : errorMessage;
} }
private async Task FetchCert() private async Task FetchCert()
@@ -137,16 +135,16 @@ public class AddServerViewModel : MyReactiveObject
} }
if (!Utils.IsDomain(serverName)) if (!Utils.IsDomain(serverName))
{ {
_certError = ResUI.ServerNameMustBeValidDomain; UpdateCertTip(ResUI.ServerNameMustBeValidDomain);
UpdateCertTip();
_certError = string.Empty;
return; return;
} }
if (SelectedSource.Port > 0) if (SelectedSource.Port > 0)
{ {
domain += $":{SelectedSource.Port}"; domain += $":{SelectedSource.Port}";
} }
Cert = await CertPemManager.Instance.GetCertPemAsync(domain, serverName); string certError;
(Cert, certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName);
UpdateCertTip(certError);
} }
private async Task FetchCertChain() private async Task FetchCertChain()
@@ -167,16 +165,16 @@ public class AddServerViewModel : MyReactiveObject
} }
if (!Utils.IsDomain(serverName)) if (!Utils.IsDomain(serverName))
{ {
_certError = ResUI.ServerNameMustBeValidDomain; UpdateCertTip(ResUI.ServerNameMustBeValidDomain);
UpdateCertTip();
_certError = string.Empty;
return; return;
} }
if (SelectedSource.Port > 0) if (SelectedSource.Port > 0)
{ {
domain += $":{SelectedSource.Port}"; domain += $":{SelectedSource.Port}";
} }
var certs = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName); string certError;
Cert = string.Join("\n", certs); (var certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName);
Cert = CertPemManager.ConcatenatePemChain(certs);
UpdateCertTip(certError);
} }
} }

View File

@@ -62,9 +62,9 @@ public class MainWindowViewModel : MyReactiveObject
[Reactive] [Reactive]
public int TabMainSelectedIndex { get; set; } public int TabMainSelectedIndex { get; set; }
#endregion Menu [Reactive] public bool BlIsWindows { get; set; }
private bool _hasNextReloadJob = false; #endregion Menu
#region Init #region Init
@@ -72,6 +72,7 @@ public class MainWindowViewModel : MyReactiveObject
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
BlIsWindows = Utils.IsWindows();
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
@@ -268,7 +269,6 @@ public class MainWindowViewModel : MyReactiveObject
} }
await RefreshServers(); await RefreshServers();
BlReloadEnabled = true;
await Reload(); await Reload();
} }
@@ -514,7 +514,7 @@ public class MainWindowViewModel : MyReactiveObject
{ {
ProcUtils.ProcessStart("xdg-open", path); ProcUtils.ProcessStart("xdg-open", path);
} }
else if (Utils.IsOSX()) else if (Utils.IsMacOS())
{ {
ProcUtils.ProcessStart("open", path); ProcUtils.ProcessStart("open", path);
} }
@@ -525,58 +525,74 @@ public class MainWindowViewModel : MyReactiveObject
#region core job #region core job
private bool _hasNextReloadJob = false;
private readonly SemaphoreSlim _reloadSemaphore = new(1, 1);
public async Task Reload() public async Task Reload()
{ {
//If there are unfinished reload job, marked with next job. //If there are unfinished reload job, marked with next job.
if (!BlReloadEnabled) if (!await _reloadSemaphore.WaitAsync(0))
{ {
_hasNextReloadJob = true; _hasNextReloadJob = true;
return; return;
} }
BlReloadEnabled = false; try
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId);
if (msgs.Count > 0)
{ {
foreach (var msg in msgs) SetReloadEnabled(false);
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId);
if (msgs.Count > 0)
{ {
NoticeManager.Instance.SendMessage(msg); foreach (var msg in msgs)
{
NoticeManager.Instance.SendMessage(msg);
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return;
} }
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
BlReloadEnabled = true; await Task.Run(async () =>
return; {
await LoadCore();
await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000);
});
AppEvents.TestServerRequested.Publish();
var showClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (showClashUI)
{
AppEvents.ProxiesReloadRequested.Publish();
}
ReloadResult(showClashUI);
} }
finally
await Task.Run(async () =>
{ {
await LoadCore(); SetReloadEnabled(true);
await SysProxyHandler.UpdateSysProxy(_config, false); _reloadSemaphore.Release();
await Task.Delay(1000); //If there is a next reload job, execute it.
}); if (_hasNextReloadJob)
AppEvents.TestServerRequested.Publish(); {
_hasNextReloadJob = false;
var showClashUI = _config.IsRunningCore(ECoreType.sing_box); await Reload();
if (showClashUI) }
{
AppEvents.ProxiesReloadRequested.Publish();
}
RxApp.MainThreadScheduler.Schedule(() => ReloadResult(showClashUI));
BlReloadEnabled = true;
if (_hasNextReloadJob)
{
_hasNextReloadJob = false;
await Reload();
} }
} }
private void ReloadResult(bool showClashUI) private void ReloadResult(bool showClashUI)
{ {
// BlReloadEnabled = true; RxApp.MainThreadScheduler.Schedule(() =>
ShowClashUI = showClashUI; {
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0; ShowClashUI = showClashUI;
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
});
}
private void SetReloadEnabled(bool enabled)
{
RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled);
} }
private async Task LoadCore() private async Task LoadCore()

View File

@@ -50,6 +50,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool EnableUpdateSubOnlyRemarksExist { get; set; } [Reactive] public bool EnableUpdateSubOnlyRemarksExist { get; set; }
[Reactive] public bool AutoHideStartup { get; set; } [Reactive] public bool AutoHideStartup { get; set; }
[Reactive] public bool Hide2TrayWhenClose { get; set; } [Reactive] public bool Hide2TrayWhenClose { get; set; }
[Reactive] public bool MacOSShowInDock { get; set; }
[Reactive] public bool EnableDragDropSort { get; set; } [Reactive] public bool EnableDragDropSort { get; set; }
[Reactive] public bool DoubleClick2Activate { get; set; } [Reactive] public bool DoubleClick2Activate { get; set; }
[Reactive] public int AutoUpdateInterval { get; set; } [Reactive] public int AutoUpdateInterval { get; set; }
@@ -69,11 +70,22 @@ public class OptionSettingViewModel : MyReactiveObject
#endregion UI #endregion UI
#region UI visibility
[Reactive] public bool BlIsWindows { get; set; }
[Reactive] public bool BlIsLinux { get; set; }
[Reactive] public bool BlIsIsMacOS { get; set; }
[Reactive] public bool BlIsNonWindows { get; set; }
#endregion UI visibility
#region System proxy #region System proxy
[Reactive] public bool notProxyLocalAddress { get; set; } [Reactive] public bool notProxyLocalAddress { get; set; }
[Reactive] public string systemProxyAdvancedProtocol { get; set; } [Reactive] public string systemProxyAdvancedProtocol { get; set; }
[Reactive] public string systemProxyExceptions { get; set; } [Reactive] public string systemProxyExceptions { get; set; }
[Reactive] public string CustomSystemProxyPacPath { get; set; }
[Reactive] public string CustomSystemProxyScriptPath { get; set; }
#endregion System proxy #endregion System proxy
@@ -106,6 +118,10 @@ public class OptionSettingViewModel : MyReactiveObject
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
BlIsWindows = Utils.IsWindows();
BlIsLinux = Utils.IsLinux();
BlIsIsMacOS = Utils.IsMacOS();
BlIsNonWindows = Utils.IsNonWindows();
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -167,6 +183,7 @@ public class OptionSettingViewModel : MyReactiveObject
EnableUpdateSubOnlyRemarksExist = _config.UiItem.EnableUpdateSubOnlyRemarksExist; EnableUpdateSubOnlyRemarksExist = _config.UiItem.EnableUpdateSubOnlyRemarksExist;
AutoHideStartup = _config.UiItem.AutoHideStartup; AutoHideStartup = _config.UiItem.AutoHideStartup;
Hide2TrayWhenClose = _config.UiItem.Hide2TrayWhenClose; Hide2TrayWhenClose = _config.UiItem.Hide2TrayWhenClose;
MacOSShowInDock = _config.UiItem.MacOSShowInDock;
EnableDragDropSort = _config.UiItem.EnableDragDropSort; EnableDragDropSort = _config.UiItem.EnableDragDropSort;
DoubleClick2Activate = _config.UiItem.DoubleClick2Activate; DoubleClick2Activate = _config.UiItem.DoubleClick2Activate;
AutoUpdateInterval = _config.GuiItem.AutoUpdateInterval; AutoUpdateInterval = _config.GuiItem.AutoUpdateInterval;
@@ -191,6 +208,8 @@ public class OptionSettingViewModel : MyReactiveObject
notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress; notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress;
systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol; systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol;
systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions; systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions;
CustomSystemProxyPacPath = _config.SystemProxyItem.CustomSystemProxyPacPath;
CustomSystemProxyScriptPath = _config.SystemProxyItem.CustomSystemProxyScriptPath;
#endregion System proxy #endregion System proxy
@@ -326,6 +345,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.UiItem.EnableUpdateSubOnlyRemarksExist = EnableUpdateSubOnlyRemarksExist; _config.UiItem.EnableUpdateSubOnlyRemarksExist = EnableUpdateSubOnlyRemarksExist;
_config.UiItem.AutoHideStartup = AutoHideStartup; _config.UiItem.AutoHideStartup = AutoHideStartup;
_config.UiItem.Hide2TrayWhenClose = Hide2TrayWhenClose; _config.UiItem.Hide2TrayWhenClose = Hide2TrayWhenClose;
_config.UiItem.MacOSShowInDock = MacOSShowInDock;
_config.GuiItem.AutoUpdateInterval = AutoUpdateInterval; _config.GuiItem.AutoUpdateInterval = AutoUpdateInterval;
_config.UiItem.EnableDragDropSort = EnableDragDropSort; _config.UiItem.EnableDragDropSort = EnableDragDropSort;
_config.UiItem.DoubleClick2Activate = DoubleClick2Activate; _config.UiItem.DoubleClick2Activate = DoubleClick2Activate;
@@ -347,6 +367,8 @@ public class OptionSettingViewModel : MyReactiveObject
_config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions; _config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions;
_config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress; _config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress;
_config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol; _config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol;
_config.SystemProxyItem.CustomSystemProxyPacPath = CustomSystemProxyPacPath;
_config.SystemProxyItem.CustomSystemProxyScriptPath = CustomSystemProxyScriptPath;
//tun mode //tun mode
_config.TunModeItem.AutoRoute = TunAutoRoute; _config.TunModeItem.AutoRoute = TunAutoRoute;

View File

@@ -504,7 +504,7 @@ public class StatusBarViewModel : MyReactiveObject
{ {
return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); return AppManager.Instance.LinuxSudoPwd.IsNotEmpty();
} }
else if (Utils.IsOSX()) else if (Utils.IsMacOS())
{ {
return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); return AppManager.Instance.LinuxSudoPwd.IsNotEmpty();
} }

View File

@@ -26,7 +26,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
Height = sizeItem.Height; Height = sizeItem.Height;
var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea; var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea;
var scaling = (Utils.IsOSX() ? null : VisualRoot?.RenderScaling) ?? 1.0; var scaling = (Utils.IsMacOS() ? null : VisualRoot?.RenderScaling) ?? 1.0;
var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2); var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2);
var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2); var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2);

View File

@@ -800,9 +800,11 @@
<Flyout> <Flyout>
<StackPanel> <StackPanel>
<TextBlock <TextBlock
Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertPinningTips}" /> Text="{x:Static resx:ResUI.TbCertPinningTips}"
TextWrapping="Wrap" />
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal"> <StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Button <Button
x:Name="btnFetchCert" x:Name="btnFetchCert"
@@ -816,13 +818,10 @@
<TextBox <TextBox
x:Name="txtCert" x:Name="txtCert"
Width="400" Width="400"
MinHeight="100"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Stretch" AcceptsReturn="True"
VerticalAlignment="Center"
Classes="TextArea" Classes="TextArea"
MinLines="6" TextWrapping="NoWrap" />
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</Flyout> </Flyout>
</Button.Flyout> </Button.Flyout>

View File

@@ -75,10 +75,10 @@
<MenuItem x:Name="menuRoutingSetting" Header="{x:Static resx:ResUI.menuRoutingSetting}" /> <MenuItem x:Name="menuRoutingSetting" Header="{x:Static resx:ResUI.menuRoutingSetting}" />
<MenuItem x:Name="menuDNSSetting" Header="{x:Static resx:ResUI.menuDNSSetting}" /> <MenuItem x:Name="menuDNSSetting" Header="{x:Static resx:ResUI.menuDNSSetting}" />
<MenuItem x:Name="menuFullConfigTemplate" Header="{x:Static resx:ResUI.menuFullConfigTemplate}" /> <MenuItem x:Name="menuFullConfigTemplate" Header="{x:Static resx:ResUI.menuFullConfigTemplate}" />
<MenuItem x:Name="menuGlobalHotkeySetting" Header="{x:Static resx:ResUI.menuGlobalHotkeySetting}" /> <MenuItem x:Name="menuGlobalHotkeySetting" Header="{x:Static resx:ResUI.menuGlobalHotkeySetting}" IsVisible="{Binding BlIsWindows}" />
<Separator /> <Separator />
<MenuItem x:Name="menuRebootAsAdmin" Header="{x:Static resx:ResUI.menuRebootAsAdmin}" /> <MenuItem x:Name="menuRebootAsAdmin" Header="{x:Static resx:ResUI.menuRebootAsAdmin}" IsVisible="{Binding BlIsWindows}" />
<MenuItem x:Name="menuSettingsSetUWP" Header="{x:Static resx:ResUI.TbSettingsSetUWP}" /> <MenuItem x:Name="menuSettingsSetUWP" Header="{x:Static resx:ResUI.TbSettingsSetUWP}" IsVisible="{Binding BlIsWindows}" />
<MenuItem x:Name="menuClearServerStatistics" Header="{x:Static resx:ResUI.menuClearServerStatistics}" /> <MenuItem x:Name="menuClearServerStatistics" Header="{x:Static resx:ResUI.menuClearServerStatistics}" />
<Separator /> <Separator />
<MenuItem Header="{x:Static resx:ResUI.menuRegionalPresets}"> <MenuItem Header="{x:Static resx:ResUI.menuRegionalPresets}">

View File

@@ -161,10 +161,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
else else
{ {
Title = $"{Utils.GetVersion()}"; Title = $"{Utils.GetVersion()}";
menuRebootAsAdmin.IsVisible = false;
menuSettingsSetUWP.IsVisible = false;
menuGlobalHotkeySetting.IsVisible = false;
} }
menuAddServerViaScan.IsVisible = false; menuAddServerViaScan.IsVisible = false;

View File

@@ -356,11 +356,11 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
x:Name="tbAutoRunTip"
Grid.Row="1" Grid.Row="1"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding BlIsWindows}"
Text="{x:Static resx:ResUI.TbSettingsStartBootTip}" Text="{x:Static resx:ResUI.TbSettingsStartBootTip}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
@@ -443,26 +443,42 @@
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
x:Name="labHide2TrayWhenClose"
Grid.Row="9" Grid.Row="9"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding BlIsLinux}"
Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenClose}" /> Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenClose}" />
<ToggleSwitch <ToggleSwitch
x:Name="togHide2TrayWhenClose" x:Name="togHide2TrayWhenClose"
Grid.Row="9" Grid.Row="9"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left"
IsVisible="{Binding BlIsLinux}" />
<TextBlock <TextBlock
x:Name="labHide2TrayWhenCloseTip"
Grid.Row="9" Grid.Row="9"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding BlIsLinux}"
Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenCloseTip}" /> Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenCloseTip}" />
<TextBlock
Grid.Row="10"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
IsVisible="{Binding BlIsIsMacOS}"
Text="{x:Static resx:ResUI.TbSettingsMacOSShowInDock}" />
<ToggleSwitch
x:Name="togMacOSShowInDock"
Grid.Row="10"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
IsVisible="{Binding BlIsIsMacOS}"/>
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="11"
Grid.Column="0" Grid.Column="0"
@@ -675,8 +691,31 @@
<TabItem Name="tabSystemproxy" Header="{x:Static resx:ResUI.TbSettingsSystemproxy}"> <TabItem Name="tabSystemproxy" Header="{x:Static resx:ResUI.TbSettingsSystemproxy}">
<DockPanel Margin="{StaticResource Margin8}"> <DockPanel Margin="{StaticResource Margin8}">
<StackPanel <StackPanel
Name="panSystemProxyAdvanced"
DockPanel.Dock="Bottom" DockPanel.Dock="Bottom"
IsVisible="{Binding BlIsNonWindows}"
Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsCustomSystemProxyScriptPath}" />
<TextBox
x:Name="txtCustomSystemProxyScriptPath"
Width="600"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
TextWrapping="Wrap"
Watermark="proxy_set.sh" />
<Button
x:Name="btnBrowseCustomSystemProxyScriptPath"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbBrowse}" />
</StackPanel>
</StackPanel>
<StackPanel
DockPanel.Dock="Bottom"
IsVisible="{Binding BlIsWindows}"
Orientation="Vertical"> Orientation="Vertical">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock <TextBlock
@@ -699,19 +738,38 @@
MinWidth="400" MinWidth="400"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsCustomSystemProxyPacPath}" />
<TextBox
x:Name="txtCustomSystemProxyPacPath"
Width="600"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
TextWrapping="Wrap"
Watermark="pac.txt" />
<Button
x:Name="btnBrowseCustomSystemProxyPacPath"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbBrowse}" />
</StackPanel>
</StackPanel> </StackPanel>
<TextBlock <TextBlock
Name="txbSettingsExceptionTip"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
DockPanel.Dock="Top" DockPanel.Dock="Top"
IsVisible="{Binding BlIsWindows}"
Text="{x:Static resx:ResUI.TbSettingsExceptionTip}" /> Text="{x:Static resx:ResUI.TbSettingsExceptionTip}" />
<TextBlock <TextBlock
Name="txbSettingsExceptionTip2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
DockPanel.Dock="Top" DockPanel.Dock="Top"
IsVisible="{Binding BlIsNonWindows}"
Text="{x:Static resx:ResUI.TbSettingsExceptionTip2}" /> Text="{x:Static resx:ResUI.TbSettingsExceptionTip2}" />
<TextBox <TextBox
x:Name="txtsystemProxyExceptions" x:Name="txtsystemProxyExceptions"

View File

@@ -1,4 +1,5 @@
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -17,6 +18,9 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
ViewModel = new OptionSettingViewModel(UpdateViewHandler); ViewModel = new OptionSettingViewModel(UpdateViewHandler);
clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged; clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged;
btnBrowseCustomSystemProxyPacPath.Click += BtnBrowseCustomSystemProxyPacPath_Click;
btnBrowseCustomSystemProxyScriptPath.Click += BtnBrowseCustomSystemProxyScriptPath_Click;
clbdestOverride.ItemsSource = Global.destOverrideProtocols; clbdestOverride.ItemsSource = Global.destOverrideProtocols;
_config.Inbound.First().DestOverride?.ForEach(it => _config.Inbound.First().DestOverride?.ForEach(it =>
{ {
@@ -84,6 +88,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.EnableUpdateSubOnlyRemarksExist, v => v.togEnableUpdateSubOnlyRemarksExist.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableUpdateSubOnlyRemarksExist, v => v.togEnableUpdateSubOnlyRemarksExist.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MacOSShowInDock, v => v.togMacOSShowInDock.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DoubleClick2Activate, v => v.togDoubleClick2Activate.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DoubleClick2Activate, v => v.togDoubleClick2Activate.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoUpdateInterval, v => v.txtautoUpdateInterval.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoUpdateInterval, v => v.txtautoUpdateInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentFontFamily, v => v.cmbcurrentFontFamily.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CurrentFontFamily, v => v.cmbcurrentFontFamily.Text).DisposeWith(disposables);
@@ -101,6 +106,8 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyPacPath, v => v.txtCustomSystemProxyPacPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyScriptPath, v => v.txtCustomSystemProxyScriptPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables);
@@ -119,33 +126,6 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
}); });
if (Utils.IsWindows())
{
txbSettingsExceptionTip2.IsVisible = false;
labHide2TrayWhenClose.IsVisible = false;
togHide2TrayWhenClose.IsVisible = false;
labHide2TrayWhenCloseTip.IsVisible = false;
}
else if (Utils.IsLinux())
{
txbSettingsExceptionTip.IsVisible = false;
panSystemProxyAdvanced.IsVisible = false;
tbAutoRunTip.IsVisible = false;
}
else if (Utils.IsOSX())
{
txbSettingsExceptionTip.IsVisible = false;
panSystemProxyAdvanced.IsVisible = false;
tbAutoRunTip.IsVisible = false;
labHide2TrayWhenClose.IsVisible = false;
togHide2TrayWhenClose.IsVisible = false;
labHide2TrayWhenCloseTip.IsVisible = false;
}
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@@ -212,6 +192,28 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
} }
} }
private async void BtnBrowseCustomSystemProxyPacPath_Click(object? sender, RoutedEventArgs e)
{
var fileName = await UI.OpenFileDialog(this, null);
if (fileName.IsNullOrEmpty())
{
return;
}
txtCustomSystemProxyPacPath.Text = fileName;
}
private async void BtnBrowseCustomSystemProxyScriptPath_Click(object? sender, RoutedEventArgs e)
{
var fileName = await UI.OpenFileDialog(this, null);
if (fileName.IsNullOrEmpty())
{
return;
}
txtCustomSystemProxyScriptPath.Text = fileName;
}
private void Window_Loaded(object? sender, RoutedEventArgs e) private void Window_Loaded(object? sender, RoutedEventArgs e)
{ {
btnCancel.Focus(); btnCancel.Focus();

View File

@@ -1021,6 +1021,7 @@
Style="{StaticResource MaterialDesignToolForegroundPopupBox}"> Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
<StackPanel> <StackPanel>
<TextBlock <TextBlock
Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
@@ -1029,13 +1030,11 @@
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal"> <StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Button <Button
x:Name="btnFetchCert" x:Name="btnFetchCert"
Width="100"
Margin="{StaticResource MarginLeftRight4}" Margin="{StaticResource MarginLeftRight4}"
Content="{x:Static resx:ResUI.TbFetchCert}" Content="{x:Static resx:ResUI.TbFetchCert}"
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
<Button <Button
x:Name="btnFetchCertChain" x:Name="btnFetchCertChain"
Width="100"
Margin="{StaticResource MarginLeftRight4}" Margin="{StaticResource MarginLeftRight4}"
Content="{x:Static resx:ResUI.TbFetchCertChain}" Content="{x:Static resx:ResUI.TbFetchCertChain}"
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
@@ -1044,15 +1043,16 @@
x:Name="txtCert" x:Name="txtCert"
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
AcceptsReturn="True" AcceptsReturn="True"
HorizontalScrollBarVisibility="Auto"
MaxLines="18"
MinLines="6" MinLines="6"
Style="{StaticResource MyOutlinedTextBox}" Style="{StaticResource MyOutlinedTextBox}"
TextWrapping="Wrap" /> TextWrapping="NoWrap"
VerticalScrollBarVisibility="Auto" />
</StackPanel> </StackPanel>
</materialDesign:PopupBox> </materialDesign:PopupBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid <Grid
x:Name="gridRealityMore" x:Name="gridRealityMore"

View File

@@ -976,6 +976,28 @@
materialDesign:HintAssist.Hint="Protocol" materialDesign:HintAssist.Hint="Protocol"
Style="{StaticResource DefComboBox}" /> Style="{StaticResource DefComboBox}" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsCustomSystemProxyPacPath}" />
<TextBox
x:Name="txtCustomSystemProxyPacPath"
Width="600"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
AcceptsReturn="True"
Style="{StaticResource DefTextBox}"
TextWrapping="Wrap" />
<Button
x:Name="btnBrowseCustomSystemProxyPacPath"
Margin="{StaticResource MarginLeftRight4}"
Content="{x:Static resx:ResUI.TbBrowse}"
Style="{StaticResource DefButton}" />
</StackPanel>
</StackPanel> </StackPanel>
<TextBlock <TextBlock

View File

@@ -16,6 +16,8 @@ public partial class OptionSettingWindow
ViewModel = new OptionSettingViewModel(UpdateViewHandler); ViewModel = new OptionSettingViewModel(UpdateViewHandler);
clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged; clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged;
btnBrowseCustomSystemProxyPacPath.Click += BtnBrowseCustomSystemProxyPacPath_Click;
clbdestOverride.ItemsSource = Global.destOverrideProtocols; clbdestOverride.ItemsSource = Global.destOverrideProtocols;
_config.Inbound.First().DestOverride?.ForEach(it => _config.Inbound.First().DestOverride?.ForEach(it =>
{ {
@@ -110,6 +112,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyPacPath, v => v.txtCustomSystemProxyPacPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables);
@@ -210,4 +213,15 @@ public partial class OptionSettingWindow
ViewModel.destOverride = clbdestOverride.SelectedItems.Cast<string>().ToList(); ViewModel.destOverride = clbdestOverride.SelectedItems.Cast<string>().ToList();
} }
} }
private void BtnBrowseCustomSystemProxyPacPath_Click(object sender, RoutedEventArgs e)
{
if (UI.OpenFileDialog(out var fileName,
"Txt|*.txt|All|*.*") != true)
{
return;
}
txtCustomSystemProxyPacPath.Text = fileName;
}
} }