Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f39bc6d3b0 | ||
|
|
0c0ecc359b | ||
|
|
68713e7b77 | ||
|
|
899b3fc97b | ||
|
|
a1490d0ac1 | ||
|
|
b23f49ffce | ||
|
|
9a9e28e494 | ||
|
|
65ee5eb510 | ||
|
|
1f42d32e1a | ||
|
|
f2ed8c1d6b | ||
|
|
308b216d1b | ||
|
|
c713f5c8f5 | ||
|
|
6771eb25d1 | ||
|
|
91af50f99a | ||
|
|
a559586e71 | ||
|
|
929520775d | ||
|
|
4eaf31bbf8 | ||
|
|
1607525539 | ||
|
|
31b5b4ca0c | ||
|
|
64c7fea2bc | ||
|
|
f76fd364a2 | ||
|
|
0a1d6db9d1 | ||
|
|
7a750a127e | ||
|
|
fce4a7b74c | ||
|
|
fec7353703 | ||
|
|
40c90d5b3b | ||
|
|
9c58fec8d4 | ||
|
|
11343a30fd | ||
|
|
3693a7fee6 | ||
|
|
a452bbe140 | ||
|
|
185c5e4bfb | ||
|
|
bbe64aa970 | ||
|
|
513662d89a | ||
|
|
22f0d04f01 | ||
|
|
d7c5161431 | ||
|
|
12cc09d0c9 | ||
|
|
5b12c36da5 | ||
|
|
e970372a9f | ||
|
|
5d6c5da9d9 | ||
|
|
ade2db3903 | ||
|
|
7f07279a4c | ||
|
|
b25d4d57bd | ||
|
|
46edd8f9a4 | ||
|
|
ebb95b5ee8 | ||
|
|
dc4611a258 | ||
|
|
03d5b7a05b | ||
|
|
a652fd879b | ||
|
|
326bf334e7 | ||
|
|
21a773f400 | ||
|
|
d86003df55 | ||
|
|
faff8e4ea2 | ||
|
|
6b85aa0b03 | ||
|
|
671678724b | ||
|
|
e96a4818c4 | ||
|
|
0377e7ce19 | ||
|
|
6929886b3e | ||
|
|
721d70c8c7 | ||
|
|
27b45aee83 | ||
|
|
18ac76e683 | ||
|
|
3e1e23a524 | ||
|
|
534c7ab444 | ||
|
|
c2c13ad318 | ||
|
|
3a21596d95 | ||
|
|
ef30d389dc | ||
|
|
bf8783fed7 | ||
|
|
4e042295d2 | ||
|
|
33d9c5db6c | ||
|
|
cb182125f6 | ||
|
|
ec627bdb82 | ||
|
|
4606e78570 | ||
|
|
f00e968b8f | ||
|
|
a87a015c03 | ||
|
|
c559914ff7 | ||
|
|
436d95576e | ||
|
|
54e83391d0 | ||
|
|
3e0578f775 | ||
|
|
29a5abf4d6 | ||
|
|
b54c67d6f1 | ||
|
|
b49486cc23 | ||
|
|
b95830b3d5 | ||
|
|
8e0c5cb9aa | ||
|
|
6ffb3bd30c | ||
|
|
2826444ffc | ||
|
|
56c3e9c46d | ||
|
|
0770e30034 | ||
|
|
04195c2957 | ||
|
|
d18d74ac1c | ||
|
|
6391667c15 | ||
|
|
7f26445327 | ||
|
|
291d4bd8e5 | ||
|
|
f2f3a7eb5f | ||
|
|
e7609619d4 | ||
|
|
84bf9ecfaf | ||
|
|
a2917b3ce8 | ||
|
|
d094370209 | ||
|
|
1a6fbf782d | ||
|
|
3f67a23f8b | ||
|
|
b8eb7e7b29 | ||
|
|
1d69916410 | ||
|
|
49fa103077 | ||
|
|
e3a63db966 | ||
|
|
ef4a1903ec | ||
|
|
5a3286dad1 | ||
|
|
058c6e4a85 | ||
|
|
ea1d438e40 | ||
|
|
a108eaf34b | ||
|
|
da28c639b3 | ||
|
|
8ef68127d4 | ||
|
|
f39d966a33 | ||
|
|
f83e83de13 | ||
|
|
abdafc9b3b | ||
|
|
8f93c50151 | ||
|
|
fe7c505cc9 | ||
|
|
0d5afa4ff5 | ||
|
|
2ad716a4ad | ||
|
|
cddf88730f | ||
|
|
3eb49aa24c | ||
|
|
45c987fd86 | ||
|
|
7bec05ec23 | ||
|
|
606b216cd0 | ||
|
|
bb4f33559f | ||
|
|
c7f3e53f28 | ||
|
|
0035e836d7 | ||
|
|
e6da14f4a8 | ||
|
|
f748f1849c | ||
|
|
f8995b78f6 | ||
|
|
a861020828 | ||
|
|
dc94962900 | ||
|
|
4a40b87bba | ||
|
|
4853e2348d | ||
|
|
e104f9f9b2 | ||
|
|
876381a7fb | ||
|
|
4f711b1bd3 | ||
|
|
89893c0945 | ||
|
|
7b7fe0ef46 | ||
|
|
f66226c103 | ||
|
|
d5c50ef27c | ||
|
|
2060ac18fd | ||
|
|
c9c1cd8cbb |
40
.github/workflows/build-linux.yml
vendored
40
.github/workflows/build-linux.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
matrix:
|
||||
configuration: [Release]
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Setup
|
||||
uses: actions/setup-dotnet@v4.3.1
|
||||
uses: actions/setup-dotnet@v5.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
@@ -98,4 +98,38 @@ jobs:
|
||||
file: ${{ github.workspace }}/v2rayN*.zip
|
||||
tag: ${{ github.event.inputs.release_tag }}
|
||||
file_glob: true
|
||||
prerelease: true
|
||||
prerelease: true
|
||||
|
||||
# release RHEL package
|
||||
- name: Package RPM (RHEL-family)
|
||||
if: github.event.inputs.release_tag != ''
|
||||
run: |
|
||||
chmod 755 package-rhel.sh
|
||||
# Build for both x86_64 and aarch64 in one go (explicit version passed; no --buildfrom)
|
||||
./package-rhel.sh "${{ github.event.inputs.release_tag }}" --arch all
|
||||
|
||||
- name: Collect RPMs into workspace
|
||||
if: github.event.inputs.release_tag != ''
|
||||
run: |
|
||||
mkdir -p "${{ github.workspace }}/dist/rpm"
|
||||
rsync -av "$HOME/rpmbuild/RPMS/" "${{ github.workspace }}/dist/rpm/"
|
||||
# Rename to requested filenames
|
||||
find "${{ github.workspace }}/dist/rpm" -name "v2rayN-*-1.x86_64.rpm" -exec mv {} "${{ github.workspace }}/dist/rpm/v2rayN-linux-rhel-x64.rpm" \; || true
|
||||
find "${{ github.workspace }}/dist/rpm" -name "v2rayN-*-1.aarch64.rpm" -exec mv {} "${{ github.workspace }}/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
|
||||
|
||||
- name: Upload RPM artifacts
|
||||
if: github.event.inputs.release_tag != ''
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: v2rayN-rpm
|
||||
path: |
|
||||
${{ github.workspace }}/dist/rpm/**/*.rpm
|
||||
|
||||
- name: Upload RPMs to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: github.event.inputs.release_tag != ''
|
||||
with:
|
||||
file: ${{ github.workspace }}/dist/rpm/**/*.rpm
|
||||
tag: ${{ github.event.inputs.release_tag }}
|
||||
file_glob: true
|
||||
prerelease: true
|
||||
|
||||
2
.github/workflows/build-osx.yml
vendored
2
.github/workflows/build-osx.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Setup
|
||||
uses: actions/setup-dotnet@v4.3.1
|
||||
uses: actions/setup-dotnet@v5.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
|
||||
2
.github/workflows/build-windows-desktop.yml
vendored
2
.github/workflows/build-windows-desktop.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Setup
|
||||
uses: actions/setup-dotnet@v4.3.1
|
||||
uses: actions/setup-dotnet@v5.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
|
||||
2
.github/workflows/build-windows.yml
vendored
2
.github/workflows/build-windows.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: actions/setup-dotnet@v4.3.1
|
||||
uses: actions/setup-dotnet@v5.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -397,4 +397,5 @@ FodyWeavers.xsd
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
@@ -1,14 +1,67 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install deps
|
||||
sudo apt update -y
|
||||
sudo apt install -y libfuse2
|
||||
wget -O pkg2appimage https://github.com/AppImageCommunity/pkg2appimage/releases/download/continuous/pkg2appimage-1eceb30-x86_64.AppImage
|
||||
chmod a+x pkg2appimage
|
||||
export AppImageOutputArch=$OutputArch
|
||||
export OutputPath=$OutputPath64
|
||||
./pkg2appimage ./pkg2appimage.yml
|
||||
mv out/*.AppImage v2rayN-${AppImageOutputArch}.AppImage
|
||||
export AppImageOutputArch=$OutputArchArm
|
||||
export OutputPath=$OutputPathArm64
|
||||
./pkg2appimage ./pkg2appimage.yml
|
||||
mv out/*.AppImage v2rayN-${AppImageOutputArch}.AppImage
|
||||
sudo apt install -y libfuse2 wget file
|
||||
|
||||
# Get tools
|
||||
wget -qO appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool
|
||||
|
||||
# x86_64 AppDir
|
||||
APPDIR_X64="AppDir-x86_64"
|
||||
rm -rf "$APPDIR_X64"
|
||||
mkdir -p "$APPDIR_X64/usr/lib/v2rayN" "$APPDIR_X64/usr/bin" "$APPDIR_X64/usr/share/applications" "$APPDIR_X64/usr/share/pixmaps"
|
||||
cp -rf "$OutputPath64"/* "$APPDIR_X64/usr/lib/v2rayN" || true
|
||||
[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/usr/share/pixmaps/v2rayN.png" || true
|
||||
[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/v2rayN.png" || true
|
||||
|
||||
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_X64/AppRun"
|
||||
chmod +x "$APPDIR_X64/AppRun"
|
||||
ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_X64/usr/bin/v2rayN"
|
||||
cat > "$APPDIR_X64/v2rayN.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=v2rayN
|
||||
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
|
||||
Exec=v2rayN
|
||||
Icon=v2rayN
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;
|
||||
EOF
|
||||
install -Dm644 "$APPDIR_X64/v2rayN.desktop" "$APPDIR_X64/usr/share/applications/v2rayN.desktop"
|
||||
|
||||
ARCH=x86_64 ./appimagetool "$APPDIR_X64" "v2rayN-${OutputArch}.AppImage"
|
||||
file "v2rayN-${OutputArch}.AppImage" | grep -q 'x86-64'
|
||||
|
||||
# aarch64 AppDir
|
||||
APPDIR_ARM64="AppDir-aarch64"
|
||||
rm -rf "$APPDIR_ARM64"
|
||||
mkdir -p "$APPDIR_ARM64/usr/lib/v2rayN" "$APPDIR_ARM64/usr/bin" "$APPDIR_ARM64/usr/share/applications" "$APPDIR_ARM64/usr/share/pixmaps"
|
||||
cp -rf "$OutputPathArm64"/* "$APPDIR_ARM64/usr/lib/v2rayN" || true
|
||||
[ -f "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_ARM64/usr/share/pixmaps/v2rayN.png" || true
|
||||
[ -f "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_ARM64/v2rayN.png" || true
|
||||
|
||||
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_ARM64/AppRun"
|
||||
chmod +x "$APPDIR_ARM64/AppRun"
|
||||
ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_ARM64/usr/bin/v2rayN"
|
||||
cat > "$APPDIR_ARM64/v2rayN.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=v2rayN
|
||||
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
|
||||
Exec=v2rayN
|
||||
Icon=v2rayN
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;
|
||||
EOF
|
||||
install -Dm644 "$APPDIR_ARM64/v2rayN.desktop" "$APPDIR_ARM64/usr/share/applications/v2rayN.desktop"
|
||||
|
||||
# aarch64 runtime
|
||||
wget -qO runtime-aarch64 https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-aarch64
|
||||
chmod +x runtime-aarch64
|
||||
|
||||
# build aarch64 AppImage
|
||||
ARCH=aarch64 ./appimagetool --runtime-file ./runtime-aarch64 "$APPDIR_ARM64" "v2rayN-${OutputArchArm}.AppImage"
|
||||
file "v2rayN-${OutputArchArm}.AppImage" | grep -q 'ARM aarch64'
|
||||
|
||||
@@ -28,6 +28,7 @@ Package: v2rayN
|
||||
Version: $Version
|
||||
Architecture: $Arch2
|
||||
Maintainer: https://github.com/2dust/v2rayN
|
||||
Depends: desktop-file-utils, xdg-utils
|
||||
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
|
||||
EOF
|
||||
|
||||
@@ -52,7 +53,17 @@ sudo chmod 0755 "${PackagePath}/DEBIAN/postinst"
|
||||
sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN"
|
||||
sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool"
|
||||
|
||||
# desktop && PATH
|
||||
# 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}/opt/v2rayN" -type d -exec chmod 755 {} +
|
||||
# set all regular files to 644 (readable by all users)
|
||||
sudo find "${PackagePath}/opt/v2rayN" -type f -exec chmod 644 {} +
|
||||
# ensure main binaries are 755 (executable by all users)
|
||||
sudo chmod 755 "${PackagePath}/opt/v2rayN/v2rayN" 2>/dev/null || true
|
||||
sudo chmod 755 "${PackagePath}/opt/v2rayN/AmazTool" 2>/dev/null || true
|
||||
|
||||
# build deb package
|
||||
sudo dpkg-deb -Zxz --build $PackagePath
|
||||
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"
|
||||
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"
|
||||
|
||||
808
package-rhel.sh
Normal file
808
package-rhel.sh
Normal file
@@ -0,0 +1,808 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# == Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ==
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
rhel|rocky|almalinux|fedora|centos|ubuntu|debian)
|
||||
echo "[OK] Detected supported system: $NAME $VERSION_ID"
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unsupported system: $NAME ($ID)."
|
||||
echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS or Ubuntu/Debian."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "[ERROR] Cannot detect system (missing /etc/os-release)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ===== Config & Parse arguments =========================================================
|
||||
VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty
|
||||
WITH_CORE="both" # Default: bundle both xray+sing-box
|
||||
AUTOSTART=0 # 1 = enable system-wide autostart (/etc/xdg/autostart)
|
||||
FORCE_NETCORE=0 # --netcore => skip archive bundle, use separate downloads
|
||||
ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target)
|
||||
BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively
|
||||
|
||||
# If the first argument starts with --, do not treat it as a version number
|
||||
if [[ "${VERSION_ARG:-}" == --* ]]; then
|
||||
VERSION_ARG=""
|
||||
fi
|
||||
# Take the first non --* argument as version, discard it
|
||||
if [[ -n "${VERSION_ARG:-}" ]]; then shift || true; fi
|
||||
|
||||
# Parse remaining optional arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--with-core) WITH_CORE="${2:-both}"; shift 2;;
|
||||
--autostart) AUTOSTART=1; shift;;
|
||||
--xray-ver) XRAY_VER="${2:-}"; shift 2;;
|
||||
--singbox-ver) SING_VER="${2:-}"; shift 2;;
|
||||
--netcore) FORCE_NETCORE=1; shift;;
|
||||
--arch) ARCH_OVERRIDE="${2:-}"; shift 2;;
|
||||
--buildfrom) BUILD_FROM="${2:-}"; shift 2;;
|
||||
*)
|
||||
if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi
|
||||
shift;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Conflict: version number AND --buildfrom cannot be used together
|
||||
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
|
||||
echo "[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
|
||||
fi
|
||||
|
||||
# ===== Environment check + Dependencies ========================================
|
||||
host_arch="$(uname -m)"
|
||||
[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; }
|
||||
|
||||
install_ok=0
|
||||
case "$ID" in
|
||||
# ------------------------------ RHEL family (UNCHANGED) ------------------------------
|
||||
rhel|rocky|almalinux|centos)
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
sudo dnf -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \
|
||||
sudo dnf -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
|
||||
install_ok=1
|
||||
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 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
|
||||
;;
|
||||
# ------------------------------ Debian (KEEP, with local dotnet install) ------------
|
||||
debian)
|
||||
sudo apt-get update
|
||||
# Base tools + rpm (provides rpmbuild on Debian) + objdump/strip
|
||||
sudo apt-get -y install curl unzip tar rsync rpm binutils || true
|
||||
# rpmbuild presence check
|
||||
if ! command -v rpmbuild >/dev/null 2>&1; then
|
||||
echo "[ERROR] 'rpmbuild' not found after installing 'rpm'."
|
||||
echo " Please ensure 'rpm' is available from Debian repos."
|
||||
exit 1
|
||||
fi
|
||||
# Try apt for dotnet; fallback to official installer into $HOME/.dotnet
|
||||
if ! command -v dotnet >/dev/null 2>&1; then
|
||||
echo "[INFO] 'dotnet' not found. Installing .NET 8 SDK locally to \$HOME/.dotnet ..."
|
||||
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
|
||||
curl -fsSL https://dot.net/v1/dotnet-install.sh -o "$tmp/dotnet-install.sh"
|
||||
bash "$tmp/dotnet-install.sh" --channel 8.0 --install-dir "$HOME/.dotnet"
|
||||
export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:$PATH"
|
||||
export DOTNET_ROOT="$HOME/.dotnet"
|
||||
if ! command -v dotnet >/dev/null 2>&1; then
|
||||
echo "[ERROR] dotnet installation failed."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
install_ok=1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$install_ok" -ne 1 ]]; then
|
||||
echo "[WARN] Could not auto-install dependencies for '$ID'. Make sure these are available:"
|
||||
echo " dotnet-sdk 8.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on RPM-based distros)"
|
||||
fi
|
||||
|
||||
command -v curl >/dev/null
|
||||
|
||||
# Root directory = the script's location
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Git submodules (best effort)
|
||||
if [[ -f .gitmodules ]]; then
|
||||
git submodule sync --recursive || true
|
||||
git submodule update --init --recursive || true
|
||||
fi
|
||||
|
||||
# ===== Locate project ================================================================
|
||||
PROJECT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||
if [[ ! -f "$PROJECT" ]]; then
|
||||
PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
|
||||
fi
|
||||
[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; }
|
||||
|
||||
# ===== Resolve GUI version & auto checkout ============================================
|
||||
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() {
|
||||
# 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 ========================================
|
||||
BUILT_RPMS=() # Will collect absolute paths of built RPMs
|
||||
BUILT_ALL=0 # Flag to know if we should print the final summary
|
||||
|
||||
# ===== Build (single-arch) function ====================================================
|
||||
build_for_arch() {
|
||||
# $1: target short arch: x64 | arm64
|
||||
local short="$1"
|
||||
local rid rpm_target archdir
|
||||
case "$short" in
|
||||
x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;;
|
||||
arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;;
|
||||
*) echo "[ERROR] Unknown arch '$short' (use x64|arm64)"; return 1;;
|
||||
esac
|
||||
|
||||
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
|
||||
|
||||
# .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 publish "$PROJECT" \
|
||||
-c Release -r "$rid" \
|
||||
-p:PublishSingleFile=false \
|
||||
-p:SelfContained=true \
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true
|
||||
|
||||
# Per-arch variables (scoped)
|
||||
local RID_DIR="$rid"
|
||||
local PUBDIR
|
||||
PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish"
|
||||
[[ -d "$PUBDIR" ]]
|
||||
|
||||
# Make RID_DIR visible to download helpers (they read this var)
|
||||
export RID_DIR
|
||||
|
||||
# Per-arch working area
|
||||
local PKGROOT="v2rayN-publish"
|
||||
local WORKDIR
|
||||
WORKDIR="$(mktemp -d)"
|
||||
trap '[[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"' RETURN
|
||||
|
||||
# rpmbuild topdir selection
|
||||
local TOPDIR SPECDIR SOURCEDIR USE_TOPDIR_DEFINE
|
||||
if [[ "$ID" =~ ^(rhel|rocky|almalinux|centos)$ ]]; then
|
||||
rpmdev-setuptree
|
||||
TOPDIR="${HOME}/rpmbuild"
|
||||
SPECDIR="${TOPDIR}/SPECS"
|
||||
SOURCEDIR="${TOPDIR}/SOURCES"
|
||||
USE_TOPDIR_DEFINE=0
|
||||
else
|
||||
TOPDIR="${WORKDIR}/rpmbuild"
|
||||
SPECDIR="${TOPDIR}/SPECS}"
|
||||
SOURCEDIR="${TOPDIR}/SOURCES"
|
||||
mkdir -p "${SPECDIR}" "${SOURCEDIR}" "${TOPDIR}/BUILD" "${TOPDIR}/RPMS" "${TOPDIR}/SRPMS"
|
||||
USE_TOPDIR_DEFINE=1
|
||||
fi
|
||||
|
||||
# Stage publish content
|
||||
mkdir -p "$WORKDIR/$PKGROOT"
|
||||
cp -a "$PUBDIR/." "$WORKDIR/$PKGROOT/"
|
||||
|
||||
# Optional icon
|
||||
local ICON_CANDIDATE
|
||||
ICON_CANDIDATE="$(dirname "$PROJECT")/../v2rayN.Desktop/v2rayN.png"
|
||||
[[ -f "$ICON_CANDIDATE" ]] && cp "$ICON_CANDIDATE" "$WORKDIR/$PKGROOT/v2rayn.png" || true
|
||||
|
||||
# Prepare bin structure
|
||||
mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box"
|
||||
|
||||
# Bundle / cores per-arch
|
||||
if [[ "$FORCE_NETCORE" -eq 0 ]]; then
|
||||
if download_v2rayn_bundle "$WORKDIR/$PKGROOT"; then
|
||||
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
|
||||
|
||||
# Tarball
|
||||
mkdir -p "$SOURCEDIR"
|
||||
tar -C "$WORKDIR" -czf "$SOURCEDIR/$PKGROOT.tar.gz" "$PKGROOT"
|
||||
|
||||
# SPEC
|
||||
local SPECFILE="$SPECDIR/v2rayN.spec"
|
||||
mkdir -p "$SPECDIR"
|
||||
cat > "$SPECFILE" <<'SPEC'
|
||||
%global debug_package %{nil}
|
||||
%undefine _debuginfo_subpackages
|
||||
%undefine _debugsource_packages
|
||||
# Ignore outdated LTTng dependencies incorrectly reported by the .NET runtime (to avoid installation failures)
|
||||
%global __requires_exclude ^liblttng-ust\.so\..*$
|
||||
|
||||
Name: v2rayN
|
||||
Version: __VERSION__
|
||||
Release: 1%{?dist}
|
||||
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64)
|
||||
License: GPL-3.0-only
|
||||
URL: https://github.com/2dust/v2rayN
|
||||
BugURL: https://github.com/2dust/v2rayN/issues
|
||||
ExclusiveArch: aarch64 x86_64
|
||||
Source0: __PKGROOT__.tar.gz
|
||||
|
||||
# Runtime dependencies (Avalonia / X11 / Fonts / GL)
|
||||
Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon
|
||||
Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL, xdg-utils
|
||||
|
||||
%description
|
||||
v2rayN Linux for Red Hat Enterprise Linux
|
||||
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
|
||||
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS
|
||||
For more information, Please visit our website
|
||||
https://github.com/2dust/v2rayN
|
||||
|
||||
%prep
|
||||
%setup -q -n __PKGROOT__
|
||||
|
||||
%build
|
||||
# no build
|
||||
|
||||
%install
|
||||
install -dm0755 %{buildroot}/opt/v2rayN
|
||||
cp -a * %{buildroot}/opt/v2rayN/
|
||||
|
||||
# Launcher (prefer native ELF first, then DLL fallback)
|
||||
install -dm0755 %{buildroot}%{_bindir}
|
||||
cat > %{buildroot}%{_bindir}/v2rayn << 'EOF'
|
||||
#!/usr/bin/bash
|
||||
set -euo pipefail
|
||||
DIR="/opt/v2rayN"
|
||||
|
||||
# Prefer native apphost
|
||||
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi
|
||||
|
||||
# DLL fallback
|
||||
for dll in v2rayN.Desktop.dll v2rayN.dll; do
|
||||
if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi
|
||||
done
|
||||
|
||||
echo "v2rayN launcher: no executable found in $DIR" >&2
|
||||
ls -l "$DIR" >&2 || true
|
||||
exit 1
|
||||
EOF
|
||||
chmod 0755 %{buildroot}%{_bindir}/v2rayn
|
||||
|
||||
# Desktop file
|
||||
install -dm0755 %{buildroot}%{_datadir}/applications
|
||||
cat > %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=v2rayN
|
||||
Comment=v2rayN for Red Hat Enterprise Linux
|
||||
Exec=v2rayn
|
||||
Icon=v2rayn
|
||||
Terminal=false
|
||||
Categories=Network;
|
||||
EOF
|
||||
|
||||
# Icon
|
||||
if [ -f "%{_builddir}/__PKGROOT__/v2rayn.png" ]; then
|
||||
install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
|
||||
install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
|
||||
fi
|
||||
|
||||
%post
|
||||
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
|
||||
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
|
||||
|
||||
%postun
|
||||
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
|
||||
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
|
||||
|
||||
%files
|
||||
%{_bindir}/v2rayn
|
||||
/opt/v2rayN
|
||||
%{_datadir}/applications/v2rayn.desktop
|
||||
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
|
||||
SPEC
|
||||
|
||||
# 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
|
||||
sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE"
|
||||
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE"
|
||||
|
||||
# ----- Select proper 'strip' per target arch on Ubuntu only (cross-binutils) -----
|
||||
# NOTE: We define only __strip to point to the target-arch strip.
|
||||
# DO NOT override __brp_strip (it must stay the brp script path).
|
||||
local STRIP_ARGS=()
|
||||
if [[ "$ID" == "ubuntu" ]]; then
|
||||
local STRIP_BIN=""
|
||||
if [[ "$short" == "x64" ]]; then
|
||||
STRIP_BIN="/usr/bin/x86_64-linux-gnu-strip"
|
||||
else
|
||||
STRIP_BIN="/usr/bin/aarch64-linux-gnu-strip"
|
||||
fi
|
||||
if [[ -x "$STRIP_BIN" ]]; then
|
||||
STRIP_ARGS=( --define "__strip $STRIP_BIN" )
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build RPM for this arch (force rpm --target to match compile arch)
|
||||
if [[ "$USE_TOPDIR_DEFINE" -eq 1 ]]; then
|
||||
rpmbuild -ba "$SPECFILE" --define "_topdir $TOPDIR" --target "$rpm_target" "${STRIP_ARGS[@]}"
|
||||
else
|
||||
rpmbuild -ba "$SPECFILE" --target "$rpm_target" "${STRIP_ARGS[@]}"
|
||||
fi
|
||||
|
||||
# Copy temporary rpmbuild to ~/rpmbuild on Debian/Ubuntu path
|
||||
if [[ "$USE_TOPDIR_DEFINE" -eq 1 ]]; then
|
||||
mkdir -p "$HOME/rpmbuild"
|
||||
rsync -a "$TOPDIR"/ "$HOME/rpmbuild"/
|
||||
TOPDIR="$HOME/rpmbuild"
|
||||
fi
|
||||
|
||||
echo "Build done for $short. RPM at:"
|
||||
local f
|
||||
for f in "${TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do
|
||||
[[ -e "$f" ]] || continue
|
||||
echo " $f"
|
||||
BUILT_RPMS+=("$f")
|
||||
done
|
||||
}
|
||||
|
||||
# ===== Arch selection and build orchestration =========================================
|
||||
case "${ARCH_OVERRIDE:-}" in
|
||||
"")
|
||||
# No --arch: use host architecture
|
||||
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
|
||||
;;
|
||||
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
|
||||
|
||||
# ===== Final summary if building both arches ==========================================
|
||||
if [[ "$BUILT_ALL" -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "================ Build Summary (both architectures) ================"
|
||||
if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then
|
||||
for rp in "${BUILT_RPMS[@]}"; do
|
||||
echo "$rp"
|
||||
done
|
||||
else
|
||||
echo "[WARN] No RPMs detected in summary (check build logs above)."
|
||||
fi
|
||||
echo "==================================================================="
|
||||
fi
|
||||
@@ -1,37 +0,0 @@
|
||||
app: v2rayN
|
||||
binpatch: true
|
||||
|
||||
ingredients:
|
||||
script:
|
||||
- export FileName="v2rayN-${AppImageOutputArch}.zip"
|
||||
- wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/${FileName}"
|
||||
- 7z x $FileName -aoa
|
||||
- cp -rf v2rayN-${AppImageOutputArch}/* $OutputPath
|
||||
|
||||
script:
|
||||
- mkdir -p usr/bin usr/lib
|
||||
- cp -rf $OutputPath usr/lib/v2rayN
|
||||
- echo "When this file exists, app will not store configs under this folder" > usr/lib/v2rayN/NotStoreConfigHere.txt
|
||||
- ln -sf usr/lib/v2rayN/v2rayN usr/bin/v2rayN
|
||||
- chmod a+x usr/lib/v2rayN/v2rayN
|
||||
- find usr -type f -exec sh -c 'file "{}" | grep -qi "executable" && chmod +x "{}"' \;
|
||||
- install -Dm644 usr/lib/v2rayN/v2rayN.png v2rayN.png
|
||||
- install -Dm644 usr/lib/v2rayN/v2rayN.png usr/share/pixmaps/v2rayN.png
|
||||
- cat > v2rayN.desktop <<EOF
|
||||
- [Desktop Entry]
|
||||
- Name=v2rayN
|
||||
- Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
|
||||
- Exec=v2rayN
|
||||
- Icon=v2rayN
|
||||
- Terminal=false
|
||||
- Type=Application
|
||||
- Categories=Network;
|
||||
- EOF
|
||||
- install -Dm644 v2rayN.desktop usr/share/applications/v2rayN.desktop
|
||||
- cat > AppRun <<\EOF
|
||||
- #!/bin/sh
|
||||
- HERE="$(dirname "$(readlink -f "${0}")")"
|
||||
- cd ${HERE}/usr/lib/v2rayN
|
||||
- exec ${HERE}/usr/lib/v2rayN/v2rayN $@
|
||||
- EOF
|
||||
- chmod a+x AppRun
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>7.14.1</Version>
|
||||
<Version>7.15.4</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -5,22 +5,24 @@
|
||||
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.7" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.7" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.7" />
|
||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.7" />
|
||||
<PackageVersion Include="CliWrap" Version="3.9.0" />
|
||||
<PackageVersion Include="Downloader" Version="4.0.3" />
|
||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
|
||||
<PackageVersion Include="MaterialDesignThemes" Version="5.2.1" />
|
||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.1" />
|
||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
|
||||
<PackageVersion Include="MessageBox.Avalonia" Version="3.2.0" />
|
||||
<PackageVersion Include="QRCoder" Version="1.6.0" />
|
||||
<PackageVersion Include="QRCoder" Version="1.7.0" />
|
||||
<PackageVersion Include="ReactiveUI" Version="20.4.1" />
|
||||
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
|
||||
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
|
||||
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.9" />
|
||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.9" />
|
||||
<PackageVersion Include="Splat.NLog" Version="15.4.1" />
|
||||
<PackageVersion Include="Semi.Avalonia" Version="11.3.7" />
|
||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
|
||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7" />
|
||||
<PackageVersion Include="NLog" Version="6.0.5" />
|
||||
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
|
||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||
|
||||
Submodule v2rayN/GlobalHotKeys updated: ef73fa22c4...ffb2850df0
@@ -84,4 +84,14 @@ public static class Extension
|
||||
{
|
||||
return source.Concat(new[] { string.Empty }).ToList();
|
||||
}
|
||||
|
||||
public static bool IsGroupType(this EConfigType configType)
|
||||
{
|
||||
return configType is EConfigType.PolicyGroup or EConfigType.ProxyChain;
|
||||
}
|
||||
|
||||
public static bool IsComplexType(this EConfigType configType)
|
||||
{
|
||||
return configType is EConfigType.Custom or EConfigType.PolicyGroup or EConfigType.ProxyChain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,31 @@ public class JsonUtils
|
||||
{
|
||||
private static readonly string _tag = "JsonUtils";
|
||||
|
||||
private static readonly JsonSerializerOptions _defaultDeserializeOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _defaultSerializeOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _nullValueSerializeOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private static readonly JsonDocumentOptions _defaultDocumentOptions = new()
|
||||
{
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// DeepCopy
|
||||
/// </summary>
|
||||
@@ -34,11 +59,7 @@ public class JsonUtils
|
||||
{
|
||||
return default;
|
||||
}
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
return JsonSerializer.Deserialize<T>(strJson, options);
|
||||
return JsonSerializer.Deserialize<T>(strJson, _defaultDeserializeOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -59,7 +80,7 @@ public class JsonUtils
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return JsonNode.Parse(strJson);
|
||||
return JsonNode.Parse(strJson, nodeOptions: null, _defaultDocumentOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -84,12 +105,7 @@ public class JsonUtils
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = indented,
|
||||
DefaultIgnoreCondition = nullValue ? JsonIgnoreCondition.Never : JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions;
|
||||
result = JsonSerializer.Serialize(obj, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -67,116 +67,4 @@ public static class ProcUtils
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ProcessKill(int pid)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessKill(Process.GetProcessById(pid), false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ProcessKill(Process? proc, bool review)
|
||||
{
|
||||
if (proc is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GetProcessKeyInfo(proc, review, out var procId, out var fileName, out var processName);
|
||||
|
||||
try
|
||||
{
|
||||
if (Utils.IsNonWindows())
|
||||
{
|
||||
proc?.Kill(true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
proc?.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
proc?.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
proc?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
await Task.Delay(300);
|
||||
await ProcessKillByKeyInfo(review, procId, fileName, processName);
|
||||
}
|
||||
|
||||
private static void GetProcessKeyInfo(Process? proc, bool review, out int? procId, out string? fileName, out string? processName)
|
||||
{
|
||||
procId = null;
|
||||
fileName = null;
|
||||
processName = null;
|
||||
if (!review)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
procId = proc?.Id;
|
||||
fileName = proc?.MainModule?.FileName;
|
||||
processName = proc?.ProcessName;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ProcessKillByKeyInfo(bool review, int? procId, string? fileName, string? processName)
|
||||
{
|
||||
if (review && procId != null && fileName != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lstProc = Process.GetProcessesByName(processName);
|
||||
foreach (var proc2 in lstProc)
|
||||
{
|
||||
if (proc2.Id == procId)
|
||||
{
|
||||
Logging.SaveLog($"{_tag}, KillProcess not completing the job, procId");
|
||||
await ProcessKill(proc2, false);
|
||||
}
|
||||
if (proc2.MainModule != null && proc2.MainModule?.FileName == fileName)
|
||||
{
|
||||
Logging.SaveLog($"{_tag}, KillProcess not completing the job, fileName");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
using QRCoder;
|
||||
using QRCoder.Exceptions;
|
||||
using SkiaSharp;
|
||||
using ZXing.SkiaSharp;
|
||||
|
||||
namespace ServiceLib.Common;
|
||||
|
||||
public class QRCodeHelper
|
||||
public class QRCodeUtils
|
||||
{
|
||||
public static byte[]? GenQRCode(string? url)
|
||||
{
|
||||
if (url.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
using QRCodeGenerator qrGenerator = new();
|
||||
using var qrCodeData = qrGenerator.CreateQrCode(url ?? string.Empty, QRCodeGenerator.ECCLevel.Q);
|
||||
using PngByteQRCode qrCode = new(qrCodeData);
|
||||
return qrCode.GetGraphic(20);
|
||||
DataTooLongException? lastDtle = null;
|
||||
|
||||
var levels = new[]
|
||||
{
|
||||
QRCodeGenerator.ECCLevel.H,
|
||||
QRCodeGenerator.ECCLevel.Q,
|
||||
QRCodeGenerator.ECCLevel.M,
|
||||
QRCodeGenerator.ECCLevel.L
|
||||
};
|
||||
foreach (var level in levels)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var qrCodeData = qrGenerator.CreateQrCode(url, level);
|
||||
using PngByteQRCode qrCode = new(qrCodeData);
|
||||
return qrCode.GetGraphic(20);
|
||||
}
|
||||
catch (DataTooLongException ex)
|
||||
{
|
||||
lastDtle = ex;
|
||||
continue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastDtle != null)
|
||||
{
|
||||
throw lastDtle;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string? ParseBarcode(string? fileName)
|
||||
@@ -85,13 +85,19 @@ public class Utils
|
||||
/// Base64 Encode
|
||||
/// </summary>
|
||||
/// <param name="plainText"></param>
|
||||
/// <param name="removePadding"></param>
|
||||
/// <returns></returns>
|
||||
public static string Base64Encode(string plainText)
|
||||
public static string Base64Encode(string plainText, bool removePadding = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
|
||||
return Convert.ToBase64String(plainTextBytes);
|
||||
var base64 = Convert.ToBase64String(plainTextBytes);
|
||||
if (removePadding)
|
||||
{
|
||||
base64 = base64.TrimEnd('=');
|
||||
}
|
||||
return base64;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -112,7 +118,7 @@ public class Utils
|
||||
{
|
||||
if (plainText.IsNullOrEmpty())
|
||||
{
|
||||
return "";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
plainText = plainText.Trim()
|
||||
@@ -331,6 +337,32 @@ public class Utils
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static Dictionary<string, List<string>> ParseHostsToDictionary(string hostsContent)
|
||||
{
|
||||
var userHostsMap = hostsContent
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(line => line.Trim())
|
||||
// skip full-line comments
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
|
||||
// strip inline comments (truncate at '#')
|
||||
.Select(line =>
|
||||
{
|
||||
var index = line.IndexOf('#');
|
||||
return index >= 0 ? line.Substring(0, index).Trim() : line;
|
||||
})
|
||||
// ensure line still contains valid parts
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' '))
|
||||
.Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
.Where(parts => parts.Length >= 2)
|
||||
.GroupBy(parts => parts[0])
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.SelectMany(parts => parts.Skip(1)).ToList()
|
||||
);
|
||||
|
||||
return userHostsMap;
|
||||
}
|
||||
|
||||
#endregion 转换函数
|
||||
|
||||
#region 数据检查
|
||||
@@ -582,9 +614,9 @@ public class Utils
|
||||
if (host.StartsWith("#"))
|
||||
continue;
|
||||
var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (hostItem.Length != 2)
|
||||
if (hostItem.Length < 2)
|
||||
continue;
|
||||
systemHosts.Add(hostItem.Last(), hostItem.First());
|
||||
systemHosts.Add(hostItem[1], hostItem[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -857,6 +889,55 @@ public class Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsPackagedInstall()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsWindows() || IsOSX())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPIMAGE")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var exePath = GetExePath();
|
||||
var baseDir = string.IsNullOrEmpty(exePath) ? StartupPath() : Path.GetDirectoryName(exePath) ?? "";
|
||||
var p = baseDir.Replace('\\', '/');
|
||||
|
||||
if (string.IsNullOrEmpty(p))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (p.Contains("/.mount_", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (p.StartsWith("/opt/v2rayN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (p.StartsWith("/usr/lib/v2rayN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (p.StartsWith("/usr/share/v2rayN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task<string?> GetLinuxUserId()
|
||||
{
|
||||
var arg = new List<string>() { "-c", "id -u" };
|
||||
@@ -872,7 +953,7 @@ public class Utils
|
||||
if (SetUnixFileMode(fileName))
|
||||
{
|
||||
Logging.SaveLog($"Successfully set the file execution permission, {fileName}");
|
||||
return "";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (fileName.Contains(' '))
|
||||
|
||||
@@ -7,11 +7,11 @@ namespace ServiceLib.Common;
|
||||
* http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net
|
||||
*/
|
||||
|
||||
public sealed class Job : IDisposable
|
||||
public sealed class WindowsJob : IDisposable
|
||||
{
|
||||
private IntPtr handle = IntPtr.Zero;
|
||||
|
||||
public Job()
|
||||
public WindowsJob()
|
||||
{
|
||||
handle = CreateJobObject(IntPtr.Zero, null);
|
||||
var extendedInfoPtr = IntPtr.Zero;
|
||||
@@ -94,7 +94,7 @@ namespace ServiceLib.Common;
|
||||
}
|
||||
}
|
||||
|
||||
~Job()
|
||||
~WindowsJob()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
@@ -12,5 +12,7 @@ public enum EConfigType
|
||||
TUIC = 8,
|
||||
WireGuard = 9,
|
||||
HTTP = 10,
|
||||
Anytls = 11
|
||||
Anytls = 11,
|
||||
PolicyGroup = 101,
|
||||
ProxyChain = 102,
|
||||
}
|
||||
|
||||
@@ -15,5 +15,6 @@ public enum ECoreType
|
||||
brook = 27,
|
||||
overtls = 28,
|
||||
shadowquic = 29,
|
||||
mieru = 30,
|
||||
v2rayN = 99
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace ServiceLib.Enums;
|
||||
|
||||
public enum EMsgCommand
|
||||
{
|
||||
ClearMsg,
|
||||
SendMsgView,
|
||||
SendSnackMsg,
|
||||
RefreshProfiles,
|
||||
AppExit
|
||||
}
|
||||
@@ -2,8 +2,9 @@ namespace ServiceLib.Enums;
|
||||
|
||||
public enum EMultipleLoad
|
||||
{
|
||||
LeastPing,
|
||||
Fallback,
|
||||
Random,
|
||||
RoundRobin,
|
||||
LeastPing,
|
||||
LeastLoad
|
||||
}
|
||||
|
||||
8
v2rayN/ServiceLib/Enums/ERuleType.cs
Normal file
8
v2rayN/ServiceLib/Enums/ERuleType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ServiceLib.Enums;
|
||||
|
||||
public enum ERuleType
|
||||
{
|
||||
ALL = 0,
|
||||
Routing = 1,
|
||||
DNS = 2,
|
||||
}
|
||||
@@ -6,17 +6,14 @@ public enum EViewAction
|
||||
ShowYesNo,
|
||||
SaveFileDialog,
|
||||
AddBatchRoutingRulesYesNo,
|
||||
AdjustMainLvColWidth,
|
||||
SetClipboardData,
|
||||
AddServerViaClipboard,
|
||||
ImportRulesFromClipboard,
|
||||
ProfilesFocus,
|
||||
ShareSub,
|
||||
ShareServer,
|
||||
ShowHideWindow,
|
||||
ScanScreenTask,
|
||||
ScanImageTask,
|
||||
Shutdown,
|
||||
BrowseServer,
|
||||
ImportRulesFromFile,
|
||||
InitSettingFont,
|
||||
@@ -26,22 +23,14 @@ public enum EViewAction
|
||||
RoutingRuleDetailsWindow,
|
||||
AddServerWindow,
|
||||
AddServer2Window,
|
||||
AddGroupServerWindow,
|
||||
DNSSettingWindow,
|
||||
RoutingSettingWindow,
|
||||
OptionSettingWindow,
|
||||
FullConfigTemplateWindow,
|
||||
GlobalHotkeySettingWindow,
|
||||
SubSettingWindow,
|
||||
DispatcherSpeedTest,
|
||||
DispatcherRefreshConnections,
|
||||
DispatcherRefreshProxyGroups,
|
||||
DispatcherProxiesDelayTest,
|
||||
DispatcherStatistics,
|
||||
DispatcherServerAvailability,
|
||||
DispatcherReload,
|
||||
DispatcherRefreshServersBiz,
|
||||
DispatcherRefreshIcon,
|
||||
DispatcherCheckUpdate,
|
||||
DispatcherCheckUpdateFinished,
|
||||
DispatcherShowMsg,
|
||||
}
|
||||
|
||||
32
v2rayN/ServiceLib/Events/AppEvents.cs
Normal file
32
v2rayN/ServiceLib/Events/AppEvents.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Reactive;
|
||||
|
||||
namespace ServiceLib.Events;
|
||||
|
||||
public static class AppEvents
|
||||
{
|
||||
public static readonly EventChannel<Unit> ReloadRequested = new();
|
||||
public static readonly EventChannel<bool?> ShowHideWindowRequested = new();
|
||||
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
|
||||
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
|
||||
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
|
||||
|
||||
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
|
||||
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
|
||||
public static readonly EventChannel<Unit> ProxiesReloadRequested = new();
|
||||
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
|
||||
|
||||
public static readonly EventChannel<string> SendSnackMsgRequested = new();
|
||||
public static readonly EventChannel<string> SendMsgViewRequested = new();
|
||||
|
||||
public static readonly EventChannel<Unit> AppExitRequested = new();
|
||||
public static readonly EventChannel<bool> ShutdownRequested = new();
|
||||
|
||||
public static readonly EventChannel<Unit> AdjustMainLvColWidthRequested = new();
|
||||
|
||||
public static readonly EventChannel<string> SetDefaultServerRequested = new();
|
||||
|
||||
public static readonly EventChannel<Unit> RoutingsMenuRefreshRequested = new();
|
||||
public static readonly EventChannel<Unit> TestServerRequested = new();
|
||||
public static readonly EventChannel<Unit> InboundDisplayRequested = new();
|
||||
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
|
||||
}
|
||||
29
v2rayN/ServiceLib/Events/EventChannel.cs
Normal file
29
v2rayN/ServiceLib/Events/EventChannel.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace ServiceLib.Events;
|
||||
|
||||
public sealed class EventChannel<T>
|
||||
{
|
||||
private readonly ISubject<T> _subject = Subject.Synchronize(new Subject<T>());
|
||||
|
||||
public IObservable<T> AsObservable()
|
||||
{
|
||||
return _subject.AsObservable();
|
||||
}
|
||||
|
||||
public void Publish(T value)
|
||||
{
|
||||
_subject.OnNext(value);
|
||||
}
|
||||
|
||||
public void Publish()
|
||||
{
|
||||
if (typeof(T) != typeof(Unit))
|
||||
{
|
||||
throw new InvalidOperationException("Publish() without value is only valid for EventChannel<Unit>.");
|
||||
}
|
||||
_subject.OnNext((T)(object)Unit.Default);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public class Global
|
||||
public const string ProxySetLinuxShellFileName = NamespaceSample + "proxy_set_linux_sh";
|
||||
public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh";
|
||||
public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
|
||||
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
|
||||
|
||||
public const string DefaultSecurity = "auto";
|
||||
public const string DefaultNetwork = "tcp";
|
||||
@@ -49,6 +50,7 @@ public class Global
|
||||
public const string DirectTag = "direct";
|
||||
public const string BlockTag = "block";
|
||||
public const string DnsTag = "dns-module";
|
||||
public const string BalancerTagSuffix = "-round";
|
||||
public const string StreamSecurity = "tls";
|
||||
public const string StreamSecurityReality = "reality";
|
||||
public const string Loopback = "127.0.0.1";
|
||||
@@ -82,8 +84,7 @@ public class Global
|
||||
|
||||
public const string SingboxDirectDNSTag = "direct_dns";
|
||||
public const string SingboxRemoteDNSTag = "remote_dns";
|
||||
public const string SingboxOutboundResolverTag = "outbound_resolver";
|
||||
public const string SingboxFinalResolverTag = "final_resolver";
|
||||
public const string SingboxLocalDNSTag = "local_local";
|
||||
public const string SingboxHostsDNSTag = "hosts_dns";
|
||||
public const string SingboxFakeDNSTag = "fake_dns";
|
||||
|
||||
@@ -289,6 +290,33 @@ public class Global
|
||||
"sing_box"
|
||||
];
|
||||
|
||||
public static readonly HashSet<EConfigType> XraySupportConfigType =
|
||||
[
|
||||
EConfigType.VMess,
|
||||
EConfigType.VLESS,
|
||||
EConfigType.Shadowsocks,
|
||||
EConfigType.Trojan,
|
||||
EConfigType.WireGuard,
|
||||
EConfigType.SOCKS,
|
||||
EConfigType.HTTP,
|
||||
];
|
||||
|
||||
public static readonly HashSet<EConfigType> SingboxSupportConfigType =
|
||||
[
|
||||
EConfigType.VMess,
|
||||
EConfigType.VLESS,
|
||||
EConfigType.Shadowsocks,
|
||||
EConfigType.Trojan,
|
||||
EConfigType.Hysteria2,
|
||||
EConfigType.TUIC,
|
||||
EConfigType.Anytls,
|
||||
EConfigType.WireGuard,
|
||||
EConfigType.SOCKS,
|
||||
EConfigType.HTTP,
|
||||
];
|
||||
|
||||
public static readonly HashSet<EConfigType> SingboxOnlyConfigType = SingboxSupportConfigType.Except(XraySupportConfigType).ToHashSet();
|
||||
|
||||
public static readonly List<string> DomainStrategies =
|
||||
[
|
||||
AsIs,
|
||||
@@ -423,6 +451,14 @@ public class Global
|
||||
"none"
|
||||
];
|
||||
|
||||
public static readonly Dictionary<string, string> LogLevelColors = new()
|
||||
{
|
||||
{ "debug", "#6C757D" },
|
||||
{ "info", "#2ECC71" },
|
||||
{ "warning", "#FFA500" },
|
||||
{ "error", "#E74C3C" },
|
||||
};
|
||||
|
||||
public static readonly List<string> InboundTags =
|
||||
[
|
||||
"socks",
|
||||
@@ -535,6 +571,7 @@ public class Global
|
||||
{ ECoreType.brook, "txthinking/brook" },
|
||||
{ ECoreType.overtls, "ShadowsocksR-Live/overtls" },
|
||||
{ ECoreType.shadowquic, "spongebob888/shadowquic" },
|
||||
{ ECoreType.mieru, "enfein/mieru" },
|
||||
{ ECoreType.v2rayN, "2dust/v2rayN" },
|
||||
};
|
||||
|
||||
@@ -571,6 +608,7 @@ public class Global
|
||||
{ "cloudflare-dns.com", new List<string> { "104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9" } },
|
||||
{ "dns.cloudflare.com", new List<string> { "104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5" } },
|
||||
{ "dot.pub", new List<string> { "1.12.12.12", "120.53.53.53" } },
|
||||
{ "doh.pub", new List<string> { "1.12.12.12", "120.53.53.53" } },
|
||||
{ "dns.quad9.net", new List<string> { "9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9" } },
|
||||
{ "dns.yandex.net", new List<string> { "77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff" } },
|
||||
{ "dns.sb", new List<string> { "185.222.222.222", "2a09::" } },
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
global using ServiceLib.Base;
|
||||
global using ServiceLib.Base;
|
||||
global using ServiceLib.Common;
|
||||
global using ServiceLib.Enums;
|
||||
global using ServiceLib.Events;
|
||||
global using ServiceLib.Handler;
|
||||
global using ServiceLib.Helper;
|
||||
global using ServiceLib.Manager;
|
||||
global using ServiceLib.Handler.Fmt;
|
||||
global using ServiceLib.Services;
|
||||
global using ServiceLib.Services.Statistics;
|
||||
global using ServiceLib.Services.CoreConfig;
|
||||
global using ServiceLib.Models;
|
||||
global using ServiceLib.Resx;
|
||||
global using ServiceLib.Handler.SysProxy;
|
||||
global using ServiceLib.Handler.SysProxy;
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
|
||||
public class ConfigHandler
|
||||
public static class ConfigHandler
|
||||
{
|
||||
private static readonly string _configRes = Global.ConfigFileName;
|
||||
private static readonly string _tag = "ConfigHandler";
|
||||
@@ -113,6 +113,10 @@ public class ConfigHandler
|
||||
config.ConstItem ??= new ConstItem();
|
||||
|
||||
config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
|
||||
if (config.SimpleDNSItem.GlobalFakeIp is null)
|
||||
{
|
||||
config.SimpleDNSItem.GlobalFakeIp = true;
|
||||
}
|
||||
|
||||
config.SpeedTestItem ??= new();
|
||||
if (config.SpeedTestItem.SpeedTestTimeout < 10)
|
||||
@@ -216,7 +220,7 @@ public class ConfigHandler
|
||||
/// <returns>Result of the operation (0 if successful, -1 if failed)</returns>
|
||||
public static async Task<int> AddServer(Config config, ProfileItem profileItem)
|
||||
{
|
||||
var item = await AppHandler.Instance.GetProfileItem(profileItem.IndexId);
|
||||
var item = await AppManager.Instance.GetProfileItem(profileItem.IndexId);
|
||||
if (item is null)
|
||||
{
|
||||
item = profileItem;
|
||||
@@ -336,7 +340,7 @@ public class ConfigHandler
|
||||
{
|
||||
foreach (var it in indexes)
|
||||
{
|
||||
var item = await AppHandler.Instance.GetProfileItem(it.IndexId);
|
||||
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
|
||||
if (item is null)
|
||||
{
|
||||
continue;
|
||||
@@ -353,6 +357,11 @@ public class ConfigHandler
|
||||
{
|
||||
}
|
||||
}
|
||||
else if (profileItem.ConfigType.IsGroupType())
|
||||
{
|
||||
var profileGroupItem = await AppManager.Instance.GetProfileGroupItem(it.IndexId);
|
||||
await AddGroupServerCommon(config, profileItem, profileGroupItem, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await AddServerCommon(config, profileItem, true);
|
||||
@@ -418,7 +427,7 @@ public class ConfigHandler
|
||||
/// <returns>The default profile item or null if none exists</returns>
|
||||
public static async Task<ProfileItem?> GetDefaultServer(Config config)
|
||||
{
|
||||
var item = await AppHandler.Instance.GetProfileItem(config.IndexId);
|
||||
var item = await AppManager.Instance.GetProfileItem(config.IndexId);
|
||||
if (item is null)
|
||||
{
|
||||
var item2 = await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync();
|
||||
@@ -449,7 +458,7 @@ public class ConfigHandler
|
||||
|
||||
for (int i = 0; i < lstProfile.Count; i++)
|
||||
{
|
||||
ProfileExHandler.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
|
||||
ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
|
||||
}
|
||||
|
||||
var sort = 0;
|
||||
@@ -461,7 +470,7 @@ public class ConfigHandler
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
sort = ProfileExHandler.Instance.GetSort(lstProfile.First().IndexId) - 1;
|
||||
sort = ProfileExManager.Instance.GetSort(lstProfile.First().IndexId) - 1;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -471,7 +480,7 @@ public class ConfigHandler
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
sort = ProfileExHandler.Instance.GetSort(lstProfile[index - 1].IndexId) - 1;
|
||||
sort = ProfileExManager.Instance.GetSort(lstProfile[index - 1].IndexId) - 1;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -482,7 +491,7 @@ public class ConfigHandler
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
sort = ProfileExHandler.Instance.GetSort(lstProfile[index + 1].IndexId) + 1;
|
||||
sort = ProfileExManager.Instance.GetSort(lstProfile[index + 1].IndexId) + 1;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -492,7 +501,7 @@ public class ConfigHandler
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
sort = ProfileExHandler.Instance.GetSort(lstProfile[^1].IndexId) + 1;
|
||||
sort = ProfileExManager.Instance.GetSort(lstProfile[^1].IndexId) + 1;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -501,7 +510,7 @@ public class ConfigHandler
|
||||
break;
|
||||
}
|
||||
|
||||
ProfileExHandler.Instance.SetSort(lstProfile[index].IndexId, sort);
|
||||
ProfileExManager.Instance.SetSort(lstProfile[index].IndexId, sort);
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
@@ -559,7 +568,7 @@ public class ConfigHandler
|
||||
/// <returns>0 if successful, -1 if failed</returns>
|
||||
public static async Task<int> EditCustomServer(Config config, ProfileItem profileItem)
|
||||
{
|
||||
var item = await AppHandler.Instance.GetProfileItem(profileItem.IndexId);
|
||||
var item = await AppManager.Instance.GetProfileItem(profileItem.IndexId);
|
||||
if (item is null)
|
||||
{
|
||||
item = profileItem;
|
||||
@@ -601,7 +610,7 @@ public class ConfigHandler
|
||||
profileItem.Id = profileItem.Id.TrimEx();
|
||||
profileItem.Security = profileItem.Security.TrimEx();
|
||||
|
||||
if (!AppHandler.Instance.GetShadowsocksSecurities(profileItem).Contains(profileItem.Security))
|
||||
if (!AppManager.Instance.GetShadowsocksSecurities(profileItem).Contains(profileItem.Security))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -829,13 +838,13 @@ public class ConfigHandler
|
||||
/// <returns>0 if successful, -1 if failed</returns>
|
||||
public static async Task<int> SortServers(Config config, string subId, string colName, bool asc)
|
||||
{
|
||||
var lstModel = await AppHandler.Instance.ProfileItems(subId, "");
|
||||
var lstModel = await AppManager.Instance.ProfileItems(subId, "");
|
||||
if (lstModel.Count <= 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
var lstServerStat = (config.GuiItem.EnableStatistics ? StatisticsHandler.Instance.ServerStat : null) ?? [];
|
||||
var lstProfileExs = await ProfileExHandler.Instance.GetProfileExs();
|
||||
var lstServerStat = (config.GuiItem.EnableStatistics ? StatisticsManager.Instance.ServerStat : null) ?? [];
|
||||
var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
|
||||
var lstProfile = (from t in lstModel
|
||||
join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b
|
||||
from t22 in t2b.DefaultIfEmpty()
|
||||
@@ -905,7 +914,7 @@ public class ConfigHandler
|
||||
|
||||
for (var i = 0; i < lstProfile.Count; i++)
|
||||
{
|
||||
ProfileExHandler.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
|
||||
ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
|
||||
}
|
||||
switch (name)
|
||||
{
|
||||
@@ -914,7 +923,7 @@ public class ConfigHandler
|
||||
var maxSort = lstProfile.Max(t => t.Sort) + 10;
|
||||
foreach (var item in lstProfile.Where(item => item.Delay <= 0))
|
||||
{
|
||||
ProfileExHandler.Instance.SetSort(item.IndexId, maxSort);
|
||||
ProfileExManager.Instance.SetSort(item.IndexId, maxSort);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -924,7 +933,7 @@ public class ConfigHandler
|
||||
var maxSort = lstProfile.Max(t => t.Sort) + 10;
|
||||
foreach (var item in lstProfile.Where(item => item.Speed <= 0))
|
||||
{
|
||||
ProfileExHandler.Instance.SetSort(item.IndexId, maxSort);
|
||||
ProfileExManager.Instance.SetSort(item.IndexId, maxSort);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -982,7 +991,7 @@ public class ConfigHandler
|
||||
/// <returns>Tuple with total count and remaining count after deduplication</returns>
|
||||
public static async Task<Tuple<int, int>> DedupServerList(Config config, string subId)
|
||||
{
|
||||
var lstProfile = await AppHandler.Instance.ProfileItems(subId);
|
||||
var lstProfile = await AppManager.Instance.ProfileItems(subId);
|
||||
if (lstProfile == null)
|
||||
{
|
||||
return new Tuple<int, int>(0, 0);
|
||||
@@ -1052,15 +1061,15 @@ public class ConfigHandler
|
||||
if (profileItem.IndexId.IsNullOrEmpty())
|
||||
{
|
||||
profileItem.IndexId = Utils.GetGuid(false);
|
||||
maxSort = ProfileExHandler.Instance.GetMaxSort();
|
||||
maxSort = ProfileExManager.Instance.GetMaxSort();
|
||||
}
|
||||
if (!toFile && maxSort < 0)
|
||||
{
|
||||
maxSort = ProfileExHandler.Instance.GetMaxSort();
|
||||
maxSort = ProfileExManager.Instance.GetMaxSort();
|
||||
}
|
||||
if (maxSort > 0)
|
||||
{
|
||||
ProfileExHandler.Instance.SetSort(profileItem.IndexId, maxSort + 1);
|
||||
ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1);
|
||||
}
|
||||
|
||||
if (toFile)
|
||||
@@ -1070,6 +1079,37 @@ public class ConfigHandler
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static async Task<int> AddGroupServerCommon(Config config, ProfileItem profileItem, ProfileGroupItem profileGroupItem, bool toFile = true)
|
||||
{
|
||||
var maxSort = -1;
|
||||
if (profileItem.IndexId.IsNullOrEmpty())
|
||||
{
|
||||
profileItem.IndexId = Utils.GetGuid(false);
|
||||
maxSort = ProfileExManager.Instance.GetMaxSort();
|
||||
}
|
||||
var groupType = profileItem.ConfigType == EConfigType.ProxyChain ? EConfigType.ProxyChain.ToString() : profileGroupItem.MultipleLoad.ToString();
|
||||
profileItem.Address = $"{profileItem.CoreType}-{groupType}";
|
||||
if (maxSort > 0)
|
||||
{
|
||||
ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1);
|
||||
}
|
||||
if (toFile)
|
||||
{
|
||||
await SQLiteHelper.Instance.ReplaceAsync(profileItem);
|
||||
if (profileGroupItem != null)
|
||||
{
|
||||
profileGroupItem.IndexId = profileItem.IndexId;
|
||||
await ProfileGroupItemManager.Instance.SaveItemAsync(profileGroupItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(profileItem.IndexId);
|
||||
await ProfileGroupItemManager.Instance.SaveTo();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare two profile items to determine if they represent the same server
|
||||
/// Used for deduplication and server matching
|
||||
@@ -1120,7 +1160,7 @@ public class ConfigHandler
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = await AppHandler.Instance.GetProfileItem(indexId);
|
||||
var item = await AppManager.Instance.GetProfileItem(indexId);
|
||||
if (item == null)
|
||||
{
|
||||
return 0;
|
||||
@@ -1141,7 +1181,7 @@ public class ConfigHandler
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a custom server that combines multiple servers for load balancing
|
||||
/// Create a group server that combines multiple servers for load balancing
|
||||
/// Generates a configuration file that references multiple servers
|
||||
/// </summary>
|
||||
/// <param name="config">Current configuration</param>
|
||||
@@ -1149,45 +1189,54 @@ public class ConfigHandler
|
||||
/// <param name="coreType">Core type to use (Xray or sing_box)</param>
|
||||
/// <param name="multipleLoad">Load balancing algorithm</param>
|
||||
/// <returns>Result object with success state and data</returns>
|
||||
public static async Task<RetResult> AddCustomServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad)
|
||||
public static async Task<RetResult> AddGroupServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad, string? subId)
|
||||
{
|
||||
var indexId = Utils.GetMd5(Global.CoreMultipleLoadConfigFileName);
|
||||
var configPath = Utils.GetConfigPath(Global.CoreMultipleLoadConfigFileName);
|
||||
var result = new RetResult();
|
||||
|
||||
var result = await CoreConfigHandler.GenerateClientMultipleLoadConfig(config, configPath, selecteds, coreType, multipleLoad);
|
||||
if (result.Success != true)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var indexId = Utils.GetGuid(false);
|
||||
var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList());
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var profileItem = await AppHandler.Instance.GetProfileItem(indexId) ?? new();
|
||||
profileItem.IndexId = indexId;
|
||||
var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId)).Remarks} ";
|
||||
if (coreType == ECoreType.Xray)
|
||||
{
|
||||
profileItem.Remarks = multipleLoad switch
|
||||
remark += multipleLoad switch
|
||||
{
|
||||
EMultipleLoad.Random => ResUI.menuSetDefaultMultipleServerXrayRandom,
|
||||
EMultipleLoad.RoundRobin => ResUI.menuSetDefaultMultipleServerXrayRoundRobin,
|
||||
EMultipleLoad.LeastPing => ResUI.menuSetDefaultMultipleServerXrayLeastPing,
|
||||
EMultipleLoad.LeastLoad => ResUI.menuSetDefaultMultipleServerXrayLeastLoad,
|
||||
_ => ResUI.menuSetDefaultMultipleServerXrayRoundRobin,
|
||||
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing,
|
||||
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback,
|
||||
EMultipleLoad.Random => ResUI.menuGenGroupMultipleServerXrayRandom,
|
||||
EMultipleLoad.RoundRobin => ResUI.menuGenGroupMultipleServerXrayRoundRobin,
|
||||
EMultipleLoad.LeastLoad => ResUI.menuGenGroupMultipleServerXrayLeastLoad,
|
||||
_ => ResUI.menuGenGroupMultipleServerXrayRoundRobin,
|
||||
};
|
||||
}
|
||||
else if (coreType == ECoreType.sing_box)
|
||||
{
|
||||
profileItem.Remarks = ResUI.menuSetDefaultMultipleServerSingBoxLeastPing;
|
||||
remark += multipleLoad switch
|
||||
{
|
||||
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
|
||||
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback,
|
||||
_ => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
|
||||
};
|
||||
}
|
||||
profileItem.Address = Global.CoreMultipleLoadConfigFileName;
|
||||
profileItem.ConfigType = EConfigType.Custom;
|
||||
profileItem.CoreType = coreType;
|
||||
|
||||
await AddServerCommon(config, profileItem, true);
|
||||
|
||||
var profile = new ProfileItem
|
||||
{
|
||||
IndexId = indexId,
|
||||
CoreType = coreType,
|
||||
ConfigType = EConfigType.PolicyGroup,
|
||||
Remarks = remark,
|
||||
};
|
||||
if (!subId.IsNullOrEmpty())
|
||||
{
|
||||
profile.Subid = subId;
|
||||
}
|
||||
var profileGroup = new ProfileGroupItem
|
||||
{
|
||||
ChildItems = childProfileIndexId,
|
||||
MultipleLoad = multipleLoad,
|
||||
IndexId = indexId,
|
||||
};
|
||||
var ret = await AddGroupServerCommon(config, profile, profileGroup, true);
|
||||
result.Success = ret == 0;
|
||||
result.Data = indexId;
|
||||
return result;
|
||||
}
|
||||
@@ -1205,16 +1254,25 @@ public class ConfigHandler
|
||||
ProfileItem? itemSocks = null;
|
||||
if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun)
|
||||
{
|
||||
var tun2SocksAddress = node.Address;
|
||||
if (node.ConfigType.IsGroupType())
|
||||
{
|
||||
var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList();
|
||||
if (lstAddresses.Count > 0)
|
||||
{
|
||||
tun2SocksAddress = Utils.List2String(lstAddresses);
|
||||
}
|
||||
}
|
||||
itemSocks = new ProfileItem()
|
||||
{
|
||||
CoreType = ECoreType.sing_box,
|
||||
ConfigType = EConfigType.SOCKS,
|
||||
Address = Global.Loopback,
|
||||
Sni = node.Address, //Tun2SocksAddress
|
||||
Port = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks)
|
||||
SpiderX = tun2SocksAddress, // Tun2SocksAddress
|
||||
Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks)
|
||||
};
|
||||
}
|
||||
else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0))
|
||||
else if (node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)
|
||||
{
|
||||
var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
|
||||
itemSocks = new ProfileItem()
|
||||
@@ -1238,12 +1296,12 @@ public class ConfigHandler
|
||||
/// <returns>Number of removed servers or -1 if failed</returns>
|
||||
public static async Task<int> RemoveInvalidServerResult(Config config, string subid)
|
||||
{
|
||||
var lstModel = await AppHandler.Instance.ProfileItems(subid, "");
|
||||
var lstModel = await AppManager.Instance.ProfileItems(subid, "");
|
||||
if (lstModel is { Count: <= 0 })
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
var lstProfileExs = await ProfileExHandler.Instance.GetProfileExs();
|
||||
var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
|
||||
var lstProfile = (from t in lstModel
|
||||
join t2 in lstProfileExs on t.IndexId equals t2.IndexId
|
||||
where t2.Delay == -1
|
||||
@@ -1279,7 +1337,7 @@ public class ConfigHandler
|
||||
if (isSub && subid.IsNotEmpty())
|
||||
{
|
||||
await RemoveServersViaSubid(config, subid, isSub);
|
||||
subFilter = (await AppHandler.Instance.GetSubItem(subid))?.Filter ?? "";
|
||||
subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? "";
|
||||
}
|
||||
|
||||
var countServers = 0;
|
||||
@@ -1363,7 +1421,7 @@ public class ConfigHandler
|
||||
return -1;
|
||||
}
|
||||
|
||||
var subItem = await AppHandler.Instance.GetSubItem(subid);
|
||||
var subItem = await AppManager.Instance.GetSubItem(subid);
|
||||
var subRemarks = subItem?.Remarks;
|
||||
var preSocksPort = subItem?.PreSocksPort;
|
||||
|
||||
@@ -1426,16 +1484,7 @@ public class ConfigHandler
|
||||
if (profileItem is null)
|
||||
{
|
||||
profileItem = Hysteria2Fmt.ResolveFull2(strData, subRemarks);
|
||||
}
|
||||
if (profileItem is null)
|
||||
{
|
||||
profileItem = Hysteria2Fmt.ResolveFull(strData, subRemarks);
|
||||
}
|
||||
//Is naiveproxy configuration
|
||||
if (profileItem is null)
|
||||
{
|
||||
profileItem = NaiveproxyFmt.ResolveFull(strData, subRemarks);
|
||||
}
|
||||
}
|
||||
if (profileItem is null || profileItem.Address.IsNullOrEmpty())
|
||||
{
|
||||
return -1;
|
||||
@@ -1519,7 +1568,7 @@ public class ConfigHandler
|
||||
ProfileItem? activeProfile = null;
|
||||
if (isSub && subid.IsNotEmpty())
|
||||
{
|
||||
lstOriSub = await AppHandler.Instance.ProfileItems(subid);
|
||||
lstOriSub = await AppManager.Instance.ProfileItems(subid);
|
||||
activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId);
|
||||
}
|
||||
|
||||
@@ -1551,7 +1600,7 @@ public class ConfigHandler
|
||||
//Select active node
|
||||
if (activeProfile != null)
|
||||
{
|
||||
var lstSub = await AppHandler.Instance.ProfileItems(subid);
|
||||
var lstSub = await AppManager.Instance.ProfileItems(subid);
|
||||
var existItem = lstSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == activeProfile.Remarks : CompareProfileItem(t, activeProfile, true));
|
||||
if (existItem != null)
|
||||
{
|
||||
@@ -1562,13 +1611,13 @@ public class ConfigHandler
|
||||
//Keep the last traffic statistics
|
||||
if (lstOriSub != null)
|
||||
{
|
||||
var lstSub = await AppHandler.Instance.ProfileItems(subid);
|
||||
var lstSub = await AppManager.Instance.ProfileItems(subid);
|
||||
foreach (var item in lstSub)
|
||||
{
|
||||
var existItem = lstOriSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == item.Remarks : CompareProfileItem(t, item, true));
|
||||
if (existItem != null)
|
||||
{
|
||||
await StatisticsHandler.Instance.CloneServerStatItem(existItem.IndexId, item.IndexId);
|
||||
await StatisticsManager.Instance.CloneServerStatItem(existItem.IndexId, item.IndexId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1608,7 +1657,7 @@ public class ConfigHandler
|
||||
if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost))
|
||||
{
|
||||
//TODO Temporary reminder to be removed later
|
||||
NoticeHandler.Instance.Enqueue(ResUI.InsecureUrlProtocol);
|
||||
NoticeManager.Instance.Enqueue(ResUI.InsecureUrlProtocol);
|
||||
//return -1;
|
||||
}
|
||||
|
||||
@@ -1626,7 +1675,7 @@ public class ConfigHandler
|
||||
/// <returns>0 if successful, -1 if failed</returns>
|
||||
public static async Task<int> AddSubItem(Config config, SubItem subItem)
|
||||
{
|
||||
var item = await AppHandler.Instance.GetSubItem(subItem.Id);
|
||||
var item = await AppManager.Instance.GetSubItem(subItem.Id);
|
||||
if (item is null)
|
||||
{
|
||||
item = subItem;
|
||||
@@ -1658,7 +1707,7 @@ public class ConfigHandler
|
||||
var maxSort = 0;
|
||||
if (await SQLiteHelper.Instance.TableAsync<SubItem>().CountAsync() > 0)
|
||||
{
|
||||
var lstSubs = (await AppHandler.Instance.SubItems());
|
||||
var lstSubs = (await AppManager.Instance.SubItems());
|
||||
maxSort = lstSubs.LastOrDefault()?.Sort ?? 0;
|
||||
}
|
||||
item.Sort = maxSort + 1;
|
||||
@@ -1712,7 +1761,7 @@ public class ConfigHandler
|
||||
/// <returns>0 if successful</returns>
|
||||
public static async Task<int> DeleteSubItem(Config config, string id)
|
||||
{
|
||||
var item = await AppHandler.Instance.GetSubItem(id);
|
||||
var item = await AppManager.Instance.GetSubItem(id);
|
||||
if (item is null)
|
||||
{
|
||||
return 0;
|
||||
@@ -1896,7 +1945,7 @@ public class ConfigHandler
|
||||
/// <returns>0 if successful</returns>
|
||||
public static async Task<int> SetDefaultRouting(Config config, RoutingItem routingItem)
|
||||
{
|
||||
var items = await AppHandler.Instance.RoutingItems();
|
||||
var items = await AppManager.Instance.RoutingItems();
|
||||
if (items.Any(t => t.Id == routingItem.Id && t.IsActive == true))
|
||||
{
|
||||
return -1;
|
||||
@@ -1976,7 +2025,7 @@ public class ConfigHandler
|
||||
if (template == null)
|
||||
return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback
|
||||
|
||||
var items = await AppHandler.Instance.RoutingItems();
|
||||
var items = await AppManager.Instance.RoutingItems();
|
||||
var maxSort = items.Count;
|
||||
if (!blImportAdvancedRules && items.Where(t => t.Remarks.StartsWith(template.Version)).ToList().Count > 0)
|
||||
{
|
||||
@@ -2023,14 +2072,14 @@ public class ConfigHandler
|
||||
public static async Task<int> InitBuiltinRouting(Config config, bool blImportAdvancedRules = false)
|
||||
{
|
||||
var ver = "V3-";
|
||||
var items = await AppHandler.Instance.RoutingItems();
|
||||
var items = await AppManager.Instance.RoutingItems();
|
||||
|
||||
//TODO Temporary code to be removed later
|
||||
var lockItem = items?.FirstOrDefault(t => t.Locked == true);
|
||||
if (lockItem != null)
|
||||
{
|
||||
await ConfigHandler.RemoveRoutingItem(lockItem);
|
||||
items = await AppHandler.Instance.RoutingItems();
|
||||
items = await AppManager.Instance.RoutingItems();
|
||||
}
|
||||
|
||||
if (!blImportAdvancedRules && items.Count > 0)
|
||||
@@ -2107,7 +2156,7 @@ public class ConfigHandler
|
||||
/// <returns>0 if successful</returns>
|
||||
public static async Task<int> InitBuiltinDNS(Config config)
|
||||
{
|
||||
var items = await AppHandler.Instance.DNSItems();
|
||||
var items = await AppManager.Instance.DNSItems();
|
||||
|
||||
// Check existing DNS items and disable those with empty NormalDNS
|
||||
var needsUpdate = false;
|
||||
@@ -2185,7 +2234,7 @@ public class ConfigHandler
|
||||
/// <returns>DNS item with configuration from the URL</returns>
|
||||
public static async Task<DNSItem> GetExternalDNSItem(ECoreType type, string url)
|
||||
{
|
||||
var currentItem = await AppHandler.Instance.GetDNSItem(type);
|
||||
var currentItem = await AppManager.Instance.GetDNSItem(type);
|
||||
|
||||
var downloadHandle = new DownloadService();
|
||||
var templateContent = await downloadHandle.TryDownloadString(url, true, "");
|
||||
@@ -2221,11 +2270,10 @@ public class ConfigHandler
|
||||
UseSystemHosts = false,
|
||||
AddCommonHosts = true,
|
||||
FakeIP = false,
|
||||
GlobalFakeIp = true,
|
||||
BlockBindingQuery = true,
|
||||
DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(),
|
||||
RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(),
|
||||
SingboxOutboundsResolveDNS = Global.DomainDirectDNSAddress.FirstOrDefault(),
|
||||
SingboxFinalResolveDNS = Global.DomainPureIPDNSAddress.FirstOrDefault()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2247,7 +2295,7 @@ public class ConfigHandler
|
||||
|
||||
public static async Task<int> InitBuiltinFullConfigTemplate(Config config)
|
||||
{
|
||||
var items = await AppHandler.Instance.FullConfigTemplateItem();
|
||||
var items = await AppManager.Instance.FullConfigTemplateItem();
|
||||
if (items.Count <= 0)
|
||||
{
|
||||
var item = new FullConfigTemplateItem()
|
||||
@@ -2321,10 +2369,22 @@ public class ConfigHandler
|
||||
config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[1];
|
||||
config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[1];
|
||||
|
||||
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[1] + "v2ray.json"));
|
||||
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[1] + "sing_box.json"));
|
||||
var xrayDnsRussia = await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[1] + "v2ray.json");
|
||||
var singboxDnsRussia = await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[1] + "sing_box.json");
|
||||
var simpleDnsRussia = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[1] + "simple_dns.json");
|
||||
|
||||
config.SimpleDNSItem = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[1] + "simple_dns.json") ?? InitBuiltinSimpleDNS();
|
||||
if (simpleDnsRussia == null)
|
||||
{
|
||||
xrayDnsRussia.Enabled = true;
|
||||
singboxDnsRussia.Enabled = true;
|
||||
config.SimpleDNSItem = InitBuiltinSimpleDNS();
|
||||
}
|
||||
else
|
||||
{
|
||||
config.SimpleDNSItem = simpleDnsRussia;
|
||||
}
|
||||
await SaveDNSItems(config, xrayDnsRussia);
|
||||
await SaveDNSItems(config, singboxDnsRussia);
|
||||
break;
|
||||
|
||||
case EPresetType.Iran:
|
||||
@@ -2332,10 +2392,22 @@ public class ConfigHandler
|
||||
config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[2];
|
||||
config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[2];
|
||||
|
||||
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[2] + "v2ray.json"));
|
||||
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[2] + "sing_box.json"));
|
||||
var xrayDnsIran = await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[2] + "v2ray.json");
|
||||
var singboxDnsIran = await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[2] + "sing_box.json");
|
||||
var simpleDnsIran = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[2] + "simple_dns.json");
|
||||
|
||||
config.SimpleDNSItem = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[2] + "simple_dns.json") ?? InitBuiltinSimpleDNS();
|
||||
if (simpleDnsIran == null)
|
||||
{
|
||||
xrayDnsIran.Enabled = true;
|
||||
singboxDnsIran.Enabled = true;
|
||||
config.SimpleDNSItem = InitBuiltinSimpleDNS();
|
||||
}
|
||||
else
|
||||
{
|
||||
config.SimpleDNSItem = simpleDnsIran;
|
||||
}
|
||||
await SaveDNSItems(config, xrayDnsIran);
|
||||
await SaveDNSItems(config, singboxDnsIran);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
using System.Net;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
|
||||
public class ConnectionHandler
|
||||
public static class ConnectionHandler
|
||||
{
|
||||
private static readonly Lazy<ConnectionHandler> _instance = new(() => new());
|
||||
public static ConnectionHandler Instance => _instance.Value;
|
||||
private static readonly string _tag = "ConnectionHandler";
|
||||
|
||||
public async Task<string> RunAvailabilityCheck()
|
||||
public static async Task<string> RunAvailabilityCheck()
|
||||
{
|
||||
var downloadHandle = new DownloadService();
|
||||
var time = await downloadHandle.RunAvailabilityCheck(null);
|
||||
var ip = time > 0 ? await GetIPInfo(downloadHandle) ?? Global.None : Global.None;
|
||||
var time = await GetRealPingTime();
|
||||
var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None;
|
||||
|
||||
return string.Format(ResUI.TestMeOutput, time, ip);
|
||||
}
|
||||
|
||||
private async Task<string?> GetIPInfo(DownloadService downloadHandle)
|
||||
private static async Task<string?> GetIPInfo()
|
||||
{
|
||||
var url = AppHandler.Instance.Config.SpeedTestItem.IPAPIUrl;
|
||||
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
|
||||
if (url.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var downloadHandle = new DownloadService();
|
||||
var result = await downloadHandle.TryDownloadString(url, true, "");
|
||||
if (result == null)
|
||||
{
|
||||
@@ -39,4 +40,31 @@ public class ConnectionHandler
|
||||
|
||||
return $"({country ?? "unknown"}) {ip}";
|
||||
}
|
||||
|
||||
private static async Task<int> GetRealPingTime()
|
||||
{
|
||||
var responseTime = -1;
|
||||
try
|
||||
{
|
||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}");
|
||||
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
responseTime = await HttpClientHelper.Instance.GetRealPingTime(url, webProxy, 10);
|
||||
if (responseTime > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
return -1;
|
||||
}
|
||||
return responseTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
|
||||
public class CoreAdminHandler
|
||||
{
|
||||
private static readonly Lazy<CoreAdminHandler> _instance = new(() => new());
|
||||
public static CoreAdminHandler Instance => _instance.Value;
|
||||
private Config _config;
|
||||
private Action<bool, string>? _updateFunc;
|
||||
private int _linuxSudoPid = -1;
|
||||
private const string _tag = "CoreAdminHandler";
|
||||
|
||||
public async Task Init(Config config, Action<bool, string> updateFunc)
|
||||
{
|
||||
if (_config != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_config = config;
|
||||
_updateFunc = updateFunc;
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void UpdateFunc(bool notify, string msg)
|
||||
{
|
||||
_updateFunc?.Invoke(notify, msg);
|
||||
}
|
||||
|
||||
public async Task<Process?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine("#!/bin/bash");
|
||||
var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";
|
||||
sb.AppendLine($"sudo -S {cmdLine}");
|
||||
var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
|
||||
|
||||
Process proc = new()
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = shFilePath,
|
||||
Arguments = "",
|
||||
WorkingDirectory = Utils.GetBinConfigPath(),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
}
|
||||
};
|
||||
|
||||
void dataHandler(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (e.Data.IsNotEmpty())
|
||||
{
|
||||
UpdateFunc(false, e.Data + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
|
||||
proc.OutputDataReceived += dataHandler;
|
||||
proc.ErrorDataReceived += dataHandler;
|
||||
|
||||
proc.Start();
|
||||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
|
||||
await Task.Delay(10);
|
||||
await proc.StandardInput.WriteLineAsync(AppHandler.Instance.LinuxSudoPwd);
|
||||
|
||||
await Task.Delay(100);
|
||||
if (proc is null or { HasExited: true })
|
||||
{
|
||||
throw new Exception(ResUI.FailedToRunCore);
|
||||
}
|
||||
|
||||
_linuxSudoPid = proc.Id;
|
||||
|
||||
return proc;
|
||||
}
|
||||
|
||||
public async Task KillProcessAsLinuxSudo()
|
||||
{
|
||||
if (_linuxSudoPid < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var shellFileName = Utils.IsOSX() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName;
|
||||
var shFilePath = await FileManager.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true);
|
||||
if (shFilePath.Contains(' '))
|
||||
{
|
||||
shFilePath = shFilePath.AppendQuotes();
|
||||
}
|
||||
var arg = new List<string>() { "-c", $"sudo -S {shFilePath} {_linuxSudoPid}" };
|
||||
var result = await Cli.Wrap(Global.LinuxBash)
|
||||
.WithArguments(arg)
|
||||
.WithStandardInputPipe(PipeSource.FromString(AppHandler.Instance.LinuxSudoPwd))
|
||||
.ExecuteBufferedAsync();
|
||||
|
||||
UpdateFunc(false, result.StandardOutput.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
_linuxSudoPid = -1;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ namespace ServiceLib.Handler;
|
||||
/// <summary>
|
||||
/// Core configuration file processing class
|
||||
/// </summary>
|
||||
public class CoreConfigHandler
|
||||
public static class CoreConfigHandler
|
||||
{
|
||||
private static readonly string _tag = "CoreConfigHandler";
|
||||
|
||||
public static async Task<RetResult> GenerateClientConfig(ProfileItem node, string? fileName)
|
||||
{
|
||||
var config = AppHandler.Instance.Config;
|
||||
var config = AppManager.Instance.Config;
|
||||
var result = new RetResult();
|
||||
|
||||
if (node.ConfigType == EConfigType.Custom)
|
||||
@@ -21,7 +21,7 @@ public class CoreConfigHandler
|
||||
_ => await GenerateClientCustomConfig(node, fileName)
|
||||
};
|
||||
}
|
||||
else if (AppHandler.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
|
||||
else if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
|
||||
{
|
||||
result = await new CoreConfigSingboxService(config).GenerateClientConfigContent(node);
|
||||
}
|
||||
@@ -112,11 +112,11 @@ public class CoreConfigHandler
|
||||
public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, ProfileItem node, ServerTestItem testItem, string fileName)
|
||||
{
|
||||
var result = new RetResult();
|
||||
var initPort = AppHandler.Instance.GetLocalPort(EInboundProtocol.speedtest);
|
||||
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
|
||||
var port = Utils.GetFreePort(initPort + testItem.QueueNum);
|
||||
testItem.Port = port;
|
||||
|
||||
if (AppHandler.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
|
||||
if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
|
||||
{
|
||||
result = await new CoreConfigSingboxService(config).GenerateClientSpeedtestConfig(node, port);
|
||||
}
|
||||
@@ -132,24 +132,4 @@ public class CoreConfigHandler
|
||||
await File.WriteAllTextAsync(fileName, result.Data.ToString());
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<RetResult> GenerateClientMultipleLoadConfig(Config config, string fileName, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad)
|
||||
{
|
||||
var result = new RetResult();
|
||||
if (coreType == ECoreType.sing_box)
|
||||
{
|
||||
result = await new CoreConfigSingboxService(config).GenerateClientMultipleLoadConfig(selecteds);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await new CoreConfigV2rayService(config).GenerateClientMultipleLoadConfig(selecteds, multipleLoad);
|
||||
}
|
||||
|
||||
if (result.Success != true)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
await File.WriteAllTextAsync(fileName, result.Data.ToString());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,61 +155,60 @@ public class BaseFmt
|
||||
|
||||
protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item)
|
||||
{
|
||||
item.Flow = query["flow"] ?? "";
|
||||
item.StreamSecurity = query["security"] ?? "";
|
||||
item.Sni = query["sni"] ?? "";
|
||||
item.Alpn = Utils.UrlDecode(query["alpn"] ?? "");
|
||||
item.Fingerprint = Utils.UrlDecode(query["fp"] ?? "");
|
||||
item.PublicKey = Utils.UrlDecode(query["pbk"] ?? "");
|
||||
item.ShortId = Utils.UrlDecode(query["sid"] ?? "");
|
||||
item.SpiderX = Utils.UrlDecode(query["spx"] ?? "");
|
||||
item.Mldsa65Verify = Utils.UrlDecode(query["pqv"] ?? "");
|
||||
item.AllowInsecure = (query["allowInsecure"] ?? "") == "1" ? "true" : "";
|
||||
item.Flow = GetQueryValue(query, "flow");
|
||||
item.StreamSecurity = GetQueryValue(query, "security");
|
||||
item.Sni = GetQueryValue(query, "sni");
|
||||
item.Alpn = GetQueryDecoded(query, "alpn");
|
||||
item.Fingerprint = GetQueryDecoded(query, "fp");
|
||||
item.PublicKey = GetQueryDecoded(query, "pbk");
|
||||
item.ShortId = GetQueryDecoded(query, "sid");
|
||||
item.SpiderX = GetQueryDecoded(query, "spx");
|
||||
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
|
||||
item.AllowInsecure = new[] { "allowInsecure", "allow_insecure", "insecure" }.Any(k => (query[k] ?? "") == "1") ? "true" : "";
|
||||
|
||||
item.Network = query["type"] ?? nameof(ETransport.tcp);
|
||||
item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp));
|
||||
switch (item.Network)
|
||||
{
|
||||
case nameof(ETransport.tcp):
|
||||
item.HeaderType = query["headerType"] ?? Global.None;
|
||||
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
|
||||
|
||||
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
|
||||
item.RequestHost = GetQueryDecoded(query, "host");
|
||||
break;
|
||||
|
||||
case nameof(ETransport.kcp):
|
||||
item.HeaderType = query["headerType"] ?? Global.None;
|
||||
item.Path = Utils.UrlDecode(query["seed"] ?? "");
|
||||
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
|
||||
item.Path = GetQueryDecoded(query, "seed");
|
||||
break;
|
||||
|
||||
case nameof(ETransport.ws):
|
||||
case nameof(ETransport.httpupgrade):
|
||||
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
|
||||
item.Path = Utils.UrlDecode(query["path"] ?? "/");
|
||||
item.RequestHost = GetQueryDecoded(query, "host");
|
||||
item.Path = GetQueryDecoded(query, "path", "/");
|
||||
break;
|
||||
|
||||
case nameof(ETransport.xhttp):
|
||||
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
|
||||
item.Path = Utils.UrlDecode(query["path"] ?? "/");
|
||||
item.HeaderType = Utils.UrlDecode(query["mode"] ?? "");
|
||||
item.Extra = Utils.UrlDecode(query["extra"] ?? "");
|
||||
item.RequestHost = GetQueryDecoded(query, "host");
|
||||
item.Path = GetQueryDecoded(query, "path", "/");
|
||||
item.HeaderType = GetQueryDecoded(query, "mode");
|
||||
item.Extra = GetQueryDecoded(query, "extra");
|
||||
break;
|
||||
|
||||
case nameof(ETransport.http):
|
||||
case nameof(ETransport.h2):
|
||||
item.Network = nameof(ETransport.h2);
|
||||
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
|
||||
item.Path = Utils.UrlDecode(query["path"] ?? "/");
|
||||
item.RequestHost = GetQueryDecoded(query, "host");
|
||||
item.Path = GetQueryDecoded(query, "path", "/");
|
||||
break;
|
||||
|
||||
case nameof(ETransport.quic):
|
||||
item.HeaderType = query["headerType"] ?? Global.None;
|
||||
item.RequestHost = query["quicSecurity"] ?? Global.None;
|
||||
item.Path = Utils.UrlDecode(query["key"] ?? "");
|
||||
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
|
||||
item.RequestHost = GetQueryValue(query, "quicSecurity", Global.None);
|
||||
item.Path = GetQueryDecoded(query, "key");
|
||||
break;
|
||||
|
||||
case nameof(ETransport.grpc):
|
||||
item.RequestHost = Utils.UrlDecode(query["authority"] ?? "");
|
||||
item.Path = Utils.UrlDecode(query["serviceName"] ?? "");
|
||||
item.HeaderType = Utils.UrlDecode(query["mode"] ?? Global.GrpcGunMode);
|
||||
item.RequestHost = GetQueryDecoded(query, "authority");
|
||||
item.Path = GetQueryDecoded(query, "serviceName");
|
||||
item.HeaderType = GetQueryDecoded(query, "mode", Global.GrpcGunMode);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -239,4 +238,14 @@ public class BaseFmt
|
||||
var url = $"{Utils.UrlEncode(userInfo)}@{GetIpv6(address)}:{port}";
|
||||
return $"{Global.ProtocolShares[eConfigType]}{url}{query}{remark}";
|
||||
}
|
||||
|
||||
protected static string GetQueryValue(NameValueCollection query, string key, string defaultValue = "")
|
||||
{
|
||||
return query[key] ?? defaultValue;
|
||||
}
|
||||
|
||||
protected static string GetQueryDecoded(NameValueCollection query, string key, string defaultValue = "")
|
||||
{
|
||||
return Utils.UrlDecode(GetQueryValue(query, key, defaultValue));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ public class ClashFmt : BaseFmt
|
||||
{
|
||||
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
|
||||
{
|
||||
if (Contains(strData, "port", "socks-port", "proxies"))
|
||||
if (Contains(strData, "rules", "-port", "proxies"))
|
||||
{
|
||||
var fileName = WriteAllText(strData, "yaml");
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class FmtHandler
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
return "";
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
public class HtmlPageFmt : BaseFmt
|
||||
|
||||
@@ -21,10 +21,10 @@ public class Hysteria2Fmt : BaseFmt
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
ResolveStdTransport(query, ref item);
|
||||
item.Path = Utils.UrlDecode(query["obfs-password"] ?? "");
|
||||
item.AllowInsecure = (query["insecure"] ?? "") == "1" ? "true" : "false";
|
||||
item.Path = GetQueryDecoded(query, "obfs-password");
|
||||
item.AllowInsecure = GetQueryValue(query, "insecure") == "1" ? "true" : "false";
|
||||
|
||||
item.Ports = Utils.UrlDecode(query["mport"] ?? "");
|
||||
item.Ports = GetQueryDecoded(query, "mport");
|
||||
|
||||
return item;
|
||||
}
|
||||
@@ -63,24 +63,6 @@ public class Hysteria2Fmt : BaseFmt
|
||||
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Id, dicQuery, remark);
|
||||
}
|
||||
|
||||
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
|
||||
{
|
||||
if (Contains(strData, "server", "up", "down", "listen", "<html>", "<body>"))
|
||||
{
|
||||
var fileName = WriteAllText(strData);
|
||||
|
||||
var profileItem = new ProfileItem
|
||||
{
|
||||
CoreType = ECoreType.hysteria,
|
||||
Address = fileName,
|
||||
Remarks = subRemarks ?? "hysteria_custom"
|
||||
};
|
||||
return profileItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ProfileItem? ResolveFull2(string strData, string? subRemarks)
|
||||
{
|
||||
if (Contains(strData, "server", "auth", "up", "down", "listen"))
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
public class NaiveproxyFmt : BaseFmt
|
||||
{
|
||||
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
|
||||
{
|
||||
if (Contains(strData, "listen", "proxy", "<html>", "<body>"))
|
||||
{
|
||||
var fileName = WriteAllText(strData);
|
||||
|
||||
var profileItem = new ProfileItem
|
||||
{
|
||||
CoreType = ECoreType.naiveproxy,
|
||||
Address = fileName,
|
||||
Remarks = subRemarks ?? "naiveproxy_custom"
|
||||
};
|
||||
return profileItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class ShadowsocksFmt : BaseFmt
|
||||
// item.port);
|
||||
//url = Utile.Base64Encode(url);
|
||||
//new Sip002
|
||||
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}");
|
||||
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
|
||||
return ToUri(EConfigType.Shadowsocks, item.Address, item.Port, pw, null, remark);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public class SocksFmt : BaseFmt
|
||||
remark = "#" + Utils.UrlEncode(item.Remarks);
|
||||
}
|
||||
//new
|
||||
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}");
|
||||
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
|
||||
return ToUri(EConfigType.SOCKS, item.Address, item.Port, pw, null, remark);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class TuicFmt : BaseFmt
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
ResolveStdTransport(query, ref item);
|
||||
item.HeaderType = query["congestion_control"] ?? "";
|
||||
item.HeaderType = GetQueryValue(query, "congestion_control");
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ public class VLESSFmt : BaseFmt
|
||||
item.Id = Utils.UrlDecode(url.UserInfo);
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
item.Security = query["encryption"] ?? Global.None;
|
||||
item.StreamSecurity = query["security"] ?? "";
|
||||
item.Security = GetQueryValue(query, "encryption", Global.None);
|
||||
item.StreamSecurity = GetQueryValue(query, "security");
|
||||
_ = ResolveStdTransport(query, ref item);
|
||||
|
||||
return item;
|
||||
|
||||
@@ -24,10 +24,10 @@ public class WireguardFmt : BaseFmt
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
|
||||
item.PublicKey = Utils.UrlDecode(query["publickey"] ?? "");
|
||||
item.Path = Utils.UrlDecode(query["reserved"] ?? "");
|
||||
item.RequestHost = Utils.UrlDecode(query["address"] ?? "");
|
||||
item.ShortId = Utils.UrlDecode(query["mtu"] ?? "");
|
||||
item.PublicKey = GetQueryDecoded(query, "publickey");
|
||||
item.Path = GetQueryDecoded(query, "reserved");
|
||||
item.RequestHost = GetQueryDecoded(query, "address");
|
||||
item.ShortId = GetQueryDecoded(query, "mtu");
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
namespace ServiceLib.Handler;
|
||||
|
||||
public class SubscriptionHandler
|
||||
public static class SubscriptionHandler
|
||||
{
|
||||
public static async Task UpdateProcess(Config config, string subId, bool blProxy, Action<bool, string> updateFunc)
|
||||
public static async Task UpdateProcess(Config config, string subId, bool blProxy, Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart);
|
||||
var subItem = await AppHandler.Instance.SubItems();
|
||||
await updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart);
|
||||
var subItem = await AppManager.Instance.SubItems();
|
||||
|
||||
if (subItem is not { Count: > 0 })
|
||||
{
|
||||
updateFunc?.Invoke(false, ResUI.MsgNoValidSubscription);
|
||||
await updateFunc?.Invoke(false, ResUI.MsgNoValidSubscription);
|
||||
return;
|
||||
}
|
||||
|
||||
var successCount = 0;
|
||||
foreach (var item in subItem)
|
||||
{
|
||||
try
|
||||
@@ -25,32 +26,35 @@ public class SubscriptionHandler
|
||||
var hashCode = $"{item.Remarks}->";
|
||||
if (item.Enabled == false)
|
||||
{
|
||||
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}");
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create download handler
|
||||
var downloadHandle = CreateDownloadHandler(hashCode, updateFunc);
|
||||
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
|
||||
|
||||
// Get all subscription content (main subscription + additional subscriptions)
|
||||
var result = await DownloadAllSubscriptions(config, item, blProxy, downloadHandle);
|
||||
|
||||
// Process download result
|
||||
await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc);
|
||||
if (await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc))
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
|
||||
updateFunc?.Invoke(false, "-------------------------------------------------------");
|
||||
await updateFunc?.Invoke(false, "-------------------------------------------------------");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var hashCode = $"{item.Remarks}->";
|
||||
Logging.SaveLog("UpdateSubscription", ex);
|
||||
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgFailedImportSubscription}: {ex.Message}");
|
||||
updateFunc?.Invoke(false, "-------------------------------------------------------");
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgFailedImportSubscription}: {ex.Message}");
|
||||
await updateFunc?.Invoke(false, "-------------------------------------------------------");
|
||||
}
|
||||
}
|
||||
|
||||
updateFunc?.Invoke(true, $"{ResUI.MsgUpdateSubscriptionEnd}");
|
||||
await updateFunc?.Invoke(successCount > 0, $"{ResUI.MsgUpdateSubscriptionEnd}");
|
||||
}
|
||||
|
||||
private static bool IsValidSubscription(SubItem item, string subId)
|
||||
@@ -76,7 +80,7 @@ public class SubscriptionHandler
|
||||
return true;
|
||||
}
|
||||
|
||||
private static DownloadService CreateDownloadHandler(string hashCode, Action<bool, string> updateFunc)
|
||||
private static DownloadService CreateDownloadHandler(string hashCode, Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
var downloadHandle = new DownloadService();
|
||||
downloadHandle.Error += (sender2, args) =>
|
||||
@@ -181,22 +185,24 @@ public class SubscriptionHandler
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task ProcessDownloadResult(Config config, string id, string result, string hashCode, Action<bool, string> updateFunc)
|
||||
private static async Task<bool> ProcessDownloadResult(Config config, string id, string result, string hashCode, Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecodingFailed}");
|
||||
return;
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecodingFailed}");
|
||||
return false;
|
||||
}
|
||||
|
||||
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgGetSubscriptionSuccessfully}");
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgGetSubscriptionSuccessfully}");
|
||||
|
||||
// If result is too short, display content directly
|
||||
if (result.Length < 99)
|
||||
{
|
||||
updateFunc?.Invoke(false, $"{hashCode}{result}");
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{result}");
|
||||
}
|
||||
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartParsingSubscription}");
|
||||
|
||||
// Add servers to configuration
|
||||
var ret = await ConfigHandler.AddBatchServers(config, result, id, true);
|
||||
if (ret <= 0)
|
||||
@@ -206,9 +212,10 @@ public class SubscriptionHandler
|
||||
}
|
||||
|
||||
// Update completion message
|
||||
updateFunc?.Invoke(false,
|
||||
ret > 0
|
||||
await updateFunc?.Invoke(false, ret > 0
|
||||
? $"{hashCode}{ResUI.MsgUpdateSubscriptionEnd}"
|
||||
: $"{hashCode}{ResUI.MsgFailedImportSubscription}");
|
||||
|
||||
return ret > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ServiceLib.Handler.SysProxy;
|
||||
|
||||
public class ProxySettingLinux
|
||||
public static class ProxySettingLinux
|
||||
{
|
||||
private static readonly string _proxySetFileName = $"{Global.ProxySetLinuxShellFileName.Replace(Global.NamespaceSample, "")}.sh";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ServiceLib.Handler.SysProxy;
|
||||
|
||||
public class ProxySettingOSX
|
||||
public static class ProxySettingOSX
|
||||
{
|
||||
private static readonly string _proxySetFileName = $"{Global.ProxySetOSXShellFileName.Replace(Global.NamespaceSample, "")}.sh";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using static ServiceLib.Handler.SysProxy.ProxySettingWindows.InternetConnectionO
|
||||
|
||||
namespace ServiceLib.Handler.SysProxy;
|
||||
|
||||
public class ProxySettingWindows
|
||||
public static class ProxySettingWindows
|
||||
{
|
||||
private const string _regPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public static class SysProxyHandler
|
||||
|
||||
try
|
||||
{
|
||||
var port = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
var exceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
|
||||
if (port <= 0)
|
||||
{
|
||||
@@ -56,7 +56,7 @@ public static class SysProxyHandler
|
||||
|
||||
if (type != ESysProxyType.Pac && Utils.IsWindows())
|
||||
{
|
||||
PacHandler.Instance.Stop();
|
||||
PacManager.Instance.Stop();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -90,8 +90,8 @@ public static class SysProxyHandler
|
||||
|
||||
private static async Task SetWindowsProxyPac(int port)
|
||||
{
|
||||
var portPac = AppHandler.Instance.GetLocalPort(EInboundProtocol.pac);
|
||||
await PacHandler.Instance.StartAsync(Utils.GetConfigPath(), port, portPac);
|
||||
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);
|
||||
await PacManager.Instance.StartAsync(Utils.GetConfigPath(), port, portPac);
|
||||
var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
|
||||
ProxySettingWindows.SetProxy(strProxy, "", 4);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Net;
|
||||
using Downloader;
|
||||
|
||||
namespace ServiceLib.Common;
|
||||
namespace ServiceLib.Helper;
|
||||
|
||||
public class DownloaderHelper
|
||||
{
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
|
||||
namespace ServiceLib.Common;
|
||||
namespace ServiceLib.Helper;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
@@ -202,4 +204,35 @@ public class HttpClientHelper
|
||||
}
|
||||
} while (isMoreToRead);
|
||||
}
|
||||
|
||||
public async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
|
||||
{
|
||||
var responseTime = -1;
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout));
|
||||
using var client = new HttpClient(new SocketsHttpHandler()
|
||||
{
|
||||
Proxy = webProxy,
|
||||
UseProxy = webProxy != null
|
||||
});
|
||||
|
||||
List<int> oneTime = new();
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var timer = Stopwatch.StartNew();
|
||||
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
|
||||
timer.Stop();
|
||||
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
|
||||
await Task.Delay(100);
|
||||
}
|
||||
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
|
||||
}
|
||||
catch //(Exception ex)
|
||||
{
|
||||
//Utile.SaveLog(ex.Message, ex);
|
||||
}
|
||||
return responseTime;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections;
|
||||
using SQLite;
|
||||
|
||||
namespace ServiceLib.Common;
|
||||
namespace ServiceLib.Helper;
|
||||
|
||||
public sealed class SQLiteHelper
|
||||
{
|
||||
@@ -26,7 +26,7 @@ public sealed class SQLiteHelper
|
||||
|
||||
public async Task<int> InsertAllAsync(IEnumerable models)
|
||||
{
|
||||
return await _dbAsync.InsertAllAsync(models);
|
||||
return await _dbAsync.InsertAllAsync(models, runInTransaction: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> InsertAsync(object model)
|
||||
@@ -46,7 +46,7 @@ public sealed class SQLiteHelper
|
||||
|
||||
public async Task<int> UpdateAllAsync(IEnumerable models)
|
||||
{
|
||||
return await _dbAsync.UpdateAllAsync(models);
|
||||
return await _dbAsync.UpdateAllAsync(models, runInTransaction: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> DeleteAsync(object model)
|
||||
282
v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs
Normal file
282
v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.).
|
||||
/// </summary>
|
||||
public class ActionPrecheckManager(Config config)
|
||||
{
|
||||
private static readonly Lazy<ActionPrecheckManager> _instance = new(() => new ActionPrecheckManager(AppManager.Instance.Config));
|
||||
public static ActionPrecheckManager Instance => _instance.Value;
|
||||
|
||||
private readonly Config _config = config;
|
||||
|
||||
public async Task<List<string>> Check(string? indexId)
|
||||
{
|
||||
if (indexId.IsNullOrEmpty())
|
||||
{
|
||||
return [ResUI.PleaseSelectServer];
|
||||
}
|
||||
|
||||
var item = await AppManager.Instance.GetProfileItem(indexId);
|
||||
if (item is null)
|
||||
{
|
||||
return [ResUI.PleaseSelectServer];
|
||||
}
|
||||
|
||||
return await Check(item);
|
||||
}
|
||||
|
||||
public async Task<List<string>> Check(ProfileItem? item)
|
||||
{
|
||||
if (item is null)
|
||||
{
|
||||
return [ResUI.PleaseSelectServer];
|
||||
}
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item));
|
||||
errors.AddRange(await ValidateRelatedNodesExistAndValid(item));
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private async Task<List<string>> ValidateCurrentNodeAndCoreSupport(ProfileItem item)
|
||||
{
|
||||
if (item.ConfigType == EConfigType.Custom)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
|
||||
return await ValidateNodeAndCoreSupport(item, coreType);
|
||||
}
|
||||
|
||||
private async Task<List<string>> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType);
|
||||
|
||||
if (item.ConfigType is EConfigType.Custom)
|
||||
{
|
||||
errors.Add(string.Format(ResUI.NotSupportProtocol, item.ConfigType.ToString()));
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!item.IsComplex())
|
||||
{
|
||||
if (item.Address.IsNullOrEmpty())
|
||||
{
|
||||
errors.Add(string.Format(ResUI.InvalidProperty, "Address"));
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (item.Port is <= 0 or >= 65536)
|
||||
{
|
||||
errors.Add(string.Format(ResUI.InvalidProperty, "Port"));
|
||||
return errors;
|
||||
}
|
||||
|
||||
switch (item.ConfigType)
|
||||
{
|
||||
case EConfigType.VMess:
|
||||
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
|
||||
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
|
||||
break;
|
||||
|
||||
case EConfigType.VLESS:
|
||||
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id) && item.Id.Length > 30)
|
||||
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
|
||||
if (!Global.Flows.Contains(item.Flow))
|
||||
errors.Add(string.Format(ResUI.InvalidProperty, "Flow"));
|
||||
break;
|
||||
|
||||
case EConfigType.Shadowsocks:
|
||||
if (item.Id.IsNullOrEmpty())
|
||||
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
|
||||
if (string.IsNullOrEmpty(item.Security) || !Global.SsSecuritiesInSingbox.Contains(item.Security))
|
||||
errors.Add(string.Format(ResUI.InvalidProperty, "Security"));
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.ConfigType is EConfigType.VLESS or EConfigType.Trojan
|
||||
&& item.StreamSecurity == Global.StreamSecurityReality
|
||||
&& item.PublicKey.IsNullOrEmpty())
|
||||
{
|
||||
errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey"));
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.ConfigType.IsGroupType())
|
||||
{
|
||||
ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group);
|
||||
if (group is null || group.ChildItems.IsNullOrEmpty())
|
||||
{
|
||||
errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks));
|
||||
return errors;
|
||||
}
|
||||
|
||||
var hasCycle = ProfileGroupItemManager.HasCycle(item.IndexId);
|
||||
if (hasCycle)
|
||||
{
|
||||
errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks));
|
||||
return errors;
|
||||
}
|
||||
|
||||
foreach (var child in Utils.String2List(group.ChildItems))
|
||||
{
|
||||
var childErrors = new List<string>();
|
||||
if (child.IsNullOrEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var childItem = await AppManager.Instance.GetProfileItem(child);
|
||||
if (childItem is null)
|
||||
{
|
||||
childErrors.Add(string.Format(ResUI.NodeTagNotExist, child));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain)
|
||||
{
|
||||
childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks));
|
||||
continue;
|
||||
}
|
||||
|
||||
childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType));
|
||||
errors.AddRange(childErrors);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
var net = item.GetNetwork() ?? item.Network;
|
||||
|
||||
if (coreType == ECoreType.sing_box)
|
||||
{
|
||||
// sing-box does not support xhttp / kcp
|
||||
// sing-box does not support transports like ws/http/httpupgrade/etc. when the node is not vmess/trojan/vless
|
||||
if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
|
||||
{
|
||||
errors.Add(string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net));
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan))
|
||||
{
|
||||
if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade))
|
||||
{
|
||||
errors.Add(string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net));
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (coreType is ECoreType.Xray)
|
||||
{
|
||||
// Xray core does not support these protocols
|
||||
if (!Global.XraySupportConfigType.Contains(item.ConfigType)
|
||||
&& !item.IsComplex())
|
||||
{
|
||||
errors.Add(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString()));
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private async Task<List<string>> ValidateRelatedNodesExistAndValid(ProfileItem? item)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item));
|
||||
errors.AddRange(await ValidateRoutingNodeExistAndValid(item));
|
||||
return errors;
|
||||
}
|
||||
|
||||
private async Task<List<string>> ValidateProxyChainedNodeExistAndValid(ProfileItem? item)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
if (item is null)
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
// prev node and next node
|
||||
var subItem = await AppManager.Instance.GetSubItem(item.Subid);
|
||||
if (subItem is null)
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
|
||||
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
|
||||
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
|
||||
|
||||
await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors);
|
||||
await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List<string> errors)
|
||||
{
|
||||
if (node is not null)
|
||||
{
|
||||
var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType);
|
||||
errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + s));
|
||||
}
|
||||
else if (tag.IsNotEmpty())
|
||||
{
|
||||
errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<string>> ValidateRoutingNodeExistAndValid(ProfileItem? item)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
|
||||
var routing = await ConfigHandler.GetDefaultRouting(_config);
|
||||
if (routing == null)
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
|
||||
foreach (var ruleItem in rules ?? [])
|
||||
{
|
||||
if (!ruleItem.Enabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outboundTag = ruleItem.OutboundTag;
|
||||
if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
|
||||
if (tagItem is null)
|
||||
{
|
||||
errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag));
|
||||
continue;
|
||||
}
|
||||
|
||||
var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType);
|
||||
errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + s));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public sealed class AppHandler
|
||||
public sealed class AppManager
|
||||
{
|
||||
#region Property
|
||||
|
||||
private static readonly Lazy<AppHandler> _instance = new(() => new());
|
||||
private static readonly Lazy<AppManager> _instance = new(() => new());
|
||||
private Config _config;
|
||||
private int? _statePort;
|
||||
private int? _statePort2;
|
||||
private Job? _processJob;
|
||||
public static AppHandler Instance => _instance.Value;
|
||||
public static AppManager Instance => _instance.Value;
|
||||
public Config Config => _config;
|
||||
|
||||
public int StatePort
|
||||
@@ -34,7 +33,7 @@ public sealed class AppHandler
|
||||
|
||||
#endregion Property
|
||||
|
||||
#region Init
|
||||
#region App
|
||||
|
||||
public bool InitApp()
|
||||
{
|
||||
@@ -65,6 +64,7 @@ public sealed class AppHandler
|
||||
SQLiteHelper.Instance.CreateTable<ProfileExItem>();
|
||||
SQLiteHelper.Instance.CreateTable<DNSItem>();
|
||||
SQLiteHelper.Instance.CreateTable<FullConfigTemplateItem>();
|
||||
SQLiteHelper.Instance.CreateTable<ProfileGroupItem>();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,46 @@ public sealed class AppHandler
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion Init
|
||||
public async Task AppExitAsync(bool needShutdown)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logging.SaveLog("AppExitAsync Begin");
|
||||
|
||||
await SysProxyHandler.UpdateSysProxy(_config, true);
|
||||
AppEvents.AppExitRequested.Publish();
|
||||
await Task.Delay(50); //Wait for AppExitRequested to be processed
|
||||
|
||||
await ConfigHandler.SaveConfig(_config);
|
||||
await ProfileExManager.Instance.SaveTo();
|
||||
await StatisticsManager.Instance.SaveTo();
|
||||
await CoreManager.Instance.CoreStop();
|
||||
StatisticsManager.Instance.Close();
|
||||
|
||||
Logging.SaveLog("AppExitAsync End");
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
if (needShutdown)
|
||||
{
|
||||
Shutdown(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Shutdown(bool byUser)
|
||||
{
|
||||
AppEvents.ShutdownRequested.Publish(byUser);
|
||||
}
|
||||
|
||||
public async Task RebootAsAdmin()
|
||||
{
|
||||
ProcUtils.RebootAsAdmin();
|
||||
await AppManager.Instance.AppExitAsync(true);
|
||||
}
|
||||
|
||||
#endregion App
|
||||
|
||||
#region Config
|
||||
|
||||
@@ -97,21 +136,6 @@ public sealed class AppHandler
|
||||
return localPort + (int)protocol;
|
||||
}
|
||||
|
||||
public void AddProcess(IntPtr processHandle)
|
||||
{
|
||||
if (Utils.IsWindows())
|
||||
{
|
||||
_processJob ??= new();
|
||||
try
|
||||
{
|
||||
_processJob?.AddProcess(processHandle);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Config
|
||||
|
||||
#region SqliteHelper
|
||||
@@ -184,6 +208,15 @@ public sealed class AppHandler
|
||||
return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.Remarks == remarks);
|
||||
}
|
||||
|
||||
public async Task<ProfileGroupItem?> GetProfileGroupItem(string indexId)
|
||||
{
|
||||
if (indexId.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().FirstOrDefaultAsync(it => it.IndexId == indexId);
|
||||
}
|
||||
|
||||
public async Task<List<RoutingItem>?> RoutingItems()
|
||||
{
|
||||
return await SQLiteHelper.Instance.TableAsync<RoutingItem>().OrderBy(t => t.Sort).ToListAsync();
|
||||
@@ -1,11 +1,11 @@
|
||||
using static ServiceLib.Models.ClashProxies;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public sealed class ClashApiHandler
|
||||
public sealed class ClashApiManager
|
||||
{
|
||||
private static readonly Lazy<ClashApiHandler> instance = new(() => new());
|
||||
public static ClashApiHandler Instance => instance.Value;
|
||||
private static readonly Lazy<ClashApiManager> instance = new(() => new());
|
||||
public static ClashApiManager Instance => instance.Value;
|
||||
|
||||
private static readonly string _tag = "ClashApiHandler";
|
||||
private Dictionary<string, ProxiesItem>? _proxies;
|
||||
@@ -35,7 +35,7 @@ public sealed class ClashApiHandler
|
||||
return null;
|
||||
}
|
||||
|
||||
public void ClashProxiesDelayTest(bool blAll, List<ClashProxyModel> lstProxy, Action<ClashProxyModel?, string> updateFunc)
|
||||
public void ClashProxiesDelayTest(bool blAll, List<ClashProxyModel> lstProxy, Func<ClashProxyModel?, string, Task> updateFunc)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
@@ -65,7 +65,7 @@ public sealed class ClashApiHandler
|
||||
return;
|
||||
}
|
||||
var urlBase = $"{GetApiUrl()}/proxies";
|
||||
urlBase += @"/{0}/delay?timeout=10000&url=" + AppHandler.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
||||
urlBase += @"/{0}/delay?timeout=10000&url=" + AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var it in lstProxy)
|
||||
@@ -79,12 +79,12 @@ public sealed class ClashApiHandler
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var result = await HttpClientHelper.Instance.TryGetAsync(url);
|
||||
updateFunc?.Invoke(it, result);
|
||||
await updateFunc?.Invoke(it, result);
|
||||
}));
|
||||
}
|
||||
await Task.WhenAll(tasks);
|
||||
await Task.Delay(1000);
|
||||
updateFunc?.Invoke(null, "");
|
||||
await updateFunc?.Invoke(null, "");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,6 +182,6 @@ public sealed class ClashApiHandler
|
||||
|
||||
private string GetApiUrl()
|
||||
{
|
||||
return $"{Global.HttpProtocol}{Global.Loopback}:{AppHandler.Instance.StatePort2}";
|
||||
return $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}";
|
||||
}
|
||||
}
|
||||
92
v2rayN/ServiceLib/Manager/CoreAdminManager.cs
Normal file
92
v2rayN/ServiceLib/Manager/CoreAdminManager.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Text;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class CoreAdminManager
|
||||
{
|
||||
private static readonly Lazy<CoreAdminManager> _instance = new(() => new());
|
||||
public static CoreAdminManager Instance => _instance.Value;
|
||||
private Config _config;
|
||||
private Func<bool, string, Task>? _updateFunc;
|
||||
private int _linuxSudoPid = -1;
|
||||
private const string _tag = "CoreAdminHandler";
|
||||
|
||||
public async Task Init(Config config, Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
if (_config != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_config = config;
|
||||
_updateFunc = updateFunc;
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task UpdateFunc(bool notify, string msg)
|
||||
{
|
||||
await _updateFunc?.Invoke(notify, msg);
|
||||
}
|
||||
|
||||
public async Task<ProcessService?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine("#!/bin/bash");
|
||||
var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";
|
||||
sb.AppendLine($"sudo -S {cmdLine}");
|
||||
var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
|
||||
|
||||
var procService = new ProcessService(
|
||||
fileName: shFilePath,
|
||||
arguments: "",
|
||||
workingDirectory: Utils.GetBinConfigPath(),
|
||||
displayLog: true,
|
||||
redirectInput: true,
|
||||
environmentVars: null,
|
||||
updateFunc: _updateFunc
|
||||
);
|
||||
|
||||
await procService.StartAsync(AppManager.Instance.LinuxSudoPwd);
|
||||
|
||||
if (procService is null or { HasExited: true })
|
||||
{
|
||||
throw new Exception(ResUI.FailedToRunCore);
|
||||
}
|
||||
_linuxSudoPid = procService.Id;
|
||||
|
||||
return procService;
|
||||
}
|
||||
|
||||
public async Task KillProcessAsLinuxSudo()
|
||||
{
|
||||
if (_linuxSudoPid < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var shellFileName = Utils.IsOSX() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName;
|
||||
var shFilePath = await FileManager.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true);
|
||||
if (shFilePath.Contains(' '))
|
||||
{
|
||||
shFilePath = shFilePath.AppendQuotes();
|
||||
}
|
||||
var arg = new List<string>() { "-c", $"sudo -S {shFilePath} {_linuxSudoPid}" };
|
||||
var result = await Cli.Wrap(Global.LinuxBash)
|
||||
.WithArguments(arg)
|
||||
.WithStandardInputPipe(PipeSource.FromString(AppManager.Instance.LinuxSudoPwd))
|
||||
.ExecuteBufferedAsync();
|
||||
|
||||
await UpdateFunc(false, result.StandardOutput.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
_linuxSudoPid = -1;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public sealed class CoreInfoHandler
|
||||
public sealed class CoreInfoManager
|
||||
{
|
||||
private static readonly Lazy<CoreInfoHandler> _instance = new(() => new());
|
||||
private static readonly Lazy<CoreInfoManager> _instance = new(() => new());
|
||||
private List<CoreInfo>? _coreInfo;
|
||||
public static CoreInfoHandler Instance => _instance.Value;
|
||||
public static CoreInfoManager Instance => _instance.Value;
|
||||
|
||||
public CoreInfoHandler()
|
||||
public CoreInfoManager()
|
||||
{
|
||||
InitCoreInfo();
|
||||
}
|
||||
@@ -80,6 +80,10 @@ public sealed class CoreInfoHandler
|
||||
Url = GetCoreUrl(ECoreType.v2fly),
|
||||
Match = "V2Ray",
|
||||
VersionArg = "-version",
|
||||
Environment = new Dictionary<string, string?>()
|
||||
{
|
||||
{ Global.V2RayLocalAsset, Utils.GetBinPath("") },
|
||||
},
|
||||
},
|
||||
|
||||
new CoreInfo
|
||||
@@ -90,6 +94,10 @@ public sealed class CoreInfoHandler
|
||||
Url = GetCoreUrl(ECoreType.v2fly_v5),
|
||||
Match = "V2Ray",
|
||||
VersionArg = "version",
|
||||
Environment = new Dictionary<string, string?>()
|
||||
{
|
||||
{ Global.V2RayLocalAsset, Utils.GetBinPath("") },
|
||||
},
|
||||
},
|
||||
|
||||
new CoreInfo
|
||||
@@ -107,20 +115,25 @@ public sealed class CoreInfoHandler
|
||||
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
|
||||
Match = "Xray",
|
||||
VersionArg = "-version",
|
||||
Environment = new Dictionary<string, string?>()
|
||||
{
|
||||
{ Global.XrayLocalAsset, Utils.GetBinPath("") },
|
||||
{ Global.XrayLocalCert, Utils.GetBinPath("") },
|
||||
},
|
||||
},
|
||||
|
||||
new CoreInfo
|
||||
{
|
||||
CoreType = ECoreType.mihomo,
|
||||
CoreExes = ["mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "clash", "mihomo"],
|
||||
CoreExes = ["mihomo-windows-amd64-v1", "mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "clash", "mihomo"],
|
||||
Arguments = "-f {0}" + PortableMode(),
|
||||
Url = GetCoreUrl(ECoreType.mihomo),
|
||||
ReleaseApiUrl = urlMihomo.Replace(Global.GithubUrl, Global.GithubApiUrl),
|
||||
DownloadUrlWin64 = urlMihomo + "/download/{0}/mihomo-windows-amd64-compatible-{0}.zip",
|
||||
DownloadUrlWin64 = urlMihomo + "/download/{0}/mihomo-windows-amd64-v1-{0}.zip",
|
||||
DownloadUrlWinArm64 = urlMihomo + "/download/{0}/mihomo-windows-arm64-{0}.zip",
|
||||
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-compatible-{0}.gz",
|
||||
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
|
||||
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
|
||||
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-compatible-{0}.gz",
|
||||
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
|
||||
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
|
||||
Match = "Mihomo",
|
||||
VersionArg = "-v",
|
||||
@@ -205,12 +218,24 @@ public sealed class CoreInfoHandler
|
||||
new CoreInfo
|
||||
{
|
||||
CoreType = ECoreType.shadowquic,
|
||||
CoreExes = [ "shadowquic", "shadowquic"],
|
||||
CoreExes = [ "shadowquic" ],
|
||||
Arguments = "-c {0}",
|
||||
Url = GetCoreUrl(ECoreType.shadowquic),
|
||||
AbsolutePath = false,
|
||||
}
|
||||
},
|
||||
|
||||
new CoreInfo
|
||||
{
|
||||
CoreType = ECoreType.mieru,
|
||||
CoreExes = [ "mieru" ],
|
||||
Arguments = "run",
|
||||
Url = GetCoreUrl(ECoreType.mieru),
|
||||
AbsolutePath = false,
|
||||
Environment = new Dictionary<string, string?>()
|
||||
{
|
||||
{ "MIERU_CONFIG_JSON_FILE", "{0}" },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
/// <summary>
|
||||
/// Core process processing class
|
||||
/// </summary>
|
||||
public class CoreHandler
|
||||
public class CoreManager
|
||||
{
|
||||
private static readonly Lazy<CoreHandler> _instance = new(() => new());
|
||||
public static CoreHandler Instance => _instance.Value;
|
||||
private static readonly Lazy<CoreManager> _instance = new(() => new());
|
||||
public static CoreManager Instance => _instance.Value;
|
||||
private Config _config;
|
||||
private Process? _process;
|
||||
private Process? _processPre;
|
||||
private WindowsJob? _processJob;
|
||||
private ProcessService? _processService;
|
||||
private ProcessService? _processPreService;
|
||||
private bool _linuxSudo = false;
|
||||
private Action<bool, string>? _updateFunc;
|
||||
private Func<bool, string, Task>? _updateFunc;
|
||||
private const string _tag = "CoreHandler";
|
||||
|
||||
public async Task Init(Config config, Action<bool, string> updateFunc)
|
||||
public async Task Init(Config config, Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
_config = config;
|
||||
_updateFunc = updateFunc;
|
||||
|
||||
Environment.SetEnvironmentVariable(Global.V2RayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
|
||||
Environment.SetEnvironmentVariable(Global.XrayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
|
||||
Environment.SetEnvironmentVariable(Global.XrayLocalCert, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
|
||||
|
||||
//Copy the bin folder to the storage location (for init)
|
||||
if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1")
|
||||
{
|
||||
@@ -39,7 +33,7 @@ public class CoreHandler
|
||||
|
||||
if (Utils.IsNonWindows())
|
||||
{
|
||||
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo();
|
||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo();
|
||||
foreach (var it in coreInfo)
|
||||
{
|
||||
if (it.CoreType == ECoreType.v2rayN)
|
||||
@@ -67,7 +61,7 @@ public class CoreHandler
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
UpdateFunc(false, ResUI.CheckServerSettings);
|
||||
await UpdateFunc(false, ResUI.CheckServerSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,13 +69,13 @@ public class CoreHandler
|
||||
var result = await CoreConfigHandler.GenerateClientConfig(node, fileName);
|
||||
if (result.Success != true)
|
||||
{
|
||||
UpdateFunc(true, result.Msg);
|
||||
await UpdateFunc(true, result.Msg);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateFunc(false, $"{node.GetSummary()}");
|
||||
UpdateFunc(false, $"{Utils.GetRuntimeInfo()}");
|
||||
UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
|
||||
await UpdateFunc(false, $"{node.GetSummary()}");
|
||||
await UpdateFunc(false, $"{Utils.GetRuntimeInfo()}");
|
||||
await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
|
||||
await CoreStop();
|
||||
await Task.Delay(100);
|
||||
|
||||
@@ -93,43 +87,37 @@ public class CoreHandler
|
||||
|
||||
await CoreStart(node);
|
||||
await CoreStartPreService(node);
|
||||
if (_process != null)
|
||||
if (_processService != null)
|
||||
{
|
||||
UpdateFunc(true, $"{node.GetSummary()}");
|
||||
await UpdateFunc(true, $"{node.GetSummary()}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds)
|
||||
public async Task<ProcessService?> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds)
|
||||
{
|
||||
var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) ? ECoreType.sing_box : ECoreType.Xray;
|
||||
var coreType = selecteds.Any(t => Global.SingboxOnlyConfigType.Contains(t.ConfigType)) ? ECoreType.sing_box : ECoreType.Xray;
|
||||
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
|
||||
var configPath = Utils.GetBinConfigPath(fileName);
|
||||
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType);
|
||||
UpdateFunc(false, result.Msg);
|
||||
await UpdateFunc(false, result.Msg);
|
||||
if (result.Success != true)
|
||||
{
|
||||
return -1;
|
||||
return null;
|
||||
}
|
||||
|
||||
UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
|
||||
UpdateFunc(false, configPath);
|
||||
await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
|
||||
await UpdateFunc(false, configPath);
|
||||
|
||||
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType);
|
||||
var proc = await RunProcess(coreInfo, fileName, true, false);
|
||||
if (proc is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return proc.Id;
|
||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
|
||||
return await RunProcess(coreInfo, fileName, true, false);
|
||||
}
|
||||
|
||||
public async Task<int> LoadCoreConfigSpeedtest(ServerTestItem testItem)
|
||||
public async Task<ProcessService?> LoadCoreConfigSpeedtest(ServerTestItem testItem)
|
||||
{
|
||||
var node = await AppHandler.Instance.GetProfileItem(testItem.IndexId);
|
||||
var node = await AppManager.Instance.GetProfileItem(testItem.IndexId);
|
||||
if (node is null)
|
||||
{
|
||||
return -1;
|
||||
return null;
|
||||
}
|
||||
|
||||
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
|
||||
@@ -137,18 +125,12 @@ public class CoreHandler
|
||||
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath);
|
||||
if (result.Success != true)
|
||||
{
|
||||
return -1;
|
||||
return null;
|
||||
}
|
||||
|
||||
var coreType = AppHandler.Instance.GetCoreType(node, node.ConfigType);
|
||||
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType);
|
||||
var proc = await RunProcess(coreInfo, fileName, true, false);
|
||||
if (proc is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return proc.Id;
|
||||
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
|
||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
|
||||
return await RunProcess(coreInfo, fileName, true, false);
|
||||
}
|
||||
|
||||
public async Task CoreStop()
|
||||
@@ -157,20 +139,22 @@ public class CoreHandler
|
||||
{
|
||||
if (_linuxSudo)
|
||||
{
|
||||
await CoreAdminHandler.Instance.KillProcessAsLinuxSudo();
|
||||
await CoreAdminManager.Instance.KillProcessAsLinuxSudo();
|
||||
_linuxSudo = false;
|
||||
}
|
||||
|
||||
if (_process != null)
|
||||
if (_processService != null)
|
||||
{
|
||||
await ProcUtils.ProcessKill(_process, Utils.IsWindows());
|
||||
_process = null;
|
||||
await _processService.StopAsync();
|
||||
_processService.Dispose();
|
||||
_processService = null;
|
||||
}
|
||||
|
||||
if (_processPre != null)
|
||||
if (_processPreService != null)
|
||||
{
|
||||
await ProcUtils.ProcessKill(_processPre, Utils.IsWindows());
|
||||
_processPre = null;
|
||||
await _processPreService.StopAsync();
|
||||
_processPreService.Dispose();
|
||||
_processPreService = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -183,8 +167,8 @@ public class CoreHandler
|
||||
|
||||
private async Task CoreStart(ProfileItem node)
|
||||
{
|
||||
var coreType = _config.RunningCoreType = AppHandler.Instance.GetCoreType(node, node.ConfigType);
|
||||
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType);
|
||||
var coreType = _config.RunningCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
|
||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
|
||||
|
||||
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
|
||||
var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true);
|
||||
@@ -192,14 +176,14 @@ public class CoreHandler
|
||||
{
|
||||
return;
|
||||
}
|
||||
_process = proc;
|
||||
_processService = proc;
|
||||
}
|
||||
|
||||
private async Task CoreStartPreService(ProfileItem node)
|
||||
{
|
||||
if (_process != null && !_process.HasExited)
|
||||
if (_processService != null && !_processService.HasExited)
|
||||
{
|
||||
var coreType = AppHandler.Instance.GetCoreType(node, node.ConfigType);
|
||||
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
|
||||
var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType);
|
||||
if (itemSocks != null)
|
||||
{
|
||||
@@ -208,33 +192,33 @@ public class CoreHandler
|
||||
var result = await CoreConfigHandler.GenerateClientConfig(itemSocks, fileName);
|
||||
if (result.Success)
|
||||
{
|
||||
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(preCoreType);
|
||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
|
||||
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true);
|
||||
if (proc is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_processPre = proc;
|
||||
_processPreService = proc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFunc(bool notify, string msg)
|
||||
private async Task UpdateFunc(bool notify, string msg)
|
||||
{
|
||||
_updateFunc?.Invoke(notify, msg);
|
||||
await _updateFunc?.Invoke(notify, msg);
|
||||
}
|
||||
|
||||
#endregion Private
|
||||
|
||||
#region Process
|
||||
|
||||
private async Task<Process?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
|
||||
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
|
||||
{
|
||||
var fileName = CoreInfoHandler.Instance.GetCoreExecFile(coreInfo, out var msg);
|
||||
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
|
||||
if (fileName.IsNullOrEmpty())
|
||||
{
|
||||
UpdateFunc(false, msg);
|
||||
await UpdateFunc(false, msg);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -246,8 +230,8 @@ public class CoreHandler
|
||||
&& Utils.IsNonWindows())
|
||||
{
|
||||
_linuxSudo = true;
|
||||
await CoreAdminHandler.Instance.Init(_config, _updateFunc);
|
||||
return await CoreAdminHandler.Instance.RunProcessAsLinuxSudo(fileName, coreInfo, configPath);
|
||||
await CoreAdminManager.Instance.Init(_config, _updateFunc);
|
||||
return await CoreAdminManager.Instance.RunProcessAsLinuxSudo(fileName, coreInfo, configPath);
|
||||
}
|
||||
|
||||
return await RunProcessNormal(fileName, coreInfo, configPath, displayLog);
|
||||
@@ -255,56 +239,53 @@ public class CoreHandler
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
UpdateFunc(mayNeedSudo, ex.Message);
|
||||
await UpdateFunc(mayNeedSudo, ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Process?> RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog)
|
||||
private async Task<ProcessService?> RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog)
|
||||
{
|
||||
Process proc = new()
|
||||
var environmentVars = new Dictionary<string, string>();
|
||||
foreach (var kv in coreInfo.Environment)
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
|
||||
WorkingDirectory = Utils.GetBinConfigPath(),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = displayLog,
|
||||
RedirectStandardError = displayLog,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = displayLog ? Encoding.UTF8 : null,
|
||||
StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
|
||||
}
|
||||
};
|
||||
|
||||
if (displayLog)
|
||||
{
|
||||
void dataHandler(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (e.Data.IsNotEmpty())
|
||||
{
|
||||
UpdateFunc(false, e.Data + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
proc.OutputDataReceived += dataHandler;
|
||||
proc.ErrorDataReceived += dataHandler;
|
||||
environmentVars[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath);
|
||||
}
|
||||
proc.Start();
|
||||
|
||||
if (displayLog)
|
||||
{
|
||||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
}
|
||||
var procService = new ProcessService(
|
||||
fileName: fileName,
|
||||
arguments: string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
|
||||
workingDirectory: Utils.GetBinConfigPath(),
|
||||
displayLog: displayLog,
|
||||
redirectInput: false,
|
||||
environmentVars: environmentVars,
|
||||
updateFunc: _updateFunc
|
||||
);
|
||||
|
||||
await procService.StartAsync();
|
||||
|
||||
await Task.Delay(100);
|
||||
AppHandler.Instance.AddProcess(proc.Handle);
|
||||
if (proc is null or { HasExited: true })
|
||||
|
||||
if (procService is null or { HasExited: true })
|
||||
{
|
||||
throw new Exception(ResUI.FailedToRunCore);
|
||||
}
|
||||
return proc;
|
||||
AddProcessJob(procService.Handle);
|
||||
|
||||
return procService;
|
||||
}
|
||||
|
||||
private void AddProcessJob(nint processHandle)
|
||||
{
|
||||
if (Utils.IsWindows())
|
||||
{
|
||||
_processJob ??= new();
|
||||
try
|
||||
{
|
||||
_processJob?.AddProcess(processHandle);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Process
|
||||
@@ -1,11 +1,9 @@
|
||||
using ReactiveUI;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
|
||||
public class NoticeHandler
|
||||
public class NoticeManager
|
||||
{
|
||||
private static readonly Lazy<NoticeHandler> _instance = new(() => new());
|
||||
public static NoticeHandler Instance => _instance.Value;
|
||||
private static readonly Lazy<NoticeManager> _instance = new(() => new());
|
||||
public static NoticeManager Instance => _instance.Value;
|
||||
|
||||
public void Enqueue(string? content)
|
||||
{
|
||||
@@ -13,7 +11,7 @@ public class NoticeHandler
|
||||
{
|
||||
return;
|
||||
}
|
||||
MessageBus.Current.SendMessage(content, EMsgCommand.SendSnackMsg.ToString());
|
||||
AppEvents.SendSnackMsgRequested.Publish(content);
|
||||
}
|
||||
|
||||
public void SendMessage(string? content)
|
||||
@@ -22,7 +20,7 @@ public class NoticeHandler
|
||||
{
|
||||
return;
|
||||
}
|
||||
MessageBus.Current.SendMessage(content, EMsgCommand.SendMsgView.ToString());
|
||||
AppEvents.SendMsgViewRequested.Publish(content);
|
||||
}
|
||||
|
||||
public void SendMessageEx(string? content)
|
||||
@@ -1,12 +1,12 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class PacHandler
|
||||
public class PacManager
|
||||
{
|
||||
private static readonly Lazy<PacHandler> _instance = new(() => new PacHandler());
|
||||
public static PacHandler Instance => _instance.Value;
|
||||
private static readonly Lazy<PacManager> _instance = new(() => new PacManager());
|
||||
public static PacManager Instance => _instance.Value;
|
||||
|
||||
private string _configPath;
|
||||
private int _httpPort;
|
||||
@@ -2,17 +2,17 @@ using System.Collections.Concurrent;
|
||||
|
||||
//using System.Reactive.Linq;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class ProfileExHandler
|
||||
public class ProfileExManager
|
||||
{
|
||||
private static readonly Lazy<ProfileExHandler> _instance = new(() => new());
|
||||
private static readonly Lazy<ProfileExManager> _instance = new(() => new());
|
||||
private ConcurrentBag<ProfileExItem> _lstProfileEx = [];
|
||||
private readonly Queue<string> _queIndexIds = new();
|
||||
public static ProfileExHandler Instance => _instance.Value;
|
||||
public static ProfileExManager Instance => _instance.Value;
|
||||
private static readonly string _tag = "ProfileExHandler";
|
||||
|
||||
public ProfileExHandler()
|
||||
public ProfileExManager()
|
||||
{
|
||||
//Init();
|
||||
}
|
||||
286
v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs
Normal file
286
v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs
Normal file
@@ -0,0 +1,286 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class ProfileGroupItemManager
|
||||
{
|
||||
private static readonly Lazy<ProfileGroupItemManager> _instance = new(() => new());
|
||||
private ConcurrentDictionary<string, ProfileGroupItem> _items = new();
|
||||
|
||||
public static ProfileGroupItemManager Instance => _instance.Value;
|
||||
private static readonly string _tag = "ProfileGroupItemManager";
|
||||
|
||||
private ProfileGroupItemManager()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task Init()
|
||||
{
|
||||
await InitData();
|
||||
}
|
||||
|
||||
// Read-only getters: do not create or mark dirty
|
||||
public bool TryGet(string indexId, out ProfileGroupItem? item)
|
||||
{
|
||||
item = null;
|
||||
if (string.IsNullOrWhiteSpace(indexId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _items.TryGetValue(indexId, out item);
|
||||
}
|
||||
|
||||
public ProfileGroupItem? GetOrDefault(string indexId)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(indexId) ? null : (_items.TryGetValue(indexId, out var v) ? v : null);
|
||||
}
|
||||
|
||||
private async Task InitData()
|
||||
{
|
||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem where IndexId not in ( select indexId from ProfileItem )");
|
||||
|
||||
var list = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
|
||||
_items = new ConcurrentDictionary<string, ProfileGroupItem>(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!));
|
||||
}
|
||||
|
||||
private ProfileGroupItem AddProfileGroupItem(string indexId)
|
||||
{
|
||||
var profileGroupItem = new ProfileGroupItem()
|
||||
{
|
||||
IndexId = indexId,
|
||||
ChildItems = string.Empty,
|
||||
MultipleLoad = EMultipleLoad.LeastPing
|
||||
};
|
||||
|
||||
_items[indexId] = profileGroupItem;
|
||||
return profileGroupItem;
|
||||
}
|
||||
|
||||
private ProfileGroupItem GetProfileGroupItem(string indexId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(indexId))
|
||||
{
|
||||
indexId = Utils.GetGuid(false);
|
||||
}
|
||||
|
||||
return _items.GetOrAdd(indexId, AddProfileGroupItem);
|
||||
}
|
||||
|
||||
public async Task ClearAll()
|
||||
{
|
||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem ");
|
||||
_items.Clear();
|
||||
}
|
||||
|
||||
public async Task SaveTo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var lstExists = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
|
||||
var existsMap = lstExists.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!);
|
||||
|
||||
var lstInserts = new List<ProfileGroupItem>();
|
||||
var lstUpdates = new List<ProfileGroupItem>();
|
||||
|
||||
foreach (var item in _items.Values)
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.IndexId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existsMap.ContainsKey(item.IndexId))
|
||||
{
|
||||
lstUpdates.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
lstInserts.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (lstInserts.Count > 0)
|
||||
{
|
||||
await SQLiteHelper.Instance.InsertAllAsync(lstInserts);
|
||||
}
|
||||
|
||||
if (lstUpdates.Count > 0)
|
||||
{
|
||||
await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public ProfileGroupItem GetOrCreateAndMarkDirty(string indexId)
|
||||
{
|
||||
return GetProfileGroupItem(indexId);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await SaveTo();
|
||||
}
|
||||
|
||||
public async Task SaveItemAsync(ProfileGroupItem item)
|
||||
{
|
||||
if (item is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.IndexId))
|
||||
{
|
||||
throw new ArgumentException("IndexId required", nameof(item));
|
||||
}
|
||||
|
||||
_items[item.IndexId] = item;
|
||||
|
||||
try
|
||||
{
|
||||
var lst = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().Where(t => t.IndexId == item.IndexId).ToListAsync();
|
||||
if (lst != null && lst.Count > 0)
|
||||
{
|
||||
await SQLiteHelper.Instance.UpdateAllAsync(new List<ProfileGroupItem> { item });
|
||||
}
|
||||
else
|
||||
{
|
||||
await SQLiteHelper.Instance.InsertAllAsync(new List<ProfileGroupItem> { item });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
#region Helper
|
||||
|
||||
public static bool HasCycle(string? indexId)
|
||||
{
|
||||
return HasCycle(indexId, new HashSet<string>(), new HashSet<string>());
|
||||
}
|
||||
|
||||
public static bool HasCycle(string? indexId, HashSet<string> visited, HashSet<string> stack)
|
||||
{
|
||||
if (indexId.IsNullOrEmpty())
|
||||
return false;
|
||||
|
||||
if (stack.Contains(indexId))
|
||||
return true;
|
||||
|
||||
if (visited.Contains(indexId))
|
||||
return false;
|
||||
|
||||
visited.Add(indexId);
|
||||
stack.Add(indexId);
|
||||
|
||||
try
|
||||
{
|
||||
Instance.TryGet(indexId, out var groupItem);
|
||||
|
||||
if (groupItem == null || groupItem.ChildItems.IsNullOrEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var childIds = Utils.String2List(groupItem.ChildItems)
|
||||
.Where(p => !string.IsNullOrEmpty(p))
|
||||
.ToList();
|
||||
if (childIds == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var child in childIds)
|
||||
{
|
||||
if (HasCycle(child, visited, stack))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stack.Remove(indexId);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<(List<ProfileItem> Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId)
|
||||
{
|
||||
Instance.TryGet(indexId, out var profileGroupItem);
|
||||
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
|
||||
{
|
||||
return (new List<ProfileItem>(), profileGroupItem);
|
||||
}
|
||||
var items = await GetChildProfileItems(profileGroupItem);
|
||||
return (items, profileGroupItem);
|
||||
}
|
||||
|
||||
public static async Task<List<ProfileItem>> GetChildProfileItems(ProfileGroupItem? group)
|
||||
{
|
||||
if (group == null || group.ChildItems.IsNullOrEmpty())
|
||||
{
|
||||
return new();
|
||||
}
|
||||
var childProfiles = (await Task.WhenAll(
|
||||
Utils.String2List(group.ChildItems)
|
||||
.Where(p => !p.IsNullOrEmpty())
|
||||
.Select(AppManager.Instance.GetProfileItem)
|
||||
))
|
||||
.Where(p =>
|
||||
p != null &&
|
||||
p.IsValid() &&
|
||||
p.ConfigType != EConfigType.Custom
|
||||
)
|
||||
.ToList();
|
||||
return childProfiles;
|
||||
}
|
||||
|
||||
public static async Task<HashSet<string>> GetAllChildDomainAddresses(string indexId)
|
||||
{
|
||||
// include grand children
|
||||
var childAddresses = new HashSet<string>();
|
||||
if (!Instance.TryGet(indexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
|
||||
return childAddresses;
|
||||
|
||||
var childIds = Utils.String2List(groupItem.ChildItems);
|
||||
|
||||
foreach (var childId in childIds)
|
||||
{
|
||||
var childNode = await AppManager.Instance.GetProfileItem(childId);
|
||||
if (childNode == null)
|
||||
continue;
|
||||
|
||||
if (!childNode.IsComplex())
|
||||
{
|
||||
childAddresses.Add(childNode.Address);
|
||||
}
|
||||
else if (childNode.ConfigType.IsGroupType())
|
||||
{
|
||||
var subAddresses = await GetAllChildDomainAddresses(childNode.IndexId);
|
||||
foreach (var addr in subAddresses)
|
||||
{
|
||||
childAddresses.Add(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return childAddresses;
|
||||
}
|
||||
|
||||
#endregion Helper
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class StatisticsHandler
|
||||
public class StatisticsManager
|
||||
{
|
||||
private static readonly Lazy<StatisticsHandler> instance = new(() => new());
|
||||
public static StatisticsHandler Instance => instance.Value;
|
||||
private static readonly Lazy<StatisticsManager> instance = new(() => new());
|
||||
public static StatisticsManager Instance => instance.Value;
|
||||
|
||||
private Config _config;
|
||||
private ServerStatItem? _serverStatItem;
|
||||
private List<ServerStatItem> _lstServerStat;
|
||||
private Action<ServerSpeedItem>? _updateFunc;
|
||||
private Func<ServerSpeedItem, Task>? _updateFunc;
|
||||
|
||||
private StatisticsXrayService? _statisticsXray;
|
||||
private StatisticsSingboxService? _statisticsSingbox;
|
||||
private static readonly string _tag = "StatisticsHandler";
|
||||
public List<ServerStatItem> ServerStat => _lstServerStat;
|
||||
|
||||
public async Task Init(Config config, Action<ServerSpeedItem> updateFunc)
|
||||
public async Task Init(Config config, Func<ServerSpeedItem, Task> updateFunc)
|
||||
{
|
||||
_config = config;
|
||||
_updateFunc = updateFunc;
|
||||
@@ -91,15 +91,15 @@ public class StatisticsHandler
|
||||
{
|
||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem where indexId not in ( select indexId from ProfileItem )");
|
||||
|
||||
long ticks = DateTime.Now.Date.Ticks;
|
||||
var ticks = DateTime.Now.Date.Ticks;
|
||||
await SQLiteHelper.Instance.ExecuteAsync($"update ServerStatItem set todayUp = 0,todayDown=0,dateNow={ticks} where dateNow<>{ticks}");
|
||||
|
||||
_lstServerStat = await SQLiteHelper.Instance.TableAsync<ServerStatItem>().ToListAsync();
|
||||
}
|
||||
|
||||
private void UpdateServerStatHandler(ServerSpeedItem server)
|
||||
private async Task UpdateServerStatHandler(ServerSpeedItem server)
|
||||
{
|
||||
_ = UpdateServerStat(server);
|
||||
await UpdateServerStat(server);
|
||||
}
|
||||
|
||||
private async Task UpdateServerStat(ServerSpeedItem server)
|
||||
@@ -123,12 +123,12 @@ public class StatisticsHandler
|
||||
server.TodayDown = _serverStatItem.TodayDown;
|
||||
server.TotalUp = _serverStatItem.TotalUp;
|
||||
server.TotalDown = _serverStatItem.TotalDown;
|
||||
_updateFunc?.Invoke(server);
|
||||
await _updateFunc?.Invoke(server);
|
||||
}
|
||||
|
||||
private async Task GetServerStatItem(string indexId)
|
||||
{
|
||||
long ticks = DateTime.Now.Date.Ticks;
|
||||
var ticks = DateTime.Now.Date.Ticks;
|
||||
if (_serverStatItem != null && _serverStatItem.IndexId != indexId)
|
||||
{
|
||||
_serverStatItem = null;
|
||||
@@ -1,16 +1,21 @@
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class TaskHandler
|
||||
public class TaskManager
|
||||
{
|
||||
private static readonly Lazy<TaskHandler> _instance = new(() => new());
|
||||
public static TaskHandler Instance => _instance.Value;
|
||||
private static readonly Lazy<TaskManager> _instance = new(() => new());
|
||||
public static TaskManager Instance => _instance.Value;
|
||||
private Config _config;
|
||||
private Func<bool, string, Task>? _updateFunc;
|
||||
|
||||
public void RegUpdateTask(Config config, Action<bool, string> updateFunc)
|
||||
public void RegUpdateTask(Config config, Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
Task.Run(() => ScheduledTasks(config, updateFunc));
|
||||
_config = config;
|
||||
_updateFunc = updateFunc;
|
||||
|
||||
Task.Run(ScheduledTasks);
|
||||
}
|
||||
|
||||
private async Task ScheduledTasks(Config config, Action<bool, string> updateFunc)
|
||||
private async Task ScheduledTasks()
|
||||
{
|
||||
Logging.SaveLog("Setup Scheduled Tasks");
|
||||
|
||||
@@ -21,15 +26,15 @@ public class TaskHandler
|
||||
await Task.Delay(1000 * 60);
|
||||
|
||||
//Execute once 1 minute
|
||||
await UpdateTaskRunSubscription(config, updateFunc);
|
||||
await UpdateTaskRunSubscription();
|
||||
|
||||
//Execute once 20 minute
|
||||
if (numOfExecuted % 20 == 0)
|
||||
{
|
||||
//Logging.SaveLog("Execute save config");
|
||||
|
||||
await ConfigHandler.SaveConfig(config);
|
||||
await ProfileExHandler.Instance.SaveTo();
|
||||
await ConfigHandler.SaveConfig(_config);
|
||||
await ProfileExManager.Instance.SaveTo();
|
||||
}
|
||||
|
||||
//Execute once 1 hour
|
||||
@@ -42,17 +47,17 @@ public class TaskHandler
|
||||
FileManager.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
|
||||
|
||||
//Check once 1 hour
|
||||
await UpdateTaskRunGeo(config, numOfExecuted / 60, updateFunc);
|
||||
await UpdateTaskRunGeo(numOfExecuted / 60);
|
||||
}
|
||||
|
||||
numOfExecuted++;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateTaskRunSubscription(Config config, Action<bool, string> updateFunc)
|
||||
private async Task UpdateTaskRunSubscription()
|
||||
{
|
||||
var updateTime = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds();
|
||||
var lstSubs = (await AppHandler.Instance.SubItems())?
|
||||
var lstSubs = (await AppManager.Instance.SubItems())?
|
||||
.Where(t => t.AutoUpdateInterval > 0)
|
||||
.Where(t => updateTime - t.UpdateTime >= t.AutoUpdateInterval * 60)
|
||||
.ToList();
|
||||
@@ -66,30 +71,30 @@ public class TaskHandler
|
||||
|
||||
foreach (var item in lstSubs)
|
||||
{
|
||||
await SubscriptionHandler.UpdateProcess(config, item.Id, true, (bool success, string msg) =>
|
||||
await SubscriptionHandler.UpdateProcess(_config, item.Id, true, async (success, msg) =>
|
||||
{
|
||||
updateFunc?.Invoke(success, msg);
|
||||
await _updateFunc?.Invoke(success, msg);
|
||||
if (success)
|
||||
{
|
||||
Logging.SaveLog($"Update subscription end. {msg}");
|
||||
}
|
||||
});
|
||||
item.UpdateTime = updateTime;
|
||||
await ConfigHandler.AddSubItem(config, item);
|
||||
await ConfigHandler.AddSubItem(_config, item);
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateTaskRunGeo(Config config, int hours, Action<bool, string> updateFunc)
|
||||
private async Task UpdateTaskRunGeo(int hours)
|
||||
{
|
||||
if (config.GuiItem.AutoUpdateInterval > 0 && hours > 0 && hours % config.GuiItem.AutoUpdateInterval == 0)
|
||||
if (_config.GuiItem.AutoUpdateInterval > 0 && hours > 0 && hours % _config.GuiItem.AutoUpdateInterval == 0)
|
||||
{
|
||||
Logging.SaveLog("Execute update geo files");
|
||||
|
||||
var updateHandle = new UpdateService();
|
||||
await updateHandle.UpdateGeoFileAll(config, (bool success, string msg) =>
|
||||
await updateHandle.UpdateGeoFileAll(_config, async (success, msg) =>
|
||||
{
|
||||
updateFunc?.Invoke(false, msg);
|
||||
await _updateFunc?.Invoke(false, msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
using System.Net;
|
||||
using WebDav;
|
||||
|
||||
namespace ServiceLib.Handler;
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public sealed class WebDavHandler
|
||||
public sealed class WebDavManager
|
||||
{
|
||||
private static readonly Lazy<WebDavHandler> _instance = new(() => new());
|
||||
public static WebDavHandler Instance => _instance.Value;
|
||||
private static readonly Lazy<WebDavManager> _instance = new(() => new());
|
||||
public static WebDavManager Instance => _instance.Value;
|
||||
|
||||
private readonly Config? _config;
|
||||
private WebDavClient? _client;
|
||||
@@ -15,9 +15,9 @@ public sealed class WebDavHandler
|
||||
private readonly string _webFileName = "backup.zip";
|
||||
private readonly string _tag = "WebDav--";
|
||||
|
||||
public WebDavHandler()
|
||||
public WebDavManager()
|
||||
{
|
||||
_config = AppHandler.Instance.Config;
|
||||
_config = AppManager.Instance.Config;
|
||||
}
|
||||
|
||||
private async Task<bool> GetClient()
|
||||
@@ -1,10 +1,13 @@
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
|
||||
namespace ServiceLib.Models;
|
||||
|
||||
public class CheckUpdateModel
|
||||
public class CheckUpdateModel : ReactiveObject
|
||||
{
|
||||
public bool? IsSelected { get; set; }
|
||||
public string? CoreType { get; set; }
|
||||
public string? Remarks { get; set; }
|
||||
[Reactive] public string? Remarks { get; set; }
|
||||
public string? FileName { get; set; }
|
||||
public bool? IsFinished { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
|
||||
namespace ServiceLib.Models;
|
||||
|
||||
[Serializable]
|
||||
public class ClashProxyModel
|
||||
public class ClashProxyModel : ReactiveObject
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
@@ -9,9 +12,9 @@ public class ClashProxyModel
|
||||
|
||||
public string? Now { get; set; }
|
||||
|
||||
public int Delay { get; set; }
|
||||
[Reactive] public int Delay { get; set; }
|
||||
|
||||
public string? DelayName { get; set; }
|
||||
[Reactive] public string? DelayName { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
@@ -260,11 +260,10 @@ public class SimpleDNSItem
|
||||
public bool? UseSystemHosts { get; set; }
|
||||
public bool? AddCommonHosts { get; set; }
|
||||
public bool? FakeIP { get; set; }
|
||||
public bool? GlobalFakeIp { get; set; }
|
||||
public bool? BlockBindingQuery { get; set; }
|
||||
public string? DirectDNS { get; set; }
|
||||
public string? RemoteDNS { get; set; }
|
||||
public string? SingboxOutboundsResolveDNS { get; set; }
|
||||
public string? SingboxFinalResolveDNS { get; set; }
|
||||
public string? RayStrategy4Freedom { get; set; }
|
||||
public string? SingboxStrategy4Direct { get; set; }
|
||||
public string? SingboxStrategy4Proxy { get; set; }
|
||||
|
||||
@@ -17,4 +17,5 @@ public class CoreInfo
|
||||
public string? Match { get; set; }
|
||||
public string? VersionArg { get; set; }
|
||||
public bool AbsolutePath { get; set; }
|
||||
public IDictionary<string, string?> Environment { get; set; } = new Dictionary<string, string?>();
|
||||
}
|
||||
|
||||
14
v2rayN/ServiceLib/Models/ProfileGroupItem.cs
Normal file
14
v2rayN/ServiceLib/Models/ProfileGroupItem.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using SQLite;
|
||||
|
||||
namespace ServiceLib.Models;
|
||||
|
||||
[Serializable]
|
||||
public class ProfileGroupItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string IndexId { get; set; }
|
||||
|
||||
public string ChildItems { get; set; }
|
||||
|
||||
public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing;
|
||||
}
|
||||
@@ -32,18 +32,21 @@ public class ProfileItem : ReactiveObject
|
||||
public string GetSummary()
|
||||
{
|
||||
var summary = $"[{(ConfigType).ToString()}] ";
|
||||
var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.');
|
||||
var addr = arrAddr.Length switch
|
||||
if (IsComplex())
|
||||
{
|
||||
> 2 => $"{arrAddr.First()}***{arrAddr.Last()}",
|
||||
> 1 => $"***{arrAddr.Last()}",
|
||||
_ => Address
|
||||
};
|
||||
summary += ConfigType switch
|
||||
summary += $"[{CoreType.ToString()}]{Remarks}";
|
||||
}
|
||||
else
|
||||
{
|
||||
EConfigType.Custom => $"[{CoreType.ToString()}]{Remarks}",
|
||||
_ => $"{Remarks}({addr}:{Port})"
|
||||
};
|
||||
var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.');
|
||||
var addr = arrAddr.Length switch
|
||||
{
|
||||
> 2 => $"{arrAddr.First()}***{arrAddr.Last()}",
|
||||
> 1 => $"***{arrAddr.Last()}",
|
||||
_ => Address
|
||||
};
|
||||
summary += $"{Remarks}({addr}:{Port})";
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
@@ -61,6 +64,51 @@ public class ProfileItem : ReactiveObject
|
||||
return Network.TrimEx();
|
||||
}
|
||||
|
||||
public bool IsComplex()
|
||||
{
|
||||
return ConfigType.IsComplexType();
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
if (IsComplex())
|
||||
return true;
|
||||
|
||||
if (Address.IsNullOrEmpty() || Port is <= 0 or >= 65536)
|
||||
return false;
|
||||
|
||||
switch (ConfigType)
|
||||
{
|
||||
case EConfigType.VMess:
|
||||
if (Id.IsNullOrEmpty() || !Utils.IsGuidByParse(Id))
|
||||
return false;
|
||||
break;
|
||||
|
||||
case EConfigType.VLESS:
|
||||
if (Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(Id) && Id.Length > 30))
|
||||
return false;
|
||||
if (!Global.Flows.Contains(Flow))
|
||||
return false;
|
||||
break;
|
||||
|
||||
case EConfigType.Shadowsocks:
|
||||
if (Id.IsNullOrEmpty())
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(Security) || !Global.SsSecuritiesInSingbox.Contains(Security))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((ConfigType is EConfigType.VLESS or EConfigType.Trojan)
|
||||
&& StreamSecurity == Global.StreamSecurityReality
|
||||
&& PublicKey.IsNullOrEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion function
|
||||
|
||||
[PrimaryKey]
|
||||
|
||||
@@ -15,4 +15,5 @@ public class RulesItem
|
||||
public List<string>? Process { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string? Remarks { get; set; }
|
||||
public ERuleType? RuleType { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ public class RulesItemModel : RulesItem
|
||||
public string Ips { get; set; }
|
||||
public string Domains { get; set; }
|
||||
public string Protocols { get; set; }
|
||||
public string RuleTypeName { get; set; }
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ public class Outbound4Sbox : BaseServer4Sbox
|
||||
public string? plugin_opts { get; set; }
|
||||
public List<string>? outbounds { get; set; }
|
||||
public bool? interrupt_exist_connections { get; set; }
|
||||
public int? tolerance { get; set; }
|
||||
}
|
||||
|
||||
public class Endpoints4Sbox : BaseServer4Sbox
|
||||
|
||||
548
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
548
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
@@ -114,6 +114,33 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Core '{0}' does not support network type '{1}'. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string CoreNotSupportNetwork {
|
||||
get {
|
||||
return ResourceManager.GetString("CoreNotSupportNetwork", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Core '{0}' does not support protocol '{1}'. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string CoreNotSupportProtocol {
|
||||
get {
|
||||
return ResourceManager.GetString("CoreNotSupportProtocol", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Core '{0}' does not support protocol '{1}' when using transport '{2}'. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string CoreNotSupportProtocolTransport {
|
||||
get {
|
||||
return ResourceManager.GetString("CoreNotSupportProtocolTransport", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Note that custom configuration relies entirely on your own configuration and does not work with all settings. If you want to use the system proxy, please modify the listening port manually. 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -267,6 +294,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Group '{0}' is empty. Please add at least one node. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string GroupEmpty {
|
||||
get {
|
||||
return ResourceManager.GetString("GroupEmpty", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 {0} Group cannot reference itself or have a circular reference 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string GroupSelfReference {
|
||||
get {
|
||||
return ResourceManager.GetString("GroupSelfReference", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 This is not the correct configuration, please check 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -294,6 +339,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 The {0} property is invalid, please check. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string InvalidProperty {
|
||||
get {
|
||||
return ResourceManager.GetString("InvalidProperty", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Invalid address (URL) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -538,7 +592,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Speed (M/s) 的本地化字符串。
|
||||
/// 查找类似 Speed (MB/s) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string LvTestSpeed {
|
||||
get {
|
||||
@@ -672,6 +726,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Add Child Configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuAddChildServer {
|
||||
get {
|
||||
return ResourceManager.GetString("menuAddChildServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Add a custom configuration Configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -699,6 +762,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Add Policy Group Configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuAddPolicyGroupServer {
|
||||
get {
|
||||
return ResourceManager.GetString("menuAddPolicyGroupServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Add Proxy Chain Configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuAddProxyChainServer {
|
||||
get {
|
||||
return ResourceManager.GetString("menuAddProxyChainServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -951,6 +1032,78 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Generate Policy Group from Multiple Profiles 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuGenGroupMultipleServer {
|
||||
get {
|
||||
return ResourceManager.GetString("menuGenGroupMultipleServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration Fallback by sing-box 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuGenGroupMultipleServerSingBoxFallback {
|
||||
get {
|
||||
return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxFallback", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuGenGroupMultipleServerSingBoxLeastPing {
|
||||
get {
|
||||
return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxLeastPing", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration Fallback by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuGenGroupMultipleServerXrayFallback {
|
||||
get {
|
||||
return ResourceManager.GetString("menuGenGroupMultipleServerXrayFallback", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuGenGroupMultipleServerXrayLeastLoad {
|
||||
get {
|
||||
return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastLoad", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuGenGroupMultipleServerXrayLeastPing {
|
||||
get {
|
||||
return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastPing", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuGenGroupMultipleServerXrayRandom {
|
||||
get {
|
||||
return ResourceManager.GetString("menuGenGroupMultipleServerXrayRandom", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuGenGroupMultipleServerXrayRoundRobin {
|
||||
get {
|
||||
return ResourceManager.GetString("menuGenGroupMultipleServerXrayRoundRobin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Global Hotkey Setting 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1320,6 +1473,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Remove Child Configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuRemoveChildServer {
|
||||
get {
|
||||
return ResourceManager.GetString("menuRemoveChildServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Remove duplicate Configurations 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1473,6 +1635,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Server List 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuServerList {
|
||||
get {
|
||||
return ResourceManager.GetString("menuServerList", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Configurations 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1482,60 +1653,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration to custom configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuSetDefaultMultipleServer {
|
||||
get {
|
||||
return ResourceManager.GetString("menuSetDefaultMultipleServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuSetDefaultMultipleServerSingBoxLeastPing {
|
||||
get {
|
||||
return ResourceManager.GetString("menuSetDefaultMultipleServerSingBoxLeastPing", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuSetDefaultMultipleServerXrayLeastLoad {
|
||||
get {
|
||||
return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastLoad", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuSetDefaultMultipleServerXrayLeastPing {
|
||||
get {
|
||||
return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastPing", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuSetDefaultMultipleServerXrayRandom {
|
||||
get {
|
||||
return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRandom", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuSetDefaultMultipleServerXrayRoundRobin {
|
||||
get {
|
||||
return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRoundRobin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Set as active Configuration (Enter) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1860,6 +1977,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Start parsing and processing subscription content 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string MsgStartParsingSubscription {
|
||||
get {
|
||||
return ResourceManager.GetString("MsgStartParsingSubscription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Started updating {0}... 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1932,6 +2058,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Node alias '{0}' does not exist. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string NodeTagNotExist {
|
||||
get {
|
||||
return ResourceManager.GetString("NodeTagNotExist", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Non-VMess or SS protocol 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1959,6 +2094,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Not support protocol '{0}'. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string NotSupportProtocol {
|
||||
get {
|
||||
return ResourceManager.GetString("NotSupportProtocol", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Scan completed, no valid QR code found 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1986,6 +2130,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Please Add At Least One Configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string PleaseAddAtLeastOneServer {
|
||||
get {
|
||||
return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Please fill Remarks 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2031,6 +2184,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Policy group: 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string PolicyGroupPrefix {
|
||||
get {
|
||||
return ResourceManager.GetString("PolicyGroupPrefix", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Proxy chained: 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string ProxyChainedPrefix {
|
||||
get {
|
||||
return ResourceManager.GetString("ProxyChainedPrefix", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2094,6 +2265,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Routing rule outbound: 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string RoutingRuleOutboundPrefix {
|
||||
get {
|
||||
return ResourceManager.GetString("RoutingRuleOutboundPrefix", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Run as Admin 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2292,15 +2472,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Apply to Proxy Domains Only 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbApplyProxyDomainsOnly {
|
||||
get {
|
||||
return ResourceManager.GetString("TbApplyProxyDomainsOnly", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Auto refresh 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2338,7 +2509,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Prevent domain-based routing rules from failing 的本地化字符串。
|
||||
/// 查找类似 Block ECH and HTTP/3 availability checks when enabled 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbBlockSVCBHTTPSQueriesTips {
|
||||
get {
|
||||
@@ -2373,6 +2544,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Policy Group 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbConfigTypePolicyGroup {
|
||||
get {
|
||||
return ResourceManager.GetString("TbConfigTypePolicyGroup", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Proxy Chain 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbConfigTypeProxyChain {
|
||||
get {
|
||||
return ResourceManager.GetString("TbConfigTypeProxyChain", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Confirm 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2418,6 +2607,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 V2ray Custom DNS 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbCustomDnsRay {
|
||||
get {
|
||||
return ResourceManager.GetString("TbCustomDnsRay", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 sing-box Custom DNS 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbCustomDnsSingbox {
|
||||
get {
|
||||
return ResourceManager.GetString("TbCustomDnsSingbox", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Display GUI 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2517,6 +2724,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Applies globally by default, with built-in FakeIP filtering (sing-box only). 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbFakeIPTips {
|
||||
get {
|
||||
return ResourceManager.GetString("TbFakeIPTips", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fallback 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbFallback {
|
||||
get {
|
||||
return ResourceManager.GetString("TbFallback", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fingerprint 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2634,6 +2859,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Most Stable 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbLeastLoad {
|
||||
get {
|
||||
return ResourceManager.GetString("TbLeastLoad", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Lowest Latency 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbLeastPing {
|
||||
get {
|
||||
return ResourceManager.GetString("TbLeastPing", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Address (IPv4, IPv6) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2688,6 +2931,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Policy Group Type 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbPolicyGroupType {
|
||||
get {
|
||||
return ResourceManager.GetString("TbPolicyGroupType", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Port 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2760,6 +3012,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Random 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbRandom {
|
||||
get {
|
||||
return ResourceManager.GetString("TbRandom", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 v2ray Full Config Template 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2796,6 +3057,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Via proxy — please ensure remote availability 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbRemoteDNSTips {
|
||||
get {
|
||||
return ResourceManager.GetString("TbRemoteDNSTips", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Camouflage domain(host) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2823,6 +3093,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Round Robin 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbRoundRobin {
|
||||
get {
|
||||
return ResourceManager.GetString("TbRoundRobin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2904,6 +3183,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Rule Type 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbRuleType {
|
||||
get {
|
||||
return ResourceManager.GetString("TbRuleType", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 You can set separate rules for Routing and DNS, or select "ALL" to apply to both 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbRuleTypeTips {
|
||||
get {
|
||||
return ResourceManager.GetString("TbRuleTypeTips", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2913,33 +3210,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 The sing-box DoH resolution server can be overwritten 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSBDoHOverride {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSBDoHOverride", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 sing-box DoH Resolver Server 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSBDoHResolverServer {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSBDoHResolverServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fallback DNS Resolution, Suggest IP 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSBFallbackDNSResolve {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSBFallbackDNSResolve", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 sing-box Full Config Template 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2958,24 +3228,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Resolve Outbound Domains 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSBOutboundDomainResolve {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSBOutboundDomainResolve", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Outbound DNS Resolution (sing-box) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSBOutboundsResolverDNS {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSBOutboundsResolverDNS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 sing-box Remote Resolution Strategy 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3021,6 +3273,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Select Profile 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSelectProfile {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSelectProfile", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Set system proxy 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3102,24 +3363,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 V2ray DNS settings 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsCoreDns {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsCoreDns", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 sing-box DNS settings 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsCoreDnsSingbox {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsCoreDnsSingbox", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Core: KCP settings 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3399,6 +3642,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 If the system does not have a tray function, please do not enable it 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsHide2TrayWhenCloseTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsHide2TrayWhenCloseTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Hysteria Max bandwidth (Up/Down) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3732,6 +3984,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Auto Route 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsTunAutoRoute {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsTunAutoRoute", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Tun Mode settings 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3741,6 +4002,33 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 MTU 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsTunMtu {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsTunMtu", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Stack 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsTunStack {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsTunStack", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Strict Route 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsTunStrictRoute {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsTunStrictRoute", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Enable UDP 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3960,9 +4248,9 @@ namespace ServiceLib.Resx {
|
||||
/// <summary>
|
||||
/// 查找类似 xray Freedom Resolution Strategy 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbXrayFreedomResolveStrategy {
|
||||
public static string TbXrayFreedomStrategy {
|
||||
get {
|
||||
return ResourceManager.GetString("TbXrayFreedomResolveStrategy", resourceCulture);
|
||||
return ResourceManager.GetString("TbXrayFreedomStrategy", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
@@ -26,36 +26,36 @@
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -675,8 +675,8 @@
|
||||
<data name="TbSettingsCore" xml:space="preserve">
|
||||
<value>هسته: تنظیمات اولیه</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDns" xml:space="preserve">
|
||||
<value>تنظیمات V2ray DNS</value>
|
||||
<data name="TbCustomDnsRay" xml:space="preserve">
|
||||
<value>V2ray Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreKcp" xml:space="preserve">
|
||||
<value>هسته: تنظیمات KCP</value>
|
||||
@@ -892,7 +892,7 @@
|
||||
<value>تاخیر (میلیثانیه)</value>
|
||||
</data>
|
||||
<data name="LvTestSpeed" xml:space="preserve">
|
||||
<value>سرعت (M/s)</value>
|
||||
<value>سرعت (MB/s)</value>
|
||||
</data>
|
||||
<data name="FailedToRunCore" xml:space="preserve">
|
||||
<value>Core اجرا نشد، لطفاً گزارش را ببینید</value>
|
||||
@@ -1011,8 +1011,8 @@
|
||||
<data name="menuDNSSetting" xml:space="preserve">
|
||||
<value>تنظیمات DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
|
||||
<value>تنظیمات DNS sing-box</value>
|
||||
<data name="TbCustomDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
||||
<value>لطفا ساختار DNS را پر کنید، برای مشاهده سند کلیک کنید</value>
|
||||
@@ -1059,6 +1059,18 @@
|
||||
<data name="LvPrevProfileTip" xml:space="preserve">
|
||||
<value>لطفاً مطمئن شوید که ملاحظات وجود دارند و منحصر به فرد هستند</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
||||
<value>مسیریابی خودکار</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
|
||||
<value>مسیریابی سختگیرانه</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>پشته شبکه</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableExInbound" xml:space="preserve">
|
||||
<value>فعال سازی additional Inbound</value>
|
||||
</data>
|
||||
@@ -1365,22 +1377,22 @@
|
||||
<data name="TbPorts7Tips" xml:space="preserve">
|
||||
<value>مخفی و پورت می شود، با کاما (،) جدا می شود</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
|
||||
<value>چند سرور به پیکربندی سفارشی</value>
|
||||
<data name="menuGenGroupMultipleServer" xml:space="preserve">
|
||||
<value>Generate Policy Group from Multiple Profiles</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
|
||||
<value>چند سرور تصادفی توسط Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<value>چند سرور RoundRobin توسط Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<value>چند سرور LeastPing توسط Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<value>چند سرور LeastLoad توسط Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<value>LeastPing چند سرور توسط sing-box</value>
|
||||
</data>
|
||||
<data name="menuExportConfig" xml:space="preserve">
|
||||
@@ -1407,19 +1419,10 @@
|
||||
<data name="TbDomesticDNS" xml:space="preserve">
|
||||
<value>Domestic DNS</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
|
||||
<value>Outbound DNS Resolution (sing-box)</value>
|
||||
<data name="TbRemoteDNSTips" xml:space="preserve">
|
||||
<value>Via proxy — please ensure remote availability</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
|
||||
<value>Resolve Outbound Domains</value>
|
||||
</data>
|
||||
<data name="TbSBDoHResolverServer" xml:space="preserve">
|
||||
<value>sing-box DoH Resolver Server</value>
|
||||
</data>
|
||||
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
|
||||
<value>Fallback DNS Resolution, Suggest IP</value>
|
||||
</data>
|
||||
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
|
||||
<data name="TbXrayFreedomStrategy" xml:space="preserve">
|
||||
<value>xray Freedom Resolution Strategy</value>
|
||||
</data>
|
||||
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
|
||||
@@ -1431,9 +1434,6 @@
|
||||
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
||||
<value>Add Common DNS Hosts</value>
|
||||
</data>
|
||||
<data name="TbSBDoHOverride" xml:space="preserve">
|
||||
<value>The sing-box DoH resolution server can be overwritten</value>
|
||||
</data>
|
||||
<data name="TbFakeIP" xml:space="preserve">
|
||||
<value>FakeIP</value>
|
||||
</data>
|
||||
@@ -1443,9 +1443,6 @@
|
||||
<data name="TbDNSHostsConfig" xml:space="preserve">
|
||||
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
|
||||
</data>
|
||||
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
|
||||
<value>Apply to Proxy Domains Only</value>
|
||||
</data>
|
||||
<data name="ThBasicDNSSettings" xml:space="preserve">
|
||||
<value>Basic DNS Settings</value>
|
||||
</data>
|
||||
@@ -1465,7 +1462,7 @@
|
||||
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Prevent domain-based routing rules from failing</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Please fill in the correct config template</value>
|
||||
@@ -1497,4 +1494,103 @@
|
||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
|
||||
</data>
|
||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Start parsing and processing subscription content</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Please Add At Least One Configuration</value>
|
||||
</data>
|
||||
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
||||
<value>Policy Group</value>
|
||||
</data>
|
||||
<data name="TbConfigTypeProxyChain" xml:space="preserve">
|
||||
<value>Proxy Chain</value>
|
||||
</data>
|
||||
<data name="TbLeastPing" xml:space="preserve">
|
||||
<value>Lowest Latency</value>
|
||||
</data>
|
||||
<data name="TbRandom" xml:space="preserve">
|
||||
<value>Random</value>
|
||||
</data>
|
||||
<data name="TbRoundRobin" xml:space="preserve">
|
||||
<value>Round Robin</value>
|
||||
</data>
|
||||
<data name="TbLeastLoad" xml:space="preserve">
|
||||
<value>Most Stable</value>
|
||||
</data>
|
||||
<data name="TbPolicyGroupType" xml:space="preserve">
|
||||
<value>Policy Group Type</value>
|
||||
</data>
|
||||
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
||||
<value>Add Policy Group Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddProxyChainServer" xml:space="preserve">
|
||||
<value>Add Proxy Chain Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddChildServer" xml:space="preserve">
|
||||
<value>Add Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuRemoveChildServer" xml:space="preserve">
|
||||
<value>Remove Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuServerList" xml:space="preserve">
|
||||
<value>Server List</value>
|
||||
</data>
|
||||
<data name="TbFallback" xml:space="preserve">
|
||||
<value>Fallback</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by sing-box</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by Xray</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportNetwork" xml:space="preserve">
|
||||
<value>Core '{0}' does not support network type '{1}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocol" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}'.</value>
|
||||
</data>
|
||||
<data name="ProxyChainedPrefix" xml:space="preserve">
|
||||
<value>Proxy chained: </value>
|
||||
</data>
|
||||
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
|
||||
<value>Routing rule outbound: </value>
|
||||
</data>
|
||||
<data name="PolicyGroupPrefix" xml:space="preserve">
|
||||
<value>Policy group: </value>
|
||||
</data>
|
||||
<data name="NodeTagNotExist" xml:space="preserve">
|
||||
<value>Node alias '{0}' does not exist.</value>
|
||||
</data>
|
||||
<data name="GroupEmpty" xml:space="preserve">
|
||||
<value>Group '{0}' is empty. Please add at least one node.</value>
|
||||
</data>
|
||||
<data name="InvalidProperty" xml:space="preserve">
|
||||
<value>The {0} property is invalid, please check.</value>
|
||||
</data>
|
||||
<data name="GroupSelfReference" xml:space="preserve">
|
||||
<value>{0} Group cannot reference itself or have a circular reference</value>
|
||||
</data>
|
||||
<data name="NotSupportProtocol" xml:space="preserve">
|
||||
<value>Not support protocol '{0}'.</value>
|
||||
</data>
|
||||
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
|
||||
<value>If the system does not have a tray function, please do not enable it</value>
|
||||
</data>
|
||||
<data name="TbRuleType" xml:space="preserve">
|
||||
<value>Rule Type</value>
|
||||
</data>
|
||||
<data name="TbRuleTypeTips" xml:space="preserve">
|
||||
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
@@ -26,36 +26,36 @@
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -675,8 +675,8 @@
|
||||
<data name="TbSettingsCore" xml:space="preserve">
|
||||
<value>Core: alapbeállítások</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDns" xml:space="preserve">
|
||||
<value>V2ray DNS beállítások</value>
|
||||
<data name="TbCustomDnsRay" xml:space="preserve">
|
||||
<value>V2ray Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreKcp" xml:space="preserve">
|
||||
<value>Core: KCP beállítások</value>
|
||||
@@ -892,7 +892,7 @@
|
||||
<value>Késleltetés (ms)</value>
|
||||
</data>
|
||||
<data name="LvTestSpeed" xml:space="preserve">
|
||||
<value>Sebesség (M/s)</value>
|
||||
<value>Sebesség (MB/s)</value>
|
||||
</data>
|
||||
<data name="FailedToRunCore" xml:space="preserve">
|
||||
<value>Nem sikerült futtatni a Core-t, kérjük, ellenőrizze a prompt információt</value>
|
||||
@@ -1011,8 +1011,8 @@
|
||||
<data name="menuDNSSetting" xml:space="preserve">
|
||||
<value>DNS beállítások</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box DNS beállítások</value>
|
||||
<data name="TbCustomDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
||||
<value>Kérjük, töltse ki a DNS struktúrát, kattintson a dokumentum megtekintéséhez</value>
|
||||
@@ -1059,6 +1059,18 @@
|
||||
<data name="LvPrevProfileTip" xml:space="preserve">
|
||||
<value>Kérjük, győződjön meg arról, hogy a konfigurációs megjegyzések léteznek és egyediek</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
||||
<value>Automatikus útválasztás</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
|
||||
<value>Szigorú útválasztás</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>Hálózati verem</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableExInbound" xml:space="preserve">
|
||||
<value>További bejövő engedélyezése</value>
|
||||
</data>
|
||||
@@ -1365,22 +1377,22 @@
|
||||
<data name="TbPorts7Tips" xml:space="preserve">
|
||||
<value>A portot lefedi, vesszővel (,) elválasztva</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
|
||||
<value>Több konfiguráció egyéni konfigurációra</value>
|
||||
<data name="menuGenGroupMultipleServer" xml:space="preserve">
|
||||
<value>Generate Policy Group from Multiple Profiles</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
|
||||
<value>Több konfiguráció véletlenszerűen Xray szerint</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<value>Több konfiguráció RoundRobin Xray szerint</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<value>Több konfiguráció legkisebb pinggel Xray szerint</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<value>Több konfiguráció legkisebb terheléssel Xray szerint</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<value>Több konfiguráció legkisebb pinggel sing-box szerint</value>
|
||||
</data>
|
||||
<data name="menuExportConfig" xml:space="preserve">
|
||||
@@ -1407,19 +1419,10 @@
|
||||
<data name="TbDomesticDNS" xml:space="preserve">
|
||||
<value>Domestic DNS</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
|
||||
<value>Outbound DNS Resolution (sing-box)</value>
|
||||
<data name="TbRemoteDNSTips" xml:space="preserve">
|
||||
<value>Via proxy — please ensure remote availability</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
|
||||
<value>Resolve Outbound Domains</value>
|
||||
</data>
|
||||
<data name="TbSBDoHResolverServer" xml:space="preserve">
|
||||
<value>sing-box DoH Resolver Server</value>
|
||||
</data>
|
||||
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
|
||||
<value>Fallback DNS Resolution, Suggest IP</value>
|
||||
</data>
|
||||
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
|
||||
<data name="TbXrayFreedomStrategy" xml:space="preserve">
|
||||
<value>xray Freedom Resolution Strategy</value>
|
||||
</data>
|
||||
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
|
||||
@@ -1431,9 +1434,6 @@
|
||||
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
||||
<value>Add Common DNS Hosts</value>
|
||||
</data>
|
||||
<data name="TbSBDoHOverride" xml:space="preserve">
|
||||
<value>The sing-box DoH resolution server can be overwritten</value>
|
||||
</data>
|
||||
<data name="TbFakeIP" xml:space="preserve">
|
||||
<value>FakeIP</value>
|
||||
</data>
|
||||
@@ -1443,9 +1443,6 @@
|
||||
<data name="TbDNSHostsConfig" xml:space="preserve">
|
||||
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
|
||||
</data>
|
||||
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
|
||||
<value>Apply to Proxy Domains Only</value>
|
||||
</data>
|
||||
<data name="ThBasicDNSSettings" xml:space="preserve">
|
||||
<value>Basic DNS Settings</value>
|
||||
</data>
|
||||
@@ -1465,7 +1462,7 @@
|
||||
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Prevent domain-based routing rules from failing</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Please fill in the correct config template</value>
|
||||
@@ -1497,4 +1494,103 @@
|
||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
|
||||
</data>
|
||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Start parsing and processing subscription content</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Please Add At Least One Configuration</value>
|
||||
</data>
|
||||
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
||||
<value>Policy Group</value>
|
||||
</data>
|
||||
<data name="TbConfigTypeProxyChain" xml:space="preserve">
|
||||
<value>Proxy Chain</value>
|
||||
</data>
|
||||
<data name="TbLeastPing" xml:space="preserve">
|
||||
<value>Lowest Latency</value>
|
||||
</data>
|
||||
<data name="TbRandom" xml:space="preserve">
|
||||
<value>Random</value>
|
||||
</data>
|
||||
<data name="TbRoundRobin" xml:space="preserve">
|
||||
<value>Round Robin</value>
|
||||
</data>
|
||||
<data name="TbLeastLoad" xml:space="preserve">
|
||||
<value>Most Stable</value>
|
||||
</data>
|
||||
<data name="TbPolicyGroupType" xml:space="preserve">
|
||||
<value>Policy Group Type</value>
|
||||
</data>
|
||||
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
||||
<value>Add Policy Group Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddProxyChainServer" xml:space="preserve">
|
||||
<value>Add Proxy Chain Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddChildServer" xml:space="preserve">
|
||||
<value>Add Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuRemoveChildServer" xml:space="preserve">
|
||||
<value>Remove Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuServerList" xml:space="preserve">
|
||||
<value>Server List</value>
|
||||
</data>
|
||||
<data name="TbFallback" xml:space="preserve">
|
||||
<value>Fallback</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by sing-box</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by Xray</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportNetwork" xml:space="preserve">
|
||||
<value>Core '{0}' does not support network type '{1}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocol" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}'.</value>
|
||||
</data>
|
||||
<data name="ProxyChainedPrefix" xml:space="preserve">
|
||||
<value>Proxy chained: </value>
|
||||
</data>
|
||||
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
|
||||
<value>Routing rule outbound: </value>
|
||||
</data>
|
||||
<data name="PolicyGroupPrefix" xml:space="preserve">
|
||||
<value>Policy group: </value>
|
||||
</data>
|
||||
<data name="NodeTagNotExist" xml:space="preserve">
|
||||
<value>Node alias '{0}' does not exist.</value>
|
||||
</data>
|
||||
<data name="GroupEmpty" xml:space="preserve">
|
||||
<value>Group '{0}' is empty. Please add at least one node.</value>
|
||||
</data>
|
||||
<data name="InvalidProperty" xml:space="preserve">
|
||||
<value>The {0} property is invalid, please check.</value>
|
||||
</data>
|
||||
<data name="GroupSelfReference" xml:space="preserve">
|
||||
<value>{0} Group cannot reference itself or have a circular reference</value>
|
||||
</data>
|
||||
<data name="NotSupportProtocol" xml:space="preserve">
|
||||
<value>Not support protocol '{0}'.</value>
|
||||
</data>
|
||||
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
|
||||
<value>If the system does not have a tray function, please do not enable it</value>
|
||||
</data>
|
||||
<data name="TbRuleTypeTips" xml:space="preserve">
|
||||
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
|
||||
</data>
|
||||
<data name="TbRuleType" xml:space="preserve">
|
||||
<value>Rule Type</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
@@ -26,36 +26,36 @@
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -675,8 +675,8 @@
|
||||
<data name="TbSettingsCore" xml:space="preserve">
|
||||
<value>Core: basic settings</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDns" xml:space="preserve">
|
||||
<value>V2ray DNS settings</value>
|
||||
<data name="TbCustomDnsRay" xml:space="preserve">
|
||||
<value>V2ray Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreKcp" xml:space="preserve">
|
||||
<value>Core: KCP settings</value>
|
||||
@@ -892,7 +892,7 @@
|
||||
<value>Delay (ms)</value>
|
||||
</data>
|
||||
<data name="LvTestSpeed" xml:space="preserve">
|
||||
<value>Speed (M/s)</value>
|
||||
<value>Speed (MB/s)</value>
|
||||
</data>
|
||||
<data name="FailedToRunCore" xml:space="preserve">
|
||||
<value>Failed to run Core, please check the prompt information</value>
|
||||
@@ -1011,8 +1011,8 @@
|
||||
<data name="menuDNSSetting" xml:space="preserve">
|
||||
<value>DNS Settings</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box DNS settings</value>
|
||||
<data name="TbCustomDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
||||
<value>Please fill in DNS Structure, Click to view the document</value>
|
||||
@@ -1059,6 +1059,18 @@
|
||||
<data name="LvPrevProfileTip" xml:space="preserve">
|
||||
<value>Please make sure the Configuration remarks exist and are unique</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
||||
<value>Auto Route</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
|
||||
<value>Strict Route</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>Stack</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableExInbound" xml:space="preserve">
|
||||
<value>Enable additional Inbound</value>
|
||||
</data>
|
||||
@@ -1365,22 +1377,22 @@
|
||||
<data name="TbPorts7Tips" xml:space="preserve">
|
||||
<value>Will cover the port, separate with commas (,)</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
|
||||
<value>Multi-Configuration to custom configuration</value>
|
||||
<data name="menuGenGroupMultipleServer" xml:space="preserve">
|
||||
<value>Generate Policy Group from Multiple Profiles</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
|
||||
<value>Multi-Configuration Random by Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<value>Multi-Configuration RoundRobin by Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<value>Multi-Configuration LeastPing by Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<value>Multi-Configuration LeastLoad by Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<value>Multi-Configuration LeastPing by sing-box</value>
|
||||
</data>
|
||||
<data name="menuExportConfig" xml:space="preserve">
|
||||
@@ -1407,19 +1419,10 @@
|
||||
<data name="TbDomesticDNS" xml:space="preserve">
|
||||
<value>Domestic DNS</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
|
||||
<value>Outbound DNS Resolution (sing-box)</value>
|
||||
<data name="TbRemoteDNSTips" xml:space="preserve">
|
||||
<value>Via proxy — please ensure remote availability</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
|
||||
<value>Resolve Outbound Domains</value>
|
||||
</data>
|
||||
<data name="TbSBDoHResolverServer" xml:space="preserve">
|
||||
<value>sing-box DoH Resolver Server</value>
|
||||
</data>
|
||||
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
|
||||
<value>Fallback DNS Resolution, Suggest IP</value>
|
||||
</data>
|
||||
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
|
||||
<data name="TbXrayFreedomStrategy" xml:space="preserve">
|
||||
<value>xray Freedom Resolution Strategy</value>
|
||||
</data>
|
||||
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
|
||||
@@ -1431,9 +1434,6 @@
|
||||
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
||||
<value>Add Common DNS Hosts</value>
|
||||
</data>
|
||||
<data name="TbSBDoHOverride" xml:space="preserve">
|
||||
<value>The sing-box DoH resolution server can be overwritten</value>
|
||||
</data>
|
||||
<data name="TbFakeIP" xml:space="preserve">
|
||||
<value>FakeIP</value>
|
||||
</data>
|
||||
@@ -1443,9 +1443,6 @@
|
||||
<data name="TbDNSHostsConfig" xml:space="preserve">
|
||||
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
|
||||
</data>
|
||||
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
|
||||
<value>Apply to Proxy Domains Only</value>
|
||||
</data>
|
||||
<data name="ThBasicDNSSettings" xml:space="preserve">
|
||||
<value>Basic DNS Settings</value>
|
||||
</data>
|
||||
@@ -1465,7 +1462,7 @@
|
||||
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Prevent domain-based routing rules from failing</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Please fill in the correct config template</value>
|
||||
@@ -1497,4 +1494,103 @@
|
||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
|
||||
</data>
|
||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Start parsing and processing subscription content</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Please Add At Least One Configuration</value>
|
||||
</data>
|
||||
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
||||
<value>Policy Group</value>
|
||||
</data>
|
||||
<data name="TbConfigTypeProxyChain" xml:space="preserve">
|
||||
<value>Proxy Chain</value>
|
||||
</data>
|
||||
<data name="TbLeastPing" xml:space="preserve">
|
||||
<value>Lowest Latency</value>
|
||||
</data>
|
||||
<data name="TbRandom" xml:space="preserve">
|
||||
<value>Random</value>
|
||||
</data>
|
||||
<data name="TbRoundRobin" xml:space="preserve">
|
||||
<value>Round Robin</value>
|
||||
</data>
|
||||
<data name="TbLeastLoad" xml:space="preserve">
|
||||
<value>Most Stable</value>
|
||||
</data>
|
||||
<data name="TbPolicyGroupType" xml:space="preserve">
|
||||
<value>Policy Group Type</value>
|
||||
</data>
|
||||
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
||||
<value>Add Policy Group Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddProxyChainServer" xml:space="preserve">
|
||||
<value>Add Proxy Chain Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddChildServer" xml:space="preserve">
|
||||
<value>Add Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuRemoveChildServer" xml:space="preserve">
|
||||
<value>Remove Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuServerList" xml:space="preserve">
|
||||
<value>Server List</value>
|
||||
</data>
|
||||
<data name="TbFallback" xml:space="preserve">
|
||||
<value>Fallback</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by sing-box</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by Xray</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportNetwork" xml:space="preserve">
|
||||
<value>Core '{0}' does not support network type '{1}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocol" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}'.</value>
|
||||
</data>
|
||||
<data name="ProxyChainedPrefix" xml:space="preserve">
|
||||
<value>Proxy chained: </value>
|
||||
</data>
|
||||
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
|
||||
<value>Routing rule outbound: </value>
|
||||
</data>
|
||||
<data name="PolicyGroupPrefix" xml:space="preserve">
|
||||
<value>Policy group: </value>
|
||||
</data>
|
||||
<data name="NodeTagNotExist" xml:space="preserve">
|
||||
<value>Node alias '{0}' does not exist.</value>
|
||||
</data>
|
||||
<data name="GroupEmpty" xml:space="preserve">
|
||||
<value>Group '{0}' is empty. Please add at least one node.</value>
|
||||
</data>
|
||||
<data name="InvalidProperty" xml:space="preserve">
|
||||
<value>The {0} property is invalid, please check.</value>
|
||||
</data>
|
||||
<data name="GroupSelfReference" xml:space="preserve">
|
||||
<value>{0} Group cannot reference itself or have a circular reference</value>
|
||||
</data>
|
||||
<data name="NotSupportProtocol" xml:space="preserve">
|
||||
<value>Not support protocol '{0}'.</value>
|
||||
</data>
|
||||
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
|
||||
<value>If the system does not have a tray function, please do not enable it</value>
|
||||
</data>
|
||||
<data name="TbRuleTypeTips" xml:space="preserve">
|
||||
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
|
||||
</data>
|
||||
<data name="TbRuleType" xml:space="preserve">
|
||||
<value>Rule Type</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
@@ -26,36 +26,36 @@
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -675,8 +675,8 @@
|
||||
<data name="TbSettingsCore" xml:space="preserve">
|
||||
<value>Ядро: базовые настройки</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDns" xml:space="preserve">
|
||||
<value>Настройки DNS V2ray</value>
|
||||
<data name="TbCustomDnsRay" xml:space="preserve">
|
||||
<value>V2ray Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreKcp" xml:space="preserve">
|
||||
<value>Ядро: настройки KCP</value>
|
||||
@@ -1011,8 +1011,8 @@
|
||||
<data name="menuDNSSetting" xml:space="preserve">
|
||||
<value>Настройки DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
|
||||
<value>Настройки DNS sing-box</value>
|
||||
<data name="TbCustomDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
||||
<value>Заполните структуру DNS, нажмите, чтобы открыть документ</value>
|
||||
@@ -1059,6 +1059,18 @@
|
||||
<data name="LvPrevProfileTip" xml:space="preserve">
|
||||
<value>Убедитесь, что примечание существует и является уникальным</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
||||
<value>Автоматическая маршрутизация</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
|
||||
<value>Строгая маршрутизация</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>Сетевой стек</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableExInbound" xml:space="preserve">
|
||||
<value>Включить дополнительный входящий канал</value>
|
||||
</data>
|
||||
@@ -1365,22 +1377,22 @@
|
||||
<data name="TbPorts7Tips" xml:space="preserve">
|
||||
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
|
||||
<value>От мультиконфигурации к пользовательской конфигурации</value>
|
||||
<data name="menuGenGroupMultipleServer" xml:space="preserve">
|
||||
<value>Generate Policy Group from Multiple Profiles</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
|
||||
<value>Случайный (Xray)</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<value>Круговой (Xray)</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<value>Минимальное RTT (минимальное время туда-обратно) (Xray)</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<value>Минимальная нагрузка (Xray)</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<value>Минимальное RTT (минимальное время туда-обратно) (sing-box)</value>
|
||||
</data>
|
||||
<data name="menuExportConfig" xml:space="preserve">
|
||||
@@ -1407,19 +1419,10 @@
|
||||
<data name="TbDomesticDNS" xml:space="preserve">
|
||||
<value>Внутренний DNS</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
|
||||
<value>Резолвер DNS для исходящих (sing-box)</value>
|
||||
<data name="TbRemoteDNSTips" xml:space="preserve">
|
||||
<value>Via proxy — please ensure remote availability</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
|
||||
<value>Разрешать домены для исходящих соединений</value>
|
||||
</data>
|
||||
<data name="TbSBDoHResolverServer" xml:space="preserve">
|
||||
<value>Сервер DoH-резолвера (sing-box)</value>
|
||||
</data>
|
||||
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
|
||||
<value>Резервное DNS-разрешение (рекомендуется указывать IP)</value>
|
||||
</data>
|
||||
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
|
||||
<data name="TbXrayFreedomStrategy" xml:space="preserve">
|
||||
<value>Стратегия резолвинга Freedom (Xray)</value>
|
||||
</data>
|
||||
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
|
||||
@@ -1431,9 +1434,6 @@
|
||||
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
||||
<value>Добавить стандартные записи hosts (DNS)</value>
|
||||
</data>
|
||||
<data name="TbSBDoHOverride" xml:space="preserve">
|
||||
<value>Сервер DoH-резолвера sing-box можно переопределить</value>
|
||||
</data>
|
||||
<data name="TbFakeIP" xml:space="preserve">
|
||||
<value>FakeIP</value>
|
||||
</data>
|
||||
@@ -1443,9 +1443,6 @@
|
||||
<data name="TbDNSHostsConfig" xml:space="preserve">
|
||||
<value>DNS hosts: (каждая строка в формате "domain1 ip1 ip2")</value>
|
||||
</data>
|
||||
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
|
||||
<value>Применять только к доменам через прокси</value>
|
||||
</data>
|
||||
<data name="ThBasicDNSSettings" xml:space="preserve">
|
||||
<value>Базовые настройки DNS</value>
|
||||
</data>
|
||||
@@ -1465,7 +1462,7 @@
|
||||
<value>Включён пользовательский DNS — настройки на этой странице не применяются</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Предотвращает сбои доменных правил маршрутизации</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Пожалуйста, заполните корректный шаблон конфигурации</value>
|
||||
@@ -1497,4 +1494,103 @@
|
||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value>
|
||||
</data>
|
||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Start parsing and processing subscription content</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Please Add At Least One Configuration</value>
|
||||
</data>
|
||||
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
||||
<value>Policy Group</value>
|
||||
</data>
|
||||
<data name="TbConfigTypeProxyChain" xml:space="preserve">
|
||||
<value>Proxy Chain</value>
|
||||
</data>
|
||||
<data name="TbLeastPing" xml:space="preserve">
|
||||
<value>Lowest Latency</value>
|
||||
</data>
|
||||
<data name="TbRandom" xml:space="preserve">
|
||||
<value>Random</value>
|
||||
</data>
|
||||
<data name="TbRoundRobin" xml:space="preserve">
|
||||
<value>Round Robin</value>
|
||||
</data>
|
||||
<data name="TbLeastLoad" xml:space="preserve">
|
||||
<value>Most Stable</value>
|
||||
</data>
|
||||
<data name="TbPolicyGroupType" xml:space="preserve">
|
||||
<value>Policy Group Type</value>
|
||||
</data>
|
||||
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
||||
<value>Add Policy Group Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddProxyChainServer" xml:space="preserve">
|
||||
<value>Add Proxy Chain Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddChildServer" xml:space="preserve">
|
||||
<value>Add Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuRemoveChildServer" xml:space="preserve">
|
||||
<value>Remove Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuServerList" xml:space="preserve">
|
||||
<value>Server List</value>
|
||||
</data>
|
||||
<data name="TbFallback" xml:space="preserve">
|
||||
<value>Fallback</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by sing-box</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by Xray</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportNetwork" xml:space="preserve">
|
||||
<value>Core '{0}' does not support network type '{1}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocol" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}'.</value>
|
||||
</data>
|
||||
<data name="ProxyChainedPrefix" xml:space="preserve">
|
||||
<value>Proxy chained: </value>
|
||||
</data>
|
||||
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
|
||||
<value>Routing rule outbound: </value>
|
||||
</data>
|
||||
<data name="PolicyGroupPrefix" xml:space="preserve">
|
||||
<value>Policy group: </value>
|
||||
</data>
|
||||
<data name="NodeTagNotExist" xml:space="preserve">
|
||||
<value>Node alias '{0}' does not exist.</value>
|
||||
</data>
|
||||
<data name="GroupEmpty" xml:space="preserve">
|
||||
<value>Group '{0}' is empty. Please add at least one node.</value>
|
||||
</data>
|
||||
<data name="InvalidProperty" xml:space="preserve">
|
||||
<value>The {0} property is invalid, please check.</value>
|
||||
</data>
|
||||
<data name="GroupSelfReference" xml:space="preserve">
|
||||
<value>{0} Group cannot reference itself or have a circular reference</value>
|
||||
</data>
|
||||
<data name="NotSupportProtocol" xml:space="preserve">
|
||||
<value>Not support protocol '{0}'.</value>
|
||||
</data>
|
||||
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
|
||||
<value>If the system does not have a tray function, please do not enable it</value>
|
||||
</data>
|
||||
<data name="TbRuleTypeTips" xml:space="preserve">
|
||||
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
|
||||
</data>
|
||||
<data name="TbRuleType" xml:space="preserve">
|
||||
<value>Rule Type</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
@@ -26,36 +26,36 @@
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -675,8 +675,8 @@
|
||||
<data name="TbSettingsCore" xml:space="preserve">
|
||||
<value>Core: 基础设置</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDns" xml:space="preserve">
|
||||
<value>v2ray DNS 设置</value>
|
||||
<data name="TbCustomDnsRay" xml:space="preserve">
|
||||
<value>v2ray 自定义 DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreKcp" xml:space="preserve">
|
||||
<value>Core: KCP 设置</value>
|
||||
@@ -892,7 +892,7 @@
|
||||
<value>延迟 (ms)</value>
|
||||
</data>
|
||||
<data name="LvTestSpeed" xml:space="preserve">
|
||||
<value>速度 (M/s)</value>
|
||||
<value>速度 (MB/s)</value>
|
||||
</data>
|
||||
<data name="FailedToRunCore" xml:space="preserve">
|
||||
<value>运行 Core 失败,请查看提示信息</value>
|
||||
@@ -1008,8 +1008,8 @@
|
||||
<data name="menuDNSSetting" xml:space="preserve">
|
||||
<value>DNS 设置</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box DNS 设置</value>
|
||||
<data name="TbCustomDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box 自定义 DNS</value>
|
||||
</data>
|
||||
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
||||
<value>请填写 DNS JSON 结构,点击查看文档</value>
|
||||
@@ -1056,6 +1056,18 @@
|
||||
<data name="LvPrevProfileTip" xml:space="preserve">
|
||||
<value>请确保配置文件别名存在并唯一</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
||||
<value>自动路由</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
|
||||
<value>严格路由</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>协议栈</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableExInbound" xml:space="preserve">
|
||||
<value>启用额外监听端口</value>
|
||||
</data>
|
||||
@@ -1362,22 +1374,22 @@
|
||||
<data name="TbPorts7Tips" xml:space="preserve">
|
||||
<value>会覆盖端口,多组时用逗号 (,) 隔开</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
|
||||
<value>多配置文件产生自定义配置 (多选)</value>
|
||||
<data name="menuGenGroupMultipleServer" xml:space="preserve">
|
||||
<value>多配置文件生成策略组</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
|
||||
<value>多配置文件随机 Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<value>多配置文件负载均衡 Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<value>多配置文件最低延迟 Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<value>多配置文件最稳定 Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<value>多配置文件最低延迟 sing-box</value>
|
||||
</data>
|
||||
<data name="menuExportConfig" xml:space="preserve">
|
||||
@@ -1404,19 +1416,10 @@
|
||||
<data name="TbDomesticDNS" xml:space="preserve">
|
||||
<value>直连 DNS</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
|
||||
<value>出站 DNS 解析(sing-box)</value>
|
||||
<data name="TbRemoteDNSTips" xml:space="preserve">
|
||||
<value>通过代理,请确保远程可用</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
|
||||
<value>解析出站域名</value>
|
||||
</data>
|
||||
<data name="TbSBDoHResolverServer" xml:space="preserve">
|
||||
<value>sing-box DoH 解析服务器</value>
|
||||
</data>
|
||||
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
|
||||
<value>兜底解析其他 DNS 域名,建议设为 ip</value>
|
||||
</data>
|
||||
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
|
||||
<data name="TbXrayFreedomStrategy" xml:space="preserve">
|
||||
<value>xray freedom 解析策略</value>
|
||||
</data>
|
||||
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
|
||||
@@ -1428,9 +1431,6 @@
|
||||
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
||||
<value>添加常用 DNS Hosts</value>
|
||||
</data>
|
||||
<data name="TbSBDoHOverride" xml:space="preserve">
|
||||
<value>开启后可覆盖 sing-box DoH 解析服务器</value>
|
||||
</data>
|
||||
<data name="TbFakeIP" xml:space="preserve">
|
||||
<value>FakeIP</value>
|
||||
</data>
|
||||
@@ -1440,9 +1440,6 @@
|
||||
<data name="TbDNSHostsConfig" xml:space="preserve">
|
||||
<value>DNS Hosts:(“域名1 ip1 ip2” 一行一个)</value>
|
||||
</data>
|
||||
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
|
||||
<value>仅对代理域名生效</value>
|
||||
</data>
|
||||
<data name="ThBasicDNSSettings" xml:space="preserve">
|
||||
<value>DNS 基础设置</value>
|
||||
</data>
|
||||
@@ -1462,7 +1459,7 @@
|
||||
<value>自定义 DNS 已启用,此页面配置将无效</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>避免域名分流规则失效</value>
|
||||
<value>开启后将阻止 ECH 和 HTTP/3 可用性查询</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>请填写正确的配置模板</value>
|
||||
@@ -1494,4 +1491,103 @@
|
||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>此功能供高级用户和有特殊需求的用户使用。 启用此功能后,将忽略 Core 的基础设置,DNS 设置 ,路由设置。你需要保证系统代理的端口和流量统计等功能的配置正确,一切都由你来设置。</value>
|
||||
</data>
|
||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>开始解析和处理订阅内容</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>选择配置文件</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>请至少添加一个配置文件</value>
|
||||
</data>
|
||||
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
||||
<value>策略组</value>
|
||||
</data>
|
||||
<data name="TbConfigTypeProxyChain" xml:space="preserve">
|
||||
<value>链式代理</value>
|
||||
</data>
|
||||
<data name="TbLeastPing" xml:space="preserve">
|
||||
<value>最低延迟</value>
|
||||
</data>
|
||||
<data name="TbRandom" xml:space="preserve">
|
||||
<value>随机</value>
|
||||
</data>
|
||||
<data name="TbRoundRobin" xml:space="preserve">
|
||||
<value>负载均衡</value>
|
||||
</data>
|
||||
<data name="TbLeastLoad" xml:space="preserve">
|
||||
<value>最稳定</value>
|
||||
</data>
|
||||
<data name="TbPolicyGroupType" xml:space="preserve">
|
||||
<value>策略组类型</value>
|
||||
</data>
|
||||
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
||||
<value>添加策略组配置文件</value>
|
||||
</data>
|
||||
<data name="menuAddProxyChainServer" xml:space="preserve">
|
||||
<value>添加链式代理配置文件</value>
|
||||
</data>
|
||||
<data name="menuAddChildServer" xml:space="preserve">
|
||||
<value>添加子配置文件</value>
|
||||
</data>
|
||||
<data name="menuRemoveChildServer" xml:space="preserve">
|
||||
<value>删除子配置文件</value>
|
||||
</data>
|
||||
<data name="menuServerList" xml:space="preserve">
|
||||
<value>服务器列表</value>
|
||||
</data>
|
||||
<data name="TbFallback" xml:space="preserve">
|
||||
<value>故障转移</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
|
||||
<value>多配置文件故障转移 sing-box</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
|
||||
<value>多配置文件故障转移 Xray</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportNetwork" xml:space="preserve">
|
||||
<value>核心 '{0}' 不支持网络类型 '{1}'。</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
|
||||
<value>核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocol" xml:space="preserve">
|
||||
<value>核心 '{0}' 不支持协议 '{1}'。</value>
|
||||
</data>
|
||||
<data name="ProxyChainedPrefix" xml:space="preserve">
|
||||
<value>代理链: </value>
|
||||
</data>
|
||||
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
|
||||
<value>路由规则出站: </value>
|
||||
</data>
|
||||
<data name="PolicyGroupPrefix" xml:space="preserve">
|
||||
<value>策略组: </value>
|
||||
</data>
|
||||
<data name="NodeTagNotExist" xml:space="preserve">
|
||||
<value>节点别名 '{0}' 不存在。</value>
|
||||
</data>
|
||||
<data name="GroupEmpty" xml:space="preserve">
|
||||
<value>组“{0}”为空。请至少添加一个节点。</value>
|
||||
</data>
|
||||
<data name="InvalidProperty" xml:space="preserve">
|
||||
<value>{0}属性无效,请检查</value>
|
||||
</data>
|
||||
<data name="GroupSelfReference" xml:space="preserve">
|
||||
<value>{0} 分组不能引用自身或循环引用</value>
|
||||
</data>
|
||||
<data name="NotSupportProtocol" xml:space="preserve">
|
||||
<value>不支持协议 '{0}'。</value>
|
||||
</data>
|
||||
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
|
||||
<value>如果系统没有托盘功能,请不要开启</value>
|
||||
</data>
|
||||
<data name="TbRuleType" xml:space="preserve">
|
||||
<value>规则类型</value>
|
||||
</data>
|
||||
<data name="TbRuleTypeTips" xml:space="preserve">
|
||||
<value>可对 Routing 和 DNS 单独设定规则,ALL 则都生效</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
@@ -26,36 +26,36 @@
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -675,8 +675,8 @@
|
||||
<data name="TbSettingsCore" xml:space="preserve">
|
||||
<value>Core: 基礎設定</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDns" xml:space="preserve">
|
||||
<value>V2ray DNS 設定</value>
|
||||
<data name="TbCustomDnsRay" xml:space="preserve">
|
||||
<value>V2ray Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreKcp" xml:space="preserve">
|
||||
<value>Core: KCP 設定</value>
|
||||
@@ -892,7 +892,7 @@
|
||||
<value>延遲 (ms)</value>
|
||||
</data>
|
||||
<data name="LvTestSpeed" xml:space="preserve">
|
||||
<value>速度 (M/s)</value>
|
||||
<value>速度 (MB/s)</value>
|
||||
</data>
|
||||
<data name="FailedToRunCore" xml:space="preserve">
|
||||
<value>執行 Core 失敗,請查看提示訊息</value>
|
||||
@@ -1008,8 +1008,8 @@
|
||||
<data name="menuDNSSetting" xml:space="preserve">
|
||||
<value>DNS 設定</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box DNS 設定</value>
|
||||
<data name="TbCustomDnsSingbox" xml:space="preserve">
|
||||
<value>sing-box Custom DNS</value>
|
||||
</data>
|
||||
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
||||
<value>請填寫 DNS JSON 結構,點擊查看檔案</value>
|
||||
@@ -1056,6 +1056,18 @@
|
||||
<data name="LvPrevProfileTip" xml:space="preserve">
|
||||
<value>請確保設定檔別名存在並且唯一</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
||||
<value>自動路由</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
|
||||
<value>嚴格路由</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>協定堆疊</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableExInbound" xml:space="preserve">
|
||||
<value>啟用額外偵聽連接埠</value>
|
||||
</data>
|
||||
@@ -1362,22 +1374,22 @@
|
||||
<data name="TbPorts7Tips" xml:space="preserve">
|
||||
<value>會覆蓋埠,多組時用逗號 (,) 隔開</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
|
||||
<value>多設定檔產生自訂配置 (多選)</value>
|
||||
<data name="menuGenGroupMultipleServer" xml:space="preserve">
|
||||
<value>Generate Policy Group from Multiple Profiles</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
|
||||
<value>多設定檔隨機 Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
|
||||
<value>多設定檔負載平衡 Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
|
||||
<value>多設定檔最低延遲 Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
|
||||
<value>多設定檔最穩定 Xray</value>
|
||||
</data>
|
||||
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
|
||||
<value>多設定檔最低延遲 sing-box</value>
|
||||
</data>
|
||||
<data name="menuExportConfig" xml:space="preserve">
|
||||
@@ -1404,19 +1416,10 @@
|
||||
<data name="TbDomesticDNS" xml:space="preserve">
|
||||
<value>Domestic DNS</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
|
||||
<value>Outbound DNS Resolution (sing-box)</value>
|
||||
<data name="TbRemoteDNSTips" xml:space="preserve">
|
||||
<value>Via proxy — please ensure remote availability</value>
|
||||
</data>
|
||||
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
|
||||
<value>Resolve Outbound Domains</value>
|
||||
</data>
|
||||
<data name="TbSBDoHResolverServer" xml:space="preserve">
|
||||
<value>sing-box DoH Resolver Server</value>
|
||||
</data>
|
||||
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
|
||||
<value>Fallback DNS Resolution, Suggest IP</value>
|
||||
</data>
|
||||
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
|
||||
<data name="TbXrayFreedomStrategy" xml:space="preserve">
|
||||
<value>xray Freedom Resolution Strategy</value>
|
||||
</data>
|
||||
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
|
||||
@@ -1428,9 +1431,6 @@
|
||||
<data name="TbAddCommonDNSHosts" xml:space="preserve">
|
||||
<value>Add Common DNS Hosts</value>
|
||||
</data>
|
||||
<data name="TbSBDoHOverride" xml:space="preserve">
|
||||
<value>The sing-box DoH resolution server can be overwritten</value>
|
||||
</data>
|
||||
<data name="TbFakeIP" xml:space="preserve">
|
||||
<value>FakeIP</value>
|
||||
</data>
|
||||
@@ -1440,9 +1440,6 @@
|
||||
<data name="TbDNSHostsConfig" xml:space="preserve">
|
||||
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
|
||||
</data>
|
||||
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
|
||||
<value>Apply to Proxy Domains Only</value>
|
||||
</data>
|
||||
<data name="ThBasicDNSSettings" xml:space="preserve">
|
||||
<value>Basic DNS Settings</value>
|
||||
</data>
|
||||
@@ -1462,7 +1459,7 @@
|
||||
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Prevent domain-based routing rules from failing</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Please fill in the correct config template</value>
|
||||
@@ -1494,4 +1491,103 @@
|
||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
|
||||
</data>
|
||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>開始解析和處理訂閱內容</value>
|
||||
</data>
|
||||
<data name="TbSelectProfile" xml:space="preserve">
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Please Add At Least One Configuration</value>
|
||||
</data>
|
||||
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
|
||||
<value>Policy Group</value>
|
||||
</data>
|
||||
<data name="TbConfigTypeProxyChain" xml:space="preserve">
|
||||
<value>Proxy Chain</value>
|
||||
</data>
|
||||
<data name="TbLeastPing" xml:space="preserve">
|
||||
<value>Lowest Latency</value>
|
||||
</data>
|
||||
<data name="TbRandom" xml:space="preserve">
|
||||
<value>Random</value>
|
||||
</data>
|
||||
<data name="TbRoundRobin" xml:space="preserve">
|
||||
<value>Round Robin</value>
|
||||
</data>
|
||||
<data name="TbLeastLoad" xml:space="preserve">
|
||||
<value>Most Stable</value>
|
||||
</data>
|
||||
<data name="TbPolicyGroupType" xml:space="preserve">
|
||||
<value>Policy Group Type</value>
|
||||
</data>
|
||||
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
||||
<value>Add Policy Group Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddProxyChainServer" xml:space="preserve">
|
||||
<value>Add Proxy Chain Configuration</value>
|
||||
</data>
|
||||
<data name="menuAddChildServer" xml:space="preserve">
|
||||
<value>Add Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuRemoveChildServer" xml:space="preserve">
|
||||
<value>Remove Child Configuration</value>
|
||||
</data>
|
||||
<data name="menuServerList" xml:space="preserve">
|
||||
<value>Server List</value>
|
||||
</data>
|
||||
<data name="TbFallback" xml:space="preserve">
|
||||
<value>Fallback</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by sing-box</value>
|
||||
</data>
|
||||
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
|
||||
<value>Multi-Configuration Fallback by Xray</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportNetwork" xml:space="preserve">
|
||||
<value>Core '{0}' does not support network type '{1}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
|
||||
</data>
|
||||
<data name="CoreNotSupportProtocol" xml:space="preserve">
|
||||
<value>Core '{0}' does not support protocol '{1}'.</value>
|
||||
</data>
|
||||
<data name="ProxyChainedPrefix" xml:space="preserve">
|
||||
<value>Proxy chained: </value>
|
||||
</data>
|
||||
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
|
||||
<value>Routing rule outbound: </value>
|
||||
</data>
|
||||
<data name="PolicyGroupPrefix" xml:space="preserve">
|
||||
<value>Policy group: </value>
|
||||
</data>
|
||||
<data name="NodeTagNotExist" xml:space="preserve">
|
||||
<value>Node alias '{0}' does not exist.</value>
|
||||
</data>
|
||||
<data name="GroupEmpty" xml:space="preserve">
|
||||
<value>Group '{0}' is empty. Please add at least one node.</value>
|
||||
</data>
|
||||
<data name="InvalidProperty" xml:space="preserve">
|
||||
<value>The {0} property is invalid, please check.</value>
|
||||
</data>
|
||||
<data name="GroupSelfReference" xml:space="preserve">
|
||||
<value>{0} 分組不能引用自身或循環引用</value>
|
||||
</data>
|
||||
<data name="NotSupportProtocol" xml:space="preserve">
|
||||
<value>Not support protocol '{0}'.</value>
|
||||
</data>
|
||||
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
|
||||
<value>如果系統沒有托盤功能,請不要開啟</value>
|
||||
</data>
|
||||
<data name="TbRuleType" xml:space="preserve">
|
||||
<value>规则类型</value>
|
||||
</data>
|
||||
<data name="TbRuleTypeTips" xml:space="preserve">
|
||||
<value>可对 Routing 和 DNS 单独设定规则,ALL 则都生效</value>
|
||||
</data>
|
||||
</root>
|
||||
File diff suppressed because it is too large
Load Diff
93
v2rayN/ServiceLib/Sample/singbox_fakeip_filter
Normal file
93
v2rayN/ServiceLib/Sample/singbox_fakeip_filter
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"domain": [
|
||||
"amobile.music.tc.qq.com",
|
||||
"api-jooxtt.sanook.com",
|
||||
"api.joox.com",
|
||||
"aqqmusic.tc.qq.com",
|
||||
"dl.stream.qqmusic.qq.com",
|
||||
"ff.dorado.sdo.com",
|
||||
"heartbeat.belkin.com",
|
||||
"isure.stream.qqmusic.qq.com",
|
||||
"joox.com",
|
||||
"lens.l.google.com",
|
||||
"localhost.ptlogin2.qq.com",
|
||||
"localhost.sec.qq.com",
|
||||
"mesu.apple.com",
|
||||
"mobileoc.music.tc.qq.com",
|
||||
"music.taihe.com",
|
||||
"musicapi.taihe.com",
|
||||
"na.b.g-tun.com",
|
||||
"proxy.golang.org",
|
||||
"ps.res.netease.com",
|
||||
"shark007.net",
|
||||
"songsearch.kugou.com",
|
||||
"static.adtidy.org",
|
||||
"streamoc.music.tc.qq.com",
|
||||
"swcdn.apple.com",
|
||||
"swdist.apple.com",
|
||||
"swdownload.apple.com",
|
||||
"swquery.apple.com",
|
||||
"swscan.apple.com",
|
||||
"turn.cloudflare.com",
|
||||
"trackercdn.kugou.com",
|
||||
"xnotify.xboxlive.com"
|
||||
],
|
||||
"domain_keyword": [
|
||||
"ntp",
|
||||
"stun",
|
||||
"time"
|
||||
],
|
||||
"domain_regex": [
|
||||
"^[^.]+$",
|
||||
"^[^.]+\\.[^.]+\\.xboxlive\\.com$",
|
||||
"^localhost\\.[^.]+\\.weixin\\.qq\\.com$",
|
||||
"^mijia\\scloud$",
|
||||
"^xbox\\.[^.]+\\.microsoft\\.com$",
|
||||
"^xbox\\.[^.]+\\.[^.]+\\.microsoft\\.com$"
|
||||
],
|
||||
"domain_suffix": [
|
||||
"126.net",
|
||||
"3gppnetwork.org",
|
||||
"battle.net",
|
||||
"battlenet.com.cn",
|
||||
"cdn.nintendo.net",
|
||||
"cmbchina.com",
|
||||
"cmbimg.com",
|
||||
"ff14.sdo.com",
|
||||
"ffxiv.com",
|
||||
"finalfantasyxiv.com",
|
||||
"gcloudcs.com",
|
||||
"home.arpa",
|
||||
"invalid",
|
||||
"kuwo.cn",
|
||||
"lan",
|
||||
"linksys.com",
|
||||
"linksyssmartwifi.com",
|
||||
"local",
|
||||
"localdomain",
|
||||
"localhost",
|
||||
"market.xiaomi.com",
|
||||
"mcdn.bilivideo.cn",
|
||||
"media.dssott.com",
|
||||
"msftconnecttest.com",
|
||||
"msftncsi.com",
|
||||
"music.163.com",
|
||||
"music.migu.cn",
|
||||
"n0808.com",
|
||||
"nflxvideo.net",
|
||||
"oray.com",
|
||||
"orayimg.com",
|
||||
"router.asus.com",
|
||||
"sandai.net",
|
||||
"square-enix.com",
|
||||
"srv.nintendo.net",
|
||||
"steamcontent.com",
|
||||
"uu.163.com",
|
||||
"wargaming.net",
|
||||
"wggames.cn",
|
||||
"wotgame.cn",
|
||||
"wowsgame.cn",
|
||||
"xiami.com",
|
||||
"y.qq.com"
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="ReactiveUI.Fody" />
|
||||
<PackageReference Include="sqlite-net-pcl" />
|
||||
<PackageReference Include="Splat.NLog" />
|
||||
<PackageReference Include="NLog" />
|
||||
<PackageReference Include="WebDav.Client" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
<PackageReference Include="QRCoder" />
|
||||
@@ -44,6 +44,7 @@
|
||||
<EmbeddedResource Include="Sample\tun_singbox_inbound" />
|
||||
<EmbeddedResource Include="Sample\tun_singbox_rules" />
|
||||
<EmbeddedResource Include="Sample\linux_autostart_config" />
|
||||
<EmbeddedResource Include="Sample\singbox_fakeip_filter" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -73,12 +73,13 @@ public class CoreConfigClashService
|
||||
}
|
||||
|
||||
//mixed-port
|
||||
fileContent["mixed-port"] = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
//log-level
|
||||
fileContent["log-level"] = GetLogLevel(_config.CoreBasicItem.Loglevel);
|
||||
|
||||
//external-controller
|
||||
fileContent["external-controller"] = $"{Global.Loopback}:{AppHandler.Instance.StatePort2}";
|
||||
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
|
||||
fileContent.Remove("secret");
|
||||
//allow-lan
|
||||
if (_config.Inbound.First().AllowLANConn)
|
||||
{
|
||||
@@ -139,7 +140,7 @@ public class CoreConfigClashService
|
||||
return ret;
|
||||
}
|
||||
|
||||
ClashApiHandler.Instance.ProfileContent = fileContent;
|
||||
ClashApiManager.Instance.ProfileContent = fileContent;
|
||||
|
||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}");
|
||||
ret.Success = true;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,533 @@
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using ServiceLib.Common;
|
||||
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService(Config config)
|
||||
{
|
||||
private readonly Config _config = config;
|
||||
private static readonly string _tag = "CoreConfigSingboxService";
|
||||
|
||||
#region public gen function
|
||||
|
||||
public async Task<RetResult> GenerateClientConfigContent(ProfileItem node)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
try
|
||||
{
|
||||
if (node == null
|
||||
|| !node.IsValid())
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
if (node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
|
||||
{
|
||||
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}";
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
if (node.ConfigType.IsGroupType())
|
||||
{
|
||||
switch (node.ConfigType)
|
||||
{
|
||||
case EConfigType.PolicyGroup:
|
||||
return await GenerateClientMultipleLoadConfig(node);
|
||||
|
||||
case EConfigType.ProxyChain:
|
||||
return await GenerateClientChainConfig(node);
|
||||
}
|
||||
}
|
||||
|
||||
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
|
||||
if (singboxConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
await GenLog(singboxConfig);
|
||||
|
||||
await GenInbounds(singboxConfig);
|
||||
|
||||
if (node.ConfigType == EConfigType.WireGuard)
|
||||
{
|
||||
singboxConfig.outbounds.RemoveAt(0);
|
||||
var endpoints = new Endpoints4Sbox();
|
||||
await GenEndpoint(node, endpoints);
|
||||
endpoints.tag = Global.ProxyTag;
|
||||
singboxConfig.endpoints = new() { endpoints };
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenOutbound(node, singboxConfig.outbounds.First());
|
||||
}
|
||||
|
||||
await GenMoreOutbounds(node, singboxConfig);
|
||||
|
||||
await GenRouting(singboxConfig);
|
||||
|
||||
await GenDns(node, singboxConfig);
|
||||
|
||||
await GenExperimental(singboxConfig);
|
||||
|
||||
await ConvertGeo2Ruleset(singboxConfig);
|
||||
|
||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||
ret.Success = true;
|
||||
|
||||
ret.Data = await ApplyFullConfigTemplate(singboxConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientSpeedtestConfig(List<ServerTestItem> selecteds)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
try
|
||||
{
|
||||
if (_config == null)
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
||||
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
|
||||
if (singboxConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
List<IPEndPoint> lstIpEndPoints = new();
|
||||
List<TcpConnectionInformation> lstTcpConns = new();
|
||||
try
|
||||
{
|
||||
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners());
|
||||
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners());
|
||||
lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
await GenLog(singboxConfig);
|
||||
//GenDns(new(), singboxConfig);
|
||||
singboxConfig.inbounds.Clear();
|
||||
singboxConfig.outbounds.RemoveAt(0);
|
||||
|
||||
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
|
||||
|
||||
foreach (var it in selecteds)
|
||||
{
|
||||
if (!Global.SingboxSupportConfigType.Contains(it.ConfigType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (it.Port <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
|
||||
if (item is null || item.IsComplex() || !item.IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//find unused port
|
||||
var port = initPort;
|
||||
for (var k = initPort; k < Global.MaxPort; k++)
|
||||
{
|
||||
if (lstIpEndPoints?.FindIndex(_it => _it.Port == k) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (lstTcpConns?.FindIndex(_it => _it.LocalEndPoint.Port == k) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//found
|
||||
port = k;
|
||||
initPort = port + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
//Port In Used
|
||||
if (lstIpEndPoints?.FindIndex(_it => _it.Port == port) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
it.Port = port;
|
||||
it.AllowTest = true;
|
||||
|
||||
//inbound
|
||||
Inbound4Sbox inbound = new()
|
||||
{
|
||||
listen = Global.Loopback,
|
||||
listen_port = port,
|
||||
type = EInboundProtocol.mixed.ToString(),
|
||||
};
|
||||
inbound.tag = inbound.type + inbound.listen_port.ToString();
|
||||
singboxConfig.inbounds.Add(inbound);
|
||||
|
||||
//outbound
|
||||
var server = await GenServer(item);
|
||||
if (server is null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
var tag = Global.ProxyTag + inbound.listen_port.ToString();
|
||||
server.tag = tag;
|
||||
if (server is Endpoints4Sbox endpoint)
|
||||
{
|
||||
singboxConfig.endpoints ??= new();
|
||||
singboxConfig.endpoints.Add(endpoint);
|
||||
}
|
||||
else if (server is Outbound4Sbox outbound)
|
||||
{
|
||||
singboxConfig.outbounds.Add(outbound);
|
||||
}
|
||||
|
||||
//rule
|
||||
Rule4Sbox rule = new()
|
||||
{
|
||||
inbound = new List<string> { inbound.tag },
|
||||
outbound = tag
|
||||
};
|
||||
singboxConfig.route.rules.Add(rule);
|
||||
}
|
||||
|
||||
var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
|
||||
if (rawDNSItem != null && rawDNSItem.Enabled == true)
|
||||
{
|
||||
await GenDnsDomainsCompatible(singboxConfig, rawDNSItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenDnsDomains(singboxConfig, _config.SimpleDNSItem);
|
||||
}
|
||||
singboxConfig.route.default_domain_resolver = new()
|
||||
{
|
||||
server = Global.SingboxLocalDNSTag,
|
||||
};
|
||||
|
||||
ret.Success = true;
|
||||
ret.Data = JsonUtils.Serialize(singboxConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientSpeedtestConfig(ProfileItem node, int port)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
try
|
||||
{
|
||||
if (node == null
|
||||
|| !node.IsValid())
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
if (node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
|
||||
{
|
||||
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}";
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
|
||||
if (singboxConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
await GenLog(singboxConfig);
|
||||
if (node.ConfigType == EConfigType.WireGuard)
|
||||
{
|
||||
singboxConfig.outbounds.RemoveAt(0);
|
||||
var endpoints = new Endpoints4Sbox();
|
||||
await GenEndpoint(node, endpoints);
|
||||
endpoints.tag = Global.ProxyTag;
|
||||
singboxConfig.endpoints = new() { endpoints };
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenOutbound(node, singboxConfig.outbounds.First());
|
||||
}
|
||||
await GenMoreOutbounds(node, singboxConfig);
|
||||
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
|
||||
if (item != null && item.Enabled == true)
|
||||
{
|
||||
await GenDnsDomainsCompatible(singboxConfig, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenDnsDomains(singboxConfig, _config.SimpleDNSItem);
|
||||
}
|
||||
singboxConfig.route.default_domain_resolver = new()
|
||||
{
|
||||
server = Global.SingboxLocalDNSTag,
|
||||
};
|
||||
|
||||
singboxConfig.route.rules.Clear();
|
||||
singboxConfig.inbounds.Clear();
|
||||
singboxConfig.inbounds.Add(new()
|
||||
{
|
||||
tag = $"{EInboundProtocol.mixed}{port}",
|
||||
listen = Global.Loopback,
|
||||
listen_port = port,
|
||||
type = EInboundProtocol.mixed.ToString(),
|
||||
});
|
||||
|
||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||
ret.Success = true;
|
||||
ret.Data = JsonUtils.Serialize(singboxConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientMultipleLoadConfig(ProfileItem parentNode)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
try
|
||||
{
|
||||
if (_config == null)
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
||||
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
|
||||
if (singboxConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
singboxConfig.outbounds.RemoveAt(0);
|
||||
|
||||
await GenLog(singboxConfig);
|
||||
await GenInbounds(singboxConfig);
|
||||
|
||||
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
|
||||
if (groupRet != 0)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
await GenRouting(singboxConfig);
|
||||
await GenExperimental(singboxConfig);
|
||||
await GenDns(null, singboxConfig);
|
||||
await ConvertGeo2Ruleset(singboxConfig);
|
||||
|
||||
ret.Success = true;
|
||||
|
||||
ret.Data = await ApplyFullConfigTemplate(singboxConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientChainConfig(ProfileItem parentNode)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
try
|
||||
{
|
||||
if (_config == null)
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
||||
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
|
||||
if (singboxConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
singboxConfig.outbounds.RemoveAt(0);
|
||||
|
||||
await GenLog(singboxConfig);
|
||||
await GenInbounds(singboxConfig);
|
||||
|
||||
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
|
||||
if (groupRet != 0)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
await GenRouting(singboxConfig);
|
||||
await GenExperimental(singboxConfig);
|
||||
await GenDns(null, singboxConfig);
|
||||
await ConvertGeo2Ruleset(singboxConfig);
|
||||
|
||||
ret.Success = true;
|
||||
|
||||
ret.Data = await ApplyFullConfigTemplate(singboxConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
if (node == null || fileName is null)
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
try
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (File.Exists(fileName))
|
||||
{
|
||||
File.Delete(fileName);
|
||||
}
|
||||
|
||||
var addressFileName = node.Address;
|
||||
if (addressFileName.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
if (!File.Exists(addressFileName))
|
||||
{
|
||||
addressFileName = Path.Combine(Utils.GetConfigPath(), addressFileName);
|
||||
}
|
||||
if (!File.Exists(addressFileName))
|
||||
{
|
||||
ret.Msg = ResUI.FailedReadConfiguration + "1";
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (node.Address == Global.CoreMultipleLoadConfigFileName)
|
||||
{
|
||||
var txtFile = File.ReadAllText(addressFileName);
|
||||
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(txtFile);
|
||||
if (singboxConfig == null)
|
||||
{
|
||||
File.Copy(addressFileName, fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenInbounds(singboxConfig);
|
||||
await GenExperimental(singboxConfig);
|
||||
|
||||
var content = JsonUtils.Serialize(singboxConfig, true);
|
||||
await File.WriteAllTextAsync(fileName, content);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Copy(addressFileName, fileName);
|
||||
}
|
||||
|
||||
//check again
|
||||
if (!File.Exists(fileName))
|
||||
{
|
||||
ret.Msg = ResUI.FailedReadConfiguration + "2";
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||
ret.Success = true;
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion public gen function
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private async Task<string> ApplyFullConfigTemplate(SingboxConfig singboxConfig)
|
||||
{
|
||||
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box);
|
||||
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled)
|
||||
{
|
||||
return JsonUtils.Serialize(singboxConfig);
|
||||
}
|
||||
|
||||
var fullConfigTemplateItem = _config.TunModeItem.EnableTun ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
|
||||
if (fullConfigTemplateItem.IsNullOrEmpty())
|
||||
{
|
||||
return JsonUtils.Serialize(singboxConfig);
|
||||
}
|
||||
|
||||
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
|
||||
if (fullConfigTemplateNode == null)
|
||||
{
|
||||
return JsonUtils.Serialize(singboxConfig);
|
||||
}
|
||||
|
||||
// Process outbounds
|
||||
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
|
||||
foreach (var outbound in singboxConfig.outbounds)
|
||||
{
|
||||
if (outbound.type.ToLower() is "direct" or "block")
|
||||
{
|
||||
if (fullConfigTemplate.AddProxyOnly == true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (outbound.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound.server ?? string.Empty))
|
||||
{
|
||||
outbound.detour = fullConfigTemplate.ProxyDetour;
|
||||
}
|
||||
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
|
||||
}
|
||||
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
|
||||
|
||||
// Process endpoints
|
||||
if (singboxConfig.endpoints != null && singboxConfig.endpoints.Count > 0)
|
||||
{
|
||||
var customEndpointsNode = fullConfigTemplateNode["endpoints"] is JsonArray endpoints ? endpoints : new JsonArray();
|
||||
foreach (var endpoint in singboxConfig.endpoints)
|
||||
{
|
||||
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
|
||||
{
|
||||
endpoint.detour = fullConfigTemplate.ProxyDetour;
|
||||
}
|
||||
customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint));
|
||||
}
|
||||
fullConfigTemplateNode["endpoints"] = customEndpointsNode;
|
||||
}
|
||||
|
||||
return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private async Task<int> GenDns(ProfileItem? node, SingboxConfig singboxConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
|
||||
if (item != null && item.Enabled == true)
|
||||
{
|
||||
return await GenDnsCompatible(node, singboxConfig);
|
||||
}
|
||||
|
||||
var simpleDNSItem = _config.SimpleDNSItem;
|
||||
await GenDnsServers(singboxConfig, simpleDNSItem);
|
||||
await GenDnsRules(singboxConfig, simpleDNSItem);
|
||||
|
||||
singboxConfig.dns ??= new Dns4Sbox();
|
||||
singboxConfig.dns.independent_cache = true;
|
||||
|
||||
// final dns
|
||||
var routing = await ConfigHandler.GetDefaultRouting(_config);
|
||||
var useDirectDns = false;
|
||||
if (routing != null)
|
||||
{
|
||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
|
||||
|
||||
useDirectDns = rules?.LastOrDefault() is { } lastRule &&
|
||||
lastRule.OutboundTag == Global.DirectTag &&
|
||||
(lastRule.Port == "0-65535" ||
|
||||
lastRule.Network == "tcp,udp" ||
|
||||
lastRule.Ip?.Contains("0.0.0.0/0") == true);
|
||||
}
|
||||
singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag;
|
||||
if ((!useDirectDns) && simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false)
|
||||
{
|
||||
singboxConfig.dns.rules.Add(new()
|
||||
{
|
||||
server = Global.SingboxFakeDNSTag,
|
||||
query_type = new List<int> { 1, 28 }, // A and AAAA
|
||||
rewrite_ttl = 1,
|
||||
});
|
||||
}
|
||||
|
||||
await GenOutboundDnsRule(node, singboxConfig);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsServers(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem)
|
||||
{
|
||||
var finalDns = await GenDnsDomains(singboxConfig, simpleDNSItem);
|
||||
|
||||
var directDns = ParseDnsAddress(simpleDNSItem.DirectDNS);
|
||||
directDns.tag = Global.SingboxDirectDNSTag;
|
||||
directDns.domain_resolver = Global.SingboxLocalDNSTag;
|
||||
|
||||
var remoteDns = ParseDnsAddress(simpleDNSItem.RemoteDNS);
|
||||
remoteDns.tag = Global.SingboxRemoteDNSTag;
|
||||
remoteDns.detour = Global.ProxyTag;
|
||||
remoteDns.domain_resolver = Global.SingboxLocalDNSTag;
|
||||
|
||||
var hostsDns = new Server4Sbox
|
||||
{
|
||||
tag = Global.SingboxHostsDNSTag,
|
||||
type = "hosts",
|
||||
predefined = new(),
|
||||
};
|
||||
if (simpleDNSItem.AddCommonHosts == true)
|
||||
{
|
||||
hostsDns.predefined = Global.PredefinedHosts;
|
||||
}
|
||||
|
||||
if (simpleDNSItem.UseSystemHosts == true)
|
||||
{
|
||||
var systemHosts = Utils.GetSystemHosts();
|
||||
if (systemHosts != null && systemHosts.Count > 0)
|
||||
{
|
||||
foreach (var host in systemHosts)
|
||||
{
|
||||
hostsDns.predefined.TryAdd(host.Key, new List<string> { host.Value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
|
||||
{
|
||||
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
|
||||
|
||||
foreach (var kvp in userHostsMap)
|
||||
{
|
||||
hostsDns.predefined[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var host in hostsDns.predefined)
|
||||
{
|
||||
if (finalDns.server == host.Key)
|
||||
{
|
||||
finalDns.domain_resolver = Global.SingboxHostsDNSTag;
|
||||
}
|
||||
if (remoteDns.server == host.Key)
|
||||
{
|
||||
remoteDns.domain_resolver = Global.SingboxHostsDNSTag;
|
||||
}
|
||||
if (directDns.server == host.Key)
|
||||
{
|
||||
directDns.domain_resolver = Global.SingboxHostsDNSTag;
|
||||
}
|
||||
}
|
||||
|
||||
singboxConfig.dns ??= new Dns4Sbox();
|
||||
singboxConfig.dns.servers ??= new List<Server4Sbox>();
|
||||
singboxConfig.dns.servers.Add(remoteDns);
|
||||
singboxConfig.dns.servers.Add(directDns);
|
||||
singboxConfig.dns.servers.Add(hostsDns);
|
||||
|
||||
// fake ip
|
||||
if (simpleDNSItem.FakeIP == true)
|
||||
{
|
||||
var fakeip = new Server4Sbox
|
||||
{
|
||||
tag = Global.SingboxFakeDNSTag,
|
||||
type = "fakeip",
|
||||
inet4_range = "198.18.0.0/15",
|
||||
inet6_range = "fc00::/18",
|
||||
};
|
||||
singboxConfig.dns.servers.Add(fakeip);
|
||||
}
|
||||
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<Server4Sbox> GenDnsDomains(SingboxConfig singboxConfig, SimpleDNSItem? simpleDNSItem)
|
||||
{
|
||||
var finalDnsAddress = "local";
|
||||
if (_config.TunModeItem.EnableTun)
|
||||
{
|
||||
finalDnsAddress = "dhcp://auto";
|
||||
}
|
||||
var finalDns = ParseDnsAddress(finalDnsAddress);
|
||||
finalDns.tag = Global.SingboxLocalDNSTag;
|
||||
singboxConfig.dns ??= new Dns4Sbox();
|
||||
singboxConfig.dns.servers ??= new List<Server4Sbox>();
|
||||
singboxConfig.dns.servers.Add(finalDns);
|
||||
return await Task.FromResult(finalDns);
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsRules(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem)
|
||||
{
|
||||
singboxConfig.dns ??= new Dns4Sbox();
|
||||
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
|
||||
|
||||
singboxConfig.dns.rules.AddRange(new[]
|
||||
{
|
||||
new Rule4Sbox { ip_accept_any = true, server = Global.SingboxHostsDNSTag },
|
||||
new Rule4Sbox
|
||||
{
|
||||
server = Global.SingboxRemoteDNSTag,
|
||||
strategy = simpleDNSItem.SingboxStrategy4Proxy.IsNullOrEmpty() ? null : simpleDNSItem.SingboxStrategy4Proxy,
|
||||
clash_mode = ERuleMode.Global.ToString()
|
||||
},
|
||||
new Rule4Sbox
|
||||
{
|
||||
server = Global.SingboxDirectDNSTag,
|
||||
strategy = simpleDNSItem.SingboxStrategy4Direct.IsNullOrEmpty() ? null : simpleDNSItem.SingboxStrategy4Direct,
|
||||
clash_mode = ERuleMode.Direct.ToString()
|
||||
}
|
||||
});
|
||||
|
||||
if (simpleDNSItem.BlockBindingQuery == true)
|
||||
{
|
||||
singboxConfig.dns.rules.Add(new()
|
||||
{
|
||||
query_type = new List<int> { 64, 65 },
|
||||
action = "predefined",
|
||||
rcode = "NOTIMP"
|
||||
});
|
||||
}
|
||||
|
||||
if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == true)
|
||||
{
|
||||
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
|
||||
fakeipFilterRule.invert = true;
|
||||
var rule4Fake = new Rule4Sbox
|
||||
{
|
||||
server = Global.SingboxFakeDNSTag,
|
||||
type = "logical",
|
||||
mode = "and",
|
||||
rewrite_ttl = 1,
|
||||
rules = new List<Rule4Sbox>
|
||||
{
|
||||
new() {
|
||||
query_type = new List<int> { 1, 28 }, // A and AAAA
|
||||
},
|
||||
fakeipFilterRule,
|
||||
}
|
||||
};
|
||||
|
||||
singboxConfig.dns.rules.Add(rule4Fake);
|
||||
}
|
||||
|
||||
var routing = await ConfigHandler.GetDefaultRouting(_config);
|
||||
if (routing == null)
|
||||
return 0;
|
||||
|
||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
|
||||
var expectedIPCidr = new List<string>();
|
||||
var expectedIPsRegions = new List<string>();
|
||||
var regionNames = new HashSet<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs))
|
||||
{
|
||||
var ipItems = simpleDNSItem.DirectExpectedIPs
|
||||
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToList();
|
||||
|
||||
foreach (var ip in ipItems)
|
||||
{
|
||||
if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var region = ip["geoip:".Length..];
|
||||
if (!string.IsNullOrEmpty(region))
|
||||
{
|
||||
expectedIPsRegions.Add(region);
|
||||
regionNames.Add(region);
|
||||
regionNames.Add($"geolocation-{region}");
|
||||
regionNames.Add($"tld-{region}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
expectedIPCidr.Add(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in rules)
|
||||
{
|
||||
if (!item.Enabled || item.Domain is null || item.Domain.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.RuleType == ERuleType.Routing)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rule = new Rule4Sbox();
|
||||
var validDomains = item.Domain.Count(it => ParseV2Domain(it, rule));
|
||||
if (validDomains <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.OutboundTag == Global.DirectTag)
|
||||
{
|
||||
rule.server = Global.SingboxDirectDNSTag;
|
||||
rule.strategy = string.IsNullOrEmpty(simpleDNSItem.SingboxStrategy4Direct) ? null : simpleDNSItem.SingboxStrategy4Direct;
|
||||
|
||||
if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0)
|
||||
{
|
||||
var geositeSet = new HashSet<string>(rule.geosite);
|
||||
if (regionNames.Intersect(geositeSet).Any())
|
||||
{
|
||||
if (expectedIPsRegions.Count > 0)
|
||||
{
|
||||
rule.geoip = expectedIPsRegions;
|
||||
}
|
||||
if (expectedIPCidr.Count > 0)
|
||||
{
|
||||
rule.ip_cidr = expectedIPCidr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (item.OutboundTag == Global.BlockTag)
|
||||
{
|
||||
rule.action = "predefined";
|
||||
rule.rcode = "NXDOMAIN";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false)
|
||||
{
|
||||
var rule4Fake = JsonUtils.DeepCopy(rule);
|
||||
rule4Fake.server = Global.SingboxFakeDNSTag;
|
||||
rule4Fake.query_type = new List<int> { 1, 28 }; // A and AAAA
|
||||
rule4Fake.rewrite_ttl = 1;
|
||||
singboxConfig.dns.rules.Add(rule4Fake);
|
||||
}
|
||||
rule.server = Global.SingboxRemoteDNSTag;
|
||||
rule.strategy = string.IsNullOrEmpty(simpleDNSItem.SingboxStrategy4Proxy) ? null : simpleDNSItem.SingboxStrategy4Proxy;
|
||||
}
|
||||
|
||||
singboxConfig.dns.rules.Add(rule);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsCompatible(ProfileItem? node, SingboxConfig singboxConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
|
||||
var strDNS = string.Empty;
|
||||
if (_config.TunModeItem.EnableTun)
|
||||
{
|
||||
strDNS = string.IsNullOrEmpty(item?.TunDNS) ? EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName) : item?.TunDNS;
|
||||
}
|
||||
else
|
||||
{
|
||||
strDNS = string.IsNullOrEmpty(item?.NormalDNS) ? EmbedUtils.GetEmbedText(Global.DNSSingboxNormalFileName) : item?.NormalDNS;
|
||||
}
|
||||
|
||||
var dns4Sbox = JsonUtils.Deserialize<Dns4Sbox>(strDNS);
|
||||
if (dns4Sbox is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
singboxConfig.dns = dns4Sbox;
|
||||
|
||||
if (dns4Sbox.servers != null && dns4Sbox.servers.Count > 0 && dns4Sbox.servers.First().address.IsNullOrEmpty())
|
||||
{
|
||||
await GenDnsDomainsCompatible(singboxConfig, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenDnsDomainsLegacyCompatible(singboxConfig, item);
|
||||
}
|
||||
|
||||
await GenOutboundDnsRule(node, singboxConfig);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsDomainsCompatible(SingboxConfig singboxConfig, DNSItem? dnsItem)
|
||||
{
|
||||
var dns4Sbox = singboxConfig.dns ?? new();
|
||||
dns4Sbox.servers ??= [];
|
||||
dns4Sbox.rules ??= [];
|
||||
|
||||
var tag = Global.SingboxLocalDNSTag;
|
||||
|
||||
var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;
|
||||
|
||||
var localDnsServer = ParseDnsAddress(finalDnsAddress);
|
||||
localDnsServer.tag = tag;
|
||||
|
||||
dns4Sbox.servers.Add(localDnsServer);
|
||||
|
||||
singboxConfig.dns = dns4Sbox;
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dnsItem)
|
||||
{
|
||||
var dns4Sbox = singboxConfig.dns ?? new();
|
||||
dns4Sbox.servers ??= [];
|
||||
dns4Sbox.rules ??= [];
|
||||
|
||||
var tag = Global.SingboxLocalDNSTag;
|
||||
dns4Sbox.servers.Add(new()
|
||||
{
|
||||
tag = tag,
|
||||
address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress,
|
||||
detour = Global.DirectTag,
|
||||
strategy = string.IsNullOrEmpty(dnsItem?.DomainStrategy4Freedom) ? null : dnsItem?.DomainStrategy4Freedom,
|
||||
});
|
||||
dns4Sbox.rules.Insert(0, new()
|
||||
{
|
||||
server = tag,
|
||||
clash_mode = ERuleMode.Direct.ToString()
|
||||
});
|
||||
dns4Sbox.rules.Insert(0, new()
|
||||
{
|
||||
server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote",
|
||||
clash_mode = ERuleMode.Global.ToString()
|
||||
});
|
||||
|
||||
var lstDomain = singboxConfig.outbounds
|
||||
.Where(t => t.server.IsNotEmpty() && Utils.IsDomain(t.server))
|
||||
.Select(t => t.server)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (lstDomain != null && lstDomain.Count > 0)
|
||||
{
|
||||
dns4Sbox.rules.Insert(0, new()
|
||||
{
|
||||
server = tag,
|
||||
domain = lstDomain
|
||||
});
|
||||
}
|
||||
|
||||
singboxConfig.dns = dns4Sbox;
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenOutboundDnsRule(ProfileItem? node, SingboxConfig singboxConfig)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<string> domain = new();
|
||||
if (Utils.IsDomain(node.Address)) // normal outbound
|
||||
{
|
||||
domain.Add(node.Address);
|
||||
}
|
||||
if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty()) // Tun2SocksAddress
|
||||
{
|
||||
domain.AddRange(Utils.String2List(node.SpiderX)
|
||||
.Where(Utils.IsDomain)
|
||||
.Distinct()
|
||||
.ToList());
|
||||
}
|
||||
if (domain.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
|
||||
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
|
||||
{
|
||||
server = Global.SingboxLocalDNSTag,
|
||||
domain = domain,
|
||||
});
|
||||
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private static Server4Sbox? ParseDnsAddress(string address)
|
||||
{
|
||||
var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim();
|
||||
if (string.IsNullOrEmpty(addressFirst))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var server = new Server4Sbox();
|
||||
|
||||
if (addressFirst is "local" or "localhost")
|
||||
{
|
||||
server.type = "local";
|
||||
return server;
|
||||
}
|
||||
|
||||
if (addressFirst.StartsWith("dhcp://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var interface_name = addressFirst.Substring(7);
|
||||
server.type = "dhcp";
|
||||
server.Interface = interface_name == "auto" ? null : interface_name;
|
||||
return server;
|
||||
}
|
||||
|
||||
if (!addressFirst.Contains("://"))
|
||||
{
|
||||
// udp dns
|
||||
server.type = "udp";
|
||||
server.server = addressFirst;
|
||||
return server;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal);
|
||||
server.type = addressFirst.Substring(0, protocolEndIndex).ToLower();
|
||||
|
||||
var uri = new Uri(addressFirst);
|
||||
server.server = uri.Host;
|
||||
|
||||
if (!uri.IsDefaultPort)
|
||||
{
|
||||
server.server_port = uri.Port;
|
||||
}
|
||||
|
||||
if ((server.type == "https" || server.type == "h3") && !string.IsNullOrEmpty(uri.AbsolutePath) && uri.AbsolutePath != "/")
|
||||
{
|
||||
server.path = uri.AbsolutePath;
|
||||
}
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal);
|
||||
if (protocolEndIndex > 0)
|
||||
{
|
||||
server.type = addressFirst.Substring(0, protocolEndIndex).ToLower();
|
||||
var remaining = addressFirst.Substring(protocolEndIndex + 3);
|
||||
|
||||
var portIndex = remaining.IndexOf(':');
|
||||
var pathIndex = remaining.IndexOf('/');
|
||||
|
||||
if (portIndex > 0)
|
||||
{
|
||||
server.server = remaining.Substring(0, portIndex);
|
||||
var portPart = pathIndex > portIndex
|
||||
? remaining.Substring(portIndex + 1, pathIndex - portIndex - 1)
|
||||
: remaining.Substring(portIndex + 1);
|
||||
|
||||
if (int.TryParse(portPart, out var parsedPort))
|
||||
{
|
||||
server.server_port = parsedPort;
|
||||
}
|
||||
}
|
||||
else if (pathIndex > 0)
|
||||
{
|
||||
server.server = remaining.Substring(0, pathIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
server.server = remaining;
|
||||
}
|
||||
|
||||
if (pathIndex > 0 && (server.type == "https" || server.type == "h3"))
|
||||
{
|
||||
server.path = remaining.Substring(pathIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private async Task<int> GenInbounds(SingboxConfig singboxConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
var listen = "0.0.0.0";
|
||||
singboxConfig.inbounds = [];
|
||||
|
||||
if (!_config.TunModeItem.EnableTun
|
||||
|| (_config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && _config.RunningCoreType == ECoreType.sing_box))
|
||||
{
|
||||
var inbound = new Inbound4Sbox()
|
||||
{
|
||||
type = EInboundProtocol.mixed.ToString(),
|
||||
tag = EInboundProtocol.socks.ToString(),
|
||||
listen = Global.Loopback,
|
||||
};
|
||||
singboxConfig.inbounds.Add(inbound);
|
||||
|
||||
inbound.listen_port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
|
||||
if (_config.Inbound.First().SecondLocalPortEnabled)
|
||||
{
|
||||
var inbound2 = GetInbound(inbound, EInboundProtocol.socks2, true);
|
||||
singboxConfig.inbounds.Add(inbound2);
|
||||
}
|
||||
|
||||
if (_config.Inbound.First().AllowLANConn)
|
||||
{
|
||||
if (_config.Inbound.First().NewPort4LAN)
|
||||
{
|
||||
var inbound3 = GetInbound(inbound, EInboundProtocol.socks3, true);
|
||||
inbound3.listen = listen;
|
||||
singboxConfig.inbounds.Add(inbound3);
|
||||
|
||||
//auth
|
||||
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
|
||||
{
|
||||
inbound3.users = new() { new() { username = _config.Inbound.First().User, password = _config.Inbound.First().Pass } };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
inbound.listen = listen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_config.TunModeItem.EnableTun)
|
||||
{
|
||||
if (_config.TunModeItem.Mtu <= 0)
|
||||
{
|
||||
_config.TunModeItem.Mtu = Global.TunMtus.First();
|
||||
}
|
||||
if (_config.TunModeItem.Stack.IsNullOrEmpty())
|
||||
{
|
||||
_config.TunModeItem.Stack = Global.TunStacks.First();
|
||||
}
|
||||
|
||||
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
|
||||
tunInbound.interface_name = Utils.IsOSX() ? $"utun{new Random().Next(99)}" : "singbox_tun";
|
||||
tunInbound.mtu = _config.TunModeItem.Mtu;
|
||||
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
|
||||
tunInbound.strict_route = _config.TunModeItem.StrictRoute;
|
||||
tunInbound.stack = _config.TunModeItem.Stack;
|
||||
if (_config.TunModeItem.EnableIPv6Address == false)
|
||||
{
|
||||
tunInbound.address = ["172.18.0.1/30"];
|
||||
}
|
||||
|
||||
singboxConfig.inbounds.Add(tunInbound);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private Inbound4Sbox GetInbound(Inbound4Sbox inItem, EInboundProtocol protocol, bool bSocks)
|
||||
{
|
||||
var inbound = JsonUtils.DeepCopy(inItem);
|
||||
inbound.tag = protocol.ToString();
|
||||
inbound.listen_port = inItem.listen_port + (int)protocol;
|
||||
inbound.type = EInboundProtocol.mixed.ToString();
|
||||
return inbound;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private async Task<int> GenLog(SingboxConfig singboxConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (_config.CoreBasicItem.Loglevel)
|
||||
{
|
||||
case "debug":
|
||||
case "info":
|
||||
case "error":
|
||||
singboxConfig.log.level = _config.CoreBasicItem.Loglevel;
|
||||
break;
|
||||
|
||||
case "warning":
|
||||
singboxConfig.log.level = "warn";
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (_config.CoreBasicItem.Loglevel == Global.None)
|
||||
{
|
||||
singboxConfig.log.disabled = true;
|
||||
}
|
||||
if (_config.CoreBasicItem.LogEnabled)
|
||||
{
|
||||
var dtNow = DateTime.Now;
|
||||
singboxConfig.log.output = Utils.GetLogPath($"sbox_{dtNow:yyyy-MM-dd}.txt");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,820 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private async Task<int> GenOutbound(ProfileItem node, Outbound4Sbox outbound)
|
||||
{
|
||||
try
|
||||
{
|
||||
outbound.server = node.Address;
|
||||
outbound.server_port = node.Port;
|
||||
outbound.type = Global.ProtocolTypes[node.ConfigType];
|
||||
|
||||
switch (node.ConfigType)
|
||||
{
|
||||
case EConfigType.VMess:
|
||||
{
|
||||
outbound.uuid = node.Id;
|
||||
outbound.alter_id = node.AlterId;
|
||||
if (Global.VmessSecurities.Contains(node.Security))
|
||||
{
|
||||
outbound.security = node.Security;
|
||||
}
|
||||
else
|
||||
{
|
||||
outbound.security = Global.DefaultSecurity;
|
||||
}
|
||||
|
||||
await GenOutboundMux(node, outbound);
|
||||
break;
|
||||
}
|
||||
case EConfigType.Shadowsocks:
|
||||
{
|
||||
outbound.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : Global.None;
|
||||
outbound.password = node.Id;
|
||||
|
||||
await GenOutboundMux(node, outbound);
|
||||
break;
|
||||
}
|
||||
case EConfigType.SOCKS:
|
||||
{
|
||||
outbound.version = "5";
|
||||
if (node.Security.IsNotEmpty()
|
||||
&& node.Id.IsNotEmpty())
|
||||
{
|
||||
outbound.username = node.Security;
|
||||
outbound.password = node.Id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EConfigType.HTTP:
|
||||
{
|
||||
if (node.Security.IsNotEmpty()
|
||||
&& node.Id.IsNotEmpty())
|
||||
{
|
||||
outbound.username = node.Security;
|
||||
outbound.password = node.Id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EConfigType.VLESS:
|
||||
{
|
||||
outbound.uuid = node.Id;
|
||||
|
||||
outbound.packet_encoding = "xudp";
|
||||
|
||||
if (node.Flow.IsNullOrEmpty())
|
||||
{
|
||||
await GenOutboundMux(node, outbound);
|
||||
}
|
||||
else
|
||||
{
|
||||
outbound.flow = node.Flow;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EConfigType.Trojan:
|
||||
{
|
||||
outbound.password = node.Id;
|
||||
|
||||
await GenOutboundMux(node, outbound);
|
||||
break;
|
||||
}
|
||||
case EConfigType.Hysteria2:
|
||||
{
|
||||
outbound.password = node.Id;
|
||||
|
||||
if (node.Path.IsNotEmpty())
|
||||
{
|
||||
outbound.obfs = new()
|
||||
{
|
||||
type = "salamander",
|
||||
password = node.Path.TrimEx(),
|
||||
};
|
||||
}
|
||||
|
||||
outbound.up_mbps = _config.HysteriaItem.UpMbps > 0 ? _config.HysteriaItem.UpMbps : null;
|
||||
outbound.down_mbps = _config.HysteriaItem.DownMbps > 0 ? _config.HysteriaItem.DownMbps : null;
|
||||
if (node.Ports.IsNotEmpty() && (node.Ports.Contains(':') || node.Ports.Contains('-') || node.Ports.Contains(',')))
|
||||
{
|
||||
outbound.server_port = null;
|
||||
outbound.server_ports = node.Ports.Split(',')
|
||||
.Select(p => p.Trim())
|
||||
.Where(p => p.IsNotEmpty())
|
||||
.Select(p =>
|
||||
{
|
||||
var port = p.Replace('-', ':');
|
||||
return port.Contains(':') ? port : $"{port}:{port}";
|
||||
})
|
||||
.ToList();
|
||||
outbound.hop_interval = _config.HysteriaItem.HopInterval > 0 ? $"{_config.HysteriaItem.HopInterval}s" : null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EConfigType.TUIC:
|
||||
{
|
||||
outbound.uuid = node.Id;
|
||||
outbound.password = node.Security;
|
||||
outbound.congestion_control = node.HeaderType;
|
||||
break;
|
||||
}
|
||||
case EConfigType.Anytls:
|
||||
{
|
||||
outbound.password = node.Id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await GenOutboundTls(node, outbound);
|
||||
|
||||
await GenOutboundTransport(node, outbound);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<int> GenEndpoint(ProfileItem node, Endpoints4Sbox endpoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
endpoint.address = Utils.String2List(node.RequestHost);
|
||||
endpoint.type = Global.ProtocolTypes[node.ConfigType];
|
||||
|
||||
switch (node.ConfigType)
|
||||
{
|
||||
case EConfigType.WireGuard:
|
||||
{
|
||||
var peer = new Peer4Sbox
|
||||
{
|
||||
public_key = node.PublicKey,
|
||||
reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(),
|
||||
address = node.Address,
|
||||
port = node.Port,
|
||||
// TODO default ["0.0.0.0/0", "::/0"]
|
||||
allowed_ips = new() { "0.0.0.0/0", "::/0" },
|
||||
};
|
||||
endpoint.private_key = node.Id;
|
||||
endpoint.mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt();
|
||||
endpoint.peers = new() { peer };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<BaseServer4Sbox?> GenServer(ProfileItem node)
|
||||
{
|
||||
try
|
||||
{
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
||||
if (node.ConfigType == EConfigType.WireGuard)
|
||||
{
|
||||
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
|
||||
var ret = await GenEndpoint(node, endpoint);
|
||||
if (ret != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
|
||||
var ret = await GenOutbound(node, outbound);
|
||||
if (ret != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return outbound;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult<BaseServer4Sbox?>(null);
|
||||
}
|
||||
|
||||
private async Task<int> GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!node.ConfigType.IsGroupType())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
|
||||
if (hasCycle)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
|
||||
if (childProfiles.Count <= 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
switch (node.ConfigType)
|
||||
{
|
||||
case EConfigType.PolicyGroup:
|
||||
if (ignoreOriginChain)
|
||||
{
|
||||
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EConfigType.ProxyChain:
|
||||
await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenOutboundMux(ProfileItem node, Outbound4Sbox outbound)
|
||||
{
|
||||
try
|
||||
{
|
||||
var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
|
||||
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
|
||||
{
|
||||
var mux = new Multiplex4Sbox()
|
||||
{
|
||||
enabled = true,
|
||||
protocol = _config.Mux4SboxItem.Protocol,
|
||||
max_connections = _config.Mux4SboxItem.MaxConnections,
|
||||
padding = _config.Mux4SboxItem.Padding,
|
||||
};
|
||||
outbound.multiplex = mux;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenOutboundTls(ProfileItem node, Outbound4Sbox outbound)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (node.StreamSecurity == Global.StreamSecurityReality || node.StreamSecurity == Global.StreamSecurity)
|
||||
{
|
||||
var server_name = string.Empty;
|
||||
if (node.Sni.IsNotEmpty())
|
||||
{
|
||||
server_name = node.Sni;
|
||||
}
|
||||
else if (node.RequestHost.IsNotEmpty())
|
||||
{
|
||||
server_name = Utils.String2List(node.RequestHost)?.First();
|
||||
}
|
||||
var tls = new Tls4Sbox()
|
||||
{
|
||||
enabled = true,
|
||||
record_fragment = _config.CoreBasicItem.EnableFragment,
|
||||
server_name = server_name,
|
||||
insecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure),
|
||||
alpn = node.GetAlpn(),
|
||||
};
|
||||
if (node.Fingerprint.IsNotEmpty())
|
||||
{
|
||||
tls.utls = new Utls4Sbox()
|
||||
{
|
||||
enabled = true,
|
||||
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint
|
||||
};
|
||||
}
|
||||
if (node.StreamSecurity == Global.StreamSecurityReality)
|
||||
{
|
||||
tls.reality = new Reality4Sbox()
|
||||
{
|
||||
enabled = true,
|
||||
public_key = node.PublicKey,
|
||||
short_id = node.ShortId
|
||||
};
|
||||
tls.insecure = false;
|
||||
}
|
||||
outbound.tls = tls;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenOutboundTransport(ProfileItem node, Outbound4Sbox outbound)
|
||||
{
|
||||
try
|
||||
{
|
||||
var transport = new Transport4Sbox();
|
||||
|
||||
switch (node.GetNetwork())
|
||||
{
|
||||
case nameof(ETransport.h2):
|
||||
transport.type = nameof(ETransport.http);
|
||||
transport.host = node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(node.RequestHost);
|
||||
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.tcp): //http
|
||||
if (node.HeaderType == Global.TcpHeaderHttp)
|
||||
{
|
||||
if (node.ConfigType == EConfigType.Shadowsocks)
|
||||
{
|
||||
outbound.plugin = "obfs-local";
|
||||
outbound.plugin_opts = $"obfs=http;obfs-host={node.RequestHost};";
|
||||
}
|
||||
else
|
||||
{
|
||||
transport.type = nameof(ETransport.http);
|
||||
transport.host = node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(node.RequestHost);
|
||||
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case nameof(ETransport.ws):
|
||||
transport.type = nameof(ETransport.ws);
|
||||
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
|
||||
if (node.RequestHost.IsNotEmpty())
|
||||
{
|
||||
transport.headers = new()
|
||||
{
|
||||
Host = node.RequestHost
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case nameof(ETransport.httpupgrade):
|
||||
transport.type = nameof(ETransport.httpupgrade);
|
||||
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
|
||||
transport.host = node.RequestHost.IsNullOrEmpty() ? null : node.RequestHost;
|
||||
|
||||
break;
|
||||
|
||||
case nameof(ETransport.quic):
|
||||
transport.type = nameof(ETransport.quic);
|
||||
break;
|
||||
|
||||
case nameof(ETransport.grpc):
|
||||
transport.type = nameof(ETransport.grpc);
|
||||
transport.service_name = node.Path;
|
||||
transport.idle_timeout = _config.GrpcItem.IdleTimeout?.ToString("##s");
|
||||
transport.ping_timeout = _config.GrpcItem.HealthCheckTimeout?.ToString("##s");
|
||||
transport.permit_without_stream = _config.GrpcItem.PermitWithoutStream;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (transport.type != null)
|
||||
{
|
||||
outbound.transport = transport;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenMoreOutbounds(ProfileItem node, SingboxConfig singboxConfig)
|
||||
{
|
||||
if (node.Subid.IsNullOrEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
try
|
||||
{
|
||||
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
|
||||
if (subItem is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
//current proxy
|
||||
BaseServer4Sbox? outbound = singboxConfig.endpoints?.FirstOrDefault(t => t.tag == Global.ProxyTag, null);
|
||||
outbound ??= singboxConfig.outbounds.First();
|
||||
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
||||
|
||||
//Previous proxy
|
||||
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
|
||||
string? prevOutboundTag = null;
|
||||
if (prevNode is not null
|
||||
&& Global.SingboxSupportConfigType.Contains(prevNode.ConfigType))
|
||||
{
|
||||
prevOutboundTag = $"prev-{Global.ProxyTag}";
|
||||
var prevServer = await GenServer(prevNode);
|
||||
prevServer.tag = prevOutboundTag;
|
||||
if (prevServer is Endpoints4Sbox endpoint)
|
||||
{
|
||||
singboxConfig.endpoints ??= new();
|
||||
singboxConfig.endpoints.Add(endpoint);
|
||||
}
|
||||
else if (prevServer is Outbound4Sbox outboundPrev)
|
||||
{
|
||||
singboxConfig.outbounds.Add(outboundPrev);
|
||||
}
|
||||
}
|
||||
var nextServer = await GenChainOutbounds(subItem, outbound, prevOutboundTag);
|
||||
|
||||
if (nextServer is not null)
|
||||
{
|
||||
if (nextServer is Endpoints4Sbox endpoint)
|
||||
{
|
||||
singboxConfig.endpoints ??= new();
|
||||
singboxConfig.endpoints.Insert(0, endpoint);
|
||||
}
|
||||
else if (nextServer is Outbound4Sbox outboundNext)
|
||||
{
|
||||
singboxConfig.outbounds.Insert(0, outboundNext);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<int> GenOutboundsListWithChain(List<ProfileItem> nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get outbound template and initialize lists
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
||||
if (txtOutbound.IsNullOrEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var resultOutbounds = new List<Outbound4Sbox>();
|
||||
var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints
|
||||
var prevOutbounds = new List<Outbound4Sbox>(); // Separate list for prev outbounds
|
||||
var prevEndpoints = new List<Endpoints4Sbox>(); // Separate list for prev endpoints
|
||||
var proxyTags = new List<string>(); // For selector and urltest outbounds
|
||||
|
||||
// Cache for chain proxies to avoid duplicate generation
|
||||
var nextProxyCache = new Dictionary<string, BaseServer4Sbox?>();
|
||||
var prevProxyTags = new Dictionary<string, string?>(); // Map from profile name to tag
|
||||
var prevIndex = 0; // Index for prev outbounds
|
||||
|
||||
// Process each node
|
||||
var index = 0;
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
index++;
|
||||
|
||||
if (node.ConfigType.IsGroupType())
|
||||
{
|
||||
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
|
||||
if (childProfiles.Count <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var childBaseTagName = $"{baseTagName}-{index}";
|
||||
var ret = node.ConfigType switch
|
||||
{
|
||||
EConfigType.PolicyGroup =>
|
||||
await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName),
|
||||
EConfigType.ProxyChain =>
|
||||
await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
if (ret == 0)
|
||||
{
|
||||
proxyTags.Add(childBaseTagName);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle proxy chain
|
||||
string? prevTag = null;
|
||||
var currentServer = await GenServer(node);
|
||||
var nextServer = nextProxyCache.GetValueOrDefault(node.Subid, null);
|
||||
if (nextServer != null)
|
||||
{
|
||||
nextServer = JsonUtils.DeepCopy(nextServer);
|
||||
}
|
||||
|
||||
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
|
||||
|
||||
// current proxy
|
||||
currentServer.tag = $"{baseTagName}-{index}";
|
||||
proxyTags.Add(currentServer.tag);
|
||||
|
||||
if (!node.Subid.IsNullOrEmpty())
|
||||
{
|
||||
if (prevProxyTags.TryGetValue(node.Subid, out var value))
|
||||
{
|
||||
prevTag = value; // maybe null
|
||||
}
|
||||
else
|
||||
{
|
||||
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
|
||||
if (prevNode is not null
|
||||
&& Global.SingboxSupportConfigType.Contains(prevNode.ConfigType))
|
||||
{
|
||||
var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
|
||||
await GenOutbound(prevNode, prevOutbound);
|
||||
prevTag = $"prev-{baseTagName}-{++prevIndex}";
|
||||
prevOutbound.tag = prevTag;
|
||||
prevOutbounds.Add(prevOutbound);
|
||||
}
|
||||
prevProxyTags[node.Subid] = prevTag;
|
||||
}
|
||||
|
||||
nextServer = await GenChainOutbounds(subItem, currentServer, prevTag, nextServer);
|
||||
if (!nextProxyCache.ContainsKey(node.Subid))
|
||||
{
|
||||
nextProxyCache[node.Subid] = nextServer;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextServer is not null)
|
||||
{
|
||||
if (nextServer is Endpoints4Sbox nextEndpoint)
|
||||
{
|
||||
resultEndpoints.Add(nextEndpoint);
|
||||
}
|
||||
else if (nextServer is Outbound4Sbox nextOutbound)
|
||||
{
|
||||
resultOutbounds.Add(nextOutbound);
|
||||
}
|
||||
}
|
||||
if (currentServer is Endpoints4Sbox currentEndpoint)
|
||||
{
|
||||
resultEndpoints.Add(currentEndpoint);
|
||||
}
|
||||
else if (currentServer is Outbound4Sbox currentOutbound)
|
||||
{
|
||||
resultOutbounds.Add(currentOutbound);
|
||||
}
|
||||
}
|
||||
|
||||
// Add urltest outbound (auto selection based on latency)
|
||||
if (proxyTags.Count > 0)
|
||||
{
|
||||
var outUrltest = new Outbound4Sbox
|
||||
{
|
||||
type = "urltest",
|
||||
tag = $"{baseTagName}-auto",
|
||||
outbounds = proxyTags,
|
||||
interrupt_exist_connections = false,
|
||||
};
|
||||
|
||||
if (multipleLoad == EMultipleLoad.Fallback)
|
||||
{
|
||||
outUrltest.tolerance = 5000;
|
||||
}
|
||||
|
||||
// Add selector outbound (manual selection)
|
||||
var outSelector = new Outbound4Sbox
|
||||
{
|
||||
type = "selector",
|
||||
tag = baseTagName,
|
||||
outbounds = JsonUtils.DeepCopy(proxyTags),
|
||||
interrupt_exist_connections = false,
|
||||
};
|
||||
outSelector.outbounds.Insert(0, outUrltest.tag);
|
||||
|
||||
// Insert these at the beginning
|
||||
resultOutbounds.Insert(0, outUrltest);
|
||||
resultOutbounds.Insert(0, outSelector);
|
||||
}
|
||||
|
||||
// Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds
|
||||
var serverList = new List<BaseServer4Sbox>();
|
||||
serverList = serverList.Concat(prevOutbounds)
|
||||
.Concat(resultOutbounds)
|
||||
.Concat(resultEndpoints)
|
||||
.ToList();
|
||||
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<BaseServer4Sbox?> GenChainOutbounds(SubItem subItem, BaseServer4Sbox outbound, string? prevOutboundTag, BaseServer4Sbox? nextOutbound = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
||||
|
||||
if (!prevOutboundTag.IsNullOrEmpty())
|
||||
{
|
||||
outbound.detour = prevOutboundTag;
|
||||
}
|
||||
|
||||
// Next proxy
|
||||
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
|
||||
if (nextNode is not null
|
||||
&& Global.SingboxSupportConfigType.Contains(nextNode.ConfigType))
|
||||
{
|
||||
nextOutbound ??= await GenServer(nextNode);
|
||||
nextOutbound.tag = outbound.tag;
|
||||
|
||||
outbound.tag = $"mid-{outbound.tag}";
|
||||
nextOutbound.detour = outbound.tag;
|
||||
}
|
||||
return nextOutbound;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
|
||||
{
|
||||
var resultOutbounds = new List<Outbound4Sbox>();
|
||||
var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints
|
||||
var proxyTags = new List<string>(); // For selector and urltest outbounds
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var node = nodes[i];
|
||||
if (node == null)
|
||||
continue;
|
||||
if (node.ConfigType.IsGroupType())
|
||||
{
|
||||
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
|
||||
if (childProfiles.Count <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var childBaseTagName = $"{baseTagName}-{i + 1}";
|
||||
var ret = node.ConfigType switch
|
||||
{
|
||||
EConfigType.PolicyGroup =>
|
||||
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName),
|
||||
EConfigType.ProxyChain =>
|
||||
await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
if (ret == 0)
|
||||
{
|
||||
proxyTags.Add(childBaseTagName);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var server = await GenServer(node);
|
||||
if (server is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
server.tag = baseTagName + (i + 1).ToString();
|
||||
if (server is Endpoints4Sbox endpoint)
|
||||
{
|
||||
resultEndpoints.Add(endpoint);
|
||||
}
|
||||
else if (server is Outbound4Sbox outbound)
|
||||
{
|
||||
resultOutbounds.Add(outbound);
|
||||
}
|
||||
proxyTags.Add(server.tag);
|
||||
}
|
||||
// Add urltest outbound (auto selection based on latency)
|
||||
if (proxyTags.Count > 0)
|
||||
{
|
||||
var outUrltest = new Outbound4Sbox
|
||||
{
|
||||
type = "urltest",
|
||||
tag = $"{baseTagName}-auto",
|
||||
outbounds = proxyTags,
|
||||
interrupt_exist_connections = false,
|
||||
};
|
||||
if (multipleLoad == EMultipleLoad.Fallback)
|
||||
{
|
||||
outUrltest.tolerance = 5000;
|
||||
}
|
||||
// Add selector outbound (manual selection)
|
||||
var outSelector = new Outbound4Sbox
|
||||
{
|
||||
type = "selector",
|
||||
tag = baseTagName,
|
||||
outbounds = JsonUtils.DeepCopy(proxyTags),
|
||||
interrupt_exist_connections = false,
|
||||
};
|
||||
outSelector.outbounds.Insert(0, outUrltest.tag);
|
||||
// Insert these at the beginning
|
||||
resultOutbounds.Insert(0, outUrltest);
|
||||
resultOutbounds.Insert(0, outSelector);
|
||||
}
|
||||
var serverList = new List<BaseServer4Sbox>();
|
||||
serverList = serverList.Concat(resultOutbounds)
|
||||
.Concat(resultEndpoints)
|
||||
.ToList();
|
||||
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag)
|
||||
{
|
||||
// Based on actual network flow instead of data packets
|
||||
var nodesReverse = nodes.AsEnumerable().Reverse().ToList();
|
||||
var resultOutbounds = new List<Outbound4Sbox>();
|
||||
var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints
|
||||
for (var i = 0; i < nodesReverse.Count; i++)
|
||||
{
|
||||
var node = nodesReverse[i];
|
||||
var server = await GenServer(node);
|
||||
|
||||
if (server is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
server.tag = baseTagName;
|
||||
}
|
||||
else
|
||||
{
|
||||
server.tag = baseTagName + i.ToString();
|
||||
}
|
||||
|
||||
if (i != nodesReverse.Count - 1)
|
||||
{
|
||||
server.detour = baseTagName + (i + 1).ToString();
|
||||
}
|
||||
|
||||
if (server is Endpoints4Sbox endpoint)
|
||||
{
|
||||
resultEndpoints.Add(endpoint);
|
||||
}
|
||||
else if (server is Outbound4Sbox outbound)
|
||||
{
|
||||
resultOutbounds.Add(outbound);
|
||||
}
|
||||
}
|
||||
var serverList = new List<BaseServer4Sbox>();
|
||||
serverList = serverList.Concat(resultOutbounds)
|
||||
.Concat(resultEndpoints)
|
||||
.ToList();
|
||||
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> AddRangeOutbounds(List<BaseServer4Sbox> servers, SingboxConfig singboxConfig, bool prepend = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (servers is null || servers.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList();
|
||||
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList();
|
||||
singboxConfig.endpoints ??= new();
|
||||
if (prepend)
|
||||
{
|
||||
singboxConfig.outbounds.InsertRange(0, outbounds);
|
||||
singboxConfig.endpoints.InsertRange(0, endpoints);
|
||||
}
|
||||
else
|
||||
{
|
||||
singboxConfig.outbounds.AddRange(outbounds);
|
||||
singboxConfig.endpoints.AddRange(endpoints);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private async Task<int> GenRouting(SingboxConfig singboxConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
singboxConfig.route.final = Global.ProxyTag;
|
||||
var item = _config.SimpleDNSItem;
|
||||
|
||||
var defaultDomainResolverTag = Global.SingboxDirectDNSTag;
|
||||
var directDNSStrategy = item.SingboxStrategy4Direct.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : item.SingboxStrategy4Direct;
|
||||
|
||||
var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
|
||||
if (rawDNSItem != null && rawDNSItem.Enabled == true)
|
||||
{
|
||||
defaultDomainResolverTag = Global.SingboxLocalDNSTag;
|
||||
directDNSStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : rawDNSItem.DomainStrategy4Freedom;
|
||||
}
|
||||
singboxConfig.route.default_domain_resolver = new()
|
||||
{
|
||||
server = defaultDomainResolverTag,
|
||||
strategy = directDNSStrategy
|
||||
};
|
||||
|
||||
if (_config.TunModeItem.EnableTun)
|
||||
{
|
||||
singboxConfig.route.auto_detect_interface = true;
|
||||
|
||||
var tunRules = JsonUtils.Deserialize<List<Rule4Sbox>>(EmbedUtils.GetEmbedText(Global.TunSingboxRulesFileName));
|
||||
if (tunRules != null)
|
||||
{
|
||||
singboxConfig.route.rules.AddRange(tunRules);
|
||||
}
|
||||
|
||||
GenRoutingDirectExe(out var lstDnsExe, out var lstDirectExe);
|
||||
singboxConfig.route.rules.Add(new()
|
||||
{
|
||||
port = new() { 53 },
|
||||
action = "hijack-dns",
|
||||
process_name = lstDnsExe
|
||||
});
|
||||
|
||||
singboxConfig.route.rules.Add(new()
|
||||
{
|
||||
outbound = Global.DirectTag,
|
||||
process_name = lstDirectExe
|
||||
});
|
||||
}
|
||||
|
||||
if (_config.Inbound.First().SniffingEnabled)
|
||||
{
|
||||
singboxConfig.route.rules.Add(new()
|
||||
{
|
||||
action = "sniff"
|
||||
});
|
||||
singboxConfig.route.rules.Add(new()
|
||||
{
|
||||
protocol = new() { "dns" },
|
||||
action = "hijack-dns"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
singboxConfig.route.rules.Add(new()
|
||||
{
|
||||
port = new() { 53 },
|
||||
network = new() { "udp" },
|
||||
action = "hijack-dns"
|
||||
});
|
||||
}
|
||||
|
||||
var hostsDomains = new List<string>();
|
||||
var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
|
||||
if (dnsItem == null || dnsItem.Enabled == false)
|
||||
{
|
||||
var simpleDNSItem = _config.SimpleDNSItem;
|
||||
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
|
||||
{
|
||||
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
|
||||
foreach (var kvp in userHostsMap)
|
||||
{
|
||||
hostsDomains.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
if (simpleDNSItem.UseSystemHosts == true)
|
||||
{
|
||||
var systemHostsMap = Utils.GetSystemHosts();
|
||||
foreach (var kvp in systemHostsMap)
|
||||
{
|
||||
hostsDomains.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hostsDomains.Count > 0)
|
||||
{
|
||||
singboxConfig.route.rules.Add(new()
|
||||
{
|
||||
action = "resolve",
|
||||
domain = hostsDomains,
|
||||
});
|
||||
}
|
||||
|
||||
singboxConfig.route.rules.Add(new()
|
||||
{
|
||||
outbound = Global.DirectTag,
|
||||
clash_mode = ERuleMode.Direct.ToString()
|
||||
});
|
||||
singboxConfig.route.rules.Add(new()
|
||||
{
|
||||
outbound = Global.ProxyTag,
|
||||
clash_mode = ERuleMode.Global.ToString()
|
||||
});
|
||||
|
||||
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.IsNullOrEmpty() ? null : _config.RoutingBasicItem.DomainStrategy4Singbox;
|
||||
var defaultRouting = await ConfigHandler.GetDefaultRouting(_config);
|
||||
if (defaultRouting.DomainStrategy4Singbox.IsNotEmpty())
|
||||
{
|
||||
domainStrategy = defaultRouting.DomainStrategy4Singbox;
|
||||
}
|
||||
var resolveRule = new Rule4Sbox
|
||||
{
|
||||
action = "resolve",
|
||||
strategy = domainStrategy
|
||||
};
|
||||
if (_config.RoutingBasicItem.DomainStrategy == Global.IPOnDemand)
|
||||
{
|
||||
singboxConfig.route.rules.Add(resolveRule);
|
||||
}
|
||||
|
||||
var routing = await ConfigHandler.GetDefaultRouting(_config);
|
||||
var ipRules = new List<RulesItem>();
|
||||
if (routing != null)
|
||||
{
|
||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
|
||||
foreach (var item1 in rules ?? [])
|
||||
{
|
||||
if (!item1.Enabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item1.RuleType == ERuleType.DNS)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await GenRoutingUserRule(item1, singboxConfig);
|
||||
|
||||
if (item1.Ip?.Count > 0)
|
||||
{
|
||||
ipRules.Add(item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_config.RoutingBasicItem.DomainStrategy == Global.IPIfNonMatch)
|
||||
{
|
||||
singboxConfig.route.rules.Add(resolveRule);
|
||||
foreach (var item2 in ipRules)
|
||||
{
|
||||
await GenRoutingUserRule(item2, singboxConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void GenRoutingDirectExe(out List<string> lstDnsExe, out List<string> lstDirectExe)
|
||||
{
|
||||
var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo();
|
||||
|
||||
foreach (var coreConfig in coreInfoResult)
|
||||
{
|
||||
if (coreConfig.CoreType == ECoreType.v2rayN)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var baseExeName in coreConfig.CoreExes)
|
||||
{
|
||||
if (coreConfig.CoreType != ECoreType.sing_box)
|
||||
{
|
||||
dnsExeSet.Add(Utils.GetExeName(baseExeName));
|
||||
}
|
||||
directExeSet.Add(Utils.GetExeName(baseExeName));
|
||||
}
|
||||
}
|
||||
|
||||
lstDnsExe = new List<string>(dnsExeSet);
|
||||
lstDirectExe = new List<string>(directExeSet);
|
||||
}
|
||||
|
||||
private async Task<int> GenRoutingUserRule(RulesItem item, SingboxConfig singboxConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
item.OutboundTag = await GenRoutingUserRuleOutbound(item.OutboundTag, singboxConfig);
|
||||
var rules = singboxConfig.route.rules;
|
||||
|
||||
var rule = new Rule4Sbox();
|
||||
if (item.OutboundTag == "block")
|
||||
{
|
||||
rule.action = "reject";
|
||||
}
|
||||
else
|
||||
{
|
||||
rule.outbound = item.OutboundTag;
|
||||
}
|
||||
|
||||
if (item.Port.IsNotEmpty())
|
||||
{
|
||||
var portRanges = item.Port.Split(',').Where(it => it.Contains('-')).Select(it => it.Replace("-", ":")).ToList();
|
||||
var ports = item.Port.Split(',').Where(it => !it.Contains('-')).Select(it => it.ToInt()).ToList();
|
||||
|
||||
rule.port_range = portRanges.Count > 0 ? portRanges : null;
|
||||
rule.port = ports.Count > 0 ? ports : null;
|
||||
}
|
||||
if (item.Network.IsNotEmpty())
|
||||
{
|
||||
rule.network = Utils.String2List(item.Network);
|
||||
}
|
||||
if (item.Protocol?.Count > 0)
|
||||
{
|
||||
rule.protocol = item.Protocol;
|
||||
}
|
||||
if (item.InboundTag?.Count >= 0)
|
||||
{
|
||||
rule.inbound = item.InboundTag;
|
||||
}
|
||||
var rule1 = JsonUtils.DeepCopy(rule);
|
||||
var rule2 = JsonUtils.DeepCopy(rule);
|
||||
var rule3 = JsonUtils.DeepCopy(rule);
|
||||
|
||||
var hasDomainIp = false;
|
||||
if (item.Domain?.Count > 0)
|
||||
{
|
||||
var countDomain = 0;
|
||||
foreach (var it in item.Domain)
|
||||
{
|
||||
if (ParseV2Domain(it, rule1))
|
||||
countDomain++;
|
||||
}
|
||||
if (countDomain > 0)
|
||||
{
|
||||
rules.Add(rule1);
|
||||
hasDomainIp = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Ip?.Count > 0)
|
||||
{
|
||||
var countIp = 0;
|
||||
foreach (var it in item.Ip)
|
||||
{
|
||||
if (ParseV2Address(it, rule2))
|
||||
countIp++;
|
||||
}
|
||||
if (countIp > 0)
|
||||
{
|
||||
rules.Add(rule2);
|
||||
hasDomainIp = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (_config.TunModeItem.EnableTun && item.Process?.Count > 0)
|
||||
{
|
||||
rule3.process_name = item.Process;
|
||||
rules.Add(rule3);
|
||||
hasDomainIp = true;
|
||||
}
|
||||
|
||||
if (!hasDomainIp
|
||||
&& (rule.port != null || rule.port_range != null || rule.protocol != null || rule.inbound != null || rule.network != null))
|
||||
{
|
||||
rules.Add(rule);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private bool ParseV2Domain(string domain, Rule4Sbox rule)
|
||||
{
|
||||
if (domain.StartsWith("#") || domain.StartsWith("ext:") || domain.StartsWith("ext-domain:"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (domain.StartsWith("geosite:"))
|
||||
{
|
||||
rule.geosite ??= [];
|
||||
rule.geosite?.Add(domain.Substring(8));
|
||||
}
|
||||
else if (domain.StartsWith("regexp:"))
|
||||
{
|
||||
rule.domain_regex ??= [];
|
||||
rule.domain_regex?.Add(domain.Replace(Global.RoutingRuleComma, ",").Substring(7));
|
||||
}
|
||||
else if (domain.StartsWith("domain:"))
|
||||
{
|
||||
rule.domain ??= [];
|
||||
rule.domain_suffix ??= [];
|
||||
rule.domain?.Add(domain.Substring(7));
|
||||
rule.domain_suffix?.Add("." + domain.Substring(7));
|
||||
}
|
||||
else if (domain.StartsWith("full:"))
|
||||
{
|
||||
rule.domain ??= [];
|
||||
rule.domain?.Add(domain.Substring(5));
|
||||
}
|
||||
else if (domain.StartsWith("keyword:"))
|
||||
{
|
||||
rule.domain_keyword ??= [];
|
||||
rule.domain_keyword?.Add(domain.Substring(8));
|
||||
}
|
||||
else
|
||||
{
|
||||
rule.domain_keyword ??= [];
|
||||
rule.domain_keyword?.Add(domain);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ParseV2Address(string address, Rule4Sbox rule)
|
||||
{
|
||||
if (address.StartsWith("ext:") || address.StartsWith("ext-ip:"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (address.Equals("geoip:private"))
|
||||
{
|
||||
rule.ip_is_private = true;
|
||||
}
|
||||
else if (address.StartsWith("geoip:"))
|
||||
{
|
||||
rule.geoip ??= new();
|
||||
rule.geoip?.Add(address.Substring(6));
|
||||
}
|
||||
else if (address.Equals("geoip:!private"))
|
||||
{
|
||||
rule.ip_is_private = false;
|
||||
}
|
||||
else if (address.StartsWith("geoip:!"))
|
||||
{
|
||||
rule.geoip ??= new();
|
||||
rule.geoip?.Add(address.Substring(6));
|
||||
rule.invert = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
rule.ip_cidr ??= new();
|
||||
rule.ip_cidr?.Add(address);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<string?> GenRoutingUserRuleOutbound(string outboundTag, SingboxConfig singboxConfig)
|
||||
{
|
||||
if (Global.OutboundTags.Contains(outboundTag))
|
||||
{
|
||||
return outboundTag;
|
||||
}
|
||||
|
||||
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
|
||||
|
||||
if (node == null
|
||||
|| (!Global.SingboxSupportConfigType.Contains(node.ConfigType)
|
||||
&& !node.ConfigType.IsGroupType()))
|
||||
{
|
||||
return Global.ProxyTag;
|
||||
}
|
||||
|
||||
var tag = $"{node.IndexId}-{Global.ProxyTag}";
|
||||
if (singboxConfig.outbounds.Any(o => o.tag == tag)
|
||||
|| (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag)))
|
||||
{
|
||||
return tag;
|
||||
}
|
||||
|
||||
if (node.ConfigType.IsGroupType())
|
||||
{
|
||||
var ret = await GenGroupOutbound(node, singboxConfig, tag);
|
||||
if (ret == 0)
|
||||
{
|
||||
return tag;
|
||||
}
|
||||
return Global.ProxyTag;
|
||||
}
|
||||
|
||||
var server = await GenServer(node);
|
||||
if (server is null)
|
||||
{
|
||||
return Global.ProxyTag;
|
||||
}
|
||||
|
||||
server.tag = tag;
|
||||
if (server is Endpoints4Sbox endpoint)
|
||||
{
|
||||
singboxConfig.endpoints ??= new();
|
||||
singboxConfig.endpoints.Add(endpoint);
|
||||
}
|
||||
else if (server is Outbound4Sbox outbound)
|
||||
{
|
||||
singboxConfig.outbounds.Add(outbound);
|
||||
}
|
||||
|
||||
return server.tag;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private async Task<int> ConvertGeo2Ruleset(SingboxConfig singboxConfig)
|
||||
{
|
||||
static void AddRuleSets(List<string> ruleSets, List<string>? rule_set)
|
||||
{
|
||||
if (rule_set != null)
|
||||
ruleSets.AddRange(rule_set);
|
||||
}
|
||||
var geosite = "geosite";
|
||||
var geoip = "geoip";
|
||||
var ruleSets = new List<string>();
|
||||
|
||||
//convert route geosite & geoip to ruleset
|
||||
foreach (var rule in singboxConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
|
||||
{
|
||||
rule.rule_set ??= new List<string>();
|
||||
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
|
||||
rule.geosite = null;
|
||||
AddRuleSets(ruleSets, rule.rule_set);
|
||||
}
|
||||
foreach (var rule in singboxConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
|
||||
{
|
||||
rule.rule_set ??= new List<string>();
|
||||
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
|
||||
rule.geoip = null;
|
||||
AddRuleSets(ruleSets, rule.rule_set);
|
||||
}
|
||||
|
||||
//convert dns geosite & geoip to ruleset
|
||||
foreach (var rule in singboxConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
|
||||
{
|
||||
rule.rule_set ??= new List<string>();
|
||||
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
|
||||
rule.geosite = null;
|
||||
}
|
||||
foreach (var rule in singboxConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
|
||||
{
|
||||
rule.rule_set ??= new List<string>();
|
||||
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
|
||||
rule.geoip = null;
|
||||
}
|
||||
foreach (var dnsRule in singboxConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? [])
|
||||
{
|
||||
AddRuleSets(ruleSets, dnsRule.rule_set);
|
||||
}
|
||||
//rules in rules
|
||||
foreach (var item in singboxConfig.dns?.rules.Where(t => t.rules?.Count > 0).Select(t => t.rules).ToList() ?? [])
|
||||
{
|
||||
foreach (var item2 in item ?? [])
|
||||
{
|
||||
AddRuleSets(ruleSets, item2.rule_set);
|
||||
}
|
||||
}
|
||||
|
||||
//load custom ruleset file
|
||||
List<Ruleset4Sbox> customRulesets = [];
|
||||
|
||||
var routing = await ConfigHandler.GetDefaultRouting(_config);
|
||||
if (routing.CustomRulesetPath4Singbox.IsNotEmpty())
|
||||
{
|
||||
var result = EmbedUtils.LoadResource(routing.CustomRulesetPath4Singbox);
|
||||
if (result.IsNotEmpty())
|
||||
{
|
||||
customRulesets = (JsonUtils.Deserialize<List<Ruleset4Sbox>>(result) ?? [])
|
||||
.Where(t => t.tag != null)
|
||||
.Where(t => t.type != null)
|
||||
.Where(t => t.format != null)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
//Local srs files address
|
||||
var localSrss = Utils.GetBinPath("srss");
|
||||
|
||||
//Add ruleset srs
|
||||
singboxConfig.route.rule_set = [];
|
||||
foreach (var item in new HashSet<string>(ruleSets))
|
||||
{
|
||||
if (item.IsNullOrEmpty())
|
||||
{ continue; }
|
||||
var customRuleset = customRulesets.FirstOrDefault(t => t.tag != null && t.tag.Equals(item));
|
||||
if (customRuleset is null)
|
||||
{
|
||||
var pathSrs = Path.Combine(localSrss, $"{item}.srs");
|
||||
if (File.Exists(pathSrs))
|
||||
{
|
||||
customRuleset = new()
|
||||
{
|
||||
type = "local",
|
||||
format = "binary",
|
||||
tag = item,
|
||||
path = pathSrs
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl)
|
||||
? Global.SingboxRulesetUrl
|
||||
: _config.ConstItem.SrsSourceUrl;
|
||||
|
||||
customRuleset = new()
|
||||
{
|
||||
type = "remote",
|
||||
format = "binary",
|
||||
tag = item,
|
||||
url = string.Format(srsUrl, item.StartsWith(geosite) ? geosite : geoip, item),
|
||||
download_detour = Global.ProxyTag
|
||||
};
|
||||
}
|
||||
}
|
||||
singboxConfig.route.rule_set.Add(customRuleset);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private async Task<int> GenExperimental(SingboxConfig singboxConfig)
|
||||
{
|
||||
//if (_config.guiItem.enableStatistics)
|
||||
{
|
||||
singboxConfig.experimental ??= new Experimental4Sbox();
|
||||
singboxConfig.experimental.clash_api = new Clash_Api4Sbox()
|
||||
{
|
||||
external_controller = $"{Global.Loopback}:{AppManager.Instance.StatePort2}",
|
||||
};
|
||||
}
|
||||
|
||||
if (_config.CoreBasicItem.EnableCacheFile4Sbox)
|
||||
{
|
||||
singboxConfig.experimental ??= new Experimental4Sbox();
|
||||
singboxConfig.experimental.cache_file = new CacheFile4Sbox()
|
||||
{
|
||||
enabled = true,
|
||||
path = Utils.GetBinPath("cache.db"),
|
||||
store_fakeip = _config.SimpleDNSItem.FakeIP == true
|
||||
};
|
||||
}
|
||||
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigV2rayService(Config config)
|
||||
{
|
||||
private readonly Config _config = config;
|
||||
private static readonly string _tag = "CoreConfigV2rayService";
|
||||
|
||||
#region public gen function
|
||||
|
||||
public async Task<RetResult> GenerateClientConfigContent(ProfileItem node)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
try
|
||||
{
|
||||
if (node == null
|
||||
|| !node.IsValid())
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (node.GetNetwork() is nameof(ETransport.quic))
|
||||
{
|
||||
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}";
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
if (node.ConfigType.IsGroupType())
|
||||
{
|
||||
switch (node.ConfigType)
|
||||
{
|
||||
case EConfigType.PolicyGroup:
|
||||
return await GenerateClientMultipleLoadConfig(node);
|
||||
|
||||
case EConfigType.ProxyChain:
|
||||
return await GenerateClientChainConfig(node);
|
||||
}
|
||||
}
|
||||
|
||||
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
|
||||
if (v2rayConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
await GenLog(v2rayConfig);
|
||||
|
||||
await GenInbounds(v2rayConfig);
|
||||
|
||||
await GenOutbound(node, v2rayConfig.outbounds.First());
|
||||
|
||||
await GenMoreOutbounds(node, v2rayConfig);
|
||||
|
||||
await GenRouting(v2rayConfig);
|
||||
|
||||
await GenDns(node, v2rayConfig);
|
||||
|
||||
await GenStatistic(v2rayConfig);
|
||||
|
||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||
ret.Success = true;
|
||||
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientMultipleLoadConfig(ProfileItem parentNode)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
|
||||
try
|
||||
{
|
||||
if (_config == null)
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
|
||||
string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
|
||||
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
|
||||
if (v2rayConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
v2rayConfig.outbounds.RemoveAt(0);
|
||||
|
||||
await GenLog(v2rayConfig);
|
||||
await GenInbounds(v2rayConfig);
|
||||
|
||||
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
|
||||
if (groupRet != 0)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
await GenRouting(v2rayConfig);
|
||||
await GenDns(null, v2rayConfig);
|
||||
await GenStatistic(v2rayConfig);
|
||||
|
||||
var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}";
|
||||
|
||||
//add rule
|
||||
var rules = v2rayConfig.routing.rules;
|
||||
if (rules?.Count > 0 && ((v2rayConfig.routing.balancers?.Count ?? 0) > 0))
|
||||
{
|
||||
var balancerTagSet = v2rayConfig.routing.balancers
|
||||
.Select(b => b.tag)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (rule.outboundTag == null)
|
||||
continue;
|
||||
|
||||
if (balancerTagSet.Contains(rule.outboundTag))
|
||||
{
|
||||
rule.balancerTag = rule.outboundTag;
|
||||
rule.outboundTag = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
var outboundWithSuffix = rule.outboundTag + Global.BalancerTagSuffix;
|
||||
if (balancerTagSet.Contains(outboundWithSuffix))
|
||||
{
|
||||
rule.balancerTag = outboundWithSuffix;
|
||||
rule.outboundTag = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
|
||||
{
|
||||
v2rayConfig.routing.rules.Add(new()
|
||||
{
|
||||
ip = ["0.0.0.0/0", "::/0"],
|
||||
balancerTag = defaultBalancerTag,
|
||||
type = "field"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
v2rayConfig.routing.rules.Add(new()
|
||||
{
|
||||
network = "tcp,udp",
|
||||
balancerTag = defaultBalancerTag,
|
||||
type = "field"
|
||||
});
|
||||
}
|
||||
|
||||
ret.Success = true;
|
||||
|
||||
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientChainConfig(ProfileItem parentNode)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
|
||||
try
|
||||
{
|
||||
if (_config == null)
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
|
||||
string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
|
||||
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
|
||||
if (v2rayConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
v2rayConfig.outbounds.RemoveAt(0);
|
||||
|
||||
await GenLog(v2rayConfig);
|
||||
await GenInbounds(v2rayConfig);
|
||||
|
||||
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
|
||||
if (groupRet != 0)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
await GenRouting(v2rayConfig);
|
||||
await GenDns(null, v2rayConfig);
|
||||
await GenStatistic(v2rayConfig);
|
||||
|
||||
ret.Success = true;
|
||||
|
||||
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientSpeedtestConfig(List<ServerTestItem> selecteds)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
try
|
||||
{
|
||||
if (_config == null)
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.Msg = ResUI.InitialConfiguration;
|
||||
|
||||
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
|
||||
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
|
||||
if (v2rayConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
List<IPEndPoint> lstIpEndPoints = new();
|
||||
List<TcpConnectionInformation> lstTcpConns = new();
|
||||
try
|
||||
{
|
||||
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners());
|
||||
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners());
|
||||
lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
await GenLog(v2rayConfig);
|
||||
v2rayConfig.inbounds.Clear();
|
||||
v2rayConfig.outbounds.Clear();
|
||||
v2rayConfig.routing.rules.Clear();
|
||||
|
||||
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
|
||||
|
||||
foreach (var it in selecteds)
|
||||
{
|
||||
if (!Global.XraySupportConfigType.Contains(it.ConfigType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (it.Port <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
|
||||
if (item is null || item.IsComplex() || !item.IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//find unused port
|
||||
var port = initPort;
|
||||
for (var k = initPort; k < Global.MaxPort; k++)
|
||||
{
|
||||
if (lstIpEndPoints?.FindIndex(_it => _it.Port == k) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (lstTcpConns?.FindIndex(_it => _it.LocalEndPoint.Port == k) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//found
|
||||
port = k;
|
||||
initPort = port + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
//Port In Used
|
||||
if (lstIpEndPoints?.FindIndex(_it => _it.Port == port) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
it.Port = port;
|
||||
it.AllowTest = true;
|
||||
|
||||
//inbound
|
||||
Inbounds4Ray inbound = new()
|
||||
{
|
||||
listen = Global.Loopback,
|
||||
port = port,
|
||||
protocol = EInboundProtocol.mixed.ToString(),
|
||||
};
|
||||
inbound.tag = inbound.protocol + inbound.port.ToString();
|
||||
v2rayConfig.inbounds.Add(inbound);
|
||||
|
||||
//outbound
|
||||
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
|
||||
await GenOutbound(item, outbound);
|
||||
outbound.tag = Global.ProxyTag + inbound.port.ToString();
|
||||
v2rayConfig.outbounds.Add(outbound);
|
||||
|
||||
//rule
|
||||
RulesItem4Ray rule = new()
|
||||
{
|
||||
inboundTag = new List<string> { inbound.tag },
|
||||
outboundTag = outbound.tag,
|
||||
type = "field"
|
||||
};
|
||||
v2rayConfig.routing.rules.Add(rule);
|
||||
}
|
||||
|
||||
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
|
||||
ret.Success = true;
|
||||
ret.Data = JsonUtils.Serialize(v2rayConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RetResult> GenerateClientSpeedtestConfig(ProfileItem node, int port)
|
||||
{
|
||||
var ret = new RetResult();
|
||||
try
|
||||
{
|
||||
if (node == null
|
||||
|| !node.IsValid())
|
||||
{
|
||||
ret.Msg = ResUI.CheckServerSettings;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (node.GetNetwork() is nameof(ETransport.quic))
|
||||
{
|
||||
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}";
|
||||
return ret;
|
||||
}
|
||||
|
||||
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
ret.Msg = ResUI.FailedGetDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
|
||||
if (v2rayConfig == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
|
||||
await GenLog(v2rayConfig);
|
||||
await GenOutbound(node, v2rayConfig.outbounds.First());
|
||||
await GenMoreOutbounds(node, v2rayConfig);
|
||||
|
||||
v2rayConfig.routing.rules.Clear();
|
||||
v2rayConfig.inbounds.Clear();
|
||||
v2rayConfig.inbounds.Add(new()
|
||||
{
|
||||
tag = $"{EInboundProtocol.socks}{port}",
|
||||
listen = Global.Loopback,
|
||||
port = port,
|
||||
protocol = EInboundProtocol.mixed.ToString(),
|
||||
});
|
||||
|
||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||
ret.Success = true;
|
||||
ret.Data = JsonUtils.Serialize(v2rayConfig);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
ret.Msg = ResUI.FailedGenDefaultConfiguration;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion public gen function
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigV2rayService
|
||||
{
|
||||
private async Task<int> GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
|
||||
{
|
||||
// Collect all existing subject selectors from both observatories
|
||||
var subjectSelectors = new List<string>();
|
||||
subjectSelectors.AddRange(v2rayConfig.burstObservatory?.subjectSelector ?? []);
|
||||
subjectSelectors.AddRange(v2rayConfig.observatory?.subjectSelector ?? []);
|
||||
|
||||
// Case 1: exact match already exists -> nothing to do
|
||||
if (subjectSelectors.Any(baseTagName.StartsWith))
|
||||
return await Task.FromResult(0);
|
||||
|
||||
// Case 2: prefix match exists -> reuse it and move to the first position
|
||||
var matched = subjectSelectors.FirstOrDefault(s => s.StartsWith(baseTagName));
|
||||
if (matched is not null)
|
||||
{
|
||||
baseTagName = matched;
|
||||
|
||||
if (v2rayConfig.burstObservatory?.subjectSelector?.Contains(baseTagName) == true)
|
||||
{
|
||||
v2rayConfig.burstObservatory.subjectSelector.Remove(baseTagName);
|
||||
v2rayConfig.burstObservatory.subjectSelector.Insert(0, baseTagName);
|
||||
}
|
||||
|
||||
if (v2rayConfig.observatory?.subjectSelector?.Contains(baseTagName) == true)
|
||||
{
|
||||
v2rayConfig.observatory.subjectSelector.Remove(baseTagName);
|
||||
v2rayConfig.observatory.subjectSelector.Insert(0, baseTagName);
|
||||
}
|
||||
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
// Case 3: need to create or insert based on multipleLoad type
|
||||
if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback)
|
||||
{
|
||||
if (v2rayConfig.burstObservatory is null)
|
||||
{
|
||||
// Create new burst observatory with default ping config
|
||||
v2rayConfig.burstObservatory = new BurstObservatory4Ray
|
||||
{
|
||||
subjectSelector = [baseTagName],
|
||||
pingConfig = new()
|
||||
{
|
||||
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
|
||||
interval = "5m",
|
||||
timeout = "30s",
|
||||
sampling = 2,
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
v2rayConfig.burstObservatory.subjectSelector ??= new();
|
||||
v2rayConfig.burstObservatory.subjectSelector.Add(baseTagName);
|
||||
}
|
||||
}
|
||||
else if (multipleLoad is EMultipleLoad.LeastPing)
|
||||
{
|
||||
if (v2rayConfig.observatory is null)
|
||||
{
|
||||
// Create new observatory with default probe config
|
||||
v2rayConfig.observatory = new Observatory4Ray
|
||||
{
|
||||
subjectSelector = [baseTagName],
|
||||
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
|
||||
probeInterval = "3m",
|
||||
enableConcurrency = true,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
v2rayConfig.observatory.subjectSelector ??= new();
|
||||
v2rayConfig.observatory.subjectSelector.Add(baseTagName);
|
||||
}
|
||||
}
|
||||
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<string> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string selector = Global.ProxyTag)
|
||||
{
|
||||
var strategyType = multipleLoad switch
|
||||
{
|
||||
EMultipleLoad.Random => "random",
|
||||
EMultipleLoad.RoundRobin => "roundRobin",
|
||||
EMultipleLoad.LeastPing => "leastPing",
|
||||
EMultipleLoad.LeastLoad => "leastLoad",
|
||||
_ => "roundRobin",
|
||||
};
|
||||
var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
|
||||
var balancer = new BalancersItem4Ray
|
||||
{
|
||||
selector = [selector],
|
||||
strategy = new()
|
||||
{
|
||||
type = strategyType,
|
||||
settings = new()
|
||||
{
|
||||
expected = 1,
|
||||
},
|
||||
},
|
||||
tag = balancerTag,
|
||||
};
|
||||
v2rayConfig.routing.balancers ??= new();
|
||||
v2rayConfig.routing.balancers.Add(balancer);
|
||||
return await Task.FromResult(balancerTag);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigV2rayService
|
||||
{
|
||||
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig)
|
||||
{
|
||||
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
|
||||
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty())
|
||||
{
|
||||
return JsonUtils.Serialize(v2rayConfig);
|
||||
}
|
||||
|
||||
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplate.Config);
|
||||
if (fullConfigTemplateNode == null)
|
||||
{
|
||||
return JsonUtils.Serialize(v2rayConfig);
|
||||
}
|
||||
|
||||
// Handle balancer and rules modifications (for multiple load scenarios)
|
||||
if (v2rayConfig.routing?.balancers?.Count > 0)
|
||||
{
|
||||
var balancer = v2rayConfig.routing.balancers.First();
|
||||
|
||||
// Modify existing rules in custom config
|
||||
var rulesNode = fullConfigTemplateNode["routing"]?["rules"];
|
||||
if (rulesNode != null)
|
||||
{
|
||||
foreach (var rule in rulesNode.AsArray())
|
||||
{
|
||||
if (rule["outboundTag"]?.GetValue<string>() == Global.ProxyTag)
|
||||
{
|
||||
rule.AsObject().Remove("outboundTag");
|
||||
rule["balancerTag"] = balancer.tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure routing node exists
|
||||
if (fullConfigTemplateNode["routing"] == null)
|
||||
{
|
||||
fullConfigTemplateNode["routing"] = new JsonObject();
|
||||
}
|
||||
|
||||
// Handle balancers - append instead of override
|
||||
if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode)
|
||||
{
|
||||
if (JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers)) is JsonArray newBalancers)
|
||||
{
|
||||
foreach (var balancerNode in newBalancers)
|
||||
{
|
||||
customBalancersNode.Add(balancerNode?.DeepClone());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fullConfigTemplateNode["routing"]["balancers"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers));
|
||||
}
|
||||
}
|
||||
|
||||
if (v2rayConfig.observatory != null)
|
||||
{
|
||||
if (fullConfigTemplateNode["observatory"] == null)
|
||||
{
|
||||
fullConfigTemplateNode["observatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.observatory));
|
||||
}
|
||||
else
|
||||
{
|
||||
var subjectSelector = v2rayConfig.observatory.subjectSelector;
|
||||
subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
|
||||
fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
|
||||
}
|
||||
}
|
||||
|
||||
if (v2rayConfig.burstObservatory != null)
|
||||
{
|
||||
if (fullConfigTemplateNode["burstObservatory"] == null)
|
||||
{
|
||||
fullConfigTemplateNode["burstObservatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.burstObservatory));
|
||||
}
|
||||
else
|
||||
{
|
||||
var subjectSelector = v2rayConfig.burstObservatory.subjectSelector;
|
||||
subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
|
||||
fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle outbounds - append instead of override
|
||||
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
|
||||
foreach (var outbound in v2rayConfig.outbounds)
|
||||
{
|
||||
if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom")
|
||||
{
|
||||
if (fullConfigTemplate.AddProxyOnly == true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if ((outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() == true) && (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) && !(Utils.IsPrivateNetwork(outbound.settings?.servers?.FirstOrDefault()?.address ?? string.Empty) || Utils.IsPrivateNetwork(outbound.settings?.vnext?.FirstOrDefault()?.address ?? string.Empty)))
|
||||
{
|
||||
outbound.streamSettings ??= new StreamSettings4Ray();
|
||||
outbound.streamSettings.sockopt ??= new Sockopt4Ray();
|
||||
outbound.streamSettings.sockopt.dialerProxy = fullConfigTemplate.ProxyDetour;
|
||||
}
|
||||
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
|
||||
}
|
||||
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
|
||||
|
||||
return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode));
|
||||
}
|
||||
}
|
||||
405
v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
Normal file
405
v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
Normal file
@@ -0,0 +1,405 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigV2rayService
|
||||
{
|
||||
private async Task<int> GenDns(ProfileItem? node, V2rayConfig v2rayConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray);
|
||||
if (item != null && item.Enabled == true)
|
||||
{
|
||||
var result = await GenDnsCompatible(node, v2rayConfig);
|
||||
|
||||
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
|
||||
{
|
||||
// DNS routing
|
||||
v2rayConfig.dns.tag = Global.DnsTag;
|
||||
v2rayConfig.routing.rules.Add(new RulesItem4Ray
|
||||
{
|
||||
type = "field",
|
||||
inboundTag = new List<string> { Global.DnsTag },
|
||||
outboundTag = Global.ProxyTag,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
var simpleDNSItem = _config.SimpleDNSItem;
|
||||
var domainStrategy4Freedom = simpleDNSItem?.RayStrategy4Freedom;
|
||||
|
||||
//Outbound Freedom domainStrategy
|
||||
if (domainStrategy4Freedom.IsNotEmpty())
|
||||
{
|
||||
var outbound = v2rayConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
|
||||
if (outbound != null)
|
||||
{
|
||||
outbound.settings = new()
|
||||
{
|
||||
domainStrategy = domainStrategy4Freedom,
|
||||
userLevel = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await GenDnsServers(node, v2rayConfig, simpleDNSItem);
|
||||
await GenDnsHosts(v2rayConfig, simpleDNSItem);
|
||||
|
||||
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
|
||||
{
|
||||
// DNS routing
|
||||
v2rayConfig.dns.tag = Global.DnsTag;
|
||||
v2rayConfig.routing.rules.Add(new RulesItem4Ray
|
||||
{
|
||||
type = "field",
|
||||
inboundTag = new List<string> { Global.DnsTag },
|
||||
outboundTag = Global.ProxyTag,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsServers(ProfileItem? node, V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem)
|
||||
{
|
||||
static List<string> ParseDnsAddresses(string? dnsInput, string defaultAddress)
|
||||
{
|
||||
var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';')
|
||||
.Select(addr => addr.Trim())
|
||||
.Where(addr => !string.IsNullOrEmpty(addr))
|
||||
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
|
||||
.Distinct()
|
||||
.ToList() ?? new List<string> { defaultAddress };
|
||||
return addresses.Count > 0 ? addresses : new List<string> { defaultAddress };
|
||||
}
|
||||
|
||||
static object CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
|
||||
{
|
||||
var dnsServer = new DnsServer4Ray
|
||||
{
|
||||
address = dnsAddress,
|
||||
skipFallback = true,
|
||||
domains = domains.Count > 0 ? domains : null,
|
||||
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
|
||||
};
|
||||
return JsonUtils.SerializeToNode(dnsServer, new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
});
|
||||
}
|
||||
|
||||
var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.FirstOrDefault());
|
||||
var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.FirstOrDefault());
|
||||
|
||||
var directDomainList = new List<string>();
|
||||
var directGeositeList = new List<string>();
|
||||
var proxyDomainList = new List<string>();
|
||||
var proxyGeositeList = new List<string>();
|
||||
var expectedDomainList = new List<string>();
|
||||
var expectedIPs = new List<string>();
|
||||
var regionNames = new HashSet<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs))
|
||||
{
|
||||
expectedIPs = simpleDNSItem.DirectExpectedIPs
|
||||
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToList();
|
||||
|
||||
foreach (var ip in expectedIPs)
|
||||
{
|
||||
if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var region = ip["geoip:".Length..];
|
||||
if (!string.IsNullOrEmpty(region))
|
||||
{
|
||||
regionNames.Add($"geosite:{region}");
|
||||
regionNames.Add($"geosite:geolocation-{region}");
|
||||
regionNames.Add($"geosite:tld-{region}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var routing = await ConfigHandler.GetDefaultRouting(_config);
|
||||
List<RulesItem>? rules = null;
|
||||
if (routing != null)
|
||||
{
|
||||
rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
|
||||
foreach (var item in rules)
|
||||
{
|
||||
if (!item.Enabled || item.Domain is null || item.Domain.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.RuleType == ERuleType.Routing)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var domain in item.Domain)
|
||||
{
|
||||
if (domain.StartsWith('#'))
|
||||
continue;
|
||||
var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ",");
|
||||
|
||||
if (item.OutboundTag == Global.DirectTag)
|
||||
{
|
||||
if (normalizedDomain.StartsWith("geosite:"))
|
||||
{
|
||||
(regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain);
|
||||
}
|
||||
else
|
||||
{
|
||||
directDomainList.Add(normalizedDomain);
|
||||
}
|
||||
}
|
||||
else if (item.OutboundTag != Global.BlockTag)
|
||||
{
|
||||
if (normalizedDomain.StartsWith("geosite:"))
|
||||
{
|
||||
proxyGeositeList.Add(normalizedDomain);
|
||||
}
|
||||
else
|
||||
{
|
||||
proxyDomainList.Add(normalizedDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Utils.IsDomain(node?.Address))
|
||||
{
|
||||
directDomainList.Add(node.Address);
|
||||
}
|
||||
|
||||
if (node?.Subid is not null)
|
||||
{
|
||||
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
|
||||
if (subItem is not null)
|
||||
{
|
||||
foreach (var profile in new[] { subItem.PrevProfile, subItem.NextProfile })
|
||||
{
|
||||
var profileNode = await AppManager.Instance.GetProfileItemViaRemarks(profile);
|
||||
if (profileNode is not null
|
||||
&& Global.XraySupportConfigType.Contains(profileNode.ConfigType)
|
||||
&& Utils.IsDomain(profileNode.Address))
|
||||
{
|
||||
directDomainList.Add(profileNode.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v2rayConfig.dns ??= new Dns4Ray();
|
||||
v2rayConfig.dns.servers ??= new List<object>();
|
||||
|
||||
void AddDnsServers(List<string> dnsAddresses, List<string> domains, List<string>? expectedIPs = null)
|
||||
{
|
||||
if (domains.Count > 0)
|
||||
{
|
||||
foreach (var dnsAddress in dnsAddresses)
|
||||
{
|
||||
v2rayConfig.dns.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddDnsServers(remoteDNSAddress, proxyDomainList);
|
||||
AddDnsServers(directDNSAddress, directDomainList);
|
||||
AddDnsServers(remoteDNSAddress, proxyGeositeList);
|
||||
AddDnsServers(directDNSAddress, directGeositeList);
|
||||
AddDnsServers(directDNSAddress, expectedDomainList, expectedIPs);
|
||||
|
||||
var useDirectDns = rules?.LastOrDefault() is { } lastRule
|
||||
&& lastRule.OutboundTag == Global.DirectTag
|
||||
&& (lastRule.Port == "0-65535"
|
||||
|| lastRule.Network == "tcp,udp"
|
||||
|| lastRule.Ip?.Contains("0.0.0.0/0") == true);
|
||||
|
||||
var defaultDnsServers = useDirectDns ? directDNSAddress : remoteDNSAddress;
|
||||
v2rayConfig.dns.servers.AddRange(defaultDnsServers);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsHosts(V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem)
|
||||
{
|
||||
if (simpleDNSItem.AddCommonHosts == false && simpleDNSItem.UseSystemHosts == false && simpleDNSItem.Hosts.IsNullOrEmpty())
|
||||
{
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
v2rayConfig.dns ??= new Dns4Ray();
|
||||
v2rayConfig.dns.hosts ??= new Dictionary<string, object>();
|
||||
if (simpleDNSItem.AddCommonHosts == true)
|
||||
{
|
||||
v2rayConfig.dns.hosts = Global.PredefinedHosts.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => (object)kvp.Value
|
||||
);
|
||||
}
|
||||
|
||||
if (simpleDNSItem.UseSystemHosts == true)
|
||||
{
|
||||
var systemHosts = Utils.GetSystemHosts();
|
||||
var normalHost = v2rayConfig?.dns?.hosts;
|
||||
|
||||
if (normalHost != null && systemHosts?.Count > 0)
|
||||
{
|
||||
foreach (var host in systemHosts)
|
||||
{
|
||||
normalHost.TryAdd(host.Key, new List<string> { host.Value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
|
||||
{
|
||||
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
|
||||
|
||||
foreach (var kvp in userHostsMap)
|
||||
{
|
||||
v2rayConfig.dns.hosts[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsCompatible(ProfileItem? node, V2rayConfig v2rayConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray);
|
||||
var normalDNS = item?.NormalDNS;
|
||||
var domainStrategy4Freedom = item?.DomainStrategy4Freedom;
|
||||
if (normalDNS.IsNullOrEmpty())
|
||||
{
|
||||
normalDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName);
|
||||
}
|
||||
|
||||
//Outbound Freedom domainStrategy
|
||||
if (domainStrategy4Freedom.IsNotEmpty())
|
||||
{
|
||||
var outbound = v2rayConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
|
||||
if (outbound != null)
|
||||
{
|
||||
outbound.settings = new();
|
||||
outbound.settings.domainStrategy = domainStrategy4Freedom;
|
||||
outbound.settings.userLevel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var obj = JsonUtils.ParseJson(normalDNS);
|
||||
if (obj is null)
|
||||
{
|
||||
List<string> servers = [];
|
||||
string[] arrDNS = normalDNS.Split(',');
|
||||
foreach (string str in arrDNS)
|
||||
{
|
||||
servers.Add(str);
|
||||
}
|
||||
obj = JsonUtils.ParseJson("{}");
|
||||
obj["servers"] = JsonUtils.SerializeToNode(servers);
|
||||
}
|
||||
|
||||
// Append to dns settings
|
||||
if (item.UseSystemHosts)
|
||||
{
|
||||
var systemHosts = Utils.GetSystemHosts();
|
||||
if (systemHosts.Count > 0)
|
||||
{
|
||||
var normalHost1 = obj["hosts"];
|
||||
if (normalHost1 != null)
|
||||
{
|
||||
foreach (var host in systemHosts)
|
||||
{
|
||||
if (normalHost1[host.Key] != null)
|
||||
continue;
|
||||
normalHost1[host.Key] = host.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var normalHost = obj["hosts"];
|
||||
if (normalHost != null)
|
||||
{
|
||||
foreach (var hostProp in normalHost.AsObject().ToList())
|
||||
{
|
||||
if (hostProp.Value is JsonValue value && value.TryGetValue<string>(out var ip))
|
||||
{
|
||||
normalHost[hostProp.Key] = new JsonArray(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await GenDnsDomainsCompatible(node, obj, item);
|
||||
|
||||
v2rayConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<int> GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dnsItem)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var servers = dns["servers"];
|
||||
if (servers != null)
|
||||
{
|
||||
var domainList = new List<string>();
|
||||
if (Utils.IsDomain(node.Address))
|
||||
{
|
||||
domainList.Add(node.Address);
|
||||
}
|
||||
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
|
||||
if (subItem is not null)
|
||||
{
|
||||
// Previous proxy
|
||||
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
|
||||
if (prevNode is not null
|
||||
&& Global.SingboxSupportConfigType.Contains(prevNode.ConfigType)
|
||||
&& Utils.IsDomain(prevNode.Address))
|
||||
{
|
||||
domainList.Add(prevNode.Address);
|
||||
}
|
||||
|
||||
// Next proxy
|
||||
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
|
||||
if (nextNode is not null
|
||||
&& Global.SingboxSupportConfigType.Contains(nextNode.ConfigType)
|
||||
&& Utils.IsDomain(nextNode.Address))
|
||||
{
|
||||
domainList.Add(nextNode.Address);
|
||||
}
|
||||
}
|
||||
if (domainList.Count > 0)
|
||||
{
|
||||
var dnsServer = new DnsServer4Ray()
|
||||
{
|
||||
address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress,
|
||||
skipFallback = true,
|
||||
domains = domainList
|
||||
};
|
||||
servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer));
|
||||
}
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigV2rayService
|
||||
{
|
||||
private async Task<int> GenInbounds(V2rayConfig v2rayConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
var listen = "0.0.0.0";
|
||||
v2rayConfig.inbounds = [];
|
||||
|
||||
var inbound = GetInbound(_config.Inbound.First(), EInboundProtocol.socks, true);
|
||||
v2rayConfig.inbounds.Add(inbound);
|
||||
|
||||
if (_config.Inbound.First().SecondLocalPortEnabled)
|
||||
{
|
||||
var inbound2 = GetInbound(_config.Inbound.First(), EInboundProtocol.socks2, true);
|
||||
v2rayConfig.inbounds.Add(inbound2);
|
||||
}
|
||||
|
||||
if (_config.Inbound.First().AllowLANConn)
|
||||
{
|
||||
if (_config.Inbound.First().NewPort4LAN)
|
||||
{
|
||||
var inbound3 = GetInbound(_config.Inbound.First(), EInboundProtocol.socks3, true);
|
||||
inbound3.listen = listen;
|
||||
v2rayConfig.inbounds.Add(inbound3);
|
||||
|
||||
//auth
|
||||
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
|
||||
{
|
||||
inbound3.settings.auth = "password";
|
||||
inbound3.settings.accounts = new List<AccountsItem4Ray> { new AccountsItem4Ray() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
inbound.listen = listen;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private Inbounds4Ray GetInbound(InItem inItem, EInboundProtocol protocol, bool bSocks)
|
||||
{
|
||||
string result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound);
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
var inbound = JsonUtils.Deserialize<Inbounds4Ray>(result);
|
||||
if (inbound == null)
|
||||
{
|
||||
return new();
|
||||
}
|
||||
inbound.tag = protocol.ToString();
|
||||
inbound.port = inItem.LocalPort + (int)protocol;
|
||||
inbound.protocol = EInboundProtocol.mixed.ToString();
|
||||
inbound.settings.udp = inItem.UdpEnabled;
|
||||
inbound.sniffing.enabled = inItem.SniffingEnabled;
|
||||
inbound.sniffing.destOverride = inItem.DestOverride;
|
||||
inbound.sniffing.routeOnly = inItem.RouteOnly;
|
||||
|
||||
return inbound;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user