diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index a9fe23e2..40e9c953 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -26,13 +26,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: submodules: 'recursive' 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 \ No newline at end of file + 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 diff --git a/.github/workflows/build-osx.yml b/.github/workflows/build-osx.yml index 78c18dc5..97c002c7 100644 --- a/.github/workflows/build-osx.yml +++ b/.github/workflows/build-osx.yml @@ -26,13 +26,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: submodules: 'recursive' 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' diff --git a/.github/workflows/build-windows-desktop.yml b/.github/workflows/build-windows-desktop.yml index 9afc504d..3b28599d 100644 --- a/.github/workflows/build-windows-desktop.yml +++ b/.github/workflows/build-windows-desktop.yml @@ -26,13 +26,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: submodules: 'recursive' 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' diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 1d33f85a..fea3aa70 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -27,10 +27,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + 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' diff --git a/.gitignore b/.gitignore index 524d236b..7d5416b6 100644 --- a/.gitignore +++ b/.gitignore @@ -397,4 +397,5 @@ FodyWeavers.xsd *.msp # JetBrains Rider -*.sln.iml \ No newline at end of file +.idea/ +*.sln.iml diff --git a/package-appimage.sh b/package-appimage.sh index 77df287b..6a8bfcca 100644 --- a/package-appimage.sh +++ b/package-appimage.sh @@ -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" < "$APPDIR_ARM64/AppRun" +chmod +x "$APPDIR_ARM64/AppRun" +ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_ARM64/usr/bin/v2rayN" +cat > "$APPDIR_ARM64/v2rayN.desktop" < "${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt" if [ $Arch = "linux-64" ]; then - Arch2="amd64" + Arch2="amd64" else Arch2="arm64" fi @@ -52,7 +52,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" \ No newline at end of file +sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb" diff --git a/package-rhel.sh b/package-rhel.sh new file mode 100644 index 00000000..ea537c62 --- /dev/null +++ b/package-rhel.sh @@ -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/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 diff --git a/v2rayN/AmazTool/UpgradeApp.cs b/v2rayN/AmazTool/UpgradeApp.cs index a4eb288c..caa269f4 100644 --- a/v2rayN/AmazTool/UpgradeApp.cs +++ b/v2rayN/AmazTool/UpgradeApp.cs @@ -79,15 +79,7 @@ internal class UpgradeApp continue; } - try - { - entry.ExtractToFile(entryOutputPath, true); - } - catch - { - Thread.Sleep(1000); - entry.ExtractToFile(entryOutputPath, true); - } + TryExtractToFile(entry, entryOutputPath); Console.WriteLine(entryOutputPath); } @@ -113,4 +105,24 @@ internal class UpgradeApp Utils.StartV2RayN(); } + + private static bool TryExtractToFile(ZipArchiveEntry entry, string outputPath) + { + var retryCount = 5; + var delayMs = 1000; + + for (var i = 1; i <= retryCount; i++) + { + try + { + entry.ExtractToFile(outputPath, true); + return true; + } + catch + { + Thread.Sleep(delayMs * i); + } + } + return false; + } } diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props index d7fd93ac..785e23d1 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,7 +1,7 @@ - 7.13.3 + 7.14.9 diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index 57a50cfb..9193271c 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -5,12 +5,12 @@ false - - - - + + + + - + @@ -20,7 +20,7 @@ - + diff --git a/v2rayN/ServiceLib/Common/FileManager.cs b/v2rayN/ServiceLib/Common/FileManager.cs index d988c702..6d4d28ca 100644 --- a/v2rayN/ServiceLib/Common/FileManager.cs +++ b/v2rayN/ServiceLib/Common/FileManager.cs @@ -223,4 +223,28 @@ public static class FileManager // ignored } } + + /// + /// Creates a Linux shell file with the specified contents. + /// + /// + /// + /// + /// + public static async Task CreateLinuxShellFile(string fileName, string contents, bool overwrite) + { + var shFilePath = Utils.GetBinConfigPath(fileName); + + // Check if the file already exists and if we should overwrite it + if (!overwrite && File.Exists(shFilePath)) + { + return shFilePath; + } + + File.Delete(shFilePath); + await File.WriteAllTextAsync(shFilePath, contents); + await Utils.SetLinuxChmod(shFilePath); + + return shFilePath; + } } diff --git a/v2rayN/ServiceLib/Common/JsonUtils.cs b/v2rayN/ServiceLib/Common/JsonUtils.cs index 773ba79c..6954e124 100644 --- a/v2rayN/ServiceLib/Common/JsonUtils.cs +++ b/v2rayN/ServiceLib/Common/JsonUtils.cs @@ -128,5 +128,8 @@ public class JsonUtils /// /// /// - public static JsonNode? SerializeToNode(object? obj) => JsonSerializer.SerializeToNode(obj); + public static JsonNode? SerializeToNode(object? obj, JsonSerializerOptions? options = null) + { + return JsonSerializer.SerializeToNode(obj, options); + } } diff --git a/v2rayN/ServiceLib/Common/QRCodeHelper.cs b/v2rayN/ServiceLib/Common/QRCodeUtils.cs similarity index 98% rename from v2rayN/ServiceLib/Common/QRCodeHelper.cs rename to v2rayN/ServiceLib/Common/QRCodeUtils.cs index 9938eadd..3d3dc90b 100644 --- a/v2rayN/ServiceLib/Common/QRCodeHelper.cs +++ b/v2rayN/ServiceLib/Common/QRCodeUtils.cs @@ -4,7 +4,7 @@ using ZXing.SkiaSharp; namespace ServiceLib.Common; -public class QRCodeHelper +public class QRCodeUtils { public static byte[]? GenQRCode(string? url) { diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index c5b670cf..49a359c0 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -390,13 +390,44 @@ public class Utils { if (IPAddress.TryParse(ip, out var address)) { + // Loopback address check (127.0.0.1 for IPv4, ::1 for IPv6) + if (IPAddress.IsLoopback(address)) + return true; + var ipBytes = address.GetAddressBytes(); - if (ipBytes[0] == 10) - return true; - if (ipBytes[0] == 172 && ipBytes[1] >= 16 && ipBytes[1] <= 31) - return true; - if (ipBytes[0] == 192 && ipBytes[1] == 168) - return true; + if (address.AddressFamily == AddressFamily.InterNetwork) + { + // IPv4 private address check + if (ipBytes[0] == 10) + return true; + if (ipBytes[0] == 172 && ipBytes[1] >= 16 && ipBytes[1] <= 31) + return true; + if (ipBytes[0] == 192 && ipBytes[1] == 168) + return true; + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + // IPv6 private address check + // Link-local address fe80::/10 + if (ipBytes[0] == 0xfe && (ipBytes[1] & 0xc0) == 0x80) + return true; + + // Unique local address fc00::/7 (typically fd00::/8) + if ((ipBytes[0] & 0xfe) == 0xfc) + return true; + + // Private portion in IPv4-mapped addresses ::ffff:0:0/96 + if (address.IsIPv4MappedToIPv6) + { + var ipv4Bytes = ipBytes.Skip(12).ToArray(); + if (ipv4Bytes[0] == 10) + return true; + if (ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31) + return true; + if (ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168) + return true; + } + } } return false; @@ -435,11 +466,11 @@ public class Utils return false; } - public static int GetFreePort(int defaultPort = 9090) + public static int GetFreePort(int defaultPort = 0) { try { - if (!Utils.PortInUse(defaultPort)) + if (!(defaultPort == 0 || Utils.PortInUse(defaultPort))) { return defaultPort; } @@ -551,9 +582,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]); } } } diff --git a/v2rayN/ServiceLib/Enums/EConfigType.cs b/v2rayN/ServiceLib/Enums/EConfigType.cs index f56d0e0f..6698f962 100644 --- a/v2rayN/ServiceLib/Enums/EConfigType.cs +++ b/v2rayN/ServiceLib/Enums/EConfigType.cs @@ -11,5 +11,6 @@ public enum EConfigType Hysteria2 = 7, TUIC = 8, WireGuard = 9, - HTTP = 10 + HTTP = 10, + Anytls = 11 } diff --git a/v2rayN/ServiceLib/Enums/ECoreType.cs b/v2rayN/ServiceLib/Enums/ECoreType.cs index e164a2ab..9310a0a9 100644 --- a/v2rayN/ServiceLib/Enums/ECoreType.cs +++ b/v2rayN/ServiceLib/Enums/ECoreType.cs @@ -15,5 +15,6 @@ public enum ECoreType brook = 27, overtls = 28, shadowquic = 29, + mieru = 30, v2rayN = 99 } diff --git a/v2rayN/ServiceLib/Enums/EMsgCommand.cs b/v2rayN/ServiceLib/Enums/EMsgCommand.cs deleted file mode 100644 index 23be4ac1..00000000 --- a/v2rayN/ServiceLib/Enums/EMsgCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ServiceLib.Enums; - -public enum EMsgCommand -{ - ClearMsg, - SendMsgView, - SendSnackMsg, - RefreshProfiles, - AppExit -} diff --git a/v2rayN/ServiceLib/Enums/EViewAction.cs b/v2rayN/ServiceLib/Enums/EViewAction.cs index a72e8765..2c47d31d 100644 --- a/v2rayN/ServiceLib/Enums/EViewAction.cs +++ b/v2rayN/ServiceLib/Enums/EViewAction.cs @@ -6,7 +6,6 @@ public enum EViewAction ShowYesNo, SaveFileDialog, AddBatchRoutingRulesYesNo, - AdjustMainLvColWidth, SetClipboardData, AddServerViaClipboard, ImportRulesFromClipboard, @@ -16,7 +15,6 @@ public enum EViewAction ShowHideWindow, ScanScreenTask, ScanImageTask, - Shutdown, BrowseServer, ImportRulesFromFile, InitSettingFont, @@ -29,18 +27,10 @@ public enum EViewAction DNSSettingWindow, RoutingSettingWindow, OptionSettingWindow, + FullConfigTemplateWindow, GlobalHotkeySettingWindow, SubSettingWindow, - DispatcherSpeedTest, - DispatcherRefreshConnections, - DispatcherRefreshProxyGroups, - DispatcherProxiesDelayTest, - DispatcherStatistics, - DispatcherServerAvailability, - DispatcherReload, DispatcherRefreshServersBiz, DispatcherRefreshIcon, - DispatcherCheckUpdate, - DispatcherCheckUpdateFinished, DispatcherShowMsg, } diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 8436c332..529d2023 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -38,6 +38,8 @@ public class Global public const string PacFileName = NamespaceSample + "pac"; public const string ProxySetOSXShellFileName = NamespaceSample + "proxy_set_osx_sh"; 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 DefaultSecurity = "auto"; public const string DefaultNetwork = "tcp"; @@ -46,6 +48,7 @@ public class Global public const string ProxyTag = "proxy"; public const string DirectTag = "direct"; public const string BlockTag = "block"; + public const string DnsTag = "dns-module"; public const string StreamSecurity = "tls"; public const string StreamSecurityReality = "reality"; public const string Loopback = "127.0.0.1"; @@ -54,6 +57,9 @@ public class Global public const string HttpsProtocol = "https://"; public const string SocksProtocol = "socks://"; public const string Socks5Protocol = "socks5://"; + public const string AsIs = "AsIs"; + public const string IPIfNonMatch = "IPIfNonMatch"; + public const string IPOnDemand = "IPOnDemand"; public const string UserEMail = "t@t.tt"; public const string AutoRunRegPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; @@ -74,6 +80,13 @@ public class Global public const int SpeedTestPageSize = 1000; public const string LinuxBash = "/bin/bash"; + 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 SingboxHostsDNSTag = "hosts_dns"; + public const string SingboxFakeDNSTag = "fake_dns"; + public static readonly List IEProxyProtocols = [ "{ip}:{http_port}", @@ -127,24 +140,24 @@ public class Global ]; public static readonly List SingboxRulesetSources = - [ - "", - @"https://cdn.jsdelivr.net/gh/runetfreedom/russia-v2ray-rules-dat@release/sing-box/rule-set-{0}/{1}.srs", - @"https://cdn.jsdelivr.net/gh/chocolate4u/Iran-sing-box-rules@rule-set/{1}.srs" - ]; +[ + "", + @"https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/sing-box/rule-set-{0}/{1}.srs", + @"https://raw.githubusercontent.com/chocolate4u/Iran-sing-box-rules/rule-set/{1}.srs" +]; public static readonly List RoutingRulesSources = [ "", - @"https://cdn.jsdelivr.net/gh/runetfreedom/russia-v2ray-custom-routing-list@main/v2rayN/template.json", - @"https://cdn.jsdelivr.net/gh/Chocolate4U/Iran-v2ray-rules@main/v2rayN/template.json" + @"https://raw.githubusercontent.com/runetfreedom/russia-v2ray-custom-routing-list/main/v2rayN/template.json", + @"https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/main/v2rayN/template.json" ]; public static readonly List DNSTemplateSources = [ "", - @"https://cdn.jsdelivr.net/gh/runetfreedom/russia-v2ray-custom-routing-list@main/v2rayN/", - @"https://cdn.jsdelivr.net/gh/Chocolate4U/Iran-v2ray-rules@main/v2rayN/" + @"https://raw.githubusercontent.com/runetfreedom/russia-v2ray-custom-routing-list/main/v2rayN/", + @"https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/main/v2rayN/" ]; public static readonly Dictionary UserAgentTexts = new() @@ -167,7 +180,8 @@ public class Global { EConfigType.Trojan, "trojan://" }, { EConfigType.Hysteria2, "hysteria2://" }, { EConfigType.TUIC, "tuic://" }, - { EConfigType.WireGuard, "wireguard://" } + { EConfigType.WireGuard, "wireguard://" }, + { EConfigType.Anytls, "anytls://" } }; public static readonly Dictionary ProtocolTypes = new() @@ -180,7 +194,8 @@ public class Global { EConfigType.Trojan, "trojan" }, { EConfigType.Hysteria2, "hysteria2" }, { EConfigType.TUIC, "tuic" }, - { EConfigType.WireGuard, "wireguard" } + { EConfigType.WireGuard, "wireguard" }, + { EConfigType.Anytls, "anytls" } }; public static readonly List VmessSecurities = @@ -274,11 +289,36 @@ public class Global "sing_box" ]; + public static readonly HashSet XraySupportConfigType = + [ + EConfigType.VMess, + EConfigType.VLESS, + EConfigType.Shadowsocks, + EConfigType.Trojan, + EConfigType.WireGuard, + EConfigType.SOCKS, + EConfigType.HTTP, + ]; + + public static readonly HashSet SingboxSupportConfigType = + [ + EConfigType.VMess, + EConfigType.VLESS, + EConfigType.Shadowsocks, + EConfigType.Trojan, + EConfigType.Hysteria2, + EConfigType.TUIC, + EConfigType.Anytls, + EConfigType.WireGuard, + EConfigType.SOCKS, + EConfigType.HTTP, + ]; + public static readonly List DomainStrategies = [ - "AsIs", - "IPIfNonMatch", - "IPOnDemand" + AsIs, + IPIfNonMatch, + IPOnDemand ]; public static readonly List DomainStrategies4Singbox = @@ -290,13 +330,6 @@ public class Global "" ]; - public static readonly List DomainMatchers = - [ - "linear", - "mph", - "" - ]; - public static readonly List Fingerprints = [ "chrome", @@ -347,25 +380,42 @@ public class Global public static readonly List SingboxDomainStrategy4Out = [ - "ipv4_only", + "", + "ipv4_only", "prefer_ipv4", "prefer_ipv6", - "ipv6_only", - "" + "ipv6_only" ]; - public static readonly List DomainDNSAddress = + public static readonly List DomainDirectDNSAddress = [ - "223.5.5.5", - "223.6.6.6", + "https://dns.alidns.com/dns-query", + "https://doh.pub/dns-query", + "223.5.5.5", + "119.29.29.29", "localhost" ]; - public static readonly List SingboxDomainDNSAddress = + public static readonly List DomainRemoteDNSAddress = + [ + "https://cloudflare-dns.com/dns-query", + "https://dns.cloudflare.com/dns-query", + "https://dns.google/dns-query", + "https://doh.dns.sb/dns-query", + "https://doh.opendns.com/dns-query", + "https://common.dot.dns.yandex.net", + "8.8.8.8", + "1.1.1.1", + "185.222.222.222", + "208.67.222.222", + "77.88.8.8" + ]; + + public static readonly List DomainPureIPDNSAddress = [ "223.5.5.5", - "223.6.6.6", - "dhcp://auto" + "119.29.29.29", + "localhost" ]; public static readonly List Languages = @@ -432,9 +482,11 @@ public class Global public static readonly List TunMtus = [ 1280, - 1408, - 1500, - 9000 + 1408, + 1500, + 4064, + 9000, + 65535 ]; public static readonly List TunStacks = @@ -508,6 +560,7 @@ public class Global { ECoreType.brook, "txthinking/brook" }, { ECoreType.overtls, "ShadowsocksR-Live/overtls" }, { ECoreType.shadowquic, "spongebob888/shadowquic" }, + { ECoreType.mieru, "enfein/mieru" }, { ECoreType.v2rayN, "cg3s/v2rayN" }, }; @@ -535,5 +588,30 @@ public class Global BlockTag ]; + public static readonly Dictionary> PredefinedHosts = new() + { + { "dns.google", new List { "8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844" } }, + { "dns.alidns.com", new List { "223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1" } }, + { "one.one.one.one", new List { "1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001" } }, + { "1dot1dot1dot1.cloudflare-dns.com", new List { "1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001" } }, + { "cloudflare-dns.com", new List { "104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9" } }, + { "dns.cloudflare.com", new List { "104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5" } }, + { "dot.pub", new List { "1.12.12.12", "120.53.53.53" } }, + { "dns.quad9.net", new List { "9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9" } }, + { "dns.yandex.net", new List { "77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff" } }, + { "dns.sb", new List { "185.222.222.222", "2a09::" } }, + { "dns.umbrella.com", new List { "208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53" } }, + { "dns.sse.cisco.com", new List { "208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53" } }, + { "engage.cloudflareclient.com", new List { "162.159.192.1", "2606:4700:d0::a29f:c001" } } + }; + + public static readonly List ExpectedIPs = + [ + "geoip:cn", + "geoip:ir", + "geoip:ru", + "" + ]; + #endregion const } diff --git a/v2rayN/ServiceLib/GlobalUsings.cs b/v2rayN/ServiceLib/GlobalUsings.cs index a4bf3ccd..9a78c73b 100644 --- a/v2rayN/ServiceLib/GlobalUsings.cs +++ b/v2rayN/ServiceLib/GlobalUsings.cs @@ -1,11 +1,13 @@ -global using ServiceLib.Base; +global using ServiceLib.Base; global using ServiceLib.Common; global using ServiceLib.Enums; 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; \ No newline at end of file +global using ServiceLib.Handler.SysProxy; diff --git a/v2rayN/ServiceLib/Handler/AppEvents.cs b/v2rayN/ServiceLib/Handler/AppEvents.cs new file mode 100644 index 00000000..109ee762 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/AppEvents.cs @@ -0,0 +1,21 @@ +using System.Reactive; +using System.Reactive.Subjects; + +namespace ServiceLib.Handler; + +public static class AppEvents +{ + public static readonly Subject ProfilesRefreshRequested = new(); + + public static readonly Subject SendSnackMsgRequested = new(); + + public static readonly Subject SendMsgViewRequested = new(); + + public static readonly Subject AppExitRequested = new(); + + public static readonly Subject ShutdownRequested = new(); + + public static readonly Subject AdjustMainLvColWidthRequested = new(); + + public static readonly Subject DispatcherStatisticsRequested = new(); +} diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index fc06751b..602134a7 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -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"; @@ -112,6 +112,8 @@ public class ConfigHandler config.ConstItem ??= new ConstItem(); + config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); + config.SpeedTestItem ??= new(); if (config.SpeedTestItem.SpeedTestTimeout < 10) { @@ -218,7 +220,7 @@ public class ConfigHandler /// Result of the operation (0 if successful, -1 if failed) public static async Task 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; @@ -250,6 +252,7 @@ public class ConfigHandler item.PublicKey = profileItem.PublicKey; item.ShortId = profileItem.ShortId; item.SpiderX = profileItem.SpiderX; + item.Mldsa65Verify = profileItem.Mldsa65Verify; item.Extra = profileItem.Extra; item.MuxEnabled = profileItem.MuxEnabled; } @@ -265,6 +268,7 @@ public class ConfigHandler EConfigType.Hysteria2 => await AddHysteria2Server(config, item), EConfigType.TUIC => await AddTuicServer(config, item), EConfigType.WireGuard => await AddWireguardServer(config, item), + EConfigType.Anytls => await AddAnytlsServer(config, item), _ => -1, }; return ret; @@ -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; @@ -418,7 +422,7 @@ public class ConfigHandler /// The default profile item or null if none exists public static async Task 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().FirstOrDefaultAsync(); @@ -449,7 +453,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 +465,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 +475,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 +486,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 +496,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 +505,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 +563,7 @@ public class ConfigHandler /// 0 if successful, -1 if failed public static async Task 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 +605,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; } @@ -789,6 +793,35 @@ public class ConfigHandler return 0; } + /// + /// Add or edit a Anytls server + /// Validates and processes Anytls-specific settings + /// + /// Current configuration + /// Anytls profile to add + /// Whether to save to file + /// 0 if successful, -1 if failed + public static async Task AddAnytlsServer(Config config, ProfileItem profileItem, bool toFile = true) + { + profileItem.ConfigType = EConfigType.Anytls; + profileItem.CoreType = ECoreType.sing_box; + + profileItem.Address = profileItem.Address.TrimEx(); + profileItem.Id = profileItem.Id.TrimEx(); + profileItem.Security = profileItem.Security.TrimEx(); + profileItem.Network = string.Empty; + if (profileItem.StreamSecurity.IsNullOrEmpty()) + { + profileItem.StreamSecurity = Global.StreamSecurity; + } + if (profileItem.Id.IsNullOrEmpty()) + { + return -1; + } + await AddServerCommon(config, profileItem, toFile); + return 0; + } + /// /// Sort the server list by the specified column /// Updates the sort order in the profile extension data @@ -800,13 +833,13 @@ public class ConfigHandler /// 0 if successful, -1 if failed public static async Task 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() @@ -876,7 +909,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) { @@ -885,7 +918,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; @@ -895,7 +928,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; @@ -934,7 +967,7 @@ public class ConfigHandler { return -1; } - if (profileItem.Security.IsNotEmpty() && profileItem.Security != Global.None) + if (profileItem.Security.IsNullOrEmpty()) { profileItem.Security = Global.None; } @@ -953,7 +986,7 @@ public class ConfigHandler /// Tuple with total count and remaining count after deduplication public static async Task> 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(0, 0); @@ -1023,15 +1056,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) @@ -1091,7 +1124,7 @@ public class ConfigHandler { try { - var item = await AppHandler.Instance.GetProfileItem(indexId); + var item = await AppManager.Instance.GetProfileItem(indexId); if (item == null) { return 0; @@ -1136,7 +1169,7 @@ public class ConfigHandler return result; } - var profileItem = await AppHandler.Instance.GetProfileItem(indexId) ?? new(); + var profileItem = await AppManager.Instance.GetProfileItem(indexId) ?? new(); profileItem.IndexId = indexId; if (coreType == ECoreType.Xray) { @@ -1182,7 +1215,7 @@ public class ConfigHandler ConfigType = EConfigType.SOCKS, Address = Global.Loopback, Sni = node.Address, //Tun2SocksAddress - Port = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks) + Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks) }; } else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)) @@ -1209,12 +1242,12 @@ public class ConfigHandler /// Number of removed servers or -1 if failed public static async Task 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 @@ -1250,7 +1283,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; @@ -1298,6 +1331,7 @@ public class ConfigHandler EConfigType.Hysteria2 => await AddHysteria2Server(config, profileItem, false), EConfigType.TUIC => await AddTuicServer(config, profileItem, false), EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false), + EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false), _ => -1, }; @@ -1333,7 +1367,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; @@ -1382,6 +1416,11 @@ public class ConfigHandler { profileItem = V2rayFmt.ResolveFull(strData, subRemarks); } + //Is Html Page + if (profileItem is null && HtmlPageFmt.IsHtmlPage(strData)) + { + return -1; + } //Is Clash configuration if (profileItem is null) { @@ -1484,7 +1523,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); } @@ -1516,7 +1555,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) { @@ -1527,13 +1566,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); } } } @@ -1573,7 +1612,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; } @@ -1591,7 +1630,7 @@ public class ConfigHandler /// 0 if successful, -1 if failed public static async Task 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; @@ -1623,7 +1662,7 @@ public class ConfigHandler var maxSort = 0; if (await SQLiteHelper.Instance.TableAsync().CountAsync() > 0) { - var lstSubs = (await AppHandler.Instance.SubItems()); + var lstSubs = (await AppManager.Instance.SubItems()); maxSort = lstSubs.LastOrDefault()?.Sort ?? 0; } item.Sort = maxSort + 1; @@ -1677,7 +1716,7 @@ public class ConfigHandler /// 0 if successful public static async Task 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; @@ -1861,7 +1900,7 @@ public class ConfigHandler /// 0 if successful public static async Task 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; @@ -1941,7 +1980,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) { @@ -1988,19 +2027,19 @@ public class ConfigHandler public static async Task 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) { - //migrate + //migrate //TODO Temporary code to be removed later if (config.RoutingBasicItem.RoutingIndexId.IsNotEmpty()) { @@ -2066,18 +2105,38 @@ public class ConfigHandler /// /// Initialize built-in DNS configurations /// Creates default DNS items for V2Ray and sing-box + /// Also checks existing DNS items and disables those with empty NormalDNS /// /// Current configuration /// 0 if successful public static async Task 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; + foreach (var existingItem in items) + { + if (existingItem.NormalDNS.IsNullOrEmpty() && existingItem.Enabled) + { + existingItem.Enabled = false; + needsUpdate = true; + } + } + + // Update items if any changes were made + if (needsUpdate) + { + await SQLiteHelper.Instance.UpdateAllAsync(items); + } + if (items.Count <= 0) { var item = new DNSItem() { Remarks = "V2ray", CoreType = ECoreType.Xray, + Enabled = false, }; await SaveDNSItems(config, item); @@ -2085,6 +2144,7 @@ public class ConfigHandler { Remarks = "sing-box", CoreType = ECoreType.sing_box, + Enabled = false, }; await SaveDNSItems(config, item2); } @@ -2129,7 +2189,7 @@ public class ConfigHandler /// DNS item with configuration from the URL public static async Task 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, ""); @@ -2156,6 +2216,86 @@ public class ConfigHandler #endregion DNS + #region Simple DNS + + public static SimpleDNSItem InitBuiltinSimpleDNS() + { + return new SimpleDNSItem() + { + UseSystemHosts = false, + AddCommonHosts = true, + FakeIP = false, + BlockBindingQuery = true, + DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), + RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(), + SingboxOutboundsResolveDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), + SingboxFinalResolveDNS = Global.DomainPureIPDNSAddress.FirstOrDefault() + }; + } + + public static async Task GetExternalSimpleDNSItem(string url) + { + var downloadHandle = new DownloadService(); + var templateContent = await downloadHandle.TryDownloadString(url, true, ""); + if (templateContent.IsNullOrEmpty()) + return null; + var template = JsonUtils.Deserialize(templateContent); + if (template == null) + return null; + return template; + } + + #endregion Simple DNS + + #region Custom Config + + public static async Task InitBuiltinFullConfigTemplate(Config config) + { + var items = await AppManager.Instance.FullConfigTemplateItem(); + if (items.Count <= 0) + { + var item = new FullConfigTemplateItem() + { + Remarks = "V2ray", + CoreType = ECoreType.Xray, + }; + await SaveFullConfigTemplate(config, item); + + var item2 = new FullConfigTemplateItem() + { + Remarks = "sing-box", + CoreType = ECoreType.sing_box, + }; + await SaveFullConfigTemplate(config, item2); + } + + return 0; + } + + public static async Task SaveFullConfigTemplate(Config config, FullConfigTemplateItem item) + { + if (item == null) + { + return -1; + } + + if (item.Id.IsNullOrEmpty()) + { + item.Id = Utils.GetGuid(false); + } + + if (await SQLiteHelper.Instance.ReplaceAsync(item) > 0) + { + return 0; + } + else + { + return -1; + } + } + + #endregion Custom Config + #region Regional Presets /// @@ -2177,30 +2317,57 @@ public class ConfigHandler await SQLiteHelper.Instance.DeleteAllAsync(); await InitBuiltinDNS(config); - return true; + config.SimpleDNSItem = InitBuiltinSimpleDNS(); + break; case EPresetType.Russia: config.ConstItem.GeoSourceUrl = Global.GeoFilesSources[1]; 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"); - return true; + 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: config.ConstItem.GeoSourceUrl = Global.GeoFilesSources[2]; 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"); - return true; + 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; } - return false; + return true; } #endregion Regional Presets diff --git a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs index 3cfc6020..38f3ae51 100644 --- a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs @@ -1,27 +1,28 @@ +using System.Net; + namespace ServiceLib.Handler; -public class ConnectionHandler +public static class ConnectionHandler { - private static readonly Lazy _instance = new(() => new()); - public static ConnectionHandler Instance => _instance.Value; + private static readonly string _tag = "ConnectionHandler"; - public async Task RunAvailabilityCheck() + public static async Task 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 GetIPInfo(DownloadService downloadHandle) + private static async Task 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 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; + } } diff --git a/v2rayN/ServiceLib/Handler/CoreAdminHandler.cs b/v2rayN/ServiceLib/Handler/CoreAdminHandler.cs deleted file mode 100644 index 3e7d3f93..00000000 --- a/v2rayN/ServiceLib/Handler/CoreAdminHandler.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Diagnostics; -using System.Text; -using CliWrap; - -namespace ServiceLib.Handler; - -public class CoreAdminHandler -{ - private static readonly Lazy _instance = new(() => new()); - public static CoreAdminHandler Instance => _instance.Value; - private Config _config; - private readonly string _sudoAccessText = "SUDO_ACCESS_VERIFIED"; - private Action? _updateFunc; - private int _linuxSudoPid = -1; - - public async Task Init(Config config, Action updateFunc) - { - if (_config != null) - { - return; - } - _config = config; - _updateFunc = updateFunc; - } - - private void UpdateFunc(bool notify, string msg) - { - _updateFunc?.Invoke(notify, msg); - } - - public async Task RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath) - { - var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}"; - var shFilePath = await CreateLinuxShellFile(cmdLine, "run_as_sudo.sh"); - - 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, - } - }; - - var sudoVerified = false; - DataReceivedEventHandler dataHandler = (sender, e) => - { - if (e.Data.IsNotEmpty()) - { - if (!sudoVerified && e.Data.Contains(_sudoAccessText)) - { - sudoVerified = true; - UpdateFunc(false, ResUI.SudoPwdVerfiedSuccessTip + Environment.NewLine); - return; - } - 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(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; - } - - var cmdLine = $"pkill -P {_linuxSudoPid} ; kill {_linuxSudoPid}"; - var shFilePath = await CreateLinuxShellFile(cmdLine, "kill_as_sudo.sh"); - - await Cli.Wrap(shFilePath) - .WithStandardInputPipe(PipeSource.FromString(AppHandler.Instance.LinuxSudoPwd)) - .ExecuteAsync(); - - _linuxSudoPid = -1; - } - - private async Task CreateLinuxShellFile(string cmdLine, string fileName) - { - var shFilePath = Utils.GetBinConfigPath(fileName); - File.Delete(shFilePath); - - var sb = new StringBuilder(); - sb.AppendLine("#!/bin/sh"); - if (Utils.IsAdministrator()) - { - sb.AppendLine($"{cmdLine}"); - } - else - { - sb.AppendLine($"sudo -S echo \"{_sudoAccessText}\" && sudo -S {cmdLine}"); - } - - await File.WriteAllTextAsync(shFilePath, sb.ToString()); - await Utils.SetLinuxChmod(shFilePath); - - return shFilePath; - } -} diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index 40c5eacf..66d261ac 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -3,13 +3,13 @@ namespace ServiceLib.Handler; /// /// Core configuration file processing class /// -public class CoreConfigHandler +public static class CoreConfigHandler { private static readonly string _tag = "CoreConfigHandler"; public static async Task 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 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); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs new file mode 100644 index 00000000..f175ce82 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs @@ -0,0 +1,48 @@ +namespace ServiceLib.Handler.Fmt; + +public class AnytlsFmt : BaseFmt +{ + public static ProfileItem? Resolve(string str, out string msg) + { + msg = ResUI.ConfigurationFormatIncorrect; + + var parsedUrl = Utils.TryUri(str); + if (parsedUrl == null) + { + return null; + } + + ProfileItem item = new() + { + ConfigType = EConfigType.Anytls, + Remarks = parsedUrl.GetComponents(UriComponents.Fragment, UriFormat.Unescaped), + Address = parsedUrl.IdnHost, + Port = parsedUrl.Port, + }; + var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo); + item.Id = rawUserInfo; + + var query = Utils.ParseQueryString(parsedUrl.Query); + _ = ResolveStdTransport(query, ref item); + + return item; + } + + public static string? ToUri(ProfileItem? item) + { + if (item == null) + { + return null; + } + var remark = string.Empty; + if (item.Remarks.IsNotEmpty()) + { + remark = "#" + Utils.UrlEncode(item.Remarks); + } + var pw = item.Id; + var dicQuery = new Dictionary(); + _ = GetStdTransport(item, Global.None, ref dicQuery); + + return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark); + } +} diff --git a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs index 86b92fa0..965efbb1 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs @@ -59,6 +59,10 @@ public class BaseFmt { dicQuery.Add("spx", Utils.UrlEncode(item.SpiderX)); } + if (item.Mldsa65Verify.IsNotEmpty()) + { + dicQuery.Add("pqv", Utils.UrlEncode(item.Mldsa65Verify)); + } if (item.AllowInsecure.Equals("true")) { dicQuery.Add("allowInsecure", "1"); @@ -159,6 +163,7 @@ public class BaseFmt 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.Network = query["type"] ?? nameof(ETransport.tcp); @@ -215,14 +220,7 @@ public class BaseFmt protected static bool Contains(string str, params string[] s) { - foreach (var item in s) - { - if (str.Contains(item, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + return s.All(item => str.Contains(item, StringComparison.OrdinalIgnoreCase)); } protected static string WriteAllText(string strData, string ext = "json") diff --git a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs index 3e8ab2ae..814d753d 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs @@ -18,6 +18,7 @@ public class FmtHandler EConfigType.Hysteria2 => Hysteria2Fmt.ToUri(item), EConfigType.TUIC => TuicFmt.ToUri(item), EConfigType.WireGuard => WireguardFmt.ToUri(item), + EConfigType.Anytls => AnytlsFmt.ToUri(item), _ => null, }; @@ -75,6 +76,10 @@ public class FmtHandler { return WireguardFmt.Resolve(str, out msg); } + else if (str.StartsWith(Global.ProtocolShares[EConfigType.Anytls])) + { + return AnytlsFmt.Resolve(str, out msg); + } else { msg = ResUI.NonvmessOrssProtocol; diff --git a/v2rayN/ServiceLib/Handler/Fmt/HtmlPageFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/HtmlPageFmt.cs new file mode 100644 index 00000000..d7c014ae --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Fmt/HtmlPageFmt.cs @@ -0,0 +1,9 @@ +namespace ServiceLib.Handler.Fmt; + +public class HtmlPageFmt : BaseFmt +{ + public static bool IsHtmlPage(string strData) + { + return Contains(strData, " updateFunc) + { + await updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart); + var subItem = await AppManager.Instance.SubItems(); + + if (subItem is not { Count: > 0 }) + { + await updateFunc?.Invoke(false, ResUI.MsgNoValidSubscription); + return; + } + + var successCount = 0; + foreach (var item in subItem) + { + try + { + if (!IsValidSubscription(item, subId)) + { + continue; + } + + var hashCode = $"{item.Remarks}->"; + if (item.Enabled == false) + { + await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}"); + continue; + } + + // Create download handler + var downloadHandle = CreateDownloadHandler(hashCode, updateFunc); + 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 + if (await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc)) + { + successCount++; + } + + await updateFunc?.Invoke(false, "-------------------------------------------------------"); + } + catch (Exception ex) + { + var hashCode = $"{item.Remarks}->"; + Logging.SaveLog("UpdateSubscription", ex); + await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgFailedImportSubscription}: {ex.Message}"); + await updateFunc?.Invoke(false, "-------------------------------------------------------"); + } + } + + await updateFunc?.Invoke(successCount > 0, $"{ResUI.MsgUpdateSubscriptionEnd}"); + } + + private static bool IsValidSubscription(SubItem item, string subId) + { + var id = item.Id.TrimEx(); + var url = item.Url.TrimEx(); + + if (id.IsNullOrEmpty() || url.IsNullOrEmpty()) + { + return false; + } + + if (subId.IsNotEmpty() && item.Id != subId) + { + return false; + } + + if (!url.StartsWith(Global.HttpsProtocol) && !url.StartsWith(Global.HttpProtocol)) + { + return false; + } + + return true; + } + + private static DownloadService CreateDownloadHandler(string hashCode, Func updateFunc) + { + var downloadHandle = new DownloadService(); + downloadHandle.Error += (sender2, args) => + { + updateFunc?.Invoke(false, $"{hashCode}{args.GetException().Message}"); + }; + return downloadHandle; + } + + private static async Task DownloadSubscriptionContent(DownloadService downloadHandle, string url, bool blProxy, string userAgent) + { + var result = await downloadHandle.TryDownloadString(url, blProxy, userAgent); + + // If download with proxy fails, try direct connection + if (blProxy && result.IsNullOrEmpty()) + { + result = await downloadHandle.TryDownloadString(url, false, userAgent); + } + + return result ?? string.Empty; + } + + private static async Task DownloadAllSubscriptions(Config config, SubItem item, bool blProxy, DownloadService downloadHandle) + { + // Download main subscription content + var result = await DownloadMainSubscription(config, item, blProxy, downloadHandle); + + // Process additional subscription links (if any) + if (item.ConvertTarget.IsNullOrEmpty() && item.MoreUrl.TrimEx().IsNotEmpty()) + { + result = await DownloadAdditionalSubscriptions(item, result, blProxy, downloadHandle); + } + + return result; + } + + private static async Task DownloadMainSubscription(Config config, SubItem item, bool blProxy, DownloadService downloadHandle) + { + // Prepare subscription URL and download directly + var url = Utils.GetPunycode(item.Url.TrimEx()); + + // If conversion is needed + if (item.ConvertTarget.IsNotEmpty()) + { + var subConvertUrl = config.ConstItem.SubConvertUrl.IsNullOrEmpty() + ? Global.SubConvertUrls.FirstOrDefault() + : config.ConstItem.SubConvertUrl; + + url = string.Format(subConvertUrl!, Utils.UrlEncode(url)); + + if (!url.Contains("target=")) + { + url += string.Format("&target={0}", item.ConvertTarget); + } + + if (!url.Contains("config=")) + { + url += string.Format("&config={0}", Global.SubConvertConfig.FirstOrDefault()); + } + } + + // Download and return result directly + return await DownloadSubscriptionContent(downloadHandle, url, blProxy, item.UserAgent); + } + + private static async Task DownloadAdditionalSubscriptions(SubItem item, string mainResult, bool blProxy, DownloadService downloadHandle) + { + var result = mainResult; + + // If main subscription result is Base64 encoded, decode it first + if (result.IsNotEmpty() && Utils.IsBase64String(result)) + { + result = Utils.Base64Decode(result); + } + + // Process additional URL list + var lstUrl = item.MoreUrl.TrimEx().Split(",") ?? []; + foreach (var it in lstUrl) + { + var url2 = Utils.GetPunycode(it); + if (url2.IsNullOrEmpty()) + { + continue; + } + + var additionalResult = await DownloadSubscriptionContent(downloadHandle, url2, blProxy, item.UserAgent); + + if (additionalResult.IsNotEmpty()) + { + // Process additional subscription results, add to main result + if (Utils.IsBase64String(additionalResult)) + { + result += Environment.NewLine + Utils.Base64Decode(additionalResult); + } + else + { + result += Environment.NewLine + additionalResult; + } + } + } + + return result; + } + + private static async Task ProcessDownloadResult(Config config, string id, string result, string hashCode, Func updateFunc) + { + if (result.IsNullOrEmpty()) + { + await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecodingFailed}"); + return false; + } + + await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgGetSubscriptionSuccessfully}"); + + // If result is too short, display content directly + if (result.Length < 99) + { + 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) + { + Logging.SaveLog("FailedImportSubscription"); + Logging.SaveLog(result); + } + + // Update completion message + await updateFunc?.Invoke(false, ret > 0 + ? $"{hashCode}{ResUI.MsgUpdateSubscriptionEnd}" + : $"{hashCode}{ResUI.MsgFailedImportSubscription}"); + + return ret > 0; + } +} diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs index 9c5a53a8..828c2102 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs @@ -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"; @@ -18,14 +18,7 @@ public class ProxySettingLinux private static async Task ExecCmd(List args) { - var fileName = Utils.GetBinConfigPath(_proxySetFileName); - if (!File.Exists(fileName)) - { - var contents = EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName); - await File.AppendAllTextAsync(fileName, contents); - - await Utils.SetLinuxChmod(fileName); - } + var fileName = await FileManager.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false); await Utils.GetCliWrapOutput(fileName, args); } diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs index c18cd728..85d9b821 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs @@ -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"; @@ -23,14 +23,7 @@ public class ProxySettingOSX private static async Task ExecCmd(List args) { - var fileName = Utils.GetBinConfigPath(_proxySetFileName); - if (!File.Exists(fileName)) - { - var contents = EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName); - await File.AppendAllTextAsync(fileName, contents); - - await Utils.SetLinuxChmod(fileName); - } + var fileName = await FileManager.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false); await Utils.GetCliWrapOutput(fileName, args); } diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs index 79b860b0..44b7e046 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs @@ -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"; diff --git a/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs b/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs index 01361a92..38ea04ae 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs @@ -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.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.Start(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); } diff --git a/v2rayN/ServiceLib/Common/DownloaderHelper.cs b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs similarity index 99% rename from v2rayN/ServiceLib/Common/DownloaderHelper.cs rename to v2rayN/ServiceLib/Helper/DownloaderHelper.cs index 073e1ab4..3764499c 100644 --- a/v2rayN/ServiceLib/Common/DownloaderHelper.cs +++ b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs @@ -1,7 +1,7 @@ using System.Net; using Downloader; -namespace ServiceLib.Common; +namespace ServiceLib.Helper; public class DownloaderHelper { diff --git a/v2rayN/ServiceLib/Common/HttpClientHelper.cs b/v2rayN/ServiceLib/Helper/HttpClientHelper.cs similarity index 84% rename from v2rayN/ServiceLib/Common/HttpClientHelper.cs rename to v2rayN/ServiceLib/Helper/HttpClientHelper.cs index 8bc1384a..a559800f 100644 --- a/v2rayN/ServiceLib/Common/HttpClientHelper.cs +++ b/v2rayN/ServiceLib/Helper/HttpClientHelper.cs @@ -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; /// /// @@ -202,4 +204,35 @@ public class HttpClientHelper } } while (isMoreToRead); } + + public async Task 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 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; + } } diff --git a/v2rayN/ServiceLib/Common/SqliteHelper.cs b/v2rayN/ServiceLib/Helper/SqliteHelper.cs similarity index 90% rename from v2rayN/ServiceLib/Common/SqliteHelper.cs rename to v2rayN/ServiceLib/Helper/SqliteHelper.cs index 62b0a830..959d5ff6 100644 --- a/v2rayN/ServiceLib/Common/SqliteHelper.cs +++ b/v2rayN/ServiceLib/Helper/SqliteHelper.cs @@ -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 InsertAllAsync(IEnumerable models) { - return await _dbAsync.InsertAllAsync(models); + return await _dbAsync.InsertAllAsync(models, runInTransaction: true).ConfigureAwait(false); } public async Task InsertAsync(object model) @@ -46,7 +46,7 @@ public sealed class SQLiteHelper public async Task UpdateAllAsync(IEnumerable models) { - return await _dbAsync.UpdateAllAsync(models); + return await _dbAsync.UpdateAllAsync(models, runInTransaction: true).ConfigureAwait(false); } public async Task DeleteAsync(object model) diff --git a/v2rayN/ServiceLib/Handler/AppHandler.cs b/v2rayN/ServiceLib/Manager/AppManager.cs similarity index 79% rename from v2rayN/ServiceLib/Handler/AppHandler.cs rename to v2rayN/ServiceLib/Manager/AppManager.cs index ad9a4029..33125289 100644 --- a/v2rayN/ServiceLib/Handler/AppHandler.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -1,15 +1,17 @@ -namespace ServiceLib.Handler; +using System.Reactive; -public sealed class AppHandler +namespace ServiceLib.Manager; + +public sealed class AppManager { #region Property - private static readonly Lazy _instance = new(() => new()); + private static readonly Lazy _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 +36,7 @@ public sealed class AppHandler #endregion Property - #region Init + #region App public bool InitApp() { @@ -64,6 +66,7 @@ public sealed class AppHandler SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); + SQLiteHelper.Instance.CreateTable(); return true; } @@ -86,7 +89,40 @@ 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.OnNext(Unit.Default); + 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.OnNext(byUser); + } + + #endregion App #region Config @@ -96,7 +132,7 @@ public sealed class AppHandler return localPort + (int)protocol; } - public void AddProcess(IntPtr processHandle) + public void AddProcess(nint processHandle) { if (Utils.IsWindows()) { @@ -203,6 +239,16 @@ public sealed class AppHandler return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.CoreType == eCoreType); } + public async Task?> FullConfigTemplateItem() + { + return await SQLiteHelper.Instance.TableAsync().ToListAsync(); + } + + public async Task GetFullConfigTemplateItem(ECoreType eCoreType) + { + return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.CoreType == eCoreType); + } + #endregion SqliteHelper #region Core Type diff --git a/v2rayN/ServiceLib/Handler/ClashApiHandler.cs b/v2rayN/ServiceLib/Manager/ClashApiManager.cs similarity index 90% rename from v2rayN/ServiceLib/Handler/ClashApiHandler.cs rename to v2rayN/ServiceLib/Manager/ClashApiManager.cs index af5b0c57..e34f838d 100644 --- a/v2rayN/ServiceLib/Handler/ClashApiHandler.cs +++ b/v2rayN/ServiceLib/Manager/ClashApiManager.cs @@ -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 instance = new(() => new()); - public static ClashApiHandler Instance => instance.Value; + private static readonly Lazy instance = new(() => new()); + public static ClashApiManager Instance => instance.Value; private static readonly string _tag = "ClashApiHandler"; private Dictionary? _proxies; @@ -35,7 +35,7 @@ public sealed class ClashApiHandler return null; } - public void ClashProxiesDelayTest(bool blAll, List lstProxy, Action updateFunc) + public void ClashProxiesDelayTest(bool blAll, List lstProxy, Func 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(); 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}"; } } diff --git a/v2rayN/ServiceLib/Manager/CoreAdminManager.cs b/v2rayN/ServiceLib/Manager/CoreAdminManager.cs new file mode 100644 index 00000000..90b47106 --- /dev/null +++ b/v2rayN/ServiceLib/Manager/CoreAdminManager.cs @@ -0,0 +1,118 @@ +using System.Diagnostics; +using System.Text; +using CliWrap; +using CliWrap.Buffered; + +namespace ServiceLib.Manager; + +public class CoreAdminManager +{ + private static readonly Lazy _instance = new(() => new()); + public static CoreAdminManager Instance => _instance.Value; + private Config _config; + private Func? _updateFunc; + private int _linuxSudoPid = -1; + private const string _tag = "CoreAdminHandler"; + + public async Task Init(Config config, Func 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 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(AppManager.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() { "-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; + } +} diff --git a/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs b/v2rayN/ServiceLib/Manager/CoreInfoManager.cs similarity index 83% rename from v2rayN/ServiceLib/Handler/CoreInfoHandler.cs rename to v2rayN/ServiceLib/Manager/CoreInfoManager.cs index 6b7e1df2..b83bd7ac 100644 --- a/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs +++ b/v2rayN/ServiceLib/Manager/CoreInfoManager.cs @@ -1,12 +1,12 @@ -namespace ServiceLib.Handler; +namespace ServiceLib.Manager; -public sealed class CoreInfoHandler +public sealed class CoreInfoManager { - private static readonly Lazy _instance = new(() => new()); + private static readonly Lazy _instance = new(() => new()); private List? _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() + { + { 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() + { + { 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() + { + { 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() + { + { "MIERU_CONFIG_JSON_FILE", "{0}" }, + }, + }, ]; } diff --git a/v2rayN/ServiceLib/Handler/CoreHandler.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs similarity index 75% rename from v2rayN/ServiceLib/Handler/CoreHandler.cs rename to v2rayN/ServiceLib/Manager/CoreManager.cs index d1cf43f0..695508c2 100644 --- a/v2rayN/ServiceLib/Handler/CoreHandler.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -1,31 +1,27 @@ using System.Diagnostics; using System.Text; -namespace ServiceLib.Handler; +namespace ServiceLib.Manager; /// /// Core process processing class /// -public class CoreHandler +public class CoreManager { - private static readonly Lazy _instance = new(() => new()); - public static CoreHandler Instance => _instance.Value; + private static readonly Lazy _instance = new(() => new()); + public static CoreManager Instance => _instance.Value; private Config _config; private Process? _process; private Process? _processPre; private bool _linuxSudo = false; - private Action? _updateFunc; + private Func? _updateFunc; private const string _tag = "CoreHandler"; - public async Task Init(Config config, Action updateFunc) + public async Task Init(Config config, Func 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 +35,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 +63,7 @@ public class CoreHandler { if (node == null) { - UpdateFunc(false, ResUI.CheckServerSettings); + await UpdateFunc(false, ResUI.CheckServerSettings); return; } @@ -75,13 +71,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); @@ -95,26 +91,26 @@ public class CoreHandler await CoreStartPreService(node); if (_process != null) { - UpdateFunc(true, $"{node.GetSummary()}"); + await UpdateFunc(true, $"{node.GetSummary()}"); } } public async Task LoadCoreConfigSpeedtest(List selecteds) { - var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC) ? ECoreType.sing_box : ECoreType.Xray; + var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) ? 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; } - 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 coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var proc = await RunProcess(coreInfo, fileName, true, false); if (proc is null) { @@ -126,7 +122,7 @@ public class CoreHandler public async Task 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; @@ -140,8 +136,8 @@ public class CoreHandler return -1; } - var coreType = AppHandler.Instance.GetCoreType(node, node.ConfigType); - var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType); + var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); + var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var proc = await RunProcess(coreInfo, fileName, true, false); if (proc is null) { @@ -157,7 +153,7 @@ public class CoreHandler { if (_linuxSudo) { - await CoreAdminHandler.Instance.KillProcessAsLinuxSudo(); + await CoreAdminManager.Instance.KillProcessAsLinuxSudo(); _linuxSudo = false; } @@ -183,8 +179,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); @@ -199,7 +195,7 @@ public class CoreHandler { if (_process != null && !_process.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,7 +204,7 @@ 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) { @@ -220,9 +216,9 @@ public class CoreHandler } } - 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 @@ -231,10 +227,10 @@ public class CoreHandler private async Task 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 +242,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,7 +251,7 @@ public class CoreHandler catch (Exception ex) { Logging.SaveLog(_tag, ex); - UpdateFunc(mayNeedSudo, ex.Message); + await UpdateFunc(mayNeedSudo, ex.Message); return null; } } @@ -277,16 +273,20 @@ public class CoreHandler StandardErrorEncoding = displayLog ? Encoding.UTF8 : null, } }; + foreach (var kv in coreInfo.Environment) + { + proc.StartInfo.Environment[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath); + } if (displayLog) { - DataReceivedEventHandler dataHandler = (sender, e) => + void dataHandler(object sender, DataReceivedEventArgs e) { if (e.Data.IsNotEmpty()) { - UpdateFunc(false, e.Data + Environment.NewLine); + _ = UpdateFunc(false, e.Data + Environment.NewLine); } - }; + } proc.OutputDataReceived += dataHandler; proc.ErrorDataReceived += dataHandler; } @@ -299,7 +299,7 @@ public class CoreHandler } await Task.Delay(100); - AppHandler.Instance.AddProcess(proc.Handle); + AppManager.Instance.AddProcess(proc.Handle); if (proc is null or { HasExited: true }) { throw new Exception(ResUI.FailedToRunCore); diff --git a/v2rayN/ServiceLib/Handler/NoticeHandler.cs b/v2rayN/ServiceLib/Manager/NoticeManager.cs similarity index 62% rename from v2rayN/ServiceLib/Handler/NoticeHandler.cs rename to v2rayN/ServiceLib/Manager/NoticeManager.cs index 31cb2204..f9e149ed 100644 --- a/v2rayN/ServiceLib/Handler/NoticeHandler.cs +++ b/v2rayN/ServiceLib/Manager/NoticeManager.cs @@ -1,11 +1,9 @@ -using ReactiveUI; +namespace ServiceLib.Manager; -namespace ServiceLib.Handler; - -public class NoticeHandler +public class NoticeManager { - private static readonly Lazy _instance = new(() => new()); - public static NoticeHandler Instance => _instance.Value; + private static readonly Lazy _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.OnNext(content); } public void SendMessage(string? content) @@ -22,7 +20,7 @@ public class NoticeHandler { return; } - MessageBus.Current.SendMessage(content, EMsgCommand.SendMsgView.ToString()); + AppEvents.SendMsgViewRequested.OnNext(content); } public void SendMessageEx(string? content) diff --git a/v2rayN/ServiceLib/Handler/PacHandler.cs b/v2rayN/ServiceLib/Manager/PacManager.cs similarity index 79% rename from v2rayN/ServiceLib/Handler/PacHandler.cs rename to v2rayN/ServiceLib/Manager/PacManager.cs index 6d2e1f19..10bedc29 100644 --- a/v2rayN/ServiceLib/Handler/PacHandler.cs +++ b/v2rayN/ServiceLib/Manager/PacManager.cs @@ -1,19 +1,22 @@ using System.Net.Sockets; using System.Text; -namespace ServiceLib.Handler; +namespace ServiceLib.Manager; -public class PacHandler +public class PacManager { - private static string _configPath; - private static int _httpPort; - private static int _pacPort; - private static TcpListener? _tcpListener; - private static byte[] _writeContent; - private static bool _isRunning; - private static bool _needRestart = true; + private static readonly Lazy _instance = new(() => new PacManager()); + public static PacManager Instance => _instance.Value; - public static async Task Start(string configPath, int httpPort, int pacPort) + private string _configPath; + private int _httpPort; + private int _pacPort; + private TcpListener? _tcpListener; + private byte[] _writeContent; + private bool _isRunning; + private bool _needRestart = true; + + public async Task StartAsync(string configPath, int httpPort, int pacPort) { _needRestart = configPath != _configPath || httpPort != _httpPort || pacPort != _pacPort || !_isRunning; @@ -30,7 +33,7 @@ public class PacHandler } } - private static async Task InitText() + private async Task InitText() { var path = Path.Combine(_configPath, "pac.txt"); @@ -59,7 +62,7 @@ public class PacHandler _writeContent = Encoding.UTF8.GetBytes(sb.ToString()); } - private static void RunListener() + private void RunListener() { _tcpListener = TcpListener.Create(_pacPort); _isRunning = true; @@ -87,14 +90,14 @@ public class PacHandler }, TaskCreationOptions.LongRunning); } - private static void WriteContent(TcpClient client) + private void WriteContent(TcpClient client) { var stream = client.GetStream(); stream.Write(_writeContent, 0, _writeContent.Length); stream.Flush(); } - public static void Stop() + public void Stop() { if (_tcpListener == null) { diff --git a/v2rayN/ServiceLib/Handler/ProfileExHandler.cs b/v2rayN/ServiceLib/Manager/ProfileExManager.cs similarity index 95% rename from v2rayN/ServiceLib/Handler/ProfileExHandler.cs rename to v2rayN/ServiceLib/Manager/ProfileExManager.cs index 5e8ff2e2..0a3b7399 100644 --- a/v2rayN/ServiceLib/Handler/ProfileExHandler.cs +++ b/v2rayN/ServiceLib/Manager/ProfileExManager.cs @@ -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 _instance = new(() => new()); + private static readonly Lazy _instance = new(() => new()); private ConcurrentBag _lstProfileEx = []; private readonly Queue _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(); } diff --git a/v2rayN/ServiceLib/Handler/StatisticsHandler.cs b/v2rayN/ServiceLib/Manager/StatisticsManager.cs similarity index 88% rename from v2rayN/ServiceLib/Handler/StatisticsHandler.cs rename to v2rayN/ServiceLib/Manager/StatisticsManager.cs index 890c047d..1e439a7f 100644 --- a/v2rayN/ServiceLib/Handler/StatisticsHandler.cs +++ b/v2rayN/ServiceLib/Manager/StatisticsManager.cs @@ -1,21 +1,21 @@ -namespace ServiceLib.Handler; +namespace ServiceLib.Manager; -public class StatisticsHandler +public class StatisticsManager { - private static readonly Lazy instance = new(() => new()); - public static StatisticsHandler Instance => instance.Value; + private static readonly Lazy instance = new(() => new()); + public static StatisticsManager Instance => instance.Value; private Config _config; private ServerStatItem? _serverStatItem; private List _lstServerStat; - private Action? _updateFunc; + private Func? _updateFunc; private StatisticsXrayService? _statisticsXray; private StatisticsSingboxService? _statisticsSingbox; private static readonly string _tag = "StatisticsHandler"; public List ServerStat => _lstServerStat; - public async Task Init(Config config, Action updateFunc) + public async Task Init(Config config, Func 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().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; diff --git a/v2rayN/ServiceLib/Handler/TaskHandler.cs b/v2rayN/ServiceLib/Manager/TaskManager.cs similarity index 56% rename from v2rayN/ServiceLib/Handler/TaskHandler.cs rename to v2rayN/ServiceLib/Manager/TaskManager.cs index ef1d4e38..dfbd242d 100644 --- a/v2rayN/ServiceLib/Handler/TaskHandler.cs +++ b/v2rayN/ServiceLib/Manager/TaskManager.cs @@ -1,16 +1,21 @@ -namespace ServiceLib.Handler; +namespace ServiceLib.Manager; -public class TaskHandler +public class TaskManager { - private static readonly Lazy _instance = new(() => new()); - public static TaskHandler Instance => _instance.Value; + private static readonly Lazy _instance = new(() => new()); + public static TaskManager Instance => _instance.Value; + private Config _config; + private Func? _updateFunc; - public void RegUpdateTask(Config config, Action updateFunc) + public void RegUpdateTask(Config config, Func updateFunc) { - Task.Run(() => ScheduledTasks(config, updateFunc)); + _config = config; + _updateFunc = updateFunc; + + Task.Run(ScheduledTasks); } - private async Task ScheduledTasks(Config config, Action 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 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(); @@ -63,34 +68,33 @@ public class TaskHandler } Logging.SaveLog("Execute update subscription"); - var updateHandle = new UpdateService(); foreach (var item in lstSubs) { - await updateHandle.UpdateSubscriptionProcess(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 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); }); } } diff --git a/v2rayN/ServiceLib/Handler/WebDavHandler.cs b/v2rayN/ServiceLib/Manager/WebDavManager.cs similarity index 94% rename from v2rayN/ServiceLib/Handler/WebDavHandler.cs rename to v2rayN/ServiceLib/Manager/WebDavManager.cs index 865f4588..3f5c9ea3 100644 --- a/v2rayN/ServiceLib/Handler/WebDavHandler.cs +++ b/v2rayN/ServiceLib/Manager/WebDavManager.cs @@ -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 _instance = new(() => new()); - public static WebDavHandler Instance => _instance.Value; + private static readonly Lazy _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 GetClient() diff --git a/v2rayN/ServiceLib/Models/Config.cs b/v2rayN/ServiceLib/Models/Config.cs index 15996608..91b49b29 100644 --- a/v2rayN/ServiceLib/Models/Config.cs +++ b/v2rayN/ServiceLib/Models/Config.cs @@ -48,6 +48,7 @@ public class Config public List Inbound { get; set; } public List GlobalHotkeys { get; set; } public List CoreTypeItem { get; set; } + public SimpleDNSItem SimpleDNSItem { get; set; } #endregion other entities } diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index 8b95fdf9..72873da4 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -142,6 +142,7 @@ public class CoreTypeItem public class TunModeItem { public bool EnableTun { get; set; } + public bool AutoRoute { get; set; } = true; public bool StrictRoute { get; set; } = true; public string Stack { get; set; } public int Mtu { get; set; } @@ -164,7 +165,6 @@ public class RoutingBasicItem { public string DomainStrategy { get; set; } public string DomainStrategy4Singbox { get; set; } - public string DomainMatcher { get; set; } public string RoutingIndexId { get; set; } } @@ -253,3 +253,21 @@ public class WindowSizeItem public int Width { get; set; } public int Height { get; set; } } + +[Serializable] +public class SimpleDNSItem +{ + public bool? UseSystemHosts { get; set; } + public bool? AddCommonHosts { get; set; } + public bool? FakeIP { 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; } + public string? Hosts { get; set; } + public string? DirectExpectedIPs { get; set; } +} diff --git a/v2rayN/ServiceLib/Models/CoreInfo.cs b/v2rayN/ServiceLib/Models/CoreInfo.cs index faf899a9..eb4404cb 100644 --- a/v2rayN/ServiceLib/Models/CoreInfo.cs +++ b/v2rayN/ServiceLib/Models/CoreInfo.cs @@ -17,4 +17,5 @@ public class CoreInfo public string? Match { get; set; } public string? VersionArg { get; set; } public bool AbsolutePath { get; set; } + public IDictionary Environment { get; set; } = new Dictionary(); } diff --git a/v2rayN/ServiceLib/Models/DNSItem.cs b/v2rayN/ServiceLib/Models/DNSItem.cs index 59ab9c9b..9474d906 100644 --- a/v2rayN/ServiceLib/Models/DNSItem.cs +++ b/v2rayN/ServiceLib/Models/DNSItem.cs @@ -9,7 +9,7 @@ public class DNSItem public string Id { get; set; } public string Remarks { get; set; } - public bool Enabled { get; set; } = true; + public bool Enabled { get; set; } = false; public ECoreType CoreType { get; set; } public bool UseSystemHosts { get; set; } public string? NormalDNS { get; set; } diff --git a/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs b/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs new file mode 100644 index 00000000..f3881325 --- /dev/null +++ b/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs @@ -0,0 +1,18 @@ +using SQLite; + +namespace ServiceLib.Models; + +[Serializable] +public class FullConfigTemplateItem +{ + [PrimaryKey] + public string Id { get; set; } + + public string Remarks { get; set; } + public bool Enabled { get; set; } = false; + public ECoreType CoreType { get; set; } + public string? Config { get; set; } + public string? TunConfig { get; set; } + public bool? AddProxyOnly { get; set; } = false; + public string? ProxyDetour { get; set; } +} diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs index bc4358b5..998aa120 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -32,7 +32,7 @@ public class ProfileItem : ReactiveObject public string GetSummary() { var summary = $"[{(ConfigType).ToString()}] "; - var arrAddr = Address.Split('.'); + var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.'); var addr = arrAddr.Length switch { > 2 => $"{arrAddr.First()}***{arrAddr.Last()}", @@ -93,6 +93,7 @@ public class ProfileItem : ReactiveObject public string PublicKey { get; set; } public string ShortId { get; set; } public string SpiderX { get; set; } + public string Mldsa65Verify { get; set; } public string Extra { get; set; } public bool? MuxEnabled { get; set; } } diff --git a/v2rayN/ServiceLib/Models/SingboxConfig.cs b/v2rayN/ServiceLib/Models/SingboxConfig.cs index 9ea1157d..a5eec4ae 100644 --- a/v2rayN/ServiceLib/Models/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/SingboxConfig.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace ServiceLib.Models; public class SingboxConfig @@ -6,6 +8,7 @@ public class SingboxConfig public Dns4Sbox? dns { get; set; } public List inbounds { get; set; } public List outbounds { get; set; } + public List? endpoints { get; set; } public Route4Sbox route { get; set; } public Experimental4Sbox? experimental { get; set; } } @@ -29,14 +32,15 @@ public class Dns4Sbox public bool? independent_cache { get; set; } public bool? reverse_mapping { get; set; } public string? client_subnet { get; set; } - public Fakeip4Sbox? fakeip { get; set; } } public class Route4Sbox { + public Rule4Sbox? default_domain_resolver { get; set; } // or string public bool? auto_detect_interface { get; set; } public List rules { get; set; } public List? rule_set { get; set; } + public string? final { get; set; } } [Serializable] @@ -49,6 +53,7 @@ public class Rule4Sbox public string? mode { get; set; } public bool? ip_is_private { get; set; } public string? client_subnet { get; set; } + public int? rewrite_ttl { get; set; } public bool? invert { get; set; } public string? clash_mode { get; set; } public List? inbound { get; set; } @@ -67,6 +72,27 @@ public class Rule4Sbox public List? process_name { get; set; } public List? rule_set { get; set; } public List? rules { get; set; } + public string? action { get; set; } + public string? strategy { get; set; } + public List? sniffer { get; set; } + public string? rcode { get; set; } + public List? query_type { get; set; } + public List? answer { get; set; } + public List? ns { get; set; } + public List? extra { get; set; } + public string? method { get; set; } + public bool? no_drop { get; set; } + public bool? source_ip_is_private { get; set; } + public bool? ip_accept_any { get; set; } + public int? source_port { get; set; } + public List? source_port_range { get; set; } + public List? network_type { get; set; } + public bool? network_is_expensive { get; set; } + public bool? network_is_constrained { get; set; } + public List? wifi_ssid { get; set; } + public List? wifi_bssid { get; set; } + public bool? rule_set_ip_cidr_match_source { get; set; } + public bool? rule_set_ip_cidr_accept_empty { get; set; } } [Serializable] @@ -76,7 +102,6 @@ public class Inbound4Sbox public string tag { get; set; } public string listen { get; set; } public int? listen_port { get; set; } - public string? domain_strategy { get; set; } public string interface_name { get; set; } public List? address { get; set; } public int? mtu { get; set; } @@ -84,8 +109,6 @@ public class Inbound4Sbox public bool? strict_route { get; set; } public bool? endpoint_independent_nat { get; set; } public string? stack { get; set; } - public bool? sniff { get; set; } - public bool? sniff_override_destination { get; set; } public List users { get; set; } } @@ -95,10 +118,8 @@ public class User4Sbox public string password { get; set; } } -public class Outbound4Sbox +public class Outbound4Sbox : BaseServer4Sbox { - public string type { get; set; } - public string tag { get; set; } public string? server { get; set; } public int? server_port { get; set; } public List? server_ports { get; set; } @@ -113,7 +134,6 @@ public class Outbound4Sbox public int? recv_window_conn { get; set; } public int? recv_window { get; set; } public bool? disable_mtu_discovery { get; set; } - public string? detour { get; set; } public string? method { get; set; } public string? username { get; set; } public string? password { get; set; } @@ -121,21 +141,36 @@ public class Outbound4Sbox public string? version { get; set; } public string? network { get; set; } public string? packet_encoding { get; set; } - public List? local_address { get; set; } - public string? private_key { get; set; } - public string? peer_public_key { get; set; } - public List? reserved { get; set; } - public int? mtu { get; set; } public string? plugin { get; set; } public string? plugin_opts { get; set; } - public Tls4Sbox? tls { get; set; } - public Multiplex4Sbox? multiplex { get; set; } - public Transport4Sbox? transport { get; set; } - public HyObfs4Sbox? obfs { get; set; } public List? outbounds { get; set; } public bool? interrupt_exist_connections { get; set; } } +public class Endpoints4Sbox : BaseServer4Sbox +{ + public bool? system { get; set; } + public string? name { get; set; } + public int? mtu { get; set; } + public List address { get; set; } + public string private_key { get; set; } + public int? listen_port { get; set; } + public string? udp_timeout { get; set; } + public int? workers { get; set; } + public List peers { get; set; } +} + +public class Peer4Sbox +{ + public string address { get; set; } + public int port { get; set; } + public string public_key { get; set; } + public string? pre_shared_key { get; set; } + public List allowed_ips { get; set; } + public int? persistent_keepalive_interval { get; set; } + public List reserved { get; set; } +} + public class Tls4Sbox { public bool enabled { get; set; } @@ -144,6 +179,9 @@ public class Tls4Sbox public List? alpn { get; set; } public Utls4Sbox? utls { get; set; } public Reality4Sbox? reality { get; set; } + public bool? fragment { get; set; } + public string? fragment_fallback_delay { get; set; } + public bool? record_fragment { get; set; } } public class Multiplex4Sbox @@ -191,15 +229,28 @@ public class HyObfs4Sbox public string? password { get; set; } } -public class Server4Sbox +public class Server4Sbox : BaseServer4Sbox { - public string? tag { get; set; } + public string? inet4_range { get; set; } + public string? inet6_range { get; set; } + public string? client_subnet { get; set; } + public string? server { get; set; } + public new string? domain_resolver { get; set; } + [JsonPropertyName("interface")] public string? Interface { get; set; } + public int? server_port { get; set; } + public string? path { get; set; } + public Headers4Sbox? headers { get; set; } + + // public List? path { get; set; } // hosts + public Dictionary>? predefined { get; set; } + + // Deprecated public string? address { get; set; } + public string? address_resolver { get; set; } public string? address_strategy { get; set; } public string? strategy { get; set; } - public string? detour { get; set; } - public string? client_subnet { get; set; } + // Deprecated End } public class Experimental4Sbox @@ -229,13 +280,6 @@ public class Stats4Sbox public List? users { get; set; } } -public class Fakeip4Sbox -{ - public bool enabled { get; set; } - public string inet4_range { get; set; } - public string inet6_range { get; set; } -} - public class CacheFile4Sbox { public bool enabled { get; set; } @@ -254,3 +298,33 @@ public class Ruleset4Sbox public string? download_detour { get; set; } public string? update_interval { get; set; } } + +public abstract class DialFields4Sbox +{ + public string? detour { get; set; } + public string? bind_interface { get; set; } + public string? inet4_bind_address { get; set; } + public string? inet6_bind_address { get; set; } + public int? routing_mark { get; set; } + public bool? reuse_addr { get; set; } + public string? netns { get; set; } + public string? connect_timeout { get; set; } + public bool? tcp_fast_open { get; set; } + public bool? tcp_multi_path { get; set; } + public bool? udp_fragment { get; set; } + public Rule4Sbox? domain_resolver { get; set; } // or string + public string? network_strategy { get; set; } + public List? network_type { get; set; } + public List? fallback_network_type { get; set; } + public string? fallback_delay { get; set; } + public Tls4Sbox? tls { get; set; } + public Multiplex4Sbox? multiplex { get; set; } + public Transport4Sbox? transport { get; set; } + public HyObfs4Sbox? obfs { get; set; } +} + +public abstract class BaseServer4Sbox : DialFields4Sbox +{ + public string type { get; set; } + public string tag { get; set; } +} diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs index 2e5ed4b4..cff3cf8b 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -5,7 +5,7 @@ namespace ServiceLib.Models; public class V2rayConfig { public Log4Ray log { get; set; } - public object dns { get; set; } + public Dns4Ray dns { get; set; } public List inbounds { get; set; } public List outbounds { get; set; } public Routing4Ray routing { get; set; } @@ -203,7 +203,15 @@ public class Response4Ray public class Dns4Ray { - public List servers { get; set; } + public Dictionary? hosts { get; set; } + public List servers { get; set; } + public string? clientIp { get; set; } + public string? queryStrategy { get; set; } + public bool? disableCache { get; set; } + public bool? disableFallback { get; set; } + public bool? disableFallbackIfMatch { get; set; } + public bool? useSystemHosts { get; set; } + public string? tag { get; set; } } public class DnsServer4Ray @@ -211,14 +219,20 @@ public class DnsServer4Ray public string? address { get; set; } public List? domains { get; set; } public bool? skipFallback { get; set; } + public List? expectedIPs { get; set; } + public List? unexpectedIPs { get; set; } + public string? clientIp { get; set; } + public string? queryStrategy { get; set; } + public int? timeoutMs { get; set; } + public bool? disableCache { get; set; } + public bool? finalQuery { get; set; } + public string? tag { get; set; } } public class Routing4Ray { public string domainStrategy { get; set; } - public string? domainMatcher { get; set; } - public List rules { get; set; } public List? balancers { get; set; } @@ -340,6 +354,7 @@ public class TlsSettings4Ray public string? publicKey { get; set; } public string? shortId { get; set; } public string? spiderX { get; set; } + public string? mldsa65Verify { get; set; } } public class TcpSettings4Ray diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index afaffebf..1a6bb674 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -186,6 +186,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please fill in the correct config template 的本地化字符串。 + /// + public static string FillCorrectConfigTemplateText { + get { + return ResourceManager.GetString("FillCorrectConfigTemplateText", resourceCulture); + } + } + /// /// 查找类似 Please fill in the correct custom DNS 的本地化字符串。 /// @@ -654,6 +663,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add [Anytls] Configuration 的本地化字符串。 + /// + public static string menuAddAnytlsServer { + get { + return ResourceManager.GetString("menuAddAnytlsServer", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration Configuration 的本地化字符串。 /// @@ -924,6 +942,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Full Config Template Setting 的本地化字符串。 + /// + public static string menuFullConfigTemplate { + get { + return ResourceManager.GetString("menuFullConfigTemplate", resourceCulture); + } + } + /// /// 查找类似 Global Hotkey Setting 的本地化字符串。 /// @@ -1824,6 +1851,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Start parsing and processing subscription content 的本地化字符串。 + /// + public static string MsgStartParsingSubscription { + get { + return ResourceManager.GetString("MsgStartParsingSubscription", resourceCulture); + } + } + /// /// 查找类似 Started updating {0}... 的本地化字符串。 /// @@ -2203,11 +2239,20 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Sudo password has been verified successfully, please ignore the incorrect password prompts! 的本地化字符串。 + /// 查找类似 Add Common DNS Hosts 的本地化字符串。 /// - public static string SudoPwdVerfiedSuccessTip { + public static string TbAddCommonDNSHosts { get { - return ResourceManager.GetString("SudoPwdVerfiedSuccessTip", resourceCulture); + return ResourceManager.GetString("TbAddCommonDNSHosts", resourceCulture); + } + } + + /// + /// 查找类似 Do Not Add Non-Proxy Protocol Outbound 的本地化字符串。 + /// + public static string TbAddProxyProtocolOutboundOnly { + get { + return ResourceManager.GetString("TbAddProxyProtocolOutboundOnly", resourceCulture); } } @@ -2247,6 +2292,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Apply to Proxy Domains Only 的本地化字符串。 + /// + public static string TbApplyProxyDomainsOnly { + get { + return ResourceManager.GetString("TbApplyProxyDomainsOnly", resourceCulture); + } + } + /// /// 查找类似 Auto refresh 的本地化字符串。 /// @@ -2274,6 +2328,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Block SVCB and HTTPS Queries 的本地化字符串。 + /// + public static string TbBlockSVCBHTTPSQueries { + get { + return ResourceManager.GetString("TbBlockSVCBHTTPSQueries", resourceCulture); + } + } + + /// + /// 查找类似 Prevent domain-based routing rules from failing 的本地化字符串。 + /// + public static string TbBlockSVCBHTTPSQueriesTips { + get { + return ResourceManager.GetString("TbBlockSVCBHTTPSQueriesTips", resourceCulture); + } + } + /// /// 查找类似 Browse 的本地化字符串。 /// @@ -2328,6 +2400,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Enable Custom DNS 的本地化字符串。 + /// + public static string TbCustomDNSEnable { + get { + return ResourceManager.GetString("TbCustomDNSEnable", resourceCulture); + } + } + + /// + /// 查找类似 Custom DNS Enabled, This Page's Settings Invalid 的本地化字符串。 + /// + public static string TbCustomDNSEnabledPageInvalid { + get { + return ResourceManager.GetString("TbCustomDNSEnabledPageInvalid", resourceCulture); + } + } + /// /// 查找类似 Display GUI 的本地化字符串。 /// @@ -2346,6 +2436,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 DNS Hosts: ("domain1 ip1 ip2" per line) 的本地化字符串。 + /// + public static string TbDNSHostsConfig { + get { + return ResourceManager.GetString("TbDNSHostsConfig", resourceCulture); + } + } + /// /// 查找类似 Supports DNS Object; Click to view documentation 的本地化字符串。 /// @@ -2364,15 +2463,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Domain Matcher 的本地化字符串。 - /// - public static string TbdomainMatcher { - get { - return ResourceManager.GetString("TbdomainMatcher", resourceCulture); - } - } - /// /// 查找类似 Domain strategy 的本地化字符串。 /// @@ -2391,6 +2481,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Domestic DNS 的本地化字符串。 + /// + public static string TbDomesticDNS { + get { + return ResourceManager.GetString("TbDomesticDNS", resourceCulture); + } + } + /// /// 查找类似 Edit 的本地化字符串。 /// @@ -2409,6 +2508,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 FakeIP 的本地化字符串。 + /// + public static string TbFakeIP { + get { + return ResourceManager.GetString("TbFakeIP", resourceCulture); + } + } + /// /// 查找类似 Fingerprint 的本地化字符串。 /// @@ -2427,6 +2535,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 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. 的本地化字符串。 + /// + public static string TbFullConfigTemplateDesc { + get { + return ResourceManager.GetString("TbFullConfigTemplateDesc", resourceCulture); + } + } + + /// + /// 查找类似 Enable Full Config Template 的本地化字符串。 + /// + public static string TbFullConfigTemplateEnable { + get { + return ResourceManager.GetString("TbFullConfigTemplateEnable", resourceCulture); + } + } + /// /// 查找类似 Global Hotkey Settings 的本地化字符串。 /// @@ -2517,6 +2643,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Mldsa65Verify 的本地化字符串。 + /// + public static string TbMldsa65Verify { + get { + return ResourceManager.GetString("TbMldsa65Verify", resourceCulture); + } + } + /// /// 查找类似 Transport protocol(network) 的本地化字符串。 /// @@ -2625,6 +2760,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 v2ray Full Config Template 的本地化字符串。 + /// + public static string TbRayFullConfigTemplate { + get { + return ResourceManager.GetString("TbRayFullConfigTemplate", resourceCulture); + } + } + + /// + /// 查找类似 Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document 的本地化字符串。 + /// + public static string TbRayFullConfigTemplateDesc { + get { + return ResourceManager.GetString("TbRayFullConfigTemplateDesc", resourceCulture); + } + } + /// /// 查找类似 Alias (remarks) 的本地化字符串。 /// @@ -2634,6 +2787,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Remote DNS 的本地化字符串。 + /// + public static string TbRemoteDNS { + get { + return ResourceManager.GetString("TbRemoteDNS", resourceCulture); + } + } + /// /// 查找类似 Camouflage domain(host) 的本地化字符串。 /// @@ -2742,6 +2904,87 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。 + /// + public static string TbSBDirectResolveStrategy { + get { + return ResourceManager.GetString("TbSBDirectResolveStrategy", resourceCulture); + } + } + + /// + /// 查找类似 The sing-box DoH resolution server can be overwritten 的本地化字符串。 + /// + public static string TbSBDoHOverride { + get { + return ResourceManager.GetString("TbSBDoHOverride", resourceCulture); + } + } + + /// + /// 查找类似 sing-box DoH Resolver Server 的本地化字符串。 + /// + public static string TbSBDoHResolverServer { + get { + return ResourceManager.GetString("TbSBDoHResolverServer", resourceCulture); + } + } + + /// + /// 查找类似 Fallback DNS Resolution, Suggest IP 的本地化字符串。 + /// + public static string TbSBFallbackDNSResolve { + get { + return ResourceManager.GetString("TbSBFallbackDNSResolve", resourceCulture); + } + } + + /// + /// 查找类似 sing-box Full Config Template 的本地化字符串。 + /// + public static string TbSBFullConfigTemplate { + get { + return ResourceManager.GetString("TbSBFullConfigTemplate", resourceCulture); + } + } + + /// + /// 查找类似 Add Outbound and Endpoint Config Only, Click to view the document 的本地化字符串。 + /// + public static string TbSBFullConfigTemplateDesc { + get { + return ResourceManager.GetString("TbSBFullConfigTemplateDesc", resourceCulture); + } + } + + /// + /// 查找类似 Resolve Outbound Domains 的本地化字符串。 + /// + public static string TbSBOutboundDomainResolve { + get { + return ResourceManager.GetString("TbSBOutboundDomainResolve", resourceCulture); + } + } + + /// + /// 查找类似 Outbound DNS Resolution (sing-box) 的本地化字符串。 + /// + public static string TbSBOutboundsResolverDNS { + get { + return ResourceManager.GetString("TbSBOutboundsResolverDNS", resourceCulture); + } + } + + /// + /// 查找类似 sing-box Remote Resolution Strategy 的本地化字符串。 + /// + public static string TbSBRemoteResolveStrategy { + get { + return ResourceManager.GetString("TbSBRemoteResolveStrategy", resourceCulture); + } + } + /// /// 查找类似 Encryption method (security) 的本地化字符串。 /// @@ -2778,6 +3021,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Select Profile 的本地化字符串。 + /// + public static string TbSelectProfile { + get { + return ResourceManager.GetString("TbSelectProfile", resourceCulture); + } + } + /// /// 查找类似 Set system proxy 的本地化字符串。 /// @@ -3067,7 +3319,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Use Xray and enable non-Tun mode, which conflicts with the group previous proxy 的本地化字符串。 + /// 查找类似 which conflicts with the group previous proxy 的本地化字符串。 /// public static string TbSettingsEnableFragmentTips { get { @@ -3489,6 +3741,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Auto Route 的本地化字符串。 + /// + public static string TbSettingsTunAutoRoute { + get { + return ResourceManager.GetString("TbSettingsTunAutoRoute", resourceCulture); + } + } + /// /// 查找类似 Tun Mode settings 的本地化字符串。 /// @@ -3498,6 +3759,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 MTU 的本地化字符串。 + /// + public static string TbSettingsTunMtu { + get { + return ResourceManager.GetString("TbSettingsTunMtu", resourceCulture); + } + } + + /// + /// 查找类似 Stack 的本地化字符串。 + /// + public static string TbSettingsTunStack { + get { + return ResourceManager.GetString("TbSettingsTunStack", resourceCulture); + } + } + + /// + /// 查找类似 Strict Route 的本地化字符串。 + /// + public static string TbSettingsTunStrictRoute { + get { + return ResourceManager.GetString("TbSettingsTunStrictRoute", resourceCulture); + } + } + /// /// 查找类似 Enable UDP 的本地化字符串。 /// @@ -3525,6 +3813,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Set Upstream Proxy Tag 的本地化字符串。 + /// + public static string TbSetUpstreamProxyDetour { + get { + return ResourceManager.GetString("TbSetUpstreamProxyDetour", resourceCulture); + } + } + /// /// 查找类似 Short Id 的本地化字符串。 /// @@ -3687,6 +3984,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Validate Regional Domain IPs 的本地化字符串。 + /// + public static string TbValidateDirectExpectedIPs { + get { + return ResourceManager.GetString("TbValidateDirectExpectedIPs", resourceCulture); + } + } + + /// + /// 查找类似 When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs 的本地化字符串。 + /// + public static string TbValidateDirectExpectedIPsDesc { + get { + return ResourceManager.GetString("TbValidateDirectExpectedIPsDesc", resourceCulture); + } + } + + /// + /// 查找类似 xray Freedom Resolution Strategy 的本地化字符串。 + /// + public static string TbXrayFreedomResolveStrategy { + get { + return ResourceManager.GetString("TbXrayFreedomResolveStrategy", resourceCulture); + } + } + /// /// 查找类似 The delay: {0} ms, {1} 的本地化字符串。 /// @@ -3696,6 +4020,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Advanced DNS Settings 的本地化字符串。 + /// + public static string ThAdvancedDNSSettings { + get { + return ResourceManager.GetString("ThAdvancedDNSSettings", resourceCulture); + } + } + + /// + /// 查找类似 Basic DNS Settings 的本地化字符串。 + /// + public static string ThBasicDNSSettings { + get { + return ResourceManager.GetString("ThBasicDNSSettings", resourceCulture); + } + } + /// /// 查找类似 Active 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 02e417fe..f4b279da 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1,17 +1,17 @@ - @@ -822,9 +822,6 @@ تنظیم کردن به عنوان قانون فعال - - تطبیق دامنه - استراتژی دامنه @@ -1059,6 +1056,18 @@ لطفاً مطمئن شوید که ملاحظات وجود دارند و منحصر به فرد هستند + + مسیریابی خودکار + + + مسیریابی سخت‌گیرانه + + + پشته شبکه + + + MTU + فعال سازی additional Inbound @@ -1102,7 +1111,7 @@ افزودن سرور [HTTP] - از Xray استفاده کنید و حالت non-Tun را فعال کنید، که با پراکسی قبلی گروه در تضاد است + which conflicts with the group previous proxy فعال کردن فرگمنت @@ -1392,10 +1401,115 @@ Can fill in the configuration remarks, please make sure it exist and are unique - - Sudo password has been verified successfully, please ignore the incorrect password prompts! - Incorrect password, please try again. + + Mldsa65Verify + + + Add [Anytls] Configuration + + + Remote DNS + + + Domestic DNS + + + Outbound DNS Resolution (sing-box) + + + Resolve Outbound Domains + + + sing-box DoH Resolver Server + + + Fallback DNS Resolution, Suggest IP + + + xray Freedom Resolution Strategy + + + sing-box Direct Resolution Strategy + + + sing-box Remote Resolution Strategy + + + Add Common DNS Hosts + + + The sing-box DoH resolution server can be overwritten + + + FakeIP + + + Block SVCB and HTTPS Queries + + + DNS Hosts: ("domain1 ip1 ip2" per line) + + + Apply to Proxy Domains Only + + + Basic DNS Settings + + + Advanced DNS Settings + + + Validate Regional Domain IPs + + + When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + + + Enable Custom DNS + + + Custom DNS Enabled, This Page's Settings Invalid + + + Prevent domain-based routing rules from failing + + + Please fill in the correct config template + + + Full Config Template Setting + + + Enable Full Config Template + + + v2ray Full Config Template + + + Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + + + Do Not Add Non-Proxy Protocol Outbound + + + Set Upstream Proxy Tag + + + sing-box Full Config Template + + + Add Outbound and Endpoint Config Only, Click to view the document + + + 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. + + + Start parsing and processing subscription content + + + Select Profile + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 85ea3335..800fa00f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1,17 +1,17 @@ - @@ -118,55 +118,55 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - A megosztási link másolása a vágólapra sikerült + Sikeresen exportálta a megosztási linket a vágólapra - Kérjük, először ellenőrizze a szerverbeállításokat + Kérjük, először ellenőrizze a konfigurációs beállításokat. - Érvénytelen konfigurációs formátum + Érvénytelen konfigurációs formátum. - Vegye figyelembe, hogy a testreszabott konfiguráció teljes mértékben az ön konfigurációjától függ, és nem működik minden beállítással. Ha a rendszer proxyját szeretné használni, kérjük, módosítsa a figyelési portot kézzel. + Ne feledje, hogy az egyéni konfiguráció teljes mértékben a saját konfigurációjától függ, és nem működik minden beállítással. Ha a rendszerproxyt szeretné használni, kérjük, manuálisan módosítsa a figyelő portot. Letöltés... - Nem sikerült a konfigurációs fájl átalakítása + Nem sikerült a konfigurációs fájl konvertálása - Nem sikerült a gyári konfigurációs fájl generálása + Nem sikerült az alapértelmezett konfigurációs fájl generálása - Nem sikerült lekérni a gyári konfigurációt + Nem sikerült lekérni az alapértelmezett konfigurációt - Nem sikerült a testreszabott konfigurációs szerver importálása + Nem sikerült importálni az egyéni konfigurációt - Nem sikerült a konfigurációs fájl beolvasása + Nem sikerült olvasni a konfigurációs fájlt - Kérjük, adja meg a helyes formátumú szerverportot + Kérjük, adja meg a helyes port formátumot. - Kérjük, adja meg a helyi figyelési portot + Kérjük, adja meg a helyi figyelő portot. - Kérjük, töltse ki a jelszót + Kérjük, adja meg a jelszót. - Kérjük, adja meg a szerver címét + Kérjük, adja meg a címet. - Kérjük, adja meg a felhasználói azonosítót + Kérjük, adja meg a felhasználói azonosítót. - Nem helyes a konfiguráció, kérjük, ellenőrizze + Ez nem a megfelelő konfiguráció, kérjük, ellenőrizze - Kezi konfiguráció + Kezdeti konfiguráció {0} {1} már naprakész. @@ -187,13 +187,13 @@ Típus - Előfizetési csoport + Előfizetés csoport - Letöltési forgalom ma + Mai letöltési forgalom - Feltöltési forgalom ma + Mai feltöltési forgalom Összes letöltési forgalom @@ -205,37 +205,37 @@ Szállítás - A Core letöltése sikerült + A Core sikeresen letöltve - Nem sikerült az előfizetési tartalom importálása + Nem sikerült importálni az előfizetés tartalmát - Az előfizetési tartalom sikeresen lekérve + Az előfizetés tartalma sikeresen lekérve - Nincs érvényes előfizetés meghatározva + Nincs érvényes előfizetés beállítva - Resolve {0} sikeresen + Resolved {0} successfully - Kezdje el az előfizetések lekérését + Előfizetések lekérdezése elindult - Kezdődik a frissítés: {0}... + Frissítés indítása: {0}... - Érvénytelen előfizetési tartalom + Érvénytelen előfizetés tartalom - Kicsomagolás folyamatban...... + Kicsomagolás... - Az előfizetések frissítése befejeződött + Előfizetés frissítése befejeződött - Az előfizetések frissítése elkezdődött + Előfizetés frissítése elindult A Core sikeresen frissítve @@ -244,16 +244,16 @@ A Core sikeresen frissítve! Szolgáltatás újraindítása... - Nem-VMess vagy ss protokoll + Nem VMess vagy SS protokoll - A Core fájl (fájl neve: {1}) nem található a mappában ({0}), kérjük, töltse le és helyezze a mappába, letöltési cím: {2} + A Core fájl (fájlnév: {1}) nem található a mappában ({0}), kérjük, töltse le és helyezze a mappába, letöltési cím: {2} - Beolvasás befejeződött, nem található érvényes QR-kód + Szkennelés befejeződött, nem található érvényes QR kód - A művelet nem sikerült, kérjük, ellenőrizze és próbálja újra + Művelet sikertelen, kérjük, ellenőrizze és próbálja újra Kérjük, töltse ki a megjegyzéseket @@ -262,97 +262,97 @@ Kérjük, válassza ki a titkosítási módszert - Kérjük, válasszon egy protokollt + Kérjük, válassza ki a protokollt - Kérjük, először válassza ki a szervert + Kérjük, először válassza ki a konfigurációt - A szerverek duplikáció eltávolítása befejeződött. Régi: {0}, Új: {1}. + Konfigurációk deduplikálása befejeződött. Régi: {0}, Új: {1}. - Biztosan el kívánja távolítani a szervert? + Biztosan eltávolítja a konfigurációt? - A kliens konfigurációs fájl mentve van itt: {0} + Az ügyfélkonfigurációs fájl mentése itt: {0} Szolgáltatás indítása ({0})... - Konfiguráció sikeres {0} + Konfiguráció sikeres. {0} - A testreszabott konfigurációs szerver sikeresen importálva + Egyéni konfiguráció sikeresen importálva - {0} szerver importálva a vágólapról + {0} konfiguráció importálva a vágólapról - A megosztott link sikeresen importálva a beolvasás során + Sikeresen beolvasta és importálta a megosztott linket A késleltetés: {0} ms, {1} - A művelet sikeres + Művelet sikeres - Kérjük, válassza ki a szabályokat + Kérjük, válasszon szabályokat - Biztosan el kívánja távolítani a szabályokat? + Biztosan eltávolítja a szabályokat? - {0}, az egyik kötelező. + {0}, az egyik kötelező mező. Megjegyzések - URL (Opcionális) + URL (opcionális) - Szám + Darabszám Kérjük, adja meg az URL-t - Hozzá kívánja adni a szabályokat? Válassza az igent az összefűzéshez, válassza a másikat a cseréhez + Hozzá szeretne fűzni szabályokat? Igen a hozzáfűzéshez, nem a cseréhez. - GeoFile letöltése: {0} sikerült + A GeoFile: {0} sikeresen letöltve Információ - Testreszabott ikona + Egyéni ikon - Kérjük, adja meg a helyes egyedi DNS-t + Kérjük, töltse ki a helyes egyéni DNS-t - *ws/httpupgrade/xhttp útvonal + *ws/http upgrade/xhttp elérési út - *h2 útvonal + *h2 elérési út - *QUIC kulcs/KCP mag + *QUIC kulcs/KCP seed - *grpc szolgáltatás neve + *grpc szolgáltatásnév - *http host, vesszővel elválasztva (,) + *http host vesszővel elválasztva (,) - *ws/httpupgrade/xhttp host + *ws/http upgrade/xhttp host - *h2 host, vesszővel elválasztva (,) + *h2 host vesszővel elválasztva (,) *QUIC biztonság @@ -373,19 +373,19 @@ TLS - *kcp mag + *kcp seed - A globális billentyűparancs {0} bejegyzése nem sikerült, ok: {1} + Globális gyorsbillentyű {0} regisztrációja sikertelen, ok: {1} - A globális billentyűparancs {0} sikeresen bejegyezve + Globális gyorsbillentyű {0} sikeresen regisztrálva Összes - Kérjük, keresse meg a szerver konfigurációjának importálásához + Kérjük, tallózzon a konfiguráció importálásához Tesztelés... @@ -397,7 +397,7 @@ Helyi - Szerver szűrő, nyomja meg az Enter-t a végrehajtáshoz + Konfigurációs szűrő, Enter billentyűvel végrehajtható Frissítés ellenőrzése @@ -409,13 +409,13 @@ Kilépés - Globális billentyűparancs beállítása + Globális gyorsbillentyű beállítás - Segítség + Súgó - Opció beállítása + Opció beállítás Promóció @@ -424,25 +424,25 @@ Újratöltés - Útvonal beállítása + Útválasztási beállítás - Szerverek + Konfigurációk Beállítások - Frissítse a jelenlegi előfizetést proxy nélkül + Aktuális előfizetés frissítése proxy nélkül - Frissítse a jelenlegi előfizetést proxyval + Aktuális előfizetés frissítése proxyval Előfizetési csoport - Előfizetési csoport beállítások + Előfizetési csoport beállításai Előfizetések frissítése proxy nélkül @@ -457,7 +457,7 @@ Rendszerproxy törlése - Ne változtassa meg a rendszer proxyját + Ne változtassa meg a rendszerproxyt PAC mód @@ -472,64 +472,64 @@ Nyelv (Újraindítás) - Importálja a megosztott linkeket a vágólapról (Ctrl+V) + Megosztási linkek importálása vágólapról (Ctrl+V) - QR kód beolvasása a képernyőn (Ctrl+S) + QR kód beolvasása a képernyőről (Ctrl+S) - Kiválasztott szerver klónozása + Kijelölt konfiguráció klónozása - Duplikált szerverek eltávolítása + Ismétlődő konfigurációk eltávolítása - Kiválasztott szerver(törlés) + Kijelölt konfigurációk eltávolítása (Delete) - Aktív szerverként beállítani (Enter) + Beállítás aktív konfigurációként (Enter) - Minden szolgáltatási statisztika törlése + Összes szolgáltatás statisztika törlése - A szerverek valós késleltetésének tesztelése (Ctrl+R) + Konfigurációk valós késleltetésének tesztelése (Ctrl+R) - Sorrend szerinti tesztelési eredmény + Rendezés teszteredmény szerint - A szerverek letöltési sebességének tesztelése (Ctrl+T) + Konfigurációk letöltési sebességének tesztelése (Ctrl+T) - Szerver tesztelése tcping-gel (Ctrl+O) + Konfigurációk tesztelése tcpinggel (Ctrl+O) - A kiválasztott szerver exportálása a teljes konfigurációhoz + Kijelölt konfiguráció exportálása teljes konfigurációként - A megosztási link kattintásra másolása (Ctrl+C) + Megosztási link exportálása vágólapra (Ctrl+C) - Testreszabott konfigurációs szerver hozzáadása + Egyéni konfiguráció hozzáadása - [Shadowsocks] szerver hozzáadása + [Shadowsocks] konfiguráció hozzáadása - [SOCKS] szerver hozzáadása + [SOCKS] konfiguráció hozzáadása - [Trojan] szerver hozzáadása + [Trojan] konfiguráció hozzáadása - [VLESS] szerver hozzáadása + [VLESS] konfiguráció hozzáadása - [VMess] szerver hozzáadása + [VMess] konfiguráció hozzáadása - Mindet kijelölni (Ctrl+A) + Összes kijelölése (Ctrl+A) Összes törlése @@ -556,19 +556,19 @@ Megosztás - A frissítés engedélyezése + Frissítés engedélyezése - Sorrend + Rendezés - Felhasználói ügynök + User Agent - Cancel + Mégsem - Megerősít + Megerősítés Szállítás @@ -577,40 +577,40 @@ Cím - Engedélyezett nem biztosított + Nem biztonságos engedélyezése ALPN - AlterID + Alter ID Ujjlenyomat - Álcázás típusa + Álcázási típus UUID(id) - Szállítási protokoll (hálózat) + Szállítási protokoll(hálózat) - Útvonal + Elérési út Port - Álnév (megjegyzések) + Alias (megjegyzések) - Álcázott domain (hoszt) + Álcázási tartomány(host) - Kódolási módszer (biztonság) + Titkosítási módszer (biztonság) SNI @@ -622,10 +622,10 @@ *Alapértelmezett érték tcp - Yalap típusa + Core Típus - Folyam + Flow Generálás @@ -634,34 +634,34 @@ Jelszó - Jelszó (opcionális) + Jelszó(Opcionális) UUID(id) - Kódolás + Titkosítás - Felhasználó (opcionális) + Felhasználó(Opcionális) - Kódolás + Titkosítás Socks port - * Ezen érték beállítása után egy socks szolgáltatás indul el Xray/sing-box(Tun) használatával, amely funkcionalitásokat biztosít, mint például a sebesség megjelenítése + * A beállítás után egy socks szolgáltatás indul az Xray/sing-box(Tun) segítségével, hogy olyan funkciókat biztosítson, mint a sebességkijelzés - Böngészés + Tallózás Szerkesztés - Fejlett proxy beállítások, protokoll választás (opcionális) + Haladó proxy beállítások, protokoll kiválasztása (opcionális) Kapcsolatok engedélyezése a LAN-ról @@ -670,61 +670,61 @@ Automatikus elrejtés indításkor - Geo automatikus frissítési időköz (órák) + Geo fájlok automatikus frissítési intervalluma (órák) - Yalap: alapbeállítások + Core: alapbeállítások V2ray DNS beállítások - Yalap: KCP beállítások + Core: KCP beállítások - Yalap típusa beállítások + Core Típus beállítások - Engedélyezett a nem biztonságos + Nem biztonságos engedélyezése - Kimenő szabadság domain stratégia + Kimenő Freedom tartomány stratégia - Otomatikusan állítja be az oszlop szélességét előfizetés frissítése után + Oszlopszélesség automatikus beállítása előfizetés frissítése után - Frissítések ellenőrzése a kiadás előtt + Előzetes kiadás frissítések ellenőrzése Kivétel - Kivétel. Ne használjon proxy szervert a címek esetében, amelyek pontosan itt kezdődnek, használjon pontosvesszőt (;) + Kivételek: Ne használjon proxy szervert a következő címmel kezdődő címekhez. Pontosvesszővel (;) válassza el a bejegyzéseket. - Display real-time speed + Valós idejű sebesség megjelenítése (újraindítást igényel) - Régi megőrzése a deduplikáció során + Régebbi bejegyzések megtartása deduplikáláskor - Napló engedélyezése + Naplózás engedélyezése - Napló szint + Naplózási szint - Mux multiplexelés bekapcsolása + Mux Multiplexing bekapcsolása v2rayN beállítások - Hitelesítő jelszó + Hitelesítési jelszó - Testreszabott DNS (több, vesszővel elválasztva (,)) + Egyéni DNS (több, vesszővel (,) elválasztva) Win10 UWP Loopback beállítása @@ -733,34 +733,34 @@ Sniffing bekapcsolása - Keverék Port + Vegyes port - Indításkor indítani + Indítás rendszerindításkor - Statisztikák engedélyezése (indítás szükséges) + Forgalmi statisztikák engedélyezése (újraindítást igényel) - Előfizetés átalakító URL + Előfizetés konverziós URL - Rendszer proxy beállítások + Rendszerproxy beállítások Biztonsági protokoll TLS v1.3 engedélyezése (előfizetés/frissítés) - A tálcán megjelenő menü szerverek megjelenítési korlátozása + Tálca jobb egérgombos menü konfigurációk megjelenítési limitje UDP engedélyezése - Hitelesítő felhasználó + Hitelesítési felhasználó - Rendszer proxy törlése + Rendszerproxy törlése GUI megjelenítése @@ -769,40 +769,40 @@ Globális gyorsbillentyű beállítások - Állítsa be közvetlenül a billentyűzet megnyomásával, indítás után érvényes + Közvetlenül beállítható billentyűnyomással; újraindítás után lép életbe - Ne változtassa meg a rendszer proxy-t + Ne változtassa meg a rendszerproxyt Visszaállítás - Rendszer proxy beállítása + Rendszerproxy beállítása - PAC módban + PAC mód - Megosztási szerver (Ctrl+F) + Konfiguráció megosztása (Ctrl+F) - Útvonal + Útválasztás - Nem adminisztrátorként fut + Nem rendszergazdaként fut - Futtatás adminisztrátorként + Futtatás rendszergazdaként - Áthelyezés a végére (B) + Mozgatás alulra (B) Le (D) - Áthelyezés a tetejére (T) + Mozgatás felülre (T) Fel (U) @@ -817,28 +817,25 @@ Hozzáadás - Fejlett szabályok importálása + Szabályok importálása - Kiválasztott eltávolítása (Törlés) + Kijelölt eltávolítása (Delete) - Aktív szabályként beállítása (Enter) - - - Domain illesztő + Beállítás aktív szabályként (Enter) - Domain stratégia + Tartomány stratégia - Előre definiált szabálykészletlista + Előre definiált szabálykészlet lista - * Állítsa be a szabályokat, vesszővel elválasztva (,); A reguláris kifejezésben a vesszőt <COMMA> -ra cseréli + *Szabályok elválasztása vesszővel (,); Szó szerinti vesszőhöz használja a <COMMA>-t; Előtag # a szabály figyelmen kívül hagyásához - Szabályok importálása a vágólapról + Szabályok importálása vágólapról Szabályok importálása fájlból @@ -847,61 +844,61 @@ Szabályok importálása előfizetési URL-ből - Szabálybeállítások + Szabály beállítások Szabály hozzáadása - Kiválasztott szabályok exportálása + Kijelölt szabályok exportálása Szabálylista - Szabály eltávolítása (Törlés) + Szabály eltávolítása (Delete) - RoutingRuleDetailsSetting + Útválasztási szabály részleteinek beállítása - Domain, ip, folyamat automatikusan rendezve lesz a mentés során + Tartomány, IP, folyamat automatikusan rendeződik mentéskor - Szabály objektum dokumentáció + Szabály objektum dokumentum - Támogatja a DnsObject, kattintson a dokumentum megtekintésére + Támogatja a DNS objektumot; Kattintson a dokumentáció megtekintéséhez - Csoporthoz, kérlek, hagyd üresen ezt a mezőt + Csoport esetén hagyja üresen - Útvonal beállítása megváltozott + Útválasztási beállítás megváltozott - Rendszer proxy beállítása megváltozott + Rendszerproxy beállítás megváltozott - Csak útvonal + Csak útválasztás - Ne használjon proxy szervert a helyi (intranet) címekhez + Ne használjon proxy szervert helyi (intranet) címekhez - Egy kattintásos több teszt késleltetés és sebesség (Ctrl+E) + Egykattintásos többszörös késleltetés és sebesség teszt (Ctrl+E) - Késleltetés(ms) + Késleltetés (ms) - Sebesség(M/s) + Sebesség (M/s) - Nem sikerült futtatni a Core-t, kérlek, nézd meg a naplót + Nem sikerült futtatni a Core-t, kérjük, ellenőrizze a prompt információt - Megjegyzések reguláris szűrő + Megjegyzések reguláris szűrője Napló megjelenítése @@ -913,61 +910,61 @@ Új port a LAN-hoz - TunMode beállítások + Tun mód beállítások - Áthelyezés csoporthoz + Mozgatás csoportba - Proxy Drag Drop Szortírozás engedélyezése (indítás szükséges) + Konfigurációk rendezésének engedélyezése húzással (újraindítást igényel) - AutoFrissítés + Automatikus frissítés Teszt kihagyása - Szerver szerkesztése (Ctrl+D) + Konfiguráció szerkesztése (Ctrl+D) - Kattintson duplán a szerver aktiválásához + Dupla kattintás a konfigurációra aktiválja - A teszt befejeződött + Teszt befejeződött Alapértelmezett TLS ujjlenyomat - Felhasználói ügynök + User-Agent Ez a paraméter csak tcp/http és ws esetén érvényes - Betűcsalád (indítás szükséges) + Betűtípus (újraindítást igényel) - Kérlek, másold a betűtípus TTF/TTC fájlt a guiFonts könyvtárba, indítsd újra a beállításokat + Másolja a TTF/TTC betűtípus fájlt a gui Fonts könyvtárba; Nyissa meg újra a beállítások ablakot Pac port = +3; Xray API port = +4; mihomo API port = +5; - Állítsd be ezt admin jogokkal, indítás után szerezd meg az admin jogokat + Rendszergazdai jogosultságokkal állítsa be, indítás után szerezzen rendszergazdai jogosultságokat Betűméret - SpeedTest egyéni időtúllépési érték + Sebességteszt egyszeri időtúllépési érték - SpeedTest URL + Sebességteszt URL - Feljebb és lejjebb mozgatás + Mozgatás fel és le Nyilvános kulcs @@ -976,40 +973,40 @@ Rövid azonosító - SpiderX + Spider X - Hardveres gyorsítás engedélyezése (indítás szükséges) + Hardveres gyorsítás engedélyezése (újraindítást igényel) - Várakozás a tesztelésre (nyomj ESC-t a leállításhoz)... + Tesztelésre vár (ESC megnyomásával megszakítható)... - Kérlek, kapcsold ki, ha szokatlan megszakítás történt + Kérjük, kapcsolja ki rendellenes megszakadás esetén - A frissítések nincsenek aktiválva, átugorva ez az előfizetés + A frissítések nincsenek engedélyezve, kihagyja ezt az előfizetést - Újraindítás adminisztrátorként + Újraindítás rendszergazdaként - Több URL, vesszővel elválasztva; Az előfizetés átalakítása érvénytelen lesz + További URL-ek, vesszővel elválasztva; Az előfizetés konverzió érvénytelen lesz {0} : {1}/s↑ | {2}/s↓ - Automatikus frissítési időköz (perc) + Automatikus frissítési intervallum (percek) - Napló engedélyezése fájlba + Naplózás engedélyezése fájlba - Átalakítási cél típusa + Konverziós cél típus - Kérlek, hagyd üresen, ha nincs szükség átalakításra + Kérjük, hagyja üresen, ha nincs szükség konverzióra DNS beállítások @@ -1018,115 +1015,127 @@ sing-box DNS beállítások - Kérlek, töltsd ki a DNS struktúrát, kattints a dokumentum megtekintésére + Kérjük, töltse ki a DNS struktúrát, kattintson a dokumentum megtekintéséhez - Kattints az alapértelmezett DNS konfiguráció importálásához + Kattintson az alapértelmezett DNS konfiguráció importálásához - sing-box domain stratégia + sing-box tartomány stratégia sing-box Mux protokoll - Teljes folyamat neve (Tun módban) + Teljes folyamatnév (Tun mód) IP vagy IP CIDR - Domain + Tartomány - [Hysteria2] szerver hozzáadása + Hysteria2 konfiguráció hozzáadása - Hysteria maximális sávszélesség (Felfelé/Letöltés) + Hysteria Max sávszélesség (Fel/Le) - Rendszer gazdagépeket használ + Rendszer Hosts használata - [TUIC] szerver hozzáadása + TUIC konfiguráció hozzáadása - Befogadó irányítás + Torlódásvezérlés - Előző proxy megjegyzések + Előző proxy konfiguráció megjegyzései - Következő proxy megjegyzések + Következő proxy konfiguráció megjegyzései - Kérlek, győződj meg róla, hogy a megjegyzés létezik és egyedi + Kérjük, győződjön meg arról, hogy a konfigurációs megjegyzések léteznek és egyediek + + + Automatikus útválasztás + + + Szigorú útválasztás + + + Hálózati verem + + + MTU - Kiegészítő bejövő engedélyezése + További bejövő engedélyezése IPv6 cím engedélyezése - [WireGuard] szerver hozzáadása + WireGuard konfiguráció hozzáadása Privát kulcs - Fenntartva (2,3,4) + Fenntartott (2,3,4) - Cím (Ipv4,Ipv6) + Cím (IPv4, IPv6) obfs jelszó - (Domain vagy IP vagy ProcName) és Port és Protokoll és InboundTag => OutboundTag + (Tartomány vagy IP vagy folyamatnév) és port és protokoll és bejövő címke => kimenő címke Automatikus görgetés a végére - Speed Ping Test URL + Sebesség Ping Teszt URL - Előfizetés frissítése, csak akkor határozd meg, ha a megjegyzések léteznek + Előfizetés frissítése, csak a megjegyzések létezésének ellenőrzése - A teszt leállítása... + Teszt megszakítása... - *grpc Hatóság + *grpc Authority - [HTTP] szerver hozzáadása + HTTP konfiguráció hozzáadása - Használja az Xray-t és engedélyezze a nem-Tun módot, amely összeférhet a csoport előző proxy-jával + which conflicts with the group previous proxy - Fragmentum engedélyezése + Fragment engedélyezése - Cache fájl engedélyezése a sing-box számára (szabálykészlet fájlok) + Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok) - Testreszabott a sing-box szabálykészletének + A sing-box szabálykészletének testreszabása - Sikeres művelet. Kattintson a beállítási menüre az alkalmazás újraindításához. + Sikeres művelet. Kattintson a beállítások menüre az alkalmazás újraindításához. - Megnyitja a tárolás helyét + Fájl helyének megnyitása Rendezés - Zárlat + Lánc Alapértelmezett @@ -1141,7 +1150,7 @@ Letöltési forgalom - Gazda + Host Név @@ -1171,7 +1180,7 @@ Összes kapcsolat bezárása - Proxy-k + Proxyk Szabály mód @@ -1183,7 +1192,7 @@ Globális - Ne változtasson + Ne változtassa meg Szabály @@ -1195,49 +1204,49 @@ Részleges csomópont késleltetés teszt - Proxy-k frissítése + Proxyk frissítése Aktív csomópont kiválasztása (Enter) - Alapértelmezett domain stratégia a kimenő forgalomhoz + Alapértelmezett tartomány stratégia kimenő forgalomhoz - Fő elrendezés irányítása (indítás szükséges) + Fő elrendezés iránya (újraindítást igényel) Kimenő DNS cím - Oszlop szélesség automatikus beállítása + Oszlopszélesség automatikus beállítása - Exportálja a Base64-kódolt megosztási linkeket a vágólapra + Base64-kódolt megosztási linkek exportálása vágólapra - Kiválasztott szerver exportálása teljes konfigurációval a vágólapra + Kijelölt konfiguráció exportálása teljes konfigurációként a vágólapra - Fő ablak megjelenítése vagy elrejtése + Főablak megjelenítése vagy elrejtése - Testreszabott konfigurációs socks port + Egyéni konfiguráció socks portja Biztonsági mentés és visszaállítás - Biztonsági mentés helyben + Biztonsági mentés helyi tárolóba - Visszaállítás helyben + Visszaállítás helyi tárolóból - Biztonsági mentés távolról (WebDAV) + Biztonsági mentés távoli helyre (WebDAV) - Visszaállítás távolról (WebDAV) + Visszaállítás távoli helyről (WebDAV) Helyi @@ -1246,25 +1255,25 @@ Távoli (WebDAV) - WebDav URL + WebDAV URL - WebDav Felhasználónév + WebDAV felhasználónév - WebDav Jelszó + WebDAV jelszó - WebDav Ellenőrzés + WebDAV ellenőrzés Távoli mappa neve (opcionális) - Érvénytelen mentési fájl + Érvénytelen biztonsági mentés fájl - Gazda szűrő + Host szűrő Aktív @@ -1276,13 +1285,13 @@ sing-box szabálykészlet fájlok forrása (opcionális) - UpgradeApp nem létezik + Frissítő alkalmazás nem létezik - Útvonal szabályok forrása (opcionális) + Útválasztási szabályok forrása (opcionális) - Regionális előbeállítások beállítása + Regionális előbeállítások Alapértelmezett @@ -1294,22 +1303,22 @@ Irán - A Kínában élő felhasználók figyelmen kívül hagyhatják ezt a tételt + Kínai régióban lévő felhasználók figyelmen kívül hagyhatják ezt az elemet - QR kód beolvasása a képen + QR kód beolvasása a képből - Érvénytelen cím (Url) + Érvénytelen cím (URL) - Kérlek, ne használd a nem biztonságos HTTP protokollt az előfizetés címeknél + Kérjük, ne használjon nem biztonságos HTTP protokoll előfizetési címet - Telepítsd a betűtípust a rendszerbe és indítsd újra a beállításokat + Telepítse a betűtípust a rendszerbe, válassza ki vagy töltse ki a betűtípus nevét, indítsa újra a beállításokat - Biztosan ki akarsz lépni? + Biztosan ki akar lépni? Megjegyzések @@ -1318,87 +1327,192 @@ Rendszer sudo jelszó - The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart. + A jelszót a parancssoron keresztül ellenőrizzük. Ha egy érvényesítési hiba miatt az alkalmazás hibásan működik, indítsa újra az alkalmazást. A jelszó nem kerül tárolásra, és minden újraindítás után újra meg kell adni. *xhttp mód - XHTTP További nyers JSON, formátum: { XHTTPObject } + XHTTP Extra nyers JSON, formátum: { XHTTP Objektum } - Minimálás tálcára ablak zárásakor + Ablak bezárásakor a tálcára rejtés - The number of concurrent during multi-test + A párhuzamos tesztek száma több teszt során - Kivétel. Ne használj proxy szervert a címeknél, évezz pontosvesszőt (,) + Kivételek: Ne használjon proxy szervert a következő címekhez. Vesszővel (,) válassza el a bejegyzéseket. - Sniffing type + Sniffing típus - Enable second mixed port + Második vegyes port engedélyezése - socks: local port, socks2: second local port, socks3: LAN port + socks: helyi port, socks2: második helyi port, socks3: LAN port - Theme + Téma - Copy proxy command to clipboard + Proxy parancs másolása vágólapra - Starting retesting failed parts, {0} remaining. Press ESC to terminate... + Sikertelen részek újratesztelése elindult, {0} maradt. ESC megnyomásával megszakítható... - By test result + Teszt eredmény szerint - Remove invalid by test results + Érvénytelenek eltávolítása teszteredmények alapján - Removed {0} invalid test results. + Eltávolítva {0} érvénytelen teszteredmény. - Server port range + Konfigurációs port tartomány - Will cover the port, separate with commas (,) + A portot lefedi, vesszővel (,) elválasztva - Multi-server to custom configuration + Több konfiguráció egyéni konfigurációra - Multi-server Random by Xray + Több konfiguráció véletlenszerűen Xray szerint - Multi-server RoundRobin by Xray + Több konfiguráció RoundRobin Xray szerint - Multi-server LeastPing by Xray + Több konfiguráció legkisebb pinggel Xray szerint - Multi-server LeastLoad by Xray + Több konfiguráció legkisebb terheléssel Xray szerint - Multi-server LeastPing by sing-box + Több konfiguráció legkisebb pinggel sing-box szerint - Export server + Konfiguráció exportálása - Current connection info test URL + Aktuális kapcsolat info teszt URL - Can fill in the configuration remarks, please make sure it exist and are unique - - - Sudo password has been verified successfully, please ignore the incorrect password prompts! + Kitöltheti a konfigurációs megjegyzéseket, kérjük, győződjön meg róla, hogy létezik és egyedi - Incorrect password, please try again. + Helytelen jelszó, próbálja újra. + + + Mldsa65Verify + + + [Anytls] konfiguráció hozzáadása + + + Remote DNS + + + Domestic DNS + + + Outbound DNS Resolution (sing-box) + + + Resolve Outbound Domains + + + sing-box DoH Resolver Server + + + Fallback DNS Resolution, Suggest IP + + + xray Freedom Resolution Strategy + + + sing-box Direct Resolution Strategy + + + sing-box Remote Resolution Strategy + + + Add Common DNS Hosts + + + The sing-box DoH resolution server can be overwritten + + + FakeIP + + + Block SVCB and HTTPS Queries + + + DNS Hosts: ("domain1 ip1 ip2" per line) + + + Apply to Proxy Domains Only + + + Basic DNS Settings + + + Advanced DNS Settings + + + Validate Regional Domain IPs + + + When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + + + Enable Custom DNS + + + Custom DNS Enabled, This Page's Settings Invalid + + + Prevent domain-based routing rules from failing + + + Please fill in the correct config template + + + Full Config Template Setting + + + Enable Full Config Template + + + v2ray Full Config Template + + + Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + + + Do Not Add Non-Proxy Protocol Outbound + + + Set Upstream Proxy Tag + + + sing-box Full Config Template + + + Add Outbound and Endpoint Config Only, Click to view the document + + + 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. + + + Start parsing and processing subscription content + + + Select Profile \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 78efe585..a13bd00a 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1,17 +1,17 @@ - @@ -822,9 +822,6 @@ Set as active rule (Enter) - - Domain Matcher - Domain strategy @@ -1059,6 +1056,18 @@ Please make sure the Configuration remarks exist and are unique + + Auto Route + + + Strict Route + + + Stack + + + MTU + Enable additional Inbound @@ -1102,7 +1111,7 @@ Add [HTTP] Configuration - Use Xray and enable non-Tun mode, which conflicts with the group previous proxy + which conflicts with the group previous proxy Enable fragment @@ -1392,10 +1401,115 @@ Can fill in the configuration remarks, please make sure it exist and are unique - - Sudo password has been verified successfully, please ignore the incorrect password prompts! - Incorrect password, please try again. + + Mldsa65Verify + + + Add [Anytls] Configuration + + + Remote DNS + + + Domestic DNS + + + Outbound DNS Resolution (sing-box) + + + Resolve Outbound Domains + + + sing-box DoH Resolver Server + + + Fallback DNS Resolution, Suggest IP + + + xray Freedom Resolution Strategy + + + sing-box Direct Resolution Strategy + + + sing-box Remote Resolution Strategy + + + Add Common DNS Hosts + + + The sing-box DoH resolution server can be overwritten + + + FakeIP + + + Block SVCB and HTTPS Queries + + + DNS Hosts: ("domain1 ip1 ip2" per line) + + + Apply to Proxy Domains Only + + + Basic DNS Settings + + + Advanced DNS Settings + + + Validate Regional Domain IPs + + + When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + + + Enable Custom DNS + + + Custom DNS Enabled, This Page's Settings Invalid + + + Prevent domain-based routing rules from failing + + + Please fill in the correct config template + + + Full Config Template Setting + + + Enable Full Config Template + + + v2ray Full Config Template + + + Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + + + Do Not Add Non-Proxy Protocol Outbound + + + Set Upstream Proxy Tag + + + sing-box Full Config Template + + + Add Outbound and Endpoint Config Only, Click to view the document + + + 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. + + + Start parsing and processing subscription content + + + Select Profile + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index aaf39391..f9bfbd68 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -804,9 +804,6 @@ Вверх (U) - - Переместить вверх/вниз - Фильтр, поддерживает regex @@ -825,9 +822,6 @@ Установить как активное правило - - Сопоставитель доменов - Доменная стратегия @@ -966,6 +960,9 @@ URL для тестирования скорости + + Переместить вверх/вниз + PublicKey @@ -1059,6 +1056,18 @@ Убедитесь, что примечание существует и является уникальным + + Автоматическая маршрутизация + + + Строгая маршрутизация + + + Сетевой стек + + + MTU + Включить дополнительный входящий канал @@ -1096,13 +1105,13 @@ Отмена тестирования... - *gRPC Authority + * gRPC Authority (HTTP/2 псевдозаголовок :authority) Добавить сервер [HTTP] - Используйте Xray и отключите режим TUN, так как он конфликтует с предыдущим прокси-сервером группы + что конфликтует с предыдущим прокси группы Включить фрагментацию (Fragment) @@ -1315,13 +1324,13 @@ Пароль sudo системы - The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart. + Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска. *XHTTP-режим - Дополнительный XHTTP сырой JSON, формат: { XHTTPObject } + Дополнительный „сырой“ JSON для XHTTP, формат: { XHTTP Object } Скрыть в трее при закрытии окна @@ -1390,12 +1399,117 @@ URL для тестирования текущего соединения - Can fill in the configuration remarks, please make sure it exist and are unique - - - Sudo password has been verified successfully, please ignore the incorrect password prompts! + Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально - Incorrect password, please try again. + Неверный пароль, попробуйте ещё раз. + + + Mldsa65Verify + + + Добавить сервер [Anytls] + + + Удалённый DNS + + + Внутренний DNS + + + Резолвер DNS для исходящих (sing-box) + + + Разрешать домены для исходящих соединений + + + Сервер DoH-резолвера (sing-box) + + + Резервное DNS-разрешение (рекомендуется указывать IP) + + + Стратегия резолвинга Freedom (Xray) + + + Стратегия прямого резолвинга (sing-box) + + + Стратегия удалённого резолвинга (sing-box) + + + Добавить стандартные записи hosts (DNS) + + + Сервер DoH-резолвера sing-box можно переопределить + + + FakeIP + + + Блокировать DNS-запросы SVCB и HTTPS + + + DNS hosts: (каждая строка в формате "domain1 ip1 ip2") + + + Применять только к доменам через прокси + + + Базовые настройки DNS + + + Расширенные настройки DNS + + + Проверять IP-адреса региональных доменов + + + При включении проверяет IP-адреса, возвращаемые для региональных доменов (например, geosite:cn), и оставляет только ожидаемые IP-адреса + + + Включить пользовательский DNS + + + Включён пользовательский DNS — настройки на этой странице не применяются + + + Предотвращает сбои доменных правил маршрутизации + + + Пожалуйста, заполните корректный шаблон конфигурации + + + Настройка полного шаблона конфигурации + + + Включить полный шаблон конфигурации + + + Полный шаблон конфигурации v2ray + + + Добавляет только конфигурацию исходящих (outbound), а также routing.balancers и routing.rules.outboundTag. Нажмите, чтобы открыть документ + + + Не добавлять исходящие для непрокси-протоколов + + + Задать тег верхнего прокси (upstream) + + + Полный шаблон конфигурации sing-box + + + Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ + + + Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную. + + + Start parsing and processing subscription content + + + Select Profile \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 03a840fd..5c55463c 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1,17 +1,17 @@ - @@ -822,9 +822,6 @@ 设为活动规则 (Enter) - - 域名匹配算法 - 域名解析策略 @@ -1056,6 +1053,18 @@ 请确保配置文件别名存在并唯一 + + 自动路由 + + + 严格路由 + + + 协议栈 + + + MTU + 启用额外监听端口 @@ -1099,7 +1108,7 @@ 添加 [HTTP] 配置文件 - 使用 Xray 且非 Tun 模式启用,和分组前置代理冲突 + 和分组前置代理冲突 启用分片 (Fragment) @@ -1389,10 +1398,115 @@ 可以填写配置文件别名,请确保存在并唯一 - - sudo 密码已经验证成功,请忽略错误密码提示! - 密码错误,请重试。 + + Mldsa65Verify + + + 添加 [Anytls] 配置文件 + + + 远程 DNS + + + 直连 DNS + + + 出站 DNS 解析(sing-box) + + + 解析出站域名 + + + sing-box DoH 解析服务器 + + + 兜底解析其他 DNS 域名,建议设为 ip + + + xray freedom 解析策略 + + + sing-box 直连解析策略 + + + sing-box 远程解析策略 + + + 添加常用 DNS Hosts + + + 开启后可覆盖 sing-box DoH 解析服务器 + + + FakeIP + + + 阻止 SVCB 和 HTTPS 查询 + + + DNS Hosts:(“域名1 ip1 ip2” 一行一个) + + + 仅对代理域名生效 + + + DNS 基础设置 + + + DNS 进阶设置 + + + 校验相应地区域名 IP + + + 配置后,会对相应地区域名(如 geosite:cn)的返回 IP 进行校验,仅返回期望 IP + + + 启用自定义 DNS + + + 自定义 DNS 已启用,此页面配置将无效 + + + 避免域名分流规则失效 + + + 请填写正确的配置模板 + + + 完整配置模板设置 + + + 启用完整配置模板 + + + v2ray 完整配置模板 + + + 仅添加出站配置,routing.balancers 和 routing.rules.outboundTag,点击查看文档 + + + 不添加非代理协议出站 + + + 设置上游代理 tag + + + sing-box 完整配置模板 + + + 仅添加出站和端点配置,点击查看文档 + + + 此功能供高级用户和有特殊需求的用户使用。 启用此功能后,将忽略 Core 的基础设置,DNS 设置 ,路由设置。你需要保证系统代理的端口和流量统计等功能的配置正确,一切都由你来设置。 + + + 开始解析和处理订阅内容 + + + 选择配置文件 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 811e6b08..06b651a9 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1,17 +1,17 @@ - @@ -822,9 +822,6 @@ 設為活動規則 (Enter) - - 域名匹配演算法 - 域名解析策略 @@ -1056,6 +1053,18 @@ 請確保設定檔別名存在並且唯一 + + 自動路由 + + + 嚴格路由 + + + 協定堆疊 + + + MTU + 啟用額外偵聽連接埠 @@ -1099,7 +1108,7 @@ 新增 [HTTP] 設定檔 - 使用 Xray 且非 Tun 模式啟用,和分組前置代理衝突 + 和分組前置代理衝突 啟用分片(Fragment) @@ -1389,10 +1398,115 @@ 可以填寫設定檔別名,請確保存在並唯一 - - sudo 密碼已經驗證成功,請忽略錯誤密碼提示! - 密碼錯誤,請重試。 + + Mldsa65Verify + + + 新增 [Anytls] 設定檔 + + + Remote DNS + + + Domestic DNS + + + Outbound DNS Resolution (sing-box) + + + Resolve Outbound Domains + + + sing-box DoH Resolver Server + + + Fallback DNS Resolution, Suggest IP + + + xray Freedom Resolution Strategy + + + sing-box Direct Resolution Strategy + + + sing-box Remote Resolution Strategy + + + Add Common DNS Hosts + + + The sing-box DoH resolution server can be overwritten + + + FakeIP + + + Block SVCB and HTTPS Queries + + + DNS Hosts: ("domain1 ip1 ip2" per line) + + + Apply to Proxy Domains Only + + + Basic DNS Settings + + + Advanced DNS Settings + + + Validate Regional Domain IPs + + + When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + + + Enable Custom DNS + + + Custom DNS Enabled, This Page's Settings Invalid + + + Prevent domain-based routing rules from failing + + + Please fill in the correct config template + + + Full Config Template Setting + + + Enable Full Config Template + + + v2ray Full Config Template + + + Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + + + Do Not Add Non-Proxy Protocol Outbound + + + Set Upstream Proxy Tag + + + sing-box Full Config Template + + + Add Outbound and Endpoint Config Only, Click to view the document + + + 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. + + + 開始解析和處理訂閱內容 + + + Select Profile + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SingboxSampleClientConfig b/v2rayN/ServiceLib/Sample/SingboxSampleClientConfig index f88422a1..b07fd72c 100644 --- a/v2rayN/ServiceLib/Sample/SingboxSampleClientConfig +++ b/v2rayN/ServiceLib/Sample/SingboxSampleClientConfig @@ -1,4 +1,4 @@ -{ +{ "log": { "level": "debug", "timestamp": true @@ -14,22 +14,10 @@ { "type": "direct", "tag": "direct" - }, - { - "type": "block", - "tag": "block" - }, - { - "tag": "dns_out", - "type": "dns" } ], "route": { "rules": [ - { - "protocol": [ "dns" ], - "outbound": "dns_out" - } ] } } \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/dns_singbox_normal b/v2rayN/ServiceLib/Sample/dns_singbox_normal index 0921fe64..b32b439c 100644 --- a/v2rayN/ServiceLib/Sample/dns_singbox_normal +++ b/v2rayN/ServiceLib/Sample/dns_singbox_normal @@ -2,28 +2,33 @@ "servers": [ { "tag": "remote", - "address": "tcp://8.8.8.8", - "strategy": "prefer_ipv4", + "type": "tcp", + "server": "8.8.8.8", "detour": "proxy" }, { "tag": "local", - "address": "223.5.5.5", - "strategy": "prefer_ipv4", - "detour": "direct" - }, - { - "tag": "block", - "address": "rcode://success" + "type": "udp", + "server": "223.5.5.5" } ], "rules": [ + { + "domain_suffix": [ + "googleapis.cn", + "gstatic.com" + ], + "server": "remote", + "strategy": "prefer_ipv4" + }, { "rule_set": [ "geosite-cn" ], - "server": "local" + "server": "local", + "strategy": "prefer_ipv4" } ], - "final": "remote" + "final": "remote", + "strategy": "prefer_ipv4" } diff --git a/v2rayN/ServiceLib/Sample/kill_as_sudo_linux_sh b/v2rayN/ServiceLib/Sample/kill_as_sudo_linux_sh new file mode 100644 index 00000000..7f62a532 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/kill_as_sudo_linux_sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Process Terminator Script for Linux +# This script forcibly terminates a process and all its child processes +# + +# Check if PID argument is provided +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +PID=$1 + +# Validate that input is a valid PID (numeric) +if ! [[ "$PID" =~ ^[0-9]+$ ]]; then + echo "Error: The PID must be a numeric value" + exit 1 +fi + +# Check if the process exists +if ! ps -p $PID > /dev/null; then + echo "Warning: No process found with PID $PID" + exit 0 +fi + +# Recursive function to find and kill all child processes +kill_children() { + local parent=$1 + local children=$(ps -o pid --no-headers --ppid "$parent") + + # Output information about processes being terminated + echo "Processing children of PID: $parent..." + + # Process each child + for child in $children; do + # Recursively find and kill child's children first + kill_children "$child" + + # Force kill the child process + echo "Terminating child process: $child" + kill -9 "$child" 2>/dev/null || true + done +} + +echo "============================================" +echo "Starting termination of process $PID and all its children" +echo "============================================" + +# Find and kill all child processes +kill_children "$PID" + +# Finally kill the main process +echo "Terminating main process: $PID" +kill -9 "$PID" 2>/dev/null || true + +echo "============================================" +echo "Process $PID and all its children have been terminated" +echo "============================================" + +exit 0 diff --git a/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh b/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh new file mode 100644 index 00000000..94011d6f --- /dev/null +++ b/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh @@ -0,0 +1,56 @@ +#!/bin/bash +# +# Process Terminator Script for macOS +# This script forcibly terminates a process and all its descendant processes +# + +# Check if PID argument is provided +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +PID=$1 + +# Validate that input is a valid PID (numeric) +if ! [[ "$PID" =~ ^[0-9]+$ ]]; then + echo "Error: The PID must be a numeric value" + exit 1 +fi + +# Check if the process exists - using kill -0 which is more reliable on macOS +if ! kill -0 $PID 2>/dev/null; then + echo "Warning: No process found with PID $PID" + exit 0 +fi + +# Recursive function to find and kill all descendant processes +kill_descendants() { + local parent=$1 + # Use ps -axo for macOS to ensure all processes are included + local children=$(ps -axo pid=,ppid= | awk -v ppid=$parent '$2==ppid {print $1}') + + echo "Processing children of PID: $parent..." + for child in $children; do + kill_descendants "$child" + echo "Terminating child process: $child" + kill -9 "$child" 2>/dev/null || true + done +} + +echo "============================================" +echo "Starting termination of process $PID and all its descendants" +echo "============================================" + +# Find and kill all descendant processes +kill_descendants "$PID" + +# Finally kill the main process +echo "Terminating main process: $PID" +kill -9 "$PID" 2>/dev/null || true + +echo "============================================" +echo "Process $PID and all its descendants have been terminated" +echo "============================================" + +exit 0 diff --git a/v2rayN/ServiceLib/Sample/tun_singbox_dns b/v2rayN/ServiceLib/Sample/tun_singbox_dns index d8ca9808..3dd55eef 100644 --- a/v2rayN/ServiceLib/Sample/tun_singbox_dns +++ b/v2rayN/ServiceLib/Sample/tun_singbox_dns @@ -2,29 +2,33 @@ "servers": [ { "tag": "remote", - "address": "tcp://8.8.8.8", - "strategy": "prefer_ipv4", + "type": "tcp", + "server": "8.8.8.8", "detour": "proxy" }, { "tag": "local", - "address": "223.5.5.5", - "strategy": "prefer_ipv4", - "detour": "direct" - }, - { - "tag": "block", - "address": "rcode://success" + "type": "udp", + "server": "223.5.5.5" } ], "rules": [ { - "rule_set": [ - "geosite-cn", - "geosite-geolocation-cn" + "domain_suffix": [ + "googleapis.cn", + "gstatic.com" ], - "server": "local" + "server": "remote", + "strategy": "prefer_ipv4" + }, + { + "rule_set": [ + "geosite-cn" + ], + "server": "local", + "strategy": "prefer_ipv4" } ], - "final": "remote" -} + "final": "remote", + "strategy": "prefer_ipv4" +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/tun_singbox_rules b/v2rayN/ServiceLib/Sample/tun_singbox_rules index df1dc4ec..a4276134 100644 --- a/v2rayN/ServiceLib/Sample/tun_singbox_rules +++ b/v2rayN/ServiceLib/Sample/tun_singbox_rules @@ -8,13 +8,13 @@ 139, 5353 ], - "outbound": "block" + "action": "reject" }, { "ip_cidr": [ "224.0.0.0/3", "ff00::/8" ], - "outbound": "block" + "action": "reject" } ] \ No newline at end of file diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj index 2f691a03..9cd69952 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -28,6 +28,8 @@ + + diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs index 481ae92d..e102f17d 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs @@ -12,7 +12,7 @@ public class CoreConfigClashService { _config = config; } - + public async Task GenerateClientCustomConfig(ProfileItem node, string? fileName) { var ret = new RetResult(); @@ -73,12 +73,12 @@ 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}"; //allow-lan if (_config.Inbound.First().AllowLANConn) { @@ -139,7 +139,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; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs deleted file mode 100644 index c9bef4f5..00000000 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs +++ /dev/null @@ -1,1618 +0,0 @@ -using System.Data; -using System.Net; -using System.Net.NetworkInformation; - -namespace ServiceLib.Services.CoreConfig; - -public class CoreConfigSingboxService -{ - private Config _config; - private static readonly string _tag = "CoreConfigSingboxService"; - - public CoreConfigSingboxService(Config config) - { - _config = config; - } - - #region public gen function - - public async Task GenerateClientConfigContent(ProfileItem node) - { - var ret = new RetResult(); - try - { - if (node == null - || node.Port <= 0) - { - 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; - - string result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); - if (result.IsNullOrEmpty()) - { - ret.Msg = ResUI.FailedGetDefaultConfiguration; - return ret; - } - - var singboxConfig = JsonUtils.Deserialize(result); - if (singboxConfig == null) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - - await GenLog(singboxConfig); - - await GenInbounds(singboxConfig); - - 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 = JsonUtils.Serialize(singboxConfig); - return ret; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - } - - public async Task GenerateClientSpeedtestConfig(List 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(result); - if (singboxConfig == null) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - List lstIpEndPoints = new(); - List 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 = AppHandler.Instance.GetLocalPort(EInboundProtocol.speedtest); - - foreach (var it in selecteds) - { - if (it.ConfigType == EConfigType.Custom) - { - continue; - } - if (it.Port <= 0) - { - continue; - } - var item = await AppHandler.Instance.GetProfileItem(it.IndexId); - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) - { - if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } - } - - //find unused port - var port = initPort; - for (int 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 - if (item is null) - { - continue; - } - if (item.ConfigType == EConfigType.Shadowsocks - && !Global.SsSecuritiesInSingbox.Contains(item.Security)) - { - continue; - } - if (item.ConfigType == EConfigType.VLESS - && !Global.Flows.Contains(item.Flow)) - { - continue; - } - if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan - && item.StreamSecurity == Global.StreamSecurityReality - && item.PublicKey.IsNullOrEmpty()) - { - continue; - } - - var outbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(item, outbound); - outbound.tag = Global.ProxyTag + inbound.listen_port.ToString(); - singboxConfig.outbounds.Add(outbound); - - //rule - Rule4Sbox rule = new() - { - inbound = new List { inbound.tag }, - outbound = outbound.tag - }; - singboxConfig.route.rules.Add(rule); - } - - await GenDnsDomains(null, singboxConfig, null); - //var dnsServer = singboxConfig.dns?.servers.FirstOrDefault(); - //if (dnsServer != null) - //{ - // dnsServer.detour = singboxConfig.route.rules.LastOrDefault()?.outbound; - //} - //var dnsRule = singboxConfig.dns?.rules.Where(t => t.outbound != null).FirstOrDefault(); - //if (dnsRule != null) - //{ - // singboxConfig.dns.rules = []; - // singboxConfig.dns.rules.Add(dnsRule); - //} - - //ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary()); - 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 GenerateClientSpeedtestConfig(ProfileItem node, int port) - { - var ret = new RetResult(); - try - { - if (node is not { Port: > 0 }) - { - 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(result); - if (singboxConfig == null) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - - await GenLog(singboxConfig); - await GenOutbound(node, singboxConfig.outbounds.First()); - await GenMoreOutbounds(node, singboxConfig); - await GenDnsDomains(null, singboxConfig, null); - - 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 GenerateClientMultipleLoadConfig(List selecteds) - { - var ret = new RetResult(); - try - { - if (_config == null) - { - ret.Msg = ResUI.CheckServerSettings; - return ret; - } - - ret.Msg = ResUI.InitialConfiguration; - - string result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); - string txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); - if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) - { - ret.Msg = ResUI.FailedGetDefaultConfiguration; - return ret; - } - - var singboxConfig = JsonUtils.Deserialize(result); - if (singboxConfig == null) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - - await GenLog(singboxConfig); - await GenInbounds(singboxConfig); - await GenRouting(singboxConfig); - await GenExperimental(singboxConfig); - singboxConfig.outbounds.RemoveAt(0); - - var proxyProfiles = new List(); - foreach (var it in selecteds) - { - if (it.ConfigType == EConfigType.Custom) - { - continue; - } - if (it.Port <= 0) - { - continue; - } - var item = await AppHandler.Instance.GetProfileItem(it.IndexId); - if (item is null) - { - continue; - } - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) - { - if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } - } - if (item.ConfigType == EConfigType.Shadowsocks - && !Global.SsSecuritiesInSingbox.Contains(item.Security)) - { - continue; - } - if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) - { - continue; - } - - //outbound - proxyProfiles.Add(item); - } - if (proxyProfiles.Count <= 0) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - await GenOutboundsList(proxyProfiles, singboxConfig); - - await GenDns(null, singboxConfig); - await ConvertGeo2Ruleset(singboxConfig); - - 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 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); - } - - string 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(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 - - #region private gen function - - private async Task 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); - } - - private async Task GenInbounds(SingboxConfig singboxConfig) - { - try - { - var listen = "::"; - 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 = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks); - inbound.sniff = _config.Inbound.First().SniffingEnabled; - inbound.sniff_override_destination = _config.Inbound.First().RouteOnly ? false : _config.Inbound.First().SniffingEnabled; - inbound.domain_strategy = _config.RoutingBasicItem.DomainStrategy4Singbox.IsNullOrEmpty() ? null : _config.RoutingBasicItem.DomainStrategy4Singbox; - - var routing = await ConfigHandler.GetDefaultRouting(_config); - if (routing.DomainStrategy4Singbox.IsNotEmpty()) - { - inbound.domain_strategy = routing.DomainStrategy4Singbox; - } - - 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(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { }; - tunInbound.interface_name = Utils.IsOSX() ? $"utun{new Random().Next(99)}" : "singbox_tun"; - tunInbound.mtu = _config.TunModeItem.Mtu; - tunInbound.strict_route = _config.TunModeItem.StrictRoute; - tunInbound.stack = _config.TunModeItem.Stack; - tunInbound.sniff = _config.Inbound.First().SniffingEnabled; - //tunInbound.sniff_override_destination = _config.inbound.First().routeOnly ? false : _config.inbound.First().sniffingEnabled; - if (_config.TunModeItem.EnableIPv6Address == false) - { - tunInbound.address = ["172.18.0.1/30"]; - } - - singboxConfig.inbounds.Add(tunInbound); - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return 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; - } - - private async Task 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 = AppHandler.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.WireGuard: - { - outbound.private_key = node.Id; - outbound.peer_public_key = node.PublicKey; - outbound.reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(); - outbound.local_address = Utils.String2List(node.RequestHost); - outbound.mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(); - break; - } - } - - await GenOutboundTls(node, outbound); - - await GenOutboundTransport(node, outbound); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return 0; - } - - private async Task 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 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, - 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 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 GenMoreOutbounds(ProfileItem node, SingboxConfig singboxConfig) - { - if (node.Subid.IsNullOrEmpty()) - { - return 0; - } - try - { - var subItem = await AppHandler.Instance.GetSubItem(node.Subid); - if (subItem is null) - { - return 0; - } - - //current proxy - var outbound = singboxConfig.outbounds.First(); - var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); - - //Previous proxy - var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - string? prevOutboundTag = null; - if (prevNode is not null - && prevNode.ConfigType != EConfigType.Custom) - { - var prevOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(prevNode, prevOutbound); - prevOutboundTag = $"prev-{Global.ProxyTag}"; - prevOutbound.tag = prevOutboundTag; - singboxConfig.outbounds.Add(prevOutbound); - } - var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag); - - if (nextOutbound is not null) - { - singboxConfig.outbounds.Insert(0, nextOutbound); - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - - return 0; - } - - private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig) - { - try - { - // Get outbound template and initialize lists - var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); - if (txtOutbound.IsNullOrEmpty()) - { - return 0; - } - - var resultOutbounds = new List(); - var prevOutbounds = new List(); // Separate list for prev outbounds - var proxyTags = new List(); // For selector and urltest outbounds - - // Cache for chain proxies to avoid duplicate generation - var nextProxyCache = new Dictionary(); - var prevProxyTags = new Dictionary(); // Map from profile name to tag - int prevIndex = 0; // Index for prev outbounds - - // Process each node - int index = 0; - foreach (var node in nodes) - { - index++; - - // Handle proxy chain - string? prevTag = null; - var currentOutbound = JsonUtils.Deserialize(txtOutbound); - var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null); - if (nextOutbound != null) - { - nextOutbound = JsonUtils.DeepCopy(nextOutbound); - } - - var subItem = await AppHandler.Instance.GetSubItem(node.Subid); - - // current proxy - await GenOutbound(node, currentOutbound); - currentOutbound.tag = $"{Global.ProxyTag}-{index}"; - proxyTags.Add(currentOutbound.tag); - - if (!node.Subid.IsNullOrEmpty()) - { - if (prevProxyTags.TryGetValue(node.Subid, out var value)) - { - prevTag = value; // maybe null - } - else - { - var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - if (prevNode is not null - && prevNode.ConfigType != EConfigType.Custom) - { - var prevOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(prevNode, prevOutbound); - prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; - prevOutbound.tag = prevTag; - prevOutbounds.Add(prevOutbound); - } - prevProxyTags[node.Subid] = prevTag; - } - - nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound); - if (!nextProxyCache.ContainsKey(node.Subid)) - { - nextProxyCache[node.Subid] = nextOutbound; - } - } - - if (nextOutbound is not null) - { - resultOutbounds.Add(nextOutbound); - } - resultOutbounds.Add(currentOutbound); - } - - // Add urltest outbound (auto selection based on latency) - if (proxyTags.Count > 0) - { - var outUrltest = new Outbound4Sbox - { - type = "urltest", - tag = $"{Global.ProxyTag}-auto", - outbounds = proxyTags, - interrupt_exist_connections = false, - }; - - // Add selector outbound (manual selection) - var outSelector = new Outbound4Sbox - { - type = "selector", - tag = Global.ProxyTag, - 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 - resultOutbounds.AddRange(prevOutbounds); - resultOutbounds.AddRange(singboxConfig.outbounds); - singboxConfig.outbounds = resultOutbounds; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - - return 0; - } - - /// - /// Generates a chained outbound configuration for the given subItem and outbound. - /// The outbound's tag must be set before calling this method. - /// Returns the next proxy's outbound configuration, which may be null if no next proxy exists. - /// - /// The subscription item containing proxy chain information. - /// The current outbound configuration. Its tag must be set before calling this method. - /// The tag of the previous outbound in the chain, if any. - /// The outbound for the next proxy in the chain, if already created. If null, will be created inside. - /// - /// The outbound configuration for the next proxy in the chain, or null if no next proxy exists. - /// - private async Task GenChainOutbounds(SubItem subItem, Outbound4Sbox outbound, string? prevOutboundTag, Outbound4Sbox? nextOutbound = null) - { - try - { - var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); - - if (!prevOutboundTag.IsNullOrEmpty()) - { - outbound.detour = prevOutboundTag; - } - - // Next proxy - var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile); - if (nextNode is not null - && nextNode.ConfigType != EConfigType.Custom) - { - if (nextOutbound == null) - { - nextOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(nextNode, nextOutbound); - } - 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 GenRouting(SingboxConfig singboxConfig) - { - try - { - var dnsOutbound = "dns_out"; - - if (_config.TunModeItem.EnableTun) - { - singboxConfig.route.auto_detect_interface = true; - - var tunRules = JsonUtils.Deserialize>(EmbedUtils.GetEmbedText(Global.TunSingboxRulesFileName)); - if (tunRules != null) - { - singboxConfig.route.rules.AddRange(tunRules); - } - - GenRoutingDirectExe(out List lstDnsExe, out List lstDirectExe); - singboxConfig.route.rules.Add(new() - { - port = new() { 53 }, - outbound = dnsOutbound, - process_name = lstDnsExe - }); - - singboxConfig.route.rules.Add(new() - { - outbound = Global.DirectTag, - process_name = lstDirectExe - }); - } - - if (!_config.Inbound.First().SniffingEnabled) - { - singboxConfig.route.rules.Add(new() - { - port = [53], - network = ["udp"], - outbound = dnsOutbound - }); - } - - 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 routing = await ConfigHandler.GetDefaultRouting(_config); - if (routing != null) - { - var rules = JsonUtils.Deserialize>(routing.RuleSet); - foreach (var item in rules ?? []) - { - if (item.Enabled) - { - await GenRoutingUserRule(item, singboxConfig); - } - } - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return 0; - } - - private void GenRoutingDirectExe(out List lstDnsExe, out List lstDirectExe) - { - var dnsExeSet = new HashSet(StringComparer.OrdinalIgnoreCase); - var directExeSet = new HashSet(StringComparer.OrdinalIgnoreCase); - - var coreInfoResult = CoreInfoHandler.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(dnsExeSet); - lstDirectExe = new List(directExeSet); - } - - private async Task 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() - { - 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.StartsWith("geoip:!")) - { - return false; - } - else if (address.Equals("geoip:private")) - { - rule.ip_is_private = true; - } - else if (address.StartsWith("geoip:")) - { - if (rule.geoip is null) - { rule.geoip = new(); } - rule.geoip?.Add(address.Substring(6)); - } - else - { - if (rule.ip_cidr is null) - { rule.ip_cidr = new(); } - rule.ip_cidr?.Add(address); - } - return true; - } - - private async Task GenRoutingUserRuleOutbound(string outboundTag, SingboxConfig singboxConfig) - { - if (Global.OutboundTags.Contains(outboundTag)) - { - return outboundTag; - } - - var node = await AppHandler.Instance.GetProfileItemViaRemarks(outboundTag); - if (node == null - || node.ConfigType == EConfigType.Custom) - { - return Global.ProxyTag; - } - - var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); - var outbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(node, outbound); - outbound.tag = Global.ProxyTag + node.IndexId.ToString(); - singboxConfig.outbounds.Add(outbound); - - return outbound.tag; - } - - private async Task GenDns(ProfileItem? node, SingboxConfig singboxConfig) - { - try - { - var item = await AppHandler.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(strDNS); - if (dns4Sbox is null) - { - return 0; - } - singboxConfig.dns = dns4Sbox; - - await GenDnsDomains(node, singboxConfig, item); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return 0; - } - - private async Task GenDnsDomains(ProfileItem? node, SingboxConfig singboxConfig, DNSItem? dNSItem) - { - var dns4Sbox = singboxConfig.dns ?? new(); - dns4Sbox.servers ??= []; - dns4Sbox.rules ??= []; - - var tag = "local_local"; - dns4Sbox.servers.Add(new() - { - tag = tag, - address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.SingboxDomainDNSAddress.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 - }); - } - - //Tun2SocksAddress - if (_config.TunModeItem.EnableTun && node?.ConfigType == EConfigType.SOCKS && Utils.IsDomain(node?.Sni)) - { - dns4Sbox.rules.Insert(0, new() - { - server = tag, - domain = [node?.Sni] - }); - } - - singboxConfig.dns = dns4Sbox; - return await Task.FromResult(0); - } - - private async Task GenExperimental(SingboxConfig singboxConfig) - { - //if (_config.guiItem.enableStatistics) - { - singboxConfig.experimental ??= new Experimental4Sbox(); - singboxConfig.experimental.clash_api = new Clash_Api4Sbox() - { - external_controller = $"{Global.Loopback}:{AppHandler.Instance.StatePort2}", - }; - } - - if (_config.CoreBasicItem.EnableCacheFile4Sbox) - { - singboxConfig.experimental ??= new Experimental4Sbox(); - singboxConfig.experimental.cache_file = new CacheFile4Sbox() - { - enabled = true, - path = Utils.GetBinPath("cache.db") - }; - } - - return await Task.FromResult(0); - } - - private async Task ConvertGeo2Ruleset(SingboxConfig singboxConfig) - { - static void AddRuleSets(List ruleSets, List? rule_set) - { - if (rule_set != null) - ruleSets.AddRange(rule_set); - } - var geosite = "geosite"; - var geoip = "geoip"; - var ruleSets = new List(); - - //convert route geosite & geoip to ruleset - foreach (var rule in singboxConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? []) - { - rule.rule_set = 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 = 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 = 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 = 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 customRulesets = []; - - var routing = await ConfigHandler.GetDefaultRouting(_config); - if (routing.CustomRulesetPath4Singbox.IsNotEmpty()) - { - var result = EmbedUtils.LoadResource(routing.CustomRulesetPath4Singbox); - if (result.IsNotEmpty()) - { - customRulesets = (JsonUtils.Deserialize>(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(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; - } - - #endregion private gen function -} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs deleted file mode 100644 index 9be1acf0..00000000 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs +++ /dev/null @@ -1,1567 +0,0 @@ -using System.Net; -using System.Net.NetworkInformation; -using System.Text.Json.Nodes; - -namespace ServiceLib.Services.CoreConfig; - -public class CoreConfigV2rayService -{ - private Config _config; - private static readonly string _tag = "CoreConfigV2rayService"; - - public CoreConfigV2rayService(Config config) - { - _config = config; - } - - #region public gen function - - public async Task GenerateClientConfigContent(ProfileItem node) - { - var ret = new RetResult(); - try - { - if (node == null - || node.Port <= 0) - { - 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; - - var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); - if (result.IsNullOrEmpty()) - { - ret.Msg = ResUI.FailedGetDefaultConfiguration; - return ret; - } - - var v2rayConfig = JsonUtils.Deserialize(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 = JsonUtils.Serialize(v2rayConfig); - return ret; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - } - - public async Task GenerateClientMultipleLoadConfig(List selecteds, EMultipleLoad multipleLoad) - { - 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(result); - if (v2rayConfig == null) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - - await GenLog(v2rayConfig); - await GenInbounds(v2rayConfig); - await GenRouting(v2rayConfig); - await GenDns(null, v2rayConfig); - await GenStatistic(v2rayConfig); - v2rayConfig.outbounds.RemoveAt(0); - - var proxyProfiles = new List(); - foreach (var it in selecteds) - { - if (it.ConfigType == EConfigType.Custom) - { - continue; - } - if (it.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC) - { - continue; - } - if (it.Port <= 0) - { - continue; - } - var item = await AppHandler.Instance.GetProfileItem(it.IndexId); - if (item is null) - { - continue; - } - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) - { - if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } - } - if (item.ConfigType == EConfigType.Shadowsocks - && !Global.SsSecuritiesInSingbox.Contains(item.Security)) - { - continue; - } - if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) - { - continue; - } - - //outbound - proxyProfiles.Add(item); - } - if (proxyProfiles.Count <= 0) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - await GenOutboundsList(proxyProfiles, v2rayConfig); - - //add balancers - await GenBalancer(v2rayConfig, multipleLoad); - - var balancer = v2rayConfig.routing.balancers.First(); - - //add rule - var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList(); - if (rules?.Count > 0) - { - foreach (var rule in rules) - { - rule.outboundTag = null; - rule.balancerTag = balancer.tag; - } - } - if (v2rayConfig.routing.domainStrategy == "IPIfNonMatch") - { - v2rayConfig.routing.rules.Add(new() - { - ip = ["0.0.0.0/0", "::/0"], - balancerTag = balancer.tag, - type = "field" - }); - } - else - { - v2rayConfig.routing.rules.Add(new() - { - network = "tcp,udp", - balancerTag = balancer.tag, - type = "field" - }); - } - - 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 GenerateClientSpeedtestConfig(List 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(result); - if (v2rayConfig == null) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - List lstIpEndPoints = new(); - List 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 = AppHandler.Instance.GetLocalPort(EInboundProtocol.speedtest); - - foreach (var it in selecteds) - { - if (it.ConfigType == EConfigType.Custom) - { - continue; - } - if (it.Port <= 0) - { - continue; - } - var item = await AppHandler.Instance.GetProfileItem(it.IndexId); - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) - { - if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - 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; - - //outbound - if (item is null) - { - continue; - } - if (item.ConfigType == EConfigType.Shadowsocks - && !Global.SsSecuritiesInXray.Contains(item.Security)) - { - continue; - } - if (item.ConfigType == EConfigType.VLESS - && !Global.Flows.Contains(item.Flow)) - { - continue; - } - if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan - && item.StreamSecurity == Global.StreamSecurityReality - && item.PublicKey.IsNullOrEmpty()) - { - continue; - } - - //inbound - Inbounds4Ray inbound = new() - { - listen = Global.Loopback, - port = port, - protocol = EInboundProtocol.mixed.ToString(), - }; - inbound.tag = inbound.protocol + inbound.port.ToString(); - v2rayConfig.inbounds.Add(inbound); - - var outbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(item, outbound); - outbound.tag = Global.ProxyTag + inbound.port.ToString(); - v2rayConfig.outbounds.Add(outbound); - - //rule - RulesItem4Ray rule = new() - { - inboundTag = new List { 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 GenerateClientSpeedtestConfig(ProfileItem node, int port) - { - var ret = new RetResult(); - try - { - if (node is not { Port: > 0 }) - { - 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(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 - - #region private gen function - - private async Task GenLog(V2rayConfig v2rayConfig) - { - try - { - if (_config.CoreBasicItem.LogEnabled) - { - var dtNow = DateTime.Now; - v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel; - v2rayConfig.log.access = Utils.GetLogPath($"Vaccess_{dtNow:yyyy-MM-dd}.txt"); - v2rayConfig.log.error = Utils.GetLogPath($"Verror_{dtNow:yyyy-MM-dd}.txt"); - } - else - { - v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel; - v2rayConfig.log.access = null; - v2rayConfig.log.error = null; - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return await Task.FromResult(0); - } - - private async Task 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 { 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(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; - } - - private async Task GenRouting(V2rayConfig v2rayConfig) - { - try - { - if (v2rayConfig.routing?.rules != null) - { - v2rayConfig.routing.domainStrategy = _config.RoutingBasicItem.DomainStrategy; - v2rayConfig.routing.domainMatcher = _config.RoutingBasicItem.DomainMatcher.IsNullOrEmpty() ? null : _config.RoutingBasicItem.DomainMatcher; - - var routing = await ConfigHandler.GetDefaultRouting(_config); - if (routing != null) - { - if (routing.DomainStrategy.IsNotEmpty()) - { - v2rayConfig.routing.domainStrategy = routing.DomainStrategy; - } - var rules = JsonUtils.Deserialize>(routing.RuleSet); - foreach (var item in rules) - { - if (item.Enabled) - { - var item2 = JsonUtils.Deserialize(JsonUtils.Serialize(item)); - await GenRoutingUserRule(item2, v2rayConfig); - } - } - } - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return 0; - } - - private async Task GenRoutingUserRule(RulesItem4Ray? rule, V2rayConfig v2rayConfig) - { - try - { - if (rule == null) - { - return 0; - } - rule.outboundTag = await GenRoutingUserRuleOutbound(rule.outboundTag, v2rayConfig); - - if (rule.port.IsNullOrEmpty()) - { - rule.port = null; - } - if (rule.network.IsNullOrEmpty()) - { - rule.network = null; - } - if (rule.domain?.Count == 0) - { - rule.domain = null; - } - if (rule.ip?.Count == 0) - { - rule.ip = null; - } - if (rule.protocol?.Count == 0) - { - rule.protocol = null; - } - if (rule.inboundTag?.Count == 0) - { - rule.inboundTag = null; - } - - var hasDomainIp = false; - if (rule.domain?.Count > 0) - { - var it = JsonUtils.DeepCopy(rule); - it.ip = null; - it.type = "field"; - for (var k = it.domain.Count - 1; k >= 0; k--) - { - if (it.domain[k].StartsWith("#")) - { - it.domain.RemoveAt(k); - } - it.domain[k] = it.domain[k].Replace(Global.RoutingRuleComma, ","); - } - v2rayConfig.routing.rules.Add(it); - hasDomainIp = true; - } - if (rule.ip?.Count > 0) - { - var it = JsonUtils.DeepCopy(rule); - it.domain = null; - it.type = "field"; - v2rayConfig.routing.rules.Add(it); - hasDomainIp = true; - } - if (!hasDomainIp) - { - if (rule.port.IsNotEmpty() - || rule.protocol?.Count > 0 - || rule.inboundTag?.Count > 0 - || rule.network != null - ) - { - var it = JsonUtils.DeepCopy(rule); - it.type = "field"; - v2rayConfig.routing.rules.Add(it); - } - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return await Task.FromResult(0); - } - - private async Task GenRoutingUserRuleOutbound(string outboundTag, V2rayConfig v2rayConfig) - { - if (Global.OutboundTags.Contains(outboundTag)) - { - return outboundTag; - } - - var node = await AppHandler.Instance.GetProfileItemViaRemarks(outboundTag); - if (node == null - || node.ConfigType == EConfigType.Custom - || node.ConfigType == EConfigType.Hysteria2 - || node.ConfigType == EConfigType.TUIC) - { - return Global.ProxyTag; - } - - var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); - var outbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(node, outbound); - outbound.tag = Global.ProxyTag + node.IndexId.ToString(); - v2rayConfig.outbounds.Add(outbound); - - return outbound.tag; - } - - private async Task GenOutbound(ProfileItem node, Outbounds4Ray outbound) - { - try - { - var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; - switch (node.ConfigType) - { - case EConfigType.VMess: - { - VnextItem4Ray vnextItem; - if (outbound.settings.vnext.Count <= 0) - { - vnextItem = new VnextItem4Ray(); - outbound.settings.vnext.Add(vnextItem); - } - else - { - vnextItem = outbound.settings.vnext.First(); - } - vnextItem.address = node.Address; - vnextItem.port = node.Port; - - UsersItem4Ray usersItem; - if (vnextItem.users.Count <= 0) - { - usersItem = new UsersItem4Ray(); - vnextItem.users.Add(usersItem); - } - else - { - usersItem = vnextItem.users.First(); - } - - usersItem.id = node.Id; - usersItem.alterId = node.AlterId; - usersItem.email = Global.UserEMail; - if (Global.VmessSecurities.Contains(node.Security)) - { - usersItem.security = node.Security; - } - else - { - usersItem.security = Global.DefaultSecurity; - } - - await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); - - outbound.settings.servers = null; - break; - } - case EConfigType.Shadowsocks: - { - ServersItem4Ray serversItem; - if (outbound.settings.servers.Count <= 0) - { - serversItem = new ServersItem4Ray(); - outbound.settings.servers.Add(serversItem); - } - else - { - serversItem = outbound.settings.servers.First(); - } - serversItem.address = node.Address; - serversItem.port = node.Port; - serversItem.password = node.Id; - serversItem.method = AppHandler.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : "none"; - - serversItem.ota = false; - serversItem.level = 1; - - await GenOutboundMux(node, outbound); - - outbound.settings.vnext = null; - break; - } - case EConfigType.SOCKS: - case EConfigType.HTTP: - { - ServersItem4Ray serversItem; - if (outbound.settings.servers.Count <= 0) - { - serversItem = new ServersItem4Ray(); - outbound.settings.servers.Add(serversItem); - } - else - { - serversItem = outbound.settings.servers.First(); - } - serversItem.address = node.Address; - serversItem.port = node.Port; - serversItem.method = null; - serversItem.password = null; - - if (node.Security.IsNotEmpty() - && node.Id.IsNotEmpty()) - { - SocksUsersItem4Ray socksUsersItem = new() - { - user = node.Security, - pass = node.Id, - level = 1 - }; - - serversItem.users = new List() { socksUsersItem }; - } - - await GenOutboundMux(node, outbound); - - outbound.settings.vnext = null; - break; - } - case EConfigType.VLESS: - { - VnextItem4Ray vnextItem; - if (outbound.settings.vnext?.Count <= 0) - { - vnextItem = new VnextItem4Ray(); - outbound.settings.vnext.Add(vnextItem); - } - else - { - vnextItem = outbound.settings.vnext.First(); - } - vnextItem.address = node.Address; - vnextItem.port = node.Port; - - UsersItem4Ray usersItem; - if (vnextItem.users.Count <= 0) - { - usersItem = new UsersItem4Ray(); - vnextItem.users.Add(usersItem); - } - else - { - usersItem = vnextItem.users.First(); - } - usersItem.id = node.Id; - usersItem.email = Global.UserEMail; - usersItem.encryption = node.Security; - - if (node.Flow.IsNullOrEmpty()) - { - await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); - } - else - { - usersItem.flow = node.Flow; - await GenOutboundMux(node, outbound, false, muxEnabled); - } - outbound.settings.servers = null; - break; - } - case EConfigType.Trojan: - { - ServersItem4Ray serversItem; - if (outbound.settings.servers.Count <= 0) - { - serversItem = new ServersItem4Ray(); - outbound.settings.servers.Add(serversItem); - } - else - { - serversItem = outbound.settings.servers.First(); - } - serversItem.address = node.Address; - serversItem.port = node.Port; - serversItem.password = node.Id; - - serversItem.ota = false; - serversItem.level = 1; - - await GenOutboundMux(node, outbound); - - outbound.settings.vnext = null; - break; - } - case EConfigType.WireGuard: - { - var peer = new WireguardPeer4Ray - { - publicKey = node.PublicKey, - endpoint = node.Address + ":" + node.Port.ToString() - }; - var setting = new Outboundsettings4Ray - { - address = Utils.String2List(node.RequestHost), - secretKey = node.Id, - reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(), - mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(), - peers = new List { peer } - }; - outbound.settings = setting; - outbound.settings.vnext = null; - outbound.settings.servers = null; - break; - } - } - - outbound.protocol = Global.ProtocolTypes[node.ConfigType]; - await GenBoundStreamSettings(node, outbound); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return 0; - } - - private async Task GenOutboundMux(ProfileItem node, Outbounds4Ray outbound, bool enabledTCP = false, bool enabledUDP = false) - { - try - { - outbound.mux.enabled = false; - outbound.mux.concurrency = -1; - - if (enabledTCP) - { - outbound.mux.enabled = true; - outbound.mux.concurrency = _config.Mux4RayItem.Concurrency; - } - else if (enabledUDP) - { - outbound.mux.enabled = true; - outbound.mux.xudpConcurrency = _config.Mux4RayItem.XudpConcurrency; - outbound.mux.xudpProxyUDP443 = _config.Mux4RayItem.XudpProxyUDP443; - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return await Task.FromResult(0); - } - - private async Task GenBoundStreamSettings(ProfileItem node, Outbounds4Ray outbound) - { - try - { - var streamSettings = outbound.streamSettings; - streamSettings.network = node.GetNetwork(); - var host = node.RequestHost.TrimEx(); - var path = node.Path.TrimEx(); - var sni = node.Sni.TrimEx(); - var useragent = ""; - if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty()) - { - try - { - useragent = Global.UserAgentTexts[_config.CoreBasicItem.DefUserAgent]; - } - catch (KeyNotFoundException) - { - useragent = _config.CoreBasicItem.DefUserAgent; - } - } - - //if tls - if (node.StreamSecurity == Global.StreamSecurity) - { - streamSettings.security = node.StreamSecurity; - - TlsSettings4Ray tlsSettings = new() - { - allowInsecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure), - alpn = node.GetAlpn(), - fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint - }; - if (sni.IsNotEmpty()) - { - tlsSettings.serverName = sni; - } - else if (host.IsNotEmpty()) - { - tlsSettings.serverName = Utils.String2List(host)?.First(); - } - streamSettings.tlsSettings = tlsSettings; - } - - //if Reality - if (node.StreamSecurity == Global.StreamSecurityReality) - { - streamSettings.security = node.StreamSecurity; - - TlsSettings4Ray realitySettings = new() - { - fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint, - serverName = sni, - publicKey = node.PublicKey, - shortId = node.ShortId, - spiderX = node.SpiderX, - show = false, - }; - - streamSettings.realitySettings = realitySettings; - } - - //streamSettings - switch (node.GetNetwork()) - { - case nameof(ETransport.kcp): - KcpSettings4Ray kcpSettings = new() - { - mtu = _config.KcpItem.Mtu, - tti = _config.KcpItem.Tti - }; - - kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity; - kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity; - - kcpSettings.congestion = _config.KcpItem.Congestion; - kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize; - kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize; - kcpSettings.header = new Header4Ray - { - type = node.HeaderType, - domain = host.IsNullOrEmpty() ? null : host - }; - if (path.IsNotEmpty()) - { - kcpSettings.seed = path; - } - streamSettings.kcpSettings = kcpSettings; - break; - //ws - case nameof(ETransport.ws): - WsSettings4Ray wsSettings = new(); - wsSettings.headers = new Headers4Ray(); - - if (host.IsNotEmpty()) - { - wsSettings.host = host; - wsSettings.headers.Host = host; - } - if (path.IsNotEmpty()) - { - wsSettings.path = path; - } - if (useragent.IsNotEmpty()) - { - wsSettings.headers.UserAgent = useragent; - } - streamSettings.wsSettings = wsSettings; - - break; - //httpupgrade - case nameof(ETransport.httpupgrade): - HttpupgradeSettings4Ray httpupgradeSettings = new(); - - if (path.IsNotEmpty()) - { - httpupgradeSettings.path = path; - } - if (host.IsNotEmpty()) - { - httpupgradeSettings.host = host; - } - streamSettings.httpupgradeSettings = httpupgradeSettings; - - break; - //xhttp - case nameof(ETransport.xhttp): - streamSettings.network = ETransport.xhttp.ToString(); - XhttpSettings4Ray xhttpSettings = new(); - - if (path.IsNotEmpty()) - { - xhttpSettings.path = path; - } - if (host.IsNotEmpty()) - { - xhttpSettings.host = host; - } - if (node.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(node.HeaderType)) - { - xhttpSettings.mode = node.HeaderType; - } - if (node.Extra.IsNotEmpty()) - { - xhttpSettings.extra = JsonUtils.ParseJson(node.Extra); - } - - streamSettings.xhttpSettings = xhttpSettings; - await GenOutboundMux(node, outbound); - - break; - //h2 - case nameof(ETransport.h2): - HttpSettings4Ray httpSettings = new(); - - if (host.IsNotEmpty()) - { - httpSettings.host = Utils.String2List(host); - } - httpSettings.path = path; - - streamSettings.httpSettings = httpSettings; - - break; - //quic - case nameof(ETransport.quic): - QuicSettings4Ray quicsettings = new() - { - security = host, - key = path, - header = new Header4Ray - { - type = node.HeaderType - } - }; - streamSettings.quicSettings = quicsettings; - if (node.StreamSecurity == Global.StreamSecurity) - { - if (sni.IsNotEmpty()) - { - streamSettings.tlsSettings.serverName = sni; - } - else - { - streamSettings.tlsSettings.serverName = node.Address; - } - } - break; - - case nameof(ETransport.grpc): - GrpcSettings4Ray grpcSettings = new() - { - authority = host.IsNullOrEmpty() ? null : host, - serviceName = path, - multiMode = node.HeaderType == Global.GrpcMultiMode, - idle_timeout = _config.GrpcItem.IdleTimeout, - health_check_timeout = _config.GrpcItem.HealthCheckTimeout, - permit_without_stream = _config.GrpcItem.PermitWithoutStream, - initial_windows_size = _config.GrpcItem.InitialWindowsSize, - }; - streamSettings.grpcSettings = grpcSettings; - break; - - default: - //tcp - if (node.HeaderType == Global.TcpHeaderHttp) - { - TcpSettings4Ray tcpSettings = new() - { - header = new Header4Ray - { - type = node.HeaderType - } - }; - - //request Host - string request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); - string[] arrHost = host.Split(','); - string host2 = string.Join(",".AppendQuotes(), arrHost); - request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}"); - request = request.Replace("$requestUserAgent$", $"{useragent.AppendQuotes()}"); - //Path - string pathHttp = @"/"; - if (path.IsNotEmpty()) - { - string[] arrPath = path.Split(','); - pathHttp = string.Join(",".AppendQuotes(), arrPath); - } - request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}"); - tcpSettings.header.request = JsonUtils.Deserialize(request); - - streamSettings.tcpSettings = tcpSettings; - } - break; - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return 0; - } - - private async Task GenDns(ProfileItem? node, V2rayConfig v2rayConfig) - { - try - { - var item = await AppHandler.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 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 normalHost = obj["hosts"]; - if (normalHost != null) - { - foreach (var host in systemHosts) - { - if (normalHost[host.Key] != null) - continue; - normalHost[host.Key] = host.Value; - } - } - } - } - - await GenDnsDomains(node, obj, item); - - v2rayConfig.dns = obj; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return 0; - } - - private async Task GenDnsDomains(ProfileItem? node, JsonNode dns, DNSItem? dNSItem) - { - if (node == null) - { - return 0; - } - var servers = dns["servers"]; - if (servers != null) - { - var domainList = new List(); - if (Utils.IsDomain(node.Address)) - { - domainList.Add(node.Address); - } - var subItem = await AppHandler.Instance.GetSubItem(node.Subid); - if (subItem is not null) - { - // Previous proxy - var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - if (prevNode is not null - && prevNode.ConfigType != EConfigType.Custom - && prevNode.ConfigType != EConfigType.Hysteria2 - && prevNode.ConfigType != EConfigType.TUIC - && Utils.IsDomain(prevNode.Address)) - { - domainList.Add(prevNode.Address); - } - - // Next proxy - var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile); - if (nextNode is not null - && nextNode.ConfigType != EConfigType.Custom - && nextNode.ConfigType != EConfigType.Hysteria2 - && nextNode.ConfigType != EConfigType.TUIC - && Utils.IsDomain(nextNode.Address)) - { - domainList.Add(nextNode.Address); - } - } - if (domainList.Count > 0) - { - var dnsServer = new DnsServer4Ray() - { - address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress, - skipFallback = true, - domains = domainList - }; - servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer)); - } - } - return await Task.FromResult(0); - } - - private async Task GenStatistic(V2rayConfig v2rayConfig) - { - if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) - { - string tag = EInboundProtocol.api.ToString(); - Metrics4Ray apiObj = new(); - Policy4Ray policyObj = new(); - SystemPolicy4Ray policySystemSetting = new(); - - v2rayConfig.stats = new Stats4Ray(); - - apiObj.tag = tag; - v2rayConfig.metrics = apiObj; - - policySystemSetting.statsOutboundDownlink = true; - policySystemSetting.statsOutboundUplink = true; - policyObj.system = policySystemSetting; - v2rayConfig.policy = policyObj; - - if (!v2rayConfig.inbounds.Exists(item => item.tag == tag)) - { - Inbounds4Ray apiInbound = new(); - Inboundsettings4Ray apiInboundSettings = new(); - apiInbound.tag = tag; - apiInbound.listen = Global.Loopback; - apiInbound.port = AppHandler.Instance.StatePort; - apiInbound.protocol = Global.InboundAPIProtocol; - apiInboundSettings.address = Global.Loopback; - apiInbound.settings = apiInboundSettings; - v2rayConfig.inbounds.Add(apiInbound); - } - - if (!v2rayConfig.routing.rules.Exists(item => item.outboundTag == tag)) - { - RulesItem4Ray apiRoutingRule = new() - { - inboundTag = new List { tag }, - outboundTag = tag, - type = "field" - }; - - v2rayConfig.routing.rules.Add(apiRoutingRule); - } - } - return await Task.FromResult(0); - } - - private async Task GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig) - { - //fragment proxy - if (_config.CoreBasicItem.EnableFragment - && v2rayConfig.outbounds.First().streamSettings?.security.IsNullOrEmpty() == false) - { - var fragmentOutbound = new Outbounds4Ray - { - protocol = "freedom", - tag = $"{Global.ProxyTag}3", - settings = new() - { - fragment = new() - { - packets = _config.Fragment4RayItem?.Packets, - length = _config.Fragment4RayItem?.Length, - interval = _config.Fragment4RayItem?.Interval - } - } - }; - - v2rayConfig.outbounds.Add(fragmentOutbound); - v2rayConfig.outbounds.First().streamSettings.sockopt = new() - { - dialerProxy = fragmentOutbound.tag - }; - return 0; - } - - if (node.Subid.IsNullOrEmpty()) - { - return 0; - } - try - { - var subItem = await AppHandler.Instance.GetSubItem(node.Subid); - if (subItem is null) - { - return 0; - } - - //current proxy - var outbound = v2rayConfig.outbounds.First(); - var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); - - //Previous proxy - var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - string? prevOutboundTag = null; - if (prevNode is not null - && prevNode.ConfigType != EConfigType.Custom - && prevNode.ConfigType != EConfigType.Hysteria2 - && prevNode.ConfigType != EConfigType.TUIC) - { - var prevOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(prevNode, prevOutbound); - prevOutboundTag = $"prev-{Global.ProxyTag}"; - prevOutbound.tag = prevOutboundTag; - v2rayConfig.outbounds.Add(prevOutbound); - } - var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag); - - if (nextOutbound is not null) - { - v2rayConfig.outbounds.Insert(0, nextOutbound); - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - - return 0; - } - - private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig) - { - try - { - // Get template and initialize list - var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); - if (txtOutbound.IsNullOrEmpty()) - { - return 0; - } - - var resultOutbounds = new List(); - var prevOutbounds = new List(); // Separate list for prev outbounds and fragment - - // Cache for chain proxies to avoid duplicate generation - var nextProxyCache = new Dictionary(); - var prevProxyTags = new Dictionary(); // Map from profile name to tag - int prevIndex = 0; // Index for prev outbounds - - // Process nodes - int index = 0; - foreach (var node in nodes) - { - index++; - - // Handle proxy chain - string? prevTag = null; - var currentOutbound = JsonUtils.Deserialize(txtOutbound); - var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null); - if (nextOutbound != null) - { - nextOutbound = JsonUtils.DeepCopy(nextOutbound); - } - - var subItem = await AppHandler.Instance.GetSubItem(node.Subid); - - // current proxy - await GenOutbound(node, currentOutbound); - currentOutbound.tag = $"{Global.ProxyTag}-{index}"; - - if (!node.Subid.IsNullOrEmpty()) - { - if (prevProxyTags.TryGetValue(node.Subid, out var value)) - { - prevTag = value; // maybe null - } - else - { - var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - if (prevNode is not null - && prevNode.ConfigType != EConfigType.Custom - && prevNode.ConfigType != EConfigType.Hysteria2 - && prevNode.ConfigType != EConfigType.TUIC) - { - var prevOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(prevNode, prevOutbound); - prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; - prevOutbound.tag = prevTag; - prevOutbounds.Add(prevOutbound); - } - prevProxyTags[node.Subid] = prevTag; - } - - nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound); - if (!nextProxyCache.ContainsKey(node.Subid)) - { - nextProxyCache[node.Subid] = nextOutbound; - } - } - - if (nextOutbound is not null) - { - resultOutbounds.Add(nextOutbound); - } - resultOutbounds.Add(currentOutbound); - } - - // Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds - resultOutbounds.AddRange(prevOutbounds); - resultOutbounds.AddRange(v2rayConfig.outbounds); - v2rayConfig.outbounds = resultOutbounds; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - - return 0; - } - - /// - /// Generates a chained outbound configuration for the given subItem and outbound. - /// The outbound's tag must be set before calling this method. - /// Returns the next proxy's outbound configuration, which may be null if no next proxy exists. - /// - /// The subscription item containing proxy chain information. - /// The current outbound configuration. Its tag must be set before calling this method. - /// The tag of the previous outbound in the chain, if any. - /// The outbound for the next proxy in the chain, if already created. If null, will be created inside. - /// - /// The outbound configuration for the next proxy in the chain, or null if no next proxy exists. - /// - private async Task GenChainOutbounds(SubItem subItem, Outbounds4Ray outbound, string? prevOutboundTag, Outbounds4Ray? nextOutbound = null) - { - try - { - var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); - - if (!prevOutboundTag.IsNullOrEmpty()) - { - outbound.streamSettings.sockopt = new() - { - dialerProxy = prevOutboundTag - }; - } - - // Next proxy - var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile); - if (nextNode is not null - && nextNode.ConfigType != EConfigType.Custom - && nextNode.ConfigType != EConfigType.Hysteria2 - && nextNode.ConfigType != EConfigType.TUIC) - { - if (nextOutbound == null) - { - nextOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(nextNode, nextOutbound); - } - nextOutbound.tag = outbound.tag; - - outbound.tag = $"mid-{outbound.tag}"; - nextOutbound.streamSettings.sockopt = new() - { - dialerProxy = outbound.tag - }; - } - return nextOutbound; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return null; - } - - private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) - { - if (multipleLoad == EMultipleLoad.LeastPing) - { - var observatory = new Observatory4Ray - { - subjectSelector = [Global.ProxyTag], - probeUrl = AppHandler.Instance.Config.SpeedTestItem.SpeedPingTestUrl, - probeInterval = "3m", - enableConcurrency = true, - }; - v2rayConfig.observatory = observatory; - } - else if (multipleLoad == EMultipleLoad.LeastLoad) - { - var burstObservatory = new BurstObservatory4Ray - { - subjectSelector = [Global.ProxyTag], - pingConfig = new() - { - destination = AppHandler.Instance.Config.SpeedTestItem.SpeedPingTestUrl, - interval = "5m", - timeout = "30s", - sampling = 2, - } - }; - v2rayConfig.burstObservatory = burstObservatory; - } - var strategyType = multipleLoad switch - { - EMultipleLoad.Random => "random", - EMultipleLoad.RoundRobin => "roundRobin", - EMultipleLoad.LeastPing => "leastPing", - EMultipleLoad.LeastLoad => "leastLoad", - _ => "roundRobin", - }; - var balancer = new BalancersItem4Ray - { - selector = [Global.ProxyTag], - strategy = new() { type = strategyType }, - tag = $"{Global.ProxyTag}-round", - }; - v2rayConfig.routing.balancers = [balancer]; - return await Task.FromResult(0); - } - - #endregion private gen function -} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs new file mode 100644 index 00000000..4e24d8e2 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs @@ -0,0 +1,522 @@ +using System.Net; +using System.Net.NetworkInformation; + +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 GenerateClientConfigContent(ProfileItem node) + { + var ret = new RetResult(); + try + { + if (node == null + || node.Port <= 0) + { + 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(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 GenerateClientSpeedtestConfig(List 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(result); + if (singboxConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + List lstIpEndPoints = new(); + List 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 (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + { + if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + 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 + if (item is null) + { + continue; + } + if (item.ConfigType == EConfigType.Shadowsocks + && !Global.SsSecuritiesInSingbox.Contains(item.Security)) + { + continue; + } + if (item.ConfigType == EConfigType.VLESS + && !Global.Flows.Contains(item.Flow)) + { + continue; + } + if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan + && item.StreamSecurity == Global.StreamSecurityReality + && item.PublicKey.IsNullOrEmpty()) + { + continue; + } + + 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 { 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.SingboxFinalResolverTag + }; + + 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 GenerateClientSpeedtestConfig(ProfileItem node, int port) + { + var ret = new RetResult(); + try + { + if (node is not { Port: > 0 }) + { + 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(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.SingboxFinalResolverTag + }; + + 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 GenerateClientMultipleLoadConfig(List 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(result); + if (singboxConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenLog(singboxConfig); + await GenInbounds(singboxConfig); + await GenRouting(singboxConfig); + await GenExperimental(singboxConfig); + singboxConfig.outbounds.RemoveAt(0); + + var proxyProfiles = new List(); + 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) + { + continue; + } + if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + { + if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + continue; + } + } + if (item.ConfigType == EConfigType.Shadowsocks + && !Global.SsSecuritiesInSingbox.Contains(item.Security)) + { + continue; + } + if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) + { + continue; + } + + //outbound + proxyProfiles.Add(item); + } + if (proxyProfiles.Count <= 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + await GenOutboundsList(proxyProfiles, 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 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(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 +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs new file mode 100644 index 00000000..c6bec22b --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Nodes; + +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task 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)); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs new file mode 100644 index 00000000..2f0f8015 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -0,0 +1,496 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task 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>(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; + + // Tun2SocksAddress + if (node != null && Utils.IsDomain(node.Address)) + { + singboxConfig.dns.rules ??= new List(); + singboxConfig.dns.rules.Insert(0, new Rule4Sbox + { + server = Global.SingboxOutboundResolverTag, + domain = [node.Address], + }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenDnsServers(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem) + { + var finalDns = await GenDnsDomains(singboxConfig, simpleDNSItem); + + var directDns = ParseDnsAddress(simpleDNSItem.DirectDNS); + directDns.tag = Global.SingboxDirectDNSTag; + directDns.domain_resolver = Global.SingboxFinalResolverTag; + + var remoteDns = ParseDnsAddress(simpleDNSItem.RemoteDNS); + remoteDns.tag = Global.SingboxRemoteDNSTag; + remoteDns.detour = Global.ProxyTag; + remoteDns.domain_resolver = Global.SingboxFinalResolverTag; + + var resolverDns = ParseDnsAddress(simpleDNSItem.SingboxOutboundsResolveDNS); + resolverDns.tag = Global.SingboxOutboundResolverTag; + resolverDns.domain_resolver = Global.SingboxFinalResolverTag; + + 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 { host.Value }); + } + } + } + + if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + { + var userHostsMap = simpleDNSItem.Hosts + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .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() + ); + + 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 (resolverDns.server == host.Key) + { + resolverDns.domain_resolver = Global.SingboxHostsDNSTag; + } + if (directDns.server == host.Key) + { + directDns.domain_resolver = Global.SingboxHostsDNSTag; + } + } + + singboxConfig.dns ??= new Dns4Sbox(); + singboxConfig.dns.servers ??= new List(); + singboxConfig.dns.servers.Add(remoteDns); + singboxConfig.dns.servers.Add(directDns); + singboxConfig.dns.servers.Add(resolverDns); + 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 GenDnsDomains(SingboxConfig singboxConfig, SimpleDNSItem? simpleDNSItem) + { + var finalDns = ParseDnsAddress(simpleDNSItem.SingboxFinalResolveDNS); + finalDns.tag = Global.SingboxFinalResolverTag; + singboxConfig.dns ??= new Dns4Sbox(); + singboxConfig.dns.servers ??= new List(); + singboxConfig.dns.servers.Add(finalDns); + return await Task.FromResult(finalDns); + } + + private async Task GenDnsRules(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem) + { + singboxConfig.dns ??= new Dns4Sbox(); + singboxConfig.dns.rules ??= new List(); + + 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 { 64, 65 }, + action = "predefined", + rcode = "NOTIMP" + }); + } + + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing == null) + return 0; + + var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; + var expectedIPCidr = new List(); + var expectedIPsRegions = new List(); + var regionNames = new HashSet(); + + 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; + } + + 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(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) + { + var rule4Fake = JsonUtils.DeepCopy(rule); + rule4Fake.server = Global.SingboxFakeDNSTag; + 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 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(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); + } + + // Tun2SocksAddress + if (node != null && Utils.IsDomain(node.Address)) + { + singboxConfig.dns.rules ??= new List(); + singboxConfig.dns.rules.Insert(0, new Rule4Sbox + { + server = Global.SingboxFinalResolverTag, + domain = [node.Address], + }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenDnsDomainsCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem) + { + var dns4Sbox = singboxConfig.dns ?? new(); + dns4Sbox.servers ??= []; + dns4Sbox.rules ??= []; + + var tag = Global.SingboxFinalResolverTag; + var localDnsAddress = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress; + + var localDnsServer = ParseDnsAddress(localDnsAddress); + localDnsServer.tag = tag; + + dns4Sbox.servers.Add(localDnsServer); + + singboxConfig.dns = dns4Sbox; + return await Task.FromResult(0); + } + + private async Task GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem) + { + var dns4Sbox = singboxConfig.dns ?? new(); + dns4Sbox.servers ??= []; + dns4Sbox.rules ??= []; + + var tag = Global.SingboxFinalResolverTag; + 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 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; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs new file mode 100644 index 00000000..91f76ad9 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs @@ -0,0 +1,92 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task 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(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; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxLogService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxLogService.cs new file mode 100644 index 00000000..59e65471 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxLogService.cs @@ -0,0 +1,40 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task 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); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs new file mode 100644 index 00000000..3f03b93c --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -0,0 +1,577 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task 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 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 GenServer(ProfileItem node) + { + try + { + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + if (node.ConfigType == EConfigType.WireGuard) + { + var endpoint = JsonUtils.Deserialize(txtOutbound); + await GenEndpoint(node, endpoint); + return endpoint; + } + else + { + var outbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(node, outbound); + return outbound; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(null); + } + + private async Task 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 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 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 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 GenOutboundsList(List nodes, SingboxConfig singboxConfig) + { + try + { + // Get outbound template and initialize lists + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + if (txtOutbound.IsNullOrEmpty()) + { + return 0; + } + + var resultOutbounds = new List(); + var resultEndpoints = new List(); // For endpoints + var prevOutbounds = new List(); // Separate list for prev outbounds + var prevEndpoints = new List(); // Separate list for prev endpoints + var proxyTags = new List(); // For selector and urltest outbounds + + // Cache for chain proxies to avoid duplicate generation + var nextProxyCache = new Dictionary(); + var prevProxyTags = new Dictionary(); // 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++; + + // 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 = $"{Global.ProxyTag}-{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(txtOutbound); + await GenOutbound(prevNode, prevOutbound); + prevTag = $"prev-{Global.ProxyTag}-{++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 = $"{Global.ProxyTag}-auto", + outbounds = proxyTags, + interrupt_exist_connections = false, + }; + + // Add selector outbound (manual selection) + var outSelector = new Outbound4Sbox + { + type = "selector", + tag = Global.ProxyTag, + 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 + resultOutbounds.AddRange(prevOutbounds); + resultOutbounds.AddRange(singboxConfig.outbounds); + singboxConfig.outbounds = resultOutbounds; + singboxConfig.endpoints ??= new List(); + resultEndpoints.AddRange(singboxConfig.endpoints); + singboxConfig.endpoints = resultEndpoints; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + return 0; + } + + private async Task 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; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs new file mode 100644 index 00000000..24804c50 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -0,0 +1,365 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task GenRouting(SingboxConfig singboxConfig) + { + try + { + singboxConfig.route.final = Global.ProxyTag; + var item = _config.SimpleDNSItem; + + var defaultDomainResolverTag = Global.SingboxOutboundResolverTag; + 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.SingboxFinalResolverTag; + 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>(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" + }); + } + + 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(); + if (routing != null) + { + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var item1 in rules ?? []) + { + if (item1.Enabled) + { + await GenRoutingUserRule(item1, singboxConfig); + if (item1.Ip != null && 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 lstDnsExe, out List lstDirectExe) + { + var dnsExeSet = new HashSet(StringComparer.OrdinalIgnoreCase); + var directExeSet = new HashSet(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(dnsExeSet); + lstDirectExe = new List(directExeSet); + } + + private async Task 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 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)) + { + return Global.ProxyTag; + } + + var server = await GenServer(node); + if (server is null) + { + return Global.ProxyTag; + } + + server.tag = Global.ProxyTag + node.IndexId.ToString(); + 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; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs new file mode 100644 index 00000000..ef611c91 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs @@ -0,0 +1,119 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task ConvertGeo2Ruleset(SingboxConfig singboxConfig) + { + static void AddRuleSets(List ruleSets, List? rule_set) + { + if (rule_set != null) + ruleSets.AddRange(rule_set); + } + var geosite = "geosite"; + var geoip = "geoip"; + var ruleSets = new List(); + + //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(); + 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(); + 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(); + 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(); + 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 customRulesets = []; + + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing.CustomRulesetPath4Singbox.IsNotEmpty()) + { + var result = EmbedUtils.LoadResource(routing.CustomRulesetPath4Singbox); + if (result.IsNotEmpty()) + { + customRulesets = (JsonUtils.Deserialize>(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(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; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxStatisticService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxStatisticService.cs new file mode 100644 index 00000000..c3acd810 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxStatisticService.cs @@ -0,0 +1,29 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task 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); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs new file mode 100644 index 00000000..b6148580 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -0,0 +1,411 @@ +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 GenerateClientConfigContent(ProfileItem node) + { + var ret = new RetResult(); + try + { + if (node == null + || node.Port <= 0) + { + 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; + + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + if (result.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var v2rayConfig = JsonUtils.Deserialize(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 GenerateClientMultipleLoadConfig(List selecteds, EMultipleLoad multipleLoad) + { + 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(result); + if (v2rayConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenLog(v2rayConfig); + await GenInbounds(v2rayConfig); + await GenRouting(v2rayConfig); + await GenDns(null, v2rayConfig); + await GenStatistic(v2rayConfig); + v2rayConfig.outbounds.RemoveAt(0); + + var proxyProfiles = new List(); + 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) + { + continue; + } + if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + { + if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + continue; + } + } + if (item.ConfigType == EConfigType.Shadowsocks + && !Global.SsSecuritiesInSingbox.Contains(item.Security)) + { + continue; + } + if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) + { + continue; + } + + //outbound + proxyProfiles.Add(item); + } + if (proxyProfiles.Count <= 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + await GenOutboundsList(proxyProfiles, v2rayConfig); + + //add balancers + await GenBalancer(v2rayConfig, multipleLoad); + + var balancer = v2rayConfig.routing.balancers.First(); + + //add rule + var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList(); + if (rules?.Count > 0) + { + foreach (var rule in rules) + { + rule.outboundTag = null; + rule.balancerTag = balancer.tag; + } + } + if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) + { + v2rayConfig.routing.rules.Add(new() + { + ip = ["0.0.0.0/0", "::/0"], + balancerTag = balancer.tag, + type = "field" + }); + } + else + { + v2rayConfig.routing.rules.Add(new() + { + network = "tcp,udp", + balancerTag = balancer.tag, + type = "field" + }); + } + + ret.Success = true; + + ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientSpeedtestConfig(List 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(result); + if (v2rayConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + List lstIpEndPoints = new(); + List 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 (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + { + if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + 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; + + //outbound + if (item is null) + { + continue; + } + if (item.ConfigType == EConfigType.Shadowsocks + && !Global.SsSecuritiesInXray.Contains(item.Security)) + { + continue; + } + if (item.ConfigType == EConfigType.VLESS + && !Global.Flows.Contains(item.Flow)) + { + continue; + } + if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan + && item.StreamSecurity == Global.StreamSecurityReality + && item.PublicKey.IsNullOrEmpty()) + { + continue; + } + + //inbound + Inbounds4Ray inbound = new() + { + listen = Global.Loopback, + port = port, + protocol = EInboundProtocol.mixed.ToString(), + }; + inbound.tag = inbound.protocol + inbound.port.ToString(); + v2rayConfig.inbounds.Add(inbound); + + var outbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(item, outbound); + outbound.tag = Global.ProxyTag + inbound.port.ToString(); + v2rayConfig.outbounds.Add(outbound); + + //rule + RulesItem4Ray rule = new() + { + inboundTag = new List { 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 GenerateClientSpeedtestConfig(ProfileItem node, int port) + { + var ret = new RetResult(); + try + { + if (node is not { Port: > 0 }) + { + 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(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 +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs new file mode 100644 index 00000000..8d2476e6 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs @@ -0,0 +1,50 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) + { + if (multipleLoad == EMultipleLoad.LeastPing) + { + var observatory = new Observatory4Ray + { + subjectSelector = [Global.ProxyTag], + probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, + probeInterval = "3m", + enableConcurrency = true, + }; + v2rayConfig.observatory = observatory; + } + else if (multipleLoad == EMultipleLoad.LeastLoad) + { + var burstObservatory = new BurstObservatory4Ray + { + subjectSelector = [Global.ProxyTag], + pingConfig = new() + { + destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, + interval = "5m", + timeout = "30s", + sampling = 2, + } + }; + v2rayConfig.burstObservatory = burstObservatory; + } + var strategyType = multipleLoad switch + { + EMultipleLoad.Random => "random", + EMultipleLoad.RoundRobin => "roundRobin", + EMultipleLoad.LeastPing => "leastPing", + EMultipleLoad.LeastLoad => "leastLoad", + _ => "roundRobin", + }; + var balancer = new BalancersItem4Ray + { + selector = [Global.ProxyTag], + strategy = new() { type = strategyType }, + tag = $"{Global.ProxyTag}-round", + }; + v2rayConfig.routing.balancers = [balancer]; + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs new file mode 100644 index 00000000..5d1f7d63 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs @@ -0,0 +1,86 @@ +using System.Text.Json.Nodes; + +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task ApplyFullConfigTemplate(V2rayConfig v2rayConfig, bool handleBalancerAndRules = false) + { + 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 (handleBalancerAndRules && 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() == 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)); + } + } + + // 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)); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs new file mode 100644 index 00000000..6f64e923 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -0,0 +1,410 @@ +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 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 { 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 { Global.DnsTag }, + outboundTag = Global.ProxyTag, + }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenDnsServers(ProfileItem? node, V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem) + { + static List 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 { defaultAddress }; + return addresses.Count > 0 ? addresses : new List { defaultAddress }; + } + + static object CreateDnsServer(string dnsAddress, List domains, List? 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(); + var directGeositeList = new List(); + var proxyDomainList = new List(); + var proxyGeositeList = new List(); + var expectedDomainList = new List(); + var expectedIPs = new List(); + var regionNames = new HashSet(); + + 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? rules = null; + if (routing != null) + { + rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; + foreach (var item in rules) + { + if (!item.Enabled || item.Domain is null || item.Domain.Count == 0) + { + 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(); + + void AddDnsServers(List dnsAddresses, List domains, List? 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 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(); + 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 { host.Value }); + } + } + } + + if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + { + var userHostsMap = simpleDNSItem.Hosts + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .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() + ); + + foreach (var kvp in userHostsMap) + { + v2rayConfig.dns.hosts[kvp.Key] = kvp.Value; + } + } + return await Task.FromResult(0); + } + + private async Task 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 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(out var ip)) + { + normalHost[hostProp.Key] = new JsonArray(ip); + } + } + } + + await GenDnsDomainsCompatible(node, obj, item); + + v2rayConfig.dns = JsonUtils.Deserialize(JsonUtils.Serialize(obj)); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dNSItem) + { + if (node == null) + { + return 0; + } + var servers = dns["servers"]; + if (servers != null) + { + var domainList = new List(); + 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); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs new file mode 100644 index 00000000..7753c21e --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -0,0 +1,72 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task 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 { 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(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; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayLogService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayLogService.cs new file mode 100644 index 00000000..5b9344fb --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayLogService.cs @@ -0,0 +1,29 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenLog(V2rayConfig v2rayConfig) + { + try + { + if (_config.CoreBasicItem.LogEnabled) + { + var dtNow = DateTime.Now; + v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel; + v2rayConfig.log.access = Utils.GetLogPath($"Vaccess_{dtNow:yyyy-MM-dd}.txt"); + v2rayConfig.log.error = Utils.GetLogPath($"Verror_{dtNow:yyyy-MM-dd}.txt"); + } + else + { + v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel; + v2rayConfig.log.access = null; + v2rayConfig.log.error = null; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs new file mode 100644 index 00000000..11e8a8fa --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -0,0 +1,695 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenOutbound(ProfileItem node, Outbounds4Ray outbound) + { + try + { + var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; + switch (node.ConfigType) + { + case EConfigType.VMess: + { + VnextItem4Ray vnextItem; + if (outbound.settings.vnext.Count <= 0) + { + vnextItem = new VnextItem4Ray(); + outbound.settings.vnext.Add(vnextItem); + } + else + { + vnextItem = outbound.settings.vnext.First(); + } + vnextItem.address = node.Address; + vnextItem.port = node.Port; + + UsersItem4Ray usersItem; + if (vnextItem.users.Count <= 0) + { + usersItem = new UsersItem4Ray(); + vnextItem.users.Add(usersItem); + } + else + { + usersItem = vnextItem.users.First(); + } + + usersItem.id = node.Id; + usersItem.alterId = node.AlterId; + usersItem.email = Global.UserEMail; + if (Global.VmessSecurities.Contains(node.Security)) + { + usersItem.security = node.Security; + } + else + { + usersItem.security = Global.DefaultSecurity; + } + + await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); + + outbound.settings.servers = null; + break; + } + case EConfigType.Shadowsocks: + { + ServersItem4Ray serversItem; + if (outbound.settings.servers.Count <= 0) + { + serversItem = new ServersItem4Ray(); + outbound.settings.servers.Add(serversItem); + } + else + { + serversItem = outbound.settings.servers.First(); + } + serversItem.address = node.Address; + serversItem.port = node.Port; + serversItem.password = node.Id; + serversItem.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : "none"; + + serversItem.ota = false; + serversItem.level = 1; + + await GenOutboundMux(node, outbound); + + outbound.settings.vnext = null; + break; + } + case EConfigType.SOCKS: + case EConfigType.HTTP: + { + ServersItem4Ray serversItem; + if (outbound.settings.servers.Count <= 0) + { + serversItem = new ServersItem4Ray(); + outbound.settings.servers.Add(serversItem); + } + else + { + serversItem = outbound.settings.servers.First(); + } + serversItem.address = node.Address; + serversItem.port = node.Port; + serversItem.method = null; + serversItem.password = null; + + if (node.Security.IsNotEmpty() + && node.Id.IsNotEmpty()) + { + SocksUsersItem4Ray socksUsersItem = new() + { + user = node.Security, + pass = node.Id, + level = 1 + }; + + serversItem.users = new List() { socksUsersItem }; + } + + await GenOutboundMux(node, outbound); + + outbound.settings.vnext = null; + break; + } + case EConfigType.VLESS: + { + VnextItem4Ray vnextItem; + if (outbound.settings.vnext?.Count <= 0) + { + vnextItem = new VnextItem4Ray(); + outbound.settings.vnext.Add(vnextItem); + } + else + { + vnextItem = outbound.settings.vnext.First(); + } + vnextItem.address = node.Address; + vnextItem.port = node.Port; + + UsersItem4Ray usersItem; + if (vnextItem.users.Count <= 0) + { + usersItem = new UsersItem4Ray(); + vnextItem.users.Add(usersItem); + } + else + { + usersItem = vnextItem.users.First(); + } + usersItem.id = node.Id; + usersItem.email = Global.UserEMail; + usersItem.encryption = node.Security; + + if (node.Flow.IsNullOrEmpty()) + { + await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); + } + else + { + usersItem.flow = node.Flow; + await GenOutboundMux(node, outbound, false, muxEnabled); + } + outbound.settings.servers = null; + break; + } + case EConfigType.Trojan: + { + ServersItem4Ray serversItem; + if (outbound.settings.servers.Count <= 0) + { + serversItem = new ServersItem4Ray(); + outbound.settings.servers.Add(serversItem); + } + else + { + serversItem = outbound.settings.servers.First(); + } + serversItem.address = node.Address; + serversItem.port = node.Port; + serversItem.password = node.Id; + + serversItem.ota = false; + serversItem.level = 1; + + await GenOutboundMux(node, outbound); + + outbound.settings.vnext = null; + break; + } + case EConfigType.WireGuard: + { + var peer = new WireguardPeer4Ray + { + publicKey = node.PublicKey, + endpoint = node.Address + ":" + node.Port.ToString() + }; + var setting = new Outboundsettings4Ray + { + address = Utils.String2List(node.RequestHost), + secretKey = node.Id, + reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(), + mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(), + peers = new List { peer } + }; + outbound.settings = setting; + outbound.settings.vnext = null; + outbound.settings.servers = null; + break; + } + } + + outbound.protocol = Global.ProtocolTypes[node.ConfigType]; + await GenBoundStreamSettings(node, outbound); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenOutboundMux(ProfileItem node, Outbounds4Ray outbound, bool enabledTCP = false, bool enabledUDP = false) + { + try + { + outbound.mux.enabled = false; + outbound.mux.concurrency = -1; + + if (enabledTCP) + { + outbound.mux.enabled = true; + outbound.mux.concurrency = _config.Mux4RayItem.Concurrency; + } + else if (enabledUDP) + { + outbound.mux.enabled = true; + outbound.mux.xudpConcurrency = _config.Mux4RayItem.XudpConcurrency; + outbound.mux.xudpProxyUDP443 = _config.Mux4RayItem.XudpProxyUDP443; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private async Task GenBoundStreamSettings(ProfileItem node, Outbounds4Ray outbound) + { + try + { + var streamSettings = outbound.streamSettings; + streamSettings.network = node.GetNetwork(); + var host = node.RequestHost.TrimEx(); + var path = node.Path.TrimEx(); + var sni = node.Sni.TrimEx(); + var useragent = ""; + if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty()) + { + try + { + useragent = Global.UserAgentTexts[_config.CoreBasicItem.DefUserAgent]; + } + catch (KeyNotFoundException) + { + useragent = _config.CoreBasicItem.DefUserAgent; + } + } + + //if tls + if (node.StreamSecurity == Global.StreamSecurity) + { + streamSettings.security = node.StreamSecurity; + + TlsSettings4Ray tlsSettings = new() + { + allowInsecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure), + alpn = node.GetAlpn(), + fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint + }; + if (sni.IsNotEmpty()) + { + tlsSettings.serverName = sni; + } + else if (host.IsNotEmpty()) + { + tlsSettings.serverName = Utils.String2List(host)?.First(); + } + streamSettings.tlsSettings = tlsSettings; + } + + //if Reality + if (node.StreamSecurity == Global.StreamSecurityReality) + { + streamSettings.security = node.StreamSecurity; + + TlsSettings4Ray realitySettings = new() + { + fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint, + serverName = sni, + publicKey = node.PublicKey, + shortId = node.ShortId, + spiderX = node.SpiderX, + mldsa65Verify = node.Mldsa65Verify, + show = false, + }; + + streamSettings.realitySettings = realitySettings; + } + + //streamSettings + switch (node.GetNetwork()) + { + case nameof(ETransport.kcp): + KcpSettings4Ray kcpSettings = new() + { + mtu = _config.KcpItem.Mtu, + tti = _config.KcpItem.Tti + }; + + kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity; + kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity; + + kcpSettings.congestion = _config.KcpItem.Congestion; + kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize; + kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize; + kcpSettings.header = new Header4Ray + { + type = node.HeaderType, + domain = host.IsNullOrEmpty() ? null : host + }; + if (path.IsNotEmpty()) + { + kcpSettings.seed = path; + } + streamSettings.kcpSettings = kcpSettings; + break; + //ws + case nameof(ETransport.ws): + WsSettings4Ray wsSettings = new(); + wsSettings.headers = new Headers4Ray(); + + if (host.IsNotEmpty()) + { + wsSettings.host = host; + wsSettings.headers.Host = host; + } + if (path.IsNotEmpty()) + { + wsSettings.path = path; + } + if (useragent.IsNotEmpty()) + { + wsSettings.headers.UserAgent = useragent; + } + streamSettings.wsSettings = wsSettings; + + break; + //httpupgrade + case nameof(ETransport.httpupgrade): + HttpupgradeSettings4Ray httpupgradeSettings = new(); + + if (path.IsNotEmpty()) + { + httpupgradeSettings.path = path; + } + if (host.IsNotEmpty()) + { + httpupgradeSettings.host = host; + } + streamSettings.httpupgradeSettings = httpupgradeSettings; + + break; + //xhttp + case nameof(ETransport.xhttp): + streamSettings.network = ETransport.xhttp.ToString(); + XhttpSettings4Ray xhttpSettings = new(); + + if (path.IsNotEmpty()) + { + xhttpSettings.path = path; + } + if (host.IsNotEmpty()) + { + xhttpSettings.host = host; + } + if (node.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(node.HeaderType)) + { + xhttpSettings.mode = node.HeaderType; + } + if (node.Extra.IsNotEmpty()) + { + xhttpSettings.extra = JsonUtils.ParseJson(node.Extra); + } + + streamSettings.xhttpSettings = xhttpSettings; + await GenOutboundMux(node, outbound); + + break; + //h2 + case nameof(ETransport.h2): + HttpSettings4Ray httpSettings = new(); + + if (host.IsNotEmpty()) + { + httpSettings.host = Utils.String2List(host); + } + httpSettings.path = path; + + streamSettings.httpSettings = httpSettings; + + break; + //quic + case nameof(ETransport.quic): + QuicSettings4Ray quicsettings = new() + { + security = host, + key = path, + header = new Header4Ray + { + type = node.HeaderType + } + }; + streamSettings.quicSettings = quicsettings; + if (node.StreamSecurity == Global.StreamSecurity) + { + if (sni.IsNotEmpty()) + { + streamSettings.tlsSettings.serverName = sni; + } + else + { + streamSettings.tlsSettings.serverName = node.Address; + } + } + break; + + case nameof(ETransport.grpc): + GrpcSettings4Ray grpcSettings = new() + { + authority = host.IsNullOrEmpty() ? null : host, + serviceName = path, + multiMode = node.HeaderType == Global.GrpcMultiMode, + idle_timeout = _config.GrpcItem.IdleTimeout, + health_check_timeout = _config.GrpcItem.HealthCheckTimeout, + permit_without_stream = _config.GrpcItem.PermitWithoutStream, + initial_windows_size = _config.GrpcItem.InitialWindowsSize, + }; + streamSettings.grpcSettings = grpcSettings; + break; + + default: + //tcp + if (node.HeaderType == Global.TcpHeaderHttp) + { + TcpSettings4Ray tcpSettings = new() + { + header = new Header4Ray + { + type = node.HeaderType + } + }; + + //request Host + string request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); + string[] arrHost = host.Split(','); + string host2 = string.Join(",".AppendQuotes(), arrHost); + request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}"); + request = request.Replace("$requestUserAgent$", $"{useragent.AppendQuotes()}"); + //Path + string pathHttp = @"/"; + if (path.IsNotEmpty()) + { + string[] arrPath = path.Split(','); + pathHttp = string.Join(",".AppendQuotes(), arrPath); + } + request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}"); + tcpSettings.header.request = JsonUtils.Deserialize(request); + + streamSettings.tcpSettings = tcpSettings; + } + break; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig) + { + //fragment proxy + if (_config.CoreBasicItem.EnableFragment + && v2rayConfig.outbounds.First().streamSettings?.security.IsNullOrEmpty() == false) + { + var fragmentOutbound = new Outbounds4Ray + { + protocol = "freedom", + tag = $"{Global.ProxyTag}3", + settings = new() + { + fragment = new() + { + packets = _config.Fragment4RayItem?.Packets, + length = _config.Fragment4RayItem?.Length, + interval = _config.Fragment4RayItem?.Interval + } + } + }; + + v2rayConfig.outbounds.Add(fragmentOutbound); + v2rayConfig.outbounds.First().streamSettings.sockopt = new() + { + dialerProxy = fragmentOutbound.tag + }; + return 0; + } + + if (node.Subid.IsNullOrEmpty()) + { + return 0; + } + try + { + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + if (subItem is null) + { + return 0; + } + + //current proxy + var outbound = v2rayConfig.outbounds.First(); + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + + //Previous proxy + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + string? prevOutboundTag = null; + if (prevNode is not null + && Global.XraySupportConfigType.Contains(prevNode.ConfigType)) + { + var prevOutbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(prevNode, prevOutbound); + prevOutboundTag = $"prev-{Global.ProxyTag}"; + prevOutbound.tag = prevOutboundTag; + v2rayConfig.outbounds.Add(prevOutbound); + } + var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag); + + if (nextOutbound is not null) + { + v2rayConfig.outbounds.Insert(0, nextOutbound); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + return 0; + } + + private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig) + { + try + { + // Get template and initialize list + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + if (txtOutbound.IsNullOrEmpty()) + { + return 0; + } + + var resultOutbounds = new List(); + var prevOutbounds = new List(); // Separate list for prev outbounds and fragment + + // Cache for chain proxies to avoid duplicate generation + var nextProxyCache = new Dictionary(); + var prevProxyTags = new Dictionary(); // Map from profile name to tag + int prevIndex = 0; // Index for prev outbounds + + // Process nodes + int index = 0; + foreach (var node in nodes) + { + index++; + + // Handle proxy chain + string? prevTag = null; + var currentOutbound = JsonUtils.Deserialize(txtOutbound); + var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null); + if (nextOutbound != null) + { + nextOutbound = JsonUtils.DeepCopy(nextOutbound); + } + + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + + // current proxy + await GenOutbound(node, currentOutbound); + currentOutbound.tag = $"{Global.ProxyTag}-{index}"; + + 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.XraySupportConfigType.Contains(prevNode.ConfigType)) + { + var prevOutbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(prevNode, prevOutbound); + prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; + prevOutbound.tag = prevTag; + prevOutbounds.Add(prevOutbound); + } + prevProxyTags[node.Subid] = prevTag; + } + + nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound); + if (!nextProxyCache.ContainsKey(node.Subid)) + { + nextProxyCache[node.Subid] = nextOutbound; + } + } + + if (nextOutbound is not null) + { + resultOutbounds.Add(nextOutbound); + } + resultOutbounds.Add(currentOutbound); + } + + // Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds + resultOutbounds.AddRange(prevOutbounds); + resultOutbounds.AddRange(v2rayConfig.outbounds); + v2rayConfig.outbounds = resultOutbounds; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + return 0; + } + + /// + /// Generates a chained outbound configuration for the given subItem and outbound. + /// The outbound's tag must be set before calling this method. + /// Returns the next proxy's outbound configuration, which may be null if no next proxy exists. + /// + /// The subscription item containing proxy chain information. + /// The current outbound configuration. Its tag must be set before calling this method. + /// The tag of the previous outbound in the chain, if any. + /// The outbound for the next proxy in the chain, if already created. If null, will be created inside. + /// + /// The outbound configuration for the next proxy in the chain, or null if no next proxy exists. + /// + private async Task GenChainOutbounds(SubItem subItem, Outbounds4Ray outbound, string? prevOutboundTag, Outbounds4Ray? nextOutbound = null) + { + try + { + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + + if (!prevOutboundTag.IsNullOrEmpty()) + { + outbound.streamSettings.sockopt = new() + { + dialerProxy = prevOutboundTag + }; + } + + // Next proxy + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + if (nextNode is not null + && Global.XraySupportConfigType.Contains(nextNode.ConfigType)) + { + if (nextOutbound == null) + { + nextOutbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(nextNode, nextOutbound); + } + nextOutbound.tag = outbound.tag; + + outbound.tag = $"mid-{outbound.tag}"; + nextOutbound.streamSettings.sockopt = new() + { + dialerProxy = outbound.tag + }; + } + return nextOutbound; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return null; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs new file mode 100644 index 00000000..1cb46bf2 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -0,0 +1,142 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenRouting(V2rayConfig v2rayConfig) + { + try + { + if (v2rayConfig.routing?.rules != null) + { + v2rayConfig.routing.domainStrategy = _config.RoutingBasicItem.DomainStrategy; + + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing != null) + { + if (routing.DomainStrategy.IsNotEmpty()) + { + v2rayConfig.routing.domainStrategy = routing.DomainStrategy; + } + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var item in rules) + { + if (item.Enabled) + { + var item2 = JsonUtils.Deserialize(JsonUtils.Serialize(item)); + await GenRoutingUserRule(item2, v2rayConfig); + } + } + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenRoutingUserRule(RulesItem4Ray? rule, V2rayConfig v2rayConfig) + { + try + { + if (rule == null) + { + return 0; + } + rule.outboundTag = await GenRoutingUserRuleOutbound(rule.outboundTag, v2rayConfig); + + if (rule.port.IsNullOrEmpty()) + { + rule.port = null; + } + if (rule.network.IsNullOrEmpty()) + { + rule.network = null; + } + if (rule.domain?.Count == 0) + { + rule.domain = null; + } + if (rule.ip?.Count == 0) + { + rule.ip = null; + } + if (rule.protocol?.Count == 0) + { + rule.protocol = null; + } + if (rule.inboundTag?.Count == 0) + { + rule.inboundTag = null; + } + + var hasDomainIp = false; + if (rule.domain?.Count > 0) + { + var it = JsonUtils.DeepCopy(rule); + it.ip = null; + it.type = "field"; + for (var k = it.domain.Count - 1; k >= 0; k--) + { + if (it.domain[k].StartsWith("#")) + { + it.domain.RemoveAt(k); + } + it.domain[k] = it.domain[k].Replace(Global.RoutingRuleComma, ","); + } + v2rayConfig.routing.rules.Add(it); + hasDomainIp = true; + } + if (rule.ip?.Count > 0) + { + var it = JsonUtils.DeepCopy(rule); + it.domain = null; + it.type = "field"; + v2rayConfig.routing.rules.Add(it); + hasDomainIp = true; + } + if (!hasDomainIp) + { + if (rule.port.IsNotEmpty() + || rule.protocol?.Count > 0 + || rule.inboundTag?.Count > 0 + || rule.network != null + ) + { + var it = JsonUtils.DeepCopy(rule); + it.type = "field"; + v2rayConfig.routing.rules.Add(it); + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private async Task GenRoutingUserRuleOutbound(string outboundTag, V2rayConfig v2rayConfig) + { + if (Global.OutboundTags.Contains(outboundTag)) + { + return outboundTag; + } + + var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null + || !Global.XraySupportConfigType.Contains(node.ConfigType)) + { + return Global.ProxyTag; + } + + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + var outbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(node, outbound); + outbound.tag = Global.ProxyTag + node.IndexId.ToString(); + v2rayConfig.outbounds.Add(outbound); + + return outbound.tag; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs new file mode 100644 index 00000000..1269a11f --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs @@ -0,0 +1,51 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenStatistic(V2rayConfig v2rayConfig) + { + if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) + { + string tag = EInboundProtocol.api.ToString(); + Metrics4Ray apiObj = new(); + Policy4Ray policyObj = new(); + SystemPolicy4Ray policySystemSetting = new(); + + v2rayConfig.stats = new Stats4Ray(); + + apiObj.tag = tag; + v2rayConfig.metrics = apiObj; + + policySystemSetting.statsOutboundDownlink = true; + policySystemSetting.statsOutboundUplink = true; + policyObj.system = policySystemSetting; + v2rayConfig.policy = policyObj; + + if (!v2rayConfig.inbounds.Exists(item => item.tag == tag)) + { + Inbounds4Ray apiInbound = new(); + Inboundsettings4Ray apiInboundSettings = new(); + apiInbound.tag = tag; + apiInbound.listen = Global.Loopback; + apiInbound.port = AppManager.Instance.StatePort; + apiInbound.protocol = Global.InboundAPIProtocol; + apiInboundSettings.address = Global.Loopback; + apiInbound.settings = apiInboundSettings; + v2rayConfig.inbounds.Add(apiInbound); + } + + if (!v2rayConfig.routing.rules.Exists(item => item.outboundTag == tag)) + { + RulesItem4Ray apiRoutingRule = new() + { + inboundTag = new List { tag }, + outboundTag = tag, + type = "field" + }; + + v2rayConfig.routing.rules.Add(apiRoutingRule); + } + } + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/DownloadService.cs b/v2rayN/ServiceLib/Services/DownloadService.cs index 51dee97a..99170646 100644 --- a/v2rayN/ServiceLib/Services/DownloadService.cs +++ b/v2rayN/ServiceLib/Services/DownloadService.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Net; using System.Net.Http.Headers; using System.Net.Sockets; @@ -16,11 +15,11 @@ public class DownloadService private static readonly string _tag = "DownloadService"; - public async Task DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Action updateFunc) + public async Task DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func updateFunc) { try { - SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); var progress = new Progress(); progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}"); @@ -32,10 +31,10 @@ public class DownloadService } catch (Exception ex) { - updateFunc?.Invoke(false, ex.Message); + await updateFunc?.Invoke(false, ex.Message); if (ex.InnerException != null) { - updateFunc?.Invoke(false, ex.InnerException.Message); + await updateFunc?.Invoke(false, ex.InnerException.Message); } } return 0; @@ -45,7 +44,7 @@ public class DownloadService { try { - SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); UpdateCompleted?.Invoke(this, new RetResult(false, $"{ResUI.Downloading} {url}")); var progress = new Progress(); @@ -72,7 +71,7 @@ public class DownloadService public async Task UrlRedirectAsync(string url, bool blProxy) { - SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); var webRequestHandler = new SocketsHttpHandler { AllowAutoRedirect = false, @@ -142,7 +141,7 @@ public class DownloadService { try { - SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); var webProxy = await GetWebProxy(blProxy); var client = new HttpClient(new SocketsHttpHandler() { @@ -187,7 +186,7 @@ public class DownloadService { try { - SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); var webProxy = await GetWebProxy(blProxy); @@ -210,70 +209,13 @@ public class DownloadService return null; } - public async Task RunAvailabilityCheck(IWebProxy? webProxy) - { - var responseTime = -1; - try - { - webProxy ??= await GetWebProxy(true); - var config = AppHandler.Instance.Config; - - for (var i = 0; i < 2; i++) - { - responseTime = await GetRealPingTime(config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); - if (responseTime > 0) - { - break; - } - await Task.Delay(500); - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - return -1; - } - return responseTime; - } - - public async Task 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 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; - } - private async Task GetWebProxy(bool blProxy) { if (!blProxy) { return null; } - var port = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks); + var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); if (await SocketCheck(Global.Loopback, port) == false) { return null; diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs index 998cedcc..5266f4d0 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -5,26 +5,20 @@ using System.Net.Sockets; namespace ServiceLib.Services; -public class SpeedtestService +public class SpeedtestService(Config config, Func updateFunc) { private static readonly string _tag = "SpeedtestService"; - private Config? _config; - private Action? _updateFunc; + private readonly Config? _config = config; + private readonly Func? _updateFunc = updateFunc; private static readonly ConcurrentBag _lstExitLoop = new(); - public SpeedtestService(Config config, Action updateFunc) - { - _config = config; - _updateFunc = updateFunc; - } - public void RunLoop(ESpeedActionType actionType, List selecteds) { Task.Run(async () => { await RunAsync(actionType, selecteds); - await ProfileExHandler.Instance.SaveTo(); - UpdateFunc("", ResUI.SpeedtestingCompleted); + await ProfileExManager.Instance.SaveTo(); + await UpdateFunc("", ResUI.SpeedtestingCompleted); }); } @@ -43,7 +37,7 @@ public class SpeedtestService var exitLoopKey = Utils.GetGuid(false); _lstExitLoop.Add(exitLoopKey); - var lstSelected = GetClearItem(actionType, selecteds); + var lstSelected = await GetClearItem(actionType, selecteds); switch (actionType) { @@ -65,7 +59,7 @@ public class SpeedtestService } } - private List GetClearItem(ESpeedActionType actionType, List selecteds) + private async Task> GetClearItem(ESpeedActionType actionType, List selecteds) { var lstSelected = new List(); foreach (var it in selecteds) @@ -97,19 +91,19 @@ public class SpeedtestService { case ESpeedActionType.Tcping: case ESpeedActionType.Realping: - UpdateFunc(it.IndexId, ResUI.Speedtesting, ""); - ProfileExHandler.Instance.SetTestDelay(it.IndexId, 0); + await UpdateFunc(it.IndexId, ResUI.Speedtesting, ""); + ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); break; case ESpeedActionType.Speedtest: - UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait); - ProfileExHandler.Instance.SetTestSpeed(it.IndexId, 0); + await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait); + ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0); break; case ESpeedActionType.Mixedtest: - UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait); - ProfileExHandler.Instance.SetTestDelay(it.IndexId, 0); - ProfileExHandler.Instance.SetTestSpeed(it.IndexId, 0); + await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait); + ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); + ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0); break; } } @@ -132,8 +126,8 @@ public class SpeedtestService { var responseTime = await GetTcpingTime(it.Address, it.Port); - ProfileExHandler.Instance.SetTestDelay(it.IndexId, responseTime); - UpdateFunc(it.IndexId, responseTime.ToString()); + ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); + await UpdateFunc(it.IndexId, responseTime.ToString()); } catch (Exception ex) { @@ -169,11 +163,11 @@ public class SpeedtestService { if (_lstExitLoop.Any(p => p == exitLoopKey) == false) { - UpdateFunc("", ResUI.SpeedtestingSkip); + await UpdateFunc("", ResUI.SpeedtestingSkip); return; } - UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); + await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount) { @@ -191,15 +185,13 @@ public class SpeedtestService var pid = -1; try { - pid = await CoreHandler.Instance.LoadCoreConfigSpeedtest(selecteds); + pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds); if (pid < 0) { return false; } await Task.Delay(1000); - var downloadHandle = new DownloadService(); - List tasks = new(); foreach (var it in selecteds) { @@ -213,7 +205,7 @@ public class SpeedtestService } tasks.Add(Task.Run(async () => { - await DoRealPing(downloadHandle, it); + await DoRealPing(it); })); } await Task.WhenAll(tasks); @@ -241,7 +233,7 @@ public class SpeedtestService { if (_lstExitLoop.Any(p => p == exitLoopKey) == false) { - UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); + await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); continue; } if (it.ConfigType == EConfigType.Custom) @@ -255,15 +247,15 @@ public class SpeedtestService var pid = -1; try { - pid = await CoreHandler.Instance.LoadCoreConfigSpeedtest(it); + pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); if (pid < 0) { - UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); + await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); } else { await Task.Delay(1000); - var delay = await DoRealPing(downloadHandle, it); + var delay = await DoRealPing(it); if (blSpeedTest) { if (delay > 0) @@ -272,7 +264,7 @@ public class SpeedtestService } else { - UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); + await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); } } } @@ -294,31 +286,31 @@ public class SpeedtestService await Task.WhenAll(tasks); } - private async Task DoRealPing(DownloadService downloadHandle, ServerTestItem it) + private async Task DoRealPing(ServerTestItem it) { var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); - var responseTime = await downloadHandle.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); + var responseTime = await HttpClientHelper.Instance.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); - ProfileExHandler.Instance.SetTestDelay(it.IndexId, responseTime); - UpdateFunc(it.IndexId, responseTime.ToString()); + ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); + await UpdateFunc(it.IndexId, responseTime.ToString()); return responseTime; } private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it) { - UpdateFunc(it.IndexId, "", ResUI.Speedtesting); + await UpdateFunc(it.IndexId, "", ResUI.Speedtesting); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var url = _config.SpeedTestItem.SpeedTestUrl; var timeout = _config.SpeedTestItem.SpeedTestTimeout; - await downloadHandle.DownloadDataAsync(url, webProxy, timeout, (success, msg) => + await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) => { decimal.TryParse(msg, out var dec); if (dec > 0) { - ProfileExHandler.Instance.SetTestSpeed(it.IndexId, dec); + ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec); } - UpdateFunc(it.IndexId, "", msg); + await UpdateFunc(it.IndexId, "", msg); }); } @@ -358,8 +350,8 @@ public class SpeedtestService private List> GetTestBatchItem(List lstSelected, int pageSize) { List> lstTest = new(); - var lst1 = lstSelected.Where(t => t.ConfigType is not (EConfigType.Hysteria2 or EConfigType.TUIC)).ToList(); - var lst2 = lstSelected.Where(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC).ToList(); + var lst1 = lstSelected.Where(t => Global.XraySupportConfigType.Contains(t.ConfigType)).ToList(); + var lst2 = lstSelected.Where(t => Global.SingboxSupportConfigType.Contains(t.ConfigType) && !Global.XraySupportConfigType.Contains(t.ConfigType)).ToList(); for (var num = 0; num < (int)Math.Ceiling(lst1.Count * 1.0 / pageSize); num++) { @@ -373,12 +365,12 @@ public class SpeedtestService return lstTest; } - private void UpdateFunc(string indexId, string delay, string speed = "") + private async Task UpdateFunc(string indexId, string delay, string speed = "") { - _updateFunc?.Invoke(new() { IndexId = indexId, Delay = delay, Speed = speed }); + await _updateFunc?.Invoke(new() { IndexId = indexId, Delay = delay, Speed = speed }); if (indexId.IsNotEmpty() && speed.IsNotEmpty()) { - ProfileExHandler.Instance.SetTestMessage(indexId, speed); + ProfileExManager.Instance.SetTestMessage(indexId, speed); } } } diff --git a/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs b/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs index f419f831..d3de6a18 100644 --- a/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs +++ b/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs @@ -8,11 +8,11 @@ public class StatisticsSingboxService private readonly Config _config; private bool _exitFlag; private ClientWebSocket? webSocket; - private Action? _updateFunc; - private string Url => $"ws://{Global.Loopback}:{AppHandler.Instance.StatePort2}/traffic"; + private readonly Func? _updateFunc; + private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic"; private static readonly string _tag = "StatisticsSingboxService"; - public StatisticsSingboxService(Config config, Action updateFunc) + public StatisticsSingboxService(Config config, Func updateFunc) { _config = config; _updateFunc = updateFunc; @@ -90,7 +90,7 @@ public class StatisticsSingboxService { ParseOutput(result, out var up, out var down); - _updateFunc?.Invoke(new ServerSpeedItem() + await _updateFunc?.Invoke(new ServerSpeedItem() { ProxyUp = (long)(up / 1000), ProxyDown = (long)(down / 1000) diff --git a/v2rayN/ServiceLib/Services/Statistics/StatisticsXrayService.cs b/v2rayN/ServiceLib/Services/Statistics/StatisticsXrayService.cs index 743114c6..b64a515d 100644 --- a/v2rayN/ServiceLib/Services/Statistics/StatisticsXrayService.cs +++ b/v2rayN/ServiceLib/Services/Statistics/StatisticsXrayService.cs @@ -6,10 +6,10 @@ public class StatisticsXrayService private ServerSpeedItem _serverSpeedItem = new(); private readonly Config _config; private bool _exitFlag; - private Action? _updateFunc; - private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppHandler.Instance.StatePort}/debug/vars"; + private readonly Func? _updateFunc; + private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars"; - public StatisticsXrayService(Config config, Action updateFunc) + public StatisticsXrayService(Config config, Func updateFunc) { _config = config; _updateFunc = updateFunc; @@ -39,7 +39,7 @@ public class StatisticsXrayService if (result != null) { var server = ParseOutput(result) ?? new ServerSpeedItem(); - _updateFunc?.Invoke(server); + await _updateFunc?.Invoke(server); } } catch diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs index 831ef2c3..429c0a62 100644 --- a/v2rayN/ServiceLib/Services/UpdateService.cs +++ b/v2rayN/ServiceLib/Services/UpdateService.cs @@ -5,11 +5,11 @@ namespace ServiceLib.Services; public class UpdateService { - private Action? _updateFunc; - private int _timeout = 30; + private Func? _updateFunc; + private readonly int _timeout = 30; private static readonly string _tag = "UpdateService"; - public async Task CheckUpdateGuiN(Config config, Action updateFunc, bool preRelease) + public async Task CheckUpdateGuiN(Config config, Func updateFunc, bool preRelease) { _updateFunc = updateFunc; var url = string.Empty; @@ -20,25 +20,25 @@ public class UpdateService { if (args.Success) { - _updateFunc?.Invoke(false, ResUI.MsgDownloadV2rayCoreSuccessfully); - _updateFunc?.Invoke(true, Utils.UrlEncode(fileName)); + UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully); + UpdateFunc(true, Utils.UrlEncode(fileName)); } else { - _updateFunc?.Invoke(false, args.Msg); + UpdateFunc(false, args.Msg); } }; downloadHandle.Error += (sender2, args) => { - _updateFunc?.Invoke(false, args.GetException().Message); + UpdateFunc(false, args.GetException().Message); }; - _updateFunc?.Invoke(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN)); + await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN)); var result = await CheckUpdateAsync(downloadHandle, ECoreType.v2rayN, preRelease); if (result.Success) { - _updateFunc?.Invoke(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN)); - _updateFunc?.Invoke(false, result.Msg); + await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN)); + await UpdateFunc(false, result.Msg); url = result.Data?.ToString(); fileName = Utils.GetTempPath(Utils.GetGuid()); @@ -46,11 +46,11 @@ public class UpdateService } else { - _updateFunc?.Invoke(false, result.Msg); + await UpdateFunc(false, result.Msg); } } - public async Task CheckUpdateCore(ECoreType type, Config config, Action updateFunc, bool preRelease) + public async Task CheckUpdateCore(ECoreType type, Config config, Func updateFunc, bool preRelease) { _updateFunc = updateFunc; var url = string.Empty; @@ -61,34 +61,34 @@ public class UpdateService { if (args.Success) { - _updateFunc?.Invoke(false, ResUI.MsgDownloadV2rayCoreSuccessfully); - _updateFunc?.Invoke(false, ResUI.MsgUnpacking); + UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully); + UpdateFunc(false, ResUI.MsgUnpacking); try { - _updateFunc?.Invoke(true, fileName); + UpdateFunc(true, fileName); } catch (Exception ex) { - _updateFunc?.Invoke(false, ex.Message); + UpdateFunc(false, ex.Message); } } else { - _updateFunc?.Invoke(false, args.Msg); + UpdateFunc(false, args.Msg); } }; downloadHandle.Error += (sender2, args) => { - _updateFunc?.Invoke(false, args.GetException().Message); + UpdateFunc(false, args.GetException().Message); }; - _updateFunc?.Invoke(false, string.Format(ResUI.MsgStartUpdating, type)); + await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, type)); var result = await CheckUpdateAsync(downloadHandle, type, preRelease); if (result.Success) { - _updateFunc?.Invoke(false, string.Format(ResUI.MsgParsingSuccessfully, type)); - _updateFunc?.Invoke(false, result.Msg); + await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type)); + await UpdateFunc(false, result.Msg); url = result.Data?.ToString(); var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url); @@ -99,148 +99,17 @@ public class UpdateService { if (!result.Msg.IsNullOrEmpty()) { - _updateFunc?.Invoke(false, result.Msg); + await UpdateFunc(false, result.Msg); } } } - public async Task UpdateSubscriptionProcess(Config config, string subId, bool blProxy, Action updateFunc) - { - _updateFunc = updateFunc; - - _updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart); - var subItem = await AppHandler.Instance.SubItems(); - - if (subItem is not { Count: > 0 }) - { - _updateFunc?.Invoke(false, ResUI.MsgNoValidSubscription); - return; - } - - foreach (var item in subItem) - { - var id = item.Id.TrimEx(); - var url = item.Url.TrimEx(); - var userAgent = item.UserAgent.TrimEx(); - var hashCode = $"{item.Remarks}->"; - if (id.IsNullOrEmpty() || url.IsNullOrEmpty() || (subId.IsNotEmpty() && item.Id != subId)) - { - //_updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgNoValidSubscription}"); - continue; - } - if (!url.StartsWith(Global.HttpsProtocol) && !url.StartsWith(Global.HttpProtocol)) - { - continue; - } - if (item.Enabled == false) - { - _updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}"); - continue; - } - - var downloadHandle = new DownloadService(); - downloadHandle.Error += (sender2, args) => - { - _updateFunc?.Invoke(false, $"{hashCode}{args.GetException().Message}"); - }; - - _updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}"); - - //one url - url = Utils.GetPunycode(url); - //convert - if (item.ConvertTarget.IsNotEmpty()) - { - var subConvertUrl = config.ConstItem.SubConvertUrl.IsNullOrEmpty() ? Global.SubConvertUrls.FirstOrDefault() : config.ConstItem.SubConvertUrl; - url = string.Format(subConvertUrl!, Utils.UrlEncode(url)); - if (!url.Contains("target=")) - { - url += string.Format("&target={0}", item.ConvertTarget); - } - if (!url.Contains("config=")) - { - url += string.Format("&config={0}", Global.SubConvertConfig.FirstOrDefault()); - } - } - var result = await downloadHandle.TryDownloadString(url, blProxy, userAgent); - if (blProxy && result.IsNullOrEmpty()) - { - result = await downloadHandle.TryDownloadString(url, false, userAgent); - } - - //more url - if (item.ConvertTarget.IsNullOrEmpty() && item.MoreUrl.TrimEx().IsNotEmpty()) - { - if (result.IsNotEmpty() && Utils.IsBase64String(result)) - { - result = Utils.Base64Decode(result); - } - - var lstUrl = item.MoreUrl.TrimEx().Split(",") ?? []; - foreach (var it in lstUrl) - { - var url2 = Utils.GetPunycode(it); - if (url2.IsNullOrEmpty()) - { - continue; - } - - var result2 = await downloadHandle.TryDownloadString(url2, blProxy, userAgent); - if (blProxy && result2.IsNullOrEmpty()) - { - result2 = await downloadHandle.TryDownloadString(url2, false, userAgent); - } - if (result2.IsNotEmpty()) - { - if (Utils.IsBase64String(result2)) - { - result += Environment.NewLine + Utils.Base64Decode(result2); - } - else - { - result += Environment.NewLine + result2; - } - } - } - } - - if (result.IsNullOrEmpty()) - { - _updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecodingFailed}"); - } - else - { - _updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgGetSubscriptionSuccessfully}"); - if (result?.Length < 99) - { - _updateFunc?.Invoke(false, $"{hashCode}{result}"); - } - - var ret = await ConfigHandler.AddBatchServers(config, result, id, true); - if (ret <= 0) - { - Logging.SaveLog("FailedImportSubscription"); - Logging.SaveLog(result); - } - _updateFunc?.Invoke(false, - ret > 0 - ? $"{hashCode}{ResUI.MsgUpdateSubscriptionEnd}" - : $"{hashCode}{ResUI.MsgFailedImportSubscription}"); - } - _updateFunc?.Invoke(false, "-------------------------------------------------------"); - - //await ConfigHandler.DedupServerList(config, id); - } - - _updateFunc?.Invoke(true, $"{ResUI.MsgUpdateSubscriptionEnd}"); - } - - public async Task UpdateGeoFileAll(Config config, Action updateFunc) + public async Task UpdateGeoFileAll(Config config, Func updateFunc) { await UpdateGeoFiles(config, updateFunc); await UpdateOtherFiles(config, updateFunc); await UpdateSrsFileAll(config, updateFunc); - _updateFunc?.Invoke(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); + await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); } #region CheckUpdate private @@ -259,14 +128,14 @@ public class UpdateService catch (Exception ex) { Logging.SaveLog(_tag, ex); - _updateFunc?.Invoke(false, ex.Message); + await UpdateFunc(false, ex.Message); return new RetResult(false, ex.Message); } } private async Task GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease) { - var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(type); + var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var tagName = string.Empty; if (preRelease) { @@ -300,7 +169,7 @@ public class UpdateService { try { - var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(type); + var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); string filePath = string.Empty; foreach (var name in coreInfo.CoreExes) { @@ -343,7 +212,7 @@ public class UpdateService catch (Exception ex) { Logging.SaveLog(_tag, ex); - _updateFunc?.Invoke(false, ex.Message); + await UpdateFunc(false, ex.Message); return new SemanticVersion(""); } } @@ -352,7 +221,7 @@ public class UpdateService { try { - var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(type); + var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var coreUrl = await GetUrlFromCore(coreInfo) ?? string.Empty; SemanticVersion curVersion; string message; @@ -403,7 +272,7 @@ public class UpdateService catch (Exception ex) { Logging.SaveLog(_tag, ex); - _updateFunc?.Invoke(false, ex.Message); + await UpdateFunc(false, ex.Message); return new RetResult(false, ex.Message); } } @@ -464,7 +333,7 @@ public class UpdateService #region Geo private - private async Task UpdateGeoFiles(Config config, Action updateFunc) + private async Task UpdateGeoFiles(Config config, Func updateFunc) { _updateFunc = updateFunc; @@ -483,7 +352,7 @@ public class UpdateService } } - private async Task UpdateOtherFiles(Config config, Action updateFunc) + private async Task UpdateOtherFiles(Config config, Func updateFunc) { //If it is not in China area, no update is required if (config.ConstItem.GeoSourceUrl.IsNotEmpty()) @@ -502,7 +371,7 @@ public class UpdateService } } - private async Task UpdateSrsFileAll(Config config, Action updateFunc) + private async Task UpdateSrsFileAll(Config config, Func updateFunc) { _updateFunc = updateFunc; @@ -510,7 +379,7 @@ public class UpdateService var geoSiteFiles = new List(); //Collect used files list - var routingItems = await AppHandler.Instance.RoutingItems(); + var routingItems = await AppManager.Instance.RoutingItems(); foreach (var routing in routingItems) { var rules = JsonUtils.Deserialize>(routing.RuleSet); @@ -557,7 +426,7 @@ public class UpdateService } } - private async Task UpdateSrsFile(string type, string srsName, Config config, Action updateFunc) + private async Task UpdateSrsFile(string type, string srsName, Config config, Func updateFunc) { var srsUrl = string.IsNullOrEmpty(config.ConstItem.SrsSourceUrl) ? Global.SingboxRulesetUrl @@ -565,12 +434,12 @@ public class UpdateService var fileName = $"{type}-{srsName}.srs"; var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName); - var url = string.Format(srsUrl, type, $"{type}-{srsName}"); + var url = string.Format(srsUrl, type, $"{type}-{srsName}", srsName); await DownloadGeoFile(url, fileName, targetPath, updateFunc); } - private async Task DownloadGeoFile(string url, string fileName, string targetPath, Action updateFunc) + private async Task DownloadGeoFile(string url, string fileName, string targetPath, Func updateFunc) { var tmpFileName = Utils.GetTempPath(Utils.GetGuid()); @@ -579,7 +448,7 @@ public class UpdateService { if (args.Success) { - _updateFunc?.Invoke(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName)); + UpdateFunc(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName)); try { @@ -588,26 +457,31 @@ public class UpdateService File.Copy(tmpFileName, targetPath, true); File.Delete(tmpFileName); - //_updateFunc?.Invoke(true, ""); + //await UpdateFunc(true, ""); } } catch (Exception ex) { - _updateFunc?.Invoke(false, ex.Message); + UpdateFunc(false, ex.Message); } } else { - _updateFunc?.Invoke(false, args.Msg); + UpdateFunc(false, args.Msg); } }; downloadHandle.Error += (sender2, args) => { - _updateFunc?.Invoke(false, args.GetException().Message); + UpdateFunc(false, args.GetException().Message); }; await downloadHandle.DownloadFileAsync(url, tmpFileName, true, _timeout); } #endregion Geo private + + private async Task UpdateFunc(bool notify, string msg) + { + await _updateFunc?.Invoke(notify, msg); + } } diff --git a/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs index 46c5298c..a94ecf74 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs @@ -19,7 +19,7 @@ public class AddServer2ViewModel : MyReactiveObject public AddServer2ViewModel(ProfileItem profileItem, Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; BrowseServerCmd = ReactiveCommand.CreateFromTask(async () => @@ -45,25 +45,25 @@ public class AddServer2ViewModel : MyReactiveObject var remarks = SelectedSource.Remarks; if (remarks.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseFillRemarks); + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); return; } if (SelectedSource.Address.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.FillServerAddressCustom); + NoticeManager.Instance.Enqueue(ResUI.FillServerAddressCustom); return; } SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0) { - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); _updateView?.Invoke(EViewAction.CloseWindow, null); } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } @@ -74,12 +74,12 @@ public class AddServer2ViewModel : MyReactiveObject return; } - var item = await AppHandler.Instance.GetProfileItem(SelectedSource.IndexId); + var item = await AppManager.Instance.GetProfileItem(SelectedSource.IndexId); item ??= SelectedSource; item.Address = fileName; if (await ConfigHandler.AddCustomServer(_config, item, false) == 0) { - NoticeHandler.Instance.Enqueue(ResUI.SuccessfullyImportedCustomServer); + NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedCustomServer); if (item.IndexId.IsNotEmpty()) { SelectedSource = JsonUtils.DeepCopy(item); @@ -88,7 +88,7 @@ public class AddServer2ViewModel : MyReactiveObject } else { - NoticeHandler.Instance.Enqueue(ResUI.FailedImportedCustomServer); + NoticeManager.Instance.Enqueue(ResUI.FailedImportedCustomServer); } } @@ -97,7 +97,7 @@ public class AddServer2ViewModel : MyReactiveObject var address = SelectedSource.Address; if (address.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.FillServerAddressCustom); + NoticeManager.Instance.Enqueue(ResUI.FillServerAddressCustom); return; } @@ -108,7 +108,7 @@ public class AddServer2ViewModel : MyReactiveObject } else { - NoticeHandler.Instance.Enqueue(ResUI.FailedReadConfiguration); + NoticeManager.Instance.Enqueue(ResUI.FailedReadConfiguration); } await Task.CompletedTask; } diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index c268ca42..cd2399bc 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -16,7 +16,7 @@ public class AddServerViewModel : MyReactiveObject public AddServerViewModel(ProfileItem profileItem, Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; SaveCmd = ReactiveCommand.CreateFromTask(async () => @@ -43,32 +43,32 @@ public class AddServerViewModel : MyReactiveObject { if (SelectedSource.Remarks.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseFillRemarks); + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); return; } if (SelectedSource.Address.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.FillServerAddress); + NoticeManager.Instance.Enqueue(ResUI.FillServerAddress); return; } var port = SelectedSource.Port.ToString(); if (port.IsNullOrEmpty() || !Utils.IsNumeric(port) || SelectedSource.Port <= 0 || SelectedSource.Port >= Global.MaxPort) { - NoticeHandler.Instance.Enqueue(ResUI.FillCorrectServerPort); + NoticeManager.Instance.Enqueue(ResUI.FillCorrectServerPort); return; } if (SelectedSource.ConfigType == EConfigType.Shadowsocks) { if (SelectedSource.Id.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.FillPassword); + NoticeManager.Instance.Enqueue(ResUI.FillPassword); return; } if (SelectedSource.Security.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectEncryption); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectEncryption); return; } } @@ -76,7 +76,7 @@ public class AddServerViewModel : MyReactiveObject { if (SelectedSource.Id.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.FillUUID); + NoticeManager.Instance.Enqueue(ResUI.FillUUID); return; } } @@ -84,12 +84,12 @@ public class AddServerViewModel : MyReactiveObject if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) { - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); _updateView?.Invoke(EViewAction.CloseWindow, null); } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } } diff --git a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs index 82fd80d7..5a96a04d 100644 --- a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs @@ -1,7 +1,6 @@ using System.Reactive; using ReactiveUI; using ReactiveUI.Fody.Helpers; -using Splat; namespace ServiceLib.ViewModels; @@ -22,7 +21,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject public BackupAndRestoreViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () => @@ -52,14 +51,14 @@ public class BackupAndRestoreViewModel : MyReactiveObject _config.WebDavItem = SelectedSource; _ = await ConfigHandler.SaveConfig(_config); - var result = await WebDavHandler.Instance.CheckConnection(); + var result = await WebDavManager.Instance.CheckConnection(); if (result) { DisplayOperationMsg(ResUI.OperationSuccess); } else { - DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); } } @@ -70,7 +69,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject var result = await CreateZipFileFromDirectory(fileName); if (result) { - var result2 = await WebDavHandler.Instance.PutFile(fileName); + var result2 = await WebDavManager.Instance.PutFile(fileName); if (result2) { DisplayOperationMsg(ResUI.OperationSuccess); @@ -78,21 +77,21 @@ public class BackupAndRestoreViewModel : MyReactiveObject } } - DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); } private async Task RemoteRestore() { DisplayOperationMsg(); var fileName = Utils.GetTempPath(Utils.GetGuid()); - var result = await WebDavHandler.Instance.GetRawFile(fileName); + var result = await WebDavManager.Instance.GetRawFile(fileName); if (result) { await LocalRestore(fileName); return; } - DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); } public async Task LocalBackup(string fileName) @@ -105,7 +104,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject } else { - DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); } return result; @@ -136,8 +135,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject var result = await CreateZipFileFromDirectory(fileBackup); if (result) { - var service = Locator.Current.GetService(); - await service?.MyAppExitAsync(true); + await AppManager.Instance.AppExitAsync(false); await SQLiteHelper.Instance.DisposeDbConnectionAsync(); var toPath = Utils.GetConfigPath(); @@ -154,11 +152,11 @@ public class BackupAndRestoreViewModel : MyReactiveObject _ = ProcUtils.ProcessStart(upgradeFileName, Global.RebootAs, Utils.StartupPath()); } } - service?.Shutdown(true); + AppManager.Instance.Shutdown(true); } else { - DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); } } diff --git a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs index f3247798..23839141 100644 --- a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -1,4 +1,6 @@ using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Runtime.InteropServices; using DynamicData; using DynamicData.Binding; @@ -11,22 +13,24 @@ namespace ServiceLib.ViewModels; public class CheckUpdateViewModel : MyReactiveObject { private const string _geo = "GeoFiles"; - private string _v2rayN = ECoreType.v2rayN.ToString(); + private readonly string _v2rayN = ECoreType.v2rayN.ToString(); private List _lstUpdated = []; + private static readonly string _tag = "CheckUpdateViewModel"; - private IObservableCollection _checkUpdateModel = new ObservableCollectionExtended(); - public IObservableCollection CheckUpdateModels => _checkUpdateModel; + public IObservableCollection CheckUpdateModels { get; } = new ObservableCollectionExtended(); public ReactiveCommand CheckUpdateCmd { get; } [Reactive] public bool EnableCheckPreReleaseUpdate { get; set; } public CheckUpdateViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; - CheckUpdateCmd = ReactiveCommand.CreateFromTask(async () => + CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate); + CheckUpdateCmd.ThrownExceptions.Subscribe(ex => { - await CheckUpdate(); + Logging.SaveLog(_tag, ex); + _ = UpdateView(_v2rayN, ex.Message); }); EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate; @@ -41,20 +45,20 @@ public class CheckUpdateViewModel : MyReactiveObject private void RefreshCheckUpdateItems() { - _checkUpdateModel.Clear(); + CheckUpdateModels.Clear(); if (RuntimeInformation.ProcessArchitecture != Architecture.X86) { - _checkUpdateModel.Add(GetCheckUpdateModel(_v2rayN)); + CheckUpdateModels.Add(GetCheckUpdateModel(_v2rayN)); //Not Windows and under Win10 if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10)) { - _checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.Xray.ToString())); - _checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString())); - _checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString())); + CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.Xray.ToString())); + CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString())); + CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString())); } } - _checkUpdateModel.Add(GetCheckUpdateModel(_geo)); + CheckUpdateModels.Add(GetCheckUpdateModel(_geo)); } private CheckUpdateModel GetCheckUpdateModel(string coreType) @@ -69,32 +73,31 @@ public class CheckUpdateViewModel : MyReactiveObject private async Task SaveSelectedCoreTypes() { - _config.CheckUpdateItem.SelectedCoreTypes = _checkUpdateModel.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList(); + _config.CheckUpdateItem.SelectedCoreTypes = CheckUpdateModels.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList(); await ConfigHandler.SaveConfig(_config); } private async Task CheckUpdate() { - await Task.Run(async () => - { - await CheckUpdateTask(); - }); + await Task.Run(CheckUpdateTask); } private async Task CheckUpdateTask() { _lstUpdated.Clear(); - _lstUpdated = _checkUpdateModel.Where(x => x.IsSelected == true) + _lstUpdated = CheckUpdateModels.Where(x => x.IsSelected == true) .Select(x => new CheckUpdateModel() { CoreType = x.CoreType }).ToList(); await SaveSelectedCoreTypes(); - for (var k = _checkUpdateModel.Count - 1; k >= 0; k--) + for (var k = CheckUpdateModels.Count - 1; k >= 0; k--) { - var item = _checkUpdateModel[k]; + var item = CheckUpdateModels[k]; if (item.IsSelected != true) + { continue; + } - UpdateView(item.CoreType, "..."); + await UpdateView(item.CoreType, "..."); if (item.CoreType == _geo) { await CheckUpdateGeo(); @@ -132,9 +135,9 @@ public class CheckUpdateViewModel : MyReactiveObject private async Task CheckUpdateGeo() { - void _updateUI(bool success, string msg) + async Task _updateUI(bool success, string msg) { - UpdateView(_geo, msg); + await UpdateView(_geo, msg); if (success) { UpdatedPlusPlus(_geo, ""); @@ -149,12 +152,12 @@ public class CheckUpdateViewModel : MyReactiveObject private async Task CheckUpdateN(bool preRelease) { - void _updateUI(bool success, string msg) + async Task _updateUI(bool success, string msg) { - UpdateView(_v2rayN, msg); + await UpdateView(_v2rayN, msg); if (success) { - UpdateView(_v2rayN, ResUI.OperationSuccess); + await UpdateView(_v2rayN, ResUI.OperationSuccess); UpdatedPlusPlus(_v2rayN, msg); } } @@ -167,12 +170,12 @@ public class CheckUpdateViewModel : MyReactiveObject private async Task CheckUpdateCore(CheckUpdateModel model, bool preRelease) { - void _updateUI(bool success, string msg) + async Task _updateUI(bool success, string msg) { - UpdateView(model.CoreType, msg); + await UpdateView(model.CoreType, msg); if (success) { - UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore); + await UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore); UpdatedPlusPlus(model.CoreType, msg); } @@ -189,21 +192,30 @@ public class CheckUpdateViewModel : MyReactiveObject { if (_lstUpdated.Count > 0 && _lstUpdated.Count(x => x.IsFinished == true) == _lstUpdated.Count) { - _updateView?.Invoke(EViewAction.DispatcherCheckUpdateFinished, false); + await UpdateFinishedSub(false); await Task.Delay(2000); await UpgradeCore(); if (_lstUpdated.Any(x => x.CoreType == _v2rayN && x.IsFinished == true)) { await Task.Delay(1000); - UpgradeN(); + await UpgradeN(); } await Task.Delay(1000); - _updateView?.Invoke(EViewAction.DispatcherCheckUpdateFinished, true); + await UpdateFinishedSub(true); } } - public void UpdateFinishedResult(bool blReload) + private async Task UpdateFinishedSub(bool blReload) + { + RxApp.MainThreadScheduler.Schedule(blReload, (scheduler, blReload) => + { + _ = UpdateFinishedResult(blReload); + return Disposable.Empty; + }); + } + + public async Task UpdateFinishedResult(bool blReload) { if (blReload) { @@ -215,7 +227,7 @@ public class CheckUpdateViewModel : MyReactiveObject } } - private void UpgradeN() + private async Task UpgradeN() { try { @@ -224,16 +236,23 @@ public class CheckUpdateViewModel : MyReactiveObject { return; } - if (!Utils.UpgradeAppExists(out _)) + if (!Utils.UpgradeAppExists(out var upgradeFileName)) { - UpdateView(_v2rayN, ResUI.UpgradeAppNotExistTip); + await UpdateView(_v2rayN, ResUI.UpgradeAppNotExistTip); + NoticeManager.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip); + Logging.SaveLog("UpgradeApp does not exist"); return; } - Locator.Current.GetService()?.UpgradeApp(fileName); + + var id = ProcUtils.ProcessStart(upgradeFileName, fileName, Utils.StartupPath()); + if (id > 0) + { + await AppManager.Instance.AppExitAsync(true); + } } catch (Exception ex) { - UpdateView(_v2rayN, ex.Message); + await UpdateView(_v2rayN, ex.Message); } } @@ -284,7 +303,7 @@ public class CheckUpdateViewModel : MyReactiveObject } } - UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully); + await UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully); if (File.Exists(fileName)) { @@ -293,23 +312,31 @@ public class CheckUpdateViewModel : MyReactiveObject } } - private void UpdateView(string coreType, string msg) + private async Task UpdateView(string coreType, string msg) { var item = new CheckUpdateModel() { CoreType = coreType, Remarks = msg, }; - _updateView?.Invoke(EViewAction.DispatcherCheckUpdate, item); + + RxApp.MainThreadScheduler.Schedule(item, (scheduler, model) => + { + _ = UpdateViewResult(model); + return Disposable.Empty; + }); } - public void UpdateViewResult(CheckUpdateModel model) + public async Task UpdateViewResult(CheckUpdateModel model) { - var found = _checkUpdateModel.FirstOrDefault(t => t.CoreType == model.CoreType); + var found = CheckUpdateModels.FirstOrDefault(t => t.CoreType == model.CoreType); if (found == null) + { return; + } + var itemCopy = JsonUtils.DeepCopy(found); itemCopy.Remarks = model.Remarks; - _checkUpdateModel.Replace(found, itemCopy); + CheckUpdateModels.Replace(found, itemCopy); } } diff --git a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs index a042da51..d45b8e7d 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs @@ -1,4 +1,5 @@ using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using DynamicData; using DynamicData.Binding; @@ -9,8 +10,7 @@ namespace ServiceLib.ViewModels; public class ClashConnectionsViewModel : MyReactiveObject { - private IObservableCollection _connectionItems = new ObservableCollectionExtended(); - public IObservableCollection ConnectionItems => _connectionItems; + public IObservableCollection ConnectionItems { get; } = new ObservableCollectionExtended(); [Reactive] public ClashConnectionModel SelectedSource { get; set; } @@ -26,7 +26,7 @@ public class ClashConnectionsViewModel : MyReactiveObject public ClashConnectionsViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh; @@ -58,18 +58,22 @@ public class ClashConnectionsViewModel : MyReactiveObject private async Task GetClashConnections() { - var ret = await ClashApiHandler.Instance.GetClashConnectionsAsync(); + var ret = await ClashApiManager.Instance.GetClashConnectionsAsync(); if (ret == null) { return; } - _ = _updateView?.Invoke(EViewAction.DispatcherRefreshConnections, ret?.connections); + RxApp.MainThreadScheduler.Schedule(ret?.connections, (scheduler, model) => + { + _ = RefreshConnections(model); + return Disposable.Empty; + }); } - public void RefreshConnections(List? connections) + public async Task RefreshConnections(List? connections) { - _connectionItems.Clear(); + ConnectionItems.Clear(); var dtNow = DateTime.Now; var lstModel = new List(); @@ -99,7 +103,7 @@ public class ClashConnectionsViewModel : MyReactiveObject return; } - _connectionItems.AddRange(lstModel); + ConnectionItems.AddRange(lstModel); } public async Task ClashConnectionClose(bool all) @@ -116,9 +120,9 @@ public class ClashConnectionsViewModel : MyReactiveObject } else { - _connectionItems.Clear(); + ConnectionItems.Clear(); } - await ClashApiHandler.Instance.ClashConnectionClose(id); + await ClashApiManager.Instance.ClashConnectionClose(id); await GetClashConnections(); } diff --git a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs index e3f053fb..54cbdc8d 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs @@ -1,4 +1,6 @@ using System.Reactive; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; using System.Reactive.Linq; using DynamicData; using DynamicData.Binding; @@ -15,11 +17,8 @@ public class ClashProxiesViewModel : MyReactiveObject private Dictionary? _providers; private readonly int _delayTimeout = 99999999; - private IObservableCollection _proxyGroups = new ObservableCollectionExtended(); - private IObservableCollection _proxyDetails = new ObservableCollectionExtended(); - - public IObservableCollection ProxyGroups => _proxyGroups; - public IObservableCollection ProxyDetails => _proxyDetails; + public IObservableCollection ProxyGroups { get; } = new ObservableCollectionExtended(); + public IObservableCollection ProxyDetails { get; } = new ObservableCollectionExtended(); [Reactive] public ClashProxyModel SelectedGroup { get; set; } @@ -43,7 +42,7 @@ public class ClashProxiesViewModel : MyReactiveObject public ClashProxiesViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () => @@ -152,13 +151,13 @@ public class ClashProxiesViewModel : MyReactiveObject { { "mode", mode.ToString().ToLower() } }; - await ClashApiHandler.Instance.ClashConfigUpdate(headers); + await ClashApiManager.Instance.ClashConfigUpdate(headers); } } private async Task GetClashProxies(bool refreshUI) { - var ret = await ClashApiHandler.Instance.GetClashProxiesAsync(); + var ret = await ClashApiManager.Instance.GetClashProxiesAsync(); if (ret?.Item1 == null || ret.Item2 == null) { return; @@ -168,11 +167,11 @@ public class ClashProxiesViewModel : MyReactiveObject if (refreshUI) { - _updateView?.Invoke(EViewAction.DispatcherRefreshProxyGroups, null); + RxApp.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups()); } } - public void RefreshProxyGroups() + public async Task RefreshProxyGroups() { if (_proxies == null) { @@ -180,9 +179,9 @@ public class ClashProxiesViewModel : MyReactiveObject } var selectedName = SelectedGroup?.Name; - _proxyGroups.Clear(); + ProxyGroups.Clear(); - var proxyGroups = ClashApiHandler.Instance.GetClashProxyGroups(); + var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups(); if (proxyGroups != null && proxyGroups.Count > 0) { foreach (var it in proxyGroups) @@ -196,7 +195,7 @@ public class ClashProxiesViewModel : MyReactiveObject { continue; } - _proxyGroups.Add(new ClashProxyModel() + ProxyGroups.Add(new ClashProxyModel() { Now = item.now, Name = item.name, @@ -212,12 +211,12 @@ public class ClashProxiesViewModel : MyReactiveObject { continue; } - var item = _proxyGroups.FirstOrDefault(t => t.Name == kv.Key); + var item = ProxyGroups.FirstOrDefault(t => t.Name == kv.Key); if (item != null && item.Name.IsNotEmpty()) { continue; } - _proxyGroups.Add(new ClashProxyModel() + ProxyGroups.Add(new ClashProxyModel() { Now = kv.Value.now, Name = kv.Key, @@ -225,16 +224,16 @@ public class ClashProxiesViewModel : MyReactiveObject }); } - if (_proxyGroups != null && _proxyGroups.Count > 0) + if (ProxyGroups != null && ProxyGroups.Count > 0) { - if (selectedName != null && _proxyGroups.Any(t => t.Name == selectedName)) + if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName)) { - SelectedGroup = _proxyGroups.FirstOrDefault(t => t.Name == selectedName); + SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName); } else { - SelectedGroup = _proxyGroups.First(); - } + SelectedGroup = ProxyGroups.First(); + } } else { @@ -244,7 +243,7 @@ public class ClashProxiesViewModel : MyReactiveObject private void RefreshProxyDetails(bool c) { - _proxyDetails.Clear(); + ProxyDetails.Clear(); if (!c) { return; @@ -297,7 +296,7 @@ public class ClashProxiesViewModel : MyReactiveObject default: break; } - _proxyDetails.AddRange(lstDetails); + ProxyDetails.AddRange(lstDetails); } private ProxiesItem? TryGetProxy(string name) @@ -352,43 +351,48 @@ public class ClashProxiesViewModel : MyReactiveObject var selectedProxy = TryGetProxy(name); if (selectedProxy == null || selectedProxy.type != "Selector") { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); return; } - await ClashApiHandler.Instance.ClashSetActiveProxy(name, nameNode); + await ClashApiManager.Instance.ClashSetActiveProxy(name, nameNode); selectedProxy.now = nameNode; - var group = _proxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name); + var group = ProxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name); if (group != null) { group.Now = nameNode; var group2 = JsonUtils.DeepCopy(group); - _proxyGroups.Replace(group, group2); + ProxyGroups.Replace(group, group2); SelectedGroup = group2; } - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } private async Task ProxiesDelayTest(bool blAll = true) { - ClashApiHandler.Instance.ClashProxiesDelayTest(blAll, _proxyDetails.ToList(), (item, result) => + ClashApiManager.Instance.ClashProxiesDelayTest(blAll, ProxyDetails.ToList(), async (item, result) => { if (item == null || result.IsNullOrEmpty()) { return; } - _updateView?.Invoke(EViewAction.DispatcherProxiesDelayTest, new SpeedTestResult() { IndexId = item.Name, Delay = result }); + var model = new SpeedTestResult() { IndexId = item.Name, Delay = result }; + RxApp.MainThreadScheduler.Schedule(model, (scheduler, model) => + { + _ = ProxiesDelayTestResult(model); + return Disposable.Empty; + }); }); await Task.CompletedTask; } - public void ProxiesDelayTestResult(SpeedTestResult result) + public async Task ProxiesDelayTestResult(SpeedTestResult result) { //UpdateHandler(false, $"{item.name}={result}"); - var detail = _proxyDetails.FirstOrDefault(it => it.Name == result.IndexId); + var detail = ProxyDetails.FirstOrDefault(it => it.Name == result.IndexId); if (detail == null) { return; @@ -410,7 +414,7 @@ public class ClashProxiesViewModel : MyReactiveObject detail.Delay = _delayTimeout; detail.DelayName = string.Empty; } - _proxyDetails.Replace(detail, JsonUtils.DeepCopy(detail)); + ProxyDetails.Replace(detail, JsonUtils.DeepCopy(detail)); } #endregion proxy function diff --git a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs index 1d6829b8..9ca2d407 100644 --- a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -6,39 +6,52 @@ namespace ServiceLib.ViewModels; public class DNSSettingViewModel : MyReactiveObject { - [Reactive] public bool UseSystemHosts { get; set; } - [Reactive] public string DomainStrategy4Freedom { get; set; } - [Reactive] public string DomainDNSAddress { get; set; } - [Reactive] public string NormalDNS { get; set; } + [Reactive] public bool? UseSystemHosts { get; set; } + [Reactive] public bool? AddCommonHosts { get; set; } + [Reactive] public bool? FakeIP { get; set; } + [Reactive] public bool? BlockBindingQuery { get; set; } + [Reactive] public string? DirectDNS { get; set; } + [Reactive] public string? RemoteDNS { get; set; } + [Reactive] public string? SingboxOutboundsResolveDNS { get; set; } + [Reactive] public string? SingboxFinalResolveDNS { get; set; } + [Reactive] public string? RayStrategy4Freedom { get; set; } + [Reactive] public string? SingboxStrategy4Direct { get; set; } + [Reactive] public string? SingboxStrategy4Proxy { get; set; } + [Reactive] public string? Hosts { get; set; } + [Reactive] public string? DirectExpectedIPs { get; set; } - [Reactive] public string DomainStrategy4Freedom2 { get; set; } - [Reactive] public string DomainDNSAddress2 { get; set; } - [Reactive] public string NormalDNS2 { get; set; } - [Reactive] public string TunDNS2 { get; set; } + [Reactive] public bool UseSystemHostsCompatible { get; set; } + [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } + [Reactive] public string DomainDNSAddressCompatible { get; set; } + [Reactive] public string NormalDNSCompatible { get; set; } + + [Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } + [Reactive] public string DomainDNSAddress2Compatible { get; set; } + [Reactive] public string NormalDNS2Compatible { get; set; } + [Reactive] public string TunDNS2Compatible { get; set; } + [Reactive] public bool RayCustomDNSEnableCompatible { get; set; } + [Reactive] public bool SBCustomDNSEnableCompatible { get; set; } public ReactiveCommand SaveCmd { get; } - public ReactiveCommand ImportDefConfig4V2rayCmd { get; } - public ReactiveCommand ImportDefConfig4SingboxCmd { get; } + public ReactiveCommand ImportDefConfig4V2rayCompatibleCmd { get; } + public ReactiveCommand ImportDefConfig4SingboxCompatibleCmd { get; } public DNSSettingViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; - SaveCmd = ReactiveCommand.CreateFromTask(async () => - { - await SaveSettingAsync(); - }); + SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync); - ImportDefConfig4V2rayCmd = ReactiveCommand.CreateFromTask(async () => + ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () => { - NormalDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); + NormalDNSCompatible = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); await Task.CompletedTask; }); - ImportDefConfig4SingboxCmd = ReactiveCommand.CreateFromTask(async () => + ImportDefConfig4SingboxCompatibleCmd = ReactiveCommand.CreateFromTask(async () => { - NormalDNS2 = EmbedUtils.GetEmbedText(Global.DNSSingboxNormalFileName); - TunDNS2 = EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName); + NormalDNS2Compatible = EmbedUtils.GetEmbedText(Global.DNSSingboxNormalFileName); + TunDNS2Compatible = EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName); await Task.CompletedTask; }); @@ -47,70 +60,107 @@ public class DNSSettingViewModel : MyReactiveObject private async Task Init() { - var item = await AppHandler.Instance.GetDNSItem(ECoreType.Xray); + _config = AppManager.Instance.Config; + var item = _config.SimpleDNSItem; UseSystemHosts = item.UseSystemHosts; - DomainStrategy4Freedom = item?.DomainStrategy4Freedom ?? string.Empty; - DomainDNSAddress = item?.DomainDNSAddress ?? string.Empty; - NormalDNS = item?.NormalDNS ?? string.Empty; + AddCommonHosts = item.AddCommonHosts; + FakeIP = item.FakeIP; + BlockBindingQuery = item.BlockBindingQuery; + DirectDNS = item.DirectDNS; + RemoteDNS = item.RemoteDNS; + RayStrategy4Freedom = item.RayStrategy4Freedom; + SingboxOutboundsResolveDNS = item.SingboxOutboundsResolveDNS; + SingboxFinalResolveDNS = item.SingboxFinalResolveDNS; + SingboxStrategy4Direct = item.SingboxStrategy4Direct; + SingboxStrategy4Proxy = item.SingboxStrategy4Proxy; + Hosts = item.Hosts; + DirectExpectedIPs = item.DirectExpectedIPs; - var item2 = await AppHandler.Instance.GetDNSItem(ECoreType.sing_box); - DomainStrategy4Freedom2 = item2?.DomainStrategy4Freedom ?? string.Empty; - DomainDNSAddress2 = item2?.DomainDNSAddress ?? string.Empty; - NormalDNS2 = item2?.NormalDNS ?? string.Empty; - TunDNS2 = item2?.TunDNS ?? string.Empty; + var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray); + RayCustomDNSEnableCompatible = item1.Enabled; + UseSystemHostsCompatible = item1.UseSystemHosts; + DomainStrategy4FreedomCompatible = item1?.DomainStrategy4Freedom ?? string.Empty; + DomainDNSAddressCompatible = item1?.DomainDNSAddress ?? string.Empty; + NormalDNSCompatible = item1?.NormalDNS ?? string.Empty; + + var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + SBCustomDNSEnableCompatible = item2.Enabled; + DomainStrategy4Freedom2Compatible = item2?.DomainStrategy4Freedom ?? string.Empty; + DomainDNSAddress2Compatible = item2?.DomainDNSAddress ?? string.Empty; + NormalDNS2Compatible = item2?.NormalDNS ?? string.Empty; + TunDNS2Compatible = item2?.TunDNS ?? string.Empty; } private async Task SaveSettingAsync() { - if (NormalDNS.IsNotEmpty()) + _config.SimpleDNSItem.UseSystemHosts = UseSystemHosts; + _config.SimpleDNSItem.AddCommonHosts = AddCommonHosts; + _config.SimpleDNSItem.FakeIP = FakeIP; + _config.SimpleDNSItem.BlockBindingQuery = BlockBindingQuery; + _config.SimpleDNSItem.DirectDNS = DirectDNS; + _config.SimpleDNSItem.RemoteDNS = RemoteDNS; + _config.SimpleDNSItem.RayStrategy4Freedom = RayStrategy4Freedom; + _config.SimpleDNSItem.SingboxOutboundsResolveDNS = SingboxOutboundsResolveDNS; + _config.SimpleDNSItem.SingboxFinalResolveDNS = SingboxFinalResolveDNS; + _config.SimpleDNSItem.SingboxStrategy4Direct = SingboxStrategy4Direct; + _config.SimpleDNSItem.SingboxStrategy4Proxy = SingboxStrategy4Proxy; + _config.SimpleDNSItem.Hosts = Hosts; + _config.SimpleDNSItem.DirectExpectedIPs = DirectExpectedIPs; + + if (NormalDNSCompatible.IsNotEmpty()) { - var obj = JsonUtils.ParseJson(NormalDNS); + var obj = JsonUtils.ParseJson(NormalDNSCompatible); if (obj != null && obj["servers"] != null) { } else { - if (NormalDNS.Contains('{') || NormalDNS.Contains('}')) + if (NormalDNSCompatible.Contains('{') || NormalDNSCompatible.Contains('}')) { - NoticeHandler.Instance.Enqueue(ResUI.FillCorrectDNSText); + NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); return; } } } - if (NormalDNS2.IsNotEmpty()) + if (NormalDNS2Compatible.IsNotEmpty()) { - var obj2 = JsonUtils.Deserialize(NormalDNS2); + var obj2 = JsonUtils.Deserialize(NormalDNS2Compatible); if (obj2 == null) { - NoticeHandler.Instance.Enqueue(ResUI.FillCorrectDNSText); + NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); return; } } - if (TunDNS2.IsNotEmpty()) + if (TunDNS2Compatible.IsNotEmpty()) { - var obj2 = JsonUtils.Deserialize(TunDNS2); + var obj2 = JsonUtils.Deserialize(TunDNS2Compatible); if (obj2 == null) { - NoticeHandler.Instance.Enqueue(ResUI.FillCorrectDNSText); + NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); return; } } - var item = await AppHandler.Instance.GetDNSItem(ECoreType.Xray); - item.DomainStrategy4Freedom = DomainStrategy4Freedom; - item.DomainDNSAddress = DomainDNSAddress; - item.UseSystemHosts = UseSystemHosts; - item.NormalDNS = NormalDNS; - await ConfigHandler.SaveDNSItems(_config, item); + var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray); + item1.Enabled = RayCustomDNSEnableCompatible; + item1.DomainStrategy4Freedom = DomainStrategy4FreedomCompatible; + item1.DomainDNSAddress = DomainDNSAddressCompatible; + item1.UseSystemHosts = UseSystemHostsCompatible; + item1.NormalDNS = NormalDNSCompatible; + await ConfigHandler.SaveDNSItems(_config, item1); - var item2 = await AppHandler.Instance.GetDNSItem(ECoreType.sing_box); - item2.DomainStrategy4Freedom = DomainStrategy4Freedom2; - item2.DomainDNSAddress = DomainDNSAddress2; - item2.NormalDNS = JsonUtils.Serialize(JsonUtils.ParseJson(NormalDNS2)); - item2.TunDNS = JsonUtils.Serialize(JsonUtils.ParseJson(TunDNS2)); + var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + item2.Enabled = SBCustomDNSEnableCompatible; + item2.DomainStrategy4Freedom = DomainStrategy4Freedom2Compatible; + item2.DomainDNSAddress = DomainDNSAddress2Compatible; + item2.NormalDNS = JsonUtils.Serialize(JsonUtils.ParseJson(NormalDNS2Compatible)); + item2.TunDNS = JsonUtils.Serialize(JsonUtils.ParseJson(TunDNS2Compatible)); await ConfigHandler.SaveDNSItems(_config, item2); - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); - _ = _updateView?.Invoke(EViewAction.CloseWindow, null); + await ConfigHandler.SaveConfig(_config); + if (_updateView != null) + { + await _updateView(EViewAction.CloseWindow, null); + } } } diff --git a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs new file mode 100644 index 00000000..3619ddef --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs @@ -0,0 +1,113 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class FullConfigTemplateViewModel : MyReactiveObject +{ + #region Reactive + + [Reactive] + public bool EnableFullConfigTemplate4Ray { get; set; } + + [Reactive] + public bool EnableFullConfigTemplate4Singbox { get; set; } + + [Reactive] + public string FullConfigTemplate4Ray { get; set; } + + [Reactive] + public string FullConfigTemplate4Singbox { get; set; } + + [Reactive] + public string FullTunConfigTemplate4Singbox { get; set; } + + [Reactive] + public bool AddProxyOnly4Ray { get; set; } + + [Reactive] + public bool AddProxyOnly4Singbox { get; set; } + + [Reactive] + public string ProxyDetour4Ray { get; set; } + + [Reactive] + public string ProxyDetour4Singbox { get; set; } + + public ReactiveCommand SaveCmd { get; } + + #endregion Reactive + + public FullConfigTemplateViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveSettingAsync(); + }); + + _ = Init(); + } + + private async Task Init() + { + var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); + EnableFullConfigTemplate4Ray = item?.Enabled ?? false; + FullConfigTemplate4Ray = item?.Config ?? string.Empty; + AddProxyOnly4Ray = item?.AddProxyOnly ?? false; + ProxyDetour4Ray = item?.ProxyDetour ?? string.Empty; + + var item2 = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); + EnableFullConfigTemplate4Singbox = item2?.Enabled ?? false; + FullConfigTemplate4Singbox = item2?.Config ?? string.Empty; + FullTunConfigTemplate4Singbox = item2?.TunConfig ?? string.Empty; + AddProxyOnly4Singbox = item2?.AddProxyOnly ?? false; + ProxyDetour4Singbox = item2?.ProxyDetour ?? string.Empty; + } + + private async Task SaveSettingAsync() + { + if (!await SaveXrayConfigAsync()) + return; + + if (!await SaveSingboxConfigAsync()) + return; + + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _ = _updateView?.Invoke(EViewAction.CloseWindow, null); + } + + private async Task SaveXrayConfigAsync() + { + var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); + item.Enabled = EnableFullConfigTemplate4Ray; + item.Config = null; + + item.Config = FullConfigTemplate4Ray; + + item.AddProxyOnly = AddProxyOnly4Ray; + item.ProxyDetour = ProxyDetour4Ray; + + await ConfigHandler.SaveFullConfigTemplate(_config, item); + return true; + } + + private async Task SaveSingboxConfigAsync() + { + var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); + item.Enabled = EnableFullConfigTemplate4Singbox; + item.Config = null; + item.TunConfig = null; + + item.Config = FullConfigTemplate4Singbox; + item.TunConfig = FullTunConfigTemplate4Singbox; + + item.AddProxyOnly = AddProxyOnly4Singbox; + item.ProxyDetour = ProxyDetour4Singbox; + + await ConfigHandler.SaveFullConfigTemplate(_config, item); + return true; + } +} diff --git a/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs index ba3293c9..0acb8726 100644 --- a/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs @@ -11,7 +11,7 @@ public class GlobalHotkeySettingViewModel : MyReactiveObject public GlobalHotkeySettingViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; _globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys); @@ -58,7 +58,7 @@ public class GlobalHotkeySettingViewModel : MyReactiveObject } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } } diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 7bae19be..4e9fc9fc 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -1,4 +1,5 @@ using System.Reactive; +using System.Reactive.Concurrency; using ReactiveUI; using ReactiveUI.Fody.Helpers; using Splat; @@ -20,6 +21,7 @@ public class MainWindowViewModel : MyReactiveObject public ReactiveCommand AddHysteria2ServerCmd { get; } public ReactiveCommand AddTuicServerCmd { get; } public ReactiveCommand AddWireguardServerCmd { get; } + public ReactiveCommand AddAnytlsServerCmd { get; } public ReactiveCommand AddCustomServerCmd { get; } public ReactiveCommand AddServerViaClipboardCmd { get; } public ReactiveCommand AddServerViaScanCmd { get; } @@ -38,6 +40,7 @@ public class MainWindowViewModel : MyReactiveObject public ReactiveCommand RoutingSettingCmd { get; } public ReactiveCommand DNSSettingCmd { get; } + public ReactiveCommand FullConfigTemplateCmd { get; } public ReactiveCommand GlobalHotkeySettingCmd { get; } public ReactiveCommand RebootAsAdminCmd { get; } public ReactiveCommand ClearServerStatisticsCmd { get; } @@ -69,7 +72,7 @@ public class MainWindowViewModel : MyReactiveObject public MainWindowViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; #region WhenAnyValue && ReactiveCommand @@ -111,6 +114,10 @@ public class MainWindowViewModel : MyReactiveObject { await AddServerAsync(true, EConfigType.WireGuard); }); + AddAnytlsServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.Anytls); + }); AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () => { await AddServerAsync(true, EConfigType.Custom); @@ -164,11 +171,15 @@ public class MainWindowViewModel : MyReactiveObject { await DNSSettingAsync(); }); + FullConfigTemplateCmd = ReactiveCommand.CreateFromTask(async () => + { + await FullConfigTemplateAsync(); + }); GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () => { if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true) { - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } }); RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () => @@ -215,14 +226,16 @@ public class MainWindowViewModel : MyReactiveObject await ConfigHandler.InitBuiltinRouting(_config); await ConfigHandler.InitBuiltinDNS(_config); - await ProfileExHandler.Instance.Init(); - await CoreHandler.Instance.Init(_config, UpdateHandler); - TaskHandler.Instance.RegUpdateTask(_config, UpdateTaskHandler); + await ConfigHandler.InitBuiltinFullConfigTemplate(_config); + await ProfileExManager.Instance.Init(); + await CoreManager.Instance.Init(_config, UpdateHandler); + TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler); if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) { - await StatisticsHandler.Instance.Init(_config, UpdateStatisticsHandler); + await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler); } + await RefreshServers(); BlReloadEnabled = true; await Reload(); @@ -234,95 +247,40 @@ public class MainWindowViewModel : MyReactiveObject #region Actions - private void UpdateHandler(bool notify, string msg) + private async Task UpdateHandler(bool notify, string msg) { - NoticeHandler.Instance.SendMessage(msg); + NoticeManager.Instance.SendMessage(msg); if (notify) { - NoticeHandler.Instance.Enqueue(msg); + NoticeManager.Instance.Enqueue(msg); } } - private void UpdateTaskHandler(bool success, string msg) + private async Task UpdateTaskHandler(bool success, string msg) { - NoticeHandler.Instance.SendMessageEx(msg); + NoticeManager.Instance.SendMessageEx(msg); if (success) { var indexIdOld = _config.IndexId; - RefreshServers(); + await RefreshServers(); if (indexIdOld != _config.IndexId) { - _ = Reload(); + await Reload(); } if (_config.UiItem.EnableAutoAdjustMainLvColWidth) { - _updateView?.Invoke(EViewAction.AdjustMainLvColWidth, null); + AppEvents.AdjustMainLvColWidthRequested.OnNext(Unit.Default); } } } - private void UpdateStatisticsHandler(ServerSpeedItem update) + private async Task UpdateStatisticsHandler(ServerSpeedItem update) { if (!_config.UiItem.ShowInTaskbar) { return; } - _updateView?.Invoke(EViewAction.DispatcherStatistics, update); - } - - public void SetStatisticsResult(ServerSpeedItem update) - { - if (_config.GuiItem.DisplayRealTimeSpeed) - { - Locator.Current.GetService()?.UpdateStatistics(update); - } - if (_config.GuiItem.EnableStatistics && (update.ProxyUp + update.ProxyDown) > 0 && DateTime.Now.Second % 9 == 0) - { - Locator.Current.GetService()?.UpdateStatistics(update); - } - } - - public async Task MyAppExitAsync(bool blWindowsShutDown) - { - try - { - Logging.SaveLog("MyAppExitAsync Begin"); - - await SysProxyHandler.UpdateSysProxy(_config, true); - MessageBus.Current.SendMessage("", EMsgCommand.AppExit.ToString()); - - await ConfigHandler.SaveConfig(_config); - await ProfileExHandler.Instance.SaveTo(); - await StatisticsHandler.Instance.SaveTo(); - await CoreHandler.Instance.CoreStop(); - StatisticsHandler.Instance.Close(); - - Logging.SaveLog("MyAppExitAsync End"); - } - catch { } - finally - { - if (!blWindowsShutDown) - { - _updateView?.Invoke(EViewAction.Shutdown, false); - } - } - } - - public async Task UpgradeApp(string arg) - { - if (!Utils.UpgradeAppExists(out var upgradeFileName)) - { - NoticeHandler.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip); - Logging.SaveLog("UpgradeApp does not exist"); - return; - } - - var id = ProcUtils.ProcessStart(upgradeFileName, arg, Utils.StartupPath()); - if (id > 0) - { - await MyAppExitAsync(false); - } + AppEvents.DispatcherStatisticsRequested.OnNext(update); } public void ShowHideWindow(bool? blShow) @@ -330,18 +288,15 @@ public class MainWindowViewModel : MyReactiveObject _updateView?.Invoke(EViewAction.ShowHideWindow, blShow); } - public void Shutdown(bool byUser) - { - _updateView?.Invoke(EViewAction.Shutdown, byUser); - } - #endregion Actions #region Servers && Groups - private void RefreshServers() + private async Task RefreshServers() { - MessageBus.Current.SendMessage("", EMsgCommand.RefreshProfiles.ToString()); + AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default); + + await Task.Delay(200); } private void RefreshSubscriptions() @@ -373,7 +328,7 @@ public class MainWindowViewModel : MyReactiveObject } if (ret == true) { - RefreshServers(); + await RefreshServers(); if (item.IndexId == _config.IndexId) { await Reload(); @@ -388,16 +343,16 @@ public class MainWindowViewModel : MyReactiveObject await _updateView?.Invoke(EViewAction.AddServerViaClipboard, null); return; } - int ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false); + var ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false); if (ret > 0) { RefreshSubscriptions(); - RefreshServers(); - NoticeHandler.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret)); + await RefreshServers(); + NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret)); } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } @@ -409,7 +364,7 @@ public class MainWindowViewModel : MyReactiveObject public async Task ScanScreenResult(byte[]? bytes) { - var result = QRCodeHelper.ParseBarcode(bytes); + var result = QRCodeUtils.ParseBarcode(bytes); await AddScanResultAsync(result); } @@ -426,7 +381,7 @@ public class MainWindowViewModel : MyReactiveObject return; } - var result = QRCodeHelper.ParseBarcode(fileName); + var result = QRCodeUtils.ParseBarcode(fileName); await AddScanResultAsync(result); } @@ -434,20 +389,20 @@ public class MainWindowViewModel : MyReactiveObject { if (result.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.NoValidQRcodeFound); + NoticeManager.Instance.Enqueue(ResUI.NoValidQRcodeFound); } else { - int ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false); + var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false); if (ret > 0) { RefreshSubscriptions(); - RefreshServers(); - NoticeHandler.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan); + await RefreshServers(); + NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan); } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } } @@ -466,7 +421,7 @@ public class MainWindowViewModel : MyReactiveObject public async Task UpdateSubscriptionProcess(string subId, bool blProxy) { - await (new UpdateService()).UpdateSubscriptionProcess(_config, subId, blProxy, UpdateTaskHandler); + await Task.Run(async () => await SubscriptionHandler.UpdateProcess(_config, subId, blProxy, UpdateTaskHandler)); } #endregion Subscription @@ -503,16 +458,25 @@ public class MainWindowViewModel : MyReactiveObject } } + private async Task FullConfigTemplateAsync() + { + var ret = await _updateView?.Invoke(EViewAction.FullConfigTemplateWindow, null); + if (ret == true) + { + await Reload(); + } + } + public async Task RebootAsAdmin() { ProcUtils.RebootAsAdmin(); - await MyAppExitAsync(false); + await AppManager.Instance.AppExitAsync(true); } private async Task ClearServerStatistics() { - await StatisticsHandler.Instance.ClearAllServerStatistics(); - RefreshServers(); + await StatisticsManager.Instance.ClearAllServerStatistics(); + await RefreshServers(); } private async Task OpenTheFileLocation() @@ -524,7 +488,7 @@ public class MainWindowViewModel : MyReactiveObject } else if (Utils.IsLinux()) { - ProcUtils.ProcessStart("nautilus", path); + ProcUtils.ProcessStart("xdg-open", path); } else if (Utils.IsOSX()) { @@ -556,7 +520,7 @@ public class MainWindowViewModel : MyReactiveObject }); Locator.Current.GetService()?.TestServerAvailability(); - _updateView?.Invoke(EViewAction.DispatcherReload, null); + RxApp.MainThreadScheduler.Schedule(() => _ = ReloadResult()); BlReloadEnabled = true; if (_hasNextReloadJob) @@ -566,7 +530,7 @@ public class MainWindowViewModel : MyReactiveObject } } - public void ReloadResult() + public async Task ReloadResult() { // BlReloadEnabled = true; //Locator.Current.GetService()?.ChangeSystemProxyAsync(_config.systemProxyItem.sysProxyType, false); @@ -576,19 +540,21 @@ public class MainWindowViewModel : MyReactiveObject Locator.Current.GetService()?.ProxiesReload(); } else - { TabMainSelectedIndex = 0; } + { + TabMainSelectedIndex = 0; + } } private async Task LoadCore() { var node = await ConfigHandler.GetDefaultServer(_config); - await CoreHandler.Instance.LoadCore(node); + await CoreManager.Instance.LoadCore(node); } public async Task CloseCore() { await ConfigHandler.SaveConfig(_config); - await CoreHandler.Instance.CoreStop(); + await CoreManager.Instance.CoreStop(); } private async Task AutoHideStartup() @@ -611,7 +577,7 @@ public class MainWindowViewModel : MyReactiveObject Locator.Current.GetService()?.RefreshRoutingsMenu(); await ConfigHandler.SaveConfig(_config); - await new UpdateService().UpdateGeoFileAll(_config, UpdateHandler); + await new UpdateService().UpdateGeoFileAll(_config, UpdateTaskHandler); await Reload(); } diff --git a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs index 80c19ebd..8fc62dfe 100644 --- a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Reactive.Linq; using System.Text.RegularExpressions; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -7,8 +8,8 @@ namespace ServiceLib.ViewModels; public class MsgViewModel : MyReactiveObject { - private ConcurrentQueue _queueMsg = new(); - private int _numMaxMsg = 500; + private readonly ConcurrentQueue _queueMsg = new(); + private readonly int _numMaxMsg = 500; private bool _lastMsgFilterNotAvailable; private bool _blLockShow = false; @@ -20,7 +21,7 @@ public class MsgViewModel : MyReactiveObject public MsgViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty; AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true; @@ -34,12 +35,10 @@ public class MsgViewModel : MyReactiveObject y => y == true) .Subscribe(c => { _config.MsgUIItem.AutoRefresh = AutoRefresh; }); - MessageBus.Current.Listen(EMsgCommand.SendMsgView.ToString()).Subscribe(OnNext); - } - - private async void OnNext(string x) - { - await AppendQueueMsg(x); + AppEvents.SendMsgViewRequested + .AsObservable() + //.ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async content => await AppendQueueMsg(content)); } private async Task AppendQueueMsg(string msg) diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index 57d8cac7..7f446cf2 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -84,6 +84,7 @@ public class OptionSettingViewModel : MyReactiveObject #region Tun mode + [Reactive] public bool TunAutoRoute { get; set; } [Reactive] public bool TunStrictRoute { get; set; } [Reactive] public string TunStack { get; set; } [Reactive] public int TunMtu { get; set; } @@ -108,7 +109,7 @@ public class OptionSettingViewModel : MyReactiveObject public OptionSettingViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; SaveCmd = ReactiveCommand.CreateFromTask(async () => @@ -201,6 +202,7 @@ public class OptionSettingViewModel : MyReactiveObject #region Tun mode + TunAutoRoute = _config.TunModeItem.AutoRoute; TunStrictRoute = _config.TunModeItem.StrictRoute; TunStack = _config.TunModeItem.Stack; TunMtu = _config.TunModeItem.Mtu; @@ -274,7 +276,7 @@ public class OptionSettingViewModel : MyReactiveObject if (localPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(localPort.ToString()) || localPort <= 0 || localPort >= Global.MaxPort) { - NoticeHandler.Instance.Enqueue(ResUI.FillLocalListeningPort); + NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort); return; } var needReboot = (EnableStatistics != _config.GuiItem.EnableStatistics @@ -354,6 +356,7 @@ public class OptionSettingViewModel : MyReactiveObject _config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol; //tun mode + _config.TunModeItem.AutoRoute = TunAutoRoute; _config.TunModeItem.StrictRoute = TunStrictRoute; _config.TunModeItem.Stack = TunStack; _config.TunModeItem.Mtu = TunMtu; @@ -366,14 +369,14 @@ public class OptionSettingViewModel : MyReactiveObject if (await ConfigHandler.SaveConfig(_config) == 0) { await AutoStartupHandler.UpdateTask(_config); - AppHandler.Instance.Reset(); + AppManager.Instance.Reset(); - NoticeHandler.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess); _updateView?.Invoke(EViewAction.CloseWindow, null); } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs new file mode 100644 index 00000000..8742755e --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -0,0 +1,352 @@ +using System.Reactive.Linq; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class ProfilesSelectViewModel : MyReactiveObject +{ + #region private prop + + private string _serverFilter = string.Empty; + private Dictionary _dicHeaderSort = new(); + private string _subIndexId = string.Empty; + + // ConfigType filter state: default include-mode with all types selected + private List _filterConfigTypes = new(); + + private bool _filterExclude = false; + + #endregion private prop + + #region ObservableCollection + + public IObservableCollection ProfileItems { get; } = new ObservableCollectionExtended(); + + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); + + [Reactive] + public ProfileItemModel SelectedProfile { get; set; } + + public IList SelectedProfiles { get; set; } + + [Reactive] + public SubItem SelectedSub { get; set; } + + [Reactive] + public string ServerFilter { get; set; } + + // Include/Exclude filter for ConfigType + public List FilterConfigTypes + { + get => _filterConfigTypes; + set => this.RaiseAndSetIfChanged(ref _filterConfigTypes, value); + } + + [Reactive] + public bool FilterExclude + { + get => _filterExclude; + set => this.RaiseAndSetIfChanged(ref _filterExclude, value); + } + + #endregion ObservableCollection + + #region Init + + public ProfilesSelectViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + _subIndexId = _config.SubIndexId ?? string.Empty; + + #region WhenAnyValue && ReactiveCommand + + this.WhenAnyValue( + x => x.SelectedSub, + y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id) + .Subscribe(async c => await SubSelectedChangedAsync(c)); + + this.WhenAnyValue( + x => x.ServerFilter, + y => y != null && _serverFilter != y) + .Subscribe(async c => await ServerFilterChanged(c)); + + // React to ConfigType filter changes + this.WhenAnyValue(x => x.FilterExclude) + .Skip(1) + .Subscribe(async _ => await RefreshServersBiz()); + + this.WhenAnyValue(x => x.FilterConfigTypes) + .Skip(1) + .Subscribe(async _ => await RefreshServersBiz()); + + #endregion WhenAnyValue && ReactiveCommand + + _ = Init(); + } + + private async Task Init() + { + SelectedProfile = new(); + SelectedSub = new(); + + // Default: include mode with all ConfigTypes selected + try + { + FilterExclude = false; + FilterConfigTypes = Enum.GetValues(typeof(EConfigType)).Cast().ToList(); + } + catch + { + FilterConfigTypes = new(); + } + + await RefreshSubscriptions(); + await RefreshServers(); + } + + #endregion Init + + #region Actions + + public bool CanOk() + { + return SelectedProfile != null && !SelectedProfile.IndexId.IsNullOrEmpty(); + } + + public bool SelectFinish() + { + if (!CanOk()) + { + return false; + } + _updateView?.Invoke(EViewAction.CloseWindow, null); + return true; + } + + #endregion Actions + + #region Servers && Groups + + private async Task SubSelectedChangedAsync(bool c) + { + if (!c) + { + return; + } + _subIndexId = SelectedSub?.Id; + + await RefreshServers(); + + await _updateView?.Invoke(EViewAction.ProfilesFocus, null); + } + + private async Task ServerFilterChanged(bool c) + { + if (!c) + { + return; + } + _serverFilter = ServerFilter; + if (_serverFilter.IsNullOrEmpty()) + { + await RefreshServers(); + } + } + + public async Task RefreshServers() + { + await RefreshServersBiz(); + } + + private async Task RefreshServersBiz() + { + var lstModel = await GetProfileItemsEx(_subIndexId, _serverFilter); + + ProfileItems.Clear(); + ProfileItems.AddRange(lstModel); + if (lstModel.Count > 0) + { + var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); + if (selected != null) + { + SelectedProfile = selected; + } + else + { + SelectedProfile = lstModel.First(); + } + } + + await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); + } + + public async Task RefreshSubscriptions() + { + SubItems.Clear(); + + SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers }); + + foreach (var item in await AppManager.Instance.SubItems()) + { + SubItems.Add(item); + } + if (_subIndexId != null && SubItems.FirstOrDefault(t => t.Id == _subIndexId) != null) + { + SelectedSub = SubItems.FirstOrDefault(t => t.Id == _subIndexId); + } + else + { + SelectedSub = SubItems.First(); + } + } + + private async Task?> GetProfileItemsEx(string subid, string filter) + { + var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter); + lstModel = (from t in lstModel + select new ProfileItemModel + { + IndexId = t.IndexId, + ConfigType = t.ConfigType, + Remarks = t.Remarks, + Address = t.Address, + Port = t.Port, + Security = t.Security, + Network = t.Network, + StreamSecurity = t.StreamSecurity, + Subid = t.Subid, + SubRemarks = t.SubRemarks, + IsActive = t.IndexId == _config.IndexId, + }).OrderBy(t => t.Sort).ToList(); + + // Apply ConfigType filter (include or exclude) + if (FilterConfigTypes != null && FilterConfigTypes.Count > 0) + { + if (FilterExclude) + { + lstModel = lstModel.Where(t => !FilterConfigTypes.Contains(t.ConfigType)).ToList(); + } + else + { + lstModel = lstModel.Where(t => FilterConfigTypes.Contains(t.ConfigType)).ToList(); + } + } + + return lstModel; + } + + public async Task GetProfileItem() + { + if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) + { + return null; + } + var indexId = SelectedProfile.IndexId; + var item = await AppManager.Instance.GetProfileItem(indexId); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return null; + } + return item; + } + + public async Task?> GetProfileItems() + { + if (SelectedProfiles == null || SelectedProfiles.Count == 0) + { + return null; + } + var lst = new List(); + foreach (var sp in SelectedProfiles) + { + if (string.IsNullOrEmpty(sp?.IndexId)) + { + continue; + } + var item = await AppManager.Instance.GetProfileItem(sp.IndexId); + if (item != null) + { + lst.Add(item); + } + } + if (lst.Count == 0) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return null; + } + return lst; + } + + public void SortServer(string colName) + { + if (colName.IsNullOrEmpty()) + { + return; + } + + var prop = typeof(ProfileItemModel).GetProperty(colName); + if (prop == null) + { + return; + } + + _dicHeaderSort.TryAdd(colName, true); + var asc = _dicHeaderSort[colName]; + + var comparer = Comparer.Create((a, b) => + { + if (ReferenceEquals(a, b)) + { + return 0; + } + if (a is null) + { + return -1; + } + if (b is null) + { + return 1; + } + if (a.GetType() == b.GetType() && a is IComparable ca) + { + return ca.CompareTo(b); + } + return string.Compare(a.ToString(), b.ToString(), StringComparison.OrdinalIgnoreCase); + }); + + object? KeySelector(ProfileItemModel x) + { + return prop.GetValue(x); + } + + IEnumerable sorted = asc + ? ProfileItems.OrderBy(KeySelector, comparer) + : ProfileItems.OrderByDescending(KeySelector, comparer); + + var list = sorted.ToList(); + ProfileItems.Clear(); + ProfileItems.AddRange(list); + + _dicHeaderSort[colName] = !asc; + + return; + } + + #endregion Servers && Groups + + #region Public API + + // External setter for ConfigType filter + public void SetConfigTypeFilter(IEnumerable types, bool exclude = false) + { + FilterConfigTypes = types?.Distinct().ToList() ?? new List(); + FilterExclude = exclude; + } + + #endregion Public API +} diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index f312b890..14216fee 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -1,4 +1,5 @@ using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Text; using DynamicData; @@ -22,13 +23,9 @@ public class ProfilesViewModel : MyReactiveObject #region ObservableCollection - private IObservableCollection _profileItems = new ObservableCollectionExtended(); - public IObservableCollection ProfileItems => _profileItems; + public IObservableCollection ProfileItems { get; } = new ObservableCollectionExtended(); - private IObservableCollection _subItems = new ObservableCollectionExtended(); - public IObservableCollection SubItems => _subItems; - - private IObservableCollection _servers = new ObservableCollectionExtended(); + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); [Reactive] public ProfileItemModel SelectedProfile { get; set; } @@ -41,15 +38,9 @@ public class ProfilesViewModel : MyReactiveObject [Reactive] public SubItem SelectedMoveToGroup { get; set; } - [Reactive] - public ComboItem SelectedServer { get; set; } - [Reactive] public string ServerFilter { get; set; } - [Reactive] - public bool BlServers { get; set; } - #endregion ObservableCollection #region Menu @@ -100,7 +91,7 @@ public class ProfilesViewModel : MyReactiveObject public ProfilesViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; #region WhenAnyValue && ReactiveCommand @@ -118,15 +109,10 @@ public class ProfilesViewModel : MyReactiveObject y => y != null && !y.Remarks.IsNullOrEmpty()) .Subscribe(async c => await MoveToGroup(c)); - this.WhenAnyValue( - x => x.SelectedServer, - y => y != null && !y.Text.IsNullOrEmpty()) - .Subscribe(async c => await ServerSelectedChanged(c)); - this.WhenAnyValue( x => x.ServerFilter, y => y != null && _serverFilter != y) - .Subscribe(c => ServerFilterChanged(c)); + .Subscribe(async c => await ServerFilterChanged(c)); //servers delete EditServerCmd = ReactiveCommand.CreateFromTask(async () => @@ -247,10 +233,19 @@ public class ProfilesViewModel : MyReactiveObject #endregion WhenAnyValue && ReactiveCommand - if (_updateView != null) - { - MessageBus.Current.Listen(EMsgCommand.RefreshProfiles.ToString()).Subscribe(OnNext); - } + #region AppEvents + + AppEvents.ProfilesRefreshRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await RefreshServersBiz()); + + AppEvents.DispatcherStatisticsRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async result => await UpdateStatistics(result)); + + #endregion AppEvents _ = Init(); } @@ -260,35 +255,29 @@ public class ProfilesViewModel : MyReactiveObject SelectedProfile = new(); SelectedSub = new(); SelectedMoveToGroup = new(); - SelectedServer = new(); await RefreshSubscriptions(); - RefreshServers(); + //await RefreshServers(); } #endregion Init #region Actions - private async void OnNext(string x) - { - await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); - } - private void Reload() { Locator.Current.GetService()?.Reload(); } - public void SetSpeedTestResult(SpeedTestResult result) + public async Task SetSpeedTestResult(SpeedTestResult result) { if (result.IndexId.IsNullOrEmpty()) { - NoticeHandler.Instance.SendMessageEx(result.Delay); - NoticeHandler.Instance.Enqueue(result.Delay); + NoticeManager.Instance.SendMessageEx(result.Delay); + NoticeManager.Instance.Enqueue(result.Delay); return; } - var item = _profileItems.FirstOrDefault(it => it.IndexId == result.IndexId); + var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId); if (item == null) { return; @@ -307,11 +296,18 @@ public class ProfilesViewModel : MyReactiveObject //_profileItems.Replace(item, JsonUtils.DeepCopy(item)); } - public void UpdateStatistics(ServerSpeedItem update) + public async Task UpdateStatistics(ServerSpeedItem update) { + if (!_config.GuiItem.EnableStatistics + || (update.ProxyUp + update.ProxyDown) <= 0 + || DateTime.Now.Second % 3 != 0) + { + return; + } + try { - var item = _profileItems.FirstOrDefault(it => it.IndexId == update.IndexId); + var item = ProfileItems.FirstOrDefault(it => it.IndexId == update.IndexId); if (item != null) { item.TodayDown = Utils.HumanFy(update.TodayDown); @@ -336,11 +332,6 @@ public class ProfilesViewModel : MyReactiveObject } } - public async Task AutofitColumnWidthAsync() - { - await _updateView?.Invoke(EViewAction.AdjustMainLvColWidth, null); - } - #endregion Actions #region Servers && Groups @@ -353,12 +344,12 @@ public class ProfilesViewModel : MyReactiveObject } _config.SubIndexId = SelectedSub?.Id; - RefreshServers(); + await RefreshServers(); await _updateView?.Invoke(EViewAction.ProfilesFocus, null); } - private void ServerFilterChanged(bool c) + private async Task ServerFilterChanged(bool c) { if (!c) { @@ -367,22 +358,24 @@ public class ProfilesViewModel : MyReactiveObject _serverFilter = ServerFilter; if (_serverFilter.IsNullOrEmpty()) { - RefreshServers(); + await RefreshServers(); } } - public void RefreshServers() + public async Task RefreshServers() { - MessageBus.Current.SendMessage("", EMsgCommand.RefreshProfiles.ToString()); + AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default); + + await Task.Delay(200); } - public async Task RefreshServersBiz() + private async Task RefreshServersBiz() { var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter); _lstProfile = JsonUtils.Deserialize>(JsonUtils.Serialize(lstModel)) ?? []; - _profileItems.Clear(); - _profileItems.AddRange(lstModel); + ProfileItems.Clear(); + ProfileItems.AddRange(lstModel); if (lstModel.Count > 0) { var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); @@ -395,36 +388,38 @@ public class ProfilesViewModel : MyReactiveObject SelectedProfile = lstModel.First(); } } + + await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); } public async Task RefreshSubscriptions() { - _subItems.Clear(); + SubItems.Clear(); - _subItems.Add(new SubItem { Remarks = ResUI.AllGroupServers }); + SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers }); - foreach (var item in await AppHandler.Instance.SubItems()) + foreach (var item in await AppManager.Instance.SubItems()) { - _subItems.Add(item); + SubItems.Add(item); } - if (_config.SubIndexId != null && _subItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null) + if (_config.SubIndexId != null && SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null) { - SelectedSub = _subItems.FirstOrDefault(t => t.Id == _config.SubIndexId); + SelectedSub = SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId); } else { - SelectedSub = _subItems.First(); + SelectedSub = SubItems.First(); } } private async Task?> GetProfileItemsEx(string subid, string filter) { - var lstModel = await AppHandler.Instance.ProfileItems(_config.SubIndexId, filter); + var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, filter); await ConfigHandler.SetDefaultServer(_config, lstModel); - 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(); lstModel = (from t in lstModel join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b from t22 in t2b.DefaultIfEmpty() @@ -474,7 +469,7 @@ public class ProfilesViewModel : MyReactiveObject { foreach (var profile in orderProfiles) { - var item = await AppHandler.Instance.GetProfileItem(profile.IndexId); + var item = await AppManager.Instance.GetProfileItem(profile.IndexId); if (item is not null) { lstSelected.Add(item); @@ -495,10 +490,10 @@ public class ProfilesViewModel : MyReactiveObject { return; } - var item = await AppHandler.Instance.GetProfileItem(SelectedProfile.IndexId); + var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); if (item is null) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } eConfigType = item.ConfigType; @@ -514,7 +509,7 @@ public class ProfilesViewModel : MyReactiveObject } if (ret == true) { - RefreshServers(); + await RefreshServers(); if (item.IndexId == _config.IndexId) { Reload(); @@ -536,12 +531,12 @@ public class ProfilesViewModel : MyReactiveObject var exists = lstSelected.Exists(t => t.IndexId == _config.IndexId); await ConfigHandler.RemoveServers(_config, lstSelected); - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); - if (lstSelected.Count == _profileItems.Count) + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + if (lstSelected.Count == ProfileItems.Count) { - _profileItems.Clear(); + ProfileItems.Clear(); } - RefreshServers(); + await RefreshServers(); if (exists) { Reload(); @@ -553,10 +548,10 @@ public class ProfilesViewModel : MyReactiveObject var tuple = await ConfigHandler.DedupServerList(_config, _config.SubIndexId); if (tuple.Item1 > 0 || tuple.Item2 > 0) { - RefreshServers(); + await RefreshServers(); Reload(); } - NoticeHandler.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2)); + NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2)); } private async Task CopyServer() @@ -568,8 +563,8 @@ public class ProfilesViewModel : MyReactiveObject } if (await ConfigHandler.CopyServer(_config, lstSelected) == 0) { - RefreshServers(); - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + await RefreshServers(); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } } @@ -592,39 +587,26 @@ public class ProfilesViewModel : MyReactiveObject { return; } - var item = await AppHandler.Instance.GetProfileItem(indexId); + var item = await AppManager.Instance.GetProfileItem(indexId); if (item is null) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) { - RefreshServers(); + await RefreshServers(); Reload(); } } - private async Task ServerSelectedChanged(bool c) - { - if (!c) - { - return; - } - if (SelectedServer == null || SelectedServer.ID.IsNullOrEmpty()) - { - return; - } - await SetDefaultServer(SelectedServer.ID); - } - public async Task ShareServerAsync() { - var item = await AppHandler.Instance.GetProfileItem(SelectedProfile.IndexId); + var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); if (item is null) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } var url = FmtHandler.GetShareUri(item); @@ -647,12 +629,12 @@ public class ProfilesViewModel : MyReactiveObject var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad); if (ret.Success != true) { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); return; } if (ret?.Data?.ToString() == _config.IndexId) { - RefreshServers(); + await RefreshServers(); Reload(); } else @@ -675,14 +657,14 @@ public class ProfilesViewModel : MyReactiveObject return; } _dicHeaderSort[colName] = !asc; - RefreshServers(); + await RefreshServers(); } public async Task RemoveInvalidServerResult() { var count = await ConfigHandler.RemoveInvalidServerResult(_config, _config.SubIndexId); - RefreshServers(); - NoticeHandler.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count)); + await RefreshServers(); + NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count)); } //move server @@ -700,9 +682,9 @@ public class ProfilesViewModel : MyReactiveObject } await ConfigHandler.MoveToGroup(_config, lstSelected, SelectedMoveToGroup.Id); - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); - RefreshServers(); + await RefreshServers(); SelectedMoveToGroup = null; SelectedMoveToGroup = new(); } @@ -712,7 +694,7 @@ public class ProfilesViewModel : MyReactiveObject var item = _lstProfile.FirstOrDefault(t => t.IndexId == SelectedProfile.IndexId); if (item is null) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } @@ -723,18 +705,18 @@ public class ProfilesViewModel : MyReactiveObject } if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0) { - RefreshServers(); + await RefreshServers(); } } public async Task MoveServerTo(int startIndex, ProfileItemModel targetItem) { - var targetIndex = _profileItems.IndexOf(targetItem); + var targetIndex = ProfileItems.IndexOf(targetItem); if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex) { if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0) { - RefreshServers(); + await RefreshServers(); } } } @@ -743,7 +725,7 @@ public class ProfilesViewModel : MyReactiveObject { if (actionType == ESpeedActionType.Mixedtest) { - SelectedProfiles = _profileItems; + SelectedProfiles = ProfileItems; } var lstSelected = await GetProfileItems(false); if (lstSelected == null) @@ -751,7 +733,14 @@ public class ProfilesViewModel : MyReactiveObject return; } - _speedtestService ??= new SpeedtestService(_config, (SpeedTestResult result) => _updateView?.Invoke(EViewAction.DispatcherSpeedTest, result)); + _speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) => + { + RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) => + { + _ = SetSpeedTestResult(result); + return Disposable.Empty; + }); + }); _speedtestService?.RunLoop(actionType, lstSelected); } @@ -762,10 +751,10 @@ public class ProfilesViewModel : MyReactiveObject private async Task Export2ClientConfigAsync(bool blClipboard) { - var item = await AppHandler.Instance.GetProfileItem(SelectedProfile.IndexId); + var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); if (item is null) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } if (blClipboard) @@ -773,12 +762,12 @@ public class ProfilesViewModel : MyReactiveObject var result = await CoreConfigHandler.GenerateClientConfig(item, null); if (result.Success != true) { - NoticeHandler.Instance.Enqueue(result.Msg); + NoticeManager.Instance.Enqueue(result.Msg); } else { await _updateView?.Invoke(EViewAction.SetClipboardData, result.Data); - NoticeHandler.Instance.SendMessage(ResUI.OperationSuccess); + NoticeManager.Instance.SendMessage(ResUI.OperationSuccess); } } else @@ -796,11 +785,11 @@ public class ProfilesViewModel : MyReactiveObject var result = await CoreConfigHandler.GenerateClientConfig(item, fileName); if (result.Success != true) { - NoticeHandler.Instance.Enqueue(result.Msg); + NoticeManager.Instance.Enqueue(result.Msg); } else { - NoticeHandler.Instance.SendMessageAndEnqueue(string.Format(ResUI.SaveClientConfigurationIn, fileName)); + NoticeManager.Instance.SendMessageAndEnqueue(string.Format(ResUI.SaveClientConfigurationIn, fileName)); } } @@ -833,7 +822,7 @@ public class ProfilesViewModel : MyReactiveObject { await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); } - NoticeHandler.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); + NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); } } @@ -850,7 +839,7 @@ public class ProfilesViewModel : MyReactiveObject } else { - item = await AppHandler.Instance.GetSubItem(_config.SubIndexId); + item = await AppManager.Instance.GetSubItem(_config.SubIndexId); if (item is null) { return; diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs index c4970f84..758aa8fe 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs @@ -28,7 +28,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject public RoutingRuleDetailsViewModel(RulesItem rulesItem, Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; SaveCmd = ReactiveCommand.CreateFromTask(async () => @@ -83,7 +83,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject if (!hasRule) { - NoticeHandler.Instance.Enqueue(string.Format(ResUI.RoutingRuleDetailRequiredTips, "Network/Port/Protocol/Domain/IP/Process")); + NoticeManager.Instance.Enqueue(string.Format(ResUI.RoutingRuleDetailRequiredTips, "Network/Port/Protocol/Domain/IP/Process")); return; } //NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs index 93658a07..4b192f04 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs @@ -14,8 +14,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject [Reactive] public RoutingItem SelectedRouting { get; set; } - private IObservableCollection _rulesItems = new ObservableCollectionExtended(); - public IObservableCollection RulesItems => _rulesItems; + public IObservableCollection RulesItems { get; } = new ObservableCollectionExtended(); [Reactive] public RulesItemModel SelectedSource { get; set; } @@ -37,7 +36,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject public RoutingRuleSettingViewModel(RoutingItem routingItem, Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; var canEditRemove = this.WhenAnyValue( @@ -101,7 +100,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject public void RefreshRulesItems() { - _rulesItems.Clear(); + RulesItems.Clear(); foreach (var item in _rules) { @@ -118,7 +117,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject Enabled = item.Enabled, Remarks = item.Remarks, }; - _rulesItems.Add(it); + RulesItems.Add(it); } } @@ -151,7 +150,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject { if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); return; } if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) @@ -174,7 +173,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject { if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); return; } @@ -205,7 +204,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject { if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); return; } @@ -226,7 +225,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject string remarks = SelectedRouting.Remarks; if (remarks.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseFillRemarks); + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); return; } var item = SelectedRouting; @@ -239,12 +238,12 @@ public class RoutingRuleSettingViewModel : MyReactiveObject if (await ConfigHandler.SaveRoutingItem(_config, item) == 0) { - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); _updateView?.Invoke(EViewAction.CloseWindow, null); } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } @@ -266,7 +265,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject if (ret == 0) { RefreshRulesItems(); - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } } @@ -281,7 +280,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject if (ret == 0) { RefreshRulesItems(); - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } } @@ -290,7 +289,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject var url = SelectedRouting.Url; if (url.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.MsgNeedUrl); + NoticeManager.Instance.Enqueue(ResUI.MsgNeedUrl); return; } @@ -300,7 +299,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject if (ret == 0) { RefreshRulesItems(); - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } } diff --git a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs index 73b18fb7..5237a8d2 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs @@ -9,8 +9,7 @@ public class RoutingSettingViewModel : MyReactiveObject { #region Reactive - private IObservableCollection _routingItems = new ObservableCollectionExtended(); - public IObservableCollection RoutingItems => _routingItems; + public IObservableCollection RoutingItems { get; } = new ObservableCollectionExtended(); [Reactive] public RoutingItemModel SelectedSource { get; set; } @@ -20,9 +19,6 @@ public class RoutingSettingViewModel : MyReactiveObject [Reactive] public string DomainStrategy { get; set; } - [Reactive] - public string DomainMatcher { get; set; } - [Reactive] public string DomainStrategy4Singbox { get; set; } @@ -38,7 +34,7 @@ public class RoutingSettingViewModel : MyReactiveObject public RoutingSettingViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; var canEditRemove = this.WhenAnyValue( @@ -75,7 +71,6 @@ public class RoutingSettingViewModel : MyReactiveObject SelectedSource = new(); DomainStrategy = _config.RoutingBasicItem.DomainStrategy; - DomainMatcher = _config.RoutingBasicItem.DomainMatcher; DomainStrategy4Singbox = _config.RoutingBasicItem.DomainStrategy4Singbox; await ConfigHandler.InitBuiltinRouting(_config); @@ -86,9 +81,9 @@ public class RoutingSettingViewModel : MyReactiveObject public async Task RefreshRoutingItems() { - _routingItems.Clear(); + RoutingItems.Clear(); - var routings = await AppHandler.Instance.RoutingItems(); + var routings = await AppManager.Instance.RoutingItems(); foreach (var item in routings) { var it = new RoutingItemModel() @@ -102,24 +97,23 @@ public class RoutingSettingViewModel : MyReactiveObject CustomRulesetPath4Singbox = item.CustomRulesetPath4Singbox, Sort = item.Sort, }; - _routingItems.Add(it); + RoutingItems.Add(it); } } private async Task SaveRoutingAsync() { _config.RoutingBasicItem.DomainStrategy = DomainStrategy; - _config.RoutingBasicItem.DomainMatcher = DomainMatcher; _config.RoutingBasicItem.DomainStrategy4Singbox = DomainStrategy4Singbox; if (await ConfigHandler.SaveConfig(_config) == 0) { - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); _updateView?.Invoke(EViewAction.CloseWindow, null); } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } @@ -134,7 +128,7 @@ public class RoutingSettingViewModel : MyReactiveObject } else { - item = await AppHandler.Instance.GetRoutingItem(SelectedSource?.Id); + item = await AppManager.Instance.GetRoutingItem(SelectedSource?.Id); if (item is null) { return; @@ -151,7 +145,7 @@ public class RoutingSettingViewModel : MyReactiveObject { if (SelectedSource is null || SelectedSource.Remarks.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); return; } if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) @@ -160,7 +154,7 @@ public class RoutingSettingViewModel : MyReactiveObject } foreach (var it in SelectedSources ?? [SelectedSource]) { - var item = await AppHandler.Instance.GetRoutingItem(it?.Id); + var item = await AppManager.Instance.GetRoutingItem(it?.Id); if (item != null) { await ConfigHandler.RemoveRoutingItem(item); @@ -173,10 +167,10 @@ public class RoutingSettingViewModel : MyReactiveObject public async Task RoutingAdvancedSetDefault() { - var item = await AppHandler.Instance.GetRoutingItem(SelectedSource?.Id); + var item = await AppManager.Instance.GetRoutingItem(SelectedSource?.Id); if (item is null) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); return; } diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs index 5c2acab6..e9ee033e 100644 --- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -1,4 +1,6 @@ using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Text; using DynamicData.Binding; using ReactiveUI; @@ -11,11 +13,9 @@ public class StatusBarViewModel : MyReactiveObject { #region ObservableCollection - private IObservableCollection _routingItems = new ObservableCollectionExtended(); - public IObservableCollection RoutingItems => _routingItems; + public IObservableCollection RoutingItems { get; } = new ObservableCollectionExtended(); - private IObservableCollection _servers = new ObservableCollectionExtended(); - public IObservableCollection Servers => _servers; + public IObservableCollection Servers { get; } = new ObservableCollectionExtended(); [Reactive] public RoutingItem SelectedRouting { get; set; } @@ -100,7 +100,7 @@ public class StatusBarViewModel : MyReactiveObject public StatusBarViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; SelectedRouting = new(); SelectedServer = new(); RunningServerToolTipText = "-"; @@ -197,10 +197,20 @@ public class StatusBarViewModel : MyReactiveObject #endregion WhenAnyValue && ReactiveCommand + #region AppEvents + if (updateView != null) { InitUpdateView(updateView); } + + AppEvents.DispatcherStatisticsRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async result => await UpdateStatistics(result)); + + #endregion AppEvents + _ = Init(); } @@ -216,19 +226,17 @@ public class StatusBarViewModel : MyReactiveObject _updateView = updateView; if (_updateView != null) { - MessageBus.Current.Listen(EMsgCommand.RefreshProfiles.ToString()).Subscribe(OnNext); + AppEvents.ProfilesRefreshRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await RefreshServersBiz()); //.DisposeWith(_disposables); } } - private async void OnNext(string x) - { - await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); - } - private async Task CopyProxyCmdToClipboard() { var cmd = Utils.IsWindows() ? "set" : "export"; - var address = $"{Global.Loopback}:{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks)}"; + var address = $"{Global.Loopback}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"; var sb = new StringBuilder(); sb.AppendLine($"{cmd} http_proxy={Global.HttpProtocol}{address}"); @@ -263,7 +271,7 @@ public class StatusBarViewModel : MyReactiveObject await service.UpdateSubscriptionProcess("", blProxy); } - public async Task RefreshServersBiz() + private async Task RefreshServersBiz() { await RefreshServersMenu(); @@ -283,9 +291,9 @@ public class StatusBarViewModel : MyReactiveObject private async Task RefreshServersMenu() { - var lstModel = await AppHandler.Instance.ProfileItems(_config.SubIndexId, ""); + var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, ""); - _servers.Clear(); + Servers.Clear(); if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit) { BlServers = false; @@ -299,7 +307,7 @@ public class StatusBarViewModel : MyReactiveObject string name = it.GetSummary(); var item = new ComboItem() { ID = it.IndexId, Text = name }; - _servers.Add(item); + Servers.Add(item); if (_config.IndexId == it.IndexId) { SelectedServer = item; @@ -332,18 +340,24 @@ public class StatusBarViewModel : MyReactiveObject return; } - _updateView?.Invoke(EViewAction.DispatcherServerAvailability, ResUI.Speedtesting); + await TestServerAvailabilitySub(ResUI.Speedtesting); - var msg = await Task.Run(async () => - { - return await ConnectionHandler.Instance.RunAvailabilityCheck(); - }); + var msg = await Task.Run(ConnectionHandler.RunAvailabilityCheck); - NoticeHandler.Instance.SendMessageEx(msg); - _updateView?.Invoke(EViewAction.DispatcherServerAvailability, msg); + NoticeManager.Instance.SendMessageEx(msg); + await TestServerAvailabilitySub(msg); } - public void TestServerAvailabilityResult(string msg) + private async Task TestServerAvailabilitySub(string msg) + { + RxApp.MainThreadScheduler.Schedule(msg, (scheduler, msg) => + { + _ = TestServerAvailabilityResult(msg); + return Disposable.Empty; + }); + } + + public async Task TestServerAvailabilityResult(string msg) { RunningInfoDisplay = msg; } @@ -358,7 +372,7 @@ public class StatusBarViewModel : MyReactiveObject } _config.SystemProxyItem.SysProxyType = type; await ChangeSystemProxyAsync(type, true); - NoticeHandler.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType.ToString()}"); + NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType.ToString()}"); SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType; await ConfigHandler.SaveConfig(_config); @@ -381,13 +395,13 @@ public class StatusBarViewModel : MyReactiveObject public async Task RefreshRoutingsMenu() { - _routingItems.Clear(); + RoutingItems.Clear(); BlRouting = true; - var routings = await AppHandler.Instance.RoutingItems(); + var routings = await AppManager.Instance.RoutingItems(); foreach (var item in routings) { - _routingItems.Add(item); + RoutingItems.Add(item); if (item.IsActive) { SelectedRouting = item; @@ -407,7 +421,7 @@ public class StatusBarViewModel : MyReactiveObject return; } - var item = await AppHandler.Instance.GetRoutingItem(SelectedRouting?.Id); + var item = await AppManager.Instance.GetRoutingItem(SelectedRouting?.Id); if (item is null) { return; @@ -415,7 +429,7 @@ public class StatusBarViewModel : MyReactiveObject if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) { - NoticeHandler.Instance.SendMessageEx(ResUI.TipChangeRouting); + NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting); Locator.Current.GetService()?.Reload(); _updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); } @@ -474,11 +488,11 @@ public class StatusBarViewModel : MyReactiveObject } else if (Utils.IsLinux()) { - return AppHandler.Instance.LinuxSudoPwd.IsNotEmpty(); + return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); } else if (Utils.IsOSX()) { - return AppHandler.Instance.LinuxSudoPwd.IsNotEmpty(); + return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); } return false; } @@ -490,10 +504,10 @@ public class StatusBarViewModel : MyReactiveObject public async Task InboundDisplayStatus() { StringBuilder sb = new(); - sb.Append($"[{EInboundProtocol.mixed}:{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks)}"); + sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"); if (_config.Inbound.First().SecondLocalPortEnabled) { - sb.Append($",{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks2)}"); + sb.Append($",{AppManager.Instance.GetLocalPort(EInboundProtocol.socks2)}"); } sb.Append(']'); InboundDisplay = $"{ResUI.LabLocal}:{sb}"; @@ -501,8 +515,8 @@ public class StatusBarViewModel : MyReactiveObject if (_config.Inbound.First().AllowLANConn) { var lan = _config.Inbound.First().NewPort4LAN - ? $"[{EInboundProtocol.mixed}:{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks3)}]" - : $"[{EInboundProtocol.mixed}:{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks)}]"; + ? $"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks3)}]" + : $"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}]"; InboundLanDisplay = $"{ResUI.LabLAN}:{lan}"; } else @@ -512,8 +526,13 @@ public class StatusBarViewModel : MyReactiveObject await Task.CompletedTask; } - public void UpdateStatistics(ServerSpeedItem update) + public async Task UpdateStatistics(ServerSpeedItem update) { + if (!_config.GuiItem.DisplayRealTimeSpeed) + { + return; + } + try { if (_config.IsRunningCore(ECoreType.sing_box)) diff --git a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs index e98a8a34..bfbfbbe7 100644 --- a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs @@ -13,7 +13,7 @@ public class SubEditViewModel : MyReactiveObject public SubEditViewModel(SubItem subItem, Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; SaveCmd = ReactiveCommand.CreateFromTask(async () => @@ -29,7 +29,7 @@ public class SubEditViewModel : MyReactiveObject var remarks = SelectedSource.Remarks; if (remarks.IsNullOrEmpty()) { - NoticeHandler.Instance.Enqueue(ResUI.PleaseFillRemarks); + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); return; } @@ -39,25 +39,25 @@ public class SubEditViewModel : MyReactiveObject var uri = Utils.TryUri(url); if (uri == null) { - NoticeHandler.Instance.Enqueue(ResUI.InvalidUrlTip); + NoticeManager.Instance.Enqueue(ResUI.InvalidUrlTip); return; } //Do not allow http protocol if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost)) { - NoticeHandler.Instance.Enqueue(ResUI.InsecureUrlProtocol); + NoticeManager.Instance.Enqueue(ResUI.InsecureUrlProtocol); //return; } } if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0) { - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); _updateView?.Invoke(EViewAction.CloseWindow, null); } else { - NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } } diff --git a/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs index 71cf1c61..88f33619 100644 --- a/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs @@ -8,8 +8,7 @@ namespace ServiceLib.ViewModels; public class SubSettingViewModel : MyReactiveObject { - private IObservableCollection _subItems = new ObservableCollectionExtended(); - public IObservableCollection SubItems => _subItems; + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); [Reactive] public SubItem SelectedSource { get; set; } @@ -24,7 +23,7 @@ public class SubSettingViewModel : MyReactiveObject public SubSettingViewModel(Func>? updateView) { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _updateView = updateView; var canEditRemove = this.WhenAnyValue( @@ -60,8 +59,8 @@ public class SubSettingViewModel : MyReactiveObject public async Task RefreshSubItems() { - _subItems.Clear(); - _subItems.AddRange(await AppHandler.Instance.SubItems()); + SubItems.Clear(); + SubItems.AddRange(await AppManager.Instance.SubItems()); } public async Task EditSubAsync(bool blNew) @@ -73,7 +72,7 @@ public class SubSettingViewModel : MyReactiveObject } else { - item = await AppHandler.Instance.GetSubItem(SelectedSource?.Id); + item = await AppManager.Instance.GetSubItem(SelectedSource?.Id); if (item is null) { return; @@ -98,7 +97,7 @@ public class SubSettingViewModel : MyReactiveObject await ConfigHandler.DeleteSubItem(_config, it.Id); } await RefreshSubItems(); - NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); IsModified = true; } } diff --git a/v2rayN/v2rayN.Desktop/App.axaml.cs b/v2rayN/v2rayN.Desktop/App.axaml.cs index ebd5c29a..5303eb6c 100644 --- a/v2rayN/v2rayN.Desktop/App.axaml.cs +++ b/v2rayN/v2rayN.Desktop/App.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using ServiceLib.Manager; using Splat; using v2rayN.Desktop.Common; using v2rayN.Desktop.Views; @@ -25,7 +26,7 @@ public partial class App : Application { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - AppHandler.Instance.InitComponents(); + AppManager.Instance.InitComponents(); desktop.Exit += OnExit; desktop.MainWindow = new MainWindow(); @@ -73,11 +74,7 @@ public partial class App : Application private async void MenuExit_Click(object? sender, EventArgs e) { - var service = Locator.Current.GetService(); - if (service != null) - { - await service.MyAppExitAsync(true); - } - service?.Shutdown(true); + await AppManager.Instance.AppExitAsync(false); + AppManager.Instance.Shutdown(true); } } diff --git a/v2rayN/v2rayN.Desktop/Assets/GlobalResources.axaml b/v2rayN/v2rayN.Desktop/Assets/GlobalResources.axaml index cdfff108..f58db043 100644 --- a/v2rayN/v2rayN.Desktop/Assets/GlobalResources.axaml +++ b/v2rayN/v2rayN.Desktop/Assets/GlobalResources.axaml @@ -10,6 +10,7 @@ 32 32 + 1000 2 4,0 diff --git a/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml b/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml index 40337f61..6a2cfacd 100644 --- a/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml +++ b/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml @@ -22,4 +22,8 @@ + + diff --git a/v2rayN/v2rayN.Desktop/Base/WindowBase.cs b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs index a84fc86c..c2c9a0bc 100644 --- a/v2rayN/v2rayN.Desktop/Base/WindowBase.cs +++ b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Interactivity; using Avalonia.ReactiveUI; +using ServiceLib.Manager; namespace v2rayN.Desktop.Base; @@ -20,7 +21,7 @@ public class WindowBase : ReactiveWindow where TViewMode { try { - var sizeItem = ConfigHandler.GetWindowSizeItem(AppHandler.Instance.Config, GetType().Name); + var sizeItem = ConfigHandler.GetWindowSizeItem(AppManager.Instance.Config, GetType().Name); if (sizeItem == null) { return; @@ -45,7 +46,7 @@ public class WindowBase : ReactiveWindow where TViewMode base.OnClosed(e); try { - ConfigHandler.SaveWindowSizeItem(AppHandler.Instance.Config, GetType().Name, Width, Height); + ConfigHandler.SaveWindowSizeItem(AppManager.Instance.Config, GetType().Name, Width, Height); } catch { } } diff --git a/v2rayN/v2rayN.Desktop/Handler/HotkeyHandler.cs b/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs similarity index 91% rename from v2rayN/v2rayN.Desktop/Handler/HotkeyHandler.cs rename to v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs index a8d257c8..5ce6ff60 100644 --- a/v2rayN/v2rayN.Desktop/Handler/HotkeyHandler.cs +++ b/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs @@ -4,12 +4,12 @@ using Avalonia.ReactiveUI; using Avalonia.Win32.Input; using GlobalHotKeys; -namespace v2rayN.Desktop.Handler; +namespace v2rayN.Desktop.Manager; -public sealed class HotkeyHandler +public sealed class HotkeyManager { - private static readonly Lazy _instance = new(() => new()); - public static HotkeyHandler Instance = _instance.Value; + private static readonly Lazy _instance = new(() => new()); + public static HotkeyManager Instance = _instance.Value; private readonly Dictionary _hotkeyTriggerDic = new(); private HotKeyManager? _hotKeyManager; diff --git a/v2rayN/v2rayN.Desktop/Program.cs b/v2rayN/v2rayN.Desktop/Program.cs index 52fdbf92..58ddb9f9 100644 --- a/v2rayN/v2rayN.Desktop/Program.cs +++ b/v2rayN/v2rayN.Desktop/Program.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.ReactiveUI; +using ServiceLib.Manager; using v2rayN.Desktop.Common; namespace v2rayN.Desktop; @@ -46,7 +47,7 @@ internal class Program } } - if (!AppHandler.Instance.InitApp()) + if (!AppManager.Instance.InitApp()) { return false; } @@ -62,6 +63,6 @@ internal class Program .WithFontByDefault() .LogToTrace() .UseReactiveUI() - .With(new MacOSPlatformOptions { ShowInDock = AppHandler.Instance.Config.UiItem.MacOSShowInDock }); + .With(new MacOSPlatformOptions { ShowInDock = AppManager.Instance.Config.UiItem.MacOSShowInDock }); } } diff --git a/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs index ea1a3b66..21899bfc 100644 --- a/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs +++ b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs @@ -8,6 +8,7 @@ using Avalonia.Styling; using ReactiveUI; using ReactiveUI.Fody.Helpers; using Semi.Avalonia; +using ServiceLib.Manager; namespace v2rayN.Desktop.ViewModels; @@ -21,7 +22,7 @@ public class ThemeSettingViewModel : MyReactiveObject public ThemeSettingViewModel() { - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; BindingUI(); RestoreUI(); @@ -74,7 +75,7 @@ public class ThemeSettingViewModel : MyReactiveObject _config.UiItem.CurrentLanguage = CurrentLanguage; Thread.CurrentThread.CurrentUICulture = new(CurrentLanguage); ConfigHandler.SaveConfig(_config); - NoticeHandler.Instance.Enqueue(ResUI.NeedRebootTips); + NoticeManager.Instance.Enqueue(ResUI.NeedRebootTips); } }); } @@ -107,6 +108,7 @@ public class ThemeSettingViewModel : MyReactiveObject x.OfType + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs new file mode 100644 index 00000000..b3319154 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs @@ -0,0 +1,195 @@ +using System.Reactive.Disposables; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.ReactiveUI; +using Avalonia.VisualTree; +using ReactiveUI; +using ServiceLib.Manager; + +namespace v2rayN.Desktop.Views; + +public partial class ProfilesSelectWindow : ReactiveWindow +{ + private static Config _config; + + public Task ProfileItem => GetProfileItem(); + public Task?> ProfileItems => GetProfileItems(); + private bool _allowMultiSelect = false; + + public ProfilesSelectWindow() + { + InitializeComponent(); + + _config = AppManager.Instance.Config; + + btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; + txtServerFilter.KeyDown += TxtServerFilter_KeyDown; + lstProfiles.KeyDown += LstProfiles_KeyDown; + lstProfiles.SelectionChanged += LstProfiles_SelectionChanged; + lstProfiles.LoadingRow += LstProfiles_LoadingRow; + lstProfiles.Sorting += LstProfiles_Sorting; + lstProfiles.DoubleTapped += LstProfiles_DoubleTapped; + + ViewModel = new ProfilesSelectViewModel(UpdateViewHandler); + DataContext = ViewModel; + + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.ProfileItems, v => v.lstProfiles.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedProfile, v => v.lstProfiles.SelectedItem).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables); + }); + + btnCancel.Click += (s, e) => Close(false); + } + + public void AllowMultiSelect(bool allow) + { + _allowMultiSelect = allow; + if (allow) + { + lstProfiles.SelectionMode = DataGridSelectionMode.Extended; + lstProfiles.SelectedItems.Clear(); + } + else + { + lstProfiles.SelectionMode = DataGridSelectionMode.Single; + if (lstProfiles.SelectedItems.Count > 0) + { + var first = lstProfiles.SelectedItems[0]; + lstProfiles.SelectedItems.Clear(); + lstProfiles.SelectedItem = first; + } + } + } + + // Expose ConfigType filter controls to callers + public void SetConfigTypeFilter(IEnumerable types, bool exclude = false) + => ViewModel?.SetConfigTypeFilter(types, exclude); + + private async Task UpdateViewHandler(EViewAction action, object? obj) + { + switch (action) + { + case EViewAction.CloseWindow: + Close(true); + break; + } + return await Task.FromResult(true); + } + + private void LstProfiles_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (ViewModel != null) + { + ViewModel.SelectedProfiles = lstProfiles.SelectedItems.Cast().ToList(); + } + } + + private void LstProfiles_LoadingRow(object? sender, DataGridRowEventArgs e) + { + e.Row.Header = $" {e.Row.Index + 1}"; + } + + private void LstProfiles_DoubleTapped(object? sender, TappedEventArgs e) + { + // 忽略表头区域的双击 + if (e.Source is Control src) + { + if (src.FindAncestorOfType() != null) + { + e.Handled = true; + return; + } + + // 仅当在数据行或其子元素上双击时才触发选择 + if (src.FindAncestorOfType() != null) + { + ViewModel?.SelectFinish(); + e.Handled = true; + } + } + } + + private void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e) + { + // 自定义排序,防止默认行为导致误触发 + e.Handled = true; + if (ViewModel != null && e.Column?.Tag?.ToString() != null) + { + ViewModel.SortServer(e.Column.Tag.ToString()); + } + } + + private void LstProfiles_KeyDown(object? sender, KeyEventArgs e) + { + if (e.KeyModifiers is KeyModifiers.Control or KeyModifiers.Meta) + { + if (e.Key == Key.A) + { + if (_allowMultiSelect) + { + lstProfiles.SelectAll(); + } + e.Handled = true; + } + } + else + { + if (e.Key is Key.Enter or Key.Return) + { + ViewModel?.SelectFinish(); + e.Handled = true; + } + } + } + + private void BtnAutofitColumnWidth_Click(object? sender, RoutedEventArgs e) + { + AutofitColumnWidth(); + } + + private void AutofitColumnWidth() + { + try + { + foreach (var col in lstProfiles.Columns) + { + col.Width = new DataGridLength(1, DataGridLengthUnitType.Auto); + } + } + catch + { + } + } + + private void TxtServerFilter_KeyDown(object? sender, KeyEventArgs e) + { + if (e.Key is Key.Enter or Key.Return) + { + ViewModel?.RefreshServers(); + } + } + + public async Task GetProfileItem() + { + var item = await ViewModel?.GetProfileItem(); + return item; + } + + public async Task?> GetProfileItems() + { + var item = await ViewModel?.GetProfileItems(); + return item; + } + + private void BtnSave_Click(object sender, RoutedEventArgs e) + { + // Trigger selection finalize when Confirm is clicked + ViewModel?.SelectFinish(); + } +} diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs index f72c1f51..c52cad9d 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs @@ -1,4 +1,5 @@ using System.Reactive.Disposables; +using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -7,6 +8,7 @@ using Avalonia.Threading; using DialogHostAvalonia; using MsBox.Avalonia.Enums; using ReactiveUI; +using ServiceLib.Manager; using Splat; using v2rayN.Desktop.Common; @@ -26,7 +28,7 @@ public partial class ProfilesView : ReactiveUserControl { InitializeComponent(); - _config = AppHandler.Instance.Config; + _config = AppManager.Instance.Config; _window = window; menuSelectAll.Click += menuSelectAll_Click; @@ -95,11 +97,21 @@ public partial class ProfilesView : ReactiveUserControl this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables); + + AppEvents.AppExitRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => StorageUI()) + .DisposeWith(disposables); + + AppEvents.AdjustMainLvColWidthRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => AutofitColumnWidth()) + .DisposeWith(disposables); }); RestoreUI(); - ViewModel?.RefreshServers(); - MessageBus.Current.Listen(EMsgCommand.AppExit.ToString()).Subscribe(StorageUI); } private async void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e) @@ -126,13 +138,6 @@ public partial class ProfilesView : ReactiveUserControl await AvaUtils.SetClipboardData(this, (string)obj); break; - case EViewAction.AdjustMainLvColWidth: - Dispatcher.UIThread.Post(() => - AutofitColumnWidth(), - DispatcherPriority.Default); - - break; - case EViewAction.ProfilesFocus: lstProfiles.Focus(); break; @@ -176,21 +181,8 @@ public partial class ProfilesView : ReactiveUserControl return false; return await new SubEditWindow((SubItem)obj).ShowDialog(_window); - case EViewAction.DispatcherSpeedTest: - if (obj is null) - return false; - Dispatcher.UIThread.Post(() => - ViewModel?.SetSpeedTestResult((SpeedTestResult)obj), - DispatcherPriority.Default); - - break; - case EViewAction.DispatcherRefreshServersBiz: - Dispatcher.UIThread.Post(() => - { - _ = RefreshServersBiz(); - }, - DispatcherPriority.Default); + Dispatcher.UIThread.Post(RefreshServersBiz, DispatcherPriority.Default); break; } @@ -208,13 +200,8 @@ public partial class ProfilesView : ReactiveUserControl await DialogHost.Show(dialog); } - public async Task RefreshServersBiz() + public void RefreshServersBiz() { - if (ViewModel != null) - { - await ViewModel.RefreshServersBiz(); - } - if (lstProfiles.SelectedIndex >= 0) { lstProfiles.ScrollIntoView(lstProfiles.SelectedItem, null); @@ -420,7 +407,7 @@ public partial class ProfilesView : ReactiveUserControl } } - private void StorageUI(string? n = null) + private void StorageUI() { List lvColumnItem = new(); foreach (var item2 in lstProfiles.Columns) diff --git a/v2rayN/v2rayN.Desktop/Views/QrcodeView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/QrcodeView.axaml.cs index c4e3052c..3c498cb6 100644 --- a/v2rayN/v2rayN.Desktop/Views/QrcodeView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/QrcodeView.axaml.cs @@ -23,7 +23,7 @@ public partial class QrcodeView : UserControl private Bitmap? GetQRCode(string? url) { - var bytes = QRCodeHelper.GenQRCode(url); + var bytes = QRCodeUtils.GenQRCode(url); return ByteToBitmap(bytes); } diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml b/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml index 460f0107..872a11f8 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml @@ -54,13 +54,22 @@ Width="300" Margin="{StaticResource Margin4}" Text="{Binding SelectedSource.OutboundTag, Mode=TwoWay}" /> - + VerticalAlignment="Center"> +